mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 10:16:26 +00:00
Compare commits
696 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78c00b1722 | ||
|
|
2ff655d2dc | ||
|
|
e53717cd87 | ||
|
|
8b8d915ab7 | ||
|
|
54de57ee7b | ||
|
|
e0d9cf7f5c | ||
|
|
10ea5d00eb | ||
|
|
de76f55fe2 | ||
|
|
cd301d514c | ||
|
|
0f232b3d86 | ||
|
|
53ff693e95 | ||
|
|
215484c19a | ||
|
|
885f656d34 | ||
|
|
851d3ba159 | ||
|
|
62ab444b29 | ||
|
|
f1b929c13b | ||
|
|
806139091c | ||
|
|
6960c8b2d6 | ||
|
|
b1c6c0442f | ||
|
|
a85a27f225 | ||
|
|
950d31ada8 | ||
|
|
543c31cec6 | ||
|
|
5e87ec2627 | ||
|
|
7165c4550b | ||
|
|
472496d59c | ||
|
|
127da40256 | ||
|
|
a113b99de0 | ||
|
|
74535c9cba | ||
|
|
79254a562f | ||
|
|
4b1469748b | ||
|
|
1683d63f33 | ||
|
|
4cce52f9ce | ||
|
|
33fb03066e | ||
|
|
a1deb15db8 | ||
|
|
96ab8ec958 | ||
|
|
c0f68dce25 | ||
|
|
2b6c38083c | ||
|
|
667ece7d3f | ||
|
|
9270e59508 | ||
|
|
b32865488e | ||
|
|
e43c7e9a6a | ||
|
|
c3a980836a | ||
|
|
304b83be89 | ||
|
|
eea01f10ac | ||
|
|
3d9b85dc6d | ||
|
|
743a9009de | ||
|
|
a0da4f9dd0 | ||
|
|
3fe45e9cbb | ||
|
|
294bf742cd | ||
|
|
ea5970ab1c | ||
|
|
72df418953 | ||
|
|
f566b567be | ||
|
|
8d817066e8 | ||
|
|
99b53f4a55 | ||
|
|
038154c441 | ||
|
|
951a126d63 | ||
|
|
fa77cda0b4 | ||
|
|
a0bfd9e497 | ||
|
|
a8e601e5e0 | ||
|
|
47d7cef214 | ||
|
|
e298739cb9 | ||
|
|
6fb72bd44a | ||
|
|
7b8fb56440 | ||
|
|
5e9bd2fd2d | ||
|
|
aac13dcdca | ||
|
|
406e230ed3 | ||
|
|
5a9de1a95d | ||
|
|
0289caad67 | ||
|
|
99cb6fa9ed | ||
|
|
fe011e87d1 | ||
|
|
5a5563f00a | ||
|
|
5a8f076d85 | ||
|
|
45deb5ba7f | ||
|
|
bcea3eb7c1 | ||
|
|
9ca5fa6144 | ||
|
|
d9809318fc | ||
|
|
04ae8a81a5 | ||
|
|
082a078b51 | ||
|
|
04fdb67fc9 | ||
|
|
e6a927e5af | ||
|
|
b05ba64db8 | ||
|
|
d0f5ec8ada | ||
|
|
caa6c8d4b9 | ||
|
|
5cf4a0e09d | ||
|
|
0a0c1c45a1 | ||
|
|
fce89fe8be | ||
|
|
2bbe39120a | ||
|
|
2a5da746c7 | ||
|
|
deb2cd0156 | ||
|
|
b4e4d7055f | ||
|
|
a9feddf6f6 | ||
|
|
071f7cb035 | ||
|
|
a11b0f1665 | ||
|
|
39442bcafe | ||
|
|
48a905bf6f | ||
|
|
440b50b4e8 | ||
|
|
0cf6487cad | ||
|
|
74825dddbf | ||
|
|
699006a3e9 | ||
|
|
6367be213f | ||
|
|
4e97ac3b8c | ||
|
|
57817fd90c | ||
|
|
5b79d0439c | ||
|
|
6d4cee0041 | ||
|
|
3e645db324 | ||
|
|
05da826c24 | ||
|
|
70e16d853e | ||
|
|
9ebf949890 | ||
|
|
a06bdced8a | ||
|
|
3ab506ea94 | ||
|
|
0340402dc1 | ||
|
|
6e8fe7308c | ||
|
|
13c2f471aa | ||
|
|
604f17fbfd | ||
|
|
50669f65bb | ||
|
|
21c61121b0 | ||
|
|
073a5d4d68 | ||
|
|
7a3cab8947 | ||
|
|
aec79c4eeb | ||
|
|
5f385e4c03 | ||
|
|
b018502079 | ||
|
|
47e0a82caf | ||
|
|
d848ee5d5f | ||
|
|
2df0f1bcb8 | ||
|
|
1ae141492a | ||
|
|
7232d07b1c | ||
|
|
64abd564b4 | ||
|
|
483ea77d14 | ||
|
|
cca5abdc8f | ||
|
|
17b3b02ac5 | ||
|
|
ce4e203c14 | ||
|
|
011defc1f7 | ||
|
|
5ccb9bde28 | ||
|
|
30e262d8ac | ||
|
|
692f6779d6 | ||
|
|
e93bf1cfe7 | ||
|
|
8d769d4c4b | ||
|
|
b539ac6335 | ||
|
|
72906b3ee7 | ||
|
|
7f6d4acf90 | ||
|
|
bb892f7e78 | ||
|
|
ead6bb09dc | ||
|
|
0b1ec3f29f | ||
|
|
e77db372bd | ||
|
|
64ca875cfd | ||
|
|
256653677e | ||
|
|
b99980fda1 | ||
|
|
13857d4313 | ||
|
|
fe1ab73818 | ||
|
|
d5a2aa6d6d | ||
|
|
4f9b37433c | ||
|
|
ec506e71a4 | ||
|
|
cfcaa58b71 | ||
|
|
29cf4769f5 | ||
|
|
58fd2273ea | ||
|
|
b52616c64d | ||
|
|
ac1ce6043b | ||
|
|
6631f98c43 | ||
|
|
37340d0445 | ||
|
|
743c97940e | ||
|
|
f2a0f59b08 | ||
|
|
4fb11b68e4 | ||
|
|
ed742c7e16 | ||
|
|
3b110bcd4b | ||
|
|
d4dd74e820 | ||
|
|
3f77cb2214 | ||
|
|
7c839a1df9 | ||
|
|
c5de940946 | ||
|
|
2943c5fafb | ||
|
|
fddeaa966d | ||
|
|
e706ec0ffe | ||
|
|
e65c48be33 | ||
|
|
d44a76bae3 | ||
|
|
4488de9add | ||
|
|
62609a2918 | ||
|
|
492294e11d | ||
|
|
f483f8fdf0 | ||
|
|
4061866042 | ||
|
|
191295b6de | ||
|
|
6d4aa27e15 | ||
|
|
d70d3b439f | ||
|
|
56851eb91f | ||
|
|
2cfe8de030 | ||
|
|
b5604ba0a9 | ||
|
|
1a0e15e04c | ||
|
|
b224c72e98 | ||
|
|
6c57ac8f01 | ||
|
|
cf35dc5345 | ||
|
|
102d13bbae | ||
|
|
faf2aff959 | ||
|
|
5d54ad3342 | ||
|
|
0f5d753910 | ||
|
|
a9442a019f | ||
|
|
1668ef6bb4 | ||
|
|
45436f65af | ||
|
|
b8a295713c | ||
|
|
aa20bc769c | ||
|
|
9bc291e618 | ||
|
|
fdb54b5cdc | ||
|
|
df20662005 | ||
|
|
900f20f164 | ||
|
|
df3b2cd8fe | ||
|
|
c2e4bae9dd | ||
|
|
a39da481e0 | ||
|
|
830ade9596 | ||
|
|
2aa296ff33 | ||
|
|
9050035c74 | ||
|
|
c245855bbf | ||
|
|
ef66e71feb | ||
|
|
c33058ae2b | ||
|
|
629d4a82ae | ||
|
|
d95d282f39 | ||
|
|
64f7233bfc | ||
|
|
9d81e4be2f | ||
|
|
0a1ee86baf | ||
|
|
2908884202 | ||
|
|
aac075be06 | ||
|
|
bf288fdaeb | ||
|
|
a6eddb5798 | ||
|
|
78ae7b847c | ||
|
|
c7bae93b48 | ||
|
|
938cf238ff | ||
|
|
5188846e2e | ||
|
|
d874a3a493 | ||
|
|
f95284622e | ||
|
|
0b6c0e6b94 | ||
|
|
b021bb73ed | ||
|
|
c76b653737 | ||
|
|
3414e2daf0 | ||
|
|
92e2cd102e | ||
|
|
b703c42ee3 | ||
|
|
94f7533ee7 | ||
|
|
101e5d5035 | ||
|
|
ab78af0691 | ||
|
|
af14b682b1 | ||
|
|
a26d4fb499 | ||
|
|
f644d21c61 | ||
|
|
2af52c193c | ||
|
|
7717cda52a | ||
|
|
c13746f10e | ||
|
|
49abfac98e | ||
|
|
aa26e5ac2a | ||
|
|
336f007fa1 | ||
|
|
a3e59d43a7 | ||
|
|
ae840ea689 | ||
|
|
79ee674ad9 | ||
|
|
4d77053313 | ||
|
|
235e88f115 | ||
|
|
021bac5b68 | ||
|
|
60c4cacfbc | ||
|
|
30e6fc516b | ||
|
|
1388e7d7f4 | ||
|
|
b79b123c65 | ||
|
|
d3a6ff6b6a | ||
|
|
10d3adbe40 | ||
|
|
e100e080da | ||
|
|
e2c7a8c384 | ||
|
|
d97bbe7918 | ||
|
|
3864d73d1a | ||
|
|
01891d46b3 | ||
|
|
82987cd53c | ||
|
|
aa56aad46e | ||
|
|
ad7b155a99 | ||
|
|
f1ca06daf5 | ||
|
|
3df743f47b | ||
|
|
637225c2bc | ||
|
|
849104f530 | ||
|
|
3a4bc33d53 | ||
|
|
3679fbe3ea | ||
|
|
b1d2c25ce5 | ||
|
|
480c05650b | ||
|
|
fa265d769c | ||
|
|
4a197a5c90 | ||
|
|
23702056fd | ||
|
|
2a774d20f4 | ||
|
|
b1a7f0fd64 | ||
|
|
70b86907f3 | ||
|
|
d0d813552c | ||
|
|
707dace3d0 | ||
|
|
8361106660 | ||
|
|
a46c519459 | ||
|
|
441c70b388 | ||
|
|
ab65fb7a5c | ||
|
|
59d31c9a18 | ||
|
|
db97ab51ac | ||
|
|
c6f1f97a57 | ||
|
|
fdb1ef540d | ||
|
|
62a8ffb4ae | ||
|
|
1e3cf6f374 | ||
|
|
48c29dd7d9 | ||
|
|
e30727ab27 | ||
|
|
157eb5f87b | ||
|
|
8f290c2a6d | ||
|
|
47f6a217e8 | ||
|
|
69691bdf2a | ||
|
|
522a37b8e3 | ||
|
|
53aa142d94 | ||
|
|
61bd591ee8 | ||
|
|
fd2b438c67 | ||
|
|
5855cdfb26 | ||
|
|
a63266b0e9 | ||
|
|
fe19d96088 | ||
|
|
37933782d2 | ||
|
|
1dcc51e5a4 | ||
|
|
eb61ce2cf2 | ||
|
|
c5bcfe6ab3 | ||
|
|
6c7ed82fa9 | ||
|
|
00ed0d79ec | ||
|
|
cfc84f3e78 | ||
|
|
27e5010d8e | ||
|
|
efa00728d6 | ||
|
|
106caf2b3e | ||
|
|
3dca6c1fd6 | ||
|
|
bd56055593 | ||
|
|
dbd2c7049e | ||
|
|
07aa775de1 | ||
|
|
88ac2a98e8 | ||
|
|
d0c5592855 | ||
|
|
b03ad0cfe0 | ||
|
|
a43b0e6a57 | ||
|
|
12176473ff | ||
|
|
1614c1452f | ||
|
|
d38f16d732 | ||
|
|
d7a2296909 | ||
|
|
4550d888bb | ||
|
|
d62d1d670b | ||
|
|
f18e8c5a38 | ||
|
|
6cb6cd3f26 | ||
|
|
c5554e8f1e | ||
|
|
5144c0ecdc | ||
|
|
642c62d8ce | ||
|
|
f92da16544 | ||
|
|
6e8b370e54 | ||
|
|
1ecf1e0413 | ||
|
|
1ea1482aad | ||
|
|
aeded9ac0e | ||
|
|
1cec872273 | ||
|
|
e550295644 | ||
|
|
70da43eeb7 | ||
|
|
7e9fb6be32 | ||
|
|
258ed4e866 | ||
|
|
b8992362c2 | ||
|
|
60fcf2734a | ||
|
|
ae9e83cf66 | ||
|
|
b8d1e37cce | ||
|
|
6ecd1e5ea5 | ||
|
|
52956503f1 | ||
|
|
3b7bedbbe8 | ||
|
|
148feac43e | ||
|
|
3272033c63 | ||
|
|
73d1bdf84b | ||
|
|
0e38f61c85 | ||
|
|
9d84fe7719 | ||
|
|
5aaecfc0fe | ||
|
|
ff3026686f | ||
|
|
83243b61a6 | ||
|
|
80c4601fdc | ||
|
|
fa3700df7c | ||
|
|
6244e44033 | ||
|
|
d64dafc715 | ||
|
|
9e67880456 | ||
|
|
7484f6e6a6 | ||
|
|
d8d5810d7c | ||
|
|
4a167aa3d7 | ||
|
|
ced3460673 | ||
|
|
d75dd874ca | ||
|
|
1da477d1d1 | ||
|
|
a4ad3896f0 | ||
|
|
e536d203d2 | ||
|
|
ca038937e9 | ||
|
|
342d0862c6 | ||
|
|
3b11285bd5 | ||
|
|
3c404f3678 | ||
|
|
eb37be1381 | ||
|
|
c5f6ace332 | ||
|
|
0e29e8ac76 | ||
|
|
71ae42056f | ||
|
|
a9bad53209 | ||
|
|
eee340366e | ||
|
|
a2bc1a5d2d | ||
|
|
3610d5ea93 | ||
|
|
a75f8e5fdf | ||
|
|
1821a5c678 | ||
|
|
5c3a62b9c5 | ||
|
|
851f57c1f5 | ||
|
|
2cd3f8c6ee | ||
|
|
a331d82cb5 | ||
|
|
6ef2ec4ed2 | ||
|
|
29ed26a503 | ||
|
|
7b3d5ab1ae | ||
|
|
6f2f6e9567 | ||
|
|
9a0bc984d4 | ||
|
|
1922c8dbf8 | ||
|
|
a841449771 | ||
|
|
ff4ef16375 | ||
|
|
33f6926916 | ||
|
|
5d9b1abe82 | ||
|
|
7a5a821f8a | ||
|
|
0b9635c160 | ||
|
|
555ae327b6 | ||
|
|
944c79ec61 | ||
|
|
39eaed260a | ||
|
|
f308836264 | ||
|
|
623688c28e | ||
|
|
8936d8b517 | ||
|
|
14fec7473a | ||
|
|
33b40bf35f | ||
|
|
e52bcf33c5 | ||
|
|
fa6c504b34 | ||
|
|
e9dac8c8f3 | ||
|
|
52bea8f808 | ||
|
|
39ce706f3e | ||
|
|
d0171a8933 | ||
|
|
9f75d2fe4b | ||
|
|
9dfc6c2bc1 | ||
|
|
025e778252 | ||
|
|
c761f631a1 | ||
|
|
e173117a44 | ||
|
|
1ff4206bed | ||
|
|
babc5626a9 | ||
|
|
b624c9a4d2 | ||
|
|
d83feafcb2 | ||
|
|
cc1cb5fbc7 | ||
|
|
80666fed1a | ||
|
|
db2c6c99f7 | ||
|
|
3f1fa44ee7 | ||
|
|
4717e4fe3f | ||
|
|
ffc390d49d | ||
|
|
002e7ab9dd | ||
|
|
ca69bd69f2 | ||
|
|
3e2a366dc6 | ||
|
|
d365aaf270 | ||
|
|
818ee16e39 | ||
|
|
533caba717 | ||
|
|
cae6fd45b3 | ||
|
|
8c268be823 | ||
|
|
17845428bd | ||
|
|
efd1b3cd3c | ||
|
|
2ccd00a378 | ||
|
|
0123a99b5d | ||
|
|
ac744fbd90 | ||
|
|
10a1104073 | ||
|
|
a39a856f69 | ||
|
|
e6e69b4fd2 | ||
|
|
b4de1b49f2 | ||
|
|
c47428b27f | ||
|
|
d267a78416 | ||
|
|
04bb04a6a9 | ||
|
|
8eb535169f | ||
|
|
e0e0fbf739 | ||
|
|
4f8e8ae7b9 | ||
|
|
15b77482ac | ||
|
|
7420363adf | ||
|
|
1666e3a58a | ||
|
|
adbe85cc33 | ||
|
|
89d8d36ec3 | ||
|
|
a631adacb5 | ||
|
|
3384d1b7c3 | ||
|
|
f70de60672 | ||
|
|
cfd54c3f0e | ||
|
|
b29c0fe8cb | ||
|
|
5c1e5e0fcc | ||
|
|
8ec56390c4 | ||
|
|
2ad27e175c | ||
|
|
da1bd3f1fd | ||
|
|
dd913279d7 | ||
|
|
1246a677d1 | ||
|
|
43f2fc0740 | ||
|
|
b821209807 | ||
|
|
39fc5da98f | ||
|
|
504b6af3f6 | ||
|
|
b3ede3230c | ||
|
|
7035503fa7 | ||
|
|
7d147fd040 | ||
|
|
2a44e0b7eb | ||
|
|
686b9bc82c | ||
|
|
0c1497a255 | ||
|
|
496090610f | ||
|
|
16177754d5 | ||
|
|
f41f4939bc | ||
|
|
610503472a | ||
|
|
5dcd74b3b0 | ||
|
|
305825da78 | ||
|
|
205451a31d | ||
|
|
f1ae04fd07 | ||
|
|
4ba82275b9 | ||
|
|
093920173e | ||
|
|
2c3d95a4db | ||
|
|
76928e43a3 | ||
|
|
a816c5dc23 | ||
|
|
5ef84e4bfc | ||
|
|
fbdc9c9f8d | ||
|
|
6510152138 | ||
|
|
df0c6a3b94 | ||
|
|
dfe0d74845 | ||
|
|
b9edd0238d | ||
|
|
bf72237b38 | ||
|
|
a24f6e80c7 | ||
|
|
297c764fe1 | ||
|
|
b03c2a1f80 | ||
|
|
7af77384e7 | ||
|
|
bacbfc8615 | ||
|
|
c8466e9fa6 | ||
|
|
4a231d6fdb | ||
|
|
4e80e1dd03 | ||
|
|
e0b18c6868 | ||
|
|
15b9f8e13f | ||
|
|
03b8dbbc44 | ||
|
|
80af8dcf80 | ||
|
|
62b2856d29 | ||
|
|
7ab3ce91a1 | ||
|
|
e040aeef55 | ||
|
|
46df5a8fa7 | ||
|
|
f61fbbaead | ||
|
|
480f515114 | ||
|
|
e206e6babf | ||
|
|
eae4b52aa1 | ||
|
|
933f75f1ee | ||
|
|
f3c72e561a | ||
|
|
44d6374cfe | ||
|
|
eba13800ff | ||
|
|
3942492f32 | ||
|
|
fe323d5764 | ||
|
|
4740edfb1f | ||
|
|
671dff060d | ||
|
|
4b0dc08426 | ||
|
|
10ffa35b29 | ||
|
|
57fadacda0 | ||
|
|
5c186f30a8 | ||
|
|
0a205f77b0 | ||
|
|
75a0f4373c | ||
|
|
189b245b1d | ||
|
|
0eae47c8be | ||
|
|
00607cb704 | ||
|
|
d79b6e094a | ||
|
|
f9d5c86245 | ||
|
|
a6a1291d0e | ||
|
|
49db1c8244 | ||
|
|
dbe1721d50 | ||
|
|
b55420e935 | ||
|
|
2706df2b24 | ||
|
|
90f791de1b | ||
|
|
c9db3f98d1 | ||
|
|
bdfe233472 | ||
|
|
d340aeb77d | ||
|
|
2da1105ff8 | ||
|
|
e6e5036474 | ||
|
|
609a1709c5 | ||
|
|
2bb9607eea | ||
|
|
ca32d05bb2 | ||
|
|
67a016add0 | ||
|
|
ea41dbb3bc | ||
|
|
637090d259 | ||
|
|
911d65131a | ||
|
|
f4203263bb | ||
|
|
79df5249ef | ||
|
|
ac71093888 | ||
|
|
d15e0f9fe5 | ||
|
|
d3861caf28 | ||
|
|
1c8e379fdd | ||
|
|
d9783490ec | ||
|
|
bb32c3a8d3 | ||
|
|
bab7ec388c | ||
|
|
e2957192d0 | ||
|
|
3994c78365 | ||
|
|
bfd4d7ffe1 | ||
|
|
cb2f18c078 | ||
|
|
69a032c1cf | ||
|
|
a287afb3e9 | ||
|
|
da81f10e04 | ||
|
|
786675a99b | ||
|
|
6603f46678 | ||
|
|
fdfa3bb8f5 | ||
|
|
da204a27c5 | ||
|
|
e11a68afba | ||
|
|
168b0f82dd | ||
|
|
6715a54da2 | ||
|
|
6b32b3ae80 | ||
|
|
6a2242725d | ||
|
|
e9070fadab | ||
|
|
1117e1b724 | ||
|
|
d015b18c66 | ||
|
|
01641b5af4 | ||
|
|
7c0c81207b | ||
|
|
c844b60941 | ||
|
|
9f8246a26a | ||
|
|
6d5141b60f | ||
|
|
8b4a9dd325 | ||
|
|
5006aaae38 | ||
|
|
1bb841d5c5 | ||
|
|
f57c4f390d | ||
|
|
646151e020 | ||
|
|
20f573c477 | ||
|
|
094e4c5da8 | ||
|
|
7716880a6c | ||
|
|
71605fb8fe | ||
|
|
fa9d8b8881 | ||
|
|
aa0566b8ca | ||
|
|
2a838ebb0b | ||
|
|
fabc975b20 | ||
|
|
3bdc88cecb | ||
|
|
a591001761 | ||
|
|
54717ea6f2 | ||
|
|
ceca4c98a3 | ||
|
|
39b4287c5e | ||
|
|
53923c9c87 | ||
|
|
ede733888d | ||
|
|
a79db03093 | ||
|
|
5c8254a9c4 | ||
|
|
2f7b62f710 | ||
|
|
a19c13eb3c | ||
|
|
64d4cd84af | ||
|
|
734db58d85 | ||
|
|
2bbcb8ca89 | ||
|
|
07e810a231 | ||
|
|
48beb184df | ||
|
|
f195e87568 | ||
|
|
13d44ae56a | ||
|
|
00b4874d09 | ||
|
|
5bb90babbc | ||
|
|
7cde30d352 | ||
|
|
73fbf49ba4 | ||
|
|
b39ef5948b | ||
|
|
7cf9dda821 | ||
|
|
657806c8cf | ||
|
|
d61a218808 | ||
|
|
039f73711a | ||
|
|
6e885acf8c | ||
|
|
9ef07cea7a | ||
|
|
9e3b321aaf | ||
|
|
21560701ea | ||
|
|
4556375174 | ||
|
|
91b5398b5a | ||
|
|
eeb8016992 | ||
|
|
736106be3a | ||
|
|
f400568dc0 | ||
|
|
0ca96cba6e | ||
|
|
df4d837026 | ||
|
|
760f84d7fa | ||
|
|
ab35c3557f | ||
|
|
174a315e3f | ||
|
|
0834313456 | ||
|
|
ce3b29085f | ||
|
|
b93d7a204f | ||
|
|
df931e10c0 | ||
|
|
9572cb2d33 | ||
|
|
51e836f32a | ||
|
|
7fefbd88d0 | ||
|
|
cb956c5508 | ||
|
|
47b0086bf8 | ||
|
|
3c14cc219e | ||
|
|
b8d66e4a95 | ||
|
|
7804a22984 | ||
|
|
bfc1c93153 | ||
|
|
5bf3824f28 | ||
|
|
dac23e38d9 | ||
|
|
404faf8a0b | ||
|
|
4a7b0f4711 | ||
|
|
dd62fca45d | ||
|
|
79fb04126c | ||
|
|
39c9574ae3 | ||
|
|
38af257adf | ||
|
|
5aae9a4722 | ||
|
|
cfe3cae88d | ||
|
|
612de84ac6 | ||
|
|
33be597ef0 | ||
|
|
cc26fd80d7 | ||
|
|
c227a1ffec | ||
|
|
f0df787bbe | ||
|
|
09188bed48 | ||
|
|
4a3bcaba06 | ||
|
|
1d1ab65edd | ||
|
|
7330cdaf1c | ||
|
|
050a1fb6cf | ||
|
|
1e8397cf17 | ||
|
|
59b53ece2b | ||
|
|
16c62cd46f | ||
|
|
eff56c2514 | ||
|
|
ee6b9a223f | ||
|
|
acc6ea434a | ||
|
|
1e5a7356f4 | ||
|
|
4c8342c19d | ||
|
|
dad5232ecb | ||
|
|
be972781ee | ||
|
|
58fbc298b1 | ||
|
|
7de7772339 | ||
|
|
ad847a2f5d | ||
|
|
4307db11c5 | ||
|
|
4f79f52524 | ||
|
|
83f8151ca4 | ||
|
|
2c7f24cb8c | ||
|
|
398ebae2ba | ||
|
|
5de176757d | ||
|
|
342575a576 | ||
|
|
785272540e | ||
|
|
82178055af |
@@ -22,7 +22,9 @@
|
|||||||
"fontSize": "14",
|
"fontSize": "14",
|
||||||
"lineNumber": true
|
"lineNumber": true
|
||||||
},
|
},
|
||||||
"sortBy": "UPDATED_AT",
|
"sortBy": {
|
||||||
|
"default": "UPDATED_AT"
|
||||||
|
},
|
||||||
"sortTagsBy": "ALPHABETICAL",
|
"sortTagsBy": "ALPHABETICAL",
|
||||||
"ui": {
|
"ui": {
|
||||||
"defaultNote": "ALWAYS_ASK",
|
"defaultNote": "ALWAYS_ASK",
|
||||||
|
|||||||
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Space indentation
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# The indent size used in the `package.json` file cannot be changed
|
||||||
|
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
|
||||||
|
[{*.yml,*.yaml,package.json}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
41
.vscode/launch.json
vendored
Normal file
41
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "BoostNote Main",
|
||||||
|
"protocol": "inspector",
|
||||||
|
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--remote-debugging-port=9223",
|
||||||
|
"--hot",
|
||||||
|
"${workspaceFolder}/index.js"
|
||||||
|
],
|
||||||
|
"windows": {
|
||||||
|
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "BoostNote Renderer",
|
||||||
|
"port": 9223,
|
||||||
|
"webRoot": "${workspaceFolder}",
|
||||||
|
"sourceMapPathOverrides": {
|
||||||
|
"webpack:///./~/*": "${webRoot}/node_modules/*",
|
||||||
|
"webpack:///*": "${webRoot}/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "BostNote All",
|
||||||
|
"configurations": ["BoostNote Main", "BoostNote Renderer"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
.vscode/tasks.json
vendored
Normal file
27
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Build Boostnote",
|
||||||
|
"group": "build",
|
||||||
|
"type": "npm",
|
||||||
|
"script": "watch",
|
||||||
|
"isBackground": true,
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
},
|
||||||
|
"problemMatcher": {
|
||||||
|
"pattern":[
|
||||||
|
{
|
||||||
|
"regexp": "^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"location": 2,
|
||||||
|
"message": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
72
Backers.md
72
Backers.md
@@ -1,72 +0,0 @@
|
|||||||
<h1 align="center">Sponsors & Backers</h1>
|
|
||||||
|
|
||||||
Boostnote is an open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome backers. If you'd like to join them, please consider:
|
|
||||||
|
|
||||||
- [Become a backer or sponsor on Open Collective.](https://opencollective.com/boostnoteio)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backers via OpenCollective
|
|
||||||
|
|
||||||
### [Gold Sponsors / $1,000 per month](https://opencollective.com/boostnoteio/order/2259)
|
|
||||||
- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/.
|
|
||||||
|
|
||||||
### [Silver Sponsors / $250 per month](https://opencollective.com/boostnoteio/order/2257)
|
|
||||||
- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/.
|
|
||||||
|
|
||||||
### [Bronze Sponsors / $50 per month](https://opencollective.com/boostnoteio/order/2258)
|
|
||||||
- Get your name and Url (or E-mail) on Readme.md on GitHub.
|
|
||||||
|
|
||||||
### [Backers3 / $10 per month](https://opencollective.com/boostnoteio/order/2176)
|
|
||||||
- [Ralph03](https://opencollective.com/ralph03)
|
|
||||||
|
|
||||||
- [Nikolas Dan](https://opencollective.com/nikolas-dan)
|
|
||||||
|
|
||||||
### [Backers2 / $5 per month](https://opencollective.com/boostnoteio/order/2175)
|
|
||||||
- [Yeojong Kim](https://twitter.com/yeojoy)
|
|
||||||
|
|
||||||
- [Scotia Draven](https://opencollective.com/scotia-draven)
|
|
||||||
|
|
||||||
- [A. J. Vargas](https://opencollective.com/aj-vargas)
|
|
||||||
|
|
||||||
### [Backers1](https://opencollective.com/boostnoteio/order/2563) and One-time sponsors
|
|
||||||
- Ryosuke Tamura - $30
|
|
||||||
|
|
||||||
- tatoosh11 - $10
|
|
||||||
|
|
||||||
- Alexander Borovkov - $10
|
|
||||||
|
|
||||||
- spoonhoop - $5
|
|
||||||
|
|
||||||
- Drew Williams - $2
|
|
||||||
|
|
||||||
- Andy Shaw - $2
|
|
||||||
|
|
||||||
- mysafesky -$2
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backers via Bountysource
|
|
||||||
https://salt.bountysource.com/teams/boostnote
|
|
||||||
|
|
||||||
- Kuzz - $65
|
|
||||||
|
|
||||||
- Intense Raiden - $45
|
|
||||||
|
|
||||||
- ravy22 - $25
|
|
||||||
|
|
||||||
- trentpolack - $20
|
|
||||||
|
|
||||||
- hikariru - $10
|
|
||||||
|
|
||||||
- kolchan11 - $10
|
|
||||||
|
|
||||||
- RonWalker22 - $10
|
|
||||||
|
|
||||||
- hocchuc - $5
|
|
||||||
|
|
||||||
- Adam - $5
|
|
||||||
|
|
||||||
- Steve - $5
|
|
||||||
|
|
||||||
- evmin - $5
|
|
||||||
29
FAQ.md
Normal file
29
FAQ.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
|
||||||
|
<details><summary>Allowing dangerous HTML tags</summary>
|
||||||
|
|
||||||
|
Sometimes it is useful to allow dangerous HTML tags to add interactivity to your notebook. One of the example is to use details/summary as a way to expand/collaps your todo-list.
|
||||||
|
|
||||||
|
* How to enable:
|
||||||
|
* Go to **Preferences** → **Interface** → **Sanitization** → **Allow dangerous html tags**
|
||||||
|
* Example note: Multiple todo-list
|
||||||
|
* Create new notes
|
||||||
|
* Paste the below code, and you'll see that you can expand/collaps the todo-list, and you can have multiple todo-list in your note.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<details><summary>What I want to do</summary>
|
||||||
|
|
||||||
|
- [x] Create an awesome feature X
|
||||||
|
- [ ] Do my homework
|
||||||
|
|
||||||
|
</details>
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Other questions
|
||||||
|
|
||||||
|
You can ask [here][ISSUES]
|
||||||
|
|
||||||
|
[ISSUES]: https://github.com/BoostIO/Boostnote/issues
|
||||||
@@ -1,15 +1,25 @@
|
|||||||
# Current behavior
|
# Current behavior
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug.
|
Let us know what is currently happening.
|
||||||
|
|
||||||
If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile.
|
Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug.
|
||||||
|
|
||||||
|
If your issue is regarding Boostnote mobile, please open an issue in the Boostnote Mobile repo 👉 https://github.com/BoostIO/boostnote-mobile.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Expected behavior
|
# Expected behavior
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Let us know what you think should happen!
|
||||||
|
-->
|
||||||
|
|
||||||
# Steps to reproduce
|
# Steps to reproduce
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please be thorough, issues we can reproduce are easier to fix!
|
||||||
|
-->
|
||||||
|
|
||||||
1.
|
1.
|
||||||
2.
|
2.
|
||||||
3.
|
3.
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -2,7 +2,7 @@ GPL-3.0
|
|||||||
|
|
||||||
Boostnote - an open source note-taking app made for programmers just like you.
|
Boostnote - an open source note-taking app made for programmers just like you.
|
||||||
|
|
||||||
Copyright (C) 2017 - 2018 BoostIO
|
Copyright (C) 2017 - 2019 BoostIO
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
|||||||
36
PULL_REQUEST_TEMPLATE.md
Normal file
36
PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!--
|
||||||
|
Before submitting this PR, please make sure that:
|
||||||
|
- You have read and understand the contributing.md
|
||||||
|
- You have checked docs/code_style.md for information on code style
|
||||||
|
-->
|
||||||
|
## Description
|
||||||
|
<!--
|
||||||
|
Tell us what your PR does.
|
||||||
|
Please attach a screenshot/ video/gif image describing your PR if possible.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Issue fixed
|
||||||
|
<!--
|
||||||
|
Please list out all issue fixed with this PR here.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please make sure you fill in these checkboxes,
|
||||||
|
your PR will be reviewed faster if we know exactly what it does.
|
||||||
|
|
||||||
|
Change :white_circle: to :radio_button: in all the options that apply
|
||||||
|
-->
|
||||||
|
## Type of changes
|
||||||
|
|
||||||
|
- :white_circle: Bug fix (Change that fixed an issue)
|
||||||
|
- :white_circle: Breaking change (Change that can cause existing functionality to change)
|
||||||
|
- :white_circle: Improvement (Change that improves the code. Maybe performance or development improvement)
|
||||||
|
- :white_circle: Feature (Change that adds new functionality)
|
||||||
|
- :white_circle: Documentation change (Change that modifies documentation. Maybe typo fixes)
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
|
||||||
|
- :white_circle: My code follows [the project code style](docs/code_style.md)
|
||||||
|
- :white_circle: I have written test for my code and it has been tested
|
||||||
|
- :white_circle: All existing tests have been passed
|
||||||
|
- :white_circle: I have attached a screenshot/video to visualize my change if possible
|
||||||
File diff suppressed because it is too large
Load Diff
6
browser/components/CodeEditor.styl
Normal file
6
browser/components/CodeEditor.styl
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.codeEditor-typo
|
||||||
|
text-decoration underline wavy red
|
||||||
|
|
||||||
|
.spellcheck-select
|
||||||
|
border: none
|
||||||
|
text-decoration underline wavy red
|
||||||
68
browser/components/ColorPicker.js
Normal file
68
browser/components/ColorPicker.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { SketchPicker } from 'react-color'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './ColorPicker.styl'
|
||||||
|
|
||||||
|
const componentHeight = 330
|
||||||
|
|
||||||
|
class ColorPicker extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
color: this.props.color || '#939395'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onColorChange = this.onColorChange.bind(this)
|
||||||
|
this.handleConfirm = this.handleConfirm.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
this.onColorChange(nextProps.color)
|
||||||
|
}
|
||||||
|
|
||||||
|
onColorChange (color) {
|
||||||
|
this.setState({
|
||||||
|
color
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfirm () {
|
||||||
|
this.props.onConfirm(this.state.color)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { onReset, onCancel, targetRect } = this.props
|
||||||
|
const { color } = this.state
|
||||||
|
|
||||||
|
const clientHeight = document.body.clientHeight
|
||||||
|
const alignX = targetRect.right + 4
|
||||||
|
let alignY = targetRect.top
|
||||||
|
if (targetRect.top + componentHeight > clientHeight) {
|
||||||
|
alignY = targetRect.bottom - componentHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div styleName='colorPicker' style={{top: `${alignY}px`, left: `${alignX}px`}}>
|
||||||
|
<div styleName='cover' onClick={onCancel} />
|
||||||
|
<SketchPicker color={color} onChange={this.onColorChange} />
|
||||||
|
<div styleName='footer'>
|
||||||
|
<button styleName='btn-reset' onClick={onReset}>Reset</button>
|
||||||
|
<button styleName='btn-cancel' onClick={onCancel}>Cancel</button>
|
||||||
|
<button styleName='btn-confirm' onClick={this.handleConfirm}>Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorPicker.propTypes = {
|
||||||
|
color: PropTypes.string,
|
||||||
|
targetRect: PropTypes.object,
|
||||||
|
onConfirm: PropTypes.func.isRequired,
|
||||||
|
onCancel: PropTypes.func.isRequired,
|
||||||
|
onReset: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(ColorPicker, styles)
|
||||||
39
browser/components/ColorPicker.styl
Normal file
39
browser/components/ColorPicker.styl
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
.colorPicker
|
||||||
|
position fixed
|
||||||
|
z-index 2
|
||||||
|
display flex
|
||||||
|
flex-direction column
|
||||||
|
|
||||||
|
.cover
|
||||||
|
position fixed
|
||||||
|
top 0
|
||||||
|
right 0
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
|
||||||
|
.footer
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
z-index 2
|
||||||
|
align-items center
|
||||||
|
& > button + button
|
||||||
|
margin-left 10px
|
||||||
|
|
||||||
|
.btn-cancel,
|
||||||
|
.btn-confirm,
|
||||||
|
.btn-reset
|
||||||
|
vertical-align middle
|
||||||
|
height 25px
|
||||||
|
margin-top 2.5px
|
||||||
|
border-radius 2px
|
||||||
|
border none
|
||||||
|
padding 0 5px
|
||||||
|
background-color $default-button-background
|
||||||
|
&:hover
|
||||||
|
background-color $default-button-background--hover
|
||||||
|
.btn-confirm
|
||||||
|
background-color #1EC38B
|
||||||
|
&:hover
|
||||||
|
background-color darken(#1EC38B, 25%)
|
||||||
|
|
||||||
|
|
||||||
@@ -6,6 +6,8 @@ import CodeEditor from 'browser/components/CodeEditor'
|
|||||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
|
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||||
|
|
||||||
class MarkdownEditor extends React.Component {
|
class MarkdownEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -18,10 +20,10 @@ class MarkdownEditor extends React.Component {
|
|||||||
this.supportMdSelectionBold = [16, 17, 186]
|
this.supportMdSelectionBold = [16, 17, 186]
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
status: 'PREVIEW',
|
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'CODE',
|
||||||
renderValue: props.value,
|
renderValue: props.value,
|
||||||
keyPressed: new Set(),
|
keyPressed: new Set(),
|
||||||
isLocked: false
|
isLocked: props.isLocked
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lockEditorCode = () => this.handleLockEditor()
|
this.lockEditorCode = () => this.handleLockEditor()
|
||||||
@@ -64,17 +66,20 @@ class MarkdownEditor extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setValue (value) {
|
||||||
|
this.refs.code.setValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
handleChange (e) {
|
handleChange (e) {
|
||||||
this.value = this.refs.code.value
|
this.value = this.refs.code.value
|
||||||
this.props.onChange(e)
|
this.props.onChange(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleContextMenu (e) {
|
handleContextMenu (e) {
|
||||||
|
if (this.state.isLocked) return
|
||||||
const { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||||
const newStatus = this.state.status === 'PREVIEW'
|
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
|
||||||
? 'CODE'
|
|
||||||
: 'PREVIEW'
|
|
||||||
this.setState({
|
this.setState({
|
||||||
status: newStatus
|
status: newStatus
|
||||||
}, () => {
|
}, () => {
|
||||||
@@ -84,6 +89,10 @@ class MarkdownEditor extends React.Component {
|
|||||||
this.refs.preview.focus()
|
this.refs.preview.focus()
|
||||||
}
|
}
|
||||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||||
|
|
||||||
|
const newConfig = Object.assign({}, config)
|
||||||
|
newConfig.editor.delfaultStatus = newStatus
|
||||||
|
ConfigManager.set(newConfig)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,8 +149,10 @@ class MarkdownEditor extends React.Component {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const idMatch = /checkbox-([0-9]+)/
|
const idMatch = /checkbox-([0-9]+)/
|
||||||
const checkedMatch = /\[x\]/i
|
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||||
const uncheckedMatch = /\[ \]/
|
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||||
|
const checkReplace = /\[x\]/i
|
||||||
|
const uncheckReplace = /\[ \]/
|
||||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
const lines = this.refs.code.value
|
const lines = this.refs.code.value
|
||||||
@@ -150,10 +161,10 @@ class MarkdownEditor extends React.Component {
|
|||||||
const targetLine = lines[lineIndex]
|
const targetLine = lines[lineIndex]
|
||||||
|
|
||||||
if (targetLine.match(checkedMatch)) {
|
if (targetLine.match(checkedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
|
||||||
}
|
}
|
||||||
if (targetLine.match(uncheckedMatch)) {
|
if (targetLine.match(uncheckedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
|
||||||
}
|
}
|
||||||
this.refs.code.setValue(lines.join('\n'))
|
this.refs.code.setValue(lines.join('\n'))
|
||||||
}
|
}
|
||||||
@@ -212,6 +223,28 @@ class MarkdownEditor extends React.Component {
|
|||||||
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
|
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDropImage (dropEvent) {
|
||||||
|
dropEvent.preventDefault()
|
||||||
|
const { storageKey, noteKey } = this.props
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
status: 'CODE'
|
||||||
|
}, () => {
|
||||||
|
this.refs.code.focus()
|
||||||
|
|
||||||
|
this.refs.code.editor.execCommand('goDocEnd')
|
||||||
|
this.refs.code.editor.execCommand('goLineEnd')
|
||||||
|
this.refs.code.editor.execCommand('newlineAndIndent')
|
||||||
|
|
||||||
|
attachmentManagement.handleAttachmentDrop(
|
||||||
|
this.refs.code,
|
||||||
|
storageKey,
|
||||||
|
noteKey,
|
||||||
|
dropEvent
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleKeyUp (e) {
|
handleKeyUp (e) {
|
||||||
const keyPressed = this.state.keyPressed
|
const keyPressed = this.state.keyPressed
|
||||||
keyPressed.delete(e.keyCode)
|
keyPressed.delete(e.keyCode)
|
||||||
@@ -223,7 +256,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {className, value, config, storageKey, noteKey} = this.props
|
const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props
|
||||||
|
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
@@ -250,7 +283,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
: 'codeEditor--hide'
|
: 'codeEditor--hide'
|
||||||
}
|
}
|
||||||
ref='code'
|
ref='code'
|
||||||
mode='GitHub Flavored Markdown'
|
mode='Boost Flavored Markdown'
|
||||||
value={value}
|
value={value}
|
||||||
theme={config.editor.theme}
|
theme={config.editor.theme}
|
||||||
keyMap={config.editor.keyMap}
|
keyMap={config.editor.keyMap}
|
||||||
@@ -261,12 +294,21 @@ class MarkdownEditor extends React.Component {
|
|||||||
enableRulers={config.editor.enableRulers}
|
enableRulers={config.editor.enableRulers}
|
||||||
rulers={config.editor.rulers}
|
rulers={config.editor.rulers}
|
||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
matchingPairs={config.editor.matchingPairs}
|
||||||
|
matchingTriples={config.editor.matchingTriples}
|
||||||
|
explodingPairs={config.editor.explodingPairs}
|
||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
noteKey={noteKey}
|
noteKey={noteKey}
|
||||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
|
enableTableEditor={config.editor.enableTableEditor}
|
||||||
|
linesHighlighted={linesHighlighted}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
onBlur={(e) => this.handleBlur(e)}
|
onBlur={(e) => this.handleBlur(e)}
|
||||||
|
spellCheck={config.editor.spellcheck}
|
||||||
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
|
hotkey={config.hotkey}
|
||||||
|
switchPreview={config.editor.switchPreview}
|
||||||
/>
|
/>
|
||||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||||
? 'preview'
|
? 'preview'
|
||||||
@@ -299,6 +341,8 @@ class MarkdownEditor extends React.Component {
|
|||||||
noteKey={noteKey}
|
noteKey={noteKey}
|
||||||
customCSS={config.preview.customCSS}
|
customCSS={config.preview.customCSS}
|
||||||
allowCustomCSS={config.preview.allowCustomCSS}
|
allowCustomCSS={config.preview.allowCustomCSS}
|
||||||
|
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||||
|
onDrop={(e) => this.handleDropImage(e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
.preview
|
.preview
|
||||||
display block
|
display block
|
||||||
absolute top bottom left right
|
absolute top bottom left right
|
||||||
z-index 100
|
|
||||||
background-color white
|
background-color white
|
||||||
height 100%
|
height 100%
|
||||||
width 100%
|
width 100%
|
||||||
|
|||||||
@@ -17,8 +17,15 @@ import copy from 'copy-to-clipboard'
|
|||||||
import mdurl from 'mdurl'
|
import mdurl from 'mdurl'
|
||||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||||
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
import context from 'browser/lib/context'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import fs from 'fs'
|
||||||
|
import { render } from 'react-dom'
|
||||||
|
import Carousel from 'react-image-carousel'
|
||||||
|
import ConfigManager from '../main/lib/ConfigManager'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote, shell } = require('electron')
|
||||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||||
|
|
||||||
const { app } = remote
|
const { app } = remote
|
||||||
@@ -27,16 +34,28 @@ const fileUrl = require('file-url')
|
|||||||
|
|
||||||
const dialog = remote.dialog
|
const dialog = remote.dialog
|
||||||
|
|
||||||
|
const uri2path = require('file-uri-to-path')
|
||||||
|
|
||||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||||
const appPath = fileUrl(process.env.NODE_ENV === 'production'
|
const appPath = fileUrl(
|
||||||
? app.getAppPath()
|
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
|
||||||
: path.resolve())
|
)
|
||||||
const CSS_FILES = [
|
const CSS_FILES = [
|
||||||
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
`${appPath}/node_modules/codemirror/lib/codemirror.css`,
|
||||||
|
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
|
||||||
]
|
]
|
||||||
|
|
||||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) {
|
function buildStyle (
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
) {
|
||||||
return `
|
return `
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
@@ -66,7 +85,6 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scro
|
|||||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
${allowCustomCSS ? customCSS : ''}
|
|
||||||
${markdownStyle}
|
${markdownStyle}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -74,6 +92,11 @@ body {
|
|||||||
font-size: ${fontSize}px;
|
font-size: ${fontSize}px;
|
||||||
${scrollPastEnd && 'padding-bottom: 90vh;'}
|
${scrollPastEnd && 'padding-bottom: 90vh;'}
|
||||||
}
|
}
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
padding-bottom: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
code {
|
code {
|
||||||
font-family: '${codeBlockFontFamily.join("','")}';
|
font-family: '${codeBlockFontFamily.join("','")}';
|
||||||
background-color: rgba(0,0,0,0.04);
|
background-color: rgba(0,0,0,0.04);
|
||||||
@@ -130,6 +153,8 @@ body p {
|
|||||||
display: none
|
display: none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${allowCustomCSS ? customCSS : ''}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +177,6 @@ const scrollBarDarkStyle = `
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const { shell } = require('electron')
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||||
@@ -160,23 +184,33 @@ if (!OSX) {
|
|||||||
defaultFontFamily.unshift('Microsoft YaHei')
|
defaultFontFamily.unshift('Microsoft YaHei')
|
||||||
defaultFontFamily.unshift('meiryo')
|
defaultFontFamily.unshift('meiryo')
|
||||||
}
|
}
|
||||||
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
const defaultCodeBlockFontFamily = [
|
||||||
|
'Monaco',
|
||||||
|
'Menlo',
|
||||||
|
'Ubuntu Mono',
|
||||||
|
'Consolas',
|
||||||
|
'source-code-pro',
|
||||||
|
'monospace'
|
||||||
|
]
|
||||||
export default class MarkdownPreview extends React.Component {
|
export default class MarkdownPreview extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
this.contextMenuHandler = e => this.handleContextMenu(e)
|
||||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
this.mouseDownHandler = e => this.handleMouseDown(e)
|
||||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
this.mouseUpHandler = e => this.handleMouseUp(e)
|
||||||
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
|
this.DoubleClickHandler = e => this.handleDoubleClick(e)
|
||||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
|
||||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
leading: false,
|
||||||
|
trailing: true
|
||||||
|
})
|
||||||
|
this.checkboxClickHandler = e => this.handleCheckboxClick(e)
|
||||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||||
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||||
this.printHandler = () => this.handlePrint()
|
this.printHandler = () => this.handlePrint()
|
||||||
|
|
||||||
this.linkClickHandler = this.handlelinkClick.bind(this)
|
this.linkClickHandler = this.handleLinkClick.bind(this)
|
||||||
this.initMarkdown = this.initMarkdown.bind(this)
|
this.initMarkdown = this.initMarkdown.bind(this)
|
||||||
this.initMarkdown()
|
this.initMarkdown()
|
||||||
}
|
}
|
||||||
@@ -200,8 +234,32 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleContextMenu (e) {
|
handleContextMenu (event) {
|
||||||
this.props.onContextMenu(e)
|
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return
|
||||||
|
if (_.isFunction(this.props.onContextMenu)) {
|
||||||
|
this.props.onContextMenu(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// No contextMenu was passed to us -> execute our own link-opener
|
||||||
|
if (event.target.tagName.toLowerCase() === 'a') {
|
||||||
|
const href = event.target.href
|
||||||
|
const isLocalFile = href.startsWith('file:')
|
||||||
|
if (isLocalFile) {
|
||||||
|
const absPath = uri2path(href)
|
||||||
|
try {
|
||||||
|
if (fs.lstatSync(absPath).isFile()) {
|
||||||
|
context.popup([
|
||||||
|
{
|
||||||
|
label: i18n.__('Show in explorer'),
|
||||||
|
click: (e) => shell.showItemInFolder(absPath)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error while evaluating if the file is locally available', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDoubleClick (e) {
|
handleDoubleClick (e) {
|
||||||
@@ -209,6 +267,10 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMouseDown (e) {
|
handleMouseDown (e) {
|
||||||
|
const config = ConfigManager.get()
|
||||||
|
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
|
||||||
|
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
||||||
|
}
|
||||||
if (e.target != null) {
|
if (e.target != null) {
|
||||||
switch (e.target.tagName) {
|
switch (e.target.tagName) {
|
||||||
case 'A':
|
case 'A':
|
||||||
@@ -237,31 +299,44 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
handleSaveAsHtml () {
|
handleSaveAsHtml () {
|
||||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
const {
|
||||||
|
fontFamily,
|
||||||
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
fontSize,
|
||||||
let body = this.markdown.render(escapeHtmlCharacters(noteContent, { detectCodeBlock: true }))
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
codeBlockTheme,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
} = this.getStyleParams()
|
||||||
|
|
||||||
|
const inlineStyles = buildStyle(
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
)
|
||||||
|
let body = this.markdown.render(noteContent)
|
||||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath)
|
files.forEach(file => {
|
||||||
|
if (global.process.platform === 'win32') {
|
||||||
files.forEach((file) => {
|
file = file.replace('file:///', '')
|
||||||
file = file.replace('file://', '')
|
} else {
|
||||||
|
file = file.replace('file://', '')
|
||||||
|
}
|
||||||
exportTasks.push({
|
exportTasks.push({
|
||||||
src: file,
|
src: file,
|
||||||
dst: 'css'
|
dst: 'css'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
attachmentsAbsolutePaths.forEach((attachment) => {
|
|
||||||
exportTasks.push({
|
|
||||||
src: attachment,
|
|
||||||
dst: attachmentManagement.DESTINATION_FOLDER
|
|
||||||
})
|
|
||||||
})
|
|
||||||
body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey)
|
|
||||||
|
|
||||||
let styles = ''
|
let styles = ''
|
||||||
files.forEach((file) => {
|
files.forEach(file => {
|
||||||
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -283,48 +358,79 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
exportAsDocument (fileType, contentFormatter) {
|
exportAsDocument (fileType, contentFormatter) {
|
||||||
const options = {
|
const options = {
|
||||||
filters: [
|
filters: [{ name: 'Documents', extensions: [fileType] }],
|
||||||
{name: 'Documents', extensions: [fileType]}
|
|
||||||
],
|
|
||||||
properties: ['openFile', 'createDirectory']
|
properties: ['openFile', 'createDirectory']
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
|
||||||
(filename) => {
|
if (filename) {
|
||||||
if (filename) {
|
const content = this.props.value
|
||||||
const content = this.props.value
|
const storage = this.props.storagePath
|
||||||
const storage = this.props.storagePath
|
const nodeKey = this.props.noteKey
|
||||||
|
|
||||||
exportNote(storage, content, filename, contentFormatter)
|
exportNote(nodeKey, storage, content, filename, contentFormatter)
|
||||||
.then((res) => {
|
.then(res => {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
}).catch((err) => {
|
type: 'info',
|
||||||
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
|
message: `Exported to ${filename}`
|
||||||
throw err
|
})
|
||||||
})
|
})
|
||||||
}
|
.catch(err => {
|
||||||
})
|
dialog.showErrorBox(
|
||||||
|
'Export error',
|
||||||
|
err ? err.message || err : 'Unexpected error during export'
|
||||||
|
)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fixDecodedURI (node) {
|
fixDecodedURI (node) {
|
||||||
if (node && node.children.length === 1 && typeof node.children[0] === 'string') {
|
if (
|
||||||
|
node &&
|
||||||
|
node.children.length === 1 &&
|
||||||
|
typeof node.children[0] === 'string'
|
||||||
|
) {
|
||||||
const { innerText, href } = node
|
const { innerText, href } = node
|
||||||
|
|
||||||
node.innerText = mdurl.decode(href) === innerText
|
node.innerText = mdurl.decode(href) === innerText ? href : innerText
|
||||||
? href
|
|
||||||
: innerText
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Convert special characters between three ```
|
||||||
|
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
|
||||||
|
* @returns {string} HTML in which special characters between three ``` have been converted
|
||||||
|
*/
|
||||||
|
escapeHtmlCharactersInCodeTag (splitWithCodeTag) {
|
||||||
|
for (let index = 0; index < splitWithCodeTag.length; index++) {
|
||||||
|
const codeTagRequired = (splitWithCodeTag[index] !== '\`\`\`' && index < splitWithCodeTag.length - 1)
|
||||||
|
if (codeTagRequired) {
|
||||||
|
splitWithCodeTag.splice((index + 1), 0, '\`\`\`')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let inCodeTag = false
|
||||||
|
let result = ''
|
||||||
|
for (let content of splitWithCodeTag) {
|
||||||
|
if (content === '\`\`\`') {
|
||||||
|
inCodeTag = !inCodeTag
|
||||||
|
} else if (inCodeTag) {
|
||||||
|
content = escapeHtmlCharacters(content)
|
||||||
|
}
|
||||||
|
result += content
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
getScrollBarStyle () {
|
getScrollBarStyle () {
|
||||||
const {
|
const { theme } = this.props
|
||||||
theme
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
switch (theme) {
|
switch (theme) {
|
||||||
case 'dark':
|
case 'dark':
|
||||||
case 'solarized-dark':
|
case 'solarized-dark':
|
||||||
case 'monokai':
|
case 'monokai':
|
||||||
|
case 'dracula':
|
||||||
return scrollBarDarkStyle
|
return scrollBarDarkStyle
|
||||||
default:
|
default:
|
||||||
return scrollBarStyle
|
return scrollBarStyle
|
||||||
@@ -332,8 +438,13 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
const { onDrop } = this.props
|
||||||
|
|
||||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||||
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
this.refs.root.contentWindow.document.body.addEventListener(
|
||||||
|
'contextmenu',
|
||||||
|
this.contextMenuHandler
|
||||||
|
)
|
||||||
|
|
||||||
let styles = `
|
let styles = `
|
||||||
<style id='style'></style>
|
<style id='style'></style>
|
||||||
@@ -344,7 +455,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
</style>
|
</style>
|
||||||
`
|
`
|
||||||
|
|
||||||
CSS_FILES.forEach((file) => {
|
CSS_FILES.forEach(file => {
|
||||||
styles += `<link rel="stylesheet" href="${file}">`
|
styles += `<link rel="stylesheet" href="${file}">`
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -352,12 +463,30 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
'mousedown',
|
||||||
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler)
|
this.mouseDownHandler
|
||||||
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
)
|
||||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler)
|
'mouseup',
|
||||||
|
this.mouseUpHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
|
'dblclick',
|
||||||
|
this.DoubleClickHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
|
'drop',
|
||||||
|
onDrop || this.preventImageDroppedHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
|
'dragover',
|
||||||
|
this.preventImageDroppedHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
|
'scroll',
|
||||||
|
this.scrollHandler
|
||||||
|
)
|
||||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||||
@@ -365,13 +494,36 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
const { onDrop } = this.props
|
||||||
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
|
||||||
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
this.refs.root.contentWindow.document.body.removeEventListener(
|
||||||
this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler)
|
'contextmenu',
|
||||||
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
this.contextMenuHandler
|
||||||
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler)
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'mousedown',
|
||||||
|
this.mouseDownHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'mouseup',
|
||||||
|
this.mouseUpHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'dblclick',
|
||||||
|
this.DoubleClickHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'drop',
|
||||||
|
onDrop || this.preventImageDroppedHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'dragover',
|
||||||
|
this.preventImageDroppedHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'scroll',
|
||||||
|
this.scrollHandler
|
||||||
|
)
|
||||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||||
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||||
@@ -380,14 +532,18 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||||
if (prevProps.smartQuotes !== this.props.smartQuotes ||
|
if (
|
||||||
prevProps.sanitize !== this.props.sanitize ||
|
prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||||
prevProps.smartArrows !== this.props.smartArrows ||
|
prevProps.sanitize !== this.props.sanitize ||
|
||||||
prevProps.breaks !== this.props.breaks) {
|
prevProps.smartArrows !== this.props.smartArrows ||
|
||||||
|
prevProps.breaks !== this.props.breaks ||
|
||||||
|
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
|
||||||
|
) {
|
||||||
this.initMarkdown()
|
this.initMarkdown()
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
}
|
}
|
||||||
if (prevProps.fontFamily !== this.props.fontFamily ||
|
if (
|
||||||
|
prevProps.fontFamily !== this.props.fontFamily ||
|
||||||
prevProps.fontSize !== this.props.fontSize ||
|
prevProps.fontSize !== this.props.fontSize ||
|
||||||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
||||||
@@ -396,34 +552,82 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
prevProps.theme !== this.props.theme ||
|
prevProps.theme !== this.props.theme ||
|
||||||
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
|
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
|
||||||
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
|
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
|
||||||
prevProps.customCSS !== this.props.customCSS) {
|
prevProps.customCSS !== this.props.customCSS
|
||||||
|
) {
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStyleParams () {
|
getStyleParams () {
|
||||||
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS } = this.props
|
const {
|
||||||
|
fontSize,
|
||||||
|
lineNumber,
|
||||||
|
codeBlockTheme,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
} = this.props
|
||||||
let { fontFamily, codeBlockFontFamily } = this.props
|
let { fontFamily, codeBlockFontFamily } = this.props
|
||||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||||
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
? fontFamily
|
||||||
: defaultFontFamily
|
.split(',')
|
||||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
.map(fontName => fontName.trim())
|
||||||
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
.concat(defaultFontFamily)
|
||||||
: defaultCodeBlockFontFamily
|
: defaultFontFamily
|
||||||
|
codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
|
||||||
|
codeBlockFontFamily.trim().length > 0
|
||||||
|
? codeBlockFontFamily
|
||||||
|
.split(',')
|
||||||
|
.map(fontName => fontName.trim())
|
||||||
|
.concat(defaultCodeBlockFontFamily)
|
||||||
|
: defaultCodeBlockFontFamily
|
||||||
|
|
||||||
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS}
|
return {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
codeBlockTheme,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStyle () {
|
applyStyle () {
|
||||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
const {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
codeBlockTheme,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
} = this.getStyleParams()
|
||||||
|
|
||||||
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
|
this.getWindow().document.getElementById(
|
||||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
'codeTheme'
|
||||||
|
).href = this.GetCodeThemeLink(codeBlockTheme)
|
||||||
|
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
GetCodeThemeLink (theme) {
|
GetCodeThemeLink (theme) {
|
||||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
theme = consts.THEMES.some(_theme => _theme === theme) &&
|
||||||
|
theme !== 'default'
|
||||||
? theme
|
? theme
|
||||||
: 'elegant'
|
: 'elegant'
|
||||||
return theme.startsWith('solarized')
|
return theme.startsWith('solarized')
|
||||||
@@ -432,71 +636,97 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rewriteIframe () {
|
rewriteIframe () {
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
_.forEach(
|
||||||
el.removeEventListener('click', this.checkboxClickHandler)
|
this.refs.root.contentWindow.document.querySelectorAll(
|
||||||
})
|
'input[type="checkbox"]'
|
||||||
|
),
|
||||||
|
el => {
|
||||||
|
el.removeEventListener('click', this.checkboxClickHandler)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
_.forEach(
|
||||||
el.removeEventListener('click', this.linkClickHandler)
|
this.refs.root.contentWindow.document.querySelectorAll('a'),
|
||||||
})
|
el => {
|
||||||
|
el.removeEventListener('click', this.linkClickHandler)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const { theme, indentSize, showCopyNotification, storagePath, noteKey } = this.props
|
const {
|
||||||
|
theme,
|
||||||
|
indentSize,
|
||||||
|
showCopyNotification,
|
||||||
|
storagePath,
|
||||||
|
noteKey,
|
||||||
|
sanitize
|
||||||
|
} = this.props
|
||||||
let { value, codeBlockTheme } = this.props
|
let { value, codeBlockTheme } = this.props
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||||
|
if (sanitize === 'NONE') {
|
||||||
const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g)
|
const splitWithCodeTag = value.split('```')
|
||||||
if (codeBlocks !== null) {
|
value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag)
|
||||||
codeBlocks.forEach((codeBlock) => {
|
|
||||||
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const renderedHTML = this.markdown.render(value)
|
const renderedHTML = this.markdown.render(value)
|
||||||
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
||||||
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)
|
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
|
||||||
|
renderedHTML,
|
||||||
|
storagePath
|
||||||
|
)
|
||||||
|
_.forEach(
|
||||||
|
this.refs.root.contentWindow.document.querySelectorAll(
|
||||||
|
'input[type="checkbox"]'
|
||||||
|
),
|
||||||
|
el => {
|
||||||
|
el.addEventListener('click', this.checkboxClickHandler)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
_.forEach(
|
||||||
el.addEventListener('click', this.checkboxClickHandler)
|
this.refs.root.contentWindow.document.querySelectorAll('a'),
|
||||||
})
|
el => {
|
||||||
|
this.fixDecodedURI(el)
|
||||||
|
el.addEventListener('click', this.linkClickHandler)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
|
||||||
this.fixDecodedURI(el)
|
|
||||||
el.addEventListener('click', this.linkClickHandler)
|
|
||||||
})
|
|
||||||
|
|
||||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
|
||||||
? codeBlockTheme
|
? codeBlockTheme
|
||||||
: 'default'
|
: 'default'
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
|
_.forEach(
|
||||||
let syntax = CodeMirror.findModeByName(convertModeName(el.className))
|
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
||||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
el => {
|
||||||
CodeMirror.requireMode(syntax.mode, () => {
|
let syntax = CodeMirror.findModeByName(convertModeName(el.className))
|
||||||
const content = htmlTextHelper.decodeEntities(el.innerHTML)
|
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||||
const copyIcon = document.createElement('i')
|
CodeMirror.requireMode(syntax.mode, () => {
|
||||||
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
const content = htmlTextHelper.decodeEntities(el.innerHTML)
|
||||||
copyIcon.onclick = (e) => {
|
const copyIcon = document.createElement('i')
|
||||||
copy(content)
|
copyIcon.innerHTML =
|
||||||
if (showCopyNotification) {
|
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||||
this.notify('Saved to Clipboard!', {
|
copyIcon.onclick = e => {
|
||||||
body: 'Paste it wherever you want!',
|
copy(content)
|
||||||
silent: true
|
if (showCopyNotification) {
|
||||||
})
|
this.notify('Saved to Clipboard!', {
|
||||||
|
body: 'Paste it wherever you want!',
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
el.parentNode.appendChild(copyIcon)
|
||||||
el.parentNode.appendChild(copyIcon)
|
el.innerHTML = ''
|
||||||
el.innerHTML = ''
|
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
const [refThema, color] = codeBlockTheme.split(' ')
|
||||||
const [refThema, color] = codeBlockTheme.split(' ')
|
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
} else {
|
||||||
} else {
|
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
}
|
||||||
}
|
CodeMirror.runMode(content, syntax.mime, el, {
|
||||||
CodeMirror.runMode(content, syntax.mime, el, {
|
tabSize: indentSize
|
||||||
tabSize: indentSize
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
})
|
)
|
||||||
const opts = {}
|
const opts = {}
|
||||||
// if (this.props.theme === 'dark') {
|
// if (this.props.theme === 'dark') {
|
||||||
// opts['font-color'] = '#DDD'
|
// opts['font-color'] = '#DDD'
|
||||||
@@ -504,50 +734,65 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
// opts['element-color'] = '#DDD'
|
// opts['element-color'] = '#DDD'
|
||||||
// opts['fill'] = '#3A404C'
|
// opts['fill'] = '#3A404C'
|
||||||
// }
|
// }
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => {
|
_.forEach(
|
||||||
Raphael.setWindow(this.getWindow())
|
this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
|
||||||
try {
|
el => {
|
||||||
const diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
Raphael.setWindow(this.getWindow())
|
||||||
el.innerHTML = ''
|
try {
|
||||||
diagram.drawSVG(el, opts)
|
const diagram = flowchart.parse(
|
||||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
htmlTextHelper.decodeEntities(el.innerHTML)
|
||||||
el.addEventListener('click', this.linkClickHandler)
|
)
|
||||||
})
|
el.innerHTML = ''
|
||||||
} catch (e) {
|
diagram.drawSVG(el, opts)
|
||||||
console.error(e)
|
_.forEach(el.querySelectorAll('a'), el => {
|
||||||
el.className = 'flowchart-error'
|
el.addEventListener('click', this.linkClickHandler)
|
||||||
el.innerHTML = 'Flowchart parse error: ' + e.message
|
})
|
||||||
|
} catch (e) {
|
||||||
|
el.className = 'flowchart-error'
|
||||||
|
el.innerHTML = 'Flowchart parse error: ' + e.message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => {
|
_.forEach(
|
||||||
Raphael.setWindow(this.getWindow())
|
this.refs.root.contentWindow.document.querySelectorAll('.sequence'),
|
||||||
try {
|
el => {
|
||||||
const diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
Raphael.setWindow(this.getWindow())
|
||||||
el.innerHTML = ''
|
try {
|
||||||
diagram.drawSVG(el, {theme: 'simple'})
|
const diagram = SequenceDiagram.parse(
|
||||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
htmlTextHelper.decodeEntities(el.innerHTML)
|
||||||
el.addEventListener('click', this.linkClickHandler)
|
)
|
||||||
})
|
el.innerHTML = ''
|
||||||
} catch (e) {
|
diagram.drawSVG(el, { theme: 'simple' })
|
||||||
console.error(e)
|
_.forEach(el.querySelectorAll('a'), el => {
|
||||||
el.className = 'sequence-error'
|
el.addEventListener('click', this.linkClickHandler)
|
||||||
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
})
|
||||||
|
} catch (e) {
|
||||||
|
el.className = 'sequence-error'
|
||||||
|
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
_.forEach(
|
_.forEach(
|
||||||
this.refs.root.contentWindow.document.querySelectorAll('.chart'),
|
this.refs.root.contentWindow.document.querySelectorAll('.chart'),
|
||||||
(el) => {
|
el => {
|
||||||
try {
|
try {
|
||||||
const chartConfig = JSON.parse(el.innerHTML)
|
const format = el.attributes.getNamedItem('data-format').value
|
||||||
|
const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
|
||||||
el.innerHTML = ''
|
el.innerHTML = ''
|
||||||
var canvas = document.createElement('canvas')
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
el.appendChild(canvas)
|
el.appendChild(canvas)
|
||||||
/* eslint-disable no-new */
|
|
||||||
new Chart(canvas, chartConfig)
|
const height = el.attributes.getNamedItem('data-height')
|
||||||
|
if (height && height.value !== 'undefined') {
|
||||||
|
el.style.height = height.value + 'vh'
|
||||||
|
canvas.height = height.value + 'vh'
|
||||||
|
}
|
||||||
|
|
||||||
|
const chart = new Chart(canvas, chartConfig)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
|
||||||
el.className = 'chart-error'
|
el.className = 'chart-error'
|
||||||
el.innerHTML = 'chartjs diagram parse error: ' + e.message
|
el.innerHTML = 'chartjs diagram parse error: ' + e.message
|
||||||
}
|
}
|
||||||
@@ -555,10 +800,113 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
)
|
)
|
||||||
_.forEach(
|
_.forEach(
|
||||||
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
||||||
(el) => {
|
el => {
|
||||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML))
|
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_.forEach(
|
||||||
|
this.refs.root.contentWindow.document.querySelectorAll('.gallery'),
|
||||||
|
el => {
|
||||||
|
const images = el.innerHTML.split(/\n/g).filter(i => i.length > 0)
|
||||||
|
el.innerHTML = ''
|
||||||
|
|
||||||
|
const height = el.attributes.getNamedItem('data-height')
|
||||||
|
if (height && height.value !== 'undefined') {
|
||||||
|
el.style.height = height.value + 'vh'
|
||||||
|
}
|
||||||
|
|
||||||
|
let autoplay = el.attributes.getNamedItem('data-autoplay')
|
||||||
|
if (autoplay && autoplay.value !== 'undefined') {
|
||||||
|
autoplay = parseInt(autoplay.value, 10) || 0
|
||||||
|
} else {
|
||||||
|
autoplay = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Carousel
|
||||||
|
images={images}
|
||||||
|
autoplay={autoplay}
|
||||||
|
/>,
|
||||||
|
el
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
|
||||||
|
const rect = markdownPreviewIframe.getBoundingClientRect()
|
||||||
|
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
|
||||||
|
for (const img of imgList) {
|
||||||
|
img.onclick = () => {
|
||||||
|
const widthMagnification = document.body.clientWidth / img.width
|
||||||
|
const heightMagnification = document.body.clientHeight / img.height
|
||||||
|
const baseOnWidth = widthMagnification < heightMagnification
|
||||||
|
const magnification = baseOnWidth ? widthMagnification : heightMagnification
|
||||||
|
|
||||||
|
const zoomImgWidth = img.width * magnification
|
||||||
|
const zoomImgHeight = img.height * magnification
|
||||||
|
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
|
||||||
|
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
|
||||||
|
const originalImgTop = img.y + rect.top
|
||||||
|
const originalImgLeft = img.x + rect.left
|
||||||
|
const originalImgRect = {
|
||||||
|
top: `${originalImgTop}px`,
|
||||||
|
left: `${originalImgLeft}px`,
|
||||||
|
width: `${img.width}px`,
|
||||||
|
height: `${img.height}px`
|
||||||
|
}
|
||||||
|
const zoomInImgRect = {
|
||||||
|
top: `${baseOnWidth ? zoomImgTop : 0}px`,
|
||||||
|
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
|
||||||
|
width: `${zoomImgWidth}px`,
|
||||||
|
height: `${zoomImgHeight}px`
|
||||||
|
}
|
||||||
|
const animationSpeed = 300
|
||||||
|
|
||||||
|
const zoomImg = document.createElement('img')
|
||||||
|
zoomImg.src = img.src
|
||||||
|
zoomImg.style = `
|
||||||
|
position: absolute;
|
||||||
|
top: ${baseOnWidth ? zoomImgTop : 0}px;
|
||||||
|
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
|
||||||
|
width: ${zoomImgWidth};
|
||||||
|
height: ${zoomImgHeight}px;
|
||||||
|
`
|
||||||
|
zoomImg.animate([
|
||||||
|
originalImgRect,
|
||||||
|
zoomInImgRect
|
||||||
|
], animationSpeed)
|
||||||
|
|
||||||
|
const overlay = document.createElement('div')
|
||||||
|
overlay.style = `
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
cursor: zoom-out;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: ${document.body.clientHeight}px;
|
||||||
|
z-index: 100;
|
||||||
|
`
|
||||||
|
overlay.onclick = () => {
|
||||||
|
zoomImg.style = `
|
||||||
|
position: absolute;
|
||||||
|
top: ${originalImgTop}px;
|
||||||
|
left: ${originalImgLeft}px;
|
||||||
|
width: ${img.width}px;
|
||||||
|
height: ${img.height}px;
|
||||||
|
`
|
||||||
|
const zoomOutImgAnimation = zoomImg.animate([
|
||||||
|
zoomInImgRect,
|
||||||
|
originalImgRect
|
||||||
|
], animationSpeed)
|
||||||
|
zoomOutImgAnimation.onfinish = () => overlay.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.appendChild(zoomImg)
|
||||||
|
document.body.appendChild(overlay)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
@@ -570,7 +918,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollTo (targetRow) {
|
scrollTo (targetRow) {
|
||||||
const blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
|
const blocks = this.getWindow().document.querySelectorAll(
|
||||||
|
'body>[data-line]'
|
||||||
|
)
|
||||||
|
|
||||||
for (let index = 0; index < blocks.length; index++) {
|
for (let index = 0; index < blocks.length; index++) {
|
||||||
let block = blocks[index]
|
let block = blocks[index]
|
||||||
@@ -590,12 +940,16 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
notify (title, options) {
|
notify (title, options) {
|
||||||
if (global.process.platform === 'win32') {
|
if (global.process.platform === 'win32') {
|
||||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
options.icon = path.join(
|
||||||
|
'file://',
|
||||||
|
global.__dirname,
|
||||||
|
'../../resources/app.png'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return new window.Notification(title, options)
|
return new window.Notification(title, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
handlelinkClick (e) {
|
handleLinkClick (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
@@ -605,7 +959,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
const regexNoteInternalLink = /main.html#(.+)/
|
const regexNoteInternalLink = /main.html#(.+)/
|
||||||
if (regexNoteInternalLink.test(linkHash)) {
|
if (regexNoteInternalLink.test(linkHash)) {
|
||||||
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
|
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
|
||||||
const targetElement = this.refs.root.contentWindow.document.getElementById(targetId)
|
const targetElement = this.refs.root.contentWindow.document.getElementById(
|
||||||
|
targetId
|
||||||
|
)
|
||||||
|
|
||||||
if (targetElement != null) {
|
if (targetElement != null) {
|
||||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
||||||
@@ -623,6 +979,15 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const regexIsLine = /^:line:[0-9]/
|
||||||
|
if (regexIsLine.test(linkHash)) {
|
||||||
|
const numberPattern = /\d+/g
|
||||||
|
|
||||||
|
const lineNumber = parseInt(linkHash.match(numberPattern)[0])
|
||||||
|
eventEmitter.emit('line:jump', lineNumber)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// this will match the old link format storage.key-note.key
|
// this will match the old link format storage.key-note.key
|
||||||
// e.g.
|
// e.g.
|
||||||
// 877f99c3268608328037-1c211eb7dcb463de6490
|
// 877f99c3268608328037-1c211eb7dcb463de6490
|
||||||
@@ -639,9 +1004,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
const { className, style, tabIndex } = this.props
|
const { className, style, tabIndex } = this.props
|
||||||
return (
|
return (
|
||||||
<iframe className={className != null
|
<iframe
|
||||||
? 'MarkdownPreview ' + className
|
className={
|
||||||
: 'MarkdownPreview'
|
className != null ? 'MarkdownPreview ' + className : 'MarkdownPreview'
|
||||||
}
|
}
|
||||||
style={style}
|
style={style}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
|||||||
@@ -20,12 +20,18 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOnChange () {
|
setValue (value) {
|
||||||
|
this.refs.code.setValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnChange (e) {
|
||||||
this.value = this.refs.code.value
|
this.value = this.refs.code.value
|
||||||
this.props.onChange()
|
this.props.onChange(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleScroll (e) {
|
handleScroll (e) {
|
||||||
|
if (!this.props.config.preview.scrollSync) return
|
||||||
|
|
||||||
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
|
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
|
||||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||||
let srcTop, srcHeight, targetTop, targetHeight
|
let srcTop, srcHeight, targetTop, targetHeight
|
||||||
@@ -72,8 +78,10 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const idMatch = /checkbox-([0-9]+)/
|
const idMatch = /checkbox-([0-9]+)/
|
||||||
const checkedMatch = /\[x\]/i
|
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||||
const uncheckedMatch = /\[ \]/
|
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||||
|
const checkReplace = /\[x\]/i
|
||||||
|
const uncheckReplace = /\[ \]/
|
||||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
const lines = this.refs.code.value
|
const lines = this.refs.code.value
|
||||||
@@ -82,10 +90,10 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
const targetLine = lines[lineIndex]
|
const targetLine = lines[lineIndex]
|
||||||
|
|
||||||
if (targetLine.match(checkedMatch)) {
|
if (targetLine.match(checkedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
|
||||||
}
|
}
|
||||||
if (targetLine.match(uncheckedMatch)) {
|
if (targetLine.match(uncheckedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
|
||||||
}
|
}
|
||||||
this.refs.code.setValue(lines.join('\n'))
|
this.refs.code.setValue(lines.join('\n'))
|
||||||
}
|
}
|
||||||
@@ -128,7 +136,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {config, value, storageKey, noteKey} = this.props
|
const {config, value, storageKey, noteKey, linesHighlighted} = this.props
|
||||||
const storage = findStorage(storageKey)
|
const storage = findStorage(storageKey)
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
@@ -145,23 +153,32 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
styleName='codeEditor'
|
styleName='codeEditor'
|
||||||
ref='code'
|
ref='code'
|
||||||
width={this.state.codeEditorWidthInPercent + '%'}
|
width={this.state.codeEditorWidthInPercent + '%'}
|
||||||
mode='GitHub Flavored Markdown'
|
mode='Boost Flavored Markdown'
|
||||||
value={value}
|
value={value}
|
||||||
theme={config.editor.theme}
|
theme={config.editor.theme}
|
||||||
keyMap={config.editor.keyMap}
|
keyMap={config.editor.keyMap}
|
||||||
fontFamily={config.editor.fontFamily}
|
fontFamily={config.editor.fontFamily}
|
||||||
fontSize={editorFontSize}
|
fontSize={editorFontSize}
|
||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
matchingPairs={config.editor.matchingPairs}
|
||||||
|
matchingTriples={config.editor.matchingTriples}
|
||||||
|
explodingPairs={config.editor.explodingPairs}
|
||||||
indentType={config.editor.indentType}
|
indentType={config.editor.indentType}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
enableRulers={config.editor.enableRulers}
|
enableRulers={config.editor.enableRulers}
|
||||||
rulers={config.editor.rulers}
|
rulers={config.editor.rulers}
|
||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
|
enableTableEditor={config.editor.enableTableEditor}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
noteKey={noteKey}
|
noteKey={noteKey}
|
||||||
onChange={this.handleOnChange.bind(this)}
|
linesHighlighted={linesHighlighted}
|
||||||
|
onChange={(e) => this.handleOnChange(e)}
|
||||||
onScroll={this.handleScroll.bind(this)}
|
onScroll={this.handleScroll.bind(this)}
|
||||||
|
spellCheck={config.editor.spellcheck}
|
||||||
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
|
hotkey={config.hotkey}
|
||||||
|
switchPreview={config.editor.switchPreview}
|
||||||
/>
|
/>
|
||||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||||
<div styleName='slider-hitbox' />
|
<div styleName='slider-hitbox' />
|
||||||
@@ -191,6 +208,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
noteKey={noteKey}
|
noteKey={noteKey}
|
||||||
customCSS={config.preview.customCSS}
|
customCSS={config.preview.customCSS}
|
||||||
allowCustomCSS={config.preview.allowCustomCSS}
|
allowCustomCSS={config.preview.allowCustomCSS}
|
||||||
|
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { isArray } from 'lodash'
|
import { isArray } from 'lodash'
|
||||||
|
import invertColor from 'invert-color'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
||||||
import styles from './NoteItem.styl'
|
import styles from './NoteItem.styl'
|
||||||
@@ -13,29 +14,39 @@ import i18n from 'browser/lib/i18n'
|
|||||||
/**
|
/**
|
||||||
* @description Tag element component.
|
* @description Tag element component.
|
||||||
* @param {string} tagName
|
* @param {string} tagName
|
||||||
|
* @param {string} color
|
||||||
* @return {React.Component}
|
* @return {React.Component}
|
||||||
*/
|
*/
|
||||||
const TagElement = ({ tagName }) => (
|
const TagElement = ({ tagName, color }) => {
|
||||||
<span styleName='item-bottom-tagList-item' key={tagName}>
|
const style = {}
|
||||||
#{tagName}
|
if (color) {
|
||||||
</span>
|
style.backgroundColor = color
|
||||||
)
|
style.color = invertColor(color, { black: '#222', white: '#f1f1f1', threshold: 0.3 })
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span styleName='item-bottom-tagList-item' key={tagName} style={style}>
|
||||||
|
#{tagName}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Tag element list component.
|
* @description Tag element list component.
|
||||||
* @param {Array|null} tags
|
* @param {Array|null} tags
|
||||||
|
* @param {boolean} showTagsAlphabetically
|
||||||
|
* @param {Object} coloredTags
|
||||||
* @return {React.Component}
|
* @return {React.Component}
|
||||||
*/
|
*/
|
||||||
const TagElementList = (tags) => {
|
const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
|
||||||
if (!isArray(tags)) {
|
if (!isArray(tags)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagElements = tags.map(tag => (
|
if (showTagsAlphabetically) {
|
||||||
TagElement({tagName: tag})
|
return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
||||||
))
|
} else {
|
||||||
|
return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
||||||
return tagElements
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +56,7 @@ const TagElementList = (tags) => {
|
|||||||
* @param {Function} handleNoteClick
|
* @param {Function} handleNoteClick
|
||||||
* @param {Function} handleNoteContextMenu
|
* @param {Function} handleNoteContextMenu
|
||||||
* @param {Function} handleDragStart
|
* @param {Function} handleDragStart
|
||||||
|
* @param {Object} coloredTags
|
||||||
* @param {string} dateDisplay
|
* @param {string} dateDisplay
|
||||||
*/
|
*/
|
||||||
const NoteItem = ({
|
const NoteItem = ({
|
||||||
@@ -57,12 +69,12 @@ const NoteItem = ({
|
|||||||
pathname,
|
pathname,
|
||||||
storageName,
|
storageName,
|
||||||
folderName,
|
folderName,
|
||||||
viewType
|
viewType,
|
||||||
|
showTagsAlphabetically,
|
||||||
|
coloredTags
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName={isActive
|
<div
|
||||||
? 'item--active'
|
styleName={isActive ? 'item--active' : 'item'}
|
||||||
: 'item'
|
|
||||||
}
|
|
||||||
key={note.key}
|
key={note.key}
|
||||||
onClick={e => handleNoteClick(e, note.key)}
|
onClick={e => handleNoteClick(e, note.key)}
|
||||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||||
@@ -72,42 +84,52 @@ const NoteItem = ({
|
|||||||
<div styleName='item-wrapper'>
|
<div styleName='item-wrapper'>
|
||||||
{note.type === 'SNIPPET_NOTE'
|
{note.type === 'SNIPPET_NOTE'
|
||||||
? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
|
? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
|
||||||
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />
|
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
|
||||||
}
|
|
||||||
<div styleName='item-title'>
|
<div styleName='item-title'>
|
||||||
{note.title.trim().length > 0
|
{note.title.trim().length > 0
|
||||||
? note.title
|
? note.title
|
||||||
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>
|
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'>
|
<div styleName='item-middle'>
|
||||||
<div styleName='item-middle-time'>{dateDisplay}</div>
|
<div styleName='item-middle-time'>{dateDisplay}</div>
|
||||||
<div styleName='item-middle-app-meta'>
|
<div styleName='item-middle-app-meta'>
|
||||||
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'>
|
<div
|
||||||
|
title={
|
||||||
|
viewType === 'ALL'
|
||||||
|
? storageName
|
||||||
|
: viewType === 'STORAGE' ? folderName : null
|
||||||
|
}
|
||||||
|
styleName='item-middle-app-meta-label'
|
||||||
|
>
|
||||||
{viewType === 'ALL' && storageName}
|
{viewType === 'ALL' && storageName}
|
||||||
{viewType === 'STORAGE' && folderName}
|
{viewType === 'STORAGE' && folderName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>
|
||||||
|
|
||||||
<div styleName='item-bottom'>
|
<div styleName='item-bottom'>
|
||||||
<div styleName='item-bottom-tagList'>
|
<div styleName='item-bottom-tagList'>
|
||||||
{note.tags.length > 0
|
{note.tags.length > 0
|
||||||
? TagElementList(note.tags)
|
? TagElementList(note.tags, showTagsAlphabetically, coloredTags)
|
||||||
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>{i18n.__('No tags')}</span>
|
: <span
|
||||||
}
|
style={{ fontStyle: 'italic', opacity: 0.5 }}
|
||||||
|
styleName='item-bottom-tagList-empty'
|
||||||
|
>
|
||||||
|
{i18n.__('No tags')}
|
||||||
|
</span>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{note.isStarred
|
{note.isStarred
|
||||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
? <img
|
||||||
}
|
styleName='item-star'
|
||||||
|
src='../resources/icon/icon-starred.svg'
|
||||||
|
/>
|
||||||
|
: ''}
|
||||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||||
}
|
: ''}
|
||||||
{note.type === 'MARKDOWN_NOTE'
|
{note.type === 'MARKDOWN_NOTE'
|
||||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||||
: ''
|
: ''}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,6 +139,7 @@ const NoteItem = ({
|
|||||||
NoteItem.propTypes = {
|
NoteItem.propTypes = {
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
dateDisplay: PropTypes.string.isRequired,
|
dateDisplay: PropTypes.string.isRequired,
|
||||||
|
coloredTags: PropTypes.object,
|
||||||
note: PropTypes.shape({
|
note: PropTypes.shape({
|
||||||
storage: PropTypes.string.isRequired,
|
storage: PropTypes.string.isRequired,
|
||||||
key: PropTypes.string.isRequired,
|
key: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -368,13 +368,13 @@ body[data-theme="monokai"]
|
|||||||
.item-title
|
.item-title
|
||||||
.item-title-icon
|
.item-title-icon
|
||||||
.item-bottom-time
|
.item-bottom-time
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-active-color
|
||||||
.item-bottom-tagList-item
|
.item-bottom-tagList-item
|
||||||
background-color alpha(white, 10%)
|
background-color alpha(white, 10%)
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
&:hover
|
&:hover
|
||||||
// background-color alpha($ui-monokai-button--active-backgroundColor, 60%)
|
// background-color alpha($ui-monokai-button--active-backgroundColor, 60%)
|
||||||
color #c0392b
|
color #f92672
|
||||||
.item-bottom-tagList-item
|
.item-bottom-tagList-item
|
||||||
background-color alpha(#fff, 20%)
|
background-color alpha(#fff, 20%)
|
||||||
|
|
||||||
@@ -394,3 +394,76 @@ body[data-theme="monokai"]
|
|||||||
.item-bottom-tagList-empty
|
.item-bottom-tagList-empty
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
|
||||||
|
.item
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
&:hover
|
||||||
|
transition 0.15s
|
||||||
|
// background-color alpha($ui-dracula-noteList-backgroundColor, 20%)
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.item-title
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-time
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha($ui-dracula-noteList-backgroundColor, 20%)
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:active
|
||||||
|
transition 0.15s
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.item-title
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-time
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha($ui-dracula-noteList-backgroundColor, 10%)
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.item-wrapper
|
||||||
|
border-color alpha($ui-dracula-button-backgroundColor, 60%)
|
||||||
|
|
||||||
|
.item--active
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
.item-wrapper
|
||||||
|
border-color transparent
|
||||||
|
.item-title
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-time
|
||||||
|
color $ui-dracula-active-color
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
background-color alpha(#f8f8f2, 10%)
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:hover
|
||||||
|
// background-color alpha($ui-dracula-button--active-backgroundColor, 60%)
|
||||||
|
color #ff79c6
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
background-color alpha(#f8f8f2, 20%)
|
||||||
|
|
||||||
|
.item-title
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-title-icon
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-title-empty
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-bottom-tagList-empty
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
vertical-align middle
|
||||||
@@ -240,7 +240,7 @@ body[data-theme="monokai"]
|
|||||||
.item-simple-title-icon
|
.item-simple-title-icon
|
||||||
.item-simple-bottom-time
|
.item-simple-bottom-time
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-monokai-text-color
|
||||||
.item-simple-bottom-tagList-item
|
.item-simple-bottom-tagList-item
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
background-color alpha(#fff, 20%)
|
background-color alpha(#fff, 20%)
|
||||||
@@ -286,3 +286,67 @@ body[data-theme="monokai"]
|
|||||||
.item-simple-right-storageName
|
.item-simple-right-storageName
|
||||||
padding-left 4px
|
padding-left 4px
|
||||||
opacity 0.4
|
opacity 0.4
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
|
||||||
|
.item-simple
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
&:hover
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha($ui-dracula-button-backgroundColor, 60%)
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.item-simple-title
|
||||||
|
.item-simple-title-empty
|
||||||
|
.item-simple-title-icon
|
||||||
|
.item-simple-bottom-time
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.item-simple-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha(#f8f8f2, 20%)
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:active
|
||||||
|
transition 0.15s
|
||||||
|
background-color $ui-dracula-button--active-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.item-simple-title
|
||||||
|
.item-simple-title-empty
|
||||||
|
.item-simple-title-icon
|
||||||
|
.item-simple-bottom-time
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.item-simple-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha(#f8f8f2, 10%)
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.item-simple--active
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-button--active-backgroundColor
|
||||||
|
.item-simple-wrapper
|
||||||
|
border-color transparent
|
||||||
|
.item-simple-title
|
||||||
|
.item-simple-title-empty
|
||||||
|
.item-simple-title-icon
|
||||||
|
.item-simple-bottom-time
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.item-simple-bottom-tagList-item
|
||||||
|
background-color alpha(#f8f8f2, 10%)
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:hover
|
||||||
|
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||||
|
color #c0392b
|
||||||
|
.item-simple-bottom-tagList-item
|
||||||
|
background-color alpha(#f8f8f2, 20%)
|
||||||
|
.item-simple-title
|
||||||
|
color $ui-dark-text-color
|
||||||
|
border-bottom $ui-dark-borderColor
|
||||||
|
.item-simple-right
|
||||||
|
float right
|
||||||
|
.item-simple-right-storageName
|
||||||
|
padding-left 4px
|
||||||
|
opacity 0.4
|
||||||
@@ -51,4 +51,15 @@ body[data-theme="monokai"]
|
|||||||
border none
|
border none
|
||||||
background-color $ui-monokai-button-backgroundColor
|
background-color $ui-monokai-button-backgroundColor
|
||||||
&:hover
|
&:hover
|
||||||
color #5CB85C
|
color #5CB85C
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.notification-area
|
||||||
|
background-color none
|
||||||
|
|
||||||
|
.notification-link
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
border none
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
&:hover
|
||||||
|
color #ff79c6
|
||||||
@@ -263,4 +263,46 @@ body[data-theme="monokai"]
|
|||||||
background-color $ui-monokai-button-backgroundColor
|
background-color $ui-monokai-button-backgroundColor
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.menu-button
|
||||||
|
&:active
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.menu-button--active
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.menu-button-star--active
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.menu-button-trash--active
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-dracula-text-color
|
||||||
@@ -3,19 +3,30 @@
|
|||||||
flex 1
|
flex 1
|
||||||
min-width 70px
|
min-width 70px
|
||||||
overflow hidden
|
overflow hidden
|
||||||
|
border-left 1px solid $ui-borderColor
|
||||||
|
border-top 1px solid $ui-borderColor
|
||||||
&:hover
|
&:hover
|
||||||
|
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||||
.deleteButton
|
.deleteButton
|
||||||
color $ui-inactive-text-color
|
color: $ui-text-color
|
||||||
&:hover
|
visibility visible
|
||||||
background-color darken($ui-backgroundColor, 15%)
|
transition 0.15s
|
||||||
&:active
|
.button
|
||||||
color white
|
color: $ui-text-color
|
||||||
background-color $ui-active-color
|
transition 0.15s
|
||||||
|
|
||||||
.root--active
|
.root--active
|
||||||
@extend .root
|
@extend .root
|
||||||
min-width 100px
|
min-width 100px
|
||||||
border-bottom $ui-border
|
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||||
|
.deleteButton
|
||||||
|
visibility visible
|
||||||
|
color: $ui-text-color
|
||||||
|
transition 0.15s
|
||||||
|
.button
|
||||||
|
font-weight bold
|
||||||
|
color: $ui-text-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
.button
|
.button
|
||||||
width 100%
|
width 100%
|
||||||
@@ -27,8 +38,7 @@
|
|||||||
background-color transparent
|
background-color transparent
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
border-left 4px solid transparent
|
border-left 4px solid transparent
|
||||||
&:hover
|
color $ui-inactive-text-color
|
||||||
background-color $ui-button--hover-backgroundColor
|
|
||||||
|
|
||||||
.deleteButton
|
.deleteButton
|
||||||
position absolute
|
position absolute
|
||||||
@@ -42,6 +52,7 @@
|
|||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
background-color transparent
|
background-color transparent
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
|
visibility hidden
|
||||||
|
|
||||||
.input
|
.input
|
||||||
height 29px
|
height 29px
|
||||||
@@ -50,76 +61,66 @@
|
|||||||
width 100%
|
width 100%
|
||||||
outline none
|
outline none
|
||||||
|
|
||||||
|
body[data-theme="default"], body[data-theme="white"]
|
||||||
|
.root--active
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
color $ui-dark-text-color
|
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
|
border-top 1px solid $ui-dark-borderColor
|
||||||
&:hover
|
&:hover
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
transition 0.15s
|
||||||
|
.button
|
||||||
|
color $ui-dark-text-color
|
||||||
|
transition 0.15s
|
||||||
.deleteButton
|
.deleteButton
|
||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-text-color
|
||||||
&:hover
|
transition 0.15s
|
||||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
|
||||||
&:active
|
|
||||||
color $ui-dark-text-color
|
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
|
||||||
.root--active
|
.root--active
|
||||||
color $ui-dark-text-color
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
border-color $ui-dark-borderColor
|
border-left 1px solid $ui-dark-borderColor
|
||||||
&:hover
|
border-top 1px solid $ui-dark-borderColor
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
.button
|
||||||
.deleteButton
|
color $ui-dark-text-color
|
||||||
color $ui-dark-inactive-text-color
|
.deleteButton
|
||||||
&:hover
|
color $ui-dark-text-color
|
||||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
|
||||||
&:active
|
|
||||||
color $ui-dark-text-color
|
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
|
||||||
.button
|
.button
|
||||||
border none
|
border none
|
||||||
color $ui-dark-text-color
|
|
||||||
background-color transparent
|
background-color transparent
|
||||||
transition color background-color 0.15s
|
transition color background-color 0.15s
|
||||||
border-left 4px solid transparent
|
border-left 4px solid transparent
|
||||||
&:hover
|
|
||||||
color $ui-dark-text-color
|
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
|
||||||
|
|
||||||
.input
|
.input
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
transition 0.15s
|
||||||
.deleteButton
|
|
||||||
color alpha($ui-dark-text-color, 30%)
|
|
||||||
|
|
||||||
body[data-theme="solarized-dark"]
|
body[data-theme="solarized-dark"]
|
||||||
.root
|
.root
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
border-color $ui-dark-borderColor
|
|
||||||
&:hover
|
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
|
||||||
.deleteButton
|
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
&:hover
|
|
||||||
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
|
|
||||||
&:active
|
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
|
||||||
.root--active
|
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
border-color $ui-solarized-dark-borderColor
|
border-color $ui-solarized-dark-borderColor
|
||||||
&:hover
|
&:hover
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
transition 0.15s
|
||||||
.deleteButton
|
.deleteButton
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-button--active-color
|
||||||
&:hover
|
transition 0.15s
|
||||||
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
|
.button
|
||||||
&:active
|
color $ui-solarized-dark-button--active-color
|
||||||
color $ui-solarized-dark-text-color
|
transition 0.15s
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
|
||||||
|
.root--active
|
||||||
|
color $ui-solarized-dark-button--active-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
.deleteButton
|
||||||
|
color $ui-solarized-dark-button--active-color
|
||||||
|
.button
|
||||||
|
color $ui-solarized-dark-button--active-color
|
||||||
|
|
||||||
.button
|
.button
|
||||||
border none
|
border none
|
||||||
@@ -127,13 +128,75 @@ body[data-theme="solarized-dark"]
|
|||||||
background-color transparent
|
background-color transparent
|
||||||
transition color background-color 0.15s
|
transition color background-color 0.15s
|
||||||
border-left 4px solid transparent
|
border-left 4px solid transparent
|
||||||
&:hover
|
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
|
||||||
|
|
||||||
.input
|
.input
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-button--active-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
.deleteButton
|
body[data-theme="monokai"]
|
||||||
color alpha($ui-solarized-dark-text-color, 30%)
|
.root
|
||||||
|
border-color $ui-monokai-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
transition 0.15s
|
||||||
|
.deleteButton
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
transition 0.15s
|
||||||
|
.button
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
|
.root--active
|
||||||
|
color $ui-monokai-active-color
|
||||||
|
background-color $ui-monokai-button-backgroundColor
|
||||||
|
border-color $ui-monokai-borderColor
|
||||||
|
.deleteButton
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
.button
|
||||||
|
color $ui-monokai-active-color
|
||||||
|
|
||||||
|
.button
|
||||||
|
border none
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
background-color transparent
|
||||||
|
transition color background-color 0.15s
|
||||||
|
border-left 4px solid transparent
|
||||||
|
|
||||||
|
.input
|
||||||
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
transition 0.15s
|
||||||
|
.deleteButton
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
transition 0.15s
|
||||||
|
.button
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
|
.root--active
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
.deleteButton
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
.button
|
||||||
|
color $ui-dracula-active-color
|
||||||
|
|
||||||
|
.button
|
||||||
|
border none
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
background-color transparent
|
||||||
|
transition color background-color 0.15s
|
||||||
|
border-left 4px solid transparent
|
||||||
|
|
||||||
|
.input
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
@@ -54,9 +54,8 @@ const StorageItem = ({
|
|||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
>
|
>
|
||||||
{!isFolded && (
|
{!isFolded &&
|
||||||
<DraggableIcon className={styles['folderList-item-reorder']} />
|
<DraggableIcon className={styles['folderList-item-reorder']} />}
|
||||||
)}
|
|
||||||
<span
|
<span
|
||||||
styleName={
|
styleName={
|
||||||
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||||
@@ -72,12 +71,10 @@ const StorageItem = ({
|
|||||||
: folderName}
|
: folderName}
|
||||||
</span>
|
</span>
|
||||||
{!isFolded &&
|
{!isFolded &&
|
||||||
_.isNumber(noteCount) && (
|
_.isNumber(noteCount) &&
|
||||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
<span styleName='folderList-item-noteCount'>{noteCount}</span>}
|
||||||
)}
|
{isFolded &&
|
||||||
{isFolded && (
|
<span styleName='folderList-item-tooltip'>{folderName}</span>}
|
||||||
<span styleName='folderList-item-tooltip'>{folderName}</span>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,4 +156,23 @@ body[data-theme="monokai"]
|
|||||||
background-color $ui-monokai-button-backgroundColor
|
background-color $ui-monokai-button-backgroundColor
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
background-color $ui-monokai-button-backgroundColor
|
background-color $ui-monokai-button-backgroundColor
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.folderList-item
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:active
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
|
||||||
|
.folderList-item--active
|
||||||
|
@extend .folderList-item
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
&:active
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
&:hover
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
@@ -7,18 +7,18 @@ import styles from './StorageList.styl'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Array} storgaeList
|
* @param {Array} storageList
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const StorageList = ({storageList, isFolded}) => (
|
const StorageList = ({storageList, isFolded}) => (
|
||||||
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
|
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
|
||||||
{storageList.length > 0 ? storageList : (
|
{storageList.length > 0 ? storageList : (
|
||||||
<div styleName='storgaeList-empty'>No storage mount.</div>
|
<div styleName='storageList-empty'>No storage mount.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
StorageList.propTypes = {
|
StorageList.propTypes = {
|
||||||
storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired
|
storageList: PropTypes.arrayOf(PropTypes.element).isRequired
|
||||||
}
|
}
|
||||||
export default CSSModules(StorageList, styles)
|
export default CSSModules(StorageList, styles)
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {Function} handleClickTagListItem
|
* @param {Function} handleClickTagListItem
|
||||||
* @param {Function} handleClickNarrowToTag
|
* @param {Function} handleClickNarrowToTag
|
||||||
* @param {bool} isActive
|
* @param {boolean} isActive
|
||||||
* @param {bool} isRelated
|
* @param {boolean} isRelated
|
||||||
|
* @param {string} bgColor tab backgroundColor
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
|
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count, color}) => (
|
||||||
<div styleName='tagList-itemContainer'>
|
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}>
|
||||||
{isRelated
|
{isRelated
|
||||||
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
||||||
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
|
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
|
||||||
@@ -23,9 +24,10 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isAc
|
|||||||
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
|
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
|
||||||
}
|
}
|
||||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||||
|
<span styleName='tagList-item-color' style={{backgroundColor: color || 'transparent'}} />
|
||||||
<span styleName='tagList-item-name'>
|
<span styleName='tagList-item-name'>
|
||||||
{`# ${name}`}
|
{`# ${name}`}
|
||||||
<span styleName='tagList-item-count'>{count}</span>
|
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +35,8 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isAc
|
|||||||
|
|
||||||
TagListItem.propTypes = {
|
TagListItem.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
handleClickTagListItem: PropTypes.func.isRequired
|
handleClickTagListItem: PropTypes.func.isRequired,
|
||||||
|
color: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(TagListItem, styles)
|
export default CSSModules(TagListItem, styles)
|
||||||
|
|||||||
@@ -71,6 +71,11 @@
|
|||||||
padding-right 15px
|
padding-right 15px
|
||||||
font-size 13px
|
font-size 13px
|
||||||
|
|
||||||
|
.tagList-item-color
|
||||||
|
height 26px
|
||||||
|
width 3px
|
||||||
|
display inline-block
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.tagList-item
|
.tagList-item
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import styles from './TodoListPercentage.styl'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const TodoListPercentage = ({
|
const TodoListPercentage = ({
|
||||||
percentageOfTodo
|
percentageOfTodo, onClearCheckboxClick
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
||||||
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
||||||
@@ -20,11 +20,15 @@ const TodoListPercentage = ({
|
|||||||
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div styleName='todoClear'>
|
||||||
|
<p styleName='todoClearText' onClick={(e) => onClearCheckboxClick(e)}>clear</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
TodoListPercentage.propTypes = {
|
TodoListPercentage.propTypes = {
|
||||||
percentageOfTodo: PropTypes.number.isRequired
|
percentageOfTodo: PropTypes.number.isRequired,
|
||||||
|
onClearCheckboxClick: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(TodoListPercentage, styles)
|
export default CSSModules(TodoListPercentage, styles)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.percentageBar
|
.percentageBar
|
||||||
|
display: flex
|
||||||
position absolute
|
position absolute
|
||||||
top 72px
|
top 72px
|
||||||
right 0px
|
right 0px
|
||||||
@@ -30,6 +31,20 @@
|
|||||||
color #f4f4f4
|
color #f4f4f4
|
||||||
font-weight 600
|
font-weight 600
|
||||||
|
|
||||||
|
.todoClear
|
||||||
|
display flex
|
||||||
|
justify-content: flex-end
|
||||||
|
position absolute
|
||||||
|
z-index 120
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
padding 2px 10px
|
||||||
|
|
||||||
|
.todoClearText
|
||||||
|
color #f4f4f4
|
||||||
|
cursor pointer
|
||||||
|
font-weight 500
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.percentageBar
|
.percentageBar
|
||||||
background-color #444444
|
background-color #444444
|
||||||
@@ -39,7 +54,10 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.percentageText
|
.percentageText
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.todoClearText
|
||||||
|
color $ui-dark-text-color
|
||||||
|
|
||||||
body[data-theme="solarized-dark"]
|
body[data-theme="solarized-dark"]
|
||||||
.percentageBar
|
.percentageBar
|
||||||
background-color #002b36
|
background-color #002b36
|
||||||
@@ -50,12 +68,28 @@ body[data-theme="solarized-dark"]
|
|||||||
.percentageText
|
.percentageText
|
||||||
color #fdf6e3
|
color #fdf6e3
|
||||||
|
|
||||||
|
.todoClearText
|
||||||
|
color #fdf6e3
|
||||||
|
|
||||||
body[data-theme="monokai"]
|
body[data-theme="monokai"]
|
||||||
.percentageBar
|
.percentageBar
|
||||||
background-color #f92672
|
background-color: $ui-monokai-borderColor
|
||||||
|
|
||||||
.progressBar
|
.progressBar
|
||||||
background-color: #373831
|
background-color $ui-monokai-active-color
|
||||||
|
|
||||||
.percentageText
|
.percentageText
|
||||||
color #fdf6e3
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.percentageBar
|
||||||
|
background-color $ui-dracula-borderColor
|
||||||
|
|
||||||
|
.progressBar
|
||||||
|
background-color: $ui-dracula-active-color
|
||||||
|
|
||||||
|
.percentageText
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.percentageText
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|||||||
@@ -55,11 +55,14 @@ body
|
|||||||
line-height 1.6
|
line-height 1.6
|
||||||
overflow-x hidden
|
overflow-x hidden
|
||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
|
// do not allow display line breaks
|
||||||
|
.katex-display > .katex
|
||||||
|
white-space nowrap
|
||||||
|
// allow inline line breaks
|
||||||
.katex
|
.katex
|
||||||
font 400 1.2em 'KaTeX_Main'
|
|
||||||
line-height 1.2em
|
|
||||||
white-space initial
|
white-space initial
|
||||||
text-indent 0
|
.katex .katex-html
|
||||||
|
display inline-flex
|
||||||
.katex .mfrac>.vlist>span:nth-child(2)
|
.katex .mfrac>.vlist>span:nth-child(2)
|
||||||
top 0 !important
|
top 0 !important
|
||||||
.katex-error
|
.katex-error
|
||||||
@@ -80,6 +83,9 @@ li
|
|||||||
&.checked
|
&.checked
|
||||||
text-decoration line-through
|
text-decoration line-through
|
||||||
opacity 0.5
|
opacity 0.5
|
||||||
|
&.taskListItem.checked
|
||||||
|
text-decoration line-through
|
||||||
|
opacity 0.5
|
||||||
div.math-rendered
|
div.math-rendered
|
||||||
text-align center
|
text-align center
|
||||||
.math-failed
|
.math-failed
|
||||||
@@ -159,6 +165,7 @@ p
|
|||||||
white-space pre-line
|
white-space pre-line
|
||||||
word-wrap break-word
|
word-wrap break-word
|
||||||
img
|
img
|
||||||
|
cursor zoom-in
|
||||||
max-width 100%
|
max-width 100%
|
||||||
strong, b
|
strong, b
|
||||||
font-weight bold
|
font-weight bold
|
||||||
@@ -180,6 +187,10 @@ ul
|
|||||||
display list-item
|
display list-item
|
||||||
&.taskListItem
|
&.taskListItem
|
||||||
list-style none
|
list-style none
|
||||||
|
&>input
|
||||||
|
margin-left -1.6em
|
||||||
|
&>p
|
||||||
|
margin-left -1.8em
|
||||||
p
|
p
|
||||||
margin 0
|
margin 0
|
||||||
&>li>ul, &>li>ol
|
&>li>ul, &>li>ol
|
||||||
@@ -206,41 +217,39 @@ code
|
|||||||
text-decoration none
|
text-decoration none
|
||||||
margin-right 2px
|
margin-right 2px
|
||||||
pre
|
pre
|
||||||
padding 0.5em !important
|
padding 0.5rem !important
|
||||||
border solid 1px #D1D1D1
|
border solid 1px #D1D1D1
|
||||||
border-radius 5px
|
border-radius 5px
|
||||||
overflow-x auto
|
overflow-x auto
|
||||||
margin 0 0 1em
|
margin 0 0 1rem
|
||||||
display flex
|
display flex
|
||||||
line-height 1.4em
|
line-height 1.4em
|
||||||
&.flowchart, &.sequence, &.chart
|
|
||||||
display flex
|
|
||||||
justify-content center
|
|
||||||
background-color white
|
|
||||||
&.CodeMirror
|
|
||||||
height initial
|
|
||||||
flex-wrap wrap
|
|
||||||
&>code
|
|
||||||
flex 1
|
|
||||||
overflow-x auto
|
|
||||||
code
|
code
|
||||||
background-color inherit
|
background-color inherit
|
||||||
margin 0
|
margin 0
|
||||||
padding 0
|
padding 0
|
||||||
border none
|
border none
|
||||||
border-radius 0
|
border-radius 0
|
||||||
|
&.CodeMirror
|
||||||
|
height initial
|
||||||
|
flex-wrap wrap
|
||||||
|
&>code
|
||||||
|
flex 1
|
||||||
|
overflow-x auto
|
||||||
|
&.mermaid svg
|
||||||
|
max-width 100% !important
|
||||||
&>span.filename
|
&>span.filename
|
||||||
width 100%
|
margin -0.5rem 100% 0.5rem -0.5rem
|
||||||
border-radius: 5px 0px 0px 0px
|
padding 0.125rem 0.375rem
|
||||||
margin -8px 100% 8px -8px
|
|
||||||
padding 0px 6px
|
|
||||||
background-color #777;
|
background-color #777;
|
||||||
color white
|
color white
|
||||||
|
&:empty
|
||||||
|
display none
|
||||||
&>span.lineNumber
|
&>span.lineNumber
|
||||||
display none
|
display none
|
||||||
font-size 1em
|
font-size 1em
|
||||||
padding 0.5em 0
|
padding 0.5rem 0
|
||||||
margin -0.5em 0.5em -0.5em -0.5em
|
margin -0.5rem 0.5rem -0.5rem -0.5rem
|
||||||
border-right 1px solid
|
border-right 1px solid
|
||||||
text-align right
|
text-align right
|
||||||
border-top-left-radius 4px
|
border-top-left-radius 4px
|
||||||
@@ -257,6 +266,7 @@ table
|
|||||||
display block
|
display block
|
||||||
width 100%
|
width 100%
|
||||||
margin 0 0 1em
|
margin 0 0 1em
|
||||||
|
overflow-x auto
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
background-color tableHeadBgColor
|
background-color tableHeadBgColor
|
||||||
@@ -360,7 +370,7 @@ for name, val in admonition_types
|
|||||||
.admonition.{name}
|
.admonition.{name}
|
||||||
@extend $admonition
|
@extend $admonition
|
||||||
border-left-color: val[color]
|
border-left-color: val[color]
|
||||||
|
|
||||||
.admonition.{name}>.admonition-title
|
.admonition.{name}>.admonition-title
|
||||||
@extend $admonition-title
|
@extend $admonition-title
|
||||||
border-bottom-color: .1rem solid rgba(val[color], 0.2)
|
border-bottom-color: .1rem solid rgba(val[color], 0.2)
|
||||||
@@ -371,6 +381,69 @@ for name, val in admonition_types
|
|||||||
color: val[color]
|
color: val[color]
|
||||||
content: val[icon]
|
content: val[icon]
|
||||||
|
|
||||||
|
dl
|
||||||
|
margin 2rem 0
|
||||||
|
padding 0
|
||||||
|
display flex
|
||||||
|
width 100%
|
||||||
|
flex-wrap wrap
|
||||||
|
align-items flex-start
|
||||||
|
border-bottom 1px solid borderColor
|
||||||
|
background-color tableHeadBgColor
|
||||||
|
|
||||||
|
dt
|
||||||
|
border-top 1px solid borderColor
|
||||||
|
font-weight bold
|
||||||
|
text-align right
|
||||||
|
overflow hidden
|
||||||
|
flex-basis 20%
|
||||||
|
padding 0.4rem 0.9rem
|
||||||
|
box-sizing border-box
|
||||||
|
|
||||||
|
dd
|
||||||
|
border-top 1px solid borderColor
|
||||||
|
flex-basis 80%
|
||||||
|
padding 0.4rem 0.9rem
|
||||||
|
min-height 2.5rem
|
||||||
|
background-color $ui-noteDetail-backgroundColor
|
||||||
|
box-sizing border-box
|
||||||
|
|
||||||
|
dd + dd
|
||||||
|
margin-left 20%
|
||||||
|
|
||||||
|
pre.fence
|
||||||
|
flex-wrap wrap
|
||||||
|
|
||||||
|
.chart, .flowchart, .mermaid, .sequence
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
background-color white
|
||||||
|
max-width 100%
|
||||||
|
flex-grow 1
|
||||||
|
|
||||||
|
canvas, svg
|
||||||
|
max-width 100% !important
|
||||||
|
|
||||||
|
.gallery
|
||||||
|
width 100%
|
||||||
|
height 50vh
|
||||||
|
|
||||||
|
.carousel
|
||||||
|
.carousel-main img
|
||||||
|
min-width auto
|
||||||
|
max-width 100%
|
||||||
|
min-height auto
|
||||||
|
max-height 100%
|
||||||
|
|
||||||
|
.carousel-footer::-webkit-scrollbar-corner
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
.carousel-main, .carousel-footer
|
||||||
|
background-color $ui-noteDetail-backgroundColor
|
||||||
|
.prev, .next
|
||||||
|
color $ui-text-color
|
||||||
|
background-color $ui-tag-backgroundColor
|
||||||
|
|
||||||
themeDarkBackground = darken(#21252B, 10%)
|
themeDarkBackground = darken(#21252B, 10%)
|
||||||
themeDarkText = #f9f9f9
|
themeDarkText = #f9f9f9
|
||||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||||
@@ -421,6 +494,22 @@ body[data-theme="dark"]
|
|||||||
kbd
|
kbd
|
||||||
background-color themeDarkBorder
|
background-color themeDarkBorder
|
||||||
color themeDarkText
|
color themeDarkText
|
||||||
|
dl
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color themeDarkTableHead
|
||||||
|
dt
|
||||||
|
border-color themeDarkBorder
|
||||||
|
dd
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color themeDarkPreview
|
||||||
|
|
||||||
|
pre.fence
|
||||||
|
.gallery
|
||||||
|
.carousel-main, .carousel-footer
|
||||||
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
|
.prev, .next
|
||||||
|
color $ui-dark-text-color
|
||||||
|
background-color $ui-dark-tag-backgroundColor
|
||||||
|
|
||||||
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
||||||
@@ -448,6 +537,22 @@ body[data-theme="solarized-dark"]
|
|||||||
border-color themeSolarizedDarkTableBorder
|
border-color themeSolarizedDarkTableBorder
|
||||||
&:last-child
|
&:last-child
|
||||||
border-right solid 1px themeSolarizedDarkTableBorder
|
border-right solid 1px themeSolarizedDarkTableBorder
|
||||||
|
dl
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color themeSolarizedDarkTableHead
|
||||||
|
dt
|
||||||
|
border-color themeDarkBorder
|
||||||
|
dd
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
pre.fence
|
||||||
|
.gallery
|
||||||
|
.carousel-main, .carousel-footer
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
.prev, .next
|
||||||
|
color $ui-solarized-dark-button--active-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
|
||||||
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
|
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
|
||||||
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
||||||
@@ -476,4 +581,67 @@ body[data-theme="monokai"]
|
|||||||
&:last-child
|
&:last-child
|
||||||
border-right solid 1px themeMonokaiTableBorder
|
border-right solid 1px themeMonokaiTableBorder
|
||||||
kbd
|
kbd
|
||||||
background-color themeDarkBackground
|
background-color themeDarkBackground
|
||||||
|
|
||||||
|
dl
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color themeMonokaiTableHead
|
||||||
|
dt
|
||||||
|
border-color themeDarkBorder
|
||||||
|
dd
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
pre.fence
|
||||||
|
.gallery
|
||||||
|
.carousel-main, .carousel-footer
|
||||||
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
.prev, .next
|
||||||
|
color $ui-monokai-button--active-color
|
||||||
|
background-color $ui-monokai-button-backgroundColor
|
||||||
|
|
||||||
|
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
|
||||||
|
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
|
||||||
|
themeDraculaTableHead = themeDraculaTableEven
|
||||||
|
themeDraculaTableBorder = themeDarkBorder
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
table
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
background-color themeDraculaTableHead
|
||||||
|
th
|
||||||
|
border-color themeDraculaTableBorder
|
||||||
|
&:last-child
|
||||||
|
border-right solid 1px themeDraculaTableBorder
|
||||||
|
tbody
|
||||||
|
tr:nth-child(2n + 1)
|
||||||
|
background-color themeDraculaTableOdd
|
||||||
|
tr:nth-child(2n)
|
||||||
|
background-color themeDraculaTableEven
|
||||||
|
td
|
||||||
|
border-color themeDraculaTableBorder
|
||||||
|
&:last-child
|
||||||
|
border-right solid 1px themeDraculaTableBorder
|
||||||
|
kbd
|
||||||
|
background-color themeDarkBackground
|
||||||
|
|
||||||
|
dl
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color themeDraculaTableHead
|
||||||
|
dt
|
||||||
|
border-color themeDarkBorder
|
||||||
|
dd
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
pre.fence
|
||||||
|
.gallery
|
||||||
|
.carousel-main, .carousel-footer
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
.prev, .next
|
||||||
|
color $ui-dracula-button--active-color
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
import mermaidAPI from 'mermaid'
|
import mermaidAPI from 'mermaid'
|
||||||
|
|
||||||
|
// fixes bad styling in the mermaid dark theme
|
||||||
|
const darkThemeStyling = `
|
||||||
|
.loopText tspan {
|
||||||
|
fill: white;
|
||||||
|
}`
|
||||||
|
|
||||||
function getRandomInt (min, max) {
|
function getRandomInt (min, max) {
|
||||||
return Math.floor(Math.random() * (max - min)) + min
|
return Math.floor(Math.random() * (max - min)) + min
|
||||||
}
|
}
|
||||||
|
|
||||||
function getId () {
|
function getId () {
|
||||||
var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||||
var id = 'm-'
|
let id = 'm-'
|
||||||
for (var i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
id += pool[getRandomInt(0, 16)]
|
id += pool[getRandomInt(0, 16)]
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
function render (element, content) {
|
function render (element, content, theme) {
|
||||||
try {
|
try {
|
||||||
|
const height = element.attributes.getNamedItem('data-height')
|
||||||
|
if (height && height.value !== 'undefined') {
|
||||||
|
element.style.height = height.value + 'vh'
|
||||||
|
}
|
||||||
|
const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
|
||||||
|
mermaidAPI.initialize({
|
||||||
|
theme: isDarkTheme ? 'dark' : 'default',
|
||||||
|
themeCSS: isDarkTheme ? darkThemeStyling : '',
|
||||||
|
useMaxWidth: false
|
||||||
|
})
|
||||||
mermaidAPI.render(getId(), content, (svgGraph) => {
|
mermaidAPI.render(getId(), content, (svgGraph) => {
|
||||||
element.innerHTML = svgGraph
|
element.innerHTML = svgGraph
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
|
||||||
element.className = 'mermaid-error'
|
element.className = 'mermaid-error'
|
||||||
element.innerHTML = 'mermaid diagram parse error: ' + e.message
|
element.innerHTML = 'mermaid diagram parse error: ' + e.message
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,8 +48,12 @@ const languages = [
|
|||||||
locale: 'pl'
|
locale: 'pl'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Portuguese',
|
name: 'Portuguese (PT-BR)',
|
||||||
locale: 'pt'
|
locale: 'pt-BR'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Portuguese (PT-PT)',
|
||||||
|
locale: 'pt-PT'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Russian',
|
name: 'Russian',
|
||||||
@@ -61,6 +65,9 @@ const languages = [
|
|||||||
}, {
|
}, {
|
||||||
name: 'Turkish',
|
name: 'Turkish',
|
||||||
locale: 'tr'
|
locale: 'tr'
|
||||||
|
}, {
|
||||||
|
name: 'Thai',
|
||||||
|
locale: 'th'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,113 @@
|
|||||||
import { Point } from '@susisu/mte-kernel'
|
import { Point } from '@susisu/mte-kernel'
|
||||||
|
|
||||||
export default class TextEditorInterface {
|
export default class TextEditorInterface {
|
||||||
constructor (editor) {
|
constructor (editor) {
|
||||||
this.editor = editor
|
this.editor = editor
|
||||||
}
|
this.doc = editor.getDoc()
|
||||||
|
this.transaction = false
|
||||||
getCursorPosition () {
|
}
|
||||||
const pos = this.editor.getCursor()
|
|
||||||
return new Point(pos.line, pos.ch)
|
getCursorPosition () {
|
||||||
}
|
const { line, ch } = this.doc.getCursor()
|
||||||
|
return new Point(line, ch)
|
||||||
setCursorPosition (pos) {
|
}
|
||||||
this.editor.setCursor({line: pos.row, ch: pos.column})
|
|
||||||
}
|
setCursorPosition (pos) {
|
||||||
|
this.doc.setCursor({
|
||||||
setSelectionRange (range) {
|
line: pos.row,
|
||||||
this.editor.setSelection({
|
ch: pos.column
|
||||||
anchor: {line: range.start.row, ch: range.start.column},
|
})
|
||||||
head: {line: range.end.row, ch: range.end.column}
|
}
|
||||||
})
|
|
||||||
}
|
setSelectionRange (range) {
|
||||||
|
this.doc.setSelection(
|
||||||
getLastRow () {
|
{ line: range.start.row, ch: range.start.column },
|
||||||
return this.editor.lastLine()
|
{ line: range.end.row, ch: range.end.column }
|
||||||
}
|
)
|
||||||
|
}
|
||||||
acceptsTableEdit (row) {
|
|
||||||
return true
|
getLastRow () {
|
||||||
}
|
return this.doc.lineCount() - 1
|
||||||
|
}
|
||||||
getLine (row) {
|
|
||||||
return this.editor.getLine(row)
|
acceptsTableEdit () {
|
||||||
}
|
return true
|
||||||
|
}
|
||||||
insertLine (row, line) {
|
|
||||||
this.editor.replaceRange(line, {line: row, ch: 0})
|
getLine (row) {
|
||||||
}
|
return this.doc.getLine(row)
|
||||||
|
}
|
||||||
deleteLine (row) {
|
|
||||||
this.editor.replaceRange('', {line: row, ch: 0}, {line: row, ch: this.editor.getLine(row).length})
|
insertLine (row, line) {
|
||||||
}
|
const lastRow = this.getLastRow()
|
||||||
|
if (row > lastRow) {
|
||||||
replaceLines (startRow, endRow, lines) {
|
const lastLine = this.getLine(lastRow)
|
||||||
endRow-- // because endRow is a first line after a table.
|
this.doc.replaceRange(
|
||||||
const endRowCh = this.editor.getLine(endRow).length
|
'\n' + line,
|
||||||
this.editor.replaceRange(lines, {line: startRow, ch: 0}, {line: endRow, ch: endRowCh})
|
{ line: lastRow, ch: lastLine.length },
|
||||||
}
|
{ line: lastRow, ch: lastLine.length }
|
||||||
|
)
|
||||||
transact (func) {
|
} else {
|
||||||
func()
|
this.doc.replaceRange(
|
||||||
}
|
line + '\n',
|
||||||
}
|
{ line: row, ch: 0 },
|
||||||
|
{ line: row, ch: 0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteLine (row) {
|
||||||
|
const lastRow = this.getLastRow()
|
||||||
|
if (row >= lastRow) {
|
||||||
|
if (lastRow > 0) {
|
||||||
|
const preLastLine = this.getLine(lastRow - 1)
|
||||||
|
const lastLine = this.getLine(lastRow)
|
||||||
|
this.doc.replaceRange(
|
||||||
|
'',
|
||||||
|
{ line: lastRow - 1, ch: preLastLine.length },
|
||||||
|
{ line: lastRow, ch: lastLine.length }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const lastLine = this.getLine(lastRow)
|
||||||
|
this.doc.replaceRange(
|
||||||
|
'',
|
||||||
|
{ line: lastRow, ch: 0 },
|
||||||
|
{ line: lastRow, ch: lastLine.length }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.doc.replaceRange(
|
||||||
|
'',
|
||||||
|
{ line: row, ch: 0 },
|
||||||
|
{ line: row + 1, ch: 0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceLines (startRow, endRow, lines) {
|
||||||
|
const lastRow = this.getLastRow()
|
||||||
|
if (endRow > lastRow) {
|
||||||
|
const lastLine = this.getLine(lastRow)
|
||||||
|
this.doc.replaceRange(
|
||||||
|
lines.join('\n'),
|
||||||
|
{ line: startRow, ch: 0 },
|
||||||
|
{ line: lastRow, ch: lastLine.length }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.doc.replaceRange(
|
||||||
|
lines.join('\n') + '\n',
|
||||||
|
{ line: startRow, ch: 0 },
|
||||||
|
{ line: endRow, ch: 0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transact (func) {
|
||||||
|
this.transaction = true
|
||||||
|
func()
|
||||||
|
this.transaction = false
|
||||||
|
if (this.onDidFinishTransaction) {
|
||||||
|
this.onDidFinishTransaction.call(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
65
browser/lib/contextMenuBuilder.js
Normal file
65
browser/lib/contextMenuBuilder.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
const {remote} = require('electron')
|
||||||
|
const {Menu} = remote.require('electron')
|
||||||
|
const spellcheck = require('./spellcheck')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
|
||||||
|
* If the word is does not contains a spelling error (determined by the 'error style'), no suggestions for corrections are requested
|
||||||
|
* => they are not visible in the context menu
|
||||||
|
* @param editor CodeMirror editor
|
||||||
|
* @param {MouseEvent} event that has triggered the creation of the context menu
|
||||||
|
* @returns {Electron.Menu} The created electron context menu
|
||||||
|
*/
|
||||||
|
const buildEditorContextMenu = function (editor, event) {
|
||||||
|
if (editor == null || event == null || event.pageX == null || event.pageY == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const cursor = editor.coordsChar({left: event.pageX, top: event.pageY})
|
||||||
|
const wordRange = editor.findWordAt(cursor)
|
||||||
|
const word = editor.getRange(wordRange.anchor, wordRange.head)
|
||||||
|
const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || []
|
||||||
|
let isMisspelled = false
|
||||||
|
for (const mark of existingMarks) {
|
||||||
|
if (mark.className === spellcheck.getCSSClassName()) {
|
||||||
|
isMisspelled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let suggestion = []
|
||||||
|
if (isMisspelled) {
|
||||||
|
suggestion = spellcheck.getSpellingSuggestion(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = {
|
||||||
|
isMisspelled: isMisspelled,
|
||||||
|
spellingSuggestions: suggestion
|
||||||
|
}
|
||||||
|
const template = [{
|
||||||
|
role: 'cut'
|
||||||
|
}, {
|
||||||
|
role: 'copy'
|
||||||
|
}, {
|
||||||
|
role: 'paste'
|
||||||
|
}, {
|
||||||
|
role: 'selectall'
|
||||||
|
}]
|
||||||
|
|
||||||
|
if (selection.isMisspelled) {
|
||||||
|
const suggestions = selection.spellingSuggestions
|
||||||
|
template.unshift.apply(template, suggestions.map(function (suggestion) {
|
||||||
|
return {
|
||||||
|
label: suggestion,
|
||||||
|
click: function (suggestion) {
|
||||||
|
if (editor != null) {
|
||||||
|
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).concat({
|
||||||
|
type: 'separator'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return Menu.buildFromTemplate(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = buildEditorContextMenu
|
||||||
@@ -1,19 +1,37 @@
|
|||||||
export function findNoteTitle (value) {
|
export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') {
|
||||||
const splitted = value.split('\n')
|
const splitted = value.split('\n')
|
||||||
let title = null
|
let title = null
|
||||||
let isInsideCodeBlock = false
|
let isInsideCodeBlock = false
|
||||||
|
|
||||||
splitted.some((line, index) => {
|
if (splitted[0] === '---') {
|
||||||
const trimmedLine = line.trim()
|
let line = 0
|
||||||
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
while (++line < splitted.length) {
|
||||||
if (trimmedLine.match('```')) {
|
if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) {
|
||||||
isInsideCodeBlock = !isInsideCodeBlock
|
title = splitted[line].substring(frontMatterTitleField.length + 1).trim()
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (splitted[line] === '---') {
|
||||||
|
splitted.splice(0, line + 1)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
|
}
|
||||||
title = trimmedLine
|
|
||||||
return true
|
if (title === null) {
|
||||||
}
|
splitted.some((line, index) => {
|
||||||
})
|
const trimmedLine = line.trim()
|
||||||
|
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
||||||
|
if (trimmedLine.match('```')) {
|
||||||
|
isInsideCodeBlock = !isInsideCodeBlock
|
||||||
|
}
|
||||||
|
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
|
||||||
|
title = trimmedLine
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (title === null) {
|
if (title === null) {
|
||||||
title = ''
|
title = ''
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ export function getTodoStatus (content) {
|
|||||||
|
|
||||||
splitted.forEach((line) => {
|
splitted.forEach((line) => {
|
||||||
const trimmedLine = line.trim()
|
const trimmedLine = line.trim()
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) {
|
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
|
||||||
numberOfTodo++
|
numberOfTodo++
|
||||||
}
|
}
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
|
||||||
numberOfCompletedTodo++
|
numberOfCompletedTodo++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
232
browser/lib/markdown-it-deflist.js
Normal file
232
browser/lib/markdown-it-deflist.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
module.exports = function definitionListPlugin (md) {
|
||||||
|
var isSpace = md.utils.isSpace
|
||||||
|
|
||||||
|
// Search `[:~][\n ]`, returns next pos after marker on success
|
||||||
|
// or -1 on fail.
|
||||||
|
function skipMarker (state, line) {
|
||||||
|
let start = state.bMarks[line] + state.tShift[line]
|
||||||
|
const max = state.eMarks[line]
|
||||||
|
|
||||||
|
if (start >= max) { return -1 }
|
||||||
|
|
||||||
|
// Check bullet
|
||||||
|
const marker = state.src.charCodeAt(start++)
|
||||||
|
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 }
|
||||||
|
|
||||||
|
const pos = state.skipSpaces(start)
|
||||||
|
|
||||||
|
// require space after ":"
|
||||||
|
if (start === pos) { return -1 }
|
||||||
|
|
||||||
|
return start
|
||||||
|
}
|
||||||
|
|
||||||
|
function markTightParagraphs (state, idx) {
|
||||||
|
const level = state.level + 2
|
||||||
|
|
||||||
|
let i
|
||||||
|
let l
|
||||||
|
for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
|
||||||
|
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
|
||||||
|
state.tokens[i + 2].hidden = true
|
||||||
|
state.tokens[i].hidden = true
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deflist (state, startLine, endLine, silent) {
|
||||||
|
var ch,
|
||||||
|
contentStart,
|
||||||
|
ddLine,
|
||||||
|
dtLine,
|
||||||
|
itemLines,
|
||||||
|
listLines,
|
||||||
|
listTokIdx,
|
||||||
|
max,
|
||||||
|
newEndLine,
|
||||||
|
nextLine,
|
||||||
|
offset,
|
||||||
|
oldDDIndent,
|
||||||
|
oldIndent,
|
||||||
|
oldLineMax,
|
||||||
|
oldParentType,
|
||||||
|
oldSCount,
|
||||||
|
oldTShift,
|
||||||
|
oldTight,
|
||||||
|
pos,
|
||||||
|
prevEmptyEnd,
|
||||||
|
tight,
|
||||||
|
token
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
// quirk: validation mode validates a dd block only, not a whole deflist
|
||||||
|
if (state.ddIndent < 0) { return false }
|
||||||
|
return skipMarker(state, startLine) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
nextLine = startLine + 1
|
||||||
|
if (nextLine >= endLine) { return false }
|
||||||
|
|
||||||
|
if (state.isEmpty(nextLine)) {
|
||||||
|
nextLine++
|
||||||
|
if (nextLine >= endLine) { return false }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.sCount[nextLine] < state.blkIndent) { return false }
|
||||||
|
contentStart = skipMarker(state, nextLine)
|
||||||
|
if (contentStart < 0) { return false }
|
||||||
|
|
||||||
|
// Start list
|
||||||
|
listTokIdx = state.tokens.length
|
||||||
|
tight = true
|
||||||
|
|
||||||
|
token = state.push('dl_open', 'dl', 1)
|
||||||
|
token.map = listLines = [ startLine, 0 ]
|
||||||
|
|
||||||
|
//
|
||||||
|
// Iterate list items
|
||||||
|
//
|
||||||
|
|
||||||
|
dtLine = startLine
|
||||||
|
ddLine = nextLine
|
||||||
|
|
||||||
|
// One definition list can contain multiple DTs,
|
||||||
|
// and one DT can be followed by multiple DDs.
|
||||||
|
//
|
||||||
|
// Thus, there is two loops here, and label is
|
||||||
|
// needed to break out of the second one
|
||||||
|
//
|
||||||
|
/* eslint no-labels:0,block-scoped-var:0 */
|
||||||
|
OUTER:
|
||||||
|
for (;;) {
|
||||||
|
prevEmptyEnd = false
|
||||||
|
|
||||||
|
token = state.push('dt_open', 'dt', 1)
|
||||||
|
token.map = [ dtLine, dtLine ]
|
||||||
|
|
||||||
|
token = state.push('inline', '', 0)
|
||||||
|
token.map = [ dtLine, dtLine ]
|
||||||
|
token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim()
|
||||||
|
token.children = []
|
||||||
|
|
||||||
|
token = state.push('dt_close', 'dt', -1)
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
token = state.push('dd_open', 'dd', 1)
|
||||||
|
token.map = itemLines = [ ddLine, 0 ]
|
||||||
|
|
||||||
|
pos = contentStart
|
||||||
|
max = state.eMarks[ddLine]
|
||||||
|
offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine])
|
||||||
|
|
||||||
|
while (pos < max) {
|
||||||
|
ch = state.src.charCodeAt(pos)
|
||||||
|
|
||||||
|
if (isSpace(ch)) {
|
||||||
|
if (ch === 0x09) {
|
||||||
|
offset += 4 - offset % 4
|
||||||
|
} else {
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
contentStart = pos
|
||||||
|
|
||||||
|
oldTight = state.tight
|
||||||
|
oldDDIndent = state.ddIndent
|
||||||
|
oldIndent = state.blkIndent
|
||||||
|
oldTShift = state.tShift[ddLine]
|
||||||
|
oldSCount = state.sCount[ddLine]
|
||||||
|
oldParentType = state.parentType
|
||||||
|
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2
|
||||||
|
state.tShift[ddLine] = contentStart - state.bMarks[ddLine]
|
||||||
|
state.sCount[ddLine] = offset
|
||||||
|
state.tight = true
|
||||||
|
state.parentType = 'deflist'
|
||||||
|
|
||||||
|
newEndLine = ddLine
|
||||||
|
while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) {
|
||||||
|
}
|
||||||
|
|
||||||
|
oldLineMax = state.lineMax
|
||||||
|
state.lineMax = newEndLine
|
||||||
|
|
||||||
|
state.md.block.tokenize(state, ddLine, newEndLine, true)
|
||||||
|
|
||||||
|
state.lineMax = oldLineMax
|
||||||
|
|
||||||
|
// If any of list item is tight, mark list as tight
|
||||||
|
if (!state.tight || prevEmptyEnd) {
|
||||||
|
tight = false
|
||||||
|
}
|
||||||
|
// Item become loose if finish with empty line,
|
||||||
|
// but we should filter last element, because it means list finish
|
||||||
|
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1)
|
||||||
|
|
||||||
|
state.tShift[ddLine] = oldTShift
|
||||||
|
state.sCount[ddLine] = oldSCount
|
||||||
|
state.tight = oldTight
|
||||||
|
state.parentType = oldParentType
|
||||||
|
state.blkIndent = oldIndent
|
||||||
|
state.ddIndent = oldDDIndent
|
||||||
|
|
||||||
|
token = state.push('dd_close', 'dd', -1)
|
||||||
|
|
||||||
|
itemLines[1] = nextLine = state.line
|
||||||
|
|
||||||
|
if (nextLine >= endLine) { break OUTER }
|
||||||
|
|
||||||
|
if (state.sCount[nextLine] < state.blkIndent) { break OUTER }
|
||||||
|
contentStart = skipMarker(state, nextLine)
|
||||||
|
if (contentStart < 0) { break }
|
||||||
|
|
||||||
|
ddLine = nextLine
|
||||||
|
|
||||||
|
// go to the next loop iteration:
|
||||||
|
// insert DD tag and repeat checking
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextLine >= endLine) { break }
|
||||||
|
dtLine = nextLine
|
||||||
|
|
||||||
|
if (state.isEmpty(dtLine)) { break }
|
||||||
|
if (state.sCount[dtLine] < state.blkIndent) { break }
|
||||||
|
|
||||||
|
ddLine = dtLine + 1
|
||||||
|
if (ddLine >= endLine) { break }
|
||||||
|
if (state.isEmpty(ddLine)) { ddLine++ }
|
||||||
|
if (ddLine >= endLine) { break }
|
||||||
|
|
||||||
|
if (state.sCount[ddLine] < state.blkIndent) { break }
|
||||||
|
contentStart = skipMarker(state, ddLine)
|
||||||
|
if (contentStart < 0) { break }
|
||||||
|
|
||||||
|
// go to the next loop iteration:
|
||||||
|
// insert DT and DD tags and repeat checking
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finilize list
|
||||||
|
token = state.push('dl_close', 'dl', -1)
|
||||||
|
|
||||||
|
listLines[1] = nextLine
|
||||||
|
|
||||||
|
state.line = nextLine
|
||||||
|
|
||||||
|
// mark paragraphs tight if needed
|
||||||
|
if (tight) {
|
||||||
|
markTightParagraphs(state, listTokIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] })
|
||||||
|
}
|
||||||
136
browser/lib/markdown-it-fence.js
Normal file
136
browser/lib/markdown-it-fence.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
module.exports = function (md, renderers, defaultRenderer) {
|
||||||
|
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
|
||||||
|
|
||||||
|
function fence (state, startLine, endLine, silent) {
|
||||||
|
let pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||||
|
let max = state.eMarks[startLine]
|
||||||
|
|
||||||
|
if (state.sCount[startLine] - state.blkIndent >= 4 || pos + 3 > max) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const marker = state.src.charCodeAt(pos)
|
||||||
|
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let mem = pos
|
||||||
|
pos = state.skipChars(pos, marker)
|
||||||
|
|
||||||
|
let len = pos - mem
|
||||||
|
if (len < 3) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const markup = state.src.slice(mem, pos)
|
||||||
|
const params = state.src.slice(pos, max)
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextLine = startLine
|
||||||
|
let haveEndMarker = false
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
nextLine++
|
||||||
|
if (nextLine >= endLine) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
|
||||||
|
max = state.eMarks[nextLine]
|
||||||
|
|
||||||
|
if (pos < max && state.sCount[nextLine] < state.blkIndent) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (state.src.charCodeAt(pos) !== marker || state.sCount[nextLine] - state.blkIndent >= 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = state.skipChars(pos, marker)
|
||||||
|
|
||||||
|
if (pos - mem < len) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = state.skipSpaces(pos)
|
||||||
|
|
||||||
|
if (pos >= max) {
|
||||||
|
haveEndMarker = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
len = state.sCount[startLine]
|
||||||
|
state.line = nextLine + (haveEndMarker ? 1 : 0)
|
||||||
|
|
||||||
|
const parameters = {}
|
||||||
|
let langType = ''
|
||||||
|
let fileName = ''
|
||||||
|
let firstLineNumber = 1
|
||||||
|
|
||||||
|
let match = paramsRE.exec(params)
|
||||||
|
if (match) {
|
||||||
|
if (match[1]) {
|
||||||
|
langType = match[1]
|
||||||
|
}
|
||||||
|
if (match[3]) {
|
||||||
|
fileName = match[3]
|
||||||
|
}
|
||||||
|
if (match[4]) {
|
||||||
|
firstLineNumber = parseInt(match[4], 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match[2]) {
|
||||||
|
const params = match[2]
|
||||||
|
const regex = /(\w[-\w]*)(?:=(?:'(.*?[^\\])?'|"(.*?[^\\])?"|([^'"][^\s]*)))?/g
|
||||||
|
|
||||||
|
let name, value
|
||||||
|
while ((match = regex.exec(params))) {
|
||||||
|
name = match[1]
|
||||||
|
value = match[2] || match[3] || match[4] || null
|
||||||
|
|
||||||
|
const height = /^(\d+)h$/.exec(name)
|
||||||
|
if (height && !value) {
|
||||||
|
parameters.height = height[1]
|
||||||
|
} else {
|
||||||
|
parameters[name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let token
|
||||||
|
if (renderers[langType]) {
|
||||||
|
token = state.push(`${langType}_fence`, 'div', 0)
|
||||||
|
} else {
|
||||||
|
token = state.push('_fence', 'code', 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
token.langType = langType
|
||||||
|
token.fileName = fileName
|
||||||
|
token.firstLineNumber = firstLineNumber
|
||||||
|
token.parameters = parameters
|
||||||
|
|
||||||
|
token.content = state.getLines(startLine + 1, nextLine, len, true)
|
||||||
|
token.markup = markup
|
||||||
|
token.map = [startLine, state.line]
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
md.block.ruler.before('fence', '_fence', fence, {
|
||||||
|
alt: ['paragraph', 'reference', 'blockquote', 'list']
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const name in renderers) {
|
||||||
|
md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultRenderer) {
|
||||||
|
md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
24
browser/lib/markdown-it-frontmatter.js
Normal file
24
browser/lib/markdown-it-frontmatter.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
module.exports = function frontMatterPlugin (md) {
|
||||||
|
function frontmatter (state, startLine, endLine, silent) {
|
||||||
|
if (startLine !== 0 || state.src.substr(startLine, state.eMarks[0]) !== '---') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = 0
|
||||||
|
while (++line < state.lineMax) {
|
||||||
|
if (state.src.substring(state.bMarks[line], state.eMarks[line]) === '---') {
|
||||||
|
state.line = line + 1
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
md.block.ruler.before('table', 'frontmatter', frontmatter, {
|
||||||
|
alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
import sanitizeHtml from 'sanitize-html'
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
import { escapeHtmlCharacters } from './utils'
|
||||||
|
import url from 'url'
|
||||||
|
|
||||||
module.exports = function sanitizePlugin (md, options) {
|
module.exports = function sanitizePlugin (md, options) {
|
||||||
options = options || {}
|
options = options || {}
|
||||||
@@ -8,19 +10,115 @@ module.exports = function sanitizePlugin (md, options) {
|
|||||||
md.core.ruler.after('linkify', 'sanitize_inline', state => {
|
md.core.ruler.after('linkify', 'sanitize_inline', state => {
|
||||||
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
|
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
|
||||||
if (state.tokens[tokenIdx].type === 'html_block') {
|
if (state.tokens[tokenIdx].type === 'html_block') {
|
||||||
state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options)
|
state.tokens[tokenIdx].content = sanitizeHtml(
|
||||||
|
state.tokens[tokenIdx].content,
|
||||||
|
options
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (state.tokens[tokenIdx].type === 'fence') {
|
if (state.tokens[tokenIdx].type === '_fence') {
|
||||||
state.tokens[tokenIdx].content = state.tokens[tokenIdx].content.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
// escapeHtmlCharacters has better performance
|
||||||
|
state.tokens[tokenIdx].content = escapeHtmlCharacters(
|
||||||
|
state.tokens[tokenIdx].content,
|
||||||
|
{ skipSingleQuote: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (state.tokens[tokenIdx].type === 'inline') {
|
if (state.tokens[tokenIdx].type === 'inline') {
|
||||||
const inlineTokens = state.tokens[tokenIdx].children
|
const inlineTokens = state.tokens[tokenIdx].children
|
||||||
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
||||||
if (inlineTokens[childIdx].type === 'html_inline') {
|
if (inlineTokens[childIdx].type === 'html_inline') {
|
||||||
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options)
|
inlineTokens[childIdx].content = sanitizeInline(
|
||||||
|
inlineTokens[childIdx].content,
|
||||||
|
options
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
|
||||||
|
const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig
|
||||||
|
|
||||||
|
function sanitizeInline (html, options) {
|
||||||
|
let match = tagRegex.exec(html)
|
||||||
|
if (!match) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options
|
||||||
|
|
||||||
|
if (match[1] !== undefined) {
|
||||||
|
// opening tag
|
||||||
|
const tag = match[1].toLowerCase()
|
||||||
|
if (allowedTags.indexOf(tag) === -1) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = match[2]
|
||||||
|
|
||||||
|
let attrs = ''
|
||||||
|
let name
|
||||||
|
let value
|
||||||
|
|
||||||
|
while ((match = attributesRegex.exec(attributes))) {
|
||||||
|
name = match[1].toLowerCase()
|
||||||
|
value = match[3]
|
||||||
|
|
||||||
|
if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) {
|
||||||
|
if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
|
||||||
|
if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs += ` ${name}`
|
||||||
|
if (match[2]) {
|
||||||
|
attrs += `="${value}"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selfClosing.indexOf(tag) === -1) {
|
||||||
|
return '<' + tag + attrs + '>'
|
||||||
|
} else {
|
||||||
|
return '<' + tag + attrs + ' />'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// closing tag
|
||||||
|
if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) {
|
||||||
|
return html
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function naughtyHRef (href, options) {
|
||||||
|
// href = href.replace(/[\x00-\x20]+/g, '')
|
||||||
|
href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
|
||||||
|
|
||||||
|
const matches = href.match(/^([a-zA-Z]+)\:/)
|
||||||
|
if (!matches) {
|
||||||
|
if (href.match(/^[\/\\]{2}/)) {
|
||||||
|
return !options.allowProtocolRelative
|
||||||
|
}
|
||||||
|
|
||||||
|
// No scheme
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheme = matches[1].toLowerCase()
|
||||||
|
|
||||||
|
return options.allowedSchemes.indexOf(scheme) === -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function naughtyIFrame (src, options) {
|
||||||
|
try {
|
||||||
|
const parsed = url.parse(src, false, true)
|
||||||
|
|
||||||
|
return options.allowedIframeHostnames.index(parsed.hostname) === -1
|
||||||
|
} catch (e) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
98
browser/lib/markdown-toc-generator.js
Normal file
98
browser/lib/markdown-toc-generator.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Markdown table of contents generator
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EOL } from 'os'
|
||||||
|
import toc from 'markdown-toc'
|
||||||
|
import mdlink from 'markdown-link'
|
||||||
|
import slugify from './slugify'
|
||||||
|
|
||||||
|
const hasProp = Object.prototype.hasOwnProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From @enyaxu/markdown-it-anchor
|
||||||
|
*/
|
||||||
|
function uniqueSlug (slug, slugs, opts) {
|
||||||
|
let uniq = slug
|
||||||
|
let i = opts.uniqueSlugStartIndex
|
||||||
|
while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}`
|
||||||
|
slugs[uniq] = true
|
||||||
|
return uniq
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkify (token) {
|
||||||
|
token.content = mdlink(token.content, '#' + token.slug)
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOC_MARKER_START = '<!-- toc -->'
|
||||||
|
const TOC_MARKER_END = '<!-- tocstop -->'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes care of proper updating given editor with TOC.
|
||||||
|
* If TOC doesn't exit in the editor, it's inserted at current caret position.
|
||||||
|
* Otherwise,TOC is updated in place.
|
||||||
|
* @param editor CodeMirror editor to be updated with TOC
|
||||||
|
*/
|
||||||
|
export function generateInEditor (editor) {
|
||||||
|
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
|
||||||
|
|
||||||
|
function tocExistsInEditor () {
|
||||||
|
return tocRegex.test(editor.getValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExistingToc () {
|
||||||
|
const toc = generate(editor.getValue())
|
||||||
|
const search = editor.getSearchCursor(tocRegex)
|
||||||
|
while (search.findNext()) {
|
||||||
|
search.replace(toc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTocAtCursorPosition () {
|
||||||
|
const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity}))
|
||||||
|
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tocExistsInEditor()) {
|
||||||
|
updateExistingToc()
|
||||||
|
} else {
|
||||||
|
addTocAtCursorPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates MD TOC based on MD document passed as string.
|
||||||
|
* @param markdownText MD document
|
||||||
|
* @returns generatedTOC String containing generated TOC
|
||||||
|
*/
|
||||||
|
export function generate (markdownText) {
|
||||||
|
const slugs = {}
|
||||||
|
const opts = {
|
||||||
|
uniqueSlugStartIndex: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = toc(markdownText, {
|
||||||
|
slugify: title => {
|
||||||
|
return uniqueSlug(slugify(title), slugs, opts)
|
||||||
|
},
|
||||||
|
linkify: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const md = toc.bullets(result.json.map(linkify), {
|
||||||
|
highest: result.highest
|
||||||
|
})
|
||||||
|
|
||||||
|
return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapTocWithEol (toc, editor) {
|
||||||
|
const leftWrap = editor.getCursor().ch === 0 ? '' : EOL
|
||||||
|
const rightWrap = editor.getLine(editor.getCursor().line).length === editor.getCursor().ch ? '' : EOL
|
||||||
|
return leftWrap + toc + rightWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
generate,
|
||||||
|
generateInEditor
|
||||||
|
}
|
||||||
@@ -27,32 +27,6 @@ class Markdown {
|
|||||||
html: true,
|
html: true,
|
||||||
xhtmlOut: true,
|
xhtmlOut: true,
|
||||||
breaks: config.preview.breaks,
|
breaks: config.preview.breaks,
|
||||||
highlight: function (str, lang) {
|
|
||||||
const delimiter = ':'
|
|
||||||
const langInfo = lang.split(delimiter)
|
|
||||||
const langType = langInfo[0]
|
|
||||||
const fileName = langInfo[1] || ''
|
|
||||||
const firstLineNumber = parseInt(langInfo[2], 10)
|
|
||||||
|
|
||||||
if (langType === 'flowchart') {
|
|
||||||
return `<pre class="flowchart">${str}</pre>`
|
|
||||||
}
|
|
||||||
if (langType === 'sequence') {
|
|
||||||
return `<pre class="sequence">${str}</pre>`
|
|
||||||
}
|
|
||||||
if (langType === 'chart') {
|
|
||||||
return `<pre class="chart">${str}</pre>`
|
|
||||||
}
|
|
||||||
if (langType === 'mermaid') {
|
|
||||||
return `<pre class="mermaid">${str}</pre>`
|
|
||||||
}
|
|
||||||
return '<pre class="code CodeMirror">' +
|
|
||||||
'<span class="filename">' + fileName + '</span>' +
|
|
||||||
createGutter(str, firstLineNumber) +
|
|
||||||
'<code class="' + langType + '">' +
|
|
||||||
str +
|
|
||||||
'</code></pre>'
|
|
||||||
},
|
|
||||||
sanitize: 'STRICT'
|
sanitize: 'STRICT'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +79,11 @@ class Markdown {
|
|||||||
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
||||||
'input': ['type', 'id', 'checked']
|
'input': ['type', 'id', 'checked']
|
||||||
},
|
},
|
||||||
allowedIframeHostnames: ['www.youtube.com']
|
allowedIframeHostnames: ['www.youtube.com'],
|
||||||
|
selfClosing: [ 'img', 'br', 'hr', 'input' ],
|
||||||
|
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
|
||||||
|
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
|
||||||
|
allowProtocolRelative: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,16 +117,68 @@ class Markdown {
|
|||||||
this.md.use(require('markdown-it-imsize'))
|
this.md.use(require('markdown-it-imsize'))
|
||||||
this.md.use(require('markdown-it-footnote'))
|
this.md.use(require('markdown-it-footnote'))
|
||||||
this.md.use(require('markdown-it-multimd-table'))
|
this.md.use(require('markdown-it-multimd-table'))
|
||||||
this.md.use(require('markdown-it-named-headers'), {
|
this.md.use(require('@enyaxu/markdown-it-anchor'), {
|
||||||
slugify: (header) => {
|
slugify: require('./slugify')
|
||||||
return encodeURI(header.trim()
|
|
||||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
|
||||||
.replace(/\s+/g, '-'))
|
|
||||||
.replace(/\-+$/, '')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
this.md.use(require('markdown-it-kbd'))
|
this.md.use(require('markdown-it-kbd'))
|
||||||
this.md.use(require('markdown-it-admonition'))
|
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
|
||||||
|
this.md.use(require('markdown-it-abbr'))
|
||||||
|
this.md.use(require('markdown-it-sub'))
|
||||||
|
this.md.use(require('markdown-it-sup'))
|
||||||
|
this.md.use(require('./markdown-it-deflist'))
|
||||||
|
this.md.use(require('./markdown-it-frontmatter'))
|
||||||
|
|
||||||
|
this.md.use(require('./markdown-it-fence'), {
|
||||||
|
chart: token => {
|
||||||
|
if (token.parameters.hasOwnProperty('yaml')) {
|
||||||
|
token.parameters.format = 'yaml'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
|
<span class="filename">${token.fileName}</span>
|
||||||
|
<div class="chart" data-height="${token.parameters.height}" data-format="${token.parameters.format || 'json'}">${token.content}</div>
|
||||||
|
</pre>`
|
||||||
|
},
|
||||||
|
flowchart: token => {
|
||||||
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
|
<span class="filename">${token.fileName}</span>
|
||||||
|
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
|
||||||
|
</pre>`
|
||||||
|
},
|
||||||
|
gallery: token => {
|
||||||
|
const content = token.content.split('\n').slice(0, -1).map(line => {
|
||||||
|
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
|
||||||
|
if (match) {
|
||||||
|
return match[1]
|
||||||
|
} else {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
}).join('\n')
|
||||||
|
|
||||||
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
|
<span class="filename">${token.fileName}</span>
|
||||||
|
<div class="gallery" data-autoplay="${token.parameters.autoplay}" data-height="${token.parameters.height}">${content}</div>
|
||||||
|
</pre>`
|
||||||
|
},
|
||||||
|
mermaid: token => {
|
||||||
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
|
<span class="filename">${token.fileName}</span>
|
||||||
|
<div class="mermaid" data-height="${token.parameters.height}">${token.content}</div>
|
||||||
|
</pre>`
|
||||||
|
},
|
||||||
|
sequence: token => {
|
||||||
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
|
<span class="filename">${token.fileName}</span>
|
||||||
|
<div class="sequence" data-height="${token.parameters.height}">${token.content}</div>
|
||||||
|
</pre>`
|
||||||
|
}
|
||||||
|
}, token => {
|
||||||
|
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
|
||||||
|
<span class="filename">${token.fileName}</span>
|
||||||
|
${createGutter(token.content, token.firstLineNumber)}
|
||||||
|
<code class="${token.langType}">${token.content}</code>
|
||||||
|
</pre>`
|
||||||
|
})
|
||||||
|
|
||||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||||
this.md.use(require('markdown-it-plantuml'), '', {
|
this.md.use(require('markdown-it-plantuml'), '', {
|
||||||
@@ -163,6 +193,22 @@ class Markdown {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Ditaa support
|
||||||
|
this.md.use(require('markdown-it-plantuml'), {
|
||||||
|
openMarker: '@startditaa',
|
||||||
|
closeMarker: '@endditaa',
|
||||||
|
generateSource: function (umlCode) {
|
||||||
|
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
||||||
|
// Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
|
||||||
|
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
|
||||||
|
const s = unescape(encodeURIComponent(umlCode))
|
||||||
|
const zippedCode = deflate.encode64(
|
||||||
|
deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9)
|
||||||
|
)
|
||||||
|
return `${serverAddress}/${zippedCode}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Override task item
|
// Override task item
|
||||||
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||||
let content, terminate, i, l, token
|
let content, terminate, i, l, token
|
||||||
@@ -205,7 +251,11 @@ class Markdown {
|
|||||||
if (!liToken.attrs) {
|
if (!liToken.attrs) {
|
||||||
liToken.attrs = []
|
liToken.attrs = []
|
||||||
}
|
}
|
||||||
liToken.attrs.push(['class', 'taskListItem'])
|
if (config.preview.lineThroughCheckbox) {
|
||||||
|
liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`])
|
||||||
|
} else {
|
||||||
|
liToken.attrs.push(['class', 'taskListItem'])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
||||||
}
|
}
|
||||||
@@ -230,9 +280,12 @@ class Markdown {
|
|||||||
this.md.renderer.render = (tokens, options, env) => {
|
this.md.renderer.render = (tokens, options, env) => {
|
||||||
tokens.forEach((token) => {
|
tokens.forEach((token) => {
|
||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
case 'heading_open':
|
|
||||||
case 'paragraph_open':
|
|
||||||
case 'blockquote_open':
|
case 'blockquote_open':
|
||||||
|
case 'dd_open':
|
||||||
|
case 'dt_open':
|
||||||
|
case 'heading_open':
|
||||||
|
case 'list_item_open':
|
||||||
|
case 'paragraph_open':
|
||||||
case 'table_open':
|
case 'table_open':
|
||||||
token.attrPush(['data-line', token.map[0]])
|
token.attrPush(['data-line', token.map[0]])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function strip (input) {
|
|||||||
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
|
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
|
||||||
.replace(/>/g, '')
|
.replace(/>/g, '')
|
||||||
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
|
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
|
||||||
.replace(/^#{1,6}\s*([^#]*)\s*(#{1,6})?/gm, '$1')
|
.replace(/^#{1,6}\s*/gm, '')
|
||||||
.replace(/(`{3,})(.*?)\1/gm, '$2')
|
.replace(/(`{3,})(.*?)\1/gm, '$2')
|
||||||
.replace(/^-{3,}\s*$/g, '')
|
.replace(/^-{3,}\s*$/g, '')
|
||||||
.replace(/`(.+?)`/g, '$1')
|
.replace(/`(.+?)`/g, '$1')
|
||||||
|
|||||||
80
browser/lib/newNote.js
Normal file
80
browser/lib/newNote.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { hashHistory } from 'react-router'
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
|
||||||
|
export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||||
|
|
||||||
|
let tags = []
|
||||||
|
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
|
||||||
|
tags = params.tagname.split(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataApi
|
||||||
|
.createNote(storage, {
|
||||||
|
type: 'MARKDOWN_NOTE',
|
||||||
|
folder: folder,
|
||||||
|
title: '',
|
||||||
|
tags,
|
||||||
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
|
})
|
||||||
|
.then(note => {
|
||||||
|
const noteHash = note.key
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
|
||||||
|
hashHistory.push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: { key: noteHash }
|
||||||
|
})
|
||||||
|
ee.emit('list:jump', noteHash)
|
||||||
|
ee.emit('detail:focus')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSnippetNote (storage, folder, dispatch, location, params, config) {
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||||
|
|
||||||
|
let tags = []
|
||||||
|
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
|
||||||
|
tags = params.tagname.split(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLanguage = config.editor.snippetDefaultLanguage === 'Auto Detect' ? null : config.editor.snippetDefaultLanguage
|
||||||
|
|
||||||
|
return dataApi
|
||||||
|
.createNote(storage, {
|
||||||
|
type: 'SNIPPET_NOTE',
|
||||||
|
folder: folder,
|
||||||
|
title: '',
|
||||||
|
tags,
|
||||||
|
description: '',
|
||||||
|
snippets: [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
mode: defaultLanguage,
|
||||||
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(note => {
|
||||||
|
const noteHash = note.key
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
hashHistory.push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: { key: noteHash }
|
||||||
|
})
|
||||||
|
ee.emit('list:jump', noteHash)
|
||||||
|
ee.emit('detail:focus')
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -23,7 +23,9 @@ function findByWordOrTag (notes, block) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (note.type === 'SNIPPET_NOTE') {
|
if (note.type === 'SNIPPET_NOTE') {
|
||||||
return note.description.match(wordRegExp)
|
return note.description.match(wordRegExp) || note.snippets.some((snippet) => {
|
||||||
|
return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
|
||||||
|
})
|
||||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||||
return note.content.match(wordRegExp)
|
return note.content.match(wordRegExp)
|
||||||
}
|
}
|
||||||
|
|||||||
17
browser/lib/slugify.js
Normal file
17
browser/lib/slugify.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import diacritics from 'diacritics-map'
|
||||||
|
|
||||||
|
function replaceDiacritics (str) {
|
||||||
|
return str.replace(/[À-ž]/g, function (ch) {
|
||||||
|
return diacritics[ch] || ch
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function slugify (title) {
|
||||||
|
let slug = title.trim()
|
||||||
|
|
||||||
|
slug = replaceDiacritics(slug)
|
||||||
|
|
||||||
|
slug = slug.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
|
||||||
|
|
||||||
|
return encodeURI(slug).replace(/\-+$/, '')
|
||||||
|
}
|
||||||
232
browser/lib/spellcheck.js
Normal file
232
browser/lib/spellcheck.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import styles from '../components/CodeEditor.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const Typo = require('typo-js')
|
||||||
|
const _ = require('lodash')
|
||||||
|
|
||||||
|
const CSS_ERROR_CLASS = 'codeEditor-typo'
|
||||||
|
const SPELLCHECK_DISABLED = 'NONE'
|
||||||
|
const DICTIONARY_PATH = '../dictionaries'
|
||||||
|
const MILLISECONDS_TILL_LIVECHECK = 500
|
||||||
|
|
||||||
|
let dictionary = null
|
||||||
|
let self
|
||||||
|
|
||||||
|
function getAvailableDictionaries () {
|
||||||
|
return [
|
||||||
|
{label: i18n.__('Disabled'), value: SPELLCHECK_DISABLED},
|
||||||
|
{label: i18n.__('English'), value: 'en_GB'},
|
||||||
|
{label: i18n.__('German'), value: 'de_DE'},
|
||||||
|
{label: i18n.__('French'), value: 'fr_FR'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only to be used in the tests :)
|
||||||
|
*/
|
||||||
|
function setDictionaryForTestsOnly (newDictionary) {
|
||||||
|
dictionary = newDictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Initializes the spellcheck. It removes all existing marks of the current editor.
|
||||||
|
* If a language was given (i.e. lang !== this.SPELLCHECK_DISABLED) it will load the stated dictionary and use it to check the whole document.
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
* @param {String} lang on of the values from getAvailableDictionaries()-Method
|
||||||
|
*/
|
||||||
|
function setLanguage (editor, lang) {
|
||||||
|
self = this
|
||||||
|
dictionary = null
|
||||||
|
|
||||||
|
if (editor == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMarks = editor.getAllMarks() || []
|
||||||
|
for (const mark of existingMarks) {
|
||||||
|
mark.clear()
|
||||||
|
}
|
||||||
|
if (lang !== SPELLCHECK_DISABLED) {
|
||||||
|
dictionary = new Typo(lang, false, false, {
|
||||||
|
dictionaryPath: DICTIONARY_PATH,
|
||||||
|
asyncLoad: true,
|
||||||
|
loadedCallback: () =>
|
||||||
|
checkWholeDocument(editor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the whole content of the editor for typos
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
*/
|
||||||
|
function checkWholeDocument (editor) {
|
||||||
|
const lastLine = editor.lineCount() - 1
|
||||||
|
const textOfLastLine = editor.getLine(lastLine) || ''
|
||||||
|
const lastChar = textOfLastLine.length
|
||||||
|
const from = {line: 0, ch: 0}
|
||||||
|
const to = {line: lastLine, ch: lastChar}
|
||||||
|
checkMultiLineRange(editor, from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the given range for typos
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
* @param {line, ch} from starting position of the spellcheck
|
||||||
|
* @param {line, ch} to end position of the spellcheck
|
||||||
|
*/
|
||||||
|
function checkMultiLineRange (editor, from, to) {
|
||||||
|
function sortRange (pos1, pos2) {
|
||||||
|
if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) {
|
||||||
|
return {from: pos2, to: pos1}
|
||||||
|
}
|
||||||
|
return {from: pos1, to: pos2}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {from: smallerPos, to: higherPos} = sortRange(from, to)
|
||||||
|
for (let l = smallerPos.line; l <= higherPos.line; l++) {
|
||||||
|
const line = editor.getLine(l) || ''
|
||||||
|
let w = 0
|
||||||
|
if (l === smallerPos.line) {
|
||||||
|
w = smallerPos.ch
|
||||||
|
}
|
||||||
|
let wEnd = line.length
|
||||||
|
if (l === higherPos.line) {
|
||||||
|
wEnd = higherPos.ch
|
||||||
|
}
|
||||||
|
while (w <= wEnd) {
|
||||||
|
const wordRange = editor.findWordAt({line: l, ch: w})
|
||||||
|
self.checkWord(editor, wordRange)
|
||||||
|
w += (wordRange.head.ch - wordRange.anchor.ch) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Checks whether a certain range of characters in the editor (i.e. a word) contains a typo.
|
||||||
|
* If so the ranged will be marked with the class CSS_ERROR_CLASS.
|
||||||
|
* Note: Due to performance considerations, only words with more then 3 signs are checked.
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
* @param wordRange Object specifying the range that should be checked.
|
||||||
|
* Having the following structure: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code>
|
||||||
|
*/
|
||||||
|
function checkWord (editor, wordRange) {
|
||||||
|
const word = editor.getRange(wordRange.anchor, wordRange.head)
|
||||||
|
if (word == null || word.length <= 3) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!dictionary.check(word)) {
|
||||||
|
editor.markText(wordRange.anchor, wordRange.head, {className: styles[CSS_ERROR_CLASS]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the changes recently made (aka live check)
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
* @param fromChangeObject codeMirror changeObject describing the start of the editing
|
||||||
|
* @param toChangeObject codeMirror changeObject describing the end of the editing
|
||||||
|
*/
|
||||||
|
function checkChangeRange (editor, fromChangeObject, toChangeObject) {
|
||||||
|
/**
|
||||||
|
* Calculate the smallest respectively largest position as a start, resp. end, position and return it
|
||||||
|
* @param start CodeMirror change object
|
||||||
|
* @param end CodeMirror change object
|
||||||
|
* @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
|
||||||
|
*/
|
||||||
|
function getStartAndEnd (start, end) {
|
||||||
|
const possiblePositions = [start.from, start.to, end.from, end.to]
|
||||||
|
let smallest = start.from
|
||||||
|
let biggest = end.to
|
||||||
|
for (const currentPos of possiblePositions) {
|
||||||
|
if (currentPos.line < smallest.line || (currentPos.line === smallest.line && currentPos.ch < smallest.ch)) {
|
||||||
|
smallest = currentPos
|
||||||
|
}
|
||||||
|
if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) {
|
||||||
|
biggest = currentPos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {start: smallest, end: biggest}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dictionary === null || editor == null) { return }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject)
|
||||||
|
|
||||||
|
// Expand the range to include words after/before whitespaces
|
||||||
|
start.ch = Math.max(start.ch - 1, 0)
|
||||||
|
end.ch = end.ch + 1
|
||||||
|
|
||||||
|
// clean existing marks
|
||||||
|
const existingMarks = editor.findMarks(start, end) || []
|
||||||
|
for (const mark of existingMarks) {
|
||||||
|
mark.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.checkMultiLineRange(editor, start, end)
|
||||||
|
} catch (e) {
|
||||||
|
console.info('Error during the spell check. It might be due to problems figuring out the range of the new text..', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLiveSpellCheckFrom (changeObject) {
|
||||||
|
liveSpellCheckFrom = changeObject
|
||||||
|
}
|
||||||
|
let liveSpellCheckFrom
|
||||||
|
const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, {
|
||||||
|
'leading': true,
|
||||||
|
'trailing': false
|
||||||
|
})
|
||||||
|
const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, {
|
||||||
|
'leading': false,
|
||||||
|
'trailing': true
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
|
||||||
|
* @param {Codemirror} editor CodeMirror-Editor
|
||||||
|
* @param changeObject codeMirror changeObject
|
||||||
|
*/
|
||||||
|
function handleChange (editor, changeObject) {
|
||||||
|
if (dictionary === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
debouncedSpellCheckLeading(changeObject)
|
||||||
|
debouncedSpellCheck(editor, liveSpellCheckFrom, changeObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of spelling suggestions for the given (wrong written) word.
|
||||||
|
* Returns an empty array if the dictionary is null (=> spellcheck is disabled) or the given word was null
|
||||||
|
* @param word word to be checked
|
||||||
|
* @returns {String[]} Array of suggestions
|
||||||
|
*/
|
||||||
|
function getSpellingSuggestion (word) {
|
||||||
|
if (dictionary == null || word == null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return dictionary.suggest(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the CSS class used for errors
|
||||||
|
*/
|
||||||
|
function getCSSClassName () {
|
||||||
|
return styles[CSS_ERROR_CLASS]
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DICTIONARY_PATH,
|
||||||
|
CSS_ERROR_CLASS,
|
||||||
|
SPELLCHECK_DISABLED,
|
||||||
|
getAvailableDictionaries,
|
||||||
|
setLanguage,
|
||||||
|
checkChangeRange,
|
||||||
|
handleChange,
|
||||||
|
getSpellingSuggestion,
|
||||||
|
checkWord,
|
||||||
|
checkMultiLineRange,
|
||||||
|
checkWholeDocument,
|
||||||
|
setDictionaryForTestsOnly,
|
||||||
|
getCSSClassName
|
||||||
|
}
|
||||||
@@ -6,8 +6,12 @@ export function lastFindInArray (array, callback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
export function escapeHtmlCharacters (
|
||||||
|
html,
|
||||||
|
opt = { detectCodeBlock: false, skipSingleQuote: false }
|
||||||
|
) {
|
||||||
const matchHtmlRegExp = /["'&<>]/g
|
const matchHtmlRegExp = /["'&<>]/g
|
||||||
|
const matchCodeBlockRegExp = /```/g
|
||||||
const escapes = ['"', '&', ''', '<', '>']
|
const escapes = ['"', '&', ''', '<', '>']
|
||||||
let match = null
|
let match = null
|
||||||
const replaceAt = (str, index, replace) =>
|
const replaceAt = (str, index, replace) =>
|
||||||
@@ -15,11 +19,18 @@ export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
|||||||
replace +
|
replace +
|
||||||
str.substr(index + replace.length - (replace.length - 1))
|
str.substr(index + replace.length - (replace.length - 1))
|
||||||
|
|
||||||
// detecting code block
|
while ((match = matchHtmlRegExp.exec(html)) !== null) {
|
||||||
while ((match = matchHtmlRegExp.exec(html)) != null) {
|
|
||||||
const current = { char: match[0], index: match.index }
|
const current = { char: match[0], index: match.index }
|
||||||
|
const codeBlockIndexs = []
|
||||||
|
let openCodeBlock = null
|
||||||
|
// if the detectCodeBlock option is activated then this function should skip
|
||||||
|
// characters that needed to be escape but located in code block
|
||||||
if (opt.detectCodeBlock) {
|
if (opt.detectCodeBlock) {
|
||||||
// position of the nearest line start
|
// The first type of code block is lines that start with 4 spaces
|
||||||
|
// Here we check for the \n character located before the character that
|
||||||
|
// needed to be escape. It means we check for the begining of the line that
|
||||||
|
// contain that character, then we check if there are 4 spaces next to the
|
||||||
|
// \n character (the line start with 4 spaces)
|
||||||
let previousLineEnd = current.index - 1
|
let previousLineEnd = current.index - 1
|
||||||
while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) {
|
while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) {
|
||||||
previousLineEnd--
|
previousLineEnd--
|
||||||
@@ -31,16 +42,54 @@ export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
|||||||
html[previousLineEnd + 3] === ' ' &&
|
html[previousLineEnd + 3] === ' ' &&
|
||||||
html[previousLineEnd + 4] === ' '
|
html[previousLineEnd + 4] === ' '
|
||||||
) {
|
) {
|
||||||
// so skip it
|
// skip the current character
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// The second type of code block is lines that wrapped in ```
|
||||||
|
// We will get the position of each ```
|
||||||
|
// then push it into an array
|
||||||
|
// then the array returned will be like this:
|
||||||
|
// [startCodeblock, endCodeBlock, startCodeBlock, endCodeBlock]
|
||||||
|
while ((openCodeBlock = matchCodeBlockRegExp.exec(html)) !== null) {
|
||||||
|
codeBlockIndexs.push(openCodeBlock.index)
|
||||||
|
}
|
||||||
|
let shouldSkipChar = false
|
||||||
|
// we loop through the array of positions
|
||||||
|
// we skip 2 element as the i index position is the position of ``` that
|
||||||
|
// open the codeblock and the i + 1 is the position of the ``` that close
|
||||||
|
// the code block
|
||||||
|
for (let i = 0; i < codeBlockIndexs.length; i += 2) {
|
||||||
|
// the i index position is the position of the ``` that open code block
|
||||||
|
// so we have to + 2 as that position is the position of the first ` in the ````
|
||||||
|
// but we need to make sure that the position current character is larger
|
||||||
|
// that the last ` in the ``` that open the code block so we have to take
|
||||||
|
// the position of the first ` and + 2
|
||||||
|
// the i + 1 index position is the closing ``` so the char must less than it
|
||||||
|
if (
|
||||||
|
current.index > codeBlockIndexs[i] + 2 &&
|
||||||
|
current.index < codeBlockIndexs[i + 1]
|
||||||
|
) {
|
||||||
|
// skip it
|
||||||
|
shouldSkipChar = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldSkipChar) {
|
||||||
|
// skip the current character
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// otherwise, escape it !!!
|
// otherwise, escape it !!!
|
||||||
if (current.char === '&') {
|
if (current.char === '&') {
|
||||||
|
// when escaping character & we have to be becareful as the & could be a part
|
||||||
|
// of an escaped character like " will be came &quot;
|
||||||
let nextStr = ''
|
let nextStr = ''
|
||||||
let nextIndex = current.index
|
let nextIndex = current.index
|
||||||
let escapedStr = false
|
let escapedStr = false
|
||||||
// maximum length of an escape string is 5. For example ('"')
|
// maximum length of an escaped string is 5. For example ('"')
|
||||||
|
// we take the next 5 character of the next string if it is one of the string:
|
||||||
|
// ['"', '&', ''', '<', '>'] then we will not escape the & character
|
||||||
|
// as it is a part of the escaped string and should not be escaped
|
||||||
while (nextStr.length <= 5) {
|
while (nextStr.length <= 5) {
|
||||||
nextStr += html[nextIndex]
|
nextStr += html[nextIndex]
|
||||||
nextIndex++
|
nextIndex++
|
||||||
@@ -55,7 +104,7 @@ export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
|||||||
}
|
}
|
||||||
} else if (current.char === '"') {
|
} else if (current.char === '"') {
|
||||||
html = replaceAt(html, current.index, '"')
|
html = replaceAt(html, current.index, '"')
|
||||||
} else if (current.char === "'") {
|
} else if (current.char === "'" && !opt.skipSingleQuote) {
|
||||||
html = replaceAt(html, current.index, ''')
|
html = replaceAt(html, current.index, ''')
|
||||||
} else if (current.char === '<') {
|
} else if (current.char === '<') {
|
||||||
html = replaceAt(html, current.index, '<')
|
html = replaceAt(html, current.index, '<')
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ body[data-theme="dark"]
|
|||||||
border-left 1px solid $ui-dark-borderColor
|
border-left 1px solid $ui-dark-borderColor
|
||||||
.empty-message
|
.empty-message
|
||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
body[data-theme="solarized-dark"]
|
body[data-theme="solarized-dark"]
|
||||||
.root
|
.root
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
@@ -37,3 +37,10 @@ body[data-theme="monokai"]
|
|||||||
border-left 1px solid $ui-monokai-borderColor
|
border-left 1px solid $ui-monokai-borderColor
|
||||||
.empty-message
|
.empty-message
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
border-left 1px solid $ui-dracula-borderColor
|
||||||
|
.empty-message
|
||||||
|
color $ui-dracula-text-color
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
height 34px
|
height 34px
|
||||||
width 20px
|
width 20px
|
||||||
line-height 34px
|
line-height 34px
|
||||||
|
|
||||||
.search-input
|
.search-input
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
position relative
|
position relative
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
cursor pointer
|
cursor pointer
|
||||||
&:hover
|
&:hover
|
||||||
background-color $ui-button--hover-backgroundColor
|
background-color $ui-button--hover-backgroundColor
|
||||||
|
|
||||||
.search-optionList-item--active
|
.search-optionList-item--active
|
||||||
@extend .search-optionList-item
|
@extend .search-optionList-item
|
||||||
@@ -159,3 +159,29 @@ body[data-theme="monokai"]
|
|||||||
color $ui-monokai-button--active-color
|
color $ui-monokai-button--active-color
|
||||||
.search-optionList-item-name-surfix
|
.search-optionList-item-name-surfix
|
||||||
color $ui-monokai-inactive-text-color
|
color $ui-monokai-inactive-text-color
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:hover
|
||||||
|
color #f8f8f2
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
|
||||||
|
.search-optionList
|
||||||
|
color #f8f8f2
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-button-backgroundColor
|
||||||
|
|
||||||
|
.search-optionList-item
|
||||||
|
&:hover
|
||||||
|
background-color lighten($ui-dracula-button--hover-backgroundColor, 15%)
|
||||||
|
|
||||||
|
.search-optionList-item--active
|
||||||
|
background-color $ui-dracula-button--active-backgroundColor
|
||||||
|
color $ui-dracula-button--active-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
|
color $ui-dracula-button--active-color
|
||||||
|
.search-optionList-item-name-surfix
|
||||||
|
color $ui-dracula-inactive-text-color
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ import styles from './FullscreenButton.styl'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
|
||||||
const FullscreenButton = ({
|
const FullscreenButton = ({
|
||||||
onClick
|
onClick
|
||||||
}) => (
|
}) => {
|
||||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
return (
|
||||||
<span styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
||||||
</button>
|
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||||
)
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
FullscreenButton.propTypes = {
|
FullscreenButton.propTypes = {
|
||||||
onClick: PropTypes.func.isRequired
|
onClick: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
|
||||||
|
.tooltip:lang(ja)
|
||||||
|
@extend .tooltip
|
||||||
|
right 35px
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.control-fullScreenButton
|
.control-fullScreenButton
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
@@ -70,22 +70,22 @@ class InfoPanel extends React.Component {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div id='export-wrap'>
|
<div id='export-wrap'>
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
|
||||||
<i className='fa fa-file-code-o' />
|
<i className='fa fa-file-code-o' />
|
||||||
<p>{i18n.__('.md')}</p>
|
<p>{i18n.__('.md')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
|
||||||
<i className='fa fa-file-text-o' />
|
<i className='fa fa-file-text-o' />
|
||||||
<p>{i18n.__('.txt')}</p>
|
<p>{i18n.__('.txt')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
||||||
<i className='fa fa-html5' />
|
<i className='fa fa-html5' />
|
||||||
<p>{i18n.__('.html')}</p>
|
<p>{i18n.__('.html')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => print(e)}>
|
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
|
||||||
<i className='fa fa-print' />
|
<i className='fa fa-print' />
|
||||||
<p>{i18n.__('Print')}</p>
|
<p>{i18n.__('Print')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
.control-infoButton-panel-trash
|
.control-infoButton-panel-trash
|
||||||
z-index 200
|
z-index 200
|
||||||
margin-top 0px
|
margin-top 0px
|
||||||
|
top 50px
|
||||||
right 0px
|
right 0px
|
||||||
position absolute
|
position absolute
|
||||||
padding 20px 25px 0 25px
|
padding 20px 25px 0 25px
|
||||||
@@ -256,3 +257,43 @@ body[data-theme="monokai"]
|
|||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-inactive-text-color
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.control-infoButton-panel
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control-infoButton-panel-trash
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
|
||||||
|
.modification-date
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.modification-date-desc
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.infoPanel-defaul-count
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.infoPanel-sub-count
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.infoPanel-default
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.infoPanel-sub
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.infoPanel-noteLink
|
||||||
|
background-color alpha($ui-dracula-borderColor, 20%)
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
[id=export-wrap]
|
||||||
|
button
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-dracula-borderColor, 20%)
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
p
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
color $ui-dracula-text-color
|
||||||
@@ -31,17 +31,17 @@ const InfoPanelTrashed = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id='export-wrap'>
|
<div id='export-wrap'>
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
|
||||||
<i className='fa fa-file-code-o' />
|
<i className='fa fa-file-code-o' />
|
||||||
<p>.md</p>
|
<p>.md</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
|
||||||
<i className='fa fa-file-text-o' />
|
<i className='fa fa-file-text-o' />
|
||||||
<p>.txt</p>
|
<p>.txt</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
||||||
<i className='fa fa-html5' />
|
<i className='fa fa-html5' />
|
||||||
<p>.html</p>
|
<p>.html</p>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { formatDate } from 'browser/lib/date-formatter'
|
|||||||
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||||
import striptags from 'striptags'
|
import striptags from 'striptags'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
|
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||||
|
|
||||||
class MarkdownNoteDetail extends React.Component {
|
class MarkdownNoteDetail extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -38,15 +39,19 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
isMovingNote: false,
|
isMovingNote: false,
|
||||||
note: Object.assign({
|
note: Object.assign({
|
||||||
title: '',
|
title: '',
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}, props.note),
|
}, props.note),
|
||||||
isLockButtonShown: false,
|
isLockButtonShown: props.config.editor.type !== 'SPLIT',
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
editorType: props.config.editor.type
|
editorType: props.config.editor.type,
|
||||||
|
switchPreview: props.config.editor.switchPreview
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchTimer = null
|
this.dispatchTimer = null
|
||||||
|
|
||||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||||
|
this.generateToc = () => this.handleGenerateToc()
|
||||||
}
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
@@ -59,13 +64,20 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
||||||
this.handleSwitchMode(reversedType)
|
this.handleSwitchMode(reversedType)
|
||||||
})
|
})
|
||||||
|
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
|
||||||
|
ee.on('code:generate-toc', this.generateToc)
|
||||||
|
|
||||||
|
// Focus content if using blur or double click
|
||||||
|
if (this.state.switchPreview === 'BLUR' || this.state.switchPreview === 'DBL_CLICK') this.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
|
const isNewNote = nextProps.note.key !== this.props.note.key
|
||||||
|
const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length
|
||||||
|
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
|
||||||
if (this.saveQueue != null) this.saveNow()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
this.setState({
|
this.setState({
|
||||||
note: Object.assign({}, nextProps.note)
|
note: Object.assign({linesHighlighted: []}, nextProps.note)
|
||||||
}, () => {
|
}, () => {
|
||||||
this.refs.content.reload()
|
this.refs.content.reload()
|
||||||
if (this.refs.tags) this.refs.tags.reset()
|
if (this.refs.tags) this.refs.tags.reset()
|
||||||
@@ -75,6 +87,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||||
|
ee.off('code:generate-toc', this.generateToc)
|
||||||
if (this.saveQueue != null) this.saveNow()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +100,12 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
handleUpdateContent () {
|
handleUpdateContent () {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
note.content = this.refs.content.value
|
note.content = this.refs.content.value
|
||||||
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
|
|
||||||
|
let title = findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)
|
||||||
|
title = striptags(title)
|
||||||
|
title = markdown.strip(title)
|
||||||
|
note.title = title
|
||||||
|
|
||||||
this.updateNote(note)
|
this.updateNote(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +201,36 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ee.emit('export:save-html')
|
ee.emit('export:save-html')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleKeyDown (e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
// tab key
|
||||||
|
case 9:
|
||||||
|
if (e.ctrlKey && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.jumpNextTab()
|
||||||
|
} else if (e.ctrlKey && e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.jumpPrevTab()
|
||||||
|
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.focusEditor()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
// I key
|
||||||
|
case 73:
|
||||||
|
{
|
||||||
|
const isSuper = global.process.platform === 'darwin'
|
||||||
|
? e.metaKey
|
||||||
|
: e.ctrlKey
|
||||||
|
if (isSuper) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.handleInfoButtonClick(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleTrashButtonClick (e) {
|
handleTrashButtonClick (e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
const { isTrashed } = note
|
const { isTrashed } = note
|
||||||
@@ -255,13 +303,18 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleToggleLockButton (event, noteStatus) {
|
handleToggleLockButton (event, noteStatus) {
|
||||||
// first argument event is not used
|
// first argument event is not used
|
||||||
if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
|
if (noteStatus === 'CODE') {
|
||||||
this.setState({isLockButtonShown: true})
|
this.setState({isLockButtonShown: true})
|
||||||
} else {
|
} else {
|
||||||
this.setState({isLockButtonShown: false})
|
this.setState({isLockButtonShown: false})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleGenerateToc () {
|
||||||
|
const editor = this.refs.content.refs.code.editor
|
||||||
|
markdownToc.generateInEditor(editor)
|
||||||
|
}
|
||||||
|
|
||||||
handleFocus (e) {
|
handleFocus (e) {
|
||||||
this.focus()
|
this.focus()
|
||||||
}
|
}
|
||||||
@@ -276,7 +329,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSwitchMode (type) {
|
handleSwitchMode (type) {
|
||||||
this.setState({ editorType: type }, () => {
|
// If in split mode, hide the lock button
|
||||||
|
this.setState({ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, () => {
|
||||||
this.focus()
|
this.focus()
|
||||||
const newConfig = Object.assign({}, this.props.config)
|
const newConfig = Object.assign({}, this.props.config)
|
||||||
newConfig.editor.type = type
|
newConfig.editor.type = type
|
||||||
@@ -284,9 +338,33 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDeleteNote () {
|
||||||
|
this.handleTrashButtonClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClearTodo () {
|
||||||
|
const { note } = this.state
|
||||||
|
const splitted = note.content.split('\n')
|
||||||
|
|
||||||
|
const clearTodoContent = splitted.map((line) => {
|
||||||
|
const trimmedLine = line.trim()
|
||||||
|
if (trimmedLine.match(/\[x\]/i)) {
|
||||||
|
return line.replace(/\[x\]/i, '[ ]')
|
||||||
|
} else {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
}).join('\n')
|
||||||
|
|
||||||
|
note.content = clearTodoContent
|
||||||
|
this.refs.content.setValue(note.content)
|
||||||
|
|
||||||
|
this.updateNote(note)
|
||||||
|
}
|
||||||
|
|
||||||
renderEditor () {
|
renderEditor () {
|
||||||
const { config, ignorePreviewPointerEvents } = this.props
|
const { config, ignorePreviewPointerEvents } = this.props
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
||||||
return <MarkdownEditor
|
return <MarkdownEditor
|
||||||
ref='content'
|
ref='content'
|
||||||
@@ -295,7 +373,9 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
value={note.content}
|
value={note.content}
|
||||||
storageKey={note.storage}
|
storageKey={note.storage}
|
||||||
noteKey={note.key}
|
noteKey={note.key}
|
||||||
|
linesHighlighted={note.linesHighlighted}
|
||||||
onChange={this.handleUpdateContent.bind(this)}
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
|
isLocked={this.state.isLocked}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
@@ -305,6 +385,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
value={note.content}
|
value={note.content}
|
||||||
storageKey={note.storage}
|
storageKey={note.storage}
|
||||||
noteKey={note.key}
|
noteKey={note.key}
|
||||||
|
linesHighlighted={note.linesHighlighted}
|
||||||
onChange={this.handleUpdateContent.bind(this)}
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
/>
|
/>
|
||||||
@@ -312,7 +393,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { data, location } = this.props
|
const { data, location, config } = this.props
|
||||||
const { note, editorType } = this.state
|
const { note, editorType } = this.state
|
||||||
const storageKey = note.storage
|
const storageKey = note.storage
|
||||||
const folderKey = note.folder
|
const folderKey = note.folder
|
||||||
@@ -363,9 +444,13 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
<TagSelect
|
<TagSelect
|
||||||
ref='tags'
|
ref='tags'
|
||||||
value={this.state.note.tags}
|
value={this.state.note.tags}
|
||||||
|
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||||
|
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||||
|
data={data}
|
||||||
onChange={this.handleUpdateTag.bind(this)}
|
onChange={this.handleUpdateTag.bind(this)}
|
||||||
|
coloredTags={config.coloredTags}
|
||||||
/>
|
/>
|
||||||
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||||
@@ -419,6 +504,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
<div className='NoteDetail'
|
<div className='NoteDetail'
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
styleName='root'
|
styleName='root'
|
||||||
|
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||||
>
|
>
|
||||||
|
|
||||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||||
|
|||||||
@@ -76,3 +76,8 @@ body[data-theme="monokai"]
|
|||||||
.root
|
.root
|
||||||
border-left 1px solid $ui-monokai-borderColor
|
border-left 1px solid $ui-monokai-borderColor
|
||||||
background-color $ui-monokai-noteDetail-backgroundColor
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
border-left 1px solid $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
@@ -13,6 +13,7 @@ $info-margin-under-border = 30px
|
|||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
padding 0 20px
|
padding 0 20px
|
||||||
|
z-index 99
|
||||||
|
|
||||||
.info-left
|
.info-left
|
||||||
padding 0 10px
|
padding 0 10px
|
||||||
@@ -97,8 +98,13 @@ body[data-theme="solarized-dark"]
|
|||||||
.info
|
.info
|
||||||
border-color $ui-solarized-dark-borderColor
|
border-color $ui-solarized-dark-borderColor
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
body[data-theme="monokai"]
|
body[data-theme="monokai"]
|
||||||
.info
|
.info
|
||||||
border-color $ui-monokai-borderColor
|
border-color $ui-monokai-borderColor
|
||||||
background-color $ui-monokai-noteDetail-backgroundColor
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.info
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
@@ -20,6 +20,7 @@ import _ from 'lodash'
|
|||||||
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
||||||
import convertModeName from 'browser/lib/convertModeName'
|
import convertModeName from 'browser/lib/convertModeName'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
import FullscreenButton from './FullscreenButton'
|
||||||
import TrashButton from './TrashButton'
|
import TrashButton from './TrashButton'
|
||||||
import RestoreButton from './RestoreButton'
|
import RestoreButton from './RestoreButton'
|
||||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||||
@@ -29,6 +30,7 @@ import InfoPanelTrashed from './InfoPanelTrashed'
|
|||||||
import { formatDate } from 'browser/lib/date-formatter'
|
import { formatDate } from 'browser/lib/date-formatter'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
|
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote } = electron
|
const { remote } = electron
|
||||||
@@ -47,11 +49,12 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
note: Object.assign({
|
note: Object.assign({
|
||||||
description: ''
|
description: ''
|
||||||
}, props.note, {
|
}, props.note, {
|
||||||
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
snippets: props.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scrollToNextTabThreshold = 0.7
|
this.scrollToNextTabThreshold = 0.7
|
||||||
|
this.generateToc = () => this.handleGenerateToc()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -65,6 +68,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
enableLeftArrow: allTabs.offsetLeft !== 0
|
enableLeftArrow: allTabs.offsetLeft !== 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
ee.on('code:generate-toc', this.generateToc)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
@@ -73,8 +77,9 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const nextNote = Object.assign({
|
const nextNote = Object.assign({
|
||||||
description: ''
|
description: ''
|
||||||
}, nextProps.note, {
|
}, nextProps.note, {
|
||||||
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
|
snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
|
||||||
})
|
})
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
snippetIndex: 0,
|
snippetIndex: 0,
|
||||||
note: nextNote
|
note: nextNote
|
||||||
@@ -91,6 +96,16 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
if (this.saveQueue != null) this.saveNow()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
|
ee.off('code:generate-toc', this.generateToc)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleGenerateToc () {
|
||||||
|
const { note, snippetIndex } = this.state
|
||||||
|
const currentMode = note.snippets[snippetIndex].mode
|
||||||
|
if (currentMode.includes('Markdown')) {
|
||||||
|
const currentEditor = this.refs[`code-${snippetIndex}`].refs.code.editor
|
||||||
|
markdownToc.generateInEditor(currentEditor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange (e) {
|
handleChange (e) {
|
||||||
@@ -99,7 +114,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||||
note.description = this.refs.description.value
|
note.description = this.refs.description.value
|
||||||
note.updatedAt = new Date()
|
note.updatedAt = new Date()
|
||||||
note.title = findNoteTitle(note.description)
|
note.title = findNoteTitle(note.description, false)
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
note
|
note
|
||||||
@@ -341,12 +356,10 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.refs['code-' + this.state.snippetIndex].reload()
|
this.refs['code-' + this.state.snippetIndex].reload()
|
||||||
|
|
||||||
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
|
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
|
||||||
console.log('no need for arrows')
|
|
||||||
this.moveTabBarBy(0)
|
this.moveTabBarBy(0)
|
||||||
} else {
|
} else {
|
||||||
const lastTab = this.allTabs.lastChild
|
const lastTab = this.allTabs.lastChild
|
||||||
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
|
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
|
||||||
console.log('need to scroll')
|
|
||||||
const width = this.visibleTabs.offsetWidth
|
const width = this.visibleTabs.offsetWidth
|
||||||
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
|
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
|
||||||
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
|
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
|
||||||
@@ -399,6 +412,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
return (e) => {
|
return (e) => {
|
||||||
const snippets = this.state.note.snippets.slice()
|
const snippets = this.state.note.snippets.slice()
|
||||||
snippets[index].content = this.refs['code-' + index].value
|
snippets[index].content = this.refs['code-' + index].value
|
||||||
|
snippets[index].linesHighlighted = e.options.linesHighlighted
|
||||||
|
|
||||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||||
this.setState(state => ({
|
this.setState(state => ({
|
||||||
note: state.note
|
note: state.note
|
||||||
@@ -423,6 +438,18 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
// I key
|
||||||
|
case 73:
|
||||||
|
{
|
||||||
|
const isSuper = global.process.platform === 'darwin'
|
||||||
|
? e.metaKey
|
||||||
|
: e.ctrlKey
|
||||||
|
if (isSuper) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.handleInfoButtonClick(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
// L key
|
// L key
|
||||||
case 76:
|
case 76:
|
||||||
{
|
{
|
||||||
@@ -441,7 +468,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const isSuper = global.process.platform === 'darwin'
|
const isSuper = global.process.platform === 'darwin'
|
||||||
? e.metaKey
|
? e.metaKey
|
||||||
: e.ctrlKey
|
: e.ctrlKey
|
||||||
if (isSuper) {
|
if (isSuper && !e.shiftKey && !e.altKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.addSnippet()
|
this.addSnippet()
|
||||||
}
|
}
|
||||||
@@ -573,12 +600,16 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSnippet () {
|
addSnippet () {
|
||||||
|
const { config: { editor: { snippetDefaultLanguage } } } = this.props
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
|
const defaultLanguage = snippetDefaultLanguage === 'Auto Detect' ? null : snippetDefaultLanguage
|
||||||
|
|
||||||
note.snippets = note.snippets.concat([{
|
note.snippets = note.snippets.concat([{
|
||||||
name: '',
|
name: '',
|
||||||
mode: 'Plain Text',
|
mode: defaultLanguage,
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}])
|
}])
|
||||||
const snippetIndex = note.snippets.length - 1
|
const snippetIndex = note.snippets.length - 1
|
||||||
|
|
||||||
@@ -613,7 +644,6 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focusEditor () {
|
focusEditor () {
|
||||||
console.log('code-' + this.state.snippetIndex)
|
|
||||||
this.refs['code-' + this.state.snippetIndex].focus()
|
this.refs['code-' + this.state.snippetIndex].focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,11 +652,18 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
showWarning () {
|
showWarning (e, msg) {
|
||||||
|
const warningMessage = (msg) => ({
|
||||||
|
'export-txt': 'Text export',
|
||||||
|
'export-md': 'Markdown export',
|
||||||
|
'export-html': 'HTML export',
|
||||||
|
'print': 'Print'
|
||||||
|
})[msg]
|
||||||
|
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Sorry!'),
|
message: i18n.__('Sorry!'),
|
||||||
detail: i18n.__('md/text import is available only a markdown note.'),
|
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
||||||
buttons: [i18n.__('OK')]
|
buttons: [i18n.__('OK')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -638,6 +675,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const storageKey = note.storage
|
const storageKey = note.storage
|
||||||
const folderKey = note.folder
|
const folderKey = note.folder
|
||||||
|
|
||||||
|
const autoDetect = config.editor.snippetDefaultLanguage === 'Auto Detect'
|
||||||
|
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||||
@@ -662,10 +701,6 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
const viewList = note.snippets.map((snippet, index) => {
|
const viewList = note.snippets.map((snippet, index) => {
|
||||||
const isActive = this.state.snippetIndex === index
|
const isActive = this.state.snippetIndex === index
|
||||||
|
|
||||||
let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
|
|
||||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
|
||||||
|
|
||||||
return <div styleName='tabView'
|
return <div styleName='tabView'
|
||||||
key={index}
|
key={index}
|
||||||
style={{zIndex: isActive ? 5 : 4}}
|
style={{zIndex: isActive ? 5 : 4}}
|
||||||
@@ -674,25 +709,34 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
? <MarkdownEditor styleName='tabView-content'
|
? <MarkdownEditor styleName='tabView-content'
|
||||||
value={snippet.content}
|
value={snippet.content}
|
||||||
config={config}
|
config={config}
|
||||||
|
linesHighlighted={snippet.linesHighlighted}
|
||||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||||
ref={'code-' + index}
|
ref={'code-' + index}
|
||||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
/>
|
/>
|
||||||
: <CodeEditor styleName='tabView-content'
|
: <CodeEditor styleName='tabView-content'
|
||||||
mode={snippet.mode}
|
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
|
||||||
value={snippet.content}
|
value={snippet.content}
|
||||||
|
linesHighlighted={snippet.linesHighlighted}
|
||||||
theme={config.editor.theme}
|
theme={config.editor.theme}
|
||||||
fontFamily={config.editor.fontFamily}
|
fontFamily={config.editor.fontFamily}
|
||||||
fontSize={editorFontSize}
|
fontSize={editorFontSize}
|
||||||
indentType={config.editor.indentType}
|
indentType={config.editor.indentType}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
matchingPairs={config.editor.matchingPairs}
|
||||||
|
matchingTriples={config.editor.matchingTriples}
|
||||||
|
explodingPairs={config.editor.explodingPairs}
|
||||||
keyMap={config.editor.keyMap}
|
keyMap={config.editor.keyMap}
|
||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
|
enableTableEditor={config.editor.enableTableEditor}
|
||||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||||
ref={'code-' + index}
|
ref={'code-' + index}
|
||||||
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
|
hotkey={config.hotkey}
|
||||||
|
autoDetect={autoDetect}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -744,7 +788,11 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
<TagSelect
|
<TagSelect
|
||||||
ref='tags'
|
ref='tags'
|
||||||
value={this.state.note.tags}
|
value={this.state.note.tags}
|
||||||
|
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||||
|
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||||
|
data={data}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
|
coloredTags={config.coloredTags}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
@@ -753,11 +801,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
|
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
|
||||||
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
|
||||||
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
|
|
||||||
@@ -773,7 +817,9 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.showWarning}
|
exportAsMd={this.showWarning}
|
||||||
exportAsTxt={this.showWarning}
|
exportAsTxt={this.showWarning}
|
||||||
|
exportAsHtml={this.showWarning}
|
||||||
type={note.type}
|
type={note.type}
|
||||||
|
print={this.showWarning}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
.tabList
|
.tabList
|
||||||
absolute left right
|
absolute left right
|
||||||
top 55px
|
top 70px
|
||||||
height 30px
|
height 30px
|
||||||
display flex
|
display flex
|
||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
@@ -57,6 +57,9 @@
|
|||||||
.tabList .tabButton
|
.tabList .tabButton
|
||||||
navWhiteButtonColor()
|
navWhiteButtonColor()
|
||||||
width 30px
|
width 30px
|
||||||
|
border-left 1px solid $ui-borderColor
|
||||||
|
border-top 1px solid $ui-borderColor
|
||||||
|
border-right 1px solid $ui-borderColor
|
||||||
|
|
||||||
.tabView
|
.tabView
|
||||||
absolute left right bottom
|
absolute left right bottom
|
||||||
@@ -98,17 +101,34 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"], body[data-theme="default"]
|
||||||
.root
|
.root
|
||||||
box-shadow $note-detail-box-shadow
|
box-shadow $note-detail-box-shadow
|
||||||
border none
|
border none
|
||||||
|
|
||||||
|
.tabButton
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||||
|
color $ui-text-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
border-left 1px solid $ui-dark-borderColor
|
border-left 1px solid $ui-dark-borderColor
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
box-shadow none
|
box-shadow none
|
||||||
|
|
||||||
|
.tabList .tabButton
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
|
||||||
|
.tabButton
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
color $ui-dark-text-color
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
.body
|
.body
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
@@ -118,7 +138,6 @@ body[data-theme="dark"]
|
|||||||
border 1px solid $ui-dark-borderColor
|
border 1px solid $ui-dark-borderColor
|
||||||
|
|
||||||
.tabList
|
.tabList
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
.tabList .list
|
.tabList .list
|
||||||
@@ -150,6 +169,15 @@ body[data-theme="solarized-dark"]
|
|||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
border 1px solid $ui-solarized-dark-borderColor
|
border 1px solid $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
.tabList .tabButton
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
.tabButton
|
||||||
|
&:hover
|
||||||
|
color $ui-solarized-dark-button--active-color
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
transition 0.15s
|
||||||
|
|
||||||
.tabList
|
.tabList
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
@@ -167,6 +195,39 @@ body[data-theme="monokai"]
|
|||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
border 1px solid $ui-monokai-borderColor
|
border 1px solid $ui-monokai-borderColor
|
||||||
|
|
||||||
|
.tabList .tabButton
|
||||||
|
border-color $ui-monokai-borderColor
|
||||||
|
|
||||||
|
.tabButton
|
||||||
|
&:hover
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
|
||||||
.tabList
|
.tabList
|
||||||
background-color $ui-monokai-noteDetail-backgroundColor
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
border-left 1px solid $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
.body
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
.body .description textarea
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
border 1px solid $ui-dracula-borderColor
|
||||||
|
|
||||||
|
.tabList .tabButton
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
|
||||||
|
.tabButton
|
||||||
|
&:hover
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
.tabList
|
||||||
|
background-color $ui-dracula-noteDetail-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
@@ -54,7 +54,7 @@ class StarButton extends React.Component {
|
|||||||
: '../resources/icon/icon-star.svg'
|
: '../resources/icon/icon-star.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span styleName='tooltip'>{i18n.__('Star')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Star')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
|
||||||
|
.tooltip:lang(ja)
|
||||||
|
@extend .tooltip
|
||||||
|
right 103px
|
||||||
|
width 70px
|
||||||
|
|
||||||
.root--active
|
.root--active
|
||||||
@extend .root
|
@extend .root
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
|
|||||||
@@ -1,65 +1,39 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import invertColor from 'invert-color'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TagSelect.styl'
|
import styles from './TagSelect.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
import Autosuggest from 'react-autosuggest'
|
||||||
|
|
||||||
class TagSelect extends React.Component {
|
class TagSelect extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
newTag: ''
|
newTag: '',
|
||||||
|
suggestions: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.handleAddTag = this.handleAddTag.bind(this)
|
||||||
|
this.onInputBlur = this.onInputBlur.bind(this)
|
||||||
|
this.onInputChange = this.onInputChange.bind(this)
|
||||||
|
this.onInputKeyDown = this.onInputKeyDown.bind(this)
|
||||||
|
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this)
|
||||||
|
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this)
|
||||||
|
this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
addNewTag (newTag) {
|
||||||
this.value = this.props.value
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this.value = this.props.value
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewTagInputKeyDown (e) {
|
|
||||||
switch (e.keyCode) {
|
|
||||||
case 9:
|
|
||||||
e.preventDefault()
|
|
||||||
this.submitTag()
|
|
||||||
break
|
|
||||||
case 13:
|
|
||||||
this.submitTag()
|
|
||||||
break
|
|
||||||
case 8:
|
|
||||||
if (this.refs.newTag.value.length === 0) {
|
|
||||||
this.removeLastTag()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewTagBlur (e) {
|
|
||||||
this.submitTag()
|
|
||||||
}
|
|
||||||
|
|
||||||
removeLastTag () {
|
|
||||||
this.removeTagByCallback((value) => {
|
|
||||||
value.pop()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
reset () {
|
|
||||||
this.setState({
|
|
||||||
newTag: ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
submitTag () {
|
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
||||||
let { value } = this.props
|
|
||||||
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
newTag = newTag.trim().replace(/ +/g, '_')
|
||||||
newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
|
if (newTag.charAt(0) === '#') {
|
||||||
|
newTag.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
if (newTag.length <= 0) {
|
if (newTag.length <= 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -68,11 +42,18 @@ class TagSelect extends React.Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let { value } = this.props
|
||||||
value = _.isArray(value)
|
value = _.isArray(value)
|
||||||
? value.slice()
|
? value.slice()
|
||||||
: []
|
: []
|
||||||
value.push(newTag)
|
|
||||||
value = _.uniq(value)
|
if (!_.includes(value, newTag)) {
|
||||||
|
value.push(newTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.saveTagsAlphabetically) {
|
||||||
|
value = _.sortBy(value)
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
newTag: ''
|
newTag: ''
|
||||||
@@ -82,10 +63,41 @@ class TagSelect extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewTagInputChange (e) {
|
buildSuggestions () {
|
||||||
this.setState({
|
this.suggestions = _.sortBy(this.props.data.tagNoteMap.map(
|
||||||
newTag: this.refs.newTag.value
|
(tag, name) => ({
|
||||||
})
|
name,
|
||||||
|
nameLC: name.toLowerCase(),
|
||||||
|
size: tag.size
|
||||||
|
})
|
||||||
|
).filter(
|
||||||
|
tag => tag.size > 0
|
||||||
|
), ['name'])
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.value = this.props.value
|
||||||
|
|
||||||
|
this.buildSuggestions()
|
||||||
|
|
||||||
|
ee.on('editor:add-tag', this.handleAddTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate () {
|
||||||
|
this.value = this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
ee.off('editor:add-tag', this.handleAddTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddTag () {
|
||||||
|
this.refs.newTag.input.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTagLabelClick (tag) {
|
||||||
|
const { router } = this.context
|
||||||
|
router.push(`/tags/${tag}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagRemoveButtonClick (tag) {
|
handleTagRemoveButtonClick (tag) {
|
||||||
@@ -94,6 +106,60 @@ class TagSelect extends React.Component {
|
|||||||
}, tag)
|
}, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onInputBlur (e) {
|
||||||
|
this.submitNewTag()
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputChange (e, { newValue, method }) {
|
||||||
|
this.setState({
|
||||||
|
newTag: newValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputKeyDown (e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 9:
|
||||||
|
e.preventDefault()
|
||||||
|
this.submitNewTag()
|
||||||
|
break
|
||||||
|
case 13:
|
||||||
|
this.submitNewTag()
|
||||||
|
break
|
||||||
|
case 8:
|
||||||
|
if (this.state.newTag.length === 0) {
|
||||||
|
this.removeLastTag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuggestionsClearRequested () {
|
||||||
|
this.setState({
|
||||||
|
suggestions: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuggestionsFetchRequested ({ value }) {
|
||||||
|
const valueLC = value.toLowerCase()
|
||||||
|
const suggestions = _.filter(
|
||||||
|
this.suggestions,
|
||||||
|
tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
|
||||||
|
)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
suggestions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuggestionSelected (event, { suggestion, suggestionValue }) {
|
||||||
|
this.addNewTag(suggestionValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLastTag () {
|
||||||
|
this.removeTagByCallback((value) => {
|
||||||
|
value.pop()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
removeTagByCallback (callback, tag = null) {
|
removeTagByCallback (callback, tag = null) {
|
||||||
let { value } = this.props
|
let { value } = this.props
|
||||||
|
|
||||||
@@ -107,26 +173,55 @@ class TagSelect extends React.Component {
|
|||||||
this.props.onChange()
|
this.props.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset () {
|
||||||
|
this.buildSuggestions()
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
newTag: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
submitNewTag () {
|
||||||
|
this.addNewTag(this.refs.newTag.input.value)
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { value, className } = this.props
|
const { value, className, showTagsAlphabetically, coloredTags } = this.props
|
||||||
|
|
||||||
const tagList = _.isArray(value)
|
const tagList = _.isArray(value)
|
||||||
? value.map((tag) => {
|
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
|
||||||
|
const wrapperStyle = {}
|
||||||
|
const textStyle = {}
|
||||||
|
const BLACK = '#333333'
|
||||||
|
const WHITE = '#f1f1f1'
|
||||||
|
const color = coloredTags[tag]
|
||||||
|
const invertedColor = color && invertColor(color, { black: BLACK, white: WHITE })
|
||||||
|
let iconRemove = '../resources/icon/icon-x.svg'
|
||||||
|
if (color) {
|
||||||
|
wrapperStyle.backgroundColor = color
|
||||||
|
textStyle.color = invertedColor
|
||||||
|
}
|
||||||
|
if (invertedColor === WHITE) {
|
||||||
|
iconRemove = '../resources/icon/icon-x-light.svg'
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<span styleName='tag'
|
<span styleName='tag'
|
||||||
key={tag}
|
key={tag}
|
||||||
|
style={wrapperStyle}
|
||||||
>
|
>
|
||||||
<span styleName='tag-label'>#{tag}</span>
|
<span styleName='tag-label' style={textStyle} onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
|
||||||
<button styleName='tag-removeButton'
|
<button styleName='tag-removeButton'
|
||||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
|
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
|
||||||
>
|
>
|
||||||
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' />
|
<img className='tag-removeButton-icon' src={iconRemove} width='8px' />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
const { newTag, suggestions } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={_.isString(className)
|
<div className={_.isString(className)
|
||||||
? 'TagSelect ' + className
|
? 'TagSelect ' + className
|
||||||
@@ -135,24 +230,40 @@ class TagSelect extends React.Component {
|
|||||||
styleName='root'
|
styleName='root'
|
||||||
>
|
>
|
||||||
{tagList}
|
{tagList}
|
||||||
<input styleName='newTag'
|
<Autosuggest
|
||||||
ref='newTag'
|
ref='newTag'
|
||||||
value={this.state.newTag}
|
suggestions={suggestions}
|
||||||
placeholder={i18n.__('Add tag...')}
|
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||||
onChange={(e) => this.handleNewTagInputChange(e)}
|
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||||
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
onSuggestionSelected={this.onSuggestionSelected}
|
||||||
onBlur={(e) => this.handleNewTagBlur(e)}
|
getSuggestionValue={suggestion => suggestion.name}
|
||||||
|
renderSuggestion={suggestion => (
|
||||||
|
<div>
|
||||||
|
{suggestion.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
inputProps={{
|
||||||
|
placeholder: i18n.__('Add tag...'),
|
||||||
|
value: newTag,
|
||||||
|
onChange: this.onInputChange,
|
||||||
|
onKeyDown: this.onInputKeyDown,
|
||||||
|
onBlur: this.onInputBlur
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TagSelect.contextTypes = {
|
||||||
|
router: PropTypes.shape({})
|
||||||
|
}
|
||||||
|
|
||||||
TagSelect.propTypes = {
|
TagSelect.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
value: PropTypes.arrayOf(PropTypes.string),
|
value: PropTypes.arrayOf(PropTypes.string),
|
||||||
onChange: PropTypes.func
|
onChange: PropTypes.func,
|
||||||
|
coloredTags: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(TagSelect, styles)
|
export default CSSModules(TagSelect, styles)
|
||||||
|
|||||||
@@ -3,19 +3,18 @@
|
|||||||
align-items center
|
align-items center
|
||||||
user-select none
|
user-select none
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
width 100%
|
width 96%
|
||||||
overflow-x scroll
|
overflow-x auto
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
margin-top 31px
|
top 50px
|
||||||
position absolute
|
position absolute
|
||||||
|
&::-webkit-scrollbar
|
||||||
.root::-webkit-scrollbar
|
height 8px
|
||||||
display none
|
|
||||||
|
|
||||||
.tag
|
.tag
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
margin 0px 2px
|
margin 0px 2px 2px
|
||||||
padding 2px 4px
|
padding 2px 4px
|
||||||
background-color alpha($ui-tag-backgroundColor, 3%)
|
background-color alpha($ui-tag-backgroundColor, 3%)
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
@@ -39,16 +38,9 @@
|
|||||||
|
|
||||||
.tag-label
|
.tag-label
|
||||||
font-size 13px
|
font-size 13px
|
||||||
color: $ui-text-color
|
color $ui-text-color
|
||||||
padding 4px 16px 4px 8px
|
padding 4px 16px 4px 8px
|
||||||
|
cursor pointer
|
||||||
.newTag
|
|
||||||
box-sizing border-box
|
|
||||||
border none
|
|
||||||
background-color transparent
|
|
||||||
outline none
|
|
||||||
padding 0 4px
|
|
||||||
font-size 13px
|
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.tag
|
.tag
|
||||||
@@ -62,11 +54,6 @@ body[data-theme="dark"]
|
|||||||
.tag-label
|
.tag-label
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
.newTag
|
|
||||||
border-color none
|
|
||||||
background-color transparent
|
|
||||||
color $ui-dark-text-color
|
|
||||||
|
|
||||||
body[data-theme="solarized-dark"]
|
body[data-theme="solarized-dark"]
|
||||||
.tag
|
.tag
|
||||||
background-color $ui-solarized-dark-tag-backgroundColor
|
background-color $ui-solarized-dark-tag-backgroundColor
|
||||||
@@ -78,14 +65,9 @@ body[data-theme="solarized-dark"]
|
|||||||
.tag-label
|
.tag-label
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
.newTag
|
|
||||||
border-color none
|
|
||||||
background-color transparent
|
|
||||||
color $ui-solarized-dark-text-color
|
|
||||||
|
|
||||||
body[data-theme="monokai"]
|
body[data-theme="monokai"]
|
||||||
.tag
|
.tag
|
||||||
background-color $ui-monokai-button-backgroundColor
|
background-color $ui-monokai-tag-backgroundColor
|
||||||
|
|
||||||
.tag-removeButton
|
.tag-removeButton
|
||||||
border-color $ui-button--focus-borderColor
|
border-color $ui-button--focus-borderColor
|
||||||
@@ -93,8 +75,14 @@ body[data-theme="monokai"]
|
|||||||
|
|
||||||
.tag-label
|
.tag-label
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
.newTag
|
body[data-theme="dracula"]
|
||||||
border-color none
|
.tag
|
||||||
|
background-color $ui-dracula-tag-backgroundColor
|
||||||
|
|
||||||
|
.tag-removeButton
|
||||||
|
border-color $ui-dracula-button--focus-borderColor
|
||||||
background-color transparent
|
background-color transparent
|
||||||
color $ui-monokai-text-color
|
|
||||||
|
.tag-label
|
||||||
|
color $ui-dracula-borderColor
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './ToggleModeButton.styl'
|
import styles from './ToggleModeButton.styl'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const ToggleModeButton = ({
|
const ToggleModeButton = ({
|
||||||
onClick, editorType
|
onClick, editorType
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName='control-toggleModeButton'>
|
<div styleName='control-toggleModeButton'>
|
||||||
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
||||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
||||||
</div>
|
</div>
|
||||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||||
</div>
|
</div>
|
||||||
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
ToggleModeButton.propTypes = {
|
ToggleModeButton.propTypes = {
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
editorType: PropTypes.string.Required
|
editorType: PropTypes.string.Required
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(ToggleModeButton, styles)
|
export default CSSModules(ToggleModeButton, styles)
|
||||||
|
|||||||
@@ -40,6 +40,11 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
|
||||||
|
.tooltip:lang(ja)
|
||||||
|
@extend .tooltip
|
||||||
|
left -8px
|
||||||
|
width 70px
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.control-fullScreenButton
|
.control-fullScreenButton
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
@@ -59,7 +64,14 @@ body[data-theme="solarized-dark"]
|
|||||||
|
|
||||||
body[data-theme="monokai"]
|
body[data-theme="monokai"]
|
||||||
.control-toggleModeButton
|
.control-toggleModeButton
|
||||||
background-color #272822
|
background-color #373831
|
||||||
.active
|
.active
|
||||||
background-color #1EC38B
|
background-color #f92672
|
||||||
|
box-shadow 2px 0px 7px #222222
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.control-toggleModeButton
|
||||||
|
background-color #44475a
|
||||||
|
.active
|
||||||
|
background-color #bd93f9
|
||||||
box-shadow 2px 0px 7px #222222
|
box-shadow 2px 0px 7px #222222
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const TrashButton = ({
|
|||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||||
<span styleName='tooltip'>{i18n.__('Trash')}</span>
|
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
|
||||||
|
.tooltip:lang(ja)
|
||||||
|
@extend .tooltip
|
||||||
|
right 46px
|
||||||
|
|
||||||
.control-trashButton--in-trash
|
.control-trashButton--in-trash
|
||||||
top 60px
|
top 60px
|
||||||
topBarButtonRight()
|
topBarButtonRight()
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const electron = require('electron')
|
|||||||
const { remote } = electron
|
const { remote } = electron
|
||||||
|
|
||||||
class Main extends React.Component {
|
class Main extends React.Component {
|
||||||
|
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
@@ -57,13 +56,13 @@ class Main extends React.Component {
|
|||||||
init () {
|
init () {
|
||||||
dataApi
|
dataApi
|
||||||
.addStorage({
|
.addStorage({
|
||||||
name: 'My Storage',
|
name: 'My Storage Location',
|
||||||
path: path.join(remote.app.getPath('home'), 'Boostnote')
|
path: path.join(remote.app.getPath('home'), 'Boostnote')
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
if (data.storage.folders[0] != null) {
|
if (data.storage.folders[0] != null) {
|
||||||
return data
|
return data
|
||||||
} else {
|
} else {
|
||||||
@@ -72,7 +71,7 @@ class Main extends React.Component {
|
|||||||
color: '#1278BD',
|
color: '#1278BD',
|
||||||
name: 'Default'
|
name: 'Default'
|
||||||
})
|
})
|
||||||
.then((_data) => {
|
.then(_data => {
|
||||||
return {
|
return {
|
||||||
storage: _data.storage,
|
storage: _data.storage,
|
||||||
notes: data.notes
|
notes: data.notes
|
||||||
@@ -80,8 +79,7 @@ class Main extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
console.log(data)
|
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'ADD_STORAGE',
|
type: 'ADD_STORAGE',
|
||||||
storage: data.storage,
|
storage: data.storage,
|
||||||
@@ -98,16 +96,18 @@ class Main extends React.Component {
|
|||||||
{
|
{
|
||||||
name: 'example.html',
|
name: 'example.html',
|
||||||
mode: 'html',
|
mode: 'html',
|
||||||
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>'
|
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
|
||||||
|
linesHighlighted: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'example.js',
|
name: 'example.js',
|
||||||
mode: 'javascript',
|
mode: 'javascript',
|
||||||
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)'
|
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)",
|
||||||
|
linesHighlighted: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then(note => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
@@ -120,7 +120,7 @@ class Main extends React.Component {
|
|||||||
title: 'Welcome to Boostnote!',
|
title: 'Welcome to Boostnote!',
|
||||||
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then(note => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
@@ -131,10 +131,10 @@ class Main extends React.Component {
|
|||||||
.then(defaultMarkdownNote)
|
.then(defaultMarkdownNote)
|
||||||
.then(() => data.storage)
|
.then(() => data.storage)
|
||||||
})
|
})
|
||||||
.then((storage) => {
|
.then(storage => {
|
||||||
hashHistory.push('/storages/' + storage.key)
|
hashHistory.push('/storages/' + storage.key)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -142,12 +142,7 @@ class Main extends React.Component {
|
|||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, config } = this.props
|
const { dispatch, config } = this.props
|
||||||
|
|
||||||
const supportedThemes = [
|
const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai', 'dracula']
|
||||||
'dark',
|
|
||||||
'white',
|
|
||||||
'solarized-dark',
|
|
||||||
'monokai'
|
|
||||||
]
|
|
||||||
|
|
||||||
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
|
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
|
||||||
document.body.setAttribute('data-theme', config.ui.theme)
|
document.body.setAttribute('data-theme', config.ui.theme)
|
||||||
@@ -162,24 +157,36 @@ class Main extends React.Component {
|
|||||||
}
|
}
|
||||||
applyShortcuts()
|
applyShortcuts()
|
||||||
// Reload all data
|
// Reload all data
|
||||||
dataApi.init()
|
dataApi.init().then(data => {
|
||||||
.then((data) => {
|
dispatch({
|
||||||
dispatch({
|
type: 'INIT_ALL',
|
||||||
type: 'INIT_ALL',
|
storages: data.storages,
|
||||||
storages: data.storages,
|
notes: data.notes
|
||||||
notes: data.notes
|
|
||||||
})
|
|
||||||
|
|
||||||
if (data.storages.length < 1) {
|
|
||||||
this.init()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (data.storages.length < 1) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
||||||
|
|
||||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||||
|
eventEmitter.on('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
|
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
|
||||||
|
eventEmitter.off('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMenuBarVisible () {
|
||||||
|
const { config } = this.props
|
||||||
|
const { ui } = config
|
||||||
|
|
||||||
|
const newUI = Object.assign(ui, {showMenuBar: !ui.showMenuBar})
|
||||||
|
const newConfig = Object.assign(config, newUI)
|
||||||
|
ConfigManager.set(newConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLeftSlideMouseDown (e) {
|
handleLeftSlideMouseDown (e) {
|
||||||
@@ -199,34 +206,40 @@ class Main extends React.Component {
|
|||||||
handleMouseUp (e) {
|
handleMouseUp (e) {
|
||||||
// Change width of NoteList component.
|
// Change width of NoteList component.
|
||||||
if (this.state.isRightSliderFocused) {
|
if (this.state.isRightSliderFocused) {
|
||||||
this.setState({
|
this.setState(
|
||||||
isRightSliderFocused: false
|
{
|
||||||
}, () => {
|
isRightSliderFocused: false
|
||||||
const { dispatch } = this.props
|
},
|
||||||
const newListWidth = this.state.listWidth
|
() => {
|
||||||
// TODO: ConfigManager should dispatch itself.
|
const { dispatch } = this.props
|
||||||
ConfigManager.set({listWidth: newListWidth})
|
const newListWidth = this.state.listWidth
|
||||||
dispatch({
|
// TODO: ConfigManager should dispatch itself.
|
||||||
type: 'SET_LIST_WIDTH',
|
ConfigManager.set({ listWidth: newListWidth })
|
||||||
listWidth: newListWidth
|
dispatch({
|
||||||
})
|
type: 'SET_LIST_WIDTH',
|
||||||
})
|
listWidth: newListWidth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change width of SideNav component.
|
// Change width of SideNav component.
|
||||||
if (this.state.isLeftSliderFocused) {
|
if (this.state.isLeftSliderFocused) {
|
||||||
this.setState({
|
this.setState(
|
||||||
isLeftSliderFocused: false
|
{
|
||||||
}, () => {
|
isLeftSliderFocused: false
|
||||||
const { dispatch } = this.props
|
},
|
||||||
const navWidth = this.state.navWidth
|
() => {
|
||||||
// TODO: ConfigManager should dispatch itself.
|
const { dispatch } = this.props
|
||||||
ConfigManager.set({ navWidth })
|
const navWidth = this.state.navWidth
|
||||||
dispatch({
|
// TODO: ConfigManager should dispatch itself.
|
||||||
type: 'SET_NAV_WIDTH',
|
ConfigManager.set({ navWidth })
|
||||||
navWidth
|
dispatch({
|
||||||
})
|
type: 'SET_NAV_WIDTH',
|
||||||
})
|
navWidth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,8 +247,8 @@ class Main extends React.Component {
|
|||||||
if (this.state.isRightSliderFocused) {
|
if (this.state.isRightSliderFocused) {
|
||||||
const offset = this.refs.body.getBoundingClientRect().left
|
const offset = this.refs.body.getBoundingClientRect().left
|
||||||
let newListWidth = e.pageX - offset
|
let newListWidth = e.pageX - offset
|
||||||
if (newListWidth < 10) {
|
if (newListWidth < 180) {
|
||||||
newListWidth = 10
|
newListWidth = 180
|
||||||
} else if (newListWidth > 600) {
|
} else if (newListWidth > 600) {
|
||||||
newListWidth = 600
|
newListWidth = 600
|
||||||
}
|
}
|
||||||
@@ -271,8 +284,8 @@ class Main extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hideLeftLists (noteDetail, noteList, mainBody) {
|
hideLeftLists (noteDetail, noteList, mainBody) {
|
||||||
this.setState({noteDetailWidth: noteDetail.style.left})
|
this.setState({ noteDetailWidth: noteDetail.style.left })
|
||||||
this.setState({mainBodyWidth: mainBody.style.left})
|
this.setState({ mainBodyWidth: mainBody.style.left })
|
||||||
noteDetail.style.left = '0px'
|
noteDetail.style.left = '0px'
|
||||||
mainBody.style.left = '0px'
|
mainBody.style.left = '0px'
|
||||||
noteList.style.display = 'none'
|
noteList.style.display = 'none'
|
||||||
@@ -294,33 +307,36 @@ class Main extends React.Component {
|
|||||||
<div
|
<div
|
||||||
className='Main'
|
className='Main'
|
||||||
styleName='root'
|
styleName='root'
|
||||||
onMouseMove={(e) => this.handleMouseMove(e)}
|
onMouseMove={e => this.handleMouseMove(e)}
|
||||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
onMouseUp={e => this.handleMouseUp(e)}
|
||||||
>
|
>
|
||||||
<SideNav
|
<SideNav
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, ['dispatch', 'data', 'config', 'params', 'location'])}
|
||||||
'dispatch',
|
|
||||||
'data',
|
|
||||||
'config',
|
|
||||||
'location'
|
|
||||||
])}
|
|
||||||
width={this.state.navWidth}
|
width={this.state.navWidth}
|
||||||
/>
|
/>
|
||||||
{!config.isSideNavFolded &&
|
{!config.isSideNavFolded &&
|
||||||
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'}
|
<div
|
||||||
style={{left: this.state.navWidth}}
|
styleName={
|
||||||
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)}
|
this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
|
||||||
|
}
|
||||||
|
style={{ left: this.state.navWidth }}
|
||||||
|
onMouseDown={e => this.handleLeftSlideMouseDown(e)}
|
||||||
draggable='false'
|
draggable='false'
|
||||||
>
|
>
|
||||||
<div styleName='slider-hitbox' />
|
<div styleName='slider-hitbox' />
|
||||||
</div>
|
</div>}
|
||||||
}
|
<div
|
||||||
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||||
id='main-body'
|
id='main-body'
|
||||||
ref='body'
|
ref='body'
|
||||||
style={{left: config.isSideNavFolded ? foldedNavigationWidth : this.state.navWidth}}
|
style={{
|
||||||
|
left: config.isSideNavFolded
|
||||||
|
? foldedNavigationWidth
|
||||||
|
: this.state.navWidth
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<TopBar style={{width: this.state.listWidth}}
|
<TopBar
|
||||||
|
style={{ width: this.state.listWidth }}
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'config',
|
'config',
|
||||||
@@ -329,7 +345,8 @@ class Main extends React.Component {
|
|||||||
'location'
|
'location'
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
<NoteList style={{width: this.state.listWidth}}
|
<NoteList
|
||||||
|
style={{ width: this.state.listWidth }}
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
@@ -338,15 +355,20 @@ class Main extends React.Component {
|
|||||||
'location'
|
'location'
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
<div styleName={this.state.isRightSliderFocused ? 'slider-right--active' : 'slider-right'}
|
<div
|
||||||
style={{left: this.state.listWidth - 1}}
|
styleName={
|
||||||
onMouseDown={(e) => this.handleRightSlideMouseDown(e)}
|
this.state.isRightSliderFocused
|
||||||
|
? 'slider-right--active'
|
||||||
|
: 'slider-right'
|
||||||
|
}
|
||||||
|
style={{ left: this.state.listWidth - 1 }}
|
||||||
|
onMouseDown={e => this.handleRightSlideMouseDown(e)}
|
||||||
draggable='false'
|
draggable='false'
|
||||||
>
|
>
|
||||||
<div styleName='slider-hitbox' />
|
<div styleName='slider-hitbox' />
|
||||||
</div>
|
</div>
|
||||||
<Detail
|
<Detail
|
||||||
style={{left: this.state.listWidth}}
|
style={{ left: this.state.listWidth }}
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
@@ -374,4 +396,4 @@ Main.propTypes = {
|
|||||||
data: PropTypes.shape({}).isRequired
|
data: PropTypes.shape({}).isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect((x) => x)(CSSModules(Main, styles))
|
export default connect(x => x)(CSSModules(Main, styles))
|
||||||
|
|||||||
@@ -79,3 +79,7 @@ body[data-theme="solarized-dark"]
|
|||||||
body[data-theme="monokai"]
|
body[data-theme="monokai"]
|
||||||
.root, .root--expanded
|
.root, .root--expanded
|
||||||
background-color $ui-monokai-noteList-backgroundColor
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root, .root--expanded
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
@@ -7,6 +7,7 @@ import modal from 'browser/main/lib/modal'
|
|||||||
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
@@ -34,15 +35,23 @@ class NewNoteButton extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNewNoteButtonClick (e) {
|
handleNewNoteButtonClick (e) {
|
||||||
const { location, dispatch } = this.props
|
const { location, params, dispatch, config } = this.props
|
||||||
const { storage, folder } = this.resolveTargetFolder()
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
|
|
||||||
modal.open(NewNoteModal, {
|
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
||||||
storage: storage.key,
|
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
|
||||||
folder: folder.key,
|
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
||||||
dispatch,
|
createSnippetNote(storage.key, folder.key, dispatch, location, params, config)
|
||||||
location
|
} else {
|
||||||
})
|
modal.open(NewNoteModal, {
|
||||||
|
storage: storage.key,
|
||||||
|
folder: folder.key,
|
||||||
|
dispatch,
|
||||||
|
location,
|
||||||
|
params,
|
||||||
|
config
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveTargetFolder () {
|
resolveTargetFolder () {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ body[data-theme="dark"]
|
|||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-inactive-text-color
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
.control-button--active
|
.control-button--active
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
&:active
|
&:active
|
||||||
@@ -109,7 +109,7 @@ body[data-theme="solarized-dark"]
|
|||||||
color $ui-solarized-dark-inactive-text-color
|
color $ui-solarized-dark-inactive-text-color
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
.control-button--active
|
.control-button--active
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
&:active
|
&:active
|
||||||
@@ -138,3 +138,27 @@ body[data-theme="monokai"]
|
|||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
&:active
|
&:active
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
|
||||||
|
.control-sortBy-select
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.control-button
|
||||||
|
color $ui-dracula-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
|
||||||
|
.control-button--active
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:active
|
||||||
|
color $ui-dracula-text-color
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import debounceRender from 'react-debounce-render'
|
|
||||||
import styles from './NoteList.styl'
|
import styles from './NoteList.styl'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
@@ -56,7 +55,6 @@ class NoteList extends React.Component {
|
|||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.selectNextNoteHandler = () => {
|
this.selectNextNoteHandler = () => {
|
||||||
console.log('fired next')
|
|
||||||
this.selectNextNote()
|
this.selectNextNote()
|
||||||
}
|
}
|
||||||
this.selectPriorNoteHandler = () => {
|
this.selectPriorNoteHandler = () => {
|
||||||
@@ -65,13 +63,14 @@ class NoteList extends React.Component {
|
|||||||
this.focusHandler = () => {
|
this.focusHandler = () => {
|
||||||
this.refs.list.focus()
|
this.refs.list.focus()
|
||||||
}
|
}
|
||||||
this.alertIfSnippetHandler = () => {
|
this.alertIfSnippetHandler = (event, msg) => {
|
||||||
this.alertIfSnippet()
|
this.alertIfSnippet(msg)
|
||||||
}
|
}
|
||||||
this.importFromFileHandler = this.importFromFile.bind(this)
|
this.importFromFileHandler = this.importFromFile.bind(this)
|
||||||
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
||||||
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
|
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
|
||||||
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
|
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
|
||||||
|
this.cloneNote = this.cloneNote.bind(this)
|
||||||
this.deleteNote = this.deleteNote.bind(this)
|
this.deleteNote = this.deleteNote.bind(this)
|
||||||
this.focusNote = this.focusNote.bind(this)
|
this.focusNote = this.focusNote.bind(this)
|
||||||
this.pinToTop = this.pinToTop.bind(this)
|
this.pinToTop = this.pinToTop.bind(this)
|
||||||
@@ -80,10 +79,13 @@ class NoteList extends React.Component {
|
|||||||
this.getViewType = this.getViewType.bind(this)
|
this.getViewType = this.getViewType.bind(this)
|
||||||
this.restoreNote = this.restoreNote.bind(this)
|
this.restoreNote = this.restoreNote.bind(this)
|
||||||
this.copyNoteLink = this.copyNoteLink.bind(this)
|
this.copyNoteLink = this.copyNoteLink.bind(this)
|
||||||
|
this.navigate = this.navigate.bind(this)
|
||||||
|
|
||||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||||
this.state = {
|
this.state = {
|
||||||
|
ctrlKeyDown: false,
|
||||||
shiftKeyDown: false,
|
shiftKeyDown: false,
|
||||||
|
prevShiftNoteIndex: -1,
|
||||||
selectedNoteKeys: []
|
selectedNoteKeys: []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,10 +96,12 @@ class NoteList extends React.Component {
|
|||||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||||
ee.on('list:next', this.selectNextNoteHandler)
|
ee.on('list:next', this.selectNextNoteHandler)
|
||||||
ee.on('list:prior', this.selectPriorNoteHandler)
|
ee.on('list:prior', this.selectPriorNoteHandler)
|
||||||
|
ee.on('list:clone', this.cloneNote)
|
||||||
ee.on('list:focus', this.focusHandler)
|
ee.on('list:focus', this.focusHandler)
|
||||||
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
|
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||||
ee.on('import:file', this.importFromFileHandler)
|
ee.on('import:file', this.importFromFileHandler)
|
||||||
ee.on('list:jump', this.jumpNoteByHash)
|
ee.on('list:jump', this.jumpNoteByHash)
|
||||||
|
ee.on('list:navigate', this.navigate)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
@@ -115,6 +119,7 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
ee.off('list:next', this.selectNextNoteHandler)
|
ee.off('list:next', this.selectNextNoteHandler)
|
||||||
ee.off('list:prior', this.selectPriorNoteHandler)
|
ee.off('list:prior', this.selectPriorNoteHandler)
|
||||||
|
ee.off('list:clone', this.cloneNote)
|
||||||
ee.off('list:focus', this.focusHandler)
|
ee.off('list:focus', this.focusHandler)
|
||||||
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
|
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||||
ee.off('import:file', this.importFromFileHandler)
|
ee.off('import:file', this.importFromFileHandler)
|
||||||
@@ -170,16 +175,15 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focusNote (selectedNoteKeys, noteKey) {
|
focusNote (selectedNoteKeys, noteKey, pathname) {
|
||||||
const { router } = this.context
|
const { router } = this.context
|
||||||
const { location } = this.props
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedNoteKeys
|
selectedNoteKeys
|
||||||
})
|
})
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
pathname: location.pathname,
|
pathname,
|
||||||
query: {
|
query: {
|
||||||
key: noteKey
|
key: noteKey
|
||||||
}
|
}
|
||||||
@@ -198,6 +202,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
let { selectedNoteKeys } = this.state
|
let { selectedNoteKeys } = this.state
|
||||||
const { shiftKeyDown } = this.state
|
const { shiftKeyDown } = this.state
|
||||||
|
const { location } = this.props
|
||||||
|
|
||||||
let targetIndex = this.getTargetIndex()
|
let targetIndex = this.getTargetIndex()
|
||||||
|
|
||||||
@@ -214,7 +219,7 @@ class NoteList extends React.Component {
|
|||||||
selectedNoteKeys.push(priorNoteKey)
|
selectedNoteKeys.push(priorNoteKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.focusNote(selectedNoteKeys, priorNoteKey)
|
this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname)
|
||||||
|
|
||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
@@ -225,6 +230,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
let { selectedNoteKeys } = this.state
|
let { selectedNoteKeys } = this.state
|
||||||
const { shiftKeyDown } = this.state
|
const { shiftKeyDown } = this.state
|
||||||
|
const { location } = this.props
|
||||||
|
|
||||||
let targetIndex = this.getTargetIndex()
|
let targetIndex = this.getTargetIndex()
|
||||||
const isTargetLastNote = targetIndex === this.notes.length - 1
|
const isTargetLastNote = targetIndex === this.notes.length - 1
|
||||||
@@ -247,7 +253,7 @@ class NoteList extends React.Component {
|
|||||||
selectedNoteKeys.push(nextNoteKey)
|
selectedNoteKeys.push(nextNoteKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.focusNote(selectedNoteKeys, nextNoteKey)
|
this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname)
|
||||||
|
|
||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
@@ -259,13 +265,13 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedNoteKeys = [noteHash]
|
const selectedNoteKeys = [noteHash]
|
||||||
this.focusNote(selectedNoteKeys, noteHash)
|
this.focusNote(selectedNoteKeys, noteHash, '/home')
|
||||||
|
|
||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNoteListKeyDown (e) {
|
handleNoteListKeyDown (e) {
|
||||||
if (e.metaKey || e.ctrlKey) return true
|
if (e.metaKey) return true
|
||||||
|
|
||||||
// A key
|
// A key
|
||||||
if (e.keyCode === 65 && !e.shiftKey) {
|
if (e.keyCode === 65 && !e.shiftKey) {
|
||||||
@@ -273,12 +279,6 @@ class NoteList extends React.Component {
|
|||||||
ee.emit('top:new-note')
|
ee.emit('top:new-note')
|
||||||
}
|
}
|
||||||
|
|
||||||
// D key
|
|
||||||
if (e.keyCode === 68) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.deleteNote()
|
|
||||||
}
|
|
||||||
|
|
||||||
// E key
|
// E key
|
||||||
if (e.keyCode === 69) {
|
if (e.keyCode === 69) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -305,6 +305,8 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
this.setState({ shiftKeyDown: true })
|
this.setState({ shiftKeyDown: true })
|
||||||
|
} else if (e.ctrlKey) {
|
||||||
|
this.setState({ ctrlKeyDown: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +314,10 @@ class NoteList extends React.Component {
|
|||||||
if (!e.shiftKey) {
|
if (!e.shiftKey) {
|
||||||
this.setState({ shiftKeyDown: false })
|
this.setState({ shiftKeyDown: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!e.ctrlKey) {
|
||||||
|
this.setState({ ctrlKeyDown: false })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNotes () {
|
getNotes () {
|
||||||
@@ -388,25 +394,65 @@ class NoteList extends React.Component {
|
|||||||
return pinnedNotes.concat(unpinnedNotes)
|
return pinnedNotes.concat(unpinnedNotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNoteIndexByKey (noteKey) {
|
||||||
|
return this.notes.findIndex((note) => {
|
||||||
|
if (!note) return -1
|
||||||
|
|
||||||
|
return note.key === noteKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleNoteClick (e, uniqueKey) {
|
handleNoteClick (e, uniqueKey) {
|
||||||
const { router } = this.context
|
const { router } = this.context
|
||||||
const { location } = this.props
|
const { location } = this.props
|
||||||
let { selectedNoteKeys } = this.state
|
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
||||||
const { shiftKeyDown } = this.state
|
const { ctrlKeyDown, shiftKeyDown } = this.state
|
||||||
|
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
||||||
|
|
||||||
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
if (ctrlKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||||
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedNoteKeys: newSelectedNoteKeys
|
selectedNoteKeys: newSelectedNoteKeys
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!shiftKeyDown) {
|
if (!ctrlKeyDown && !shiftKeyDown) {
|
||||||
selectedNoteKeys = []
|
selectedNoteKeys = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!shiftKeyDown) {
|
||||||
|
prevShiftNoteIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
selectedNoteKeys.push(uniqueKey)
|
selectedNoteKeys.push(uniqueKey)
|
||||||
|
|
||||||
|
if (shiftKeyDown && hasSelectedNoteKey) {
|
||||||
|
let firstShiftNoteIndex = this.getNoteIndexByKey(selectedNoteKeys[0])
|
||||||
|
// Shift selection can either start from first note in the exisiting selectedNoteKeys
|
||||||
|
// or previous first shift note index
|
||||||
|
firstShiftNoteIndex = firstShiftNoteIndex > prevShiftNoteIndex
|
||||||
|
? firstShiftNoteIndex : prevShiftNoteIndex
|
||||||
|
|
||||||
|
const lastShiftNoteIndex = this.getNoteIndexByKey(uniqueKey)
|
||||||
|
|
||||||
|
const startIndex = firstShiftNoteIndex < lastShiftNoteIndex
|
||||||
|
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||||
|
const endIndex = firstShiftNoteIndex > lastShiftNoteIndex
|
||||||
|
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||||
|
|
||||||
|
selectedNoteKeys = []
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
selectedNoteKeys.push(this.notes[i].key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevShiftNoteIndex < 0) {
|
||||||
|
prevShiftNoteIndex = firstShiftNoteIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedNoteKeys
|
selectedNoteKeys,
|
||||||
|
prevShiftNoteIndex
|
||||||
})
|
})
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
@@ -418,10 +464,10 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSortByChange (e) {
|
handleSortByChange (e) {
|
||||||
const { dispatch } = this.props
|
const { dispatch, params: { folderKey } } = this.props
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
sortBy: e.target.value
|
[folderKey]: { sortBy: e.target.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigManager.set(config)
|
ConfigManager.set(config)
|
||||||
@@ -445,14 +491,21 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
alertIfSnippet () {
|
alertIfSnippet (msg) {
|
||||||
|
const warningMessage = (msg) => ({
|
||||||
|
'export-txt': 'Text export',
|
||||||
|
'export-md': 'Markdown export',
|
||||||
|
'export-html': 'HTML export',
|
||||||
|
'print': 'Print'
|
||||||
|
})[msg]
|
||||||
|
|
||||||
const targetIndex = this.getTargetIndex()
|
const targetIndex = this.getTargetIndex()
|
||||||
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: i18n.__('Sorry!'),
|
message: i18n.__('Sorry!'),
|
||||||
detail: i18n.__('md/text import is available only a markdown note.'),
|
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
||||||
buttons: [i18n.__('OK'), i18n.__('Cancel')]
|
buttons: [i18n.__('OK')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,7 +570,7 @@ class NoteList extends React.Component {
|
|||||||
click: this.cloneNote.bind(this)
|
click: this.cloneNote.bind(this)
|
||||||
}, {
|
}, {
|
||||||
label: copyNoteLink,
|
label: copyNoteLink,
|
||||||
click: this.copyNoteLink(note)
|
click: this.copyNoteLink.bind(this, note)
|
||||||
})
|
})
|
||||||
if (note.type === 'MARKDOWN_NOTE') {
|
if (note.type === 'MARKDOWN_NOTE') {
|
||||||
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||||
@@ -603,18 +656,21 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
data.forEach((item) => {
|
const dispatchHandler = () => {
|
||||||
dispatch({
|
data.forEach((item) => {
|
||||||
type: 'DELETE_NOTE',
|
dispatch({
|
||||||
storageKey: item.storageKey,
|
type: 'DELETE_NOTE',
|
||||||
noteKey: item.noteKey
|
storageKey: item.storageKey,
|
||||||
|
noteKey: item.noteKey
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
ee.once('list:next', dispatchHandler)
|
||||||
})
|
})
|
||||||
|
.then(() => ee.emit('list:next'))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Cannot Delete note: ' + err)
|
console.error('Cannot Delete note: ' + err)
|
||||||
})
|
})
|
||||||
console.log('Notes were all deleted')
|
|
||||||
} else {
|
} else {
|
||||||
if (!confirmDeleteNote(confirmDeletion, false)) return
|
if (!confirmDeleteNote(confirmDeletion, false)) return
|
||||||
|
|
||||||
@@ -634,8 +690,8 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||||
console.log('Notes went to trash')
|
|
||||||
})
|
})
|
||||||
|
.then(() => ee.emit('list:next'))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Notes could not go to trash: ' + err)
|
console.error('Notes could not go to trash: ' + err)
|
||||||
})
|
})
|
||||||
@@ -659,7 +715,8 @@ class NoteList extends React.Component {
|
|||||||
type: firstNote.type,
|
type: firstNote.type,
|
||||||
folder: folder.key,
|
folder: folder.key,
|
||||||
title: firstNote.title + ' ' + i18n.__('copy'),
|
title: firstNote.title + ' ' + i18n.__('copy'),
|
||||||
content: firstNote.content
|
content: firstNote.content,
|
||||||
|
linesHighlighted: firstNote.linesHighlighted
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
attachmentManagement.cloneAttachments(firstNote, note)
|
attachmentManagement.cloneAttachments(firstNote, note)
|
||||||
@@ -687,6 +744,16 @@ class NoteList extends React.Component {
|
|||||||
return copy(noteLink)
|
return copy(noteLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigate (sender, pathname) {
|
||||||
|
const { router } = this.context
|
||||||
|
router.push({
|
||||||
|
pathname,
|
||||||
|
query: {
|
||||||
|
// key: noteKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
save (note) {
|
save (note) {
|
||||||
const { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
dataApi
|
dataApi
|
||||||
@@ -909,12 +976,13 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { location, config } = this.props
|
const { location, config, params: { folderKey } } = this.props
|
||||||
let { notes } = this.props
|
let { notes } = this.props
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const sortFunc = config.sortBy === 'CREATED_AT'
|
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
||||||
|
const sortFunc = sortBy === 'CREATED_AT'
|
||||||
? sortByCreatedAt
|
? sortByCreatedAt
|
||||||
: config.sortBy === 'ALPHABETICAL'
|
: sortBy === 'ALPHABETICAL'
|
||||||
? sortByAlphabetical
|
? sortByAlphabetical
|
||||||
: sortByUpdatedAt
|
: sortByUpdatedAt
|
||||||
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||||
@@ -965,7 +1033,7 @@ class NoteList extends React.Component {
|
|||||||
notes.length === 1 ||
|
notes.length === 1 ||
|
||||||
(autoSelectFirst && index === 0)
|
(autoSelectFirst && index === 0)
|
||||||
const dateDisplay = moment(
|
const dateDisplay = moment(
|
||||||
config.sortBy === 'CREATED_AT'
|
sortBy === 'CREATED_AT'
|
||||||
? note.createdAt : note.updatedAt
|
? note.createdAt : note.updatedAt
|
||||||
).fromNow('D')
|
).fromNow('D')
|
||||||
|
|
||||||
@@ -983,6 +1051,8 @@ class NoteList extends React.Component {
|
|||||||
folderName={this.getNoteFolder(note).name}
|
folderName={this.getNoteFolder(note).name}
|
||||||
storageName={this.getNoteStorage(note).name}
|
storageName={this.getNoteStorage(note).name}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
|
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||||
|
coloredTags={config.coloredTags}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1014,7 +1084,7 @@ class NoteList extends React.Component {
|
|||||||
<i className='fa fa-angle-down' />
|
<i className='fa fa-angle-down' />
|
||||||
<select styleName='control-sortBy-select'
|
<select styleName='control-sortBy-select'
|
||||||
title={i18n.__('Select filter mode')}
|
title={i18n.__('Select filter mode')}
|
||||||
value={config.sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => this.handleSortByChange(e)}
|
onChange={(e) => this.handleSortByChange(e)}
|
||||||
>
|
>
|
||||||
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
|
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
|
||||||
@@ -1065,4 +1135,4 @@ NoteList.propTypes = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default debounceRender(CSSModules(NoteList, styles))
|
export default CSSModules(NoteList, styles)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
text-align center
|
text-align center
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.top-menu-label
|
.top-menu-label
|
||||||
margin-left 5px
|
margin-left 5px
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
@@ -122,3 +122,8 @@ body[data-theme="monokai"]
|
|||||||
.root, .root--folded
|
.root, .root--folded
|
||||||
background-color $ui-monokai-backgroundColor
|
background-color $ui-monokai-backgroundColor
|
||||||
border-right 1px solid $ui-monokai-borderColor
|
border-right 1px solid $ui-monokai-borderColor
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root, .root--folded
|
||||||
|
background-color $ui-dracula-backgroundColor
|
||||||
|
border-right 1px solid $ui-dracula-borderColor
|
||||||
@@ -25,7 +25,8 @@ class StorageItem extends React.Component {
|
|||||||
const { storage } = this.props
|
const { storage } = this.props
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isOpen: !!storage.isOpen
|
isOpen: !!storage.isOpen,
|
||||||
|
draggedOver: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +39,22 @@ class StorageItem extends React.Component {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: i18n.__('Export Storage'),
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: i18n.__('Export as txt'),
|
||||||
|
click: (e) => this.handleExportStorageClick(e, 'txt')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.__('Export as md'),
|
||||||
|
click: (e) => this.handleExportStorageClick(e, 'md')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: i18n.__('Unlink Storage'),
|
label: i18n.__('Unlink Storage'),
|
||||||
click: (e) => this.handleUnlinkStorageClick(e)
|
click: (e) => this.handleUnlinkStorageClick(e)
|
||||||
@@ -68,6 +85,30 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleExportStorageClick (e, fileType) {
|
||||||
|
const options = {
|
||||||
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
|
buttonLabel: i18n.__('Select directory'),
|
||||||
|
title: i18n.__('Select a folder to export the files to'),
|
||||||
|
multiSelections: false
|
||||||
|
}
|
||||||
|
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
||||||
|
(paths) => {
|
||||||
|
if (paths && paths.length === 1) {
|
||||||
|
const { storage, dispatch } = this.props
|
||||||
|
dataApi
|
||||||
|
.exportStorage(storage.key, fileType, paths[0])
|
||||||
|
.then(data => {
|
||||||
|
dispatch({
|
||||||
|
type: 'EXPORT_STORAGE',
|
||||||
|
storage: data.storage,
|
||||||
|
fileType: data.fileType
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleToggleButtonClick (e) {
|
handleToggleButtonClick (e) {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch } = this.props
|
||||||
const isOpen = !this.state.isOpen
|
const isOpen = !this.state.isOpen
|
||||||
@@ -164,6 +205,20 @@ class StorageItem extends React.Component {
|
|||||||
folderKey: data.folderKey,
|
folderKey: data.folderKey,
|
||||||
fileType: data.fileType
|
fileType: data.fileType
|
||||||
})
|
})
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
type: 'info',
|
||||||
|
message: 'Exported to "' + data.exportDir + '"'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dialog.showErrorBox(
|
||||||
|
'Export error',
|
||||||
|
err ? err.message || err : 'Unexpected error during export'
|
||||||
|
)
|
||||||
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -191,14 +246,20 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragEnter (e) {
|
handleDragEnter (e, key) {
|
||||||
e.dataTransfer.setData('defaultColor', e.target.style.backgroundColor)
|
e.preventDefault()
|
||||||
e.target.style.backgroundColor = 'rgba(129, 130, 131, 0.08)'
|
if (this.state.draggedOver === key) { return }
|
||||||
|
this.setState({
|
||||||
|
draggedOver: key
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragLeave (e) {
|
handleDragLeave (e) {
|
||||||
e.target.style.opacity = '1'
|
e.preventDefault()
|
||||||
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
if (this.state.draggedOver === null) { return }
|
||||||
|
this.setState({
|
||||||
|
draggedOver: null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
dropNote (storage, folder, dispatch, location, noteData) {
|
dropNote (storage, folder, dispatch, location, noteData) {
|
||||||
@@ -223,8 +284,12 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDrop (e, storage, folder, dispatch, location) {
|
handleDrop (e, storage, folder, dispatch, location) {
|
||||||
e.target.style.opacity = '1'
|
e.preventDefault()
|
||||||
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
if (this.state.draggedOver !== null) {
|
||||||
|
this.setState({
|
||||||
|
draggedOver: null
|
||||||
|
})
|
||||||
|
}
|
||||||
const noteData = JSON.parse(e.dataTransfer.getData('note'))
|
const noteData = JSON.parse(e.dataTransfer.getData('note'))
|
||||||
this.dropNote(storage, folder, dispatch, location, noteData)
|
this.dropNote(storage, folder, dispatch, location, noteData)
|
||||||
}
|
}
|
||||||
@@ -234,7 +299,7 @@ class StorageItem extends React.Component {
|
|||||||
const { folderNoteMap, trashedSet } = data
|
const { folderNoteMap, trashedSet } = data
|
||||||
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||||
const folderList = storage.folders.map((folder, index) => {
|
const folderList = storage.folders.map((folder, index) => {
|
||||||
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
||||||
const isActive = !!(location.pathname.match(folderRegex))
|
const isActive = !!(location.pathname.match(folderRegex))
|
||||||
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||||
|
|
||||||
@@ -251,16 +316,22 @@ class StorageItem extends React.Component {
|
|||||||
<SortableStorageItemChild
|
<SortableStorageItemChild
|
||||||
key={folder.key}
|
key={folder.key}
|
||||||
index={index}
|
index={index}
|
||||||
isActive={isActive}
|
isActive={isActive || folder.key === this.state.draggedOver}
|
||||||
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||||
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
||||||
folderName={folder.name}
|
folderName={folder.name}
|
||||||
folderColor={folder.color}
|
folderColor={folder.color}
|
||||||
isFolded={isFolded}
|
isFolded={isFolded}
|
||||||
noteCount={noteCount}
|
noteCount={noteCount}
|
||||||
handleDrop={(e) => this.handleDrop(e, storage, folder, dispatch, location)}
|
handleDrop={(e) => {
|
||||||
handleDragEnter={this.handleDragEnter}
|
this.handleDrop(e, storage, folder, dispatch, location)
|
||||||
handleDragLeave={this.handleDragLeave}
|
}}
|
||||||
|
handleDragEnter={(e) => {
|
||||||
|
this.handleDragEnter(e, folder.key)
|
||||||
|
}}
|
||||||
|
handleDragLeave={(e) => {
|
||||||
|
this.handleDragLeave(e, folder)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,9 +18,32 @@ import TagButton from './TagButton'
|
|||||||
import {SortableContainer} from 'react-sortable-hoc'
|
import {SortableContainer} from 'react-sortable-hoc'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
|
import { remote } from 'electron'
|
||||||
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
|
import ColorPicker from 'browser/components/ColorPicker'
|
||||||
|
|
||||||
|
function matchActiveTags (tags, activeTags) {
|
||||||
|
return _.every(activeTags, v => tags.indexOf(v) >= 0)
|
||||||
|
}
|
||||||
|
|
||||||
class SideNav extends React.Component {
|
class SideNav extends React.Component {
|
||||||
// TODO: should not use electron stuff v0.7
|
// TODO: should not use electron stuff v0.7
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
colorPicker: {
|
||||||
|
show: false,
|
||||||
|
color: null,
|
||||||
|
tagName: null,
|
||||||
|
targetRect: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dismissColorPicker = this.dismissColorPicker.bind(this)
|
||||||
|
this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this)
|
||||||
|
this.handleColorPickerReset = this.handleColorPickerReset.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
|
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
|
||||||
@@ -30,6 +53,52 @@ class SideNav extends React.Component {
|
|||||||
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
|
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteTag (tag) {
|
||||||
|
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
ype: 'warning',
|
||||||
|
message: i18n.__('Confirm tag deletion'),
|
||||||
|
detail: i18n.__('This will permanently remove this tag.'),
|
||||||
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selectedButton === 0) {
|
||||||
|
const { data, dispatch, location, params } = this.props
|
||||||
|
|
||||||
|
const notes = data.noteMap
|
||||||
|
.map(note => note)
|
||||||
|
.filter(note => note.tags.indexOf(tag) !== -1)
|
||||||
|
.map(note => {
|
||||||
|
note = Object.assign({}, note)
|
||||||
|
note.tags = note.tags.slice()
|
||||||
|
|
||||||
|
note.tags.splice(note.tags.indexOf(tag), 1)
|
||||||
|
|
||||||
|
return note
|
||||||
|
})
|
||||||
|
|
||||||
|
Promise
|
||||||
|
.all(notes.map(note => dataApi.updateNote(note.storage, note.key, note)))
|
||||||
|
.then(updatedNotes => {
|
||||||
|
updatedNotes.forEach(note => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (location.pathname.match('/tags')) {
|
||||||
|
const tags = params.tagname.split(' ')
|
||||||
|
const index = tags.indexOf(tag)
|
||||||
|
if (index !== -1) {
|
||||||
|
tags.splice(index, 1)
|
||||||
|
|
||||||
|
this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleMenuButtonClick (e) {
|
handleMenuButtonClick (e) {
|
||||||
openModal(PreferencesModal)
|
openModal(PreferencesModal)
|
||||||
}
|
}
|
||||||
@@ -44,6 +113,72 @@ class SideNav extends React.Component {
|
|||||||
router.push('/starred')
|
router.push('/starred')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTagContextMenu (e, tag) {
|
||||||
|
const menu = []
|
||||||
|
|
||||||
|
menu.push({
|
||||||
|
label: i18n.__('Delete Tag'),
|
||||||
|
click: this.deleteTag.bind(this, tag)
|
||||||
|
})
|
||||||
|
|
||||||
|
menu.push({
|
||||||
|
label: i18n.__('Customize Color'),
|
||||||
|
click: this.displayColorPicker.bind(this, tag, e.target.getBoundingClientRect())
|
||||||
|
})
|
||||||
|
|
||||||
|
context.popup(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissColorPicker () {
|
||||||
|
this.setState({
|
||||||
|
colorPicker: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
displayColorPicker (tagName, rect) {
|
||||||
|
const { config } = this.props
|
||||||
|
this.setState({
|
||||||
|
colorPicker: {
|
||||||
|
show: true,
|
||||||
|
color: config.coloredTags[tagName],
|
||||||
|
tagName,
|
||||||
|
targetRect: rect
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleColorPickerConfirm (color) {
|
||||||
|
const { dispatch, config: {coloredTags} } = this.props
|
||||||
|
const { colorPicker: { tagName } } = this.state
|
||||||
|
const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex})
|
||||||
|
|
||||||
|
const config = { coloredTags: newColoredTags }
|
||||||
|
ConfigManager.set(config)
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config
|
||||||
|
})
|
||||||
|
this.dismissColorPicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleColorPickerReset () {
|
||||||
|
const { dispatch, config: {coloredTags} } = this.props
|
||||||
|
const { colorPicker: { tagName } } = this.state
|
||||||
|
const newColoredTags = Object.assign({}, coloredTags)
|
||||||
|
|
||||||
|
delete newColoredTags[tagName]
|
||||||
|
|
||||||
|
const config = { coloredTags: newColoredTags }
|
||||||
|
ConfigManager.set(config)
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config
|
||||||
|
})
|
||||||
|
this.dismissColorPicker()
|
||||||
|
}
|
||||||
|
|
||||||
handleToggleButtonClick (e) {
|
handleToggleButtonClick (e) {
|
||||||
const { dispatch, config } = this.props
|
const { dispatch, config } = this.props
|
||||||
|
|
||||||
@@ -144,12 +279,21 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
tagListComponent () {
|
tagListComponent () {
|
||||||
const { data, location, config } = this.props
|
const { data, location, config } = this.props
|
||||||
const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
|
const { colorPicker } = this.state
|
||||||
|
const activeTags = this.getActiveTags(location.pathname)
|
||||||
|
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
||||||
let tagList = _.sortBy(data.tagNoteMap.map(
|
let tagList = _.sortBy(data.tagNoteMap.map(
|
||||||
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
||||||
), ['name']).filter(
|
).filter(
|
||||||
tag => tag.size > 0
|
tag => tag.size > 0
|
||||||
)
|
), ['name'])
|
||||||
|
if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
|
||||||
|
const notesTags = data.noteMap.map(note => note.tags)
|
||||||
|
tagList = tagList.map(tag => {
|
||||||
|
tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length
|
||||||
|
return tag
|
||||||
|
})
|
||||||
|
}
|
||||||
if (config.sortTagsBy === 'COUNTER') {
|
if (config.sortTagsBy === 'COUNTER') {
|
||||||
tagList = _.sortBy(tagList, item => (0 - item.size))
|
tagList = _.sortBy(tagList, item => (0 - item.size))
|
||||||
}
|
}
|
||||||
@@ -165,10 +309,12 @@ class SideNav extends React.Component {
|
|||||||
name={tag.name}
|
name={tag.name}
|
||||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||||
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
||||||
isActive={this.getTagActive(location.pathname, tag.name)}
|
handleContextMenu={this.handleTagContextMenu.bind(this)}
|
||||||
|
isActive={this.getTagActive(location.pathname, tag.name) || (colorPicker.tagName === tag.name)}
|
||||||
isRelated={tag.related}
|
isRelated={tag.related}
|
||||||
key={tag.name}
|
key={tag.name}
|
||||||
count={tag.size}
|
count={tag.size}
|
||||||
|
color={config.coloredTags[tag.name]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -198,12 +344,12 @@ class SideNav extends React.Component {
|
|||||||
const tags = pathSegments[pathSegments.length - 1]
|
const tags = pathSegments[pathSegments.length - 1]
|
||||||
return (tags === 'alltags')
|
return (tags === 'alltags')
|
||||||
? []
|
? []
|
||||||
: tags.split(' ')
|
: decodeURIComponent(tags).split(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickTagListItem (name) {
|
handleClickTagListItem (name) {
|
||||||
const { router } = this.context
|
const { router } = this.context
|
||||||
router.push(`/tags/${name}`)
|
router.push(`/tags/${encodeURIComponent(name)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortTagsByChange (e) {
|
handleSortTagsByChange (e) {
|
||||||
@@ -230,7 +376,7 @@ class SideNav extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
listOfTags.push(tag)
|
listOfTags.push(tag)
|
||||||
}
|
}
|
||||||
router.push(`/tags/${listOfTags.join(' ')}`)
|
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
emptyTrash (entries) {
|
emptyTrash (entries) {
|
||||||
@@ -238,6 +384,8 @@ class SideNav extends React.Component {
|
|||||||
const deletionPromises = entries.map((note) => {
|
const deletionPromises = entries.map((note) => {
|
||||||
return dataApi.deleteNote(note.storage, note.key)
|
return dataApi.deleteNote(note.storage, note.key)
|
||||||
})
|
})
|
||||||
|
const { confirmDeletion } = this.props.config.ui
|
||||||
|
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||||
Promise.all(deletionPromises)
|
Promise.all(deletionPromises)
|
||||||
.then((arrayOfStorageAndNoteKeys) => {
|
.then((arrayOfStorageAndNoteKeys) => {
|
||||||
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||||
@@ -247,7 +395,6 @@ class SideNav extends React.Component {
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Cannot Delete note: ' + err)
|
console.error('Cannot Delete note: ' + err)
|
||||||
})
|
})
|
||||||
console.log('Trash emptied')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFilterButtonContextMenu (event) {
|
handleFilterButtonContextMenu (event) {
|
||||||
@@ -260,6 +407,7 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { data, location, config, dispatch } = this.props
|
const { data, location, config, dispatch } = this.props
|
||||||
|
const { colorPicker: colorPickerState } = this.state
|
||||||
|
|
||||||
const isFolded = config.isSideNavFolded
|
const isFolded = config.isSideNavFolded
|
||||||
|
|
||||||
@@ -276,6 +424,20 @@ class SideNav extends React.Component {
|
|||||||
useDragHandle
|
useDragHandle
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let colorPicker
|
||||||
|
if (colorPickerState.show) {
|
||||||
|
colorPicker = (
|
||||||
|
<ColorPicker
|
||||||
|
color={colorPickerState.color}
|
||||||
|
targetRect={colorPickerState.targetRect}
|
||||||
|
onConfirm={this.handleColorPickerConfirm}
|
||||||
|
onCancel={this.dismissColorPicker}
|
||||||
|
onReset={this.handleColorPickerReset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const style = {}
|
const style = {}
|
||||||
if (!isFolded) style.width = this.props.width
|
if (!isFolded) style.width = this.props.width
|
||||||
const isTagActive = location.pathname.match(/tag/)
|
const isTagActive = location.pathname.match(/tag/)
|
||||||
@@ -295,6 +457,7 @@ class SideNav extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.SideNavComponent(isFolded, storageList)}
|
{this.SideNavComponent(isFolded, storageList)}
|
||||||
|
{colorPicker}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
color $ui-active-color
|
color $ui-active-color
|
||||||
span
|
span
|
||||||
margin-left 5px
|
margin-left 5px
|
||||||
|
|
||||||
.update
|
.update
|
||||||
navButtonColor()
|
navButtonColor()
|
||||||
height 24px
|
height 24px
|
||||||
@@ -47,6 +47,14 @@
|
|||||||
.update-icon
|
.update-icon
|
||||||
color $brand-color
|
color $brand-color
|
||||||
|
|
||||||
|
body[data-theme="default"]
|
||||||
|
.zoom
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
|
body[data-theme="white"]
|
||||||
|
.zoom
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
@@ -80,3 +88,14 @@ body[data-theme="monokai"]
|
|||||||
color $ui-monokai-active-color
|
color $ui-monokai-active-color
|
||||||
&:active
|
&:active
|
||||||
color $ui-monokai-active-color
|
color $ui-monokai-active-color
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
navButtonColor()
|
||||||
|
.zoom
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
|
&:hover
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-dracula-active-color
|
||||||
|
&:active
|
||||||
|
color $ui-dracula-active-color
|
||||||
@@ -5,6 +5,7 @@ import styles from './StatusBar.styl'
|
|||||||
import ZoomManager from 'browser/main/lib/ZoomManager'
|
import ZoomManager from 'browser/main/lib/ZoomManager'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
|
import EventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote, ipcRenderer } = electron
|
const { remote, ipcRenderer } = electron
|
||||||
@@ -13,6 +14,26 @@ const { dialog } = remote
|
|||||||
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
|
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
|
||||||
|
|
||||||
class StatusBar extends React.Component {
|
class StatusBar extends React.Component {
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this)
|
||||||
|
this.handleZoomOutMenuItem = this.handleZoomOutMenuItem.bind(this)
|
||||||
|
this.handleZoomResetMenuItem = this.handleZoomResetMenuItem.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
EventEmitter.on('status:zoomin', this.handleZoomInMenuItem)
|
||||||
|
EventEmitter.on('status:zoomout', this.handleZoomOutMenuItem)
|
||||||
|
EventEmitter.on('status:zoomreset', this.handleZoomResetMenuItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
EventEmitter.off('status:zoomin', this.handleZoomInMenuItem)
|
||||||
|
EventEmitter.off('status:zoomout', this.handleZoomOutMenuItem)
|
||||||
|
EventEmitter.off('status:zoomreset', this.handleZoomResetMenuItem)
|
||||||
|
}
|
||||||
|
|
||||||
updateApp () {
|
updateApp () {
|
||||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@@ -48,6 +69,20 @@ class StatusBar extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleZoomInMenuItem () {
|
||||||
|
const zoomFactor = ZoomManager.getZoom() + 0.1
|
||||||
|
this.handleZoomMenuItemClick(zoomFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleZoomOutMenuItem () {
|
||||||
|
const zoomFactor = ZoomManager.getZoom() - 0.1
|
||||||
|
this.handleZoomMenuItemClick(zoomFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleZoomResetMenuItem () {
|
||||||
|
this.handleZoomMenuItemClick(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { config, status } = this.context
|
const { config, status } = this.context
|
||||||
|
|
||||||
|
|||||||
@@ -256,3 +256,25 @@ body[data-theme="monokai"]
|
|||||||
input
|
input
|
||||||
background-color $ui-monokai-noteList-backgroundColor
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
color $ui-monokai-text-color
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
.root, .root--expanded
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control
|
||||||
|
border-color $ui-dracula-borderColor
|
||||||
|
.control-search
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control-search-icon
|
||||||
|
absolute top bottom left
|
||||||
|
line-height 32px
|
||||||
|
width 35px
|
||||||
|
color $ui-dracula-inactive-text-color
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control-search-input
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
input
|
||||||
|
background-color $ui-dracula-noteList-backgroundColor
|
||||||
|
color $ui-dracula-text-color
|
||||||
@@ -6,6 +6,7 @@ import _ from 'lodash'
|
|||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import NewNoteButton from 'browser/main/NewNoteButton'
|
import NewNoteButton from 'browser/main/NewNoteButton'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import debounce from 'lodash/debounce'
|
||||||
|
|
||||||
class TopBar extends React.Component {
|
class TopBar extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -25,6 +26,10 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.codeInitHandler = this.handleCodeInit.bind(this)
|
this.codeInitHandler = this.handleCodeInit.bind(this)
|
||||||
|
|
||||||
|
this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, {
|
||||||
|
maxWait: 1000 / 8
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -94,7 +99,6 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleKeyUp (e) {
|
handleKeyUp (e) {
|
||||||
const { router } = this.context
|
|
||||||
// reset states
|
// reset states
|
||||||
this.setState({
|
this.setState({
|
||||||
isConfirmTranslation: false
|
isConfirmTranslation: false
|
||||||
@@ -106,21 +110,21 @@ class TopBar extends React.Component {
|
|||||||
isConfirmTranslation: true
|
isConfirmTranslation: true
|
||||||
})
|
})
|
||||||
const keyword = this.refs.searchInput.value
|
const keyword = this.refs.searchInput.value
|
||||||
router.push(`/searched/${encodeURIComponent(keyword)}`)
|
this.updateKeyword(keyword)
|
||||||
this.setState({
|
|
||||||
search: keyword
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchChange (e) {
|
handleSearchChange (e) {
|
||||||
const { router } = this.context
|
|
||||||
const keyword = this.refs.searchInput.value
|
|
||||||
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
||||||
router.push(`/searched/${encodeURIComponent(keyword)}`)
|
const keyword = this.refs.searchInput.value
|
||||||
|
this.updateKeyword(keyword)
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateKeyword (keyword) {
|
||||||
|
this.context.router.push(`/searched/${encodeURIComponent(keyword)}`)
|
||||||
this.setState({
|
this.setState({
|
||||||
search: keyword
|
search: keyword
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ body
|
|||||||
::-webkit-scrollbar
|
::-webkit-scrollbar
|
||||||
width 12px
|
width 12px
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb
|
::-webkit-scrollbar-thumb
|
||||||
background-color rgba(0, 0, 0, 0.15)
|
background-color rgba(0, 0, 0, 0.15)
|
||||||
|
|
||||||
@@ -94,6 +97,7 @@ modalBackColor = white
|
|||||||
|
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
|
background-color $ui-dark-backgroundColor
|
||||||
::-webkit-scrollbar-thumb
|
::-webkit-scrollbar-thumb
|
||||||
background-color rgba(0, 0, 0, 0.3)
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
.ModalBase
|
.ModalBase
|
||||||
@@ -132,10 +136,20 @@ body[data-theme="dark"]
|
|||||||
.CodeMirror-foldgutter-folded:after
|
.CodeMirror-foldgutter-folded:after
|
||||||
content: "\25B8"
|
content: "\25B8"
|
||||||
|
|
||||||
|
.CodeMirror-hover
|
||||||
|
padding 2px 4px 0 4px
|
||||||
|
position absolute
|
||||||
|
z-index 99
|
||||||
|
|
||||||
|
.CodeMirror-hyperlink
|
||||||
|
cursor pointer
|
||||||
|
|
||||||
|
|
||||||
.sortableItemHelper
|
.sortableItemHelper
|
||||||
z-index modalZIndex + 5
|
z-index modalZIndex + 5
|
||||||
|
|
||||||
body[data-theme="solarized-dark"]
|
body[data-theme="solarized-dark"]
|
||||||
|
background-color $ui-solarized-dark-backgroundColor
|
||||||
::-webkit-scrollbar-thumb
|
::-webkit-scrollbar-thumb
|
||||||
background-color rgba(0, 0, 0, 0.3)
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
.ModalBase
|
.ModalBase
|
||||||
@@ -145,6 +159,7 @@ body[data-theme="solarized-dark"]
|
|||||||
color: $ui-solarized-dark-text-color
|
color: $ui-solarized-dark-text-color
|
||||||
|
|
||||||
body[data-theme="monokai"]
|
body[data-theme="monokai"]
|
||||||
|
background-color $ui-monokai-backgroundColor
|
||||||
::-webkit-scrollbar-thumb
|
::-webkit-scrollbar-thumb
|
||||||
background-color rgba(0, 0, 0, 0.3)
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
.ModalBase
|
.ModalBase
|
||||||
@@ -152,3 +167,19 @@ body[data-theme="monokai"]
|
|||||||
background-color $ui-monokai-backgroundColor
|
background-color $ui-monokai-backgroundColor
|
||||||
.sortableItemHelper
|
.sortableItemHelper
|
||||||
color: $ui-monokai-text-color
|
color: $ui-monokai-text-color
|
||||||
|
|
||||||
|
body[data-theme="dracula"]
|
||||||
|
background-color $ui-dracula-backgroundColor
|
||||||
|
::-webkit-scrollbar-thumb
|
||||||
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
|
.ModalBase
|
||||||
|
.modalBack
|
||||||
|
background-color $ui-dracula-backgroundColor
|
||||||
|
.sortableItemHelper
|
||||||
|
color: $ui-dracula-text-color
|
||||||
|
|
||||||
|
body[data-theme="default"]
|
||||||
|
.SideNav ::-webkit-scrollbar-thumb
|
||||||
|
background-color rgba(255, 255, 255, 0.3)
|
||||||
|
|
||||||
|
@import '../styles/Detail/TagSelect.styl'
|
||||||
@@ -45,7 +45,6 @@ function initAwsMobileAnalytics () {
|
|||||||
if (getSendEventCond()) return
|
if (getSendEventCond()) return
|
||||||
AWS.config.credentials.get((err) => {
|
AWS.config.credentials.get((err) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
|
|
||||||
recordDynamicCustomEvent('APP_STARTED')
|
recordDynamicCustomEvent('APP_STARTED')
|
||||||
recordStaticCustomEvent()
|
recordStaticCustomEvent()
|
||||||
}
|
}
|
||||||
@@ -58,7 +57,7 @@ function recordDynamicCustomEvent (type, options = {}) {
|
|||||||
mobileAnalyticsClient.recordEvent(type, options)
|
mobileAnalyticsClient.recordEvent(type, options)
|
||||||
} catch (analyticsError) {
|
} catch (analyticsError) {
|
||||||
if (analyticsError instanceof ReferenceError) {
|
if (analyticsError instanceof ReferenceError) {
|
||||||
console.log(analyticsError.name + ': ' + analyticsError.message)
|
console.error(analyticsError.name + ': ' + analyticsError.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +70,7 @@ function recordStaticCustomEvent () {
|
|||||||
})
|
})
|
||||||
} catch (analyticsError) {
|
} catch (analyticsError) {
|
||||||
if (analyticsError instanceof ReferenceError) {
|
if (analyticsError instanceof ReferenceError) {
|
||||||
console.log(analyticsError.name + ': ' + analyticsError.message)
|
console.error(analyticsError.name + ': ' + analyticsError.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,35 +16,50 @@ export const DEFAULT_CONFIG = {
|
|||||||
isSideNavFolded: false,
|
isSideNavFolded: false,
|
||||||
listWidth: 280,
|
listWidth: 280,
|
||||||
navWidth: 200,
|
navWidth: 200,
|
||||||
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
sortBy: {
|
||||||
|
default: 'UPDATED_AT' // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
||||||
|
},
|
||||||
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
|
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
|
||||||
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
||||||
amaEnabled: true,
|
amaEnabled: true,
|
||||||
hotkey: {
|
hotkey: {
|
||||||
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
||||||
toggleMode: OSX ? 'Command + M' : 'Ctrl + M'
|
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
||||||
|
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
|
||||||
|
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
|
||||||
|
toggleMenuBar: 'Alt'
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
language: 'en',
|
language: 'en',
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
showCopyNotification: true,
|
showCopyNotification: true,
|
||||||
disableDirectWrite: false,
|
disableDirectWrite: false,
|
||||||
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
||||||
|
showMenuBar: false
|
||||||
},
|
},
|
||||||
editor: {
|
editor: {
|
||||||
theme: 'base16-light',
|
theme: 'base16-light',
|
||||||
keyMap: 'sublime',
|
keyMap: 'sublime',
|
||||||
fontSize: '14',
|
fontSize: '14',
|
||||||
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
|
fontFamily: win ? 'Consolas' : 'Monaco',
|
||||||
indentType: 'space',
|
indentType: 'space',
|
||||||
indentSize: '2',
|
indentSize: '2',
|
||||||
enableRulers: false,
|
enableRulers: false,
|
||||||
rulers: [80, 120],
|
rulers: [80, 120],
|
||||||
displayLineNumbers: true,
|
displayLineNumbers: true,
|
||||||
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
|
matchingPairs: '()[]{}\'\'""$$**``',
|
||||||
|
matchingTriples: '```"""\'\'\'',
|
||||||
|
explodingPairs: '[]{}``$$',
|
||||||
|
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
||||||
|
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
|
||||||
scrollPastEnd: false,
|
scrollPastEnd: false,
|
||||||
type: 'SPLIT',
|
type: 'SPLIT', // 'SPLIT', 'EDITOR_PREVIEW'
|
||||||
fetchUrlTitle: true
|
fetchUrlTitle: true,
|
||||||
|
enableTableEditor: false,
|
||||||
|
enableFrontMatterTitle: true,
|
||||||
|
frontMatterTitleField: 'title',
|
||||||
|
spellcheck: false,
|
||||||
|
enableSmartPaste: false
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
fontSize: '14',
|
fontSize: '14',
|
||||||
@@ -57,12 +72,14 @@ export const DEFAULT_CONFIG = {
|
|||||||
latexBlockClose: '$$',
|
latexBlockClose: '$$',
|
||||||
plantUMLServerAddress: 'http://www.plantuml.com/plantuml',
|
plantUMLServerAddress: 'http://www.plantuml.com/plantuml',
|
||||||
scrollPastEnd: false,
|
scrollPastEnd: false,
|
||||||
|
scrollSync: true,
|
||||||
smartQuotes: true,
|
smartQuotes: true,
|
||||||
breaks: true,
|
breaks: true,
|
||||||
smartArrows: false,
|
smartArrows: false,
|
||||||
allowCustomCSS: false,
|
allowCustomCSS: false,
|
||||||
customCSS: '',
|
customCSS: '',
|
||||||
sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||||
|
lineThroughCheckbox: true
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
type: 'wordpress', // Available value: wordpress, add more types in the future plz
|
type: 'wordpress', // Available value: wordpress, add more types in the future plz
|
||||||
@@ -71,7 +88,8 @@ export const DEFAULT_CONFIG = {
|
|||||||
token: '',
|
token: '',
|
||||||
username: '',
|
username: '',
|
||||||
password: ''
|
password: ''
|
||||||
}
|
},
|
||||||
|
coloredTags: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate (config) {
|
function validate (config) {
|
||||||
@@ -144,6 +162,8 @@ function set (updates) {
|
|||||||
document.body.setAttribute('data-theme', 'solarized-dark')
|
document.body.setAttribute('data-theme', 'solarized-dark')
|
||||||
} else if (newConfig.ui.theme === 'monokai') {
|
} else if (newConfig.ui.theme === 'monokai') {
|
||||||
document.body.setAttribute('data-theme', 'monokai')
|
document.body.setAttribute('data-theme', 'monokai')
|
||||||
|
} else if (newConfig.ui.theme === 'dracula') {
|
||||||
|
document.body.setAttribute('data-theme', 'dracula')
|
||||||
} else {
|
} else {
|
||||||
document.body.setAttribute('data-theme', 'default')
|
document.body.setAttribute('data-theme', 'default')
|
||||||
}
|
}
|
||||||
@@ -191,7 +211,8 @@ function assignConfigValues (originalConfig, rcConfig) {
|
|||||||
function rewriteHotkey (config) {
|
function rewriteHotkey (config) {
|
||||||
const keys = [...Object.keys(config.hotkey)]
|
const keys = [...Object.keys(config.hotkey)]
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
|
config.hotkey[key] = config.hotkey[key].replace(/Cmd\s/g, 'Command ')
|
||||||
|
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
|
||||||
})
|
})
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,132 @@ const mdurl = require('mdurl')
|
|||||||
const fse = require('fs-extra')
|
const fse = require('fs-extra')
|
||||||
const escapeStringRegexp = require('escape-string-regexp')
|
const escapeStringRegexp = require('escape-string-regexp')
|
||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
|
const url = require('url')
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
||||||
const DESTINATION_FOLDER = 'attachments'
|
const DESTINATION_FOLDER = 'attachments'
|
||||||
const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
|
const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
|
||||||
|
/**
|
||||||
|
* @description
|
||||||
|
* Create a Image element to get the real size of image.
|
||||||
|
* @param {File} file the File object dropped.
|
||||||
|
* @returns {Promise<Image>} Image element created
|
||||||
|
*/
|
||||||
|
function getImage (file) {
|
||||||
|
if (_.isString(file)) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => resolve(img)
|
||||||
|
img.src = file
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => resolve(img)
|
||||||
|
reader.onload = e => {
|
||||||
|
img.src = e.target.result
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description
|
||||||
|
* Get the orientation info from iamges's EXIF data.
|
||||||
|
* case 1: The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
|
||||||
|
* case 2: The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
|
||||||
|
* case 3: The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
|
||||||
|
* case 4: The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
|
||||||
|
* case 5: The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
|
||||||
|
* case 6: The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
|
||||||
|
* case 7: The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
|
||||||
|
* case 8: The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
|
||||||
|
* Other: reserved
|
||||||
|
* ref: http://sylvana.net/jpegcrop/exif_orientation.html
|
||||||
|
* @param {File} file the File object dropped.
|
||||||
|
* @returns {Promise<Number>} Orientation info
|
||||||
|
*/
|
||||||
|
function getOrientation (file) {
|
||||||
|
const getData = arrayBuffer => {
|
||||||
|
const view = new DataView(arrayBuffer)
|
||||||
|
|
||||||
|
// Not start with SOI(Start of image) Marker return fail value
|
||||||
|
if (view.getUint16(0, false) !== 0xFFD8) return -2
|
||||||
|
const length = view.byteLength
|
||||||
|
let offset = 2
|
||||||
|
while (offset < length) {
|
||||||
|
const marker = view.getUint16(offset, false)
|
||||||
|
offset += 2
|
||||||
|
// Loop and seed for APP1 Marker
|
||||||
|
if (marker === 0xFFE1) {
|
||||||
|
// return fail value if it isn't EXIF data
|
||||||
|
if (view.getUint32(offset += 2, false) !== 0x45786966) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
// Read TIFF header,
|
||||||
|
// First 2bytes defines byte align of TIFF data.
|
||||||
|
// If it is 0x4949="II", it means "Intel" type byte align.
|
||||||
|
// If it is 0x4d4d="MM", it means "Motorola" type byte align
|
||||||
|
const little = view.getUint16(offset += 6, false) === 0x4949
|
||||||
|
offset += view.getUint32(offset + 4, little)
|
||||||
|
const tags = view.getUint16(offset, little) // Get TAG number
|
||||||
|
offset += 2
|
||||||
|
for (let i = 0; i < tags; i++) {
|
||||||
|
// Loop to find Orientation TAG and return the value
|
||||||
|
if (view.getUint16(offset + (i * 12), little) === 0x0112) {
|
||||||
|
return view.getUint16(offset + (i * 12) + 8, little)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
offset += view.getUint16(offset, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = event => resolve(getData(event.target.result))
|
||||||
|
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @description
|
||||||
|
* Rotate image file to correct direction.
|
||||||
|
* Create a canvas and draw the image with correct direction, then export to base64 format.
|
||||||
|
* @param {*} file the File object dropped.
|
||||||
|
* @return {String} Base64 encoded image.
|
||||||
|
*/
|
||||||
|
function fixRotate (file) {
|
||||||
|
return Promise.all([getImage(file), getOrientation(file)])
|
||||||
|
.then(([img, orientation]) => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (orientation > 4 && orientation < 9) {
|
||||||
|
canvas.width = img.height
|
||||||
|
canvas.height = img.width
|
||||||
|
} else {
|
||||||
|
canvas.width = img.width
|
||||||
|
canvas.height = img.height
|
||||||
|
}
|
||||||
|
switch (orientation) {
|
||||||
|
case 2: ctx.transform(-1, 0, 0, 1, img.width, 0); break
|
||||||
|
case 3: ctx.transform(-1, 0, 0, -1, img.width, img.height); break
|
||||||
|
case 4: ctx.transform(1, 0, 0, -1, 0, img.height); break
|
||||||
|
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break
|
||||||
|
case 6: ctx.transform(0, 1, -1, 0, img.height, 0); break
|
||||||
|
case 7: ctx.transform(0, -1, -1, 0, img.height, img.width); break
|
||||||
|
case 8: ctx.transform(0, -1, 1, 0, 0, img.width); break
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
ctx.drawImage(img, 0, 0)
|
||||||
|
return canvas.toDataURL()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description
|
* @description
|
||||||
@@ -38,26 +159,39 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(sourceFilePath)) {
|
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
|
||||||
reject('source file does not exist')
|
if (!isBase64 && !fs.existsSync(sourceFilePath)) {
|
||||||
|
return reject('source file does not exist')
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourcePath = sourceFilePath.sourceFilePath || sourceFilePath
|
||||||
|
const sourceURL = url.parse(/^\w+:\/\//.test(sourcePath) ? sourcePath : 'file:///' + sourcePath)
|
||||||
|
|
||||||
|
let destinationName
|
||||||
|
if (useRandomName) {
|
||||||
|
destinationName = `${uniqueSlug()}${path.extname(sourceURL.pathname) || '.png'}`
|
||||||
|
} else {
|
||||||
|
destinationName = path.basename(sourceURL.pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetStorage = findStorage.findStorage(storageKey)
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
|
|
||||||
const inputFileStream = fs.createReadStream(sourceFilePath)
|
|
||||||
let destinationName
|
|
||||||
if (useRandomName) {
|
|
||||||
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}`
|
|
||||||
} else {
|
|
||||||
destinationName = path.basename(sourceFilePath)
|
|
||||||
}
|
|
||||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||||
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
|
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
|
||||||
inputFileStream.pipe(outputFile)
|
|
||||||
inputFileStream.on('end', () => {
|
if (isBase64) {
|
||||||
resolve(destinationName)
|
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
|
||||||
})
|
const dataBuffer = Buffer.from(base64Data, 'base64')
|
||||||
|
outputFile.write(dataBuffer, () => {
|
||||||
|
resolve(destinationName)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const inputFileStream = fs.createReadStream(sourceFilePath)
|
||||||
|
inputFileStream.pipe(outputFile)
|
||||||
|
inputFileStream.on('end', () => {
|
||||||
|
resolve(destinationName)
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return reject(e)
|
return reject(e)
|
||||||
}
|
}
|
||||||
@@ -82,9 +216,9 @@ function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
|
|||||||
* @param noteKey Key of the current note
|
* @param noteKey Key of the current note
|
||||||
*/
|
*/
|
||||||
function migrateAttachments (markdownContent, storagePath, noteKey) {
|
function migrateAttachments (markdownContent, storagePath, noteKey) {
|
||||||
if (sander.existsSync(path.join(storagePath, 'images'))) {
|
if (noteKey !== undefined && sander.existsSync(path.join(storagePath, 'images'))) {
|
||||||
const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
|
const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
|
||||||
if (attachments !== []) {
|
if (attachments.length) {
|
||||||
createAttachmentDestinationFolder(storagePath, noteKey)
|
createAttachmentDestinationFolder(storagePath, noteKey)
|
||||||
}
|
}
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
@@ -107,7 +241,15 @@ function migrateAttachments (markdownContent, storagePath, noteKey) {
|
|||||||
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
|
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
|
||||||
*/
|
*/
|
||||||
function fixLocalURLS (renderedHTML, storagePath) {
|
function fixLocalURLS (renderedHTML, storagePath) {
|
||||||
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) {
|
/*
|
||||||
|
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
|
||||||
|
|
||||||
|
- `STORAGE_FOLDER_PLACEHOLDER` will match `:storage`
|
||||||
|
- `(?:(?:\\\/|%5C)[-.\\w]+)+` will match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`
|
||||||
|
- `(?:\\\/|%5C)[-.\\w]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg`
|
||||||
|
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
|
||||||
|
*/
|
||||||
|
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) {
|
||||||
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
|
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
|
||||||
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
|
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
|
||||||
})
|
})
|
||||||
@@ -133,15 +275,69 @@ function generateAttachmentMarkdown (fileName, path, showPreview) {
|
|||||||
* @param {Event} dropEvent DropEvent
|
* @param {Event} dropEvent DropEvent
|
||||||
*/
|
*/
|
||||||
function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
||||||
const file = dropEvent.dataTransfer.files[0]
|
let promise
|
||||||
const filePath = file.path
|
if (dropEvent.dataTransfer.files.length > 0) {
|
||||||
const originalFileName = path.basename(filePath)
|
promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => {
|
||||||
const fileType = file['type']
|
if (file.type.startsWith('image')) {
|
||||||
|
if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
|
||||||
|
return copyAttachment(file.path, storageKey, noteKey).then(fileName => ({
|
||||||
|
fileName,
|
||||||
|
title: path.basename(file.path),
|
||||||
|
isImage: true
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
return fixRotate(file)
|
||||||
|
.then(data => copyAttachment({type: 'base64', data: data, sourceFilePath: file.path}, storageKey, noteKey)
|
||||||
|
.then(fileName => ({
|
||||||
|
fileName,
|
||||||
|
title: path.basename(file.path),
|
||||||
|
isImage: true
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return copyAttachment(file.path, storageKey, noteKey).then(fileName => ({
|
||||||
|
fileName,
|
||||||
|
title: path.basename(file.path),
|
||||||
|
isImage: false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
let imageURL = dropEvent.dataTransfer.getData('text/plain')
|
||||||
|
|
||||||
copyAttachment(filePath, storageKey, noteKey).then((fileName) => {
|
if (!imageURL) {
|
||||||
const showPreview = fileType.startsWith('image')
|
const match = /<img[^>]*[\s"']src="([^"]+)"/.exec(dropEvent.dataTransfer.getData('text/html'))
|
||||||
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview)
|
if (match) {
|
||||||
codeEditor.insertAttachmentMd(imageMd)
|
imageURL = match[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
promise = Promise.all([getImage(imageURL)
|
||||||
|
.then(image => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
canvas.width = image.width
|
||||||
|
canvas.height = image.height
|
||||||
|
context.drawImage(image, 0, 0)
|
||||||
|
|
||||||
|
return copyAttachment({type: 'base64', data: canvas.toDataURL(), sourceFilePath: imageURL}, storageKey, noteKey)
|
||||||
|
})
|
||||||
|
.then(fileName => ({
|
||||||
|
fileName,
|
||||||
|
title: imageURL,
|
||||||
|
isImage: true
|
||||||
|
}))])
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(files => {
|
||||||
|
const attachments = files.filter(file => !!file).map(file => generateAttachmentMarkdown(file.title, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, file.fileName), file.isImage))
|
||||||
|
|
||||||
|
codeEditor.insertAttachmentMd(attachments.join('\n'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +348,7 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
|||||||
* @param {String} noteKey Key of the current note
|
* @param {String} noteKey Key of the current note
|
||||||
* @param {DataTransferItem} dataTransferItem Part of the past-event
|
* @param {DataTransferItem} dataTransferItem Part of the past-event
|
||||||
*/
|
*/
|
||||||
function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
|
function handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
|
||||||
if (!codeEditor) {
|
if (!codeEditor) {
|
||||||
throw new Error('codeEditor has to be given')
|
throw new Error('codeEditor has to be given')
|
||||||
}
|
}
|
||||||
@@ -189,6 +385,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
|
|||||||
reader.readAsDataURL(blob)
|
reader.readAsDataURL(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
|
||||||
|
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
|
||||||
|
* @param {String} storageKey Key of the current storage
|
||||||
|
* @param {String} noteKey Key of the current note
|
||||||
|
* @param {NativeImage} image The native image
|
||||||
|
*/
|
||||||
|
function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) {
|
||||||
|
if (!codeEditor) {
|
||||||
|
throw new Error('codeEditor has to be given')
|
||||||
|
}
|
||||||
|
if (!storageKey) {
|
||||||
|
throw new Error('storageKey has to be given')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noteKey) {
|
||||||
|
throw new Error('noteKey has to be given')
|
||||||
|
}
|
||||||
|
if (!image) {
|
||||||
|
throw new Error('image has to be given')
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
|
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||||
|
|
||||||
|
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||||
|
|
||||||
|
const imageName = `${uniqueSlug()}.png`
|
||||||
|
const imagePath = path.join(destinationDir, imageName)
|
||||||
|
|
||||||
|
const binaryData = image.toPNG()
|
||||||
|
fs.writeFileSync(imagePath, binaryData, 'binary')
|
||||||
|
|
||||||
|
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
|
||||||
|
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
|
||||||
|
codeEditor.insertAttachmentMd(imageMd)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Returns all attachment paths of the given markdown
|
* @description Returns all attachment paths of the given markdown
|
||||||
* @param {String} markdownContent content in which the attachment paths should be found
|
* @param {String} markdownContent content in which the attachment paths should be found
|
||||||
@@ -256,7 +490,14 @@ function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
|
|||||||
* @returns {String} Input without the references
|
* @returns {String} Input without the references
|
||||||
*/
|
*/
|
||||||
function removeStorageAndNoteReferences (input, noteKey) {
|
function removeStorageAndNoteReferences (input, noteKey) {
|
||||||
return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
|
return input.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|])', 'g'), function (match) {
|
||||||
|
const temp = match
|
||||||
|
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.sep)
|
||||||
|
.replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.sep)
|
||||||
|
.replace(new RegExp(escapeStringRegexp(path.win32.sep), 'g'), path.sep)
|
||||||
|
.replace(new RegExp(escapeStringRegexp(path.posix.sep), 'g'), path.sep)
|
||||||
|
return temp.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -402,7 +643,6 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
|
|||||||
return modifiedLinkText
|
return modifiedLinkText
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log('One if the parameters was null -> Do nothing..')
|
|
||||||
return Promise.resolve(linkText)
|
return Promise.resolve(linkText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,7 +652,8 @@ module.exports = {
|
|||||||
fixLocalURLS,
|
fixLocalURLS,
|
||||||
generateAttachmentMarkdown,
|
generateAttachmentMarkdown,
|
||||||
handleAttachmentDrop,
|
handleAttachmentDrop,
|
||||||
handlePastImageEvent,
|
handlePasteImageEvent,
|
||||||
|
handlePasteNativeImage,
|
||||||
getAttachmentsInMarkdownContent,
|
getAttachmentsInMarkdownContent,
|
||||||
getAbsolutePathsOfAttachmentsInContent,
|
getAbsolutePathsOfAttachmentsInContent,
|
||||||
removeStorageAndNoteReferences,
|
removeStorageAndNoteReferences,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function copyFile (srcPath, dstPath) {
|
|||||||
const dstFolder = path.dirname(dstPath)
|
const dstFolder = path.dirname(dstPath)
|
||||||
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
||||||
|
|
||||||
const input = fs.createReadStream(srcPath)
|
const input = fs.createReadStream(decodeURI(srcPath))
|
||||||
const output = fs.createWriteStream(dstPath)
|
const output = fs.createWriteStream(dstPath)
|
||||||
|
|
||||||
output.on('error', reject)
|
output.on('error', reject)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function validateInput (input) {
|
|||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case 'MARKDOWN_NOTE':
|
case 'MARKDOWN_NOTE':
|
||||||
if (!_.isString(input.content)) input.content = ''
|
if (!_.isString(input.content)) input.content = ''
|
||||||
|
if (!_.isArray(input.linesHighlighted)) input.linesHighlighted = []
|
||||||
break
|
break
|
||||||
case 'SNIPPET_NOTE':
|
case 'SNIPPET_NOTE':
|
||||||
if (!_.isString(input.description)) input.description = ''
|
if (!_.isString(input.description)) input.description = ''
|
||||||
@@ -23,7 +24,8 @@ function validateInput (input) {
|
|||||||
input.snippets = [{
|
input.snippets = [{
|
||||||
name: '',
|
name: '',
|
||||||
mode: 'text',
|
mode: 'text',
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ function createSnippet (snippetFile) {
|
|||||||
id: crypto.randomBytes(16).toString('hex'),
|
id: crypto.randomBytes(16).toString('hex'),
|
||||||
name: 'Unnamed snippet',
|
name: 'Unnamed snippet',
|
||||||
prefix: [],
|
prefix: [],
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}
|
}
|
||||||
fetchSnippet(null, snippetFile).then((snippets) => {
|
fetchSnippet(null, snippetFile).then((snippets) => {
|
||||||
snippets.push(newSnippet)
|
snippets.push(newSnippet)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
import resolveStorageData from './resolveStorageData'
|
import resolveStorageData from './resolveStorageData'
|
||||||
import resolveStorageNotes from './resolveStorageNotes'
|
import resolveStorageNotes from './resolveStorageNotes'
|
||||||
|
import exportNote from './exportNote'
|
||||||
import filenamify from 'filenamify'
|
import filenamify from 'filenamify'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} storageKey
|
* @param {String} storageKey
|
||||||
@@ -45,9 +45,9 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
|||||||
|
|
||||||
notes
|
notes
|
||||||
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
||||||
.forEach(snippet => {
|
.forEach(note => {
|
||||||
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
|
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
|
||||||
fs.writeFileSync(notePath, snippet.content)
|
exportNote(note.key, storage.path, note.content, notePath, null)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,27 +4,43 @@ import { findStorage } from 'browser/lib/findStorage'
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
|
const attachmentManagement = require('./attachmentManagement')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export note together with images
|
* Export note together with attachments
|
||||||
*
|
*
|
||||||
* If images is stored in the storage, creates 'images' subfolder in target directory
|
* If attachments are stored in the storage, creates 'attachments' subfolder in target directory
|
||||||
* and copies images to it. Changes links to images in the content of the note
|
* and copies attachments to it. Changes links to images in the content of the note
|
||||||
*
|
*
|
||||||
|
* @param {String} nodeKey key of the node that should be exported
|
||||||
* @param {String} storageKey or storage path
|
* @param {String} storageKey or storage path
|
||||||
* @param {String} noteContent Content to export
|
* @param {String} noteContent Content to export
|
||||||
* @param {String} targetPath Path to exported file
|
* @param {String} targetPath Path to exported file
|
||||||
* @param {function} outputFormatter
|
* @param {function} outputFormatter
|
||||||
* @return {Promise.<*[]>}
|
* @return {Promise.<*[]>}
|
||||||
*/
|
*/
|
||||||
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) {
|
||||||
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
||||||
const exportTasks = []
|
const exportTasks = []
|
||||||
|
|
||||||
if (!storagePath) {
|
if (!storagePath) {
|
||||||
throw new Error('Storage path is not found')
|
throw new Error('Storage path is not found')
|
||||||
}
|
}
|
||||||
|
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||||
|
noteContent,
|
||||||
|
storagePath
|
||||||
|
)
|
||||||
|
attachmentsAbsolutePaths.forEach(attachment => {
|
||||||
|
exportTasks.push({
|
||||||
|
src: attachment,
|
||||||
|
dst: attachmentManagement.DESTINATION_FOLDER
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
let exportedData = noteContent
|
let exportedData = attachmentManagement.removeStorageAndNoteReferences(
|
||||||
|
noteContent,
|
||||||
|
nodeKey
|
||||||
|
)
|
||||||
|
|
||||||
if (outputFormatter) {
|
if (outputFormatter) {
|
||||||
exportedData = outputFormatter(exportedData, exportTasks)
|
exportedData = outputFormatter(exportedData, exportTasks)
|
||||||
|
|||||||
63
browser/main/lib/dataApi/exportStorage.js
Normal file
63
browser/main/lib/dataApi/exportStorage.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
|
import resolveStorageData from './resolveStorageData'
|
||||||
|
import resolveStorageNotes from './resolveStorageNotes'
|
||||||
|
import filenamify from 'filenamify'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} storageKey
|
||||||
|
* @param {String} fileType
|
||||||
|
* @param {String} exportDir
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* ```
|
||||||
|
* {
|
||||||
|
* storage: Object,
|
||||||
|
* fileType: String,
|
||||||
|
* exportDir: String
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
function exportStorage (storageKey, fileType, exportDir) {
|
||||||
|
let targetStorage
|
||||||
|
try {
|
||||||
|
targetStorage = findStorage(storageKey)
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveStorageData(targetStorage)
|
||||||
|
.then(storage => (
|
||||||
|
resolveStorageNotes(storage).then(notes => ({storage, notes}))
|
||||||
|
))
|
||||||
|
.then(function exportNotes (data) {
|
||||||
|
const { storage, notes } = data
|
||||||
|
const folderNamesMapping = {}
|
||||||
|
storage.folders.forEach(folder => {
|
||||||
|
const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
|
||||||
|
folderNamesMapping[folder.key] = folderExportedDir
|
||||||
|
// make sure directory exists
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(folderExportedDir)
|
||||||
|
} catch (e) {}
|
||||||
|
})
|
||||||
|
notes
|
||||||
|
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
|
||||||
|
.forEach(markdownNote => {
|
||||||
|
const folderExportedDir = folderNamesMapping[markdownNote.folder]
|
||||||
|
const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}`
|
||||||
|
const notePath = path.join(folderExportedDir, snippetName)
|
||||||
|
fs.writeFileSync(notePath, markdownNote.content)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
storage,
|
||||||
|
fileType,
|
||||||
|
exportDir
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exportStorage
|
||||||
@@ -9,6 +9,7 @@ const dataApi = {
|
|||||||
deleteFolder: require('./deleteFolder'),
|
deleteFolder: require('./deleteFolder'),
|
||||||
reorderFolder: require('./reorderFolder'),
|
reorderFolder: require('./reorderFolder'),
|
||||||
exportFolder: require('./exportFolder'),
|
exportFolder: require('./exportFolder'),
|
||||||
|
exportStorage: require('./exportStorage'),
|
||||||
createNote: require('./createNote'),
|
createNote: require('./createNote'),
|
||||||
updateNote: require('./updateNote'),
|
updateNote: require('./updateNote'),
|
||||||
deleteNote: require('./deleteNote'),
|
deleteNote: require('./deleteNote'),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const resolveStorageData = require('./resolveStorageData')
|
|||||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||||
const consts = require('browser/lib/consts')
|
const consts = require('browser/lib/consts')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
const CSON = require('@rokt33r/season')
|
const CSON = require('@rokt33r/season')
|
||||||
/**
|
/**
|
||||||
* @return {Object} all storages and notes
|
* @return {Object} all storages and notes
|
||||||
@@ -19,11 +20,14 @@ const CSON = require('@rokt33r/season')
|
|||||||
* 2. legacy
|
* 2. legacy
|
||||||
* 3. empty directory
|
* 3. empty directory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
const fetchStorages = function () {
|
const fetchStorages = function () {
|
||||||
let rawStorages
|
let rawStorages
|
||||||
try {
|
try {
|
||||||
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
|
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
|
||||||
|
// Remove storages who's location is inaccesible.
|
||||||
|
rawStorages = rawStorages.filter(storage => fs.existsSync(storage.path))
|
||||||
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
|
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to parse cached data from localStorage', e)
|
console.warn('Failed to parse cached data from localStorage', e)
|
||||||
@@ -36,6 +40,7 @@ function init () {
|
|||||||
|
|
||||||
const fetchNotes = function (storages) {
|
const fetchNotes = function (storages) {
|
||||||
const findNotesFromEachStorage = storages
|
const findNotesFromEachStorage = storages
|
||||||
|
.filter(storage => fs.existsSync(storage.path))
|
||||||
.map((storage) => {
|
.map((storage) => {
|
||||||
return resolveStorageNotes(storage)
|
return resolveStorageNotes(storage)
|
||||||
.then((notes) => {
|
.then((notes) => {
|
||||||
@@ -51,7 +56,11 @@ function init () {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (unknownCount > 0) {
|
if (unknownCount > 0) {
|
||||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
try {
|
||||||
|
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error writting boostnote.json: ' + e + ' from init.js')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return notes
|
return notes
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ function importAll (storage, data) {
|
|||||||
isStarred: false,
|
isStarred: false,
|
||||||
title: article.title,
|
title: article.title,
|
||||||
content: '# ' + article.title + '\n\n' + article.content,
|
content: '# ' + article.title + '\n\n' + article.content,
|
||||||
key: noteKey
|
key: noteKey,
|
||||||
|
linesHighlighted: article.linesHighlighted
|
||||||
}
|
}
|
||||||
notes.push(newNote)
|
notes.push(newNote)
|
||||||
} else {
|
} else {
|
||||||
@@ -87,7 +88,8 @@ function importAll (storage, data) {
|
|||||||
snippets: [{
|
snippets: [{
|
||||||
name: article.mode,
|
name: article.mode,
|
||||||
mode: article.mode,
|
mode: article.mode,
|
||||||
content: article.content
|
content: article.content,
|
||||||
|
linesHighlighted: article.linesHighlighted
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
notes.push(newNote)
|
notes.push(newNote)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ function renameStorage (key, name) {
|
|||||||
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||||
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('error got')
|
|
||||||
console.error(err)
|
console.error(err)
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,13 +31,9 @@ function resolveStorageData (storageCache) {
|
|||||||
|
|
||||||
const version = parseInt(storage.version, 10)
|
const version = parseInt(storage.version, 10)
|
||||||
if (version >= 1) {
|
if (version >= 1) {
|
||||||
if (version > 1) {
|
|
||||||
console.log('The repository version is newer than one of current app.')
|
|
||||||
}
|
|
||||||
return Promise.resolve(storage)
|
return Promise.resolve(storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Transform Legacy storage', storage.path)
|
|
||||||
return migrateFromV6Storage(storage.path)
|
return migrateFromV6Storage(storage.path)
|
||||||
.then(() => storage)
|
.then(() => storage)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ function resolveStorageNotes (storage) {
|
|||||||
notePathList = sander.readdirSync(notesDirPath)
|
notePathList = sander.readdirSync(notesDirPath)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
console.log(notesDirPath, ' doesn\'t exist.')
|
console.error(notesDirPath, ' doesn\'t exist.')
|
||||||
sander.mkdirSync(notesDirPath)
|
sander.mkdirSync(notesDirPath)
|
||||||
} else {
|
} else {
|
||||||
console.warn('Failed to find note dir', notesDirPath, err)
|
console.warn('Failed to find note dir', notesDirPath, err)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ function toggleStorage (key, isOpen) {
|
|||||||
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||||
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('error got')
|
|
||||||
console.error(err)
|
console.error(err)
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ function validateInput (input) {
|
|||||||
if (input.content != null) {
|
if (input.content != null) {
|
||||||
if (!_.isString(input.content)) validatedInput.content = ''
|
if (!_.isString(input.content)) validatedInput.content = ''
|
||||||
else validatedInput.content = input.content
|
else validatedInput.content = input.content
|
||||||
|
|
||||||
|
if (!_.isArray(input.linesHighlighted)) validatedInput.linesHighlighted = []
|
||||||
|
else validatedInput.linesHighlighted = input.linesHighlighted
|
||||||
}
|
}
|
||||||
return validatedInput
|
return validatedInput
|
||||||
case 'SNIPPET_NOTE':
|
case 'SNIPPET_NOTE':
|
||||||
@@ -51,7 +54,8 @@ function validateInput (input) {
|
|||||||
validatedInput.snippets = [{
|
validatedInput.snippets = [{
|
||||||
name: '',
|
name: '',
|
||||||
mode: 'text',
|
mode: 'text',
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}]
|
}]
|
||||||
} else {
|
} else {
|
||||||
validatedInput.snippets = input.snippets
|
validatedInput.snippets = input.snippets
|
||||||
@@ -96,12 +100,14 @@ function updateNote (storageKey, noteKey, input) {
|
|||||||
snippets: [{
|
snippets: [{
|
||||||
name: '',
|
name: '',
|
||||||
mode: 'text',
|
mode: 'text',
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: 'MARKDOWN_NOTE',
|
type: 'MARKDOWN_NOTE',
|
||||||
content: ''
|
content: '',
|
||||||
|
linesHighlighted: []
|
||||||
}
|
}
|
||||||
noteData.title = ''
|
noteData.title = ''
|
||||||
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')
|
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ function updateSnippet (snippet, snippetFile) {
|
|||||||
if (
|
if (
|
||||||
currentSnippet.name === snippet.name &&
|
currentSnippet.name === snippet.name &&
|
||||||
currentSnippet.prefix === snippet.prefix &&
|
currentSnippet.prefix === snippet.prefix &&
|
||||||
currentSnippet.content === snippet.content
|
currentSnippet.content === snippet.content &&
|
||||||
|
currentSnippet.linesHighlighted === snippet.linesHighlighted
|
||||||
) {
|
) {
|
||||||
// if everything is the same then don't write to disk
|
// if everything is the same then don't write to disk
|
||||||
resolve(snippets)
|
resolve(snippets)
|
||||||
@@ -20,6 +21,7 @@ function updateSnippet (snippet, snippetFile) {
|
|||||||
currentSnippet.name = snippet.name
|
currentSnippet.name = snippet.name
|
||||||
currentSnippet.prefix = snippet.prefix
|
currentSnippet.prefix = snippet.prefix
|
||||||
currentSnippet.content = snippet.content
|
currentSnippet.content = snippet.content
|
||||||
|
currentSnippet.linesHighlighted = (snippet.linesHighlighted)
|
||||||
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
|
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
|
||||||
if (err) reject(err)
|
if (err) reject(err)
|
||||||
resolve(snippets)
|
resolve(snippets)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user