mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
1411 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6cad2ab4df | ||
|
|
be972781ee | ||
|
|
58fbc298b1 | ||
|
|
7de7772339 | ||
|
|
ad847a2f5d | ||
|
|
856d52891c | ||
|
|
8de3b3bd8d | ||
|
|
0414483be2 | ||
|
|
22939aa472 | ||
|
|
0cb7c44985 | ||
|
|
b18a09e5eb | ||
|
|
ef3649b1d6 | ||
|
|
ac70a0d94d | ||
|
|
3b91f9b88b | ||
|
|
c37b780ca4 | ||
|
|
20061d2c65 | ||
|
|
f18fa77c1c | ||
|
|
a4c6869d4d | ||
|
|
f9a0070c82 | ||
|
|
5cc52f91cb | ||
|
|
a46b9fb2be | ||
|
|
933e38eca9 | ||
|
|
e182390480 | ||
|
|
563fdcba94 | ||
|
|
bc640834cd | ||
|
|
0e9e7d644a | ||
|
|
1d9b3ac2b5 | ||
|
|
aebed4a644 | ||
|
|
7bfb094a40 | ||
|
|
f90a44c1d0 | ||
|
|
dfcf6d2729 | ||
|
|
806a5daa86 | ||
|
|
4a3602099a | ||
|
|
c69be54655 | ||
|
|
680eaa1d4a | ||
|
|
9cc7b8bcc6 | ||
|
|
55d86d853a | ||
|
|
7d9f309e04 | ||
|
|
c2f0147cff | ||
|
|
05488e66ae | ||
|
|
09eac89086 | ||
|
|
866a0e7534 | ||
|
|
4307db11c5 | ||
|
|
3c8337cf54 | ||
|
|
883b4c4c26 | ||
|
|
6bc42c564d | ||
|
|
4f79f52524 | ||
|
|
83f8151ca4 | ||
|
|
d2b2e76a6a | ||
|
|
9d9109e9e5 | ||
|
|
18efb89b9a | ||
|
|
cefe883025 | ||
|
|
0ffa0b96d3 | ||
|
|
0429acfa1b | ||
|
|
827e3c1829 | ||
|
|
aa756ef194 | ||
|
|
d8aad65b24 | ||
|
|
1038e86196 | ||
|
|
47845fd4e3 | ||
|
|
294c3f10ab | ||
|
|
f6afc756dc | ||
|
|
64407e5ca6 | ||
|
|
0a42b0f61f | ||
|
|
ae493cbd0e | ||
|
|
ddd339851b | ||
|
|
82db986bd7 | ||
|
|
7bacd6f8f0 | ||
|
|
f0941f47dd | ||
|
|
c9d05b1117 | ||
|
|
7ee12752ec | ||
|
|
b44772441d | ||
|
|
72e3784fa5 | ||
|
|
2c7f24cb8c | ||
|
|
8a6c86bf65 | ||
|
|
cc52cf60dc | ||
|
|
398ebae2ba | ||
|
|
0095735841 | ||
|
|
95c10a1de7 | ||
|
|
8f4c92e251 | ||
|
|
58354061d8 | ||
|
|
5de176757d | ||
|
|
7414d52dc2 | ||
|
|
c42b5c8806 | ||
|
|
5c60da0f8f | ||
|
|
70d02e9a6d | ||
|
|
6401016424 | ||
|
|
cd031a89fb | ||
|
|
aadba79002 | ||
|
|
4a017fd8ae | ||
|
|
e6072c8fe9 | ||
|
|
f7fd99ec20 | ||
|
|
836fc13449 | ||
|
|
e1f78cd682 | ||
|
|
c69f34836a | ||
|
|
48a0db28d7 | ||
|
|
58eeb90158 | ||
|
|
c36689934d | ||
|
|
342575a576 | ||
|
|
785272540e | ||
|
|
653b985018 | ||
|
|
40d90a53b2 | ||
|
|
7c3aaff635 | ||
|
|
4bb7929229 | ||
|
|
bdd5b7b3a7 | ||
|
|
a2ddb56540 | ||
|
|
7c0097951c | ||
|
|
7970016fbf | ||
|
|
129e3b283d | ||
|
|
868eefd2cf | ||
|
|
82178055af | ||
|
|
c42e1a0ab4 | ||
|
|
720475dae5 | ||
|
|
259df880ba | ||
|
|
68a328a364 | ||
|
|
3bc21cdb09 | ||
|
|
9e8ef70510 | ||
|
|
5fd822a24d | ||
|
|
8ee4dbbb5c | ||
|
|
9522a4d5d9 | ||
|
|
eefecdefbe | ||
|
|
84e670e71c | ||
|
|
c22b69234f | ||
|
|
29cd63d3a7 | ||
|
|
214ed388f2 | ||
|
|
0bd3445370 | ||
|
|
002dc9b017 | ||
|
|
ebf5a03f56 | ||
|
|
2797faafe5 | ||
|
|
84ac739993 | ||
|
|
13c37f046f | ||
|
|
2631cc3747 | ||
|
|
5873e8e896 | ||
|
|
6dc633c2a1 | ||
|
|
8b07126285 | ||
|
|
93a120543a | ||
|
|
af0aa4a567 | ||
|
|
58fd1f0c46 | ||
|
|
030041932e | ||
|
|
8dbf456398 | ||
|
|
3a90a078ce | ||
|
|
c30957fc9f | ||
|
|
f6db946c9a | ||
|
|
c6c0d4c62a | ||
|
|
57befc4ccb | ||
|
|
e716af75ed | ||
|
|
efe2bea64b | ||
|
|
9b926326ef | ||
|
|
de71033fe2 | ||
|
|
2c10bf251d | ||
|
|
8af50aa5bd | ||
|
|
05bedfe3d4 | ||
|
|
a1929dac8a | ||
|
|
834ecc643a | ||
|
|
60baabf7e7 | ||
|
|
185a149d74 | ||
|
|
5d62dd2002 | ||
|
|
9eaa6b5cec | ||
|
|
6ff03bbb95 | ||
|
|
9fac6bca64 | ||
|
|
88856b788a | ||
|
|
eda4e46d9f | ||
|
|
0ae1263d9d | ||
|
|
31f1ebe801 | ||
|
|
c6a9c9c57d | ||
|
|
35bcbbbae4 | ||
|
|
22e2c3da1f | ||
|
|
b526d48946 | ||
|
|
91a95b7c20 | ||
|
|
d634e1124a | ||
|
|
309e159df1 | ||
|
|
ffae53326a | ||
|
|
ddd1522e19 | ||
|
|
4bc0cccb24 | ||
|
|
72fbefa300 | ||
|
|
30378eeb50 | ||
|
|
03293c0d25 | ||
|
|
2e3f6e39f6 | ||
|
|
e4d4041c6b | ||
|
|
166a5c10a7 | ||
|
|
1fec81cc3e | ||
|
|
9c247bcb22 | ||
|
|
707356bffe | ||
|
|
fc88a49acc | ||
|
|
8ccf490e9b | ||
|
|
ea768f982e | ||
|
|
172ea82954 | ||
|
|
10500c3c1c | ||
|
|
225916fbba | ||
|
|
2fce78422b | ||
|
|
8132dd6847 | ||
|
|
4caee1e103 | ||
|
|
ca0b03e97c | ||
|
|
f03178bb8d | ||
|
|
d083a86138 | ||
|
|
8216b992ea | ||
|
|
8e74ee7dde | ||
|
|
b207fe14df | ||
|
|
92cfa21be6 | ||
|
|
5fd482428a | ||
|
|
98b09f7edc | ||
|
|
7b83a34777 | ||
|
|
aeb63ec901 | ||
|
|
436093e0b6 | ||
|
|
d7ee06ce6d | ||
|
|
e72003009d | ||
|
|
cfe8235a36 | ||
|
|
8c1ac9c5b3 | ||
|
|
77e3a7d43a | ||
|
|
53728a0f4a | ||
|
|
06f33d9a63 | ||
|
|
ed2698ecc3 | ||
|
|
0cb5554ae5 | ||
|
|
89850c0b22 | ||
|
|
d78b94f4e8 | ||
|
|
c2c50817f1 | ||
|
|
680c2a2904 | ||
|
|
a1085e3863 | ||
|
|
37f6a05170 | ||
|
|
0d296c3b25 | ||
|
|
73caa2508e | ||
|
|
69e012a6f0 | ||
|
|
21251a1915 | ||
|
|
67143ba2d5 | ||
|
|
d399cba4c0 | ||
|
|
9cad7cd025 | ||
|
|
a593842265 | ||
|
|
2f4eb595f6 | ||
|
|
bfcf349ffe | ||
|
|
2bd78cd47f | ||
|
|
713615e28b | ||
|
|
2b2f17525e | ||
|
|
cd6233a3d7 | ||
|
|
f76224bd17 | ||
|
|
1516807ed5 | ||
|
|
8afa373726 | ||
|
|
199f2202e0 | ||
|
|
86a6311f75 | ||
|
|
9893fd9ae5 | ||
|
|
d6c28da3a8 | ||
|
|
52f694a714 | ||
|
|
0ca4e6ca4f | ||
|
|
50bce4892f | ||
|
|
ca345cf008 | ||
|
|
e0e1290fae | ||
|
|
a84b2611e4 | ||
|
|
ec31fab344 | ||
|
|
1b96eee4de | ||
|
|
a6af5de3e1 | ||
|
|
52b3068330 | ||
|
|
0934c08dfe | ||
|
|
f0428fde66 | ||
|
|
b3f57a67c4 | ||
|
|
8bc2b1262b | ||
|
|
fb24efd3de | ||
|
|
ce594b0b5a | ||
|
|
7b39ab4ec4 | ||
|
|
f717ed9f66 | ||
|
|
d3091a5384 | ||
|
|
2a6d950a4b | ||
|
|
b03b9d5334 | ||
|
|
c9cb31bd02 | ||
|
|
905d6860fc | ||
|
|
6f52744b0f | ||
|
|
007d3e52c5 | ||
|
|
f10fa632ca | ||
|
|
266323b90b | ||
|
|
60707a8f45 | ||
|
|
b89896a4e7 | ||
|
|
d69fd12fb9 | ||
|
|
0bdcfa6028 | ||
|
|
55b8488901 | ||
|
|
8ddbf2067b | ||
|
|
fa5cebda6d | ||
|
|
4fd01b4234 | ||
|
|
0f354f4f06 | ||
|
|
f94a197828 | ||
|
|
a26ff660b0 | ||
|
|
50cc648799 | ||
|
|
a7946805ae | ||
|
|
f3b2969b42 | ||
|
|
d6c3490165 | ||
|
|
6ee594d4d1 | ||
|
|
7e1596de30 | ||
|
|
67bba043ed | ||
|
|
26d7f4923d | ||
|
|
e9218d1088 | ||
|
|
03fd1e29e3 | ||
|
|
ff59af6b51 | ||
|
|
73ba8b8b13 | ||
|
|
ffc3fb770c | ||
|
|
d58ea70a95 | ||
|
|
90e8dd038d | ||
|
|
56d1e3edaa | ||
|
|
83a9e54896 | ||
|
|
ab038b1f31 | ||
|
|
f5a9d3928c | ||
|
|
2bc0bce1b5 | ||
|
|
372933fd99 | ||
|
|
9112347e95 | ||
|
|
30548a68e4 | ||
|
|
ce052d1691 | ||
|
|
765ba8c867 | ||
|
|
a20c0cd49e | ||
|
|
e06ca9a056 | ||
|
|
d04048c749 | ||
|
|
2d0f7589ea | ||
|
|
dc60be404a | ||
|
|
9ff5cc51f9 | ||
|
|
e9de8f42e5 | ||
|
|
5bd0499ae4 | ||
|
|
99e706bcd2 | ||
|
|
239edb0605 | ||
|
|
bf3f5a5971 | ||
|
|
92be3f32d6 | ||
|
|
106f5a53ff | ||
|
|
8c43f3d567 | ||
|
|
2e09501c8a | ||
|
|
a2592e48c8 | ||
|
|
291d76674b | ||
|
|
78957cf128 | ||
|
|
e88694b049 | ||
|
|
5e7bdf7354 | ||
|
|
2e3e0bc1d8 | ||
|
|
b33e6b232c | ||
|
|
df6b083670 | ||
|
|
05009d40c4 | ||
|
|
ea27a3b449 | ||
|
|
2831b0bd2a | ||
|
|
32e22dd507 | ||
|
|
2ee9951853 | ||
|
|
b1912135ed | ||
|
|
bed3d42923 | ||
|
|
c4ec69a43f | ||
|
|
24b004bb2d | ||
|
|
84925b24b5 | ||
|
|
c02b91dfd4 | ||
|
|
066d97220f | ||
|
|
61ed47dda0 | ||
|
|
68c0f210cc | ||
|
|
6c542750f4 | ||
|
|
25440a26ee | ||
|
|
ab393b1f6d | ||
|
|
e643147b69 | ||
|
|
6c4aa71cbc | ||
|
|
9930ba8748 | ||
|
|
fbb8b4687b | ||
|
|
01b1c49738 | ||
|
|
c9c28eda1b | ||
|
|
744bcba599 | ||
|
|
d76db726c4 | ||
|
|
71ec528a87 | ||
|
|
fbbc93900e | ||
|
|
33b45737c9 | ||
|
|
4a55f78a48 | ||
|
|
a82a79e25c | ||
|
|
6ec2124a9c | ||
|
|
1f1ef1440e | ||
|
|
a7d0a4bdac | ||
|
|
d2129ffac6 | ||
|
|
a4782f0663 | ||
|
|
16794b9d78 | ||
|
|
a76aed2d4e | ||
|
|
d2163dacf9 | ||
|
|
158305346f | ||
|
|
e692432242 | ||
|
|
a7b85b123e | ||
|
|
ddcd722598 | ||
|
|
358458a937 | ||
|
|
8925f7c381 | ||
|
|
ff2e39901a | ||
|
|
90ff0f43ea | ||
|
|
442c352c8d | ||
|
|
e91b7fb082 | ||
|
|
88de66a31f | ||
|
|
d3b3e45800 | ||
|
|
50d2f90621 | ||
|
|
8ccf6cb8a3 | ||
|
|
2e9b478824 | ||
|
|
89b2d54725 | ||
|
|
3d0af2d8ca | ||
|
|
813b433f4d | ||
|
|
0bce96b0c6 | ||
|
|
1d4f1764fc | ||
|
|
2994420160 | ||
|
|
65d8d7282f | ||
|
|
47af3f09fc | ||
|
|
f4024f4683 | ||
|
|
ee0ed6df7a | ||
|
|
d3fbba3572 | ||
|
|
4a6b22f5b7 | ||
|
|
d070305002 | ||
|
|
a8500150b0 | ||
|
|
02fb1d01ad | ||
|
|
497dee038f | ||
|
|
a4af77f91e | ||
|
|
f2a4e1d230 | ||
|
|
8560901f80 | ||
|
|
daea604c60 | ||
|
|
7aedb59f26 | ||
|
|
0be1c2f464 | ||
|
|
0dbfaf0e79 | ||
|
|
4147399cda | ||
|
|
cc667ac738 | ||
|
|
022915ffc9 | ||
|
|
4b9cf775ff | ||
|
|
56879924bd | ||
|
|
6142f2d641 | ||
|
|
0d53f799b7 | ||
|
|
65e77e9669 | ||
|
|
2c8f3b56ae | ||
|
|
8ab190affc | ||
|
|
eafccc4fc4 | ||
|
|
ce440351a5 | ||
|
|
be94edde0f | ||
|
|
4fcc9af933 | ||
|
|
e2b4ac6ee8 | ||
|
|
c151049cc2 | ||
|
|
ac778d3f65 | ||
|
|
1aed0cb4b9 | ||
|
|
5836b65aad | ||
|
|
46f750efba | ||
|
|
b33c9e23ce | ||
|
|
14694f1cb0 | ||
|
|
75575348cd | ||
|
|
f6ad0a235c | ||
|
|
bbf6c60888 | ||
|
|
f5915f3e10 | ||
|
|
a32cfc8aff | ||
|
|
c90a878c9e | ||
|
|
b46b958105 | ||
|
|
6943b06a88 | ||
|
|
27a9def88c | ||
|
|
11f8cfe0e6 | ||
|
|
e1e3cc7999 | ||
|
|
254c8816f1 | ||
|
|
9a445e34fd | ||
|
|
ee78e113de | ||
|
|
0dfb14962a | ||
|
|
d493df4295 | ||
|
|
f0144233f9 | ||
|
|
90f21f4ed1 | ||
|
|
646ebe592e | ||
|
|
b098a15e9c | ||
|
|
4f98995fe4 | ||
|
|
56231edc3a | ||
|
|
871ab428c2 | ||
|
|
a9b75f752e | ||
|
|
9590559b81 | ||
|
|
24bd2eddf1 | ||
|
|
f4ba06c401 | ||
|
|
cd405d1df9 | ||
|
|
9d6dbc1a6f | ||
|
|
6d57712fca | ||
|
|
191f2cacbf | ||
|
|
080448af3a | ||
|
|
6e2272d043 | ||
|
|
71078dea4f | ||
|
|
333f0be879 | ||
|
|
3b0f664a3b | ||
|
|
2a23d19321 | ||
|
|
5ee4237510 | ||
|
|
bdb906c26d | ||
|
|
02095ac155 | ||
|
|
80d1ca81ac | ||
|
|
3bbabbc80b | ||
|
|
f8ff3d4bf5 | ||
|
|
3a40f9ebd6 | ||
|
|
cf776088e6 | ||
|
|
7ab81608e8 | ||
|
|
017e40fcb9 | ||
|
|
29309fbaa3 | ||
|
|
b766b08bf5 | ||
|
|
e796e00963 | ||
|
|
10136df977 | ||
|
|
44ec107ce8 | ||
|
|
c7fb5b0475 | ||
|
|
785735ccf5 | ||
|
|
f0736ccf8d | ||
|
|
a24a7e03be | ||
|
|
be235e5204 | ||
|
|
57ec598672 | ||
|
|
2627c09cda | ||
|
|
36e63bb8a9 | ||
|
|
5ea24f650b | ||
|
|
8c792ce7a1 | ||
|
|
055969f5c6 | ||
|
|
145ae10a79 | ||
|
|
bdb9349b52 | ||
|
|
8b11b57ec5 | ||
|
|
29888c89ad | ||
|
|
281fb2afd3 | ||
|
|
fbb7839f83 | ||
|
|
d3f9c170ac | ||
|
|
4f9a0b0040 | ||
|
|
aae584106a | ||
|
|
2a784deb4b | ||
|
|
5f5a7880a6 | ||
|
|
ab8b6d806d | ||
|
|
6c4206a2dd | ||
|
|
0d61d9cee4 | ||
|
|
48bdb9e818 | ||
|
|
cfe447a403 | ||
|
|
2c30f0e487 | ||
|
|
147211478c | ||
|
|
35b7674e69 | ||
|
|
55075b2a96 | ||
|
|
6577599959 | ||
|
|
0c4e72e507 | ||
|
|
9bf96e943d | ||
|
|
2811843e70 | ||
|
|
842ece2a8a | ||
|
|
222de03ea3 | ||
|
|
d31ae9f3fc | ||
|
|
1dd7644e12 | ||
|
|
2cee54f70a | ||
|
|
a8431fae96 | ||
|
|
03d11b7b58 | ||
|
|
316e2eeefb | ||
|
|
c21e19337a | ||
|
|
b9cab0dae8 | ||
|
|
33b3299ca2 | ||
|
|
00ba38beba | ||
|
|
433fce286e | ||
|
|
b36322bba4 | ||
|
|
c147e0a789 | ||
|
|
8bf5d02624 | ||
|
|
35616c1ccd | ||
|
|
7c8939ecb8 | ||
|
|
ccb0302d3f | ||
|
|
0cfd048013 | ||
|
|
f72b4f0249 | ||
|
|
b5cb209f14 | ||
|
|
1af374439d | ||
|
|
bbcd674516 | ||
|
|
3cba71b7a8 | ||
|
|
6a1e9c5818 | ||
|
|
ede41d01b4 | ||
|
|
826a67b550 | ||
|
|
f3d59a9b61 | ||
|
|
65434453b8 | ||
|
|
b8658abbad | ||
|
|
0387b33f6c | ||
|
|
41042fe64e | ||
|
|
1efc3bb15d | ||
|
|
0816a9410b | ||
|
|
cb823eaaa6 | ||
|
|
44f027bf18 | ||
|
|
571d159a1f | ||
|
|
a30d977727 | ||
|
|
847ad2d781 | ||
|
|
da9067a672 | ||
|
|
5ed5950ed5 | ||
|
|
f53c182cfb | ||
|
|
bb2978b370 | ||
|
|
66ae4a5163 | ||
|
|
f435595ad8 | ||
|
|
4bec4596fe | ||
|
|
10b86aec49 | ||
|
|
ecabd37cbb | ||
|
|
f3cabfcb4d | ||
|
|
46fc010cf9 | ||
|
|
e370c76521 | ||
|
|
1c283f88d0 | ||
|
|
cfffa1b10a | ||
|
|
8aaae4de49 | ||
|
|
4f6c35713e | ||
|
|
8fb0b5b572 | ||
|
|
12262671ee | ||
|
|
3165c62985 | ||
|
|
1ca1166f74 | ||
|
|
7b4d2f3b97 | ||
|
|
ccce75a047 | ||
|
|
410e82e375 | ||
|
|
bd385ec062 | ||
|
|
638227ef25 | ||
|
|
e70b35126b | ||
|
|
4d41c7f37f | ||
|
|
d01a7b16a1 | ||
|
|
326d873279 | ||
|
|
74c32bed29 | ||
|
|
a222fd0786 | ||
|
|
c633ba9497 | ||
|
|
40b5472866 | ||
|
|
a413e273ca | ||
|
|
0f82085cae | ||
|
|
fa1ab04409 | ||
|
|
f370508c93 | ||
|
|
7472019422 | ||
|
|
aa3597881a | ||
|
|
a36841e501 | ||
|
|
fe9afc8952 | ||
|
|
63d39a81aa | ||
|
|
b11dc2ca20 | ||
|
|
10f5ad6127 | ||
|
|
4800a88cf6 | ||
|
|
9d8bc40594 | ||
|
|
7d3d96a6e0 | ||
|
|
4c99429d76 | ||
|
|
96fbd7572c | ||
|
|
b012bba6d6 | ||
|
|
45d90b2530 | ||
|
|
a8b946a07a | ||
|
|
18392c4f23 | ||
|
|
88e7869284 | ||
|
|
3c8332c448 | ||
|
|
207f95693a | ||
|
|
de15120bf8 | ||
|
|
1dcf11e1c4 | ||
|
|
b74ba22c44 | ||
|
|
fa2d34dcfc | ||
|
|
0280a5f09e | ||
|
|
a35f876f4f | ||
|
|
c22b7f81c1 | ||
|
|
9344fd78d8 | ||
|
|
e89c0a3e61 | ||
|
|
5aa7ef5738 | ||
|
|
88ac6c6fc6 | ||
|
|
a537f9762b | ||
|
|
aab192223a | ||
|
|
6439810d03 | ||
|
|
8ae7d96cc7 | ||
|
|
c5c78763d1 | ||
|
|
012908e32d | ||
|
|
1f1e545538 | ||
|
|
5b8519b2b8 | ||
|
|
6502158716 | ||
|
|
7c525ab7a6 | ||
|
|
778e3d9799 | ||
|
|
e5f986a959 | ||
|
|
d76ecd5423 | ||
|
|
6bcb6398f8 | ||
|
|
6c341d8fa5 | ||
|
|
0d5e57eeab | ||
|
|
aef603ed8c | ||
|
|
97600e526b | ||
|
|
f3f6095d81 | ||
|
|
fb7280127c | ||
|
|
60df509fc6 | ||
|
|
ced237fbda | ||
|
|
9473a26892 | ||
|
|
e5825e898d | ||
|
|
8d59bd9f71 | ||
|
|
8f90bb83e7 | ||
|
|
ba888a7165 | ||
|
|
9591a7cac2 | ||
|
|
e8fd53d8b7 | ||
|
|
3b7c36b1c9 | ||
|
|
2750a3ef30 | ||
|
|
7d59f51244 | ||
|
|
a3ec6f470a | ||
|
|
8e85a33dac | ||
|
|
77542597f5 | ||
|
|
2a26e8a010 | ||
|
|
8b4c1a6c38 | ||
|
|
7d7e277cc6 | ||
|
|
cb1da609a4 | ||
|
|
e4fbdb35a3 | ||
|
|
c3586372e0 | ||
|
|
808d193543 | ||
|
|
0b65b70af2 | ||
|
|
deab34abd6 | ||
|
|
646cded650 | ||
|
|
9047320301 | ||
|
|
f16b054f01 | ||
|
|
fea856202d | ||
|
|
4f15cc3f08 | ||
|
|
1769dd959e | ||
|
|
1253b81a01 | ||
|
|
3b524f6aba | ||
|
|
051ccad29a | ||
|
|
c82eba05d1 | ||
|
|
74af199afc | ||
|
|
c2afdba659 | ||
|
|
e6ae45f133 | ||
|
|
129ef6766b | ||
|
|
3194a7b1a2 | ||
|
|
46a733ba5b | ||
|
|
466844fc55 | ||
|
|
0cc793e3fe | ||
|
|
0fdfb385a4 | ||
|
|
419a4c6b2d | ||
|
|
55e9441547 | ||
|
|
7abff6ded4 | ||
|
|
a648d310a5 | ||
|
|
4b56fa56b5 | ||
|
|
18bf700936 | ||
|
|
67ddff736c | ||
|
|
1bba125a2a | ||
|
|
187acad592 | ||
|
|
5e07c7b3e1 | ||
|
|
cd942cf8b6 | ||
|
|
c43589fe6a | ||
|
|
d4594eff3b | ||
|
|
809a0e846b | ||
|
|
7f00ce2598 | ||
|
|
d7d77dbfe9 | ||
|
|
7a124c74cc | ||
|
|
f8549f4643 | ||
|
|
0476ff70da | ||
|
|
5ec541c3c1 | ||
|
|
7b920348f3 | ||
|
|
51b1ef41a1 | ||
|
|
12447effc9 | ||
|
|
dc1b059a9d | ||
|
|
a676ebf46c | ||
|
|
338f9fb5a9 | ||
|
|
98c9132d4a | ||
|
|
a054f12492 | ||
|
|
c42ac1df1b | ||
|
|
d9d46cda1c | ||
|
|
f678a17505 | ||
|
|
3da4bb69ce | ||
|
|
3e2b876c59 | ||
|
|
10daf2599d | ||
|
|
56d6eb7077 | ||
|
|
9b54272f8e | ||
|
|
685e003db8 | ||
|
|
701fc0bc80 | ||
|
|
e5518ac0fa | ||
|
|
8e1bf48cd1 | ||
|
|
8dd82e1a3b | ||
|
|
4418bfe965 | ||
|
|
39c4d710bc | ||
|
|
51a8c47afd | ||
|
|
922570bb5c | ||
|
|
ca282d5635 | ||
|
|
f04ad1e702 | ||
|
|
dc03bb76e6 | ||
|
|
d894bb4aab | ||
|
|
83da07a941 |
2
.babelrc
2
.babelrc
@@ -5,7 +5,7 @@
|
||||
"presets": ["react-hmre"]
|
||||
},
|
||||
"test": {
|
||||
"presets": ["react", "es2015"],
|
||||
"presets": ["env" ,"react", "es2015"],
|
||||
"plugins": [
|
||||
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
|
||||
]
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"theme": "monokai"
|
||||
},
|
||||
"hotkey": {
|
||||
"toggleFinder": "Cmd + Alt + S",
|
||||
"toggleMain": "Cmd + Alt + L"
|
||||
},
|
||||
"isSideNavFolded": false,
|
||||
@@ -23,7 +22,10 @@
|
||||
"fontSize": "14",
|
||||
"lineNumber": true
|
||||
},
|
||||
"sortBy": "UPDATED_AT",
|
||||
"sortBy": {
|
||||
"default": "UPDATED_AT"
|
||||
},
|
||||
"sortTagsBy": "ALPHABETICAL",
|
||||
"ui": {
|
||||
"defaultNote": "ALWAYS_ASK",
|
||||
"disableDirectWrite": false,
|
||||
|
||||
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
|
||||
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
compiled/
|
||||
dist/
|
||||
extra_scripts/
|
||||
@@ -3,7 +3,9 @@
|
||||
"plugins": ["react"],
|
||||
"rules": {
|
||||
"no-useless-escape": 0,
|
||||
"prefer-const": "warn",
|
||||
"prefer-const": ["warn", {
|
||||
"destructuring": "all"
|
||||
}],
|
||||
"no-unused-vars": "warn",
|
||||
"no-undef": "warn",
|
||||
"no-lone-blocks": "warn",
|
||||
@@ -17,5 +19,8 @@
|
||||
"FileReader": true,
|
||||
"localStorage": true,
|
||||
"fetch": true
|
||||
},
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- stable
|
||||
- lts/*
|
||||
- 7
|
||||
script:
|
||||
- npm run lint && npm run test
|
||||
- yarn jest
|
||||
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
|
||||
after_success:
|
||||
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
||||
|
||||
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,10 +1,35 @@
|
||||
<!--
|
||||
Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug.
|
||||
|
||||
If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile.
|
||||
-->
|
||||
# Current behavior
|
||||
|
||||
<!--
|
||||
Love Boostnote? Please consider supporting us via OpenCollective:
|
||||
👉 https://opencollective.com/boostnoteio
|
||||
Let us know what is currently happening.
|
||||
|
||||
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
|
||||
|
||||
<!--
|
||||
Let us know what you think should happen!
|
||||
-->
|
||||
|
||||
# Steps to reproduce
|
||||
|
||||
<!--
|
||||
Please be thorough, issues we can reproduce are easier to fix!
|
||||
-->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
# Environment
|
||||
|
||||
- Version :
|
||||
- OS Version and name :
|
||||
|
||||
<!--
|
||||
Love Boostnote? Please consider supporting us on IssueHunt:
|
||||
👉 https://issuehunt.io/repos/53266139
|
||||
-->
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -2,7 +2,7 @@ GPL-3.0
|
||||
|
||||
Boostnote - an open source note-taking app made for programmers just like you.
|
||||
|
||||
Copyright (C) 2017 Maisin&Co., Inc.
|
||||
Copyright (C) 2017 - 2019 BoostIO
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
7
__mocks__/electron.js
Normal file
7
__mocks__/electron.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
require: jest.genMockFunction(),
|
||||
match: jest.genMockFunction(),
|
||||
app: jest.genMockFunction(),
|
||||
remote: jest.genMockFunction(),
|
||||
dialog: jest.genMockFunction()
|
||||
}
|
||||
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 eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
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 {
|
||||
constructor (props) {
|
||||
@@ -18,10 +20,10 @@ class MarkdownEditor extends React.Component {
|
||||
this.supportMdSelectionBold = [16, 17, 186]
|
||||
|
||||
this.state = {
|
||||
status: 'PREVIEW',
|
||||
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'CODE',
|
||||
renderValue: props.value,
|
||||
keyPressed: new Set(),
|
||||
isLocked: false
|
||||
isLocked: props.isLocked
|
||||
}
|
||||
|
||||
this.lockEditorCode = () => this.handleLockEditor()
|
||||
@@ -64,17 +66,20 @@ class MarkdownEditor extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
setValue (value) {
|
||||
this.refs.code.setValue(value)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.value = this.refs.code.value
|
||||
this.props.onChange(e)
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
if (this.state.isLocked) return
|
||||
const { config } = this.props
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||
const newStatus = this.state.status === 'PREVIEW'
|
||||
? 'CODE'
|
||||
: 'PREVIEW'
|
||||
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
|
||||
this.setState({
|
||||
status: newStatus
|
||||
}, () => {
|
||||
@@ -84,6 +89,10 @@ class MarkdownEditor extends React.Component {
|
||||
this.refs.preview.focus()
|
||||
}
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
|
||||
const newConfig = Object.assign({}, config)
|
||||
newConfig.editor.delfaultStatus = newStatus
|
||||
ConfigManager.set(newConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -92,7 +101,9 @@ class MarkdownEditor extends React.Component {
|
||||
if (this.state.isLocked) return
|
||||
this.setState({ keyPressed: new Set() })
|
||||
const { config } = this.props
|
||||
if (config.editor.switchPreview === 'BLUR') {
|
||||
if (config.editor.switchPreview === 'BLUR' ||
|
||||
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
|
||||
) {
|
||||
const cursorPosition = this.refs.code.editor.getCursor()
|
||||
this.setState({
|
||||
status: 'PREVIEW'
|
||||
@@ -104,6 +115,20 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleDoubleClick (e) {
|
||||
if (this.state.isLocked) return
|
||||
this.setState({keyPressed: new Set()})
|
||||
const { config } = this.props
|
||||
if (config.editor.switchPreview === 'DBL_CLICK') {
|
||||
this.setState({
|
||||
status: 'CODE'
|
||||
}, () => {
|
||||
this.refs.code.focus()
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handlePreviewMouseDown (e) {
|
||||
this.previewMouseDownedAt = new Date()
|
||||
}
|
||||
@@ -124,8 +149,10 @@ class MarkdownEditor extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /\[x\]/i
|
||||
const uncheckedMatch = /\[ \]/
|
||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||
const checkReplace = /\[x\]/i
|
||||
const uncheckReplace = /\[ \]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
@@ -134,10 +161,10 @@ class MarkdownEditor extends React.Component {
|
||||
const targetLine = lines[lineIndex]
|
||||
|
||||
if (targetLine.match(checkedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
|
||||
}
|
||||
if (targetLine.match(uncheckedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
||||
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
|
||||
}
|
||||
this.refs.code.setValue(lines.join('\n'))
|
||||
}
|
||||
@@ -196,6 +223,28 @@ class MarkdownEditor extends React.Component {
|
||||
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) {
|
||||
const keyPressed = this.state.keyPressed
|
||||
keyPressed.delete(e.keyCode)
|
||||
@@ -207,7 +256,7 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, value, config, storageKey } = this.props
|
||||
const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
@@ -234,7 +283,7 @@ class MarkdownEditor extends React.Component {
|
||||
: 'codeEditor--hide'
|
||||
}
|
||||
ref='code'
|
||||
mode='GitHub Flavored Markdown'
|
||||
mode='Boost Flavored Markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
@@ -242,11 +291,24 @@ class MarkdownEditor extends React.Component {
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
enableRulers={config.editor.enableRulers}
|
||||
rulers={config.editor.rulers}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
linesHighlighted={linesHighlighted}
|
||||
onChange={(e) => this.handleChange(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'
|
||||
? 'preview'
|
||||
@@ -262,8 +324,13 @@ class MarkdownEditor extends React.Component {
|
||||
lineNumber={config.preview.lineNumber}
|
||||
indentSize={editorIndentSize}
|
||||
scrollPastEnd={config.preview.scrollPastEnd}
|
||||
smartQuotes={config.preview.smartQuotes}
|
||||
smartArrows={config.preview.smartArrows}
|
||||
breaks={config.preview.breaks}
|
||||
sanitize={config.preview.sanitize}
|
||||
ref='preview'
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
||||
tabIndex='0'
|
||||
value={this.state.renderValue}
|
||||
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
||||
@@ -271,6 +338,11 @@ class MarkdownEditor extends React.Component {
|
||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||
onDrop={(e) => this.handleDropImage(e)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
.preview
|
||||
display block
|
||||
absolute top bottom left right
|
||||
z-index 100
|
||||
background-color white
|
||||
height 100%
|
||||
width 100%
|
||||
|
||||
834
browser/components/MarkdownPreview.js
Normal file → Executable file
834
browser/components/MarkdownPreview.js
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import _ from 'lodash'
|
||||
|
||||
import styles from './MarkdownSplitEditor.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
@@ -12,19 +13,75 @@ class MarkdownSplitEditor extends React.Component {
|
||||
this.value = props.value
|
||||
this.focus = () => this.refs.code.focus()
|
||||
this.reload = () => this.refs.code.reload()
|
||||
this.userScroll = true
|
||||
this.state = {
|
||||
isSliderFocused: false,
|
||||
codeEditorWidthInPercent: 50
|
||||
}
|
||||
}
|
||||
|
||||
handleOnChange () {
|
||||
setValue (value) {
|
||||
this.refs.code.setValue(value)
|
||||
}
|
||||
|
||||
handleOnChange (e) {
|
||||
this.value = this.refs.code.value
|
||||
this.props.onChange()
|
||||
this.props.onChange(e)
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
if (!this.props.config.preview.scrollSync) return
|
||||
|
||||
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
|
||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||
let srcTop, srcHeight, targetTop, targetHeight
|
||||
|
||||
if (this.userScroll) {
|
||||
if (e.doc) {
|
||||
srcTop = _.get(e, 'doc.scrollTop')
|
||||
srcHeight = _.get(e, 'doc.height')
|
||||
targetTop = _.get(previewDoc, 'body.scrollTop')
|
||||
targetHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||
} else {
|
||||
srcTop = _.get(previewDoc, 'body.scrollTop')
|
||||
srcHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||
targetTop = _.get(codeDoc, 'scrollTop')
|
||||
targetHeight = _.get(codeDoc, 'height')
|
||||
}
|
||||
|
||||
const distance = (targetHeight * srcTop / srcHeight) - targetTop
|
||||
const framerate = 1000 / 60
|
||||
const frames = 20
|
||||
const refractory = frames * framerate
|
||||
|
||||
this.userScroll = false
|
||||
|
||||
let frame = 0
|
||||
let scrollPos, time
|
||||
const timer = setInterval(() => {
|
||||
time = frame / frames
|
||||
scrollPos = time < 0.5
|
||||
? 2 * time * time // ease in
|
||||
: -1 + (4 - 2 * time) * time // ease out
|
||||
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
|
||||
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance)
|
||||
if (frame >= frames) {
|
||||
clearInterval(timer)
|
||||
setTimeout(() => { this.userScroll = true }, refractory)
|
||||
}
|
||||
frame++
|
||||
}, framerate)
|
||||
}
|
||||
}
|
||||
|
||||
handleCheckboxClick (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /\[x\]/i
|
||||
const uncheckedMatch = /\[ \]/
|
||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||
const checkReplace = /\[x\]/i
|
||||
const uncheckReplace = /\[ \]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
@@ -33,42 +90,99 @@ class MarkdownSplitEditor extends React.Component {
|
||||
const targetLine = lines[lineIndex]
|
||||
|
||||
if (targetLine.match(checkedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
|
||||
}
|
||||
if (targetLine.match(uncheckedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
||||
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
|
||||
}
|
||||
this.refs.code.setValue(lines.join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove (e) {
|
||||
if (this.state.isSliderFocused) {
|
||||
const rootRect = this.refs.root.getBoundingClientRect()
|
||||
const rootWidth = rootRect.width
|
||||
const offset = rootRect.left
|
||||
let newCodeEditorWidthInPercent = (e.pageX - offset) / rootWidth * 100
|
||||
|
||||
// limit minSize to 10%, maxSize to 90%
|
||||
if (newCodeEditorWidthInPercent <= 10) {
|
||||
newCodeEditorWidthInPercent = 10
|
||||
}
|
||||
|
||||
if (newCodeEditorWidthInPercent >= 90) {
|
||||
newCodeEditorWidthInPercent = 90
|
||||
}
|
||||
|
||||
this.setState({
|
||||
codeEditorWidthInPercent: newCodeEditorWidthInPercent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isSliderFocused: false
|
||||
})
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isSliderFocused: true
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, value, storageKey } = this.props
|
||||
const {config, value, storageKey, noteKey, linesHighlighted} = this.props
|
||||
const storage = findStorage(storageKey)
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||
const previewStyle = {}
|
||||
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
||||
previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%'
|
||||
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none'
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='root' ref='root'
|
||||
onMouseMove={e => this.handleMouseMove(e)}
|
||||
onMouseUp={e => this.handleMouseUp(e)}>
|
||||
<CodeEditor
|
||||
styleName='codeEditor'
|
||||
ref='code'
|
||||
mode='GitHub Flavored Markdown'
|
||||
width={this.state.codeEditorWidthInPercent + '%'}
|
||||
mode='Boost Flavored Markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
enableRulers={config.editor.enableRulers}
|
||||
rulers={config.editor.rulers}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
storageKey={storageKey}
|
||||
onChange={this.handleOnChange.bind(this)}
|
||||
noteKey={noteKey}
|
||||
linesHighlighted={linesHighlighted}
|
||||
onChange={(e) => this.handleOnChange(e)}
|
||||
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-hitbox' />
|
||||
</div>
|
||||
<MarkdownPreview
|
||||
style={previewStyle}
|
||||
styleName='preview'
|
||||
@@ -80,12 +194,21 @@ class MarkdownSplitEditor extends React.Component {
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
scrollPastEnd={config.preview.scrollPastEnd}
|
||||
smartQuotes={config.preview.smartQuotes}
|
||||
smartArrows={config.preview.smartArrows}
|
||||
breaks={config.preview.breaks}
|
||||
sanitize={config.preview.sanitize}
|
||||
ref='preview'
|
||||
tabInde='0'
|
||||
value={value}
|
||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
height 100%
|
||||
font-size 30px
|
||||
display flex
|
||||
.codeEditor
|
||||
width 50%
|
||||
.preview
|
||||
width 50%
|
||||
.slider
|
||||
absolute top bottom
|
||||
top -2px
|
||||
width 0
|
||||
z-index 0
|
||||
.slider-hitbox
|
||||
absolute top bottom left right
|
||||
width 7px
|
||||
left -3px
|
||||
z-index 10
|
||||
cursor col-resize
|
||||
|
||||
@@ -4,37 +4,49 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { isArray } from 'lodash'
|
||||
import invertColor from 'invert-color'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
||||
import styles from './NoteItem.styl'
|
||||
import TodoProcess from './TodoProcess'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
/**
|
||||
* @description Tag element component.
|
||||
* @param {string} tagName
|
||||
* @param {string} color
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const TagElement = ({ tagName }) => (
|
||||
<span styleName='item-bottom-tagList-item' key={tagName}>
|
||||
const TagElement = ({ tagName, color }) => {
|
||||
const style = {}
|
||||
if (color) {
|
||||
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.
|
||||
* @param {Array|null} tags
|
||||
* @param {boolean} showTagsAlphabetically
|
||||
* @param {Object} coloredTags
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const TagElementList = (tags) => {
|
||||
const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
|
||||
if (!isArray(tags)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tagElements = tags.map(tag => (
|
||||
TagElement({tagName: tag})
|
||||
))
|
||||
|
||||
return tagElements
|
||||
if (showTagsAlphabetically) {
|
||||
return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
||||
} else {
|
||||
return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,48 +56,80 @@ const TagElementList = (tags) => {
|
||||
* @param {Function} handleNoteClick
|
||||
* @param {Function} handleNoteContextMenu
|
||||
* @param {Function} handleDragStart
|
||||
* @param {Object} coloredTags
|
||||
* @param {string} dateDisplay
|
||||
*/
|
||||
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
|
||||
<div styleName={isActive
|
||||
? 'item--active'
|
||||
: 'item'
|
||||
}
|
||||
key={`${note.storage}-${note.key}`}
|
||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
||||
const NoteItem = ({
|
||||
isActive,
|
||||
note,
|
||||
dateDisplay,
|
||||
handleNoteClick,
|
||||
handleNoteContextMenu,
|
||||
handleDragStart,
|
||||
pathname,
|
||||
storageName,
|
||||
folderName,
|
||||
viewType,
|
||||
showTagsAlphabetically,
|
||||
coloredTags
|
||||
}) => (
|
||||
<div
|
||||
styleName={isActive ? 'item--active' : 'item'}
|
||||
key={note.key}
|
||||
onClick={e => handleNoteClick(e, note.key)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||
onDragStart={e => handleDragStart(e, note)}
|
||||
draggable='true'
|
||||
>
|
||||
<div styleName='item-wrapper'>
|
||||
{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-file-text-o' />
|
||||
}
|
||||
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
|
||||
<div styleName='item-title'>
|
||||
{note.title.trim().length > 0
|
||||
? note.title
|
||||
: <span styleName='item-title-empty'>Empty</span>
|
||||
}
|
||||
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
|
||||
</div>
|
||||
|
||||
<div styleName='item-bottom-time'>{dateDisplay}</div>
|
||||
{note.isStarred
|
||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||
}
|
||||
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||
}
|
||||
{note.type === 'MARKDOWN_NOTE'
|
||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||
: ''
|
||||
<div styleName='item-middle'>
|
||||
<div styleName='item-middle-time'>{dateDisplay}</div>
|
||||
<div styleName='item-middle-app-meta'>
|
||||
<div
|
||||
title={
|
||||
viewType === 'ALL'
|
||||
? storageName
|
||||
: viewType === 'STORAGE' ? folderName : null
|
||||
}
|
||||
styleName='item-middle-app-meta-label'
|
||||
>
|
||||
{viewType === 'ALL' && storageName}
|
||||
{viewType === 'STORAGE' && folderName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='item-bottom'>
|
||||
<div styleName='item-bottom-tagList'>
|
||||
{note.tags.length > 0
|
||||
? TagElementList(note.tags)
|
||||
: <span styleName='item-bottom-tagList-empty' />
|
||||
}
|
||||
? TagElementList(note.tags, showTagsAlphabetically, coloredTags)
|
||||
: <span
|
||||
style={{ fontStyle: 'italic', opacity: 0.5 }}
|
||||
styleName='item-bottom-tagList-empty'
|
||||
>
|
||||
{i18n.__('No tags')}
|
||||
</span>}
|
||||
</div>
|
||||
<div>
|
||||
{note.isStarred
|
||||
? <img
|
||||
styleName='item-star'
|
||||
src='../resources/icon/icon-starred.svg'
|
||||
/>
|
||||
: ''}
|
||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||
: ''}
|
||||
{note.type === 'MARKDOWN_NOTE'
|
||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,6 +139,7 @@ const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteCont
|
||||
NoteItem.propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
dateDisplay: PropTypes.string.isRequired,
|
||||
coloredTags: PropTypes.object,
|
||||
note: PropTypes.shape({
|
||||
storage: PropTypes.string.isRequired,
|
||||
key: PropTypes.string.isRequired,
|
||||
@@ -102,7 +147,11 @@ NoteItem.propTypes = {
|
||||
title: PropTypes.string.isrequired,
|
||||
tags: PropTypes.array,
|
||||
isStarred: PropTypes.bool.isRequired,
|
||||
isTrashed: PropTypes.bool.isRequired
|
||||
isTrashed: PropTypes.bool.isRequired,
|
||||
blog: {
|
||||
blogLink: PropTypes.string,
|
||||
blogId: PropTypes.number
|
||||
}
|
||||
}),
|
||||
handleNoteClick: PropTypes.func.isRequired,
|
||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||
|
||||
@@ -90,6 +90,26 @@ $control-height = 30px
|
||||
font-weight normal
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-middle
|
||||
font-size 13px
|
||||
padding-left 2px
|
||||
padding-bottom 2px
|
||||
|
||||
.item-middle-time
|
||||
color $ui-inactive-text-color
|
||||
display inline-block
|
||||
|
||||
.item-middle-app-meta
|
||||
float right
|
||||
.item-middle-app-meta-label
|
||||
opacity 0.4
|
||||
color $ui-inactive-text-color
|
||||
padding 0 3px
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
max-width 200px
|
||||
|
||||
.item-bottom
|
||||
position relative
|
||||
bottom 0px
|
||||
@@ -97,7 +117,7 @@ $control-height = 30px
|
||||
font-size 12px
|
||||
line-height 20px
|
||||
overflow ellipsis
|
||||
display flex
|
||||
display block
|
||||
|
||||
.item-bottom-tagList
|
||||
flex 1
|
||||
@@ -125,10 +145,8 @@ $control-height = 30px
|
||||
|
||||
.item-star
|
||||
position absolute
|
||||
right -6px
|
||||
bottom 23px
|
||||
width 16px
|
||||
height 16px
|
||||
right 2px
|
||||
top 5px
|
||||
color alpha($ui-favorite-star-button-color, 60%)
|
||||
font-size 12px
|
||||
padding 0
|
||||
@@ -136,10 +154,8 @@ $control-height = 30px
|
||||
|
||||
.item-pin
|
||||
position absolute
|
||||
right 0px
|
||||
bottom 2px
|
||||
width 34px
|
||||
height 34px
|
||||
right 25px
|
||||
top 7px
|
||||
color #E54D42
|
||||
font-size 14px
|
||||
padding 0
|
||||
@@ -305,3 +321,149 @@ body[data-theme="solarized-dark"]
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.item
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
// background-color alpha($ui-monokai-noteList-backgroundColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-monokai-noteList-backgroundColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-monokai-noteList-backgroundColor, 10%)
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.item-wrapper
|
||||
border-color alpha($ui-monokai-button-backgroundColor, 60%)
|
||||
|
||||
.item--active
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.item-wrapper
|
||||
border-color transparent
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
color $ui-monokai-active-color
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-monokai-button--active-backgroundColor, 60%)
|
||||
color #f92672
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(#fff, 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
|
||||
|
||||
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
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NoteItemSimple.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
/**
|
||||
* @description Note item component when using simple display mode.
|
||||
@@ -14,14 +15,23 @@ import styles from './NoteItemSimple.styl'
|
||||
* @param {Function} handleNoteContextMenu
|
||||
* @param {Function} handleDragStart
|
||||
*/
|
||||
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
|
||||
const NoteItemSimple = ({
|
||||
isActive,
|
||||
isAllNotesView,
|
||||
note,
|
||||
handleNoteClick,
|
||||
handleNoteContextMenu,
|
||||
handleDragStart,
|
||||
pathname,
|
||||
storage
|
||||
}) => (
|
||||
<div styleName={isActive
|
||||
? 'item-simple--active'
|
||||
: 'item-simple'
|
||||
}
|
||||
key={`${note.storage}-${note.key}`}
|
||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
||||
key={note.key}
|
||||
onClick={e => handleNoteClick(e, note.key)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||
onDragStart={e => handleDragStart(e, note)}
|
||||
draggable='true'
|
||||
>
|
||||
@@ -30,14 +40,19 @@ const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu
|
||||
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
|
||||
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
|
||||
}
|
||||
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||
: ''
|
||||
}
|
||||
{note.title.trim().length > 0
|
||||
? note.title
|
||||
: <span styleName='item-simple-title-empty'>Empty</span>
|
||||
: <span styleName='item-simple-title-empty'>{i18n.__('Empty note')}</span>
|
||||
}
|
||||
{isAllNotesView && <div styleName='item-simple-right'>
|
||||
<span styleName='item-simple-right-storageName'>
|
||||
{storage.name}
|
||||
</span>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -104,6 +104,7 @@ body[data-theme="dark"]
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
color $ui-dark-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
@@ -117,6 +118,7 @@ body[data-theme="dark"]
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
@@ -132,6 +134,7 @@ body[data-theme="dark"]
|
||||
.item-simple-wrapper
|
||||
border-color transparent
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
color $ui-dark-text-color
|
||||
@@ -165,9 +168,10 @@ body[data-theme="solarized-dark"]
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
// background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
@@ -178,9 +182,10 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-solarized-dark-button--active-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
// background-color $ui-solarized-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
@@ -192,11 +197,13 @@ body[data-theme="solarized-dark"]
|
||||
|
||||
.item-simple--active
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-button--active-backgroundColor
|
||||
background-color $ui-solarized-dark-tag-backgroundColor
|
||||
.item-simple-wrapper
|
||||
border-color transparent
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
color $ui-dark-text-color
|
||||
.item-simple-bottom-time
|
||||
color $ui-solarized-dark-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
@@ -207,3 +214,139 @@ body[data-theme="solarized-dark"]
|
||||
color #c0392b
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(#fff, 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
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.item-simple
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
background-color alpha($ui-monokai-button-backgroundColor, 60%)
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(#fff, 20%)
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.item-simple--active
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-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-monokai-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(#fff, 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
|
||||
|
||||
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
|
||||
@@ -41,3 +41,25 @@ body[data-theme="solarized-dark"]
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
&:hover
|
||||
color #5CB85C
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.notification-area
|
||||
background-color none
|
||||
|
||||
.notification-link
|
||||
color $ui-monokai-text-color
|
||||
border none
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:hover
|
||||
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
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SideNavFilter.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
/**
|
||||
* @param {boolean} isFolded
|
||||
@@ -17,7 +18,7 @@ import styles from './SideNavFilter.styl'
|
||||
const SideNavFilter = ({
|
||||
isFolded, isHomeActive, handleAllNotesButtonClick,
|
||||
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
||||
counterTotalNote, counterStarredNote
|
||||
counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
|
||||
}) => (
|
||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||
|
||||
@@ -31,7 +32,7 @@ const SideNavFilter = ({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>All Notes</span>
|
||||
<span styleName='menu-button-label'>{i18n.__('All Notes')}</span>
|
||||
<span styleName='counters'>{counterTotalNote}</span>
|
||||
</button>
|
||||
|
||||
@@ -45,12 +46,12 @@ const SideNavFilter = ({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>Starred</span>
|
||||
<span styleName='menu-button-label'>{i18n.__('Starred')}</span>
|
||||
<span styleName='counters'>{counterStarredNote}</span>
|
||||
</button>
|
||||
|
||||
<button styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
|
||||
onClick={handleTrashedButtonClick}
|
||||
onClick={handleTrashedButtonClick} onContextMenu={handleFilterButtonContextMenu}
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isTrashedActive
|
||||
@@ -59,7 +60,7 @@ const SideNavFilter = ({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span styleName='menu-button-label'>Trash</span>
|
||||
<span styleName='menu-button-label'>{i18n.__('Trash')}</span>
|
||||
<span styleName='counters'>{counterDelNote}</span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -68,10 +68,9 @@
|
||||
.menu-button-label
|
||||
position fixed
|
||||
display inline-block
|
||||
height 32px
|
||||
height 36px
|
||||
left 44px
|
||||
padding 0 10px
|
||||
margin-top -8px
|
||||
margin-left 0
|
||||
overflow ellipsis
|
||||
z-index 10
|
||||
@@ -223,3 +222,87 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-text-color
|
||||
.menu-button-label
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.menu-button
|
||||
&:active
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.menu-button--active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.menu-button-star--active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.menu-button-trash--active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
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
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SnippetTab.styl'
|
||||
import context from 'browser/lib/context'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
class SnippetTab extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -28,7 +29,7 @@ class SnippetTab extends React.Component {
|
||||
handleContextMenu (e) {
|
||||
context.popup([
|
||||
{
|
||||
label: 'Rename',
|
||||
label: i18n.__('Rename'),
|
||||
click: (e) => this.handleRenameClick(e)
|
||||
}
|
||||
])
|
||||
@@ -54,10 +55,10 @@ class SnippetTab extends React.Component {
|
||||
this.handleRename()
|
||||
break
|
||||
case 27:
|
||||
this.setState({
|
||||
name: this.props.snippet.name,
|
||||
this.setState((prevState, props) => ({
|
||||
name: props.snippet.name,
|
||||
isRenaming: false
|
||||
})
|
||||
}))
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -114,7 +115,7 @@ class SnippetTab extends React.Component {
|
||||
{snippet.name.trim().length > 0
|
||||
? snippet.name
|
||||
: <span styleName='button-unnamed'>
|
||||
Unnamed
|
||||
{i18n.__('Unnamed')}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
@@ -1,33 +1,44 @@
|
||||
.root
|
||||
position relative
|
||||
flex 1
|
||||
min-width 70px
|
||||
overflow hidden
|
||||
border-left 1px solid $ui-borderColor
|
||||
border-top 1px solid $ui-borderColor
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||
.deleteButton
|
||||
color $ui-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-backgroundColor, 15%)
|
||||
&:active
|
||||
color white
|
||||
background-color $ui-active-color
|
||||
color: $ui-text-color
|
||||
visibility visible
|
||||
transition 0.15s
|
||||
.button
|
||||
color: $ui-text-color
|
||||
transition 0.15s
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
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
|
||||
width 100%
|
||||
height 29px
|
||||
overflow ellipsis
|
||||
text-align left
|
||||
padding-right 30px
|
||||
padding-right 23px
|
||||
border none
|
||||
background-color transparent
|
||||
transition 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.deleteButton
|
||||
position absolute
|
||||
@@ -41,6 +52,7 @@
|
||||
color $ui-inactive-text-color
|
||||
background-color transparent
|
||||
border-radius 2px
|
||||
visibility hidden
|
||||
|
||||
.input
|
||||
height 29px
|
||||
@@ -49,76 +61,66 @@
|
||||
width 100%
|
||||
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"]
|
||||
.root
|
||||
color $ui-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
border-top 1px solid $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
||||
&:active
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
transition 0.15s
|
||||
.button
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
transition 0.15s
|
||||
.deleteButton
|
||||
color $ui-dark-text-color
|
||||
transition 0.15s
|
||||
|
||||
.root--active
|
||||
color $ui-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
border-top 1px solid $ui-dark-borderColor
|
||||
.button
|
||||
color $ui-dark-text-color
|
||||
.deleteButton
|
||||
color $ui-dark-text-color
|
||||
|
||||
.button
|
||||
border none
|
||||
color $ui-dark-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-dark-text-color, 30%)
|
||||
transition 0.15s
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.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
|
||||
&:hover
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
transition 0.15s
|
||||
.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
|
||||
color $ui-solarized-dark-button--active-color
|
||||
transition 0.15s
|
||||
.button
|
||||
color $ui-solarized-dark-button--active-color
|
||||
transition 0.15s
|
||||
|
||||
.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
|
||||
border none
|
||||
@@ -126,13 +128,75 @@ body[data-theme="solarized-dark"]
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-solarized-dark-button--active-color
|
||||
transition 0.15s
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-color $ui-monokai-borderColor
|
||||
&:hover
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
transition 0.15s
|
||||
.deleteButton
|
||||
color alpha($ui-solarized-dark-text-color, 30%)
|
||||
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
|
||||
@@ -6,6 +6,18 @@ import React from 'react'
|
||||
import styles from './StorageItem.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import _ from 'lodash'
|
||||
import { SortableHandle } from 'react-sortable-hoc'
|
||||
|
||||
const DraggableIcon = SortableHandle(({ className }) => (
|
||||
<i className={`fa ${className}`} />
|
||||
))
|
||||
|
||||
const FolderIcon = ({ className, color, isActive }) => {
|
||||
const iconStyle = isActive ? 'fa-folder-open-o' : 'fa-folder-o'
|
||||
return (
|
||||
<i className={`fa ${iconStyle} ${className}`} style={{ color: color }} />
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} isActive
|
||||
@@ -21,34 +33,51 @@ import _ from 'lodash'
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const StorageItem = ({
|
||||
isActive, handleButtonClick, handleContextMenu, folderName,
|
||||
folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave
|
||||
}) => (
|
||||
<button styleName={isActive
|
||||
? 'folderList-item--active'
|
||||
: 'folderList-item'
|
||||
}
|
||||
styles,
|
||||
isActive,
|
||||
handleButtonClick,
|
||||
handleContextMenu,
|
||||
folderName,
|
||||
folderColor,
|
||||
isFolded,
|
||||
noteCount,
|
||||
handleDrop,
|
||||
handleDragEnter,
|
||||
handleDragLeave
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
|
||||
onClick={handleButtonClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<span styleName={isFolded
|
||||
? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||
}>
|
||||
<text style={{color: folderColor, paddingRight: '10px'}}>{isActive ? <i className='fa fa-folder-open-o' /> : <i className='fa fa-folder-o' />}</text>{isFolded ? _.truncate(folderName, {length: 1, omission: ''}) : folderName}
|
||||
</span>
|
||||
{(!isFolded && _.isNumber(noteCount)) &&
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||
{!isFolded &&
|
||||
<DraggableIcon className={styles['folderList-item-reorder']} />}
|
||||
<span
|
||||
styleName={
|
||||
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||
}
|
||||
>
|
||||
<FolderIcon
|
||||
styleName='folderList-item-icon'
|
||||
color={folderColor}
|
||||
isActive={isActive}
|
||||
/>
|
||||
{isFolded
|
||||
? _.truncate(folderName, { length: 1, omission: '' })
|
||||
: folderName}
|
||||
</span>
|
||||
{!isFolded &&
|
||||
_.isNumber(noteCount) &&
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>}
|
||||
{isFolded &&
|
||||
<span styleName='folderList-item-tooltip'>
|
||||
{folderName}
|
||||
</span>
|
||||
}
|
||||
<span styleName='folderList-item-tooltip'>{folderName}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
StorageItem.propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
border none
|
||||
overflow ellipsis
|
||||
font-size 14px
|
||||
align-items: center
|
||||
&:first-child
|
||||
margin-top 0
|
||||
&:hover
|
||||
@@ -34,9 +35,7 @@
|
||||
.folderList-item-name
|
||||
display block
|
||||
flex 1
|
||||
padding 0 12px
|
||||
height 26px
|
||||
line-height 26px
|
||||
padding-right: 10px
|
||||
border-width 0 0 0 2px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
@@ -59,8 +58,8 @@
|
||||
opacity 0
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
height 26px
|
||||
line-height 26px
|
||||
height 34px
|
||||
line-height 32px
|
||||
|
||||
.folderList-item:hover, .folderList-item--active:hover
|
||||
.folderList-item-tooltip
|
||||
@@ -69,9 +68,20 @@
|
||||
.folderList-item-name--folded
|
||||
@extend .folderList-item-name
|
||||
padding-left 7px
|
||||
text
|
||||
.folderList-item-icon
|
||||
font-size 9px
|
||||
|
||||
.folderList-item-icon
|
||||
padding-right: 10px
|
||||
|
||||
.folderList-item-reorder
|
||||
font-size: 9px
|
||||
padding: 10px 8px 10px 9px;
|
||||
color: rgba(147, 147, 149, 0.3)
|
||||
cursor: ns-resize
|
||||
&:before
|
||||
content: "\f142 \f142"
|
||||
|
||||
body[data-theme="white"]
|
||||
.folderList-item
|
||||
color $ui-inactive-text-color
|
||||
@@ -128,3 +138,41 @@ body[data-theme="solarized-dark"]
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.folderList-item
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:active
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
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'
|
||||
|
||||
/**
|
||||
* @param {Array} storgaeList
|
||||
* @param {Array} storageList
|
||||
*/
|
||||
|
||||
const StorageList = ({storageList}) => (
|
||||
<div styleName='storageList'>
|
||||
const StorageList = ({storageList, isFolded}) => (
|
||||
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
|
||||
{storageList.length > 0 ? storageList : (
|
||||
<div styleName='storgaeList-empty'>No storage mount.</div>
|
||||
<div styleName='storageList-empty'>No storage mount.</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
StorageList.propTypes = {
|
||||
storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired
|
||||
storageList: PropTypes.arrayOf(PropTypes.element).isRequired
|
||||
}
|
||||
export default CSSModules(StorageList, styles)
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
top 180px
|
||||
overflow-y auto
|
||||
|
||||
.storageList-folded
|
||||
@extend .storageList
|
||||
width 44px
|
||||
|
||||
.storageList-empty
|
||||
padding 0 10px
|
||||
margin-top 15px
|
||||
|
||||
@@ -9,20 +9,34 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Function} handleClickTagListItem
|
||||
* @param {bool} isActive
|
||||
* @param {Function} handleClickNarrowToTag
|
||||
* @param {boolean} isActive
|
||||
* @param {boolean} isRelated
|
||||
* @param {string} bgColor tab backgroundColor
|
||||
*/
|
||||
|
||||
const TagListItem = ({name, handleClickTagListItem, isActive}) => (
|
||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count, color}) => (
|
||||
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}>
|
||||
{isRelated
|
||||
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
||||
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
|
||||
</button>
|
||||
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
|
||||
}
|
||||
<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'>
|
||||
{`# ${name}`}
|
||||
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
TagListItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
handleClickTagListItem: PropTypes.func.isRequired
|
||||
handleClickTagListItem: PropTypes.func.isRequired,
|
||||
color: PropTypes.string
|
||||
}
|
||||
|
||||
export default CSSModules(TagListItem, styles)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
.tagList-itemContainer
|
||||
display flex
|
||||
|
||||
.tagList-item
|
||||
display flex
|
||||
flex 1
|
||||
width 100%
|
||||
height 26px
|
||||
background-color transparent
|
||||
@@ -20,9 +24,16 @@
|
||||
color $ui-button-default-color
|
||||
background-color $ui-button-default--active-backgroundColor
|
||||
|
||||
.tagList-itemNarrow
|
||||
composes tagList-item
|
||||
flex none
|
||||
width 20px
|
||||
padding 0 4px
|
||||
|
||||
.tagList-item-active
|
||||
background-color $ui-button-default--active-backgroundColor
|
||||
display flex
|
||||
flex 1
|
||||
width 100%
|
||||
height 26px
|
||||
padding 0
|
||||
@@ -36,10 +47,16 @@
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 60%)
|
||||
transition 0.2s
|
||||
|
||||
.tagList-itemNarrow-active
|
||||
composes tagList-item-active
|
||||
flex none
|
||||
width 20px
|
||||
padding 0 4px
|
||||
|
||||
.tagList-item-name
|
||||
display block
|
||||
flex 1
|
||||
padding 0 15px
|
||||
padding 0 8px 0 4px
|
||||
height 26px
|
||||
line-height 26px
|
||||
border-width 0 0 0 2px
|
||||
@@ -48,6 +65,17 @@
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
.tagList-item-count
|
||||
float right
|
||||
line-height 26px
|
||||
padding-right 15px
|
||||
font-size 13px
|
||||
|
||||
.tagList-item-color
|
||||
height 26px
|
||||
width 3px
|
||||
display inline-block
|
||||
|
||||
body[data-theme="white"]
|
||||
.tagList-item
|
||||
color $ui-inactive-text-color
|
||||
@@ -63,6 +91,8 @@ body[data-theme="white"]
|
||||
color $ui-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||
.tagList-item-count
|
||||
color $ui-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.tagList-item
|
||||
@@ -82,3 +112,5 @@ body[data-theme="dark"]
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||
.tagList-item-count
|
||||
color $ui-dark-button--active-color
|
||||
|
||||
@@ -12,7 +12,7 @@ import styles from './TodoListPercentage.styl'
|
||||
*/
|
||||
|
||||
const TodoListPercentage = ({
|
||||
percentageOfTodo
|
||||
percentageOfTodo, onClearCheckboxClick
|
||||
}) => (
|
||||
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
||||
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
||||
@@ -20,11 +20,15 @@ const TodoListPercentage = ({
|
||||
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='todoClear'>
|
||||
<p styleName='todoClearText' onClick={(e) => onClearCheckboxClick(e)}>clear</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
TodoListPercentage.propTypes = {
|
||||
percentageOfTodo: PropTypes.number.isRequired
|
||||
percentageOfTodo: PropTypes.number.isRequired,
|
||||
onClearCheckboxClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(TodoListPercentage, styles)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.percentageBar
|
||||
display: flex
|
||||
position absolute
|
||||
top 50px
|
||||
top 72px
|
||||
right 0px
|
||||
left 0px
|
||||
background-color #DADFE1
|
||||
@@ -30,6 +31,20 @@
|
||||
color #f4f4f4
|
||||
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"]
|
||||
.percentageBar
|
||||
background-color #444444
|
||||
@@ -40,6 +55,9 @@ body[data-theme="dark"]
|
||||
.percentageText
|
||||
color $ui-dark-text-color
|
||||
|
||||
.todoClearText
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.percentageBar
|
||||
background-color #002b36
|
||||
@@ -49,3 +67,29 @@ body[data-theme="solarized-dark"]
|
||||
|
||||
.percentageText
|
||||
color #fdf6e3
|
||||
|
||||
.todoClearText
|
||||
color #fdf6e3
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.percentageBar
|
||||
background-color: $ui-monokai-borderColor
|
||||
|
||||
.progressBar
|
||||
background-color $ui-monokai-active-color
|
||||
|
||||
.percentageText
|
||||
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
|
||||
overflow-x hidden
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
.katex
|
||||
font 400 1.2em 'KaTeX_Main'
|
||||
line-height 1.2em
|
||||
// do not allow display line breaks
|
||||
.katex-display > .katex
|
||||
white-space nowrap
|
||||
text-indent 0
|
||||
// allow inline line breaks
|
||||
.katex
|
||||
white-space initial
|
||||
.katex .katex-html
|
||||
display inline-flex
|
||||
.katex .mfrac>.vlist>span:nth-child(2)
|
||||
top 0 !important
|
||||
.katex-error
|
||||
@@ -68,7 +71,7 @@ body
|
||||
padding 5px
|
||||
margin -5px
|
||||
border-radius 5px
|
||||
.flowchart-error, .sequence-error
|
||||
.flowchart-error, .sequence-error .chart-error
|
||||
background-color errorBackgroundColor
|
||||
color errorTextColor
|
||||
padding 5px
|
||||
@@ -76,10 +79,13 @@ body
|
||||
justify-content left
|
||||
li
|
||||
label.taskListItem
|
||||
margin-left -2em
|
||||
margin-left -1.8em
|
||||
&.checked
|
||||
text-decoration line-through
|
||||
opacity 0.5
|
||||
&.taskListItem.checked
|
||||
text-decoration line-through
|
||||
opacity 0.5
|
||||
div.math-rendered
|
||||
text-align center
|
||||
.math-failed
|
||||
@@ -159,6 +165,7 @@ p
|
||||
white-space pre-line
|
||||
word-wrap break-word
|
||||
img
|
||||
cursor zoom-in
|
||||
max-width 100%
|
||||
strong, b
|
||||
font-weight bold
|
||||
@@ -178,6 +185,12 @@ ul
|
||||
margin-bottom 1em
|
||||
li
|
||||
display list-item
|
||||
&.taskListItem
|
||||
list-style none
|
||||
&>input
|
||||
margin-left -1.6em
|
||||
&>p
|
||||
margin-left -1.8em
|
||||
p
|
||||
margin 0
|
||||
&>li>ul, &>li>ol
|
||||
@@ -197,7 +210,6 @@ ol
|
||||
&>li>ul, &>li>ol
|
||||
margin 0
|
||||
code
|
||||
color #CC305F
|
||||
padding 0.2em 0.4em
|
||||
background-color #f7f7f7
|
||||
border-radius 3px
|
||||
@@ -205,33 +217,39 @@ code
|
||||
text-decoration none
|
||||
margin-right 2px
|
||||
pre
|
||||
padding 0.5em !important
|
||||
padding 0.5rem !important
|
||||
border solid 1px #D1D1D1
|
||||
border-radius 5px
|
||||
overflow-x auto
|
||||
margin 0 0 1em
|
||||
margin 0 0 1rem
|
||||
display flex
|
||||
line-height 1.4em
|
||||
&.flowchart, &.sequence
|
||||
display flex
|
||||
justify-content center
|
||||
background-color white
|
||||
&.CodeMirror
|
||||
height initial
|
||||
&>code
|
||||
flex 1
|
||||
overflow-x auto
|
||||
code
|
||||
background-color inherit
|
||||
margin 0
|
||||
padding 0
|
||||
border none
|
||||
border-radius 0
|
||||
&.CodeMirror
|
||||
height initial
|
||||
flex-wrap wrap
|
||||
&>code
|
||||
flex 1
|
||||
overflow-x auto
|
||||
&.mermaid svg
|
||||
max-width 100% !important
|
||||
&>span.filename
|
||||
margin -0.5rem 100% 0.5rem -0.5rem
|
||||
padding 0.125rem 0.375rem
|
||||
background-color #777;
|
||||
color white
|
||||
&:empty
|
||||
display none
|
||||
&>span.lineNumber
|
||||
display none
|
||||
font-size 1em
|
||||
padding 0.5em 0
|
||||
margin -0.5em 0.5em -0.5em -0.5em
|
||||
padding 0.5rem 0
|
||||
margin -0.5rem 0.5rem -0.5rem -0.5rem
|
||||
border-right 1px solid
|
||||
text-align right
|
||||
border-top-left-radius 4px
|
||||
@@ -248,6 +266,7 @@ table
|
||||
display block
|
||||
width 100%
|
||||
margin 0 0 1em
|
||||
overflow-x auto
|
||||
thead
|
||||
tr
|
||||
background-color tableHeadBgColor
|
||||
@@ -284,6 +303,147 @@ kbd
|
||||
line-height 1
|
||||
padding 3px 5px
|
||||
|
||||
$admonition
|
||||
box-shadow 0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)
|
||||
position relative
|
||||
margin 1.5625em 0
|
||||
padding 0 1.2rem
|
||||
border-left .4rem solid #448aff
|
||||
border-radius .2rem
|
||||
overflow auto
|
||||
|
||||
html .admonition>:last-child
|
||||
margin-bottom 1.2rem
|
||||
|
||||
.admonition .admonition
|
||||
margin 1em 0
|
||||
|
||||
.admonition p
|
||||
margin-top: 0.5em
|
||||
|
||||
$admonition-icon
|
||||
position absolute
|
||||
left 1.2rem
|
||||
font-family: "Material Icons"
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
|
||||
$admonition-title
|
||||
margin 0 -1.2rem
|
||||
padding .8rem 1.2rem .8rem 4rem
|
||||
border-bottom .1rem solid rgba(68,138,255,.1)
|
||||
background-color rgba(68,138,255,.1)
|
||||
font-weight 700
|
||||
|
||||
.admonition>.admonition-title:last-child
|
||||
margin-bottom 0
|
||||
|
||||
admonition_types = {
|
||||
note: {color: #0288D1, icon: "note"},
|
||||
hint: {color: #009688, icon: "info_outline"},
|
||||
danger: {color: #c2185b, icon: "block"},
|
||||
caution: {color: #ffa726, icon: "warning"},
|
||||
error: {color: #d32f2f, icon: "error_outline"},
|
||||
attention: {color: #455a64, icon: "priority_high"}
|
||||
}
|
||||
|
||||
for name, val in admonition_types
|
||||
.admonition.{name}
|
||||
@extend $admonition
|
||||
border-left-color: val[color]
|
||||
|
||||
.admonition.{name}>.admonition-title
|
||||
@extend $admonition-title
|
||||
border-bottom-color: .1rem solid rgba(val[color], 0.2)
|
||||
background-color: rgba(val[color], 0.2)
|
||||
|
||||
.admonition.{name}>.admonition-title:before
|
||||
@extend $admonition-icon
|
||||
color: val[color]
|
||||
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%)
|
||||
themeDarkText = #f9f9f9
|
||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||
@@ -334,6 +494,22 @@ body[data-theme="dark"]
|
||||
kbd
|
||||
background-color themeDarkBorder
|
||||
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
|
||||
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
||||
@@ -361,3 +537,111 @@ body[data-theme="solarized-dark"]
|
||||
border-color themeSolarizedDarkTableBorder
|
||||
&:last-child
|
||||
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
|
||||
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
||||
themeMonokaiTableHead = themeMonokaiTableEven
|
||||
themeMonokaiTableBorder = themeDarkBorder
|
||||
|
||||
body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
table
|
||||
thead
|
||||
tr
|
||||
background-color themeMonokaiTableHead
|
||||
th
|
||||
border-color themeMonokaiTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
tbody
|
||||
tr:nth-child(2n + 1)
|
||||
background-color themeMonokaiTableOdd
|
||||
tr:nth-child(2n)
|
||||
background-color themeMonokaiTableEven
|
||||
td
|
||||
border-color themeMonokaiTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
kbd
|
||||
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
|
||||
|
||||
43
browser/components/render/MermaidRender.js
Normal file
43
browser/components/render/MermaidRender.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import mermaidAPI from 'mermaid'
|
||||
|
||||
// fixes bad styling in the mermaid dark theme
|
||||
const darkThemeStyling = `
|
||||
.loopText tspan {
|
||||
fill: white;
|
||||
}`
|
||||
|
||||
function getRandomInt (min, max) {
|
||||
return Math.floor(Math.random() * (max - min)) + min
|
||||
}
|
||||
|
||||
function getId () {
|
||||
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
let id = 'm-'
|
||||
for (let i = 0; i < 7; i++) {
|
||||
id += pool[getRandomInt(0, 16)]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
function render (element, content, theme) {
|
||||
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) => {
|
||||
element.innerHTML = svgGraph
|
||||
})
|
||||
} catch (e) {
|
||||
element.className = 'mermaid-error'
|
||||
element.innerHTML = 'mermaid diagram parse error: ' + e.message
|
||||
}
|
||||
}
|
||||
|
||||
export default render
|
||||
@@ -1,156 +0,0 @@
|
||||
$search-height = 50px
|
||||
$nav-width = 175px
|
||||
$list-width = 250px
|
||||
|
||||
.root
|
||||
absolute top left right bottom
|
||||
|
||||
.search
|
||||
height $search-height
|
||||
padding 10px
|
||||
box-sizing border-box
|
||||
border-bottom $ui-border
|
||||
text-align center
|
||||
|
||||
.search-input
|
||||
height 30px
|
||||
width 100%
|
||||
margin 0 auto
|
||||
font-size 18px
|
||||
border none
|
||||
outline none
|
||||
text-align center
|
||||
background-color transparent
|
||||
|
||||
.result
|
||||
absolute left right bottom
|
||||
top $search-height
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.result-nav
|
||||
user-select none
|
||||
absolute left top bottom
|
||||
width $nav-width
|
||||
background-color $ui-backgroundColor
|
||||
|
||||
.result-nav-filter
|
||||
margin-bottom 10px
|
||||
|
||||
.result-nav-filter-option
|
||||
height 25px
|
||||
line-height 25px
|
||||
padding 0 10px
|
||||
label
|
||||
cursor pointer
|
||||
|
||||
.result-nav-menu
|
||||
navButtonColor()
|
||||
height 32px
|
||||
padding 0 10px
|
||||
font-size 14px
|
||||
width 100%
|
||||
outline none
|
||||
text-align left
|
||||
line-height 32px
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
|
||||
.result-nav-menu--active
|
||||
@extend .result-nav-menu
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.result-nav-storageList
|
||||
absolute bottom left right
|
||||
top 110px + 32px + 10px + 10px + 20px
|
||||
overflow-y auto
|
||||
|
||||
.result-list
|
||||
user-select none
|
||||
absolute top bottom
|
||||
left $nav-width
|
||||
width $list-width
|
||||
box-sizing border-box
|
||||
overflow-y auto
|
||||
box-shadow 2px 0 15px -8px #b1b1b1
|
||||
z-index 1
|
||||
|
||||
.result-detail
|
||||
absolute top bottom right
|
||||
left $nav-width + $list-width
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-backgroundColor
|
||||
.search
|
||||
border-color $ui-dark-borderColor
|
||||
.search-input
|
||||
color $ui-dark-text-color
|
||||
|
||||
.result
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.result-nav
|
||||
background-color $ui-dark-backgroundColor
|
||||
label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.result-nav-menu
|
||||
navDarkButtonColor()
|
||||
|
||||
.result-nav-menu--active
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.result-list
|
||||
border-color $ui-dark-borderColor
|
||||
box-shadow none
|
||||
top 0
|
||||
|
||||
.result-detail
|
||||
absolute top bottom right
|
||||
left $nav-width + $list-width
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
.search
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
.search-input
|
||||
color $ui-dark-text-color
|
||||
|
||||
.result
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
|
||||
.result-nav
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.result-nav-menu
|
||||
navDarkButtonColor()
|
||||
|
||||
.result-nav-menu--active
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.result-list
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
box-shadow none
|
||||
top 0
|
||||
|
||||
.result-detail
|
||||
absolute top bottom right
|
||||
left $nav-width + $list-width
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NoteDetail.styl'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror-mode-elixir'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
|
||||
const electron = require('electron')
|
||||
const { clipboard } = electron
|
||||
const path = require('path')
|
||||
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
function notify (title, options) {
|
||||
if (global.process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
class NoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
snippetIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note !== this.props.note) {
|
||||
this.setState({
|
||||
snippetIndex: 0
|
||||
}, () => {
|
||||
if (nextProps.note.type === 'SNIPPET_NOTE') {
|
||||
nextProps.note.snippets.forEach((snippet, index) => {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectPriorSnippet () {
|
||||
const { note } = this.props
|
||||
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + note.snippets.length - 1) % note.snippets.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectNextSnippet () {
|
||||
const { note } = this.props
|
||||
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
saveToClipboard () {
|
||||
const { note } = this.props
|
||||
|
||||
if (note.type === 'MARKDOWN_NOTE') {
|
||||
clipboard.writeText(note.content)
|
||||
} else {
|
||||
clipboard.writeText(note.snippets[this.state.snippetIndex].content)
|
||||
}
|
||||
|
||||
notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
}
|
||||
|
||||
handleTabButtonClick (e, index) {
|
||||
this.setState({
|
||||
snippetIndex: index
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { note, config } = this.props
|
||||
if (note == null) {
|
||||
return (
|
||||
<div styleName='root' />
|
||||
)
|
||||
}
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||
|
||||
const storage = findStorage(note.storage)
|
||||
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
const tabList = note.snippets.map((snippet, index) => {
|
||||
const isActive = this.state.snippetIndex === index
|
||||
return <div styleName={isActive
|
||||
? 'tabList-item--active'
|
||||
: 'tabList-item'
|
||||
}
|
||||
key={index}
|
||||
>
|
||||
<button styleName='tabList-item-button'
|
||||
onClick={(e) => this.handleTabButtonClick(e, index)}
|
||||
>
|
||||
{snippet.name.trim().length > 0
|
||||
? snippet.name
|
||||
: <span styleName='tabList-item-unnamed'>
|
||||
Unnamed
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
})
|
||||
|
||||
const viewList = note.snippets.map((snippet, index) => {
|
||||
const isActive = this.state.snippetIndex === index
|
||||
|
||||
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
return <div styleName='tabView'
|
||||
key={index}
|
||||
style={{zIndex: isActive ? 5 : 4}}
|
||||
>
|
||||
{snippet.mode === 'markdown'
|
||||
? <MarkdownEditor styleName='tabView-content'
|
||||
config={config}
|
||||
value={snippet.content}
|
||||
ref={'code-' + index}
|
||||
storageKey={note.storage}
|
||||
/>
|
||||
: <CodeEditor styleName='tabView-content'
|
||||
mode={snippet.mode}
|
||||
value={snippet.content}
|
||||
theme={config.editor.theme}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
keyMap={config.editor.keyMap}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
readOnly
|
||||
ref={'code-' + index}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
})
|
||||
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='description'>
|
||||
<textarea styleName='description-textarea'
|
||||
style={{
|
||||
fontFamily: config.preview.fontFamily,
|
||||
fontSize: parseInt(config.preview.fontSize, 10)
|
||||
}}
|
||||
ref='description'
|
||||
placeholder='Description...'
|
||||
value={note.description}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div styleName='tabList'>
|
||||
{tabList}
|
||||
</div>
|
||||
{viewList}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownPreview styleName='root'
|
||||
theme={config.ui.theme}
|
||||
fontSize={config.preview.fontSize}
|
||||
fontFamily={config.preview.fontFamily}
|
||||
codeBlockTheme={config.preview.codeBlockTheme}
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
indentSize={editorIndentSize}
|
||||
value={note.content}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteDetail.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(NoteDetail, styles)
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
@import('../main/Detail/DetailVars.styl')
|
||||
|
||||
.root
|
||||
absolute top bottom left right
|
||||
bottom 30px
|
||||
margin 0 25px
|
||||
height 100%
|
||||
width 365px
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.description
|
||||
absolute top left right
|
||||
height 80px
|
||||
box-sizing border-box
|
||||
|
||||
.description-textarea
|
||||
display block
|
||||
height 100%
|
||||
width 100%
|
||||
resize none
|
||||
border none
|
||||
padding 10px
|
||||
line-height 1.6
|
||||
box-sizing border-box
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.tabList
|
||||
absolute left right
|
||||
top 80px
|
||||
box-sizing border-box
|
||||
height 30px
|
||||
display flex
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.tabList-item
|
||||
position relative
|
||||
flex 1
|
||||
overflow hidden
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColorg
|
||||
|
||||
.tabList-item--active
|
||||
@extend .tabList-item
|
||||
border-bottom $ui-border
|
||||
|
||||
.tabList-item-button
|
||||
width 100%
|
||||
height 29px
|
||||
overflow ellipsis
|
||||
text-align left
|
||||
padding-right 30px
|
||||
padding-left 10px
|
||||
border none
|
||||
background-color transparent
|
||||
transition 0.15s
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.tabView
|
||||
absolute left right bottom
|
||||
top 130px
|
||||
|
||||
.tabView-content
|
||||
absolute top left right bottom
|
||||
box-sizing border-box
|
||||
height 100%
|
||||
width 100%
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.description
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.description-textarea
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
color white
|
||||
|
||||
.tabList
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.tabList-item
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
|
||||
.tabList-item-button
|
||||
border none
|
||||
color $ui-dark-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color white
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
|
||||
.description
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
|
||||
.description-textarea
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
color white
|
||||
|
||||
.tabList
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
|
||||
.tabList-item
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
|
||||
.tabList-item-button
|
||||
border none
|
||||
color $ui-dark-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color white
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react'
|
||||
import NoteItem from 'browser/components/NoteItem'
|
||||
import moment from 'moment'
|
||||
|
||||
class NoteList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
range: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.props.search !== nextProps.search) {
|
||||
this.resetScroll()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const { index } = this.props
|
||||
|
||||
if (index > -1) {
|
||||
const list = this.refs.root
|
||||
const item = list.childNodes[index]
|
||||
if (item == null) return null
|
||||
|
||||
const overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
||||
if (overflowBelow) {
|
||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
||||
}
|
||||
const overflowAbove = list.scrollTop > item.offsetTop
|
||||
if (overflowAbove) {
|
||||
list.scrollTop = item.offsetTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetScroll () {
|
||||
this.refs.root.scrollTop = 0
|
||||
this.setState({
|
||||
range: 0
|
||||
})
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
const { notes } = this.props
|
||||
|
||||
if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 100 && notes.length > this.state.range * 10 + 10) {
|
||||
this.setState({
|
||||
range: this.state.range + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { notes, index } = this.props
|
||||
|
||||
const notesList = notes
|
||||
.slice(0, 10 + 10 * this.state.range)
|
||||
.map((note, _index) => {
|
||||
const isActive = (index === _index)
|
||||
const key = `${note.storage}-${note.key}`
|
||||
const dateDisplay = moment(note.updatedAt).fromNow()
|
||||
|
||||
return (
|
||||
<NoteItem
|
||||
isActive={isActive}
|
||||
note={note}
|
||||
dateDisplay={dateDisplay}
|
||||
key={key}
|
||||
handleNoteClick={(e) => this.props.handleNoteClick(e, _index)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div className={this.props.className}
|
||||
onScroll={(e) => this.handleScroll(e)}
|
||||
ref='root'
|
||||
>
|
||||
{notesList}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteList.propTypes = {
|
||||
}
|
||||
|
||||
export default NoteList
|
||||
@@ -1,77 +0,0 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StorageSection.styl'
|
||||
import StorageItem from 'browser/components/StorageItem'
|
||||
|
||||
class StorageSection extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isOpen: true
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
this.setState({
|
||||
isOpen: !this.state.isOpen
|
||||
})
|
||||
}
|
||||
|
||||
handleHeaderClick (e) {
|
||||
const { storage } = this.props
|
||||
this.props.handleStorageButtonClick(e, storage.key)
|
||||
}
|
||||
|
||||
handleFolderClick (e, folder) {
|
||||
const { storage } = this.props
|
||||
this.props.handleFolderButtonClick(e, storage.key, folder.key)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { storage, filter } = this.props
|
||||
const folderList = storage.folders
|
||||
.map(folder => (
|
||||
<StorageItem
|
||||
key={folder.key}
|
||||
isActive={filter.type === 'FOLDER' && filter.folder === folder.key && filter.storage === storage.key}
|
||||
handleButtonClick={(e) => this.handleFolderClick(e, folder)}
|
||||
folderName={folder.name}
|
||||
folderColor={folder.color}
|
||||
isFolded={false}
|
||||
/>
|
||||
))
|
||||
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='header'>
|
||||
<button styleName='header-toggleButton'
|
||||
onClick={(e) => this.handleToggleButtonClick(e)}
|
||||
>
|
||||
<i className={this.state.isOpen
|
||||
? 'fa fa-caret-down'
|
||||
: 'fa fa-caret-right'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<button styleName={filter.type === 'STORAGE' && filter.storage === storage.key
|
||||
? 'header-name--active'
|
||||
: 'header-name'
|
||||
}
|
||||
onClick={(e) => this.handleHeaderClick(e)}
|
||||
>{storage.name}</button>
|
||||
</div>
|
||||
{this.state.isOpen &&
|
||||
<div styleName='folderList'>
|
||||
{folderList}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StorageSection.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(StorageSection, styles)
|
||||
@@ -1,85 +0,0 @@
|
||||
.root
|
||||
position relative
|
||||
|
||||
.header
|
||||
height 26px
|
||||
.header-toggleButton
|
||||
absolute top left
|
||||
width 25px
|
||||
height 26px
|
||||
navButtonColor()
|
||||
border none
|
||||
outline none
|
||||
.header-name
|
||||
display block
|
||||
height 26px
|
||||
navButtonColor()
|
||||
padding 0 10px 0 25px
|
||||
font-size 14px
|
||||
width 100%
|
||||
text-align left
|
||||
line-height 26px
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
outline none
|
||||
|
||||
.header-name--active
|
||||
@extend .header-name
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.folderList-item
|
||||
display block
|
||||
width 100%
|
||||
height 26px
|
||||
navButtonColor()
|
||||
padding 0 10px 0 25px
|
||||
font-size 14px
|
||||
width 100%
|
||||
text-align left
|
||||
line-height 26px
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
outline none
|
||||
padding 0 10px
|
||||
margin 2px 0
|
||||
height 26px
|
||||
line-height 26px
|
||||
border-width 0 0 0 6px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
body[data-theme="dark"]
|
||||
.header-toggleButton
|
||||
navDarkButtonColor()
|
||||
.header-name
|
||||
navDarkButtonColor()
|
||||
|
||||
.header-name--active
|
||||
@extend .header-name
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.folderList-item
|
||||
navDarkButtonColor()
|
||||
border-width 0 0 0 6px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
@@ -1,357 +0,0 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { connect, Provider } from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import store from './store'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './FinderMain.styl'
|
||||
import StorageSection from './StorageSection'
|
||||
import NoteList from './NoteList'
|
||||
import NoteDetail from './NoteDetail'
|
||||
import SideNavFilter from 'browser/components/SideNavFilter'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
require('!!style!css!stylus?sourceMap!../main/global.styl')
|
||||
require('../lib/customMeta')
|
||||
require('./ipcClient.js')
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const { Menu } = remote
|
||||
|
||||
function hideFinder () {
|
||||
const finderWindow = remote.getCurrentWindow()
|
||||
if (global.process.platform === 'win32') {
|
||||
finderWindow.blur()
|
||||
finderWindow.hide()
|
||||
}
|
||||
if (global.process.platform === 'darwin') {
|
||||
Menu.sendActionToFirstResponder('hide:')
|
||||
}
|
||||
remote.getCurrentWindow().hide()
|
||||
}
|
||||
|
||||
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
|
||||
|
||||
class FinderMain extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
search: '',
|
||||
index: 0,
|
||||
filter: {
|
||||
includeSnippet: true,
|
||||
includeMarkdown: false,
|
||||
type: 'ALL',
|
||||
storage: null,
|
||||
folder: null
|
||||
}
|
||||
}
|
||||
|
||||
this.focusHandler = (e) => this.handleWindowFocus(e)
|
||||
this.blurHandler = (e) => this.handleWindowBlur(e)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refs.search.focus()
|
||||
window.addEventListener('focus', this.focusHandler)
|
||||
window.addEventListener('blur', this.blurHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('focus', this.focusHandler)
|
||||
window.removeEventListener('blur', this.blurHandler)
|
||||
}
|
||||
|
||||
handleWindowFocus (e) {
|
||||
this.refs.search.focus()
|
||||
}
|
||||
|
||||
handleWindowBlur (e) {
|
||||
this.setState({
|
||||
search: ''
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
this.refs.search.focus()
|
||||
if (e.keyCode === 9) {
|
||||
if (e.shiftKey) {
|
||||
this.refs.detail.selectPriorSnippet()
|
||||
} else {
|
||||
this.refs.detail.selectNextSnippet()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 38) {
|
||||
this.selectPrevious()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) {
|
||||
this.selectNext()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
this.refs.detail.saveToClipboard()
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('COPY_FINDER')
|
||||
hideFinder()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
hideFinder()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 91) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
this.setState({
|
||||
search: e.target.value,
|
||||
index: 0
|
||||
})
|
||||
}
|
||||
|
||||
selectArticle (article) {
|
||||
this.setState({currentArticle: article})
|
||||
}
|
||||
|
||||
selectPrevious () {
|
||||
if (this.state.index > 0) {
|
||||
this.setState({
|
||||
index: this.state.index - 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectNext () {
|
||||
if (this.state.index < this.noteCount - 1) {
|
||||
this.setState({
|
||||
index: this.state.index + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleOnlySnippetCheckboxChange (e) {
|
||||
const { filter } = this.state
|
||||
filter.includeSnippet = e.target.checked
|
||||
this.setState({
|
||||
filter: filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleOnlyMarkdownCheckboxChange (e) {
|
||||
const { filter } = this.state
|
||||
filter.includeMarkdown = e.target.checked
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter: filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleAllNotesButtonClick (e) {
|
||||
const { filter } = this.state
|
||||
filter.type = 'ALL'
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleStarredButtonClick (e) {
|
||||
const { filter } = this.state
|
||||
filter.type = 'STARRED'
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleStorageButtonClick (e, storage) {
|
||||
const { filter } = this.state
|
||||
filter.type = 'STORAGE'
|
||||
filter.storage = storage
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleFolderButtonClick (e, storage, folder) {
|
||||
const { filter } = this.state
|
||||
filter.type = 'FOLDER'
|
||||
filter.storage = storage
|
||||
filter.folder = folder
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteClick (e, index) {
|
||||
this.setState({
|
||||
index
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, config } = this.props
|
||||
const { filter, search } = this.state
|
||||
const storageList = []
|
||||
for (const key in data.storageMap) {
|
||||
const storage = data.storageMap[key]
|
||||
const item = (
|
||||
<StorageSection
|
||||
filter={filter}
|
||||
storage={storage}
|
||||
key={storage.key}
|
||||
handleStorageButtonClick={(e, storage) => this.handleStorageButtonClick(e, storage)}
|
||||
handleFolderButtonClick={(e, storage, folder) => this.handleFolderButtonClick(e, storage, folder)}
|
||||
/>
|
||||
)
|
||||
storageList.push(item)
|
||||
}
|
||||
let notes = []
|
||||
let noteIds
|
||||
|
||||
switch (filter.type) {
|
||||
case 'STORAGE':
|
||||
noteIds = data.storageNoteMap[filter.storage]
|
||||
break
|
||||
case 'FOLDER':
|
||||
noteIds = data.folderNoteMap[filter.storage + '-' + filter.folder]
|
||||
break
|
||||
case 'STARRED':
|
||||
noteIds = data.starredSet
|
||||
}
|
||||
if (noteIds != null) {
|
||||
noteIds.forEach((id) => {
|
||||
notes.push(data.noteMap[id])
|
||||
})
|
||||
} else {
|
||||
for (const key in data.noteMap) {
|
||||
notes.push(data.noteMap[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (!filter.includeSnippet && filter.includeMarkdown) {
|
||||
notes = notes.filter((note) => note.type === 'MARKDOWN_NOTE')
|
||||
} else if (filter.includeSnippet && !filter.includeMarkdown) {
|
||||
notes = notes.filter((note) => note.type === 'SNIPPET_NOTE')
|
||||
}
|
||||
|
||||
if (search.trim().length > 0) {
|
||||
const needle = new RegExp(_.escapeRegExp(search.trim()), 'i')
|
||||
notes = notes.filter((note) => note.title.match(needle))
|
||||
}
|
||||
notes = notes
|
||||
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
||||
|
||||
const activeNote = notes[this.state.index]
|
||||
this.noteCount = notes.length
|
||||
|
||||
return (
|
||||
<div className='Finder'
|
||||
styleName='root'
|
||||
ref='-1'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
<div styleName='search'>
|
||||
<input
|
||||
styleName='search-input'
|
||||
ref='search'
|
||||
value={search}
|
||||
placeholder='Search...'
|
||||
onChange={(e) => this.handleSearchChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='result'>
|
||||
<div styleName='result-nav'>
|
||||
<div styleName='result-nav-filter'>
|
||||
<div styleName='result-nav-filter-option'>
|
||||
<label>
|
||||
<input type='checkbox'
|
||||
checked={filter.includeSnippet}
|
||||
onChange={(e) => this.handleOnlySnippetCheckboxChange(e)}
|
||||
/> Only Snippets</label>
|
||||
</div>
|
||||
<div styleName='result-nav-filter-option'>
|
||||
<label>
|
||||
<input type='checkbox'
|
||||
checked={filter.includeMarkdown}
|
||||
onChange={(e) => this.handleOnlyMarkdownCheckboxChange(e)}
|
||||
/> Only Markdown</label>
|
||||
</div>
|
||||
</div>
|
||||
<SideNavFilter
|
||||
isHomeActive={filter.type === 'ALL'}
|
||||
handleAllNotesButtonClick={(e) => this.handleAllNotesButtonClick(e)}
|
||||
isStarredActive={filter.type === 'STARRED'}
|
||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
||||
/>
|
||||
<div styleName='result-nav-storageList'>
|
||||
{storageList}
|
||||
</div>
|
||||
</div>
|
||||
<NoteList styleName='result-list'
|
||||
storageMap={data.storageMap}
|
||||
notes={notes}
|
||||
ref='list'
|
||||
search={search}
|
||||
index={this.state.index}
|
||||
handleNoteClick={(e, _index) => this.handleNoteClick(e, _index)}
|
||||
/>
|
||||
<div styleName='result-detail'>
|
||||
<NoteDetail
|
||||
note={activeNote}
|
||||
config={config}
|
||||
ref='detail'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderMain.propTypes = {
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
var Finder = connect((x) => x)(CSSModules(FinderMain, styles))
|
||||
|
||||
function refreshData () {
|
||||
// let data = dataStore.getData(true)
|
||||
}
|
||||
|
||||
ReactDOM.render((
|
||||
<Provider store={store}>
|
||||
<Finder />
|
||||
</Provider>
|
||||
), document.getElementById('content'), function () {
|
||||
refreshData()
|
||||
})
|
||||
@@ -1,126 +0,0 @@
|
||||
const nodeIpc = require('node-ipc')
|
||||
const { remote, ipcRenderer } = require('electron')
|
||||
const { app, Menu } = remote
|
||||
const path = require('path')
|
||||
const store = require('./store')
|
||||
const consts = require('browser/lib/consts')
|
||||
|
||||
nodeIpc.config.id = 'finder'
|
||||
nodeIpc.config.retry = 1500
|
||||
nodeIpc.config.silent = true
|
||||
|
||||
function killFinder () {
|
||||
const finderWindow = remote.getCurrentWindow()
|
||||
finderWindow.removeAllListeners()
|
||||
if (global.process.platform === 'darwin') {
|
||||
// Only OSX has another app process.
|
||||
nodeIpc.of.node.emit('quit-from-finder')
|
||||
} else {
|
||||
finderWindow.close()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFinder () {
|
||||
const finderWindow = remote.getCurrentWindow()
|
||||
if (global.process.platform === 'darwin') {
|
||||
if (finderWindow.isVisible()) {
|
||||
finderWindow.hide()
|
||||
Menu.sendActionToFirstResponder('hide:')
|
||||
} else {
|
||||
nodeIpc.of.node.emit('request-data-from-finder')
|
||||
finderWindow.show()
|
||||
}
|
||||
} else {
|
||||
if (finderWindow.isVisible()) {
|
||||
finderWindow.blur()
|
||||
finderWindow.hide()
|
||||
} else {
|
||||
nodeIpc.of.node.emit('request-data-from-finder')
|
||||
finderWindow.show()
|
||||
finderWindow.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodeIpc.connectTo(
|
||||
'node',
|
||||
path.join(app.getPath('userData'), 'boostnote.service'),
|
||||
function () {
|
||||
nodeIpc.of.node.on('error', function (err) {
|
||||
console.log(err)
|
||||
})
|
||||
nodeIpc.of.node.on('connect', function () {
|
||||
console.log('Conncted successfully')
|
||||
})
|
||||
nodeIpc.of.node.on('disconnect', function () {
|
||||
console.log('disconnected')
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('open-finder', function () {
|
||||
toggleFinder()
|
||||
})
|
||||
|
||||
ipcRenderer.on('open-finder-from-tray', function () {
|
||||
toggleFinder()
|
||||
})
|
||||
ipcRenderer.on('open-main-from-tray', function () {
|
||||
nodeIpc.of.node.emit('open-main-from-finder')
|
||||
})
|
||||
|
||||
ipcRenderer.on('quit-from-tray', function () {
|
||||
nodeIpc.of.node.emit('quit-from-finder')
|
||||
killFinder()
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('throttle-data', function (payload) {
|
||||
console.log('Received data from Main renderer')
|
||||
store.default.dispatch({
|
||||
type: 'THROTTLE_DATA',
|
||||
data: payload
|
||||
})
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('config-renew', function (payload) {
|
||||
const { config } = payload
|
||||
if (config.ui.theme === 'dark') {
|
||||
document.body.setAttribute('data-theme', 'dark')
|
||||
} else if (config.ui.theme === 'white') {
|
||||
document.body.setAttribute('data-theme', 'white')
|
||||
} else if (config.ui.theme === 'solarized-dark') {
|
||||
document.body.setAttribute('data-theme', 'solarized-dark')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
if (editorTheme == null) {
|
||||
editorTheme = document.createElement('link')
|
||||
editorTheme.setAttribute('id', 'editorTheme')
|
||||
editorTheme.setAttribute('rel', 'stylesheet')
|
||||
document.head.appendChild(editorTheme)
|
||||
}
|
||||
|
||||
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
|
||||
? config.editor.theme
|
||||
: 'default'
|
||||
|
||||
if (config.editor.theme !== 'default') {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
|
||||
}
|
||||
|
||||
store.default.dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config: config
|
||||
})
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('quit-finder-app', function () {
|
||||
nodeIpc.of.node.emit('quit-finder-app-confirm')
|
||||
killFinder()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const ipc = {}
|
||||
|
||||
module.exports = ipc
|
||||
@@ -1,51 +0,0 @@
|
||||
import { combineReducers, createStore } from 'redux'
|
||||
import { routerReducer } from 'react-router-redux'
|
||||
import { DEFAULT_CONFIG } from 'browser/main/lib/ConfigManager'
|
||||
|
||||
const defaultData = {
|
||||
storageMap: {},
|
||||
noteMap: {},
|
||||
starredSet: [],
|
||||
storageNoteMap: {},
|
||||
folderNoteMap: {},
|
||||
tagNoteMap: {}
|
||||
}
|
||||
|
||||
function data (state = defaultData, action) {
|
||||
switch (action.type) {
|
||||
case 'THROTTLE_DATA':
|
||||
console.log(action)
|
||||
state = action.data
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function config (state = DEFAULT_CONFIG, action) {
|
||||
switch (action.type) {
|
||||
case 'INIT_CONFIG':
|
||||
case 'SET_CONFIG':
|
||||
return Object.assign({}, state, action.config)
|
||||
case 'SET_IS_SIDENAV_FOLDED':
|
||||
state.isSideNavFolded = action.isFolded
|
||||
return Object.assign({}, state)
|
||||
case 'SET_ZOOM':
|
||||
state.zoom = action.zoom
|
||||
return Object.assign({}, state)
|
||||
case 'SET_LIST_WIDTH':
|
||||
state.listWidth = action.listWidth
|
||||
return Object.assign({}, state)
|
||||
case 'SET_UI':
|
||||
return Object.assign({}, state, action.config)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
const reducer = combineReducers({
|
||||
data,
|
||||
config,
|
||||
routing: routerReducer
|
||||
})
|
||||
|
||||
const store = createStore(reducer)
|
||||
|
||||
export default store
|
||||
85
browser/lib/Languages.js
Normal file
85
browser/lib/Languages.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const languages = [
|
||||
{
|
||||
name: 'Albanian',
|
||||
locale: 'sq'
|
||||
},
|
||||
{
|
||||
name: 'Chinese (zh-CN)',
|
||||
locale: 'zh-CN'
|
||||
},
|
||||
{
|
||||
name: 'Chinese (zh-TW)',
|
||||
locale: 'zh-TW'
|
||||
},
|
||||
{
|
||||
name: 'Danish',
|
||||
locale: 'da'
|
||||
},
|
||||
{
|
||||
name: 'English',
|
||||
locale: 'en'
|
||||
},
|
||||
{
|
||||
name: 'French',
|
||||
locale: 'fr'
|
||||
},
|
||||
{
|
||||
name: 'German',
|
||||
locale: 'de'
|
||||
},
|
||||
{
|
||||
name: 'Hungarian',
|
||||
locale: 'hu'
|
||||
},
|
||||
{
|
||||
name: 'Japanese',
|
||||
locale: 'ja'
|
||||
},
|
||||
{
|
||||
name: 'Korean',
|
||||
locale: 'ko'
|
||||
},
|
||||
{
|
||||
name: 'Norwegian',
|
||||
locale: 'no'
|
||||
},
|
||||
{
|
||||
name: 'Polish',
|
||||
locale: 'pl'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (PT-BR)',
|
||||
locale: 'pt-BR'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (PT-PT)',
|
||||
locale: 'pt-PT'
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
locale: 'ru'
|
||||
},
|
||||
{
|
||||
name: 'Spanish',
|
||||
locale: 'es-ES'
|
||||
}, {
|
||||
name: 'Turkish',
|
||||
locale: 'tr'
|
||||
}, {
|
||||
name: 'Thai',
|
||||
locale: 'th'
|
||||
}
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
getLocales () {
|
||||
return languages.reduce(function (localeList, locale) {
|
||||
localeList.push(locale.locale)
|
||||
return localeList
|
||||
}, [])
|
||||
},
|
||||
getLanguages () {
|
||||
return languages
|
||||
}
|
||||
}
|
||||
|
||||
113
browser/lib/TextEditorInterface.js
Normal file
113
browser/lib/TextEditorInterface.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Point } from '@susisu/mte-kernel'
|
||||
|
||||
export default class TextEditorInterface {
|
||||
constructor (editor) {
|
||||
this.editor = editor
|
||||
this.doc = editor.getDoc()
|
||||
this.transaction = false
|
||||
}
|
||||
|
||||
getCursorPosition () {
|
||||
const { line, ch } = this.doc.getCursor()
|
||||
return new Point(line, ch)
|
||||
}
|
||||
|
||||
setCursorPosition (pos) {
|
||||
this.doc.setCursor({
|
||||
line: pos.row,
|
||||
ch: pos.column
|
||||
})
|
||||
}
|
||||
|
||||
setSelectionRange (range) {
|
||||
this.doc.setSelection(
|
||||
{ line: range.start.row, ch: range.start.column },
|
||||
{ line: range.end.row, ch: range.end.column }
|
||||
)
|
||||
}
|
||||
|
||||
getLastRow () {
|
||||
return this.doc.lineCount() - 1
|
||||
}
|
||||
|
||||
acceptsTableEdit () {
|
||||
return true
|
||||
}
|
||||
|
||||
getLine (row) {
|
||||
return this.doc.getLine(row)
|
||||
}
|
||||
|
||||
insertLine (row, line) {
|
||||
const lastRow = this.getLastRow()
|
||||
if (row > lastRow) {
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
'\n' + line,
|
||||
{ line: lastRow, ch: lastLine.length },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
browser/lib/confirmDeleteNote.js
Normal file
23
browser/lib/confirmDeleteNote.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import electron from 'electron'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
const { remote } = electron
|
||||
const { dialog } = remote
|
||||
|
||||
export function confirmDeleteNote (confirmDeletion, permanent) {
|
||||
if (confirmDeletion || permanent) {
|
||||
const alertConfig = {
|
||||
ype: 'warning',
|
||||
message: i18n.__('Confirm note deletion'),
|
||||
detail: i18n.__('This will permanently remove this note.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
}
|
||||
|
||||
const dialogButtonIndex = dialog.showMessageBox(
|
||||
remote.getCurrentWindow(), alertConfig
|
||||
)
|
||||
|
||||
return dialogButtonIndex === 0
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -12,6 +12,10 @@ const themes = fs.readdirSync(themePath)
|
||||
})
|
||||
themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light')
|
||||
|
||||
const snippetFile = process.env.NODE_ENV !== 'test'
|
||||
? path.join(app.getPath('userData'), 'snippets.json')
|
||||
: '' // return nothing as we specified different path to snippets.json in test
|
||||
|
||||
const consts = {
|
||||
FOLDER_COLORS: [
|
||||
'#E10051',
|
||||
@@ -31,7 +35,16 @@ const consts = {
|
||||
'Dodger Blue',
|
||||
'Violet Eggplant'
|
||||
],
|
||||
THEMES: ['default'].concat(themes)
|
||||
THEMES: ['default'].concat(themes),
|
||||
SNIPPET_FILE: snippetFile,
|
||||
DEFAULT_EDITOR_FONT_FAMILY: [
|
||||
'Monaco',
|
||||
'Menlo',
|
||||
'Ubuntu Mono',
|
||||
'Consolas',
|
||||
'source-code-pro',
|
||||
'monospace'
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = consts
|
||||
|
||||
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
|
||||
14
browser/lib/convertModeName.js
Normal file
14
browser/lib/convertModeName.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function convertModeName (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,25 @@
|
||||
export function findNoteTitle (value) {
|
||||
export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') {
|
||||
const splitted = value.split('\n')
|
||||
let title = null
|
||||
let isInsideCodeBlock = false
|
||||
|
||||
if (splitted[0] === '---') {
|
||||
let line = 0
|
||||
while (++line < splitted.length) {
|
||||
if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) {
|
||||
title = splitted[line].substring(frontMatterTitleField.length + 1).trim()
|
||||
|
||||
break
|
||||
}
|
||||
if (splitted[line] === '---') {
|
||||
splitted.splice(0, line + 1)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (title === null) {
|
||||
splitted.some((line, index) => {
|
||||
const trimmedLine = line.trim()
|
||||
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
||||
@@ -14,6 +31,7 @@ export function findNoteTitle (value) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (title === null) {
|
||||
title = ''
|
||||
|
||||
@@ -5,10 +5,10 @@ export function getTodoStatus (content) {
|
||||
|
||||
splitted.forEach((line) => {
|
||||
const trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) {
|
||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
|
||||
numberOfTodo++
|
||||
}
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
|
||||
numberOfCompletedTodo++
|
||||
}
|
||||
})
|
||||
|
||||
17
browser/lib/i18n.js
Normal file
17
browser/lib/i18n.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const path = require('path')
|
||||
const { remote } = require('electron')
|
||||
const { app } = remote
|
||||
const { getLocales } = require('./Languages.js')
|
||||
|
||||
// load package for localization
|
||||
const i18n = new (require('i18n-2'))({
|
||||
// setup some locales - other locales default to the first locale
|
||||
locales: getLocales(),
|
||||
extension: '.json',
|
||||
directory: process.env.NODE_ENV === 'production'
|
||||
? path.join(app.getAppPath(), './locales')
|
||||
: path.resolve('./locales'),
|
||||
devMode: false
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -1,7 +1,11 @@
|
||||
const crypto = require('crypto')
|
||||
const _ = require('lodash')
|
||||
const uuidv4 = require('uuid/v4')
|
||||
|
||||
module.exports = function (length) {
|
||||
if (!_.isFinite(length)) length = 10
|
||||
module.exports = function (uuid) {
|
||||
if (typeof uuid === typeof true && uuid) {
|
||||
return uuidv4()
|
||||
}
|
||||
const length = 10
|
||||
return crypto.randomBytes(length).toString('hex')
|
||||
}
|
||||
|
||||
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' ]
|
||||
})
|
||||
}
|
||||
124
browser/lib/markdown-it-sanitize-html.js
Normal file
124
browser/lib/markdown-it-sanitize-html.js
Normal file
@@ -0,0 +1,124 @@
|
||||
'use strict'
|
||||
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
import { escapeHtmlCharacters } from './utils'
|
||||
import url from 'url'
|
||||
|
||||
module.exports = function sanitizePlugin (md, options) {
|
||||
options = options || {}
|
||||
|
||||
md.core.ruler.after('linkify', 'sanitize_inline', state => {
|
||||
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
|
||||
if (state.tokens[tokenIdx].type === 'html_block') {
|
||||
state.tokens[tokenIdx].content = sanitizeHtml(
|
||||
state.tokens[tokenIdx].content,
|
||||
options
|
||||
)
|
||||
}
|
||||
if (state.tokens[tokenIdx].type === '_fence') {
|
||||
// escapeHtmlCharacters has better performance
|
||||
state.tokens[tokenIdx].content = escapeHtmlCharacters(
|
||||
state.tokens[tokenIdx].content,
|
||||
{ skipSingleQuote: true }
|
||||
)
|
||||
}
|
||||
if (state.tokens[tokenIdx].type === 'inline') {
|
||||
const inlineTokens = state.tokens[tokenIdx].children
|
||||
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
||||
if (inlineTokens[childIdx].type === 'html_inline') {
|
||||
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
|
||||
}
|
||||
@@ -1,46 +1,96 @@
|
||||
import markdownit from 'markdown-it'
|
||||
import sanitize from './markdown-it-sanitize-html'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import math from '@rokt33r/markdown-it-math'
|
||||
import smartArrows from 'markdown-it-smartarrows'
|
||||
import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import katex from 'katex'
|
||||
import { lastFindInArray } from './utils'
|
||||
|
||||
// FIXME We should not depend on global variable.
|
||||
const katex = window.katex
|
||||
const config = ConfigManager.get()
|
||||
|
||||
function createGutter (str) {
|
||||
const lc = (str.match(/\n/g) || []).length
|
||||
function createGutter (str, firstLineNumber) {
|
||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
|
||||
const lines = []
|
||||
for (let i = 1; i <= lc; i++) {
|
||||
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
|
||||
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
||||
}
|
||||
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
||||
}
|
||||
|
||||
var md = markdownit({
|
||||
typographer: true,
|
||||
class Markdown {
|
||||
constructor (options = {}) {
|
||||
const config = ConfigManager.get()
|
||||
const defaultOptions = {
|
||||
typographer: config.preview.smartQuotes,
|
||||
linkify: true,
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: true,
|
||||
highlight: function (str, lang) {
|
||||
if (lang === 'flowchart') {
|
||||
return `<pre class="flowchart">${str}</pre>`
|
||||
breaks: config.preview.breaks,
|
||||
sanitize: 'STRICT'
|
||||
}
|
||||
if (lang === 'sequence') {
|
||||
return `<pre class="sequence">${str}</pre>`
|
||||
}
|
||||
return '<pre class="code">' +
|
||||
createGutter(str) +
|
||||
'<code class="' + lang + '">' +
|
||||
str +
|
||||
'</code></pre>'
|
||||
|
||||
const updatedOptions = Object.assign(defaultOptions, options)
|
||||
this.md = markdownit(updatedOptions)
|
||||
|
||||
if (updatedOptions.sanitize !== 'NONE') {
|
||||
const allowedTags = ['iframe', 'input', 'b',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
|
||||
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote',
|
||||
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details'
|
||||
]
|
||||
const allowedAttributes = [
|
||||
'abbr', 'accept', 'accept-charset',
|
||||
'accesskey', 'action', 'align', 'alt', 'axis',
|
||||
'border', 'cellpadding', 'cellspacing', 'char',
|
||||
'charoff', 'charset', 'checked',
|
||||
'clear', 'cols', 'colspan', 'color',
|
||||
'compact', 'coords', 'datetime', 'dir',
|
||||
'disabled', 'enctype', 'for', 'frame',
|
||||
'headers', 'height', 'hreflang',
|
||||
'hspace', 'ismap', 'label', 'lang',
|
||||
'maxlength', 'media', 'method',
|
||||
'multiple', 'name', 'nohref', 'noshade',
|
||||
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
|
||||
'rows', 'rowspan', 'rules', 'scope',
|
||||
'selected', 'shape', 'size', 'span',
|
||||
'start', 'summary', 'tabindex', 'target',
|
||||
'title', 'type', 'usemap', 'valign', 'value',
|
||||
'vspace', 'width', 'itemprop'
|
||||
]
|
||||
|
||||
if (updatedOptions.sanitize === 'ALLOW_STYLES') {
|
||||
allowedTags.push('style')
|
||||
allowedAttributes.push('style')
|
||||
}
|
||||
|
||||
// Sanitize use rinput before other plugins
|
||||
this.md.use(sanitize, {
|
||||
allowedTags,
|
||||
allowedAttributes: {
|
||||
'*': allowedAttributes,
|
||||
'a': ['href'],
|
||||
'div': ['itemscope', 'itemtype'],
|
||||
'blockquote': ['cite'],
|
||||
'del': ['cite'],
|
||||
'ins': ['cite'],
|
||||
'q': ['cite'],
|
||||
'img': ['src', 'width', 'height'],
|
||||
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
||||
'input': ['type', 'id', 'checked']
|
||||
},
|
||||
allowedIframeHostnames: ['www.youtube.com'],
|
||||
selfClosing: [ 'img', 'br', 'hr', 'input' ],
|
||||
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
|
||||
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
|
||||
allowProtocolRelative: true
|
||||
})
|
||||
md.use(emoji, {
|
||||
}
|
||||
|
||||
this.md.use(emoji, {
|
||||
shortcuts: {}
|
||||
})
|
||||
md.use(math, {
|
||||
this.md.use(math, {
|
||||
inlineOpen: config.preview.latexInlineOpen,
|
||||
inlineClose: config.preview.latexInlineClose,
|
||||
blockOpen: config.preview.latexBlockOpen,
|
||||
@@ -64,32 +114,103 @@ md.use(math, {
|
||||
return output
|
||||
}
|
||||
})
|
||||
md.use(require('markdown-it-imsize'))
|
||||
md.use(require('markdown-it-footnote'))
|
||||
md.use(require('markdown-it-multimd-table'))
|
||||
md.use(require('markdown-it-named-headers'), {
|
||||
slugify: (header) => {
|
||||
return encodeURI(header.trim()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||
.replace(/\s+/g, '-'))
|
||||
.replace(/\-+$/, '')
|
||||
}
|
||||
this.md.use(require('markdown-it-imsize'))
|
||||
this.md.use(require('markdown-it-footnote'))
|
||||
this.md.use(require('markdown-it-multimd-table'))
|
||||
this.md.use(require('@enyaxu/markdown-it-anchor'), {
|
||||
slugify: require('./slugify')
|
||||
})
|
||||
this.md.use(require('markdown-it-kbd'))
|
||||
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>`
|
||||
})
|
||||
md.use(require('markdown-it-kbd'))
|
||||
|
||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||
md.use(require('markdown-it-plantuml'), '', {
|
||||
this.md.use(require('markdown-it-plantuml'), '', {
|
||||
generateSource: function (umlCode) {
|
||||
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
||||
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg'
|
||||
const s = unescape(encodeURIComponent(umlCode))
|
||||
const zippedCode = deflate.encode64(
|
||||
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
|
||||
)
|
||||
return `http://www.plantuml.com/plantuml/svg/${zippedCode}`
|
||||
return `${serverAddress}/${zippedCode}`
|
||||
}
|
||||
})
|
||||
|
||||
// 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
|
||||
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 nextLine = startLine + 1
|
||||
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||
@@ -125,6 +246,17 @@ md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||
if (state.parentType === 'list') {
|
||||
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||
if (match) {
|
||||
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
|
||||
if (liToken) {
|
||||
if (!liToken.attrs) {
|
||||
liToken.attrs = []
|
||||
}
|
||||
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>`
|
||||
}
|
||||
}
|
||||
@@ -139,35 +271,36 @@ md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||
return true
|
||||
})
|
||||
|
||||
if (config.preview.smartArrows) {
|
||||
this.md.use(smartArrows)
|
||||
}
|
||||
|
||||
// Add line number attribute for scrolling
|
||||
const originalRender = md.renderer.render
|
||||
md.renderer.render = function render (tokens, options, env) {
|
||||
const originalRender = this.md.renderer.render
|
||||
this.md.renderer.render = (tokens, options, env) => {
|
||||
tokens.forEach((token) => {
|
||||
switch (token.type) {
|
||||
case 'heading_open':
|
||||
case 'paragraph_open':
|
||||
case 'blockquote_open':
|
||||
case 'dd_open':
|
||||
case 'dt_open':
|
||||
case 'heading_open':
|
||||
case 'list_item_open':
|
||||
case 'paragraph_open':
|
||||
case 'table_open':
|
||||
token.attrPush(['data-line', token.map[0]])
|
||||
}
|
||||
})
|
||||
const result = originalRender.call(md.renderer, tokens, options, env)
|
||||
const result = originalRender.call(this.md.renderer, tokens, options, env)
|
||||
return result
|
||||
}
|
||||
// FIXME We should not depend on global variable.
|
||||
window.md = md
|
||||
|
||||
function normalizeLinkText (linkText) {
|
||||
return md.normalizeLinkText(linkText)
|
||||
window.md = this.md
|
||||
}
|
||||
|
||||
const markdown = {
|
||||
render: function markdown (content) {
|
||||
render (content) {
|
||||
if (!_.isString(content)) content = ''
|
||||
const renderedContent = md.render(content)
|
||||
return renderedContent
|
||||
},
|
||||
normalizeLinkText
|
||||
return this.md.render(content)
|
||||
}
|
||||
}
|
||||
|
||||
export default markdown
|
||||
export default Markdown
|
||||
|
||||
0
browser/lib/markdown2.js
Normal file
0
browser/lib/markdown2.js
Normal file
@@ -22,7 +22,7 @@ export function strip (input) {
|
||||
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
|
||||
.replace(/>/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,}\s*$/g, '')
|
||||
.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')
|
||||
})
|
||||
}
|
||||
9
browser/lib/normalizeEditorFontFamily.js
Normal file
9
browser/lib/normalizeEditorFontFamily.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import consts from 'browser/lib/consts'
|
||||
import isString from 'lodash/isString'
|
||||
|
||||
export default function normalizeEditorFontFamily (fontFamily) {
|
||||
const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
|
||||
return isString(fontFamily) && fontFamily.length > 0
|
||||
? [fontFamily].concat(defaultEditorFontFamily).join(', ')
|
||||
: defaultEditorFontFamily.join(', ')
|
||||
}
|
||||
@@ -4,39 +4,30 @@ export default function searchFromNotes (notes, search) {
|
||||
if (search.trim().length === 0) return []
|
||||
const searchBlocks = search.split(' ').filter(block => { return block !== '' })
|
||||
|
||||
let foundNotes = findByWord(notes, searchBlocks[0])
|
||||
let foundNotes = notes
|
||||
searchBlocks.forEach((block) => {
|
||||
foundNotes = findByWord(foundNotes, block)
|
||||
if (block.match(/^#.+/)) {
|
||||
foundNotes = foundNotes.concat(findByTag(notes, block))
|
||||
}
|
||||
foundNotes = findByWordOrTag(foundNotes, block)
|
||||
})
|
||||
return foundNotes
|
||||
}
|
||||
|
||||
function findByTag (notes, block) {
|
||||
const tag = block.match(/#(.+)/)[1]
|
||||
const regExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||
return notes.filter((note) => {
|
||||
if (!_.isArray(note.tags)) return false
|
||||
return note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})
|
||||
})
|
||||
function findByWordOrTag (notes, block) {
|
||||
let tag = block
|
||||
if (tag.match(/^#.+/)) {
|
||||
tag = tag.match(/#(.+)/)[1]
|
||||
}
|
||||
|
||||
function findByWord (notes, block) {
|
||||
const regExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||
return notes.filter((note) => {
|
||||
if (_.isArray(note.tags) && note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})) {
|
||||
if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) {
|
||||
return true
|
||||
}
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
return note.description.match(regExp)
|
||||
return note.description.match(wordRegExp) || note.snippets.some((snippet) => {
|
||||
return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
|
||||
})
|
||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||
return note.content.match(regExp)
|
||||
return note.content.match(wordRegExp)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
139
browser/lib/utils.js
Normal file
139
browser/lib/utils.js
Normal file
@@ -0,0 +1,139 @@
|
||||
export function lastFindInArray (array, callback) {
|
||||
for (let i = array.length - 1; i >= 0; --i) {
|
||||
if (callback(array[i], i, array)) {
|
||||
return array[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeHtmlCharacters (
|
||||
html,
|
||||
opt = { detectCodeBlock: false, skipSingleQuote: false }
|
||||
) {
|
||||
const matchHtmlRegExp = /["'&<>]/g
|
||||
const matchCodeBlockRegExp = /```/g
|
||||
const escapes = ['"', '&', ''', '<', '>']
|
||||
let match = null
|
||||
const replaceAt = (str, index, replace) =>
|
||||
str.substr(0, index) +
|
||||
replace +
|
||||
str.substr(index + replace.length - (replace.length - 1))
|
||||
|
||||
while ((match = matchHtmlRegExp.exec(html)) !== null) {
|
||||
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) {
|
||||
// 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
|
||||
while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) {
|
||||
previousLineEnd--
|
||||
}
|
||||
// 4 spaces means this character is in a code block
|
||||
if (
|
||||
html[previousLineEnd + 1] === ' ' &&
|
||||
html[previousLineEnd + 2] === ' ' &&
|
||||
html[previousLineEnd + 3] === ' ' &&
|
||||
html[previousLineEnd + 4] === ' '
|
||||
) {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
// otherwise, escape it !!!
|
||||
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 nextIndex = current.index
|
||||
let escapedStr = false
|
||||
// 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) {
|
||||
nextStr += html[nextIndex]
|
||||
nextIndex++
|
||||
if (escapes.indexOf(nextStr) !== -1) {
|
||||
escapedStr = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!escapedStr) {
|
||||
// this & char is not a part of an escaped string
|
||||
html = replaceAt(html, current.index, '&')
|
||||
}
|
||||
} else if (current.char === '"') {
|
||||
html = replaceAt(html, current.index, '"')
|
||||
} else if (current.char === "'" && !opt.skipSingleQuote) {
|
||||
html = replaceAt(html, current.index, ''')
|
||||
} else if (current.char === '<') {
|
||||
html = replaceAt(html, current.index, '<')
|
||||
} else if (current.char === '>') {
|
||||
html = replaceAt(html, current.index, '>')
|
||||
}
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
export function isObjectEqual (a, b) {
|
||||
const aProps = Object.getOwnPropertyNames(a)
|
||||
const bProps = Object.getOwnPropertyNames(b)
|
||||
|
||||
if (aProps.length !== bProps.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (var i = 0; i < aProps.length; i++) {
|
||||
const propName = aProps[i]
|
||||
if (a[propName] !== b[propName]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export default {
|
||||
lastFindInArray,
|
||||
escapeHtmlCharacters,
|
||||
isObjectEqual
|
||||
}
|
||||
@@ -30,3 +30,17 @@ body[data-theme="solarized-dark"]
|
||||
border-left 1px solid $ui-solarized-dark-borderColor
|
||||
.empty-message
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
.empty-message
|
||||
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
|
||||
@@ -3,6 +3,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './FolderSelect.styl'
|
||||
import _ from 'lodash'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
class FolderSelect extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -249,7 +250,7 @@ class FolderSelect extends React.Component {
|
||||
<input styleName='search-input'
|
||||
ref='search'
|
||||
value={this.state.search}
|
||||
placeholder='Folder...'
|
||||
placeholder={i18n.__('Folder...')}
|
||||
onChange={(e) => this.handleSearchInputChange(e)}
|
||||
onBlur={(e) => this.handleSearchInputBlur(e)}
|
||||
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
|
||||
|
||||
@@ -133,3 +133,55 @@ body[data-theme="dark"]
|
||||
color $ui-dark-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
color white
|
||||
background-color $ui-monokai-button--hover-backgroundColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.search-optionList
|
||||
color white
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
.search-optionList-item
|
||||
&:hover
|
||||
background-color lighten($ui-monokai-button--hover-backgroundColor, 15%)
|
||||
|
||||
.search-optionList-item--active
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
color $ui-monokai-button--active-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
color $ui-monokai-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
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
|
||||
|
||||
@@ -2,15 +2,20 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './FullscreenButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
const FullscreenButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-fullScreenButton' title='Fullscreen' onMouseDown={(e) => onClick(e)}>
|
||||
}) => {
|
||||
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
||||
return (
|
||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||
<span styleName='tooltip'>Fullscreen</span>
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
FullscreenButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.tooltip:lang(ja)
|
||||
@extend .tooltip
|
||||
right 35px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const InfoButton = ({
|
||||
onClick
|
||||
@@ -10,7 +11,7 @@ const InfoButton = ({
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img className='infoButton' src='../resources/icon/icon-info.svg' />
|
||||
<span styleName='tooltip'>Info</span>
|
||||
<span styleName='tooltip'>{i18n.__('Info')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoPanel.styl'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
class InfoPanel extends React.Component {
|
||||
copyNoteLink () {
|
||||
@@ -19,7 +20,7 @@ class InfoPanel extends React.Component {
|
||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
||||
<div>
|
||||
<p styleName='modification-date'>{updatedAt}</p>
|
||||
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
|
||||
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@@ -29,11 +30,11 @@ class InfoPanel extends React.Component {
|
||||
: <div styleName='count-wrap'>
|
||||
<div styleName='count-number'>
|
||||
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
|
||||
<p styleName='infoPanel-sub-count'>Words</p>
|
||||
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
|
||||
</div>
|
||||
<div styleName='count-number'>
|
||||
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
|
||||
<p styleName='infoPanel-sub-count'>Letters</p>
|
||||
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -45,17 +46,17 @@ class InfoPanel extends React.Component {
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{storageName}</p>
|
||||
<p styleName='infoPanel-sub'>STORAGE</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{folderName}</p>
|
||||
<p styleName='infoPanel-sub'>FOLDER</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||
<p styleName='infoPanel-sub'>CREATION DATE</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -63,30 +64,30 @@ class InfoPanel extends React.Component {
|
||||
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
||||
<i className='fa fa-clipboard' />
|
||||
</button>
|
||||
<p styleName='infoPanel-sub'>NOTE LINK</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<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' />
|
||||
<p>.md</p>
|
||||
<p>{i18n.__('.md')}</p>
|
||||
</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' />
|
||||
<p>.txt</p>
|
||||
<p>{i18n.__('.txt')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
||||
<i className='fa fa-html5' />
|
||||
<p>.html</p>
|
||||
<p>{i18n.__('.html')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => print(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
|
||||
<i className='fa fa-print' />
|
||||
<p>Print</p>
|
||||
<p>{i18n.__('Print')}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
.control-infoButton-panel
|
||||
z-index 200
|
||||
margin-top 0px
|
||||
top: 50px
|
||||
right 25px
|
||||
position absolute
|
||||
padding 20px 25px 0 25px
|
||||
@@ -32,6 +33,7 @@
|
||||
.control-infoButton-panel-trash
|
||||
z-index 200
|
||||
margin-top 0px
|
||||
top 50px
|
||||
right 0px
|
||||
position absolute
|
||||
padding 20px 25px 0 25px
|
||||
@@ -215,3 +217,83 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-solarized-ark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.control-infoButton-panel
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control-infoButton-panel-trash
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.modification-date
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.modification-date-desc
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-defaul-count
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.infoPanel-sub-count
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-default
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.infoPanel-sub
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-noteLink
|
||||
background-color alpha($ui-monokai-borderColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
|
||||
[id=export-wrap]
|
||||
button
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-monokai-borderColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
p
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
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
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoPanel.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const InfoPanelTrashed = ({
|
||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
|
||||
@@ -9,38 +10,38 @@ const InfoPanelTrashed = ({
|
||||
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
||||
<div>
|
||||
<p styleName='modification-date'>{updatedAt}</p>
|
||||
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
|
||||
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{storageName}</p>
|
||||
<p styleName='infoPanel-sub'>STORAGE</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
|
||||
<p styleName='infoPanel-sub'>FOLDER</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||
<p styleName='infoPanel-sub'>CREATION DATE</p>
|
||||
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
|
||||
</div>
|
||||
|
||||
<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' />
|
||||
<p>.md</p>
|
||||
</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' />
|
||||
<p>.txt</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
||||
<i className='fa fa-html5' />
|
||||
<p>.html</p>
|
||||
</button>
|
||||
|
||||
145
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
145
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
@@ -19,6 +19,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import TrashButton from './TrashButton'
|
||||
import FullscreenButton from './FullscreenButton'
|
||||
import RestoreButton from './RestoreButton'
|
||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||
import InfoButton from './InfoButton'
|
||||
import ToggleModeButton from './ToggleModeButton'
|
||||
@@ -27,6 +28,8 @@ import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
import { formatDate } from 'browser/lib/date-formatter'
|
||||
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||
import striptags from 'striptags'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||
|
||||
class MarkdownNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -36,15 +39,19 @@ class MarkdownNoteDetail extends React.Component {
|
||||
isMovingNote: false,
|
||||
note: Object.assign({
|
||||
title: '',
|
||||
content: ''
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}, props.note),
|
||||
isLockButtonShown: false,
|
||||
isLockButtonShown: props.config.editor.type !== 'SPLIT',
|
||||
isLocked: false,
|
||||
editorType: props.config.editor.type
|
||||
editorType: props.config.editor.type,
|
||||
switchPreview: props.config.editor.switchPreview
|
||||
}
|
||||
|
||||
this.dispatchTimer = null
|
||||
|
||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||
this.generateToc = () => this.handleGenerateToc()
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -53,13 +60,24 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
||||
ee.on('topbar:togglemodebutton', () => {
|
||||
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
||||
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) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.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()
|
||||
this.setState({
|
||||
note: Object.assign({}, nextProps.note)
|
||||
note: Object.assign({linesHighlighted: []}, nextProps.note)
|
||||
}, () => {
|
||||
this.refs.content.reload()
|
||||
if (this.refs.tags) this.refs.tags.reset()
|
||||
@@ -68,11 +86,9 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
componentDidUnmount () {
|
||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||
ee.off('code:generate-toc', this.generateToc)
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
handleUpdateTag () {
|
||||
@@ -84,7 +100,12 @@ class MarkdownNoteDetail extends React.Component {
|
||||
handleUpdateContent () {
|
||||
const { note } = this.state
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -141,7 +162,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
key: newNote.key
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
@@ -180,13 +201,43 @@ class MarkdownNoteDetail extends React.Component {
|
||||
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) {
|
||||
const { note } = this.state
|
||||
const { isTrashed } = note
|
||||
const { confirmDeletion } = this.props
|
||||
const { confirmDeletion } = this.props.config.ui
|
||||
|
||||
if (isTrashed) {
|
||||
if (confirmDeletion(true)) {
|
||||
if (confirmDeleteNote(confirmDeletion, true)) {
|
||||
const {note, dispatch} = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
@@ -198,11 +249,12 @@ class MarkdownNoteDetail extends React.Component {
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.once('list:next', dispatchHandler)
|
||||
})
|
||||
.then(() => ee.emit('list:next'))
|
||||
}
|
||||
} else {
|
||||
if (confirmDeletion()) {
|
||||
if (confirmDeleteNote(confirmDeletion, false)) {
|
||||
note.isTrashed = true
|
||||
|
||||
this.setState({
|
||||
@@ -251,13 +303,18 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
handleToggleLockButton (event, noteStatus) {
|
||||
// first argument event is not used
|
||||
if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
|
||||
if (noteStatus === 'CODE') {
|
||||
this.setState({isLockButtonShown: true})
|
||||
} else {
|
||||
this.setState({isLockButtonShown: false})
|
||||
}
|
||||
}
|
||||
|
||||
handleGenerateToc () {
|
||||
const editor = this.refs.content.refs.code.editor
|
||||
markdownToc.generateInEditor(editor)
|
||||
}
|
||||
|
||||
handleFocus (e) {
|
||||
this.focus()
|
||||
}
|
||||
@@ -272,16 +329,42 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
handleSwitchMode (type) {
|
||||
this.setState({ editorType: type }, () => {
|
||||
// If in split mode, hide the lock button
|
||||
this.setState({ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, () => {
|
||||
this.focus()
|
||||
const newConfig = Object.assign({}, this.props.config)
|
||||
newConfig.editor.type = type
|
||||
ConfigManager.set(newConfig)
|
||||
})
|
||||
}
|
||||
|
||||
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 () {
|
||||
const { config, ignorePreviewPointerEvents } = this.props
|
||||
const { note } = this.state
|
||||
|
||||
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
||||
return <MarkdownEditor
|
||||
ref='content'
|
||||
@@ -289,7 +372,10 @@ class MarkdownNoteDetail extends React.Component {
|
||||
config={config}
|
||||
value={note.content}
|
||||
storageKey={note.storage}
|
||||
noteKey={note.key}
|
||||
linesHighlighted={note.linesHighlighted}
|
||||
onChange={this.handleUpdateContent.bind(this)}
|
||||
isLocked={this.state.isLocked}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
/>
|
||||
} else {
|
||||
@@ -298,6 +384,8 @@ class MarkdownNoteDetail extends React.Component {
|
||||
config={config}
|
||||
value={note.content}
|
||||
storageKey={note.storage}
|
||||
noteKey={note.key}
|
||||
linesHighlighted={note.linesHighlighted}
|
||||
onChange={this.handleUpdateContent.bind(this)}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
/>
|
||||
@@ -305,7 +393,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, location } = this.props
|
||||
const { data, location, config } = this.props
|
||||
const { note, editorType } = this.state
|
||||
const storageKey = note.storage
|
||||
const folderKey = note.folder
|
||||
@@ -323,10 +411,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
@@ -359,14 +444,16 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
data={data}
|
||||
onChange={this.handleUpdateTag.bind(this)}
|
||||
coloredTags={config.coloredTags}
|
||||
/>
|
||||
|
||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||
|
||||
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||
<StarButton
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
@@ -399,7 +486,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](${location.query.key})`}
|
||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.exportAsMd}
|
||||
@@ -417,6 +504,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
@@ -443,8 +531,7 @@ MarkdownNoteDetail.propTypes = {
|
||||
style: PropTypes.shape({
|
||||
left: PropTypes.number
|
||||
}),
|
||||
ignorePreviewPointerEvents: PropTypes.bool,
|
||||
confirmDeletion: PropTypes.bool.isRequired
|
||||
ignorePreviewPointerEvents: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CSSModules(MarkdownNoteDetail, styles)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
box-shadow none
|
||||
padding 20px 40px
|
||||
overflow hidden
|
||||
|
||||
.lock-button
|
||||
padding-bottom 3px
|
||||
@@ -70,3 +71,13 @@ body[data-theme="solarized-dark"]
|
||||
.root
|
||||
border-left 1px solid $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
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
|
||||
align-items center
|
||||
padding 0 20px
|
||||
z-index 99
|
||||
|
||||
.info-left
|
||||
padding 0 10px
|
||||
@@ -98,3 +99,12 @@ body[data-theme="solarized-dark"]
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.info
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.info
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TrashButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const PermanentDeleteButton = ({
|
||||
onClick
|
||||
@@ -10,7 +11,7 @@ const PermanentDeleteButton = ({
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||
<span styleName='tooltip'>Permanent Delete</span>
|
||||
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
22
browser/main/Detail/RestoreButton.js
Normal file
22
browser/main/Detail/RestoreButton.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './RestoreButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const RestoreButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-restoreButton'
|
||||
onClick={onClick}
|
||||
>
|
||||
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
|
||||
<span styleName='tooltip'>{i18n.__('Restore')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
RestoreButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(RestoreButton, styles)
|
||||
22
browser/main/Detail/RestoreButton.styl
Normal file
22
browser/main/Detail/RestoreButton.styl
Normal file
@@ -0,0 +1,22 @@
|
||||
.control-restoreButton
|
||||
top 115px
|
||||
topBarButtonRight()
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
pointer-events none
|
||||
top 50px
|
||||
left 25px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-restoreButton
|
||||
topBarButtonDark()
|
||||
@@ -18,32 +18,23 @@ import context from 'browser/lib/context'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import _ from 'lodash'
|
||||
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
||||
import convertModeName from 'browser/lib/convertModeName'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import FullscreenButton from './FullscreenButton'
|
||||
import TrashButton from './TrashButton'
|
||||
import RestoreButton from './RestoreButton'
|
||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||
import InfoButton from './InfoButton'
|
||||
import InfoPanel from './InfoPanel'
|
||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
import { formatDate } from 'browser/lib/date-formatter'
|
||||
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
const { dialog } = remote
|
||||
|
||||
class SnippetNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -52,22 +43,43 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.state = {
|
||||
isMovingNote: false,
|
||||
snippetIndex: 0,
|
||||
showArrows: false,
|
||||
enableLeftArrow: false,
|
||||
enableRightArrow: false,
|
||||
note: Object.assign({
|
||||
description: ''
|
||||
}, 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.generateToc = () => this.handleGenerateToc()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const visibleTabs = this.visibleTabs
|
||||
const allTabs = this.allTabs
|
||||
|
||||
if (visibleTabs.offsetWidth < allTabs.scrollWidth) {
|
||||
this.setState({
|
||||
showArrows: visibleTabs.offsetWidth < allTabs.scrollWidth,
|
||||
enableRightArrow: allTabs.offsetLeft !== visibleTabs.offsetWidth - allTabs.scrollWidth,
|
||||
enableLeftArrow: allTabs.offsetLeft !== 0
|
||||
})
|
||||
}
|
||||
ee.on('code:generate-toc', this.generateToc)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
const nextNote = Object.assign({
|
||||
description: ''
|
||||
}, nextProps.note, {
|
||||
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||
snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
|
||||
})
|
||||
|
||||
this.setState({
|
||||
snippetIndex: 0,
|
||||
note: nextNote
|
||||
@@ -77,12 +89,23 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
if (this.refs.tags) this.refs.tags.reset()
|
||||
this.setState(this.getArrowsState())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
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) {
|
||||
@@ -91,7 +114,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
note.description = this.refs.description.value
|
||||
note.updatedAt = new Date()
|
||||
note.title = findNoteTitle(note.description)
|
||||
note.title = findNoteTitle(note.description, false)
|
||||
|
||||
this.setState({
|
||||
note
|
||||
@@ -146,7 +169,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
key: newNote.key
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
@@ -176,10 +199,10 @@ class SnippetNoteDetail extends React.Component {
|
||||
handleTrashButtonClick (e) {
|
||||
const { note } = this.state
|
||||
const { isTrashed } = note
|
||||
const { confirmDeletion } = this.props
|
||||
const { confirmDeletion } = this.props.config.ui
|
||||
|
||||
if (isTrashed) {
|
||||
if (confirmDeletion(true)) {
|
||||
if (confirmDeleteNote(confirmDeletion, true)) {
|
||||
const {note, dispatch} = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
@@ -191,11 +214,12 @@ class SnippetNoteDetail extends React.Component {
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.once('list:next', dispatchHandler)
|
||||
})
|
||||
.then(() => ee.emit('list:next'))
|
||||
}
|
||||
} else {
|
||||
if (confirmDeletion()) {
|
||||
if (confirmDeleteNote(confirmDeletion, false)) {
|
||||
note.isTrashed = true
|
||||
|
||||
this.setState({
|
||||
@@ -226,6 +250,51 @@ class SnippetNoteDetail extends React.Component {
|
||||
ee.emit('editor:fullscreen')
|
||||
}
|
||||
|
||||
handleTabMoveLeftButtonClick (e) {
|
||||
{
|
||||
const left = this.visibleTabs.scrollLeft
|
||||
|
||||
const tabs = this.allTabs.querySelectorAll('div')
|
||||
const lastVisibleTab = Array.from(tabs).find((tab) => {
|
||||
return tab.offsetLeft + tab.offsetWidth >= left
|
||||
})
|
||||
|
||||
if (lastVisibleTab) {
|
||||
const visiblePart = lastVisibleTab.offsetWidth + lastVisibleTab.offsetLeft - left
|
||||
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
|
||||
const scrollToTab = (isFullyVisible && lastVisibleTab.previousSibling)
|
||||
? lastVisibleTab.previousSibling
|
||||
: lastVisibleTab
|
||||
|
||||
// FIXME use `scrollIntoView()` instead of custom method after update to Electron2.0 (with Chrome 61 its possible animate the scroll)
|
||||
this.moveToTab(scrollToTab)
|
||||
// scrollToTab.scrollIntoView({behavior: 'smooth', inline: 'start', block: 'start'})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleTabMoveRightButtonClick (e) {
|
||||
const left = this.visibleTabs.scrollLeft
|
||||
const width = this.visibleTabs.offsetWidth
|
||||
|
||||
const tabs = this.allTabs.querySelectorAll('div')
|
||||
const lastVisibleTab = Array.from(tabs).find((tab) => {
|
||||
return tab.offsetLeft + tab.offsetWidth >= width + left
|
||||
})
|
||||
|
||||
if (lastVisibleTab) {
|
||||
const visiblePart = width + left - lastVisibleTab.offsetLeft
|
||||
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
|
||||
const scrollToTab = (isFullyVisible && lastVisibleTab.nextSibling)
|
||||
? lastVisibleTab.nextSibling
|
||||
: lastVisibleTab
|
||||
|
||||
// FIXME use `scrollIntoView()` instead of custom method after update to Electron2.0 (with Chrome 61 its possible animate the scroll)
|
||||
this.moveToTab(scrollToTab)
|
||||
// scrollToTab.scrollIntoView({behavior: 'smooth', inline: 'end', block: 'end'})
|
||||
}
|
||||
}
|
||||
|
||||
handleTabPlusButtonClick (e) {
|
||||
this.addSnippet()
|
||||
}
|
||||
@@ -262,9 +331,9 @@ class SnippetNoteDetail extends React.Component {
|
||||
if (this.state.note.snippets[index].content.trim().length > 0) {
|
||||
const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a snippet',
|
||||
detail: 'This work cannot be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
message: i18n.__('Delete a snippet'),
|
||||
detail: i18n.__('This work cannot be undone.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
})
|
||||
if (dialogIndex === 0) {
|
||||
this.deleteSnippetByIndex(index)
|
||||
@@ -285,6 +354,19 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.setState({ note, snippetIndex }, () => {
|
||||
this.save()
|
||||
this.refs['code-' + this.state.snippetIndex].reload()
|
||||
|
||||
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
|
||||
this.moveTabBarBy(0)
|
||||
} else {
|
||||
const lastTab = this.allTabs.lastChild
|
||||
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
|
||||
const width = this.visibleTabs.offsetWidth
|
||||
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
|
||||
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
|
||||
} else {
|
||||
this.setState(this.getArrowsState())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -299,11 +381,11 @@ class SnippetNoteDetail extends React.Component {
|
||||
name: mode
|
||||
})
|
||||
}
|
||||
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
|
||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
this.setState(state => ({
|
||||
note: state.note
|
||||
}), () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
@@ -312,11 +394,11 @@ class SnippetNoteDetail extends React.Component {
|
||||
return (e) => {
|
||||
const snippets = this.state.note.snippets.slice()
|
||||
snippets[index].mode = name
|
||||
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
|
||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
this.setState(state => ({
|
||||
note: state.note
|
||||
}), () => {
|
||||
this.save()
|
||||
})
|
||||
|
||||
@@ -330,10 +412,12 @@ class SnippetNoteDetail extends React.Component {
|
||||
return (e) => {
|
||||
const snippets = this.state.note.snippets.slice()
|
||||
snippets[index].content = this.refs['code-' + index].value
|
||||
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
snippets[index].linesHighlighted = e.options.linesHighlighted
|
||||
|
||||
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||
this.setState(state => ({
|
||||
note: state.note
|
||||
}), () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
@@ -341,6 +425,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
handleKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
// tab key
|
||||
case 9:
|
||||
if (e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
@@ -353,6 +438,19 @@ class SnippetNoteDetail extends React.Component {
|
||||
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
|
||||
// L key
|
||||
case 76:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
@@ -364,12 +462,13 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
}
|
||||
break
|
||||
// T key
|
||||
case 84:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
if (isSuper && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault()
|
||||
this.addSnippet()
|
||||
}
|
||||
@@ -379,14 +478,14 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
handleModeButtonClick (e, index) {
|
||||
const menu = new Menu()
|
||||
const templetes = []
|
||||
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
|
||||
menu.append(new MenuItem({
|
||||
templetes.push({
|
||||
label: mode.name,
|
||||
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
||||
}))
|
||||
})
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
})
|
||||
context.popup(templetes)
|
||||
}
|
||||
|
||||
handleIndentTypeButtonClick (e) {
|
||||
@@ -455,42 +554,96 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.refs.description.focus()
|
||||
}
|
||||
|
||||
moveToTab (tab) {
|
||||
const easeOutCubic = t => (--t) * t * t + 1
|
||||
const startScrollPosition = this.visibleTabs.scrollLeft
|
||||
const animationTiming = 300
|
||||
const scrollMoreCoeff = 1.4 // introduce coefficient, because we want to scroll a bit further to see next tab
|
||||
|
||||
let scrollBy = (tab.offsetLeft - startScrollPosition)
|
||||
|
||||
if (tab.offsetLeft > startScrollPosition) {
|
||||
// if tab is on the right side and we want to show the whole tab in visible area,
|
||||
// we need to include width of the tab and visible area in the formula
|
||||
// ___________________________________________
|
||||
// |____|_______|________|________|_show_this_|
|
||||
// ↑_____________________↑
|
||||
// visible area
|
||||
scrollBy += (tab.offsetWidth - this.visibleTabs.offsetWidth)
|
||||
}
|
||||
|
||||
let startTime = null
|
||||
const scrollAnimation = time => {
|
||||
startTime = startTime || time
|
||||
const elapsed = (time - startTime) / animationTiming
|
||||
|
||||
this.visibleTabs.scrollLeft = startScrollPosition + easeOutCubic(elapsed) * scrollBy * scrollMoreCoeff
|
||||
if (elapsed < 1) {
|
||||
window.requestAnimationFrame(scrollAnimation)
|
||||
} else {
|
||||
this.setState(this.getArrowsState())
|
||||
}
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(scrollAnimation)
|
||||
}
|
||||
|
||||
getArrowsState () {
|
||||
const allTabs = this.allTabs
|
||||
const visibleTabs = this.visibleTabs
|
||||
|
||||
const showArrows = visibleTabs.offsetWidth < allTabs.scrollWidth
|
||||
const enableRightArrow = visibleTabs.scrollLeft !== allTabs.scrollWidth - visibleTabs.offsetWidth
|
||||
const enableLeftArrow = visibleTabs.scrollLeft !== 0
|
||||
|
||||
return {showArrows, enableRightArrow, enableLeftArrow}
|
||||
}
|
||||
|
||||
addSnippet () {
|
||||
const { config: { editor: { snippetDefaultLanguage } } } = this.props
|
||||
const { note } = this.state
|
||||
|
||||
const defaultLanguage = snippetDefaultLanguage === 'Auto Detect' ? null : snippetDefaultLanguage
|
||||
|
||||
note.snippets = note.snippets.concat([{
|
||||
name: '',
|
||||
mode: 'Plain Text',
|
||||
content: ''
|
||||
mode: defaultLanguage,
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}])
|
||||
const snippetIndex = note.snippets.length - 1
|
||||
|
||||
this.setState({
|
||||
this.setState(Object.assign({
|
||||
note,
|
||||
snippetIndex
|
||||
}, () => {
|
||||
}, this.getArrowsState()), () => {
|
||||
if (this.state.showArrows) {
|
||||
const tabs = this.allTabs.querySelectorAll('div')
|
||||
if (tabs) {
|
||||
this.moveToTab(tabs[snippetIndex])
|
||||
}
|
||||
}
|
||||
this.refs['tab-' + snippetIndex].startRenaming()
|
||||
})
|
||||
}
|
||||
|
||||
jumpNextTab () {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + 1) % this.state.note.snippets.length
|
||||
}, () => {
|
||||
this.setState(state => ({
|
||||
snippetIndex: (state.snippetIndex + 1) % state.note.snippets.length
|
||||
}), () => {
|
||||
this.focusEditor()
|
||||
})
|
||||
}
|
||||
|
||||
jumpPrevTab () {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex - 1 + this.state.note.snippets.length) % this.state.note.snippets.length
|
||||
}, () => {
|
||||
this.setState(state => ({
|
||||
snippetIndex: (state.snippetIndex - 1 + state.note.snippets.length) % state.note.snippets.length
|
||||
}), () => {
|
||||
this.focusEditor()
|
||||
})
|
||||
}
|
||||
|
||||
focusEditor () {
|
||||
console.log('code-' + this.state.snippetIndex)
|
||||
this.refs['code-' + this.state.snippetIndex].focus()
|
||||
}
|
||||
|
||||
@@ -499,12 +652,19 @@ class SnippetNoteDetail extends React.Component {
|
||||
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(), {
|
||||
type: 'warning',
|
||||
message: 'Sorry!',
|
||||
detail: 'md/text import is available only a markdown note.',
|
||||
buttons: ['OK']
|
||||
message: i18n.__('Sorry!'),
|
||||
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
||||
buttons: [i18n.__('OK')]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -515,6 +675,8 @@ class SnippetNoteDetail extends React.Component {
|
||||
const storageKey = note.storage
|
||||
const folderKey = note.folder
|
||||
|
||||
const autoDetect = config.editor.snippetDefaultLanguage === 'Auto Detect'
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
@@ -539,10 +701,6 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
const viewList = note.snippets.map((snippet, index) => {
|
||||
const isActive = this.state.snippetIndex === index
|
||||
|
||||
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
return <div styleName='tabView'
|
||||
key={index}
|
||||
style={{zIndex: isActive ? 5 : 4}}
|
||||
@@ -551,24 +709,34 @@ class SnippetNoteDetail extends React.Component {
|
||||
? <MarkdownEditor styleName='tabView-content'
|
||||
value={snippet.content}
|
||||
config={config}
|
||||
linesHighlighted={snippet.linesHighlighted}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||
storageKey={storageKey}
|
||||
/>
|
||||
: <CodeEditor styleName='tabView-content'
|
||||
mode={snippet.mode}
|
||||
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
|
||||
value={snippet.content}
|
||||
linesHighlighted={snippet.linesHighlighted}
|
||||
theme={config.editor.theme}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
keyMap={config.editor.keyMap}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
autoDetect={autoDetect}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -587,10 +755,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
@@ -623,7 +788,11 @@ class SnippetNoteDetail extends React.Component {
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
data={data}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
coloredTags={config.coloredTags}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
@@ -632,11 +801,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
|
||||
<button styleName='control-fullScreenButton' title='Fullscreen'
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||
<span styleName='tooltip'>Fullscreen</span>
|
||||
</button>
|
||||
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
|
||||
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
|
||||
@@ -647,12 +812,14 @@ class SnippetNoteDetail extends React.Component {
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](${location.query.key})`}
|
||||
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
exportAsHtml={this.showWarning}
|
||||
type={note.type}
|
||||
print={this.showWarning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -673,16 +840,32 @@ class SnippetNoteDetail extends React.Component {
|
||||
fontSize: parseInt(config.preview.fontSize, 10)
|
||||
}}
|
||||
ref='description'
|
||||
placeholder='Description...'
|
||||
placeholder={i18n.__('Description...')}
|
||||
value={this.state.note.description}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='tabList'>
|
||||
<div styleName='list'>
|
||||
<button styleName='tabButton'
|
||||
hidden={!this.state.showArrows}
|
||||
disabled={!this.state.enableLeftArrow}
|
||||
onClick={(e) => this.handleTabMoveLeftButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-chevron-left' />
|
||||
</button>
|
||||
<div styleName='list' onScroll={(e) => { this.setState(this.getArrowsState()) }} ref={(tabs) => { this.visibleTabs = tabs }}>
|
||||
<div styleName='allTabs' ref={(tabs) => { this.allTabs = tabs }}>
|
||||
{tabList}
|
||||
</div>
|
||||
<button styleName='plusButton'
|
||||
</div>
|
||||
<button styleName='tabButton'
|
||||
hidden={!this.state.showArrows}
|
||||
disabled={!this.state.enableRightArrow}
|
||||
onClick={(e) => this.handleTabMoveRightButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-chevron-right' />
|
||||
</button>
|
||||
<button styleName='tabButton'
|
||||
onClick={(e) => this.handleTabPlusButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-plus' />
|
||||
@@ -696,7 +879,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
|
||||
>
|
||||
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
||||
? 'Select Syntax...'
|
||||
? i18n.__('Select Syntax...')
|
||||
: this.state.note.snippets[this.state.snippetIndex].mode
|
||||
}
|
||||
<i className='fa fa-caret-down' />
|
||||
@@ -733,8 +916,7 @@ SnippetNoteDetail.propTypes = {
|
||||
style: PropTypes.shape({
|
||||
left: PropTypes.number
|
||||
}),
|
||||
ignorePreviewPointerEvents: PropTypes.bool,
|
||||
confirmDeletion: PropTypes.bool.isRequired
|
||||
ignorePreviewPointerEvents: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetNoteDetail, styles)
|
||||
|
||||
@@ -31,19 +31,35 @@
|
||||
|
||||
.tabList
|
||||
absolute left right
|
||||
top 55px
|
||||
top 70px
|
||||
height 30px
|
||||
display flex
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
overflow hidden
|
||||
|
||||
.tabList .list
|
||||
flex 1
|
||||
display flex
|
||||
overflow hidden
|
||||
overflow-x scroll
|
||||
position relative
|
||||
|
||||
.tabList .plusButton
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.allTabs
|
||||
display flex
|
||||
position relative
|
||||
overflow visible
|
||||
left 0
|
||||
transition left 0.1s
|
||||
|
||||
.tabList .tabButton
|
||||
navWhiteButtonColor()
|
||||
width 30px
|
||||
border-left 1px solid $ui-borderColor
|
||||
border-top 1px solid $ui-borderColor
|
||||
border-right 1px solid $ui-borderColor
|
||||
|
||||
.tabView
|
||||
absolute left right bottom
|
||||
@@ -85,17 +101,34 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="white"]
|
||||
body[data-theme="white"], body[data-theme="default"]
|
||||
.root
|
||||
box-shadow $note-detail-box-shadow
|
||||
border none
|
||||
|
||||
.tabButton
|
||||
&:hover
|
||||
background-color alpha($ui-button--active-backgroundColor, 20%)
|
||||
color $ui-text-color
|
||||
transition 0.15s
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
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
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
@@ -105,7 +138,6 @@ body[data-theme="dark"]
|
||||
border 1px solid $ui-dark-borderColor
|
||||
|
||||
.tabList
|
||||
background-color $ui-button--active-backgroundColor
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.tabList .list
|
||||
@@ -137,6 +169,65 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-text-color
|
||||
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
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
.body
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
.body .description textarea
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
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
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
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
|
||||
@@ -3,6 +3,7 @@ import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StarButton.styl'
|
||||
import _ from 'lodash'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
class StarButton extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -53,7 +54,7 @@ class StarButton extends React.Component {
|
||||
: '../resources/icon/icon-star.svg'
|
||||
}
|
||||
/>
|
||||
<span styleName='tooltip'>Star</span>
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Star')}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.tooltip:lang(ja)
|
||||
@extend .tooltip
|
||||
right 103px
|
||||
width 70px
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
transition 0.15s
|
||||
|
||||
@@ -1,71 +1,39 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import invertColor from 'invert-color'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TagSelect.styl'
|
||||
import _ from 'lodash'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import Autosuggest from 'react-autosuggest'
|
||||
|
||||
class TagSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
newTag: ''
|
||||
}
|
||||
newTag: '',
|
||||
suggestions: []
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.props.value
|
||||
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)
|
||||
}
|
||||
|
||||
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 () {
|
||||
let { value } = this.props
|
||||
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
value.pop()
|
||||
value = _.uniq(value)
|
||||
|
||||
this.value = value
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.setState({
|
||||
newTag: ''
|
||||
})
|
||||
}
|
||||
|
||||
submitTag () {
|
||||
addNewTag (newTag) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
||||
let { value } = this.props
|
||||
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||
newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
|
||||
|
||||
newTag = newTag.trim().replace(/ +/g, '_')
|
||||
if (newTag.charAt(0) === '#') {
|
||||
newTag.substring(1)
|
||||
}
|
||||
|
||||
if (newTag.length <= 0) {
|
||||
this.setState({
|
||||
@@ -74,11 +42,18 @@ class TagSelect extends React.Component {
|
||||
return
|
||||
}
|
||||
|
||||
let { value } = this.props
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
|
||||
if (!_.includes(value, newTag)) {
|
||||
value.push(newTag)
|
||||
value = _.uniq(value)
|
||||
}
|
||||
|
||||
if (this.props.saveTagsAlphabetically) {
|
||||
value = _.sortBy(value)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
newTag: ''
|
||||
@@ -88,44 +63,165 @@ class TagSelect extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleNewTagInputChange (e) {
|
||||
this.setState({
|
||||
newTag: this.refs.newTag.value
|
||||
buildSuggestions () {
|
||||
this.suggestions = _.sortBy(this.props.data.tagNoteMap.map(
|
||||
(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) {
|
||||
return (e) => {
|
||||
this.removeTagByCallback((value, tag) => {
|
||||
value.splice(value.indexOf(tag), 1)
|
||||
}, 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) {
|
||||
let { value } = this.props
|
||||
|
||||
value.splice(value.indexOf(tag), 1)
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
callback(value, tag)
|
||||
value = _.uniq(value)
|
||||
|
||||
this.value = value
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.buildSuggestions()
|
||||
|
||||
this.setState({
|
||||
newTag: ''
|
||||
})
|
||||
}
|
||||
|
||||
submitNewTag () {
|
||||
this.addNewTag(this.refs.newTag.input.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, className } = this.props
|
||||
const { value, className, showTagsAlphabetically, coloredTags } = this.props
|
||||
|
||||
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 (
|
||||
<span styleName='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'
|
||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
|
||||
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>
|
||||
</span>
|
||||
)
|
||||
})
|
||||
: []
|
||||
|
||||
const { newTag, suggestions } = this.state
|
||||
|
||||
return (
|
||||
<div className={_.isString(className)
|
||||
? 'TagSelect ' + className
|
||||
@@ -134,24 +230,40 @@ class TagSelect extends React.Component {
|
||||
styleName='root'
|
||||
>
|
||||
{tagList}
|
||||
<input styleName='newTag'
|
||||
<Autosuggest
|
||||
ref='newTag'
|
||||
value={this.state.newTag}
|
||||
placeholder='Add tag...'
|
||||
onChange={(e) => this.handleNewTagInputChange(e)}
|
||||
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
||||
onBlur={(e) => this.handleNewTagBlur(e)}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TagSelect.contextTypes = {
|
||||
router: PropTypes.shape({})
|
||||
}
|
||||
|
||||
TagSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func
|
||||
|
||||
onChange: PropTypes.func,
|
||||
coloredTags: PropTypes.object
|
||||
}
|
||||
|
||||
export default CSSModules(TagSelect, styles)
|
||||
|
||||
@@ -3,19 +3,18 @@
|
||||
align-items center
|
||||
user-select none
|
||||
vertical-align middle
|
||||
width 100%
|
||||
overflow-x scroll
|
||||
width 96%
|
||||
overflow-x auto
|
||||
white-space nowrap
|
||||
margin-top 31px
|
||||
top 50px
|
||||
position absolute
|
||||
|
||||
.root::-webkit-scrollbar
|
||||
display none
|
||||
&::-webkit-scrollbar
|
||||
height 8px
|
||||
|
||||
.tag
|
||||
display flex
|
||||
align-items center
|
||||
margin 0px 2px
|
||||
margin 0px 2px 2px
|
||||
padding 2px 4px
|
||||
background-color alpha($ui-tag-backgroundColor, 3%)
|
||||
border-radius 4px
|
||||
@@ -39,16 +38,9 @@
|
||||
|
||||
.tag-label
|
||||
font-size 13px
|
||||
color: $ui-text-color
|
||||
color $ui-text-color
|
||||
padding 4px 16px 4px 8px
|
||||
|
||||
.newTag
|
||||
box-sizing border-box
|
||||
border none
|
||||
background-color transparent
|
||||
outline none
|
||||
padding 0 4px
|
||||
font-size 13px
|
||||
cursor pointer
|
||||
|
||||
body[data-theme="dark"]
|
||||
.tag
|
||||
@@ -62,11 +54,6 @@ body[data-theme="dark"]
|
||||
.tag-label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.tag
|
||||
background-color $ui-solarized-dark-tag-backgroundColor
|
||||
@@ -78,7 +65,24 @@ body[data-theme="solarized-dark"]
|
||||
.tag-label
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
body[data-theme="monokai"]
|
||||
.tag
|
||||
background-color $ui-monokai-tag-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-button--focus-borderColor
|
||||
background-color transparent
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.tag-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.tag
|
||||
background-color $ui-dracula-tag-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-dracula-button--focus-borderColor
|
||||
background-color transparent
|
||||
|
||||
.tag-label
|
||||
color $ui-dracula-borderColor
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ToggleModeButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const ToggleModeButton = ({
|
||||
onClick, editorType
|
||||
@@ -13,7 +14,7 @@ const ToggleModeButton = ({
|
||||
<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'} />
|
||||
</div>
|
||||
<span styleName='tooltip'>Toggle Mode</span>
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
width 52px
|
||||
display flex
|
||||
align-items center
|
||||
position absolute
|
||||
right 165px
|
||||
position: relative
|
||||
top 2px
|
||||
.active
|
||||
background-color #1EC38B
|
||||
width 33px
|
||||
@@ -40,6 +40,11 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.tooltip:lang(ja)
|
||||
@extend .tooltip
|
||||
left -8px
|
||||
width 70px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
@@ -56,3 +61,17 @@ body[data-theme="solarized-dark"]
|
||||
.active
|
||||
background-color #1EC38B
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.control-toggleModeButton
|
||||
background-color #373831
|
||||
.active
|
||||
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
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TrashButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const TrashButton = ({
|
||||
onClick
|
||||
@@ -10,7 +11,7 @@ const TrashButton = ({
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||
<span styleName='tooltip'>Trash</span>
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.tooltip:lang(ja)
|
||||
@extend .tooltip
|
||||
right 46px
|
||||
|
||||
.control-trashButton--in-trash
|
||||
top 60px
|
||||
topBarButtonRight()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user