mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
919 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
259df880ba | ||
|
|
9e8ef70510 | ||
|
|
5fd822a24d | ||
|
|
8ee4dbbb5c | ||
|
|
9522a4d5d9 | ||
|
|
eefecdefbe | ||
|
|
84e670e71c | ||
|
|
c22b69234f | ||
|
|
214ed388f2 | ||
|
|
0bd3445370 | ||
|
|
002dc9b017 | ||
|
|
ebf5a03f56 | ||
|
|
2797faafe5 | ||
|
|
84ac739993 | ||
|
|
13c37f046f | ||
|
|
2631cc3747 | ||
|
|
5873e8e896 | ||
|
|
6dc633c2a1 | ||
|
|
8b07126285 | ||
|
|
93a120543a | ||
|
|
af0aa4a567 | ||
|
|
58fd1f0c46 | ||
|
|
030041932e | ||
|
|
3a90a078ce | ||
|
|
c30957fc9f | ||
|
|
f6db946c9a | ||
|
|
c6c0d4c62a | ||
|
|
57befc4ccb | ||
|
|
e716af75ed | ||
|
|
efe2bea64b | ||
|
|
9b926326ef | ||
|
|
de71033fe2 | ||
|
|
2c10bf251d | ||
|
|
8af50aa5bd | ||
|
|
05bedfe3d4 | ||
|
|
a1929dac8a | ||
|
|
834ecc643a | ||
|
|
60baabf7e7 | ||
|
|
185a149d74 | ||
|
|
5d62dd2002 | ||
|
|
9eaa6b5cec | ||
|
|
6ff03bbb95 | ||
|
|
9fac6bca64 | ||
|
|
88856b788a | ||
|
|
eda4e46d9f | ||
|
|
35bcbbbae4 | ||
|
|
22e2c3da1f | ||
|
|
b526d48946 | ||
|
|
91a95b7c20 | ||
|
|
d634e1124a | ||
|
|
309e159df1 | ||
|
|
ffae53326a | ||
|
|
ddd1522e19 | ||
|
|
4bc0cccb24 | ||
|
|
72fbefa300 | ||
|
|
30378eeb50 | ||
|
|
03293c0d25 | ||
|
|
2e3f6e39f6 | ||
|
|
e4d4041c6b | ||
|
|
166a5c10a7 | ||
|
|
1fec81cc3e | ||
|
|
9c247bcb22 | ||
|
|
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 | ||
|
|
8afa373726 | ||
|
|
199f2202e0 | ||
|
|
86a6311f75 | ||
|
|
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 | ||
|
|
ff67043210 | ||
|
|
31da231c1c | ||
|
|
eb698a7430 | ||
|
|
03be809ba9 | ||
|
|
69601bf15a | ||
|
|
7cad3d403b | ||
|
|
cc84af3346 | ||
|
|
33ef54a162 | ||
|
|
ac43ff886a | ||
|
|
a64f73ca0c | ||
|
|
314477d2fc | ||
|
|
c63fc93daa | ||
|
|
f04ad1e702 | ||
|
|
2183c4bda6 | ||
|
|
6ba91c1515 | ||
|
|
dc03bb76e6 | ||
|
|
d0559c16b5 | ||
|
|
eb693f7b48 | ||
|
|
ef5639ff4b | ||
|
|
34a335797c | ||
|
|
edda3a4d23 | ||
|
|
184839423f | ||
|
|
c0f3600a52 | ||
|
|
2b1302aa07 | ||
|
|
d894bb4aab | ||
|
|
470c071344 | ||
|
|
4bd639c6c4 | ||
|
|
4cb7e63421 | ||
|
|
9f14a503d8 | ||
|
|
d5da6de86c | ||
|
|
4c2b233722 | ||
|
|
ca5b1eea13 | ||
|
|
614e9b6d55 | ||
|
|
27b2530b8d | ||
|
|
2259167200 | ||
|
|
2e05214828 | ||
|
|
cfb996039b | ||
|
|
44c4d56214 | ||
|
|
8e6be91f7c | ||
|
|
00816fb2c8 | ||
|
|
535356b77f | ||
|
|
5e558746ce | ||
|
|
f7bd52ac0c | ||
|
|
9165f518a9 | ||
|
|
01605aa221 | ||
|
|
8b0b29c424 | ||
|
|
7a116966fa | ||
|
|
e7e8f11a74 | ||
|
|
f235d832d5 | ||
|
|
7730b5e20b | ||
|
|
8c3ba4ce48 | ||
|
|
e9a126f586 | ||
|
|
097e7d2ff2 | ||
|
|
81265f1238 | ||
|
|
2b507e6e20 | ||
|
|
747d3a8f13 | ||
|
|
30f6f07434 | ||
|
|
6de5488a15 | ||
|
|
5413647166 | ||
|
|
e83fe73b18 | ||
|
|
87a289ec65 | ||
|
|
8a0a118dba | ||
|
|
687126ce87 | ||
|
|
8a05d577da | ||
|
|
4c3ebfc0f8 | ||
|
|
6093f25f9a | ||
|
|
ecab68d676 | ||
|
|
1cb4f37c95 | ||
|
|
14318528b9 | ||
|
|
9c0e1f8f1a | ||
|
|
2034ce9e4d | ||
|
|
657489caf6 | ||
|
|
94be3d1fe5 | ||
|
|
f6eae41cee | ||
|
|
69c64434e3 | ||
|
|
256cabfce1 | ||
|
|
e8b8272cf9 | ||
|
|
bd5ab4881c | ||
|
|
9630744bdb | ||
|
|
ab3ad0eb97 | ||
|
|
2393889028 | ||
|
|
c36ecb1ed1 | ||
|
|
3e9b28ff0c | ||
|
|
d4eec461a9 | ||
|
|
b584f37087 | ||
|
|
5c75644db2 | ||
|
|
72d9e3e00b | ||
|
|
b3d3362f34 | ||
|
|
1cbaf55cee | ||
|
|
7771875d57 | ||
|
|
85937d8e23 | ||
|
|
1480986a3a | ||
|
|
bc968736df | ||
|
|
ad80b8e8b6 | ||
|
|
d44d220c55 | ||
|
|
f6bcef0789 | ||
|
|
a28ec752e8 | ||
|
|
48ea5746d9 | ||
|
|
f473d31970 | ||
|
|
9cd1d4b466 | ||
|
|
4f02065eaf | ||
|
|
4b85e3e8fb | ||
|
|
e4e8c2205e | ||
|
|
18649dd074 | ||
|
|
9f9463f0e8 | ||
|
|
6cf9bc5de2 | ||
|
|
297b4346c5 | ||
|
|
767a203439 | ||
|
|
c564c253b1 | ||
|
|
b4e138e21b | ||
|
|
8ca01921c4 | ||
|
|
c8b97ffde3 | ||
|
|
abc84e5710 | ||
|
|
d732d195f3 | ||
|
|
789975abb0 | ||
|
|
ed1eab7fcc | ||
|
|
29b31c114a | ||
|
|
c8cf353c21 | ||
|
|
e82e2c71f1 | ||
|
|
dd729c406f | ||
|
|
3127e85900 | ||
|
|
304eeb3158 | ||
|
|
bf781c6b50 | ||
|
|
da1098e441 | ||
|
|
85065357e2 | ||
|
|
1f5f6c3b0e | ||
|
|
4f7026969f | ||
|
|
16d264ebfa | ||
|
|
04ffbe945b | ||
|
|
974a1a1e7d | ||
|
|
ca2c07244f | ||
|
|
f90786d1c0 | ||
|
|
bdf55568c7 | ||
|
|
e262d2f19b | ||
|
|
aabfe820ac | ||
|
|
3bba5442bd | ||
|
|
6ce1922fb3 | ||
|
|
9367a404ef | ||
|
|
7c8e19c681 | ||
|
|
7ccc5eb9b8 | ||
|
|
b4b6d3d07c | ||
|
|
9007bac7b2 | ||
|
|
df13ca3c92 | ||
|
|
d86935acaa | ||
|
|
72b450d526 | ||
|
|
4b7afeeb4f | ||
|
|
0e0e779cbe | ||
|
|
89b2f48a06 | ||
|
|
c80bdb8d0c | ||
|
|
50d89a8ec9 | ||
|
|
f5ccaa7b48 | ||
|
|
e682ee8541 | ||
|
|
caaa7a9e74 | ||
|
|
6ef0e325e2 | ||
|
|
ea064deeb8 | ||
|
|
8e81609a39 | ||
|
|
83da07a941 | ||
|
|
977e80c829 | ||
|
|
8ba0d10f40 | ||
|
|
2581091652 | ||
|
|
e72a7ceaea | ||
|
|
a17ddf6d54 | ||
|
|
b5e2d21f33 | ||
|
|
d09f8dff18 | ||
|
|
bdb3406dcb | ||
|
|
f54b49db1a | ||
|
|
0cc9f006c5 | ||
|
|
db1398b65f | ||
|
|
e8192e6c3f | ||
|
|
775200bdd6 | ||
|
|
820a2a093c | ||
|
|
d3995b9b10 | ||
|
|
f6867f9338 | ||
|
|
3e2548fcd5 | ||
|
|
745d250787 | ||
|
|
b1063eb38f | ||
|
|
9032a1debb | ||
|
|
795fe8ae1d | ||
|
|
e5a908af68 | ||
|
|
6ce16c1cc0 | ||
|
|
6ff2a5ac94 | ||
|
|
fcea16e43a | ||
|
|
7b8e42382e | ||
|
|
a372b5ea39 | ||
|
|
1aafee2a7c | ||
|
|
7afe3d5181 | ||
|
|
6fba62d062 | ||
|
|
6906c0ab0d | ||
|
|
5d46adf8fd | ||
|
|
eda1f11d42 | ||
|
|
6431a8255d | ||
|
|
48fd1d11e2 | ||
|
|
4c3e62efad | ||
|
|
52a15a5d92 | ||
|
|
57f4aa5995 | ||
|
|
ab9ab004b7 | ||
|
|
ac624eb98f | ||
|
|
1a4c37820d | ||
|
|
685206950b | ||
|
|
eececf8a93 | ||
|
|
9bc3d65554 | ||
|
|
f9b854ce39 | ||
|
|
1416968fe5 | ||
|
|
efc183c709 | ||
|
|
07a2442718 | ||
|
|
f549c50a58 | ||
|
|
8d6ce1a2f7 | ||
|
|
c5b33e025e | ||
|
|
4cdfc738c0 | ||
|
|
46d46f21e4 | ||
|
|
4983605b23 | ||
|
|
9e8d04d806 | ||
|
|
042f935133 | ||
|
|
ed9d3639e2 | ||
|
|
728f105830 | ||
|
|
6f359fa6a8 | ||
|
|
57a88743bc | ||
|
|
667f022086 | ||
|
|
b742a3a4cd | ||
|
|
8d5c9742f8 | ||
|
|
c2a49efe73 | ||
|
|
8c8a0ab46d | ||
|
|
959b75bddd | ||
|
|
d29d5105f1 | ||
|
|
38e82872a5 | ||
|
|
15d9ce1d36 | ||
|
|
67f7cdb36c | ||
|
|
6a9d4ae0fd | ||
|
|
6a761c3fb5 | ||
|
|
3baf97e69f | ||
|
|
694dc60481 | ||
|
|
e3c6f0452c | ||
|
|
ed2401a87b | ||
|
|
cb59458c79 | ||
|
|
125a493400 | ||
|
|
83910b55d2 | ||
|
|
f4fd131100 | ||
|
|
cfdc880d8c | ||
|
|
7303e8bdd2 | ||
|
|
ecde465d9f | ||
|
|
5c5e70a805 | ||
|
|
4e41d9be55 | ||
|
|
d06d94449c | ||
|
|
1af2c83c63 | ||
|
|
6c75136777 | ||
|
|
31351e34e1 | ||
|
|
a058a774e9 | ||
|
|
e6db28485c | ||
|
|
391bb096d6 | ||
|
|
a7a5b789fa | ||
|
|
10b7d58dc6 | ||
|
|
2b496dc2e5 | ||
|
|
a6e0b30576 | ||
|
|
16f0e95e32 | ||
|
|
55395d3a2d | ||
|
|
4e0fa63fad | ||
|
|
b9ea7696d8 | ||
|
|
7de3308f52 | ||
|
|
b2b2373c7b | ||
|
|
1c54f40a28 | ||
|
|
1ac31264b7 | ||
|
|
ca4b8224fd | ||
|
|
d1e5781c24 | ||
|
|
c86e451198 | ||
|
|
4735992835 | ||
|
|
cba3519458 | ||
|
|
c64a5e1cca | ||
|
|
47ee8b8ce7 | ||
|
|
b4a7b547f0 | ||
|
|
455b424429 | ||
|
|
7d67ac3f12 | ||
|
|
34f377eb5c | ||
|
|
b7f4af8c78 | ||
|
|
07b395c24a | ||
|
|
a6c7dde194 | ||
|
|
43ebe4ecfd | ||
|
|
53b9630fa5 | ||
|
|
1d38f1abb4 | ||
|
|
061a0cd219 | ||
|
|
f0ed20ee2c | ||
|
|
edfc8d95c8 | ||
|
|
f820c3089e | ||
|
|
c33f9d8307 | ||
|
|
eee212f5b8 | ||
|
|
b690147b0b | ||
|
|
10879d0f67 | ||
|
|
b48b8f39fc | ||
|
|
3d0b3e759b | ||
|
|
3e7f4a41e2 | ||
|
|
b89b888129 | ||
|
|
cba9afc9ba | ||
|
|
a66a11b81e | ||
|
|
22cc2791b6 | ||
|
|
369cf16b68 | ||
|
|
0d38baf194 | ||
|
|
5b63c95f40 | ||
|
|
52497999a0 | ||
|
|
6bab108a35 | ||
|
|
eec22e6b7d | ||
|
|
edaa0713e8 | ||
|
|
6b6a415dd5 | ||
|
|
3fbc749395 | ||
|
|
460437397f | ||
|
|
a36e779980 | ||
|
|
84f18ced47 | ||
|
|
037ff2e749 | ||
|
|
f0f23ede3d | ||
|
|
c8763063c0 | ||
|
|
e57fef2413 | ||
|
|
9095fe934d | ||
|
|
9139495f02 | ||
|
|
bcb1fb4331 | ||
|
|
70b69a3bc9 | ||
|
|
a7e458b784 | ||
|
|
a504a45d99 | ||
|
|
2d0e14c1cc | ||
|
|
7a4258bb20 |
2
.babelrc
2
.babelrc
@@ -5,7 +5,7 @@
|
|||||||
"presets": ["react-hmre"]
|
"presets": ["react-hmre"]
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"presets": ["react", "es2015"],
|
"presets": ["env" ,"react", "es2015"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
|
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"theme": "monokai"
|
"theme": "monokai"
|
||||||
},
|
},
|
||||||
"hotkey": {
|
"hotkey": {
|
||||||
"toggleFinder": "Cmd + Alt + S",
|
|
||||||
"toggleMain": "Cmd + Alt + L"
|
"toggleMain": "Cmd + Alt + L"
|
||||||
},
|
},
|
||||||
"isSideNavFolded": false,
|
"isSideNavFolded": false,
|
||||||
@@ -24,6 +23,7 @@
|
|||||||
"lineNumber": true
|
"lineNumber": true
|
||||||
},
|
},
|
||||||
"sortBy": "UPDATED_AT",
|
"sortBy": "UPDATED_AT",
|
||||||
|
"sortTagsBy": "ALPHABETICAL",
|
||||||
"ui": {
|
"ui": {
|
||||||
"defaultNote": "ALWAYS_ASK",
|
"defaultNote": "ALWAYS_ASK",
|
||||||
"disableDirectWrite": false,
|
"disableDirectWrite": false,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
compiled/
|
compiled/
|
||||||
dist/
|
dist/
|
||||||
|
extra_scripts/
|
||||||
12
.eslintrc
12
.eslintrc
@@ -3,7 +3,9 @@
|
|||||||
"plugins": ["react"],
|
"plugins": ["react"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-useless-escape": 0,
|
"no-useless-escape": 0,
|
||||||
"prefer-const": "warn",
|
"prefer-const": ["warn", {
|
||||||
|
"destructuring": "all"
|
||||||
|
}],
|
||||||
"no-unused-vars": "warn",
|
"no-unused-vars": "warn",
|
||||||
"no-undef": "warn",
|
"no-undef": "warn",
|
||||||
"no-lone-blocks": "warn",
|
"no-lone-blocks": "warn",
|
||||||
@@ -12,5 +14,13 @@
|
|||||||
"react/no-find-dom-node": "warn",
|
"react/no-find-dom-node": "warn",
|
||||||
"react/no-render-return-value": "warn",
|
"react/no-render-return-value": "warn",
|
||||||
"react/no-deprecated": "warn"
|
"react/no-deprecated": "warn"
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"FileReader": true,
|
||||||
|
"localStorage": true,
|
||||||
|
"fetch": true
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ node_modules/*
|
|||||||
/compiled
|
/compiled
|
||||||
/secret
|
/secret
|
||||||
*.log
|
*.log
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- stable
|
- 7
|
||||||
- lts/*
|
|
||||||
script:
|
script:
|
||||||
- npm run lint && npm run test
|
- 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'
|
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
|
||||||
after_success:
|
after_success:
|
||||||
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
||||||
|
|||||||
41
Backers.md
41
Backers.md
@@ -1,27 +1,50 @@
|
|||||||
<h1 align="center">Sponsors & Backers</h1>
|
<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](https://github.com/BoostIO/Boostnote/blob/master/Backers.md). If you'd like to join them, please consider:
|
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)
|
- [Become a backer or sponsor on Open Collective.](https://opencollective.com/boostnoteio)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Backers via OpenCollective
|
## Backers via OpenCollective
|
||||||
<a href="https://opencollective.com/boostnoteio#backers" target="_blank"><img src="https://opencollective.com/boostnoteio/backers.svg?width=890"></a>
|
|
||||||
|
|
||||||
- [Ralph03](https://opencollective.com/ralph03) - $24
|
### [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/.
|
||||||
|
|
||||||
- [Nikolas Dan](https://opencollective.com/nikolas-dan) - $20
|
### [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/.
|
||||||
|
|
||||||
- [tatoosh11](https://twitter.com/ta11) - $10
|
### [Bronze Sponsors / $50 per month](https://opencollective.com/boostnoteio/order/2258)
|
||||||
|
- Get your name and Url (or E-mail) on Readme.md on GitHub.
|
||||||
|
|
||||||
- [Alexander Borovkov](https://opencollective.com/alexander-borovkov) - $10
|
### [Backers3 / $10 per month](https://opencollective.com/boostnoteio/order/2176)
|
||||||
|
- [Ralph03](https://opencollective.com/ralph03)
|
||||||
|
|
||||||
- [Yeojong Kim](https://twitter.com/yeojoy) - $5
|
- [Nikolas Dan](https://opencollective.com/nikolas-dan)
|
||||||
|
|
||||||
- [Scotia Draven](https://opencollective.com/scotia-draven) - $5
|
### [Backers2 / $5 per month](https://opencollective.com/boostnoteio/order/2175)
|
||||||
|
- [Yeojong Kim](https://twitter.com/yeojoy)
|
||||||
|
|
||||||
- [spoonhoop](https://opencollective.com/spoonhoop) - $5
|
- [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
|
## Backers via Bountysource
|
||||||
https://salt.bountysource.com/teams/boostnote
|
https://salt.bountysource.com/teams/boostnote
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
|
# Current behavior
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug.
|
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.
|
If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# Expected behavior
|
||||||
|
|
||||||
|
# Steps to reproduce
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
|
||||||
|
- Version :
|
||||||
|
- OS Version and name :
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Love Boostnote? Please consider supporting us via OpenCollective:
|
Love Boostnote? Please consider supporting us via OpenCollective:
|
||||||
👉 https://opencollective.com/boostnoteio
|
👉 https://opencollective.com/boostnoteio
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -2,7 +2,7 @@ GPL-3.0
|
|||||||
|
|
||||||
Boostnote - an open source note-taking app made for programmers just like you.
|
Boostnote - an open source note-taking app made for programmers just like you.
|
||||||
|
|
||||||
Copyright (C) 2017 Maisin&Co., Inc.
|
Copyright (C) 2017 - 2018 BoostIO
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
|||||||
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()
|
||||||
|
}
|
||||||
@@ -1,36 +1,34 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
import path from 'path'
|
import 'codemirror-mode-elixir'
|
||||||
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
import convertModeName from 'browser/lib/convertModeName'
|
||||||
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
|
import iconv from 'iconv-lite'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import consts from 'browser/lib/consts'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
const { ipcRenderer } = require('electron')
|
||||||
|
|
||||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||||
|
|
||||||
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||||
|
const buildCMRulers = (rulers, enableRulers) =>
|
||||||
function pass (name) {
|
enableRulers ? rulers.map(ruler => ({column: ruler})) : []
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class CodeEditor extends React.Component {
|
export default class CodeEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||||
this.changeHandler = (e) => this.handleChange(e)
|
this.changeHandler = (e) => this.handleChange(e)
|
||||||
|
this.focusHandler = () => {
|
||||||
|
ipcRenderer.send('editor:focused', true)
|
||||||
|
}
|
||||||
this.blurHandler = (editor, e) => {
|
this.blurHandler = (editor, e) => {
|
||||||
|
ipcRenderer.send('editor:focused', false)
|
||||||
if (e == null) return null
|
if (e == null) return null
|
||||||
let el = e.relatedTarget
|
let el = e.relatedTarget
|
||||||
while (el != null) {
|
while (el != null) {
|
||||||
@@ -40,34 +38,92 @@ export default class CodeEditor extends React.Component {
|
|||||||
el = el.parentNode
|
el = el.parentNode
|
||||||
}
|
}
|
||||||
this.props.onBlur != null && this.props.onBlur(e)
|
this.props.onBlur != null && this.props.onBlur(e)
|
||||||
|
|
||||||
|
const {storageKey, noteKey} = this.props
|
||||||
|
attachmentManagement.deleteAttachmentsNotPresentInNote(this.editor.getValue(), storageKey, noteKey)
|
||||||
}
|
}
|
||||||
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
|
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
|
||||||
this.loadStyleHandler = (e) => {
|
this.loadStyleHandler = (e) => {
|
||||||
this.editor.refresh()
|
this.editor.refresh()
|
||||||
}
|
}
|
||||||
|
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||||
|
this.searchState = null
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearch (msg) {
|
||||||
|
const cm = this.editor
|
||||||
|
const component = this
|
||||||
|
|
||||||
|
if (component.searchState) cm.removeOverlay(component.searchState)
|
||||||
|
if (msg.length < 3) return
|
||||||
|
|
||||||
|
cm.operation(function () {
|
||||||
|
component.searchState = makeOverlay(msg, 'searching')
|
||||||
|
cm.addOverlay(component.searchState)
|
||||||
|
|
||||||
|
function makeOverlay (query, style) {
|
||||||
|
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi')
|
||||||
|
return {
|
||||||
|
token: function (stream) {
|
||||||
|
query.lastIndex = stream.pos
|
||||||
|
var match = query.exec(stream.string)
|
||||||
|
if (match && match.index === stream.pos) {
|
||||||
|
stream.pos += match[0].length || 1
|
||||||
|
return style
|
||||||
|
} else if (match) {
|
||||||
|
stream.pos = match.index
|
||||||
|
} else {
|
||||||
|
stream.skipToEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
const { rulers, enableRulers } = this.props
|
||||||
|
const expandSnippet = this.expandSnippet.bind(this)
|
||||||
|
|
||||||
|
const defaultSnippet = [
|
||||||
|
{
|
||||||
|
id: crypto.randomBytes(16).toString('hex'),
|
||||||
|
name: 'Dummy text',
|
||||||
|
prefix: ['lorem', 'ipsum'],
|
||||||
|
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if (!fs.existsSync(consts.SNIPPET_FILE)) {
|
||||||
|
fs.writeFileSync(consts.SNIPPET_FILE, JSON.stringify(defaultSnippet, null, 4), 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
this.value = this.props.value
|
this.value = this.props.value
|
||||||
this.editor = CodeMirror(this.refs.root, {
|
this.editor = CodeMirror(this.refs.root, {
|
||||||
|
rulers: buildCMRulers(rulers, enableRulers),
|
||||||
value: this.props.value,
|
value: this.props.value,
|
||||||
lineNumbers: true,
|
lineNumbers: this.props.displayLineNumbers,
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
theme: this.props.theme,
|
theme: this.props.theme,
|
||||||
indentUnit: this.props.indentSize,
|
indentUnit: this.props.indentSize,
|
||||||
tabSize: this.props.indentSize,
|
tabSize: this.props.indentSize,
|
||||||
indentWithTabs: this.props.indentType !== 'space',
|
indentWithTabs: this.props.indentType !== 'space',
|
||||||
keyMap: this.props.keyMap,
|
keyMap: this.props.keyMap,
|
||||||
|
scrollPastEnd: this.props.scrollPastEnd,
|
||||||
inputStyle: 'textarea',
|
inputStyle: 'textarea',
|
||||||
dragDrop: false,
|
dragDrop: false,
|
||||||
|
foldGutter: true,
|
||||||
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||||
|
autoCloseBrackets: true,
|
||||||
extraKeys: {
|
extraKeys: {
|
||||||
Tab: function (cm) {
|
Tab: function (cm) {
|
||||||
const cursor = cm.getCursor()
|
const cursor = cm.getCursor()
|
||||||
const line = cm.getLine(cursor.line)
|
const line = cm.getLine(cursor.line)
|
||||||
|
const cursorPosition = cursor.ch
|
||||||
|
const charBeforeCursor = line.substr(cursorPosition - 1, 1)
|
||||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||||
else {
|
else {
|
||||||
const tabs = cm.getOption('indentWithTabs')
|
const tabs = cm.getOption('indentWithTabs')
|
||||||
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)\] )?$/)) {
|
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
|
||||||
cm.execCommand('goLineStart')
|
cm.execCommand('goLineStart')
|
||||||
if (tabs) {
|
if (tabs) {
|
||||||
cm.execCommand('insertTab')
|
cm.execCommand('insertTab')
|
||||||
@@ -75,6 +131,16 @@ export default class CodeEditor extends React.Component {
|
|||||||
cm.execCommand('insertSoftTab')
|
cm.execCommand('insertSoftTab')
|
||||||
}
|
}
|
||||||
cm.execCommand('goLineEnd')
|
cm.execCommand('goLineEnd')
|
||||||
|
} else if (!charBeforeCursor.match(/\t|\s|\r|\n/) && cursor.ch > 1) {
|
||||||
|
// text expansion on tab key if the char before is alphabet
|
||||||
|
const snippets = JSON.parse(fs.readFileSync(consts.SNIPPET_FILE, 'utf8'))
|
||||||
|
if (expandSnippet(line, cursor, cm, snippets) === false) {
|
||||||
|
if (tabs) {
|
||||||
|
cm.execCommand('insertTab')
|
||||||
|
} else {
|
||||||
|
cm.execCommand('insertSoftTab')
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (tabs) {
|
if (tabs) {
|
||||||
cm.execCommand('insertTab')
|
cm.execCommand('insertTab')
|
||||||
@@ -87,7 +153,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
'Cmd-T': function (cm) {
|
'Cmd-T': function (cm) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
},
|
},
|
||||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||||
'Ctrl-C': (cm) => {
|
'Ctrl-C': (cm) => {
|
||||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
@@ -99,11 +165,16 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
this.setMode(this.props.mode)
|
this.setMode(this.props.mode)
|
||||||
|
|
||||||
|
this.editor.on('focus', this.focusHandler)
|
||||||
this.editor.on('blur', this.blurHandler)
|
this.editor.on('blur', this.blurHandler)
|
||||||
this.editor.on('change', this.changeHandler)
|
this.editor.on('change', this.changeHandler)
|
||||||
this.editor.on('paste', this.pasteHandler)
|
this.editor.on('paste', this.pasteHandler)
|
||||||
|
eventEmitter.on('top:search', this.searchHandler)
|
||||||
|
|
||||||
let editorTheme = document.getElementById('editorTheme')
|
eventEmitter.emit('code:init')
|
||||||
|
this.editor.on('scroll', this.scrollHandler)
|
||||||
|
|
||||||
|
const editorTheme = document.getElementById('editorTheme')
|
||||||
editorTheme.addEventListener('load', this.loadStyleHandler)
|
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||||
|
|
||||||
CodeMirror.Vim.defineEx('quit', 'q', this.quitEditor)
|
CodeMirror.Vim.defineEx('quit', 'q', this.quitEditor)
|
||||||
@@ -113,20 +184,91 @@ export default class CodeEditor extends React.Component {
|
|||||||
CodeMirror.Vim.map('ZZ', ':q', 'normal')
|
CodeMirror.Vim.map('ZZ', ':q', 'normal')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expandSnippet (line, cursor, cm, snippets) {
|
||||||
|
const wordBeforeCursor = this.getWordBeforeCursor(line, cursor.line, cursor.ch)
|
||||||
|
const templateCursorString = ':{}'
|
||||||
|
for (let i = 0; i < snippets.length; i++) {
|
||||||
|
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
|
||||||
|
if (snippets[i].content.indexOf(templateCursorString) !== -1) {
|
||||||
|
const snippetLines = snippets[i].content.split('\n')
|
||||||
|
let cursorLineNumber = 0
|
||||||
|
let cursorLinePosition = 0
|
||||||
|
for (let j = 0; j < snippetLines.length; j++) {
|
||||||
|
const cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||||
|
if (cursorIndex !== -1) {
|
||||||
|
cursorLineNumber = j
|
||||||
|
cursorLinePosition = cursorIndex
|
||||||
|
cm.replaceRange(
|
||||||
|
snippets[i].content.replace(templateCursorString, ''),
|
||||||
|
wordBeforeCursor.range.from,
|
||||||
|
wordBeforeCursor.range.to
|
||||||
|
)
|
||||||
|
cm.setCursor({ line: cursor.line + cursorLineNumber, ch: cursorLinePosition })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cm.replaceRange(
|
||||||
|
snippets[i].content,
|
||||||
|
wordBeforeCursor.range.from,
|
||||||
|
wordBeforeCursor.range.to
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
getWordBeforeCursor (line, lineNumber, cursorPosition) {
|
||||||
|
let wordBeforeCursor = ''
|
||||||
|
const originCursorPosition = cursorPosition
|
||||||
|
const emptyChars = /\t|\s|\r|\n/
|
||||||
|
|
||||||
|
// to prevent the word to expand is long that will crash the whole app
|
||||||
|
// the safeStop is there to stop user to expand words that longer than 20 chars
|
||||||
|
const safeStop = 20
|
||||||
|
|
||||||
|
while (cursorPosition > 0) {
|
||||||
|
const currentChar = line.substr(cursorPosition - 1, 1)
|
||||||
|
// if char is not an empty char
|
||||||
|
if (!emptyChars.test(currentChar)) {
|
||||||
|
wordBeforeCursor = currentChar + wordBeforeCursor
|
||||||
|
} else if (wordBeforeCursor.length >= safeStop) {
|
||||||
|
throw new Error('Your snippet trigger is too long !')
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cursorPosition--
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: wordBeforeCursor,
|
||||||
|
range: {
|
||||||
|
from: {line: lineNumber, ch: originCursorPosition},
|
||||||
|
to: {line: lineNumber, ch: cursorPosition}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
quitEditor () {
|
quitEditor () {
|
||||||
document.querySelector('textarea').blur()
|
document.querySelector('textarea').blur()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
this.editor.off('focus', this.focusHandler)
|
||||||
this.editor.off('blur', this.blurHandler)
|
this.editor.off('blur', this.blurHandler)
|
||||||
this.editor.off('change', this.changeHandler)
|
this.editor.off('change', this.changeHandler)
|
||||||
this.editor.off('paste', this.pasteHandler)
|
this.editor.off('paste', this.pasteHandler)
|
||||||
let editorTheme = document.getElementById('editorTheme')
|
eventEmitter.off('top:search', this.searchHandler)
|
||||||
|
this.editor.off('scroll', this.scrollHandler)
|
||||||
|
const editorTheme = document.getElementById('editorTheme')
|
||||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps, prevState) {
|
componentDidUpdate (prevProps, prevState) {
|
||||||
let needRefresh = false
|
let needRefresh = false
|
||||||
|
const {rulers, enableRulers} = this.props
|
||||||
if (prevProps.mode !== this.props.mode) {
|
if (prevProps.mode !== this.props.mode) {
|
||||||
this.setMode(this.props.mode)
|
this.setMode(this.props.mode)
|
||||||
}
|
}
|
||||||
@@ -144,6 +286,10 @@ export default class CodeEditor extends React.Component {
|
|||||||
needRefresh = true
|
needRefresh = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevProps.enableRulers !== enableRulers || prevProps.rulers !== rulers) {
|
||||||
|
this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
|
||||||
|
}
|
||||||
|
|
||||||
if (prevProps.indentSize !== this.props.indentSize) {
|
if (prevProps.indentSize !== this.props.indentSize) {
|
||||||
this.editor.setOption('indentUnit', this.props.indentSize)
|
this.editor.setOption('indentUnit', this.props.indentSize)
|
||||||
this.editor.setOption('tabSize', this.props.indentSize)
|
this.editor.setOption('tabSize', this.props.indentSize)
|
||||||
@@ -152,13 +298,21 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
|
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevProps.displayLineNumbers !== this.props.displayLineNumbers) {
|
||||||
|
this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
|
||||||
|
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
|
||||||
|
}
|
||||||
|
|
||||||
if (needRefresh) {
|
if (needRefresh) {
|
||||||
this.editor.refresh()
|
this.editor.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setMode (mode) {
|
setMode (mode) {
|
||||||
let syntax = CodeMirror.findModeByName(pass(mode))
|
let syntax = CodeMirror.findModeByName(convertModeName(mode))
|
||||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||||
|
|
||||||
this.editor.setOption('mode', syntax.mime)
|
this.editor.setOption('mode', syntax.mime)
|
||||||
@@ -197,55 +351,155 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setValue (value) {
|
setValue (value) {
|
||||||
let cursor = this.editor.getCursor()
|
const cursor = this.editor.getCursor()
|
||||||
this.editor.setValue(value)
|
this.editor.setValue(value)
|
||||||
this.editor.setCursor(cursor)
|
this.editor.setCursor(cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDropImage (e) {
|
handleDropImage (dropEvent) {
|
||||||
e.preventDefault()
|
dropEvent.preventDefault()
|
||||||
const imagePath = e.dataTransfer.files[0].path
|
const {storageKey, noteKey} = this.props
|
||||||
const filename = path.basename(imagePath)
|
attachmentManagement.handleAttachmentDrop(this, storageKey, noteKey, dropEvent)
|
||||||
|
|
||||||
copyImage(imagePath, this.props.storageKey).then((imagePath) => {
|
|
||||||
const imageMd = `})`
|
|
||||||
this.insertImageMd(imageMd)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
insertImageMd (imageMd) {
|
insertAttachmentMd (imageMd) {
|
||||||
this.editor.replaceSelection(imageMd)
|
this.editor.replaceSelection(imageMd)
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePaste (editor, e) {
|
handlePaste (editor, e) {
|
||||||
const dataTransferItem = e.clipboardData.items[0]
|
const clipboardData = e.clipboardData
|
||||||
if (!dataTransferItem.type.match('image')) return
|
const {storageKey, noteKey} = this.props
|
||||||
|
const dataTransferItem = clipboardData.items[0]
|
||||||
const blob = dataTransferItem.getAsFile()
|
const pastedTxt = clipboardData.getData('text')
|
||||||
let reader = new FileReader()
|
const isURL = (str) => {
|
||||||
let base64data
|
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
||||||
|
return matcher.test(str)
|
||||||
reader.readAsDataURL(blob)
|
|
||||||
reader.onloadend = () => {
|
|
||||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
|
||||||
base64data += base64data.replace('+', ' ')
|
|
||||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
|
||||||
const imageName = Math.random().toString(36).slice(-16)
|
|
||||||
const storagePath = findStorage(this.props.storageKey).path
|
|
||||||
const imageDir = path.join(storagePath, 'images')
|
|
||||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
|
||||||
const imagePath = path.join(imageDir, `${imageName}.png`)
|
|
||||||
fs.writeFile(imagePath, binaryData, 'binary')
|
|
||||||
const imageMd = `})`
|
|
||||||
this.insertImageMd(imageMd)
|
|
||||||
}
|
}
|
||||||
|
const isInLinkTag = (editor) => {
|
||||||
|
const startCursor = editor.getCursor('start')
|
||||||
|
const prevChar = editor.getRange(
|
||||||
|
{line: startCursor.line, ch: startCursor.ch - 2},
|
||||||
|
{line: startCursor.line, ch: startCursor.ch}
|
||||||
|
)
|
||||||
|
const endCursor = editor.getCursor('end')
|
||||||
|
const nextChar = editor.getRange(
|
||||||
|
{line: endCursor.line, ch: endCursor.ch},
|
||||||
|
{line: endCursor.line, ch: endCursor.ch + 1}
|
||||||
|
)
|
||||||
|
return prevChar === '](' && nextChar === ')'
|
||||||
|
}
|
||||||
|
if (dataTransferItem.type.match('image')) {
|
||||||
|
attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem)
|
||||||
|
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||||
|
this.handlePasteUrl(e, editor, pastedTxt)
|
||||||
|
}
|
||||||
|
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||||
|
attachmentManagement.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
|
||||||
|
.then((modifiedText) => {
|
||||||
|
this.editor.replaceSelection(modifiedText)
|
||||||
|
})
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll (e) {
|
||||||
|
if (this.props.onScroll) {
|
||||||
|
this.props.onScroll(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePasteUrl (e, editor, pastedTxt) {
|
||||||
|
e.preventDefault()
|
||||||
|
const taggedUrl = `<${pastedTxt}>`
|
||||||
|
editor.replaceSelection(taggedUrl)
|
||||||
|
|
||||||
|
const isImageReponse = (response) => {
|
||||||
|
return response.headers.has('content-type') &&
|
||||||
|
response.headers.get('content-type').match(/^image\/.+$/)
|
||||||
|
}
|
||||||
|
const replaceTaggedUrl = (replacement) => {
|
||||||
|
const value = editor.getValue()
|
||||||
|
const cursor = editor.getCursor()
|
||||||
|
const newValue = value.replace(taggedUrl, replacement)
|
||||||
|
const newCursor = Object.assign({}, cursor, { ch: cursor.ch + newValue.length - value.length })
|
||||||
|
editor.setValue(newValue)
|
||||||
|
editor.setCursor(newCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(pastedTxt, {
|
||||||
|
method: 'get'
|
||||||
|
}).then((response) => {
|
||||||
|
if (isImageReponse(response)) {
|
||||||
|
return this.mapImageResponse(response, pastedTxt)
|
||||||
|
} else {
|
||||||
|
return this.mapNormalResponse(response, pastedTxt)
|
||||||
|
}
|
||||||
|
}).then((replacement) => {
|
||||||
|
replaceTaggedUrl(replacement)
|
||||||
|
}).catch((e) => {
|
||||||
|
replaceTaggedUrl(pastedTxt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mapNormalResponse (response, pastedTxt) {
|
||||||
|
return this.decodeResponse(response).then((body) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const parsedBody = (new window.DOMParser()).parseFromString(body, 'text/html')
|
||||||
|
const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
|
||||||
|
resolve(linkWithTitle)
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mapImageResponse (response, pastedTxt) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const url = response.url
|
||||||
|
const name = url.substring(url.lastIndexOf('/') + 1)
|
||||||
|
const imageLinkWithName = ``
|
||||||
|
resolve(imageLinkWithName)
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeResponse (response) {
|
||||||
|
const headers = response.headers
|
||||||
|
const _charset = headers.has('content-type')
|
||||||
|
? this.extractContentTypeCharset(headers.get('content-type'))
|
||||||
|
: undefined
|
||||||
|
return response.arrayBuffer().then((buff) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8'
|
||||||
|
resolve(iconv.decode(new Buffer(buff), charset).toString())
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
extractContentTypeCharset (contentType) {
|
||||||
|
return contentType.split(';').filter((str) => {
|
||||||
|
return str.trim().toLowerCase().startsWith('charset')
|
||||||
|
}).map((str) => {
|
||||||
|
return str.replace(/['"]/g, '').split('=')[1]
|
||||||
|
})[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { className, fontFamily, fontSize } = this.props
|
const {className, fontSize} = this.props
|
||||||
|
let fontFamily = this.props.fontFamily
|
||||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||||
? [fontFamily].concat(defaultEditorFontFamily)
|
? [fontFamily].concat(defaultEditorFontFamily)
|
||||||
: defaultEditorFontFamily
|
: defaultEditorFontFamily
|
||||||
|
const width = this.props.width
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={className == null
|
className={className == null
|
||||||
@@ -256,7 +510,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
style={{
|
style={{
|
||||||
fontFamily: fontFamily.join(', '),
|
fontFamily: fontFamily.join(', '),
|
||||||
fontSize: fontSize
|
fontSize: fontSize,
|
||||||
|
width: width
|
||||||
}}
|
}}
|
||||||
onDrop={(e) => this.handleDropImage(e)}
|
onDrop={(e) => this.handleDropImage(e)}
|
||||||
/>
|
/>
|
||||||
@@ -266,6 +521,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
CodeEditor.propTypes = {
|
CodeEditor.propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
|
enableRulers: PropTypes.bool,
|
||||||
|
rulers: PropTypes.arrayOf(Number),
|
||||||
mode: PropTypes.string,
|
mode: PropTypes.string,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
onBlur: PropTypes.func,
|
onBlur: PropTypes.func,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './MarkdownEditor.styl'
|
import styles from './MarkdownEditor.styl'
|
||||||
import CodeEditor from 'browser/components/CodeEditor'
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
class MarkdownEditor extends React.Component {
|
class MarkdownEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -70,9 +70,9 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleContextMenu (e) {
|
handleContextMenu (e) {
|
||||||
let { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||||
let newStatus = this.state.status === 'PREVIEW'
|
const newStatus = this.state.status === 'PREVIEW'
|
||||||
? 'CODE'
|
? 'CODE'
|
||||||
: 'PREVIEW'
|
: 'PREVIEW'
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -91,9 +91,11 @@ class MarkdownEditor extends React.Component {
|
|||||||
handleBlur (e) {
|
handleBlur (e) {
|
||||||
if (this.state.isLocked) return
|
if (this.state.isLocked) return
|
||||||
this.setState({ keyPressed: new Set() })
|
this.setState({ keyPressed: new Set() })
|
||||||
let { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'BLUR') {
|
if (config.editor.switchPreview === 'BLUR' ||
|
||||||
let cursorPosition = this.refs.code.editor.getCursor()
|
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
|
||||||
|
) {
|
||||||
|
const cursorPosition = this.refs.code.editor.getCursor()
|
||||||
this.setState({
|
this.setState({
|
||||||
status: 'PREVIEW'
|
status: 'PREVIEW'
|
||||||
}, () => {
|
}, () => {
|
||||||
@@ -104,12 +106,26 @@ 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) {
|
handlePreviewMouseDown (e) {
|
||||||
this.previewMouseDownedAt = new Date()
|
this.previewMouseDownedAt = new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePreviewMouseUp (e) {
|
handlePreviewMouseUp (e) {
|
||||||
let { config } = this.props
|
const { config } = this.props
|
||||||
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) {
|
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) {
|
||||||
this.setState({
|
this.setState({
|
||||||
status: 'CODE'
|
status: 'CODE'
|
||||||
@@ -123,15 +139,15 @@ class MarkdownEditor extends React.Component {
|
|||||||
handleCheckboxClick (e) {
|
handleCheckboxClick (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
let idMatch = /checkbox-([0-9]+)/
|
const idMatch = /checkbox-([0-9]+)/
|
||||||
let checkedMatch = /\[x\]/i
|
const checkedMatch = /\[x\]/i
|
||||||
let uncheckedMatch = /\[ \]/
|
const uncheckedMatch = /\[ \]/
|
||||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||||
let lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||||
let lines = this.refs.code.value
|
const lines = this.refs.code.value
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
|
||||||
let targetLine = lines[lineIndex]
|
const targetLine = lines[lineIndex]
|
||||||
|
|
||||||
if (targetLine.match(checkedMatch)) {
|
if (targetLine.match(checkedMatch)) {
|
||||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||||
@@ -163,12 +179,12 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
let { config } = this.props
|
const { config } = this.props
|
||||||
if (this.state.status !== 'CODE') return false
|
if (this.state.status !== 'CODE') return false
|
||||||
const keyPressed = this.state.keyPressed
|
const keyPressed = this.state.keyPressed
|
||||||
keyPressed.add(e.keyCode)
|
keyPressed.add(e.keyCode)
|
||||||
this.setState({ keyPressed })
|
this.setState({ keyPressed })
|
||||||
let isNoteHandlerKey = (el) => { return keyPressed.has(el) }
|
const isNoteHandlerKey = (el) => { return keyPressed.has(el) }
|
||||||
// These conditions are for ctrl-e and ctrl-w
|
// These conditions are for ctrl-e and ctrl-w
|
||||||
if (keyPressed.size === this.escapeFromEditor.length &&
|
if (keyPressed.size === this.escapeFromEditor.length &&
|
||||||
!this.state.isLocked && this.state.status === 'CODE' &&
|
!this.state.isLocked && this.state.status === 'CODE' &&
|
||||||
@@ -207,14 +223,14 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { className, value, config, storageKey } = this.props
|
const {className, value, config, storageKey, noteKey} = this.props
|
||||||
|
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||||
|
|
||||||
let previewStyle = {}
|
const previewStyle = {}
|
||||||
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
||||||
|
|
||||||
const storage = findStorage(storageKey)
|
const storage = findStorage(storageKey)
|
||||||
@@ -242,7 +258,13 @@ class MarkdownEditor extends React.Component {
|
|||||||
fontSize={editorFontSize}
|
fontSize={editorFontSize}
|
||||||
indentType={config.editor.indentType}
|
indentType={config.editor.indentType}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
|
enableRulers={config.editor.enableRulers}
|
||||||
|
rulers={config.editor.rulers}
|
||||||
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
|
noteKey={noteKey}
|
||||||
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
onBlur={(e) => this.handleBlur(e)}
|
onBlur={(e) => this.handleBlur(e)}
|
||||||
/>
|
/>
|
||||||
@@ -259,8 +281,14 @@ class MarkdownEditor extends React.Component {
|
|||||||
codeBlockFontFamily={config.editor.fontFamily}
|
codeBlockFontFamily={config.editor.fontFamily}
|
||||||
lineNumber={config.preview.lineNumber}
|
lineNumber={config.preview.lineNumber}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
|
scrollPastEnd={config.preview.scrollPastEnd}
|
||||||
|
smartQuotes={config.preview.smartQuotes}
|
||||||
|
smartArrows={config.preview.smartArrows}
|
||||||
|
breaks={config.preview.breaks}
|
||||||
|
sanitize={config.preview.sanitize}
|
||||||
ref='preview'
|
ref='preview'
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
|
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
value={this.state.renderValue}
|
value={this.state.renderValue}
|
||||||
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
||||||
@@ -268,6 +296,9 @@ class MarkdownEditor extends React.Component {
|
|||||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||||
showCopyNotification={config.ui.showCopyNotification}
|
showCopyNotification={config.ui.showCopyNotification}
|
||||||
storagePath={storage.path}
|
storagePath={storage.path}
|
||||||
|
noteKey={noteKey}
|
||||||
|
customCSS={config.preview.customCSS}
|
||||||
|
allowCustomCSS={config.preview.allowCustomCSS}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
289
browser/components/MarkdownPreview.js
Normal file → Executable file
289
browser/components/MarkdownPreview.js
Normal file → Executable file
@@ -1,18 +1,24 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
import markdown from 'browser/lib/markdown'
|
import React from 'react'
|
||||||
|
import Markdown from 'browser/lib/markdown'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
|
import 'codemirror-mode-elixir'
|
||||||
import consts from 'browser/lib/consts'
|
import consts from 'browser/lib/consts'
|
||||||
import Raphael from 'raphael'
|
import Raphael from 'raphael'
|
||||||
import flowchart from 'flowchart'
|
import flowchart from 'flowchart'
|
||||||
import SequenceDiagram from 'js-sequence-diagrams'
|
import SequenceDiagram from 'js-sequence-diagrams'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import fs from 'fs'
|
|
||||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||||
|
import convertModeName from 'browser/lib/convertModeName'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import mdurl from 'mdurl'
|
import mdurl from 'mdurl'
|
||||||
|
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||||
|
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
|
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||||
|
|
||||||
const { app } = remote
|
const { app } = remote
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const dialog = remote.dialog
|
const dialog = remote.dialog
|
||||||
@@ -21,8 +27,12 @@ const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
|||||||
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
||||||
? app.getAppPath()
|
? app.getAppPath()
|
||||||
: path.resolve())
|
: path.resolve())
|
||||||
|
const CSS_FILES = [
|
||||||
|
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||||
|
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
||||||
|
]
|
||||||
|
|
||||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
|
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) {
|
||||||
return `
|
return `
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
@@ -42,18 +52,31 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Icons';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Material Icons'),
|
||||||
|
local('MaterialIcons-Regular'),
|
||||||
|
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
|
||||||
|
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||||
|
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
${allowCustomCSS ? customCSS : ''}
|
||||||
${markdownStyle}
|
${markdownStyle}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: '${fontFamily.join("','")}';
|
font-family: '${fontFamily.join("','")}';
|
||||||
font-size: ${fontSize}px;
|
font-size: ${fontSize}px;
|
||||||
|
${scrollPastEnd && 'padding-bottom: 90vh;'}
|
||||||
}
|
}
|
||||||
code {
|
code {
|
||||||
font-family: ${codeBlockFontFamily.join(', ')};
|
font-family: '${codeBlockFontFamily.join("','")}';
|
||||||
background-color: rgba(0,0,0,0.04);
|
background-color: rgba(0,0,0,0.04);
|
||||||
}
|
}
|
||||||
.lineNumber {
|
.lineNumber {
|
||||||
${lineNumber && 'display: block !important;'}
|
${lineNumber && 'display: block !important;'}
|
||||||
font-family: ${codeBlockFontFamily.join(', ')};
|
font-family: '${codeBlockFontFamily.join("','")}';
|
||||||
}
|
}
|
||||||
|
|
||||||
.clipboardButton {
|
.clipboardButton {
|
||||||
@@ -93,6 +116,16 @@ h2 {
|
|||||||
body p {
|
body p {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body[data-theme="${theme}"] {
|
||||||
|
color: #000;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.clipboardButton {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +138,6 @@ if (!OSX) {
|
|||||||
defaultFontFamily.unshift('meiryo')
|
defaultFontFamily.unshift('meiryo')
|
||||||
}
|
}
|
||||||
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||||
|
|
||||||
export default class MarkdownPreview extends React.Component {
|
export default class MarkdownPreview extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -113,39 +145,46 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
|
||||||
|
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||||
|
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||||
this.printHandler = () => this.handlePrint()
|
this.printHandler = () => this.handlePrint()
|
||||||
|
|
||||||
this.linkClickHandler = this.handlelinkClick.bind(this)
|
this.linkClickHandler = this.handlelinkClick.bind(this)
|
||||||
|
this.initMarkdown = this.initMarkdown.bind(this)
|
||||||
|
this.initMarkdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePreviewAnchorClick (e) {
|
initMarkdown () {
|
||||||
e.preventDefault()
|
const { smartQuotes, sanitize, breaks } = this.props
|
||||||
e.stopPropagation()
|
this.markdown = new Markdown({
|
||||||
|
typographer: smartQuotes,
|
||||||
let anchor = e.target.closest('a')
|
sanitize,
|
||||||
let href = anchor.getAttribute('href')
|
breaks
|
||||||
if (_.isString(href) && href.match(/^#/)) {
|
})
|
||||||
let targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length))
|
|
||||||
if (targetElement != null) {
|
|
||||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
shell.openExternal(href)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCheckboxClick (e) {
|
handleCheckboxClick (e) {
|
||||||
this.props.onCheckboxClick(e)
|
this.props.onCheckboxClick(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleScroll (e) {
|
||||||
|
if (this.props.onScroll) {
|
||||||
|
this.props.onScroll(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleContextMenu (e) {
|
handleContextMenu (e) {
|
||||||
this.props.onContextMenu(e)
|
this.props.onContextMenu(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDoubleClick (e) {
|
||||||
|
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
handleMouseDown (e) {
|
handleMouseDown (e) {
|
||||||
if (e.target != null) {
|
if (e.target != null) {
|
||||||
switch (e.target.tagName) {
|
switch (e.target.tagName) {
|
||||||
@@ -158,6 +197,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUp (e) {
|
handleMouseUp (e) {
|
||||||
|
if (!this.props.onMouseUp) return
|
||||||
if (e.target != null && e.target.tagName === 'A') {
|
if (e.target != null && e.target.tagName === 'A') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -172,22 +212,72 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.exportAsDocument('md')
|
this.exportAsDocument('md')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSaveAsHtml () {
|
||||||
|
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||||
|
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
||||||
|
|
||||||
|
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
||||||
|
let body = this.markdown.render(escapeHtmlCharacters(noteContent))
|
||||||
|
|
||||||
|
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||||
|
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath)
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
file = file.replace('file://', '')
|
||||||
|
exportTasks.push({
|
||||||
|
src: file,
|
||||||
|
dst: 'css'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
attachmentsAbsolutePaths.forEach((attachment) => {
|
||||||
|
exportTasks.push({
|
||||||
|
src: attachment,
|
||||||
|
dst: attachmentManagement.DESTINATION_FOLDER
|
||||||
|
})
|
||||||
|
})
|
||||||
|
body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey)
|
||||||
|
|
||||||
|
let styles = ''
|
||||||
|
files.forEach((file) => {
|
||||||
|
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||||
|
})
|
||||||
|
|
||||||
|
return `<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
||||||
|
<style id="style">${inlineStyles}</style>
|
||||||
|
${styles}
|
||||||
|
</head>
|
||||||
|
<body>${body}</body>
|
||||||
|
</html>`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handlePrint () {
|
handlePrint () {
|
||||||
this.refs.root.contentWindow.print()
|
this.refs.root.contentWindow.print()
|
||||||
}
|
}
|
||||||
|
|
||||||
exportAsDocument (fileType) {
|
exportAsDocument (fileType, contentFormatter) {
|
||||||
const options = {
|
const options = {
|
||||||
filters: [
|
filters: [
|
||||||
{name: 'Documents', extensions: [fileType]}
|
{name: 'Documents', extensions: [fileType]}
|
||||||
],
|
],
|
||||||
properties: ['openFile', 'createDirectory']
|
properties: ['openFile', 'createDirectory']
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
||||||
(filename) => {
|
(filename) => {
|
||||||
if (filename) {
|
if (filename) {
|
||||||
fs.writeFile(filename, this.props.value, (err) => {
|
const content = this.props.value
|
||||||
if (err) throw err
|
const storage = this.props.storagePath
|
||||||
|
|
||||||
|
exportNote(storage, content, filename, contentFormatter)
|
||||||
|
.then((res) => {
|
||||||
|
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
|
||||||
|
}).catch((err) => {
|
||||||
|
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
|
||||||
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -207,21 +297,29 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||||
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.head.innerHTML = `
|
let styles = `
|
||||||
<style id='style'></style>
|
<style id='style'></style>
|
||||||
<link rel="stylesheet" href="${appPath}/node_modules/katex/dist/katex.min.css">
|
|
||||||
<link rel="stylesheet" href="${appPath}/node_modules/codemirror/lib/codemirror.css">
|
|
||||||
<link rel="stylesheet" id="codeTheme">
|
<link rel="stylesheet" id="codeTheme">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
`
|
`
|
||||||
|
|
||||||
|
CSS_FILES.forEach((file) => {
|
||||||
|
styles += `<link rel="stylesheet" href="${file}">`
|
||||||
|
})
|
||||||
|
|
||||||
|
this.refs.root.contentWindow.document.head.innerHTML = styles
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler)
|
||||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||||
|
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||||
eventEmitter.on('print', this.printHandler)
|
eventEmitter.on('print', this.printHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,53 +327,70 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler)
|
||||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||||
|
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||||
eventEmitter.off('print', this.printHandler)
|
eventEmitter.off('print', this.printHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||||
|
if (prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||||
|
prevProps.sanitize !== this.props.sanitize ||
|
||||||
|
prevProps.smartArrows !== this.props.smartArrows ||
|
||||||
|
prevProps.breaks !== this.props.breaks) {
|
||||||
|
this.initMarkdown()
|
||||||
|
this.rewriteIframe()
|
||||||
|
}
|
||||||
if (prevProps.fontFamily !== this.props.fontFamily ||
|
if (prevProps.fontFamily !== this.props.fontFamily ||
|
||||||
prevProps.fontSize !== this.props.fontSize ||
|
prevProps.fontSize !== this.props.fontSize ||
|
||||||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
||||||
prevProps.lineNumber !== this.props.lineNumber ||
|
prevProps.lineNumber !== this.props.lineNumber ||
|
||||||
prevProps.showCopyNotification !== this.props.showCopyNotification ||
|
prevProps.showCopyNotification !== this.props.showCopyNotification ||
|
||||||
prevProps.theme !== this.props.theme) {
|
prevProps.theme !== this.props.theme ||
|
||||||
|
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
|
||||||
|
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
|
||||||
|
prevProps.customCSS !== this.props.customCSS) {
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStyle () {
|
getStyleParams () {
|
||||||
let { fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props
|
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS } = this.props
|
||||||
|
let { fontFamily, codeBlockFontFamily } = this.props
|
||||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||||
? [fontFamily].concat(defaultFontFamily)
|
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
||||||
: defaultFontFamily
|
: defaultFontFamily
|
||||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||||
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
|
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
||||||
: defaultCodeBlockFontFamily
|
: defaultCodeBlockFontFamily
|
||||||
|
|
||||||
this.setCodeTheme(codeBlockTheme)
|
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS}
|
||||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCodeTheme (theme) {
|
applyStyle () {
|
||||||
|
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
||||||
|
|
||||||
|
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
|
||||||
|
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
||||||
|
}
|
||||||
|
|
||||||
|
GetCodeThemeLink (theme) {
|
||||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
||||||
? theme
|
? theme
|
||||||
: 'elegant'
|
: 'elegant'
|
||||||
this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized')
|
return theme.startsWith('solarized')
|
||||||
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
? `${appPath}/node_modules/codemirror/theme/solarized.css`
|
||||||
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
|
||||||
}
|
}
|
||||||
|
|
||||||
rewriteIframe () {
|
rewriteIframe () {
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
|
||||||
el.removeEventListener('click', this.anchorClickHandler)
|
|
||||||
})
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||||
el.removeEventListener('click', this.checkboxClickHandler)
|
el.removeEventListener('click', this.checkboxClickHandler)
|
||||||
})
|
})
|
||||||
@@ -284,7 +399,8 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
el.removeEventListener('click', this.linkClickHandler)
|
el.removeEventListener('click', this.linkClickHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
let { value, theme, indentSize, codeBlockTheme, showCopyNotification, storagePath } = this.props
|
const { theme, indentSize, showCopyNotification, storagePath, noteKey } = this.props
|
||||||
|
let { value, codeBlockTheme } = this.props
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||||
|
|
||||||
@@ -294,40 +410,28 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
|
let renderedHTML = this.markdown.render(value)
|
||||||
|
attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey)
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => {
|
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)
|
||||||
el.parentNode.parentNode.style.listStyleType = 'none'
|
|
||||||
})
|
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
|
||||||
this.fixDecodedURI(el)
|
|
||||||
el.addEventListener('click', this.anchorClickHandler)
|
|
||||||
})
|
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||||
el.addEventListener('click', this.checkboxClickHandler)
|
el.addEventListener('click', this.checkboxClickHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||||
|
this.fixDecodedURI(el)
|
||||||
el.addEventListener('click', this.linkClickHandler)
|
el.addEventListener('click', this.linkClickHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
|
|
||||||
el.src = markdown.normalizeLinkText(el.src)
|
|
||||||
if (!/\/:storage/.test(el.src)) return
|
|
||||||
el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
|
|
||||||
})
|
|
||||||
|
|
||||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
||||||
? codeBlockTheme
|
? codeBlockTheme
|
||||||
: 'default'
|
: 'default'
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
|
||||||
let syntax = CodeMirror.findModeByName(el.className)
|
let syntax = CodeMirror.findModeByName(convertModeName(el.className))
|
||||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||||
CodeMirror.requireMode(syntax.mode, () => {
|
CodeMirror.requireMode(syntax.mode, () => {
|
||||||
let content = htmlTextHelper.decodeEntities(el.innerHTML)
|
const content = htmlTextHelper.decodeEntities(el.innerHTML)
|
||||||
const copyIcon = document.createElement('i')
|
const copyIcon = document.createElement('i')
|
||||||
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||||
copyIcon.onclick = (e) => {
|
copyIcon.onclick = (e) => {
|
||||||
@@ -343,16 +447,16 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
el.innerHTML = ''
|
el.innerHTML = ''
|
||||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||||
const [refThema, color] = codeBlockTheme.split(' ')
|
const [refThema, color] = codeBlockTheme.split(' ')
|
||||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror`
|
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||||
} else {
|
} else {
|
||||||
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror`
|
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||||
}
|
}
|
||||||
CodeMirror.runMode(content, syntax.mime, el, {
|
CodeMirror.runMode(content, syntax.mime, el, {
|
||||||
tabSize: indentSize
|
tabSize: indentSize
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
let opts = {}
|
const opts = {}
|
||||||
// if (this.props.theme === 'dark') {
|
// if (this.props.theme === 'dark') {
|
||||||
// opts['font-color'] = '#DDD'
|
// opts['font-color'] = '#DDD'
|
||||||
// opts['line-color'] = '#DDD'
|
// opts['line-color'] = '#DDD'
|
||||||
@@ -362,11 +466,11 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => {
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => {
|
||||||
Raphael.setWindow(this.getWindow())
|
Raphael.setWindow(this.getWindow())
|
||||||
try {
|
try {
|
||||||
let diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
const diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
||||||
el.innerHTML = ''
|
el.innerHTML = ''
|
||||||
diagram.drawSVG(el, opts)
|
diagram.drawSVG(el, opts)
|
||||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||||
el.addEventListener('click', this.anchorClickHandler)
|
el.addEventListener('click', this.linkClickHandler)
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@@ -378,11 +482,11 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => {
|
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => {
|
||||||
Raphael.setWindow(this.getWindow())
|
Raphael.setWindow(this.getWindow())
|
||||||
try {
|
try {
|
||||||
let diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
const diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
||||||
el.innerHTML = ''
|
el.innerHTML = ''
|
||||||
diagram.drawSVG(el, {theme: 'simple'})
|
diagram.drawSVG(el, {theme: 'simple'})
|
||||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||||
el.addEventListener('click', this.anchorClickHandler)
|
el.addEventListener('click', this.linkClickHandler)
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@@ -401,11 +505,11 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollTo (targetRow) {
|
scrollTo (targetRow) {
|
||||||
let blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
|
const blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
|
||||||
|
|
||||||
for (let index = 0; index < blocks.length; index++) {
|
for (let index = 0; index < blocks.length; index++) {
|
||||||
let block = blocks[index]
|
let block = blocks[index]
|
||||||
let row = parseInt(block.getAttribute('data-line'))
|
const row = parseInt(block.getAttribute('data-line'))
|
||||||
if (row > targetRow || index === blocks.length - 1) {
|
if (row > targetRow || index === blocks.length - 1) {
|
||||||
block = blocks[index - 1]
|
block = blocks[index - 1]
|
||||||
block != null && this.getWindow().scrollTo(0, block.offsetTop)
|
block != null && this.getWindow().scrollTo(0, block.offsetTop)
|
||||||
@@ -427,15 +531,48 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlelinkClick (e) {
|
handlelinkClick (e) {
|
||||||
const noteHash = e.target.href.split('/').pop()
|
e.preventDefault()
|
||||||
const regexIsNoteLink = /^(.{20})-(.{20})$/
|
e.stopPropagation()
|
||||||
if (regexIsNoteLink.test(noteHash)) {
|
|
||||||
eventEmitter.emit('list:jump', noteHash)
|
const href = e.target.href
|
||||||
|
const linkHash = href.split('/').pop()
|
||||||
|
|
||||||
|
const regexNoteInternalLink = /main.html#(.+)/
|
||||||
|
if (regexNoteInternalLink.test(linkHash)) {
|
||||||
|
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
|
||||||
|
const targetElement = this.refs.root.contentWindow.document.getElementById(targetId)
|
||||||
|
|
||||||
|
if (targetElement != null) {
|
||||||
|
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will match the new uuid v4 hash and the old hash
|
||||||
|
// e.g.
|
||||||
|
// :note:1c211eb7dcb463de6490 and
|
||||||
|
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
|
||||||
|
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
|
||||||
|
if (regexIsNoteLink.test(linkHash)) {
|
||||||
|
eventEmitter.emit('list:jump', linkHash.replace(':note:', ''))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will match the old link format storage.key-note.key
|
||||||
|
// e.g.
|
||||||
|
// 877f99c3268608328037-1c211eb7dcb463de6490
|
||||||
|
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
|
||||||
|
if (regexIsLegacyNoteLink.test(linkHash)) {
|
||||||
|
eventEmitter.emit('list:jump', linkHash.split('-')[1])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// other case
|
||||||
|
shell.openExternal(href)
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { className, style, tabIndex } = this.props
|
const { className, style, tabIndex } = this.props
|
||||||
return (
|
return (
|
||||||
<iframe className={className != null
|
<iframe className={className != null
|
||||||
? 'MarkdownPreview ' + className
|
? 'MarkdownPreview ' + className
|
||||||
@@ -454,8 +591,12 @@ MarkdownPreview.propTypes = {
|
|||||||
onDoubleClick: PropTypes.func,
|
onDoubleClick: PropTypes.func,
|
||||||
onMouseUp: PropTypes.func,
|
onMouseUp: PropTypes.func,
|
||||||
onMouseDown: PropTypes.func,
|
onMouseDown: PropTypes.func,
|
||||||
|
onContextMenu: PropTypes.func,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
showCopyNotification: PropTypes.bool,
|
showCopyNotification: PropTypes.bool,
|
||||||
storagePath: PropTypes.string
|
storagePath: PropTypes.string,
|
||||||
|
smartQuotes: PropTypes.bool,
|
||||||
|
smartArrows: PropTypes.bool,
|
||||||
|
breaks: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|||||||
200
browser/components/MarkdownSplitEditor.js
Normal file
200
browser/components/MarkdownSplitEditor.js
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
class MarkdownSplitEditor extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
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 () {
|
||||||
|
this.value = this.refs.code.value
|
||||||
|
this.props.onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll (e) {
|
||||||
|
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 = /\[ \]/
|
||||||
|
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
|
||||||
|
.split('\n')
|
||||||
|
|
||||||
|
const targetLine = lines[lineIndex]
|
||||||
|
|
||||||
|
if (targetLine.match(checkedMatch)) {
|
||||||
|
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||||
|
}
|
||||||
|
if (targetLine.match(uncheckedMatch)) {
|
||||||
|
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[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, noteKey} = 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 = {}
|
||||||
|
previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%'
|
||||||
|
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none'
|
||||||
|
return (
|
||||||
|
<div styleName='root' ref='root'
|
||||||
|
onMouseMove={e => this.handleMouseMove(e)}
|
||||||
|
onMouseUp={e => this.handleMouseUp(e)}>
|
||||||
|
<CodeEditor
|
||||||
|
styleName='codeEditor'
|
||||||
|
ref='code'
|
||||||
|
width={this.state.codeEditorWidthInPercent + '%'}
|
||||||
|
mode='GitHub Flavored Markdown'
|
||||||
|
value={value}
|
||||||
|
theme={config.editor.theme}
|
||||||
|
keyMap={config.editor.keyMap}
|
||||||
|
fontFamily={config.editor.fontFamily}
|
||||||
|
fontSize={editorFontSize}
|
||||||
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
|
indentType={config.editor.indentType}
|
||||||
|
indentSize={editorIndentSize}
|
||||||
|
enableRulers={config.editor.enableRulers}
|
||||||
|
rulers={config.editor.rulers}
|
||||||
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
|
storageKey={storageKey}
|
||||||
|
noteKey={noteKey}
|
||||||
|
onChange={this.handleOnChange.bind(this)}
|
||||||
|
onScroll={this.handleScroll.bind(this)}
|
||||||
|
/>
|
||||||
|
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||||
|
<div styleName='slider-hitbox' />
|
||||||
|
</div>
|
||||||
|
<MarkdownPreview
|
||||||
|
style={previewStyle}
|
||||||
|
styleName='preview'
|
||||||
|
theme={config.ui.theme}
|
||||||
|
keyMap={config.editor.keyMap}
|
||||||
|
fontSize={config.preview.fontSize}
|
||||||
|
fontFamily={config.preview.fontFamily}
|
||||||
|
codeBlockTheme={config.preview.codeBlockTheme}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(MarkdownSplitEditor, styles)
|
||||||
16
browser/components/MarkdownSplitEditor.styl
Normal file
16
browser/components/MarkdownSplitEditor.styl
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.root
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
font-size 30px
|
||||||
|
display flex
|
||||||
|
.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
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, {PropTypes} from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './ModalEscButton.styl'
|
import styles from './ModalEscButton.styl'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Micro component for toggle SideNav
|
* @fileoverview Micro component for toggle SideNav
|
||||||
*/
|
*/
|
||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import styles from './NavToggleButton.styl'
|
import styles from './NavToggleButton.styl'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
width 34px
|
width 34px
|
||||||
line-height 32px
|
line-height 32px
|
||||||
padding 0
|
padding 0
|
||||||
|
&:hover
|
||||||
|
border: 1px solid #1EC38B;
|
||||||
|
background-color: alpha(#1EC38B, 30%)
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
navWhiteButtonColor()
|
navWhiteButtonColor()
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Note item component.
|
* @fileoverview Note item component.
|
||||||
*/
|
*/
|
||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import { isArray } from 'lodash'
|
import { isArray } from 'lodash'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
||||||
import styles from './NoteItem.styl'
|
import styles from './NoteItem.styl'
|
||||||
import TodoProcess from './TodoProcess'
|
import TodoProcess from './TodoProcess'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Tag element component.
|
* @description Tag element component.
|
||||||
@@ -45,14 +47,25 @@ const TagElementList = (tags) => {
|
|||||||
* @param {Function} handleDragStart
|
* @param {Function} handleDragStart
|
||||||
* @param {string} dateDisplay
|
* @param {string} dateDisplay
|
||||||
*/
|
*/
|
||||||
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
|
const NoteItem = ({
|
||||||
|
isActive,
|
||||||
|
note,
|
||||||
|
dateDisplay,
|
||||||
|
handleNoteClick,
|
||||||
|
handleNoteContextMenu,
|
||||||
|
handleDragStart,
|
||||||
|
pathname,
|
||||||
|
storageName,
|
||||||
|
folderName,
|
||||||
|
viewType
|
||||||
|
}) => (
|
||||||
<div styleName={isActive
|
<div styleName={isActive
|
||||||
? 'item--active'
|
? 'item--active'
|
||||||
: 'item'
|
: 'item'
|
||||||
}
|
}
|
||||||
key={`${note.storage}-${note.key}`}
|
key={note.key}
|
||||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
onClick={e => handleNoteClick(e, note.key)}
|
||||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||||
onDragStart={e => handleDragStart(e, note)}
|
onDragStart={e => handleDragStart(e, note)}
|
||||||
draggable='true'
|
draggable='true'
|
||||||
>
|
>
|
||||||
@@ -64,27 +77,37 @@ const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteCont
|
|||||||
<div styleName='item-title'>
|
<div styleName='item-title'>
|
||||||
{note.title.trim().length > 0
|
{note.title.trim().length > 0
|
||||||
? note.title
|
? note.title
|
||||||
: <span styleName='item-title-empty'>Empty</span>
|
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{['ALL', 'STORAGE'].includes(viewType) && <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-time'>{dateDisplay}</div>
|
<div styleName='item-bottom'>
|
||||||
|
<div styleName='item-bottom-tagList'>
|
||||||
|
{note.tags.length > 0
|
||||||
|
? TagElementList(note.tags)
|
||||||
|
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>{i18n.__('No tags')}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
{note.isStarred
|
{note.isStarred
|
||||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||||
}
|
}
|
||||||
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
|
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||||
}
|
}
|
||||||
{note.type === 'MARKDOWN_NOTE'
|
{note.type === 'MARKDOWN_NOTE'
|
||||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
<div styleName='item-bottom'>
|
|
||||||
<div styleName='item-bottom-tagList'>
|
|
||||||
{note.tags.length > 0
|
|
||||||
? TagElementList(note.tags)
|
|
||||||
: <span styleName='item-bottom-tagList-empty' />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +124,11 @@ NoteItem.propTypes = {
|
|||||||
title: PropTypes.string.isrequired,
|
title: PropTypes.string.isrequired,
|
||||||
tags: PropTypes.array,
|
tags: PropTypes.array,
|
||||||
isStarred: PropTypes.bool.isRequired,
|
isStarred: PropTypes.bool.isRequired,
|
||||||
isTrashed: PropTypes.bool.isRequired
|
isTrashed: PropTypes.bool.isRequired,
|
||||||
|
blog: {
|
||||||
|
blogLink: PropTypes.string,
|
||||||
|
blogId: PropTypes.number
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
handleNoteClick: PropTypes.func.isRequired,
|
handleNoteClick: PropTypes.func.isRequired,
|
||||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ $control-height = 30px
|
|||||||
position relative
|
position relative
|
||||||
font-size 12px
|
font-size 12px
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
|
top 2px
|
||||||
|
|
||||||
.item-title
|
.item-title
|
||||||
font-size 15px
|
font-size 15px
|
||||||
@@ -80,8 +81,8 @@ $control-height = 30px
|
|||||||
position relative
|
position relative
|
||||||
top -12px
|
top -12px
|
||||||
left 20px
|
left 20px
|
||||||
padding-right 15px
|
padding 0px 15px 0px 0px
|
||||||
padding-bottom 4px
|
margin-bottom 4px
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
@@ -89,6 +90,26 @@ $control-height = 30px
|
|||||||
font-weight normal
|
font-weight normal
|
||||||
color $ui-inactive-text-color
|
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
|
.item-bottom
|
||||||
position relative
|
position relative
|
||||||
bottom 0px
|
bottom 0px
|
||||||
@@ -96,7 +117,7 @@ $control-height = 30px
|
|||||||
font-size 12px
|
font-size 12px
|
||||||
line-height 20px
|
line-height 20px
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
display flex
|
display block
|
||||||
|
|
||||||
.item-bottom-tagList
|
.item-bottom-tagList
|
||||||
flex 1
|
flex 1
|
||||||
@@ -124,10 +145,8 @@ $control-height = 30px
|
|||||||
|
|
||||||
.item-star
|
.item-star
|
||||||
position absolute
|
position absolute
|
||||||
right -6px
|
right 2px
|
||||||
bottom 23px
|
top 5px
|
||||||
width 16px
|
|
||||||
height 16px
|
|
||||||
color alpha($ui-favorite-star-button-color, 60%)
|
color alpha($ui-favorite-star-button-color, 60%)
|
||||||
font-size 12px
|
font-size 12px
|
||||||
padding 0
|
padding 0
|
||||||
@@ -135,10 +154,8 @@ $control-height = 30px
|
|||||||
|
|
||||||
.item-pin
|
.item-pin
|
||||||
position absolute
|
position absolute
|
||||||
right 0px
|
right 25px
|
||||||
bottom 2px
|
top 7px
|
||||||
width 34px
|
|
||||||
height 34px
|
|
||||||
color #E54D42
|
color #E54D42
|
||||||
font-size 14px
|
font-size 14px
|
||||||
padding 0
|
padding 0
|
||||||
@@ -230,3 +247,150 @@ body[data-theme="dark"]
|
|||||||
.item-bottom-tagList-empty
|
.item-bottom-tagList-empty
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.item
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
&:hover
|
||||||
|
transition 0.15s
|
||||||
|
// background-color alpha($ui-solarized-dark-noteList-backgroundColor, 20%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-title
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-time
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 20%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:active
|
||||||
|
transition 0.15s
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-title
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-time
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 10%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.item-wrapper
|
||||||
|
border-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
|
||||||
|
|
||||||
|
.item--active
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
.item-wrapper
|
||||||
|
border-color transparent
|
||||||
|
.item-title
|
||||||
|
.item-title-icon
|
||||||
|
.item-bottom-time
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-bottom-tagList-item
|
||||||
|
background-color alpha(white, 10%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
// background-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
|
||||||
|
color #c0392b
|
||||||
|
.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="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-text-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 #c0392b
|
||||||
|
.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
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Note item component with simple display mode.
|
* @fileoverview Note item component with simple display mode.
|
||||||
*/
|
*/
|
||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './NoteItemSimple.styl'
|
import styles from './NoteItemSimple.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Note item component when using simple display mode.
|
* @description Note item component when using simple display mode.
|
||||||
@@ -13,14 +15,23 @@ import styles from './NoteItemSimple.styl'
|
|||||||
* @param {Function} handleNoteContextMenu
|
* @param {Function} handleNoteContextMenu
|
||||||
* @param {Function} handleDragStart
|
* @param {Function} handleDragStart
|
||||||
*/
|
*/
|
||||||
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu, handleDragStart }) => (
|
const NoteItemSimple = ({
|
||||||
|
isActive,
|
||||||
|
isAllNotesView,
|
||||||
|
note,
|
||||||
|
handleNoteClick,
|
||||||
|
handleNoteContextMenu,
|
||||||
|
handleDragStart,
|
||||||
|
pathname,
|
||||||
|
storage
|
||||||
|
}) => (
|
||||||
<div styleName={isActive
|
<div styleName={isActive
|
||||||
? 'item-simple--active'
|
? 'item-simple--active'
|
||||||
: 'item-simple'
|
: 'item-simple'
|
||||||
}
|
}
|
||||||
key={`${note.storage}-${note.key}`}
|
key={note.key}
|
||||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
onClick={e => handleNoteClick(e, note.key)}
|
||||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||||
onDragStart={e => handleDragStart(e, note)}
|
onDragStart={e => handleDragStart(e, note)}
|
||||||
draggable='true'
|
draggable='true'
|
||||||
>
|
>
|
||||||
@@ -29,10 +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-code' />
|
||||||
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
|
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
|
||||||
}
|
}
|
||||||
|
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||||
|
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||||
|
: ''
|
||||||
|
}
|
||||||
{note.title.trim().length > 0
|
{note.title.trim().length > 0
|
||||||
? note.title
|
? 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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ $control-height = 30px
|
|||||||
.item-simple-title
|
.item-simple-title
|
||||||
font-size 13px
|
font-size 13px
|
||||||
height 40px
|
height 40px
|
||||||
|
padding-right 20px
|
||||||
box-sizing border-box
|
box-sizing border-box
|
||||||
line-height 24px
|
line-height 24px
|
||||||
padding-top 8px
|
padding-top 8px
|
||||||
@@ -67,6 +68,15 @@ $control-height = 30px
|
|||||||
font-weight normal
|
font-weight normal
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.item-pin
|
||||||
|
position absolute
|
||||||
|
right 0px
|
||||||
|
top 12px
|
||||||
|
color #E54D42
|
||||||
|
font-size 14px
|
||||||
|
padding 0
|
||||||
|
border-radius 17px
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.item-simple
|
.item-simple
|
||||||
background-color $ui-white-noteList-backgroundColor
|
background-color $ui-white-noteList-backgroundColor
|
||||||
@@ -94,6 +104,7 @@ body[data-theme="dark"]
|
|||||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
.item-simple-title
|
.item-simple-title
|
||||||
|
.item-simple-title-empty
|
||||||
.item-simple-title-icon
|
.item-simple-title-icon
|
||||||
.item-simple-bottom-time
|
.item-simple-bottom-time
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
@@ -107,6 +118,7 @@ body[data-theme="dark"]
|
|||||||
background-color $ui-dark-button--active-backgroundColor
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
.item-simple-title
|
.item-simple-title
|
||||||
|
.item-simple-title-empty
|
||||||
.item-simple-title-icon
|
.item-simple-title-icon
|
||||||
.item-simple-bottom-time
|
.item-simple-bottom-time
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
@@ -122,6 +134,7 @@ body[data-theme="dark"]
|
|||||||
.item-simple-wrapper
|
.item-simple-wrapper
|
||||||
border-color transparent
|
border-color transparent
|
||||||
.item-simple-title
|
.item-simple-title
|
||||||
|
.item-simple-title-empty
|
||||||
.item-simple-title-icon
|
.item-simple-title-icon
|
||||||
.item-simple-bottom-time
|
.item-simple-bottom-time
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
@@ -143,3 +156,133 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.item-simple-title-empty
|
.item-simple-title-empty
|
||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.item-simple
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
&:hover
|
||||||
|
transition 0.15s
|
||||||
|
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
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-simple-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha(#fff, 20%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:active
|
||||||
|
transition 0.15s
|
||||||
|
// 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
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.item-simple-bottom-tagList-item
|
||||||
|
transition 0.15s
|
||||||
|
background-color alpha(white, 10%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.item-simple--active
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
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
|
||||||
|
background-color alpha(white, 10%)
|
||||||
|
color $ui-solarized-dark-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="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-solarized-dark-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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './RealtimeNotification.styl'
|
import styles from './RealtimeNotification.styl'
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
.notification-area
|
.notification-area
|
||||||
z-index 1000
|
z-index 1000
|
||||||
font-size 12px
|
font-size 12px
|
||||||
position absolute
|
position: relative
|
||||||
bottom 20px
|
top: 12px
|
||||||
width 100%
|
|
||||||
float left
|
|
||||||
height 30px
|
|
||||||
background-color none
|
background-color none
|
||||||
|
|
||||||
.notification-link
|
.notification-link
|
||||||
@@ -32,3 +29,26 @@ body[data-theme="dark"]
|
|||||||
transition 0.2s
|
transition 0.2s
|
||||||
&:hover
|
&:hover
|
||||||
color #5CB85C
|
color #5CB85C
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.notification-area
|
||||||
|
background-color none
|
||||||
|
|
||||||
|
.notification-link
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
border none
|
||||||
|
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
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Filter for all notes.
|
* @fileoverview Filter for all notes.
|
||||||
*/
|
*/
|
||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './SideNavFilter.styl'
|
import styles from './SideNavFilter.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} isFolded
|
* @param {boolean} isFolded
|
||||||
@@ -16,7 +18,7 @@ import styles from './SideNavFilter.styl'
|
|||||||
const SideNavFilter = ({
|
const SideNavFilter = ({
|
||||||
isFolded, isHomeActive, handleAllNotesButtonClick,
|
isFolded, isHomeActive, handleAllNotesButtonClick,
|
||||||
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
|
||||||
counterTotalNote, counterStarredNote
|
counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ const SideNavFilter = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span styleName='menu-button-label'>All Notes</span>
|
<span styleName='menu-button-label'>{i18n.__('All Notes')}</span>
|
||||||
<span styleName='counters'>{counterTotalNote}</span>
|
<span styleName='counters'>{counterTotalNote}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -40,25 +42,25 @@ const SideNavFilter = ({
|
|||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isStarredActive
|
<img src={isStarredActive
|
||||||
? '../resources/icon/icon-star-active.svg'
|
? '../resources/icon/icon-star-active.svg'
|
||||||
: '../resources/icon/icon-star.svg'
|
: '../resources/icon/icon-star-sidenav.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span styleName='menu-button-label'>Starred</span>
|
<span styleName='menu-button-label'>{i18n.__('Starred')}</span>
|
||||||
<span styleName='counters'>{counterStarredNote}</span>
|
<span styleName='counters'>{counterStarredNote}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
|
<button styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
|
||||||
onClick={handleTrashedButtonClick}
|
onClick={handleTrashedButtonClick} onContextMenu={handleFilterButtonContextMenu}
|
||||||
>
|
>
|
||||||
<div styleName='iconWrap'>
|
<div styleName='iconWrap'>
|
||||||
<img src={isTrashedActive
|
<img src={isTrashedActive
|
||||||
? '../resources/icon/icon-trash-active.svg'
|
? '../resources/icon/icon-trash-active.svg'
|
||||||
: '../resources/icon/icon-trash.svg'
|
: '../resources/icon/icon-trash-sidenav.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span styleName='menu-button-label'>Trash</span>
|
<span styleName='menu-button-label'>{i18n.__('Trash')}</span>
|
||||||
<span styleName='counters'>{counterDelNote}</span>
|
<span styleName='counters'>{counterDelNote}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -55,19 +55,22 @@
|
|||||||
|
|
||||||
.menu--folded
|
.menu--folded
|
||||||
@extend .menu
|
@extend .menu
|
||||||
.menu-button, .menu-button--active
|
.menu-button, .menu-button--active, .menu-button-star--active, .menu-button-trash--active
|
||||||
text-align center
|
text-align center
|
||||||
|
padding 0 12px
|
||||||
&:hover .menu-button-label
|
&:hover .menu-button-label
|
||||||
transition opacity 0.15s
|
transition opacity 0.15s
|
||||||
opacity 1
|
opacity 1
|
||||||
|
color $ui-tooltip-text-color
|
||||||
|
background-color $ui-tooltip-backgroundColor
|
||||||
|
|
||||||
|
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
position fixed
|
position fixed
|
||||||
display inline-block
|
display inline-block
|
||||||
height 32px
|
height 36px
|
||||||
left 44px
|
left 44px
|
||||||
padding 0 10px
|
padding 0 10px
|
||||||
margin-top -8px
|
|
||||||
margin-left 0
|
margin-left 0
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
z-index 10
|
z-index 10
|
||||||
@@ -88,7 +91,6 @@ body[data-theme="white"]
|
|||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
.menu-button--active
|
.menu-button--active
|
||||||
@extend .menu-button
|
|
||||||
color #e74c3c
|
color #e74c3c
|
||||||
background-color $ui-button--active-backgroundColor
|
background-color $ui-button--active-backgroundColor
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
@@ -105,7 +107,6 @@ body[data-theme="white"]
|
|||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
|
|
||||||
.menu-button-star--active
|
.menu-button-star--active
|
||||||
@extend .menu-button
|
|
||||||
color #F9BF3B
|
color #F9BF3B
|
||||||
background-color $ui-button--active-backgroundColor
|
background-color $ui-button--active-backgroundColor
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
@@ -122,7 +123,6 @@ body[data-theme="white"]
|
|||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
|
|
||||||
.menu-button-trash--active
|
.menu-button-trash--active
|
||||||
@extend .menu-button
|
|
||||||
color #5D9E36
|
color #5D9E36
|
||||||
background-color $ui-button--active-backgroundColor
|
background-color $ui-button--active-backgroundColor
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
@@ -179,3 +179,88 @@ body[data-theme="dark"]
|
|||||||
color #5D9E36
|
color #5D9E36
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.menu-button
|
||||||
|
&:active
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.menu-button--active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.menu-button-star--active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.menu-button-trash--active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
.menu-button-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
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
|
||||||
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './SnippetTab.styl'
|
import styles from './SnippetTab.styl'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class SnippetTab extends React.Component {
|
class SnippetTab extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -28,7 +29,7 @@ class SnippetTab extends React.Component {
|
|||||||
handleContextMenu (e) {
|
handleContextMenu (e) {
|
||||||
context.popup([
|
context.popup([
|
||||||
{
|
{
|
||||||
label: 'Rename',
|
label: i18n.__('Rename'),
|
||||||
click: (e) => this.handleRenameClick(e)
|
click: (e) => this.handleRenameClick(e)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -54,10 +55,10 @@ class SnippetTab extends React.Component {
|
|||||||
this.handleRename()
|
this.handleRename()
|
||||||
break
|
break
|
||||||
case 27:
|
case 27:
|
||||||
this.setState({
|
this.setState((prevState, props) => ({
|
||||||
name: this.props.snippet.name,
|
name: props.snippet.name,
|
||||||
isRenaming: false
|
isRenaming: false
|
||||||
})
|
}))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,8 +86,17 @@ class SnippetTab extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDragStart (e) {
|
||||||
|
e.dataTransfer.dropEffect = 'move'
|
||||||
|
this.props.onDragStart(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrop (e) {
|
||||||
|
this.props.onDrop(e)
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { isActive, snippet, isDeletable } = this.props
|
const { isActive, snippet, isDeletable } = this.props
|
||||||
return (
|
return (
|
||||||
<div styleName={isActive
|
<div styleName={isActive
|
||||||
? 'root--active'
|
? 'root--active'
|
||||||
@@ -98,11 +108,14 @@ class SnippetTab extends React.Component {
|
|||||||
onClick={(e) => this.handleClick(e)}
|
onClick={(e) => this.handleClick(e)}
|
||||||
onDoubleClick={(e) => this.handleRenameClick(e)}
|
onDoubleClick={(e) => this.handleRenameClick(e)}
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
|
onDragStart={(e) => this.handleDragStart(e)}
|
||||||
|
onDrop={(e) => this.handleDrop(e)}
|
||||||
|
draggable='true'
|
||||||
>
|
>
|
||||||
{snippet.name.trim().length > 0
|
{snippet.name.trim().length > 0
|
||||||
? snippet.name
|
? snippet.name
|
||||||
: <span styleName='button-unnamed'>
|
: <span styleName='button-unnamed'>
|
||||||
Unnamed
|
{i18n.__('Unnamed')}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
@@ -127,6 +140,7 @@ class SnippetTab extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SnippetTab.propTypes = {
|
SnippetTab.propTypes = {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(SnippetTab, styles)
|
export default CSSModules(SnippetTab, styles)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.root
|
.root
|
||||||
position relative
|
position relative
|
||||||
flex 1
|
flex 1
|
||||||
|
min-width 70px
|
||||||
overflow hidden
|
overflow hidden
|
||||||
&:hover
|
&:hover
|
||||||
.deleteButton
|
.deleteButton
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
height 29px
|
height 29px
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
text-align left
|
text-align left
|
||||||
padding-right 30px
|
padding-right 23px
|
||||||
border none
|
border none
|
||||||
background-color transparent
|
background-color transparent
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
text-align center
|
text-align center
|
||||||
border none
|
border none
|
||||||
padding 0
|
padding 0
|
||||||
color transparent
|
color $ui-inactive-text-color
|
||||||
background-color transparent
|
background-color transparent
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
|
|
||||||
@@ -89,3 +90,50 @@ body[data-theme="dark"]
|
|||||||
.input
|
.input
|
||||||
background-color $ui-dark-button--hover-backgroundColor
|
background-color $ui-dark-button--hover-backgroundColor
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
.deleteButton
|
||||||
|
color alpha($ui-dark-text-color, 30%)
|
||||||
|
|
||||||
|
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
|
||||||
|
.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
|
||||||
|
|
||||||
|
.button
|
||||||
|
border none
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
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
|
||||||
|
|
||||||
|
.deleteButton
|
||||||
|
color alpha($ui-solarized-dark-text-color, 30%)
|
||||||
@@ -1,10 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Micro component for showing storage.
|
* @fileoverview Micro component for showing storage.
|
||||||
*/
|
*/
|
||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import styles from './StorageItem.styl'
|
import styles from './StorageItem.styl'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import { isNumber } from 'lodash'
|
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
|
* @param {boolean} isActive
|
||||||
@@ -20,34 +33,54 @@ import { isNumber } from 'lodash'
|
|||||||
* @return {React.Component}
|
* @return {React.Component}
|
||||||
*/
|
*/
|
||||||
const StorageItem = ({
|
const StorageItem = ({
|
||||||
isActive, handleButtonClick, handleContextMenu, folderName,
|
styles,
|
||||||
folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave
|
isActive,
|
||||||
}) => (
|
handleButtonClick,
|
||||||
<button styleName={isActive
|
handleContextMenu,
|
||||||
? 'folderList-item--active'
|
folderName,
|
||||||
: 'folderList-item'
|
folderColor,
|
||||||
}
|
isFolded,
|
||||||
|
noteCount,
|
||||||
|
handleDrop,
|
||||||
|
handleDragEnter,
|
||||||
|
handleDragLeave
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
>
|
>
|
||||||
<span styleName={isFolded
|
{!isFolded && (
|
||||||
? 'folderList-item-name--folded' : 'folderList-item-name'
|
<DraggableIcon className={styles['folderList-item-reorder']} />
|
||||||
}>
|
)}
|
||||||
<text style={{color: folderColor, paddingRight: '10px'}}>{isActive ? <i className='fa fa-folder-open-o' /> : <i className='fa fa-folder-o' />}</text>{isFolded ? folderName.substring(0, 1) : folderName}
|
<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>
|
</span>
|
||||||
{(!isFolded && isNumber(noteCount)) &&
|
{!isFolded &&
|
||||||
|
_.isNumber(noteCount) && (
|
||||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||||
}
|
)}
|
||||||
{isFolded &&
|
{isFolded && (
|
||||||
<span styleName='folderList-item-tooltip'>
|
<span styleName='folderList-item-tooltip'>{folderName}</span>
|
||||||
{folderName}
|
)}
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
StorageItem.propTypes = {
|
StorageItem.propTypes = {
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
border none
|
border none
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
font-size 14px
|
font-size 14px
|
||||||
|
align-items: center
|
||||||
&:first-child
|
&:first-child
|
||||||
margin-top 0
|
margin-top 0
|
||||||
&:hover
|
&:hover
|
||||||
@@ -34,9 +35,7 @@
|
|||||||
.folderList-item-name
|
.folderList-item-name
|
||||||
display block
|
display block
|
||||||
flex 1
|
flex 1
|
||||||
padding 0 12px
|
padding-right: 10px
|
||||||
height 26px
|
|
||||||
line-height 26px
|
|
||||||
border-width 0 0 0 2px
|
border-width 0 0 0 2px
|
||||||
border-style solid
|
border-style solid
|
||||||
border-color transparent
|
border-color transparent
|
||||||
@@ -59,8 +58,8 @@
|
|||||||
opacity 0
|
opacity 0
|
||||||
border-top-right-radius 2px
|
border-top-right-radius 2px
|
||||||
border-bottom-right-radius 2px
|
border-bottom-right-radius 2px
|
||||||
height 26px
|
height 34px
|
||||||
line-height 26px
|
line-height 32px
|
||||||
|
|
||||||
.folderList-item:hover, .folderList-item--active:hover
|
.folderList-item:hover, .folderList-item--active:hover
|
||||||
.folderList-item-tooltip
|
.folderList-item-tooltip
|
||||||
@@ -68,9 +67,20 @@
|
|||||||
|
|
||||||
.folderList-item-name--folded
|
.folderList-item-name--folded
|
||||||
@extend .folderList-item-name
|
@extend .folderList-item-name
|
||||||
padding-left 17px
|
padding-left 7px
|
||||||
text
|
.folderList-item-icon
|
||||||
display none
|
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"]
|
body[data-theme="white"]
|
||||||
.folderList-item
|
.folderList-item
|
||||||
@@ -109,3 +119,41 @@ body[data-theme="dark"]
|
|||||||
&:hover
|
&:hover
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.folderList-item
|
||||||
|
&:hover
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
|
||||||
|
.folderList-item--active
|
||||||
|
@extend .folderList-item
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
&:active
|
||||||
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
|
&: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
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Micro component for showing StorageList
|
* @fileoverview Micro component for showing StorageList
|
||||||
*/
|
*/
|
||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import styles from './StorageList.styl'
|
import styles from './StorageList.styl'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
|
||||||
@@ -9,8 +10,8 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
* @param {Array} storgaeList
|
* @param {Array} storgaeList
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const StorageList = ({storageList}) => (
|
const StorageList = ({storageList, isFolded}) => (
|
||||||
<div styleName='storageList'>
|
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
|
||||||
{storageList.length > 0 ? storageList : (
|
{storageList.length > 0 ? storageList : (
|
||||||
<div styleName='storgaeList-empty'>No storage mount.</div>
|
<div styleName='storgaeList-empty'>No storage mount.</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
top 180px
|
top 180px
|
||||||
overflow-y auto
|
overflow-y auto
|
||||||
|
|
||||||
|
.storageList-folded
|
||||||
|
@extend .storageList
|
||||||
|
width 44px
|
||||||
|
|
||||||
.storageList-empty
|
.storageList-empty
|
||||||
padding 0 10px
|
padding 0 10px
|
||||||
margin-top 15px
|
margin-top 15px
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Micro component for showing TagList.
|
* @fileoverview Micro component for showing TagList.
|
||||||
*/
|
*/
|
||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import styles from './TagListItem.styl'
|
import styles from './TagListItem.styl'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {Function} handleClickTagListItem
|
* @param {Function} handleClickTagListItem
|
||||||
|
* @param {Function} handleClickNarrowToTag
|
||||||
* @param {bool} isActive
|
* @param {bool} isActive
|
||||||
|
* @param {bool} isRelated
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TagListItem = ({name, handleClickTagListItem, isActive}) => (
|
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
|
||||||
|
<div styleName='tagList-itemContainer'>
|
||||||
|
{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)}>
|
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||||
<span styleName='tagList-item-name'>
|
<span styleName='tagList-item-name'>
|
||||||
{`# ${name}`}
|
{`# ${name}`}
|
||||||
|
<span styleName='tagList-item-count'>{count}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
TagListItem.propTypes = {
|
TagListItem.propTypes = {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
.tagList-itemContainer
|
||||||
|
display flex
|
||||||
|
|
||||||
.tagList-item
|
.tagList-item
|
||||||
display flex
|
display flex
|
||||||
|
flex 1
|
||||||
width 100%
|
width 100%
|
||||||
height 26px
|
height 26px
|
||||||
background-color transparent
|
background-color transparent
|
||||||
@@ -20,9 +24,16 @@
|
|||||||
color $ui-button-default-color
|
color $ui-button-default-color
|
||||||
background-color $ui-button-default--active-backgroundColor
|
background-color $ui-button-default--active-backgroundColor
|
||||||
|
|
||||||
|
.tagList-itemNarrow
|
||||||
|
composes tagList-item
|
||||||
|
flex none
|
||||||
|
width 20px
|
||||||
|
padding 0 4px
|
||||||
|
|
||||||
.tagList-item-active
|
.tagList-item-active
|
||||||
background-color $ui-button-default--active-backgroundColor
|
background-color $ui-button-default--active-backgroundColor
|
||||||
display flex
|
display flex
|
||||||
|
flex 1
|
||||||
width 100%
|
width 100%
|
||||||
height 26px
|
height 26px
|
||||||
padding 0
|
padding 0
|
||||||
@@ -36,10 +47,16 @@
|
|||||||
background-color alpha($ui-button-default--active-backgroundColor, 60%)
|
background-color alpha($ui-button-default--active-backgroundColor, 60%)
|
||||||
transition 0.2s
|
transition 0.2s
|
||||||
|
|
||||||
|
.tagList-itemNarrow-active
|
||||||
|
composes tagList-item-active
|
||||||
|
flex none
|
||||||
|
width 20px
|
||||||
|
padding 0 4px
|
||||||
|
|
||||||
.tagList-item-name
|
.tagList-item-name
|
||||||
display block
|
display block
|
||||||
flex 1
|
flex 1
|
||||||
padding 0 15px
|
padding 0 8px 0 4px
|
||||||
height 26px
|
height 26px
|
||||||
line-height 26px
|
line-height 26px
|
||||||
border-width 0 0 0 2px
|
border-width 0 0 0 2px
|
||||||
@@ -48,6 +65,12 @@
|
|||||||
overflow hidden
|
overflow hidden
|
||||||
text-overflow ellipsis
|
text-overflow ellipsis
|
||||||
|
|
||||||
|
.tagList-item-count
|
||||||
|
float right
|
||||||
|
line-height 26px
|
||||||
|
padding-right 15px
|
||||||
|
font-size 13px
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.tagList-item
|
.tagList-item
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
@@ -63,6 +86,8 @@ body[data-theme="white"]
|
|||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
&:hover
|
&:hover
|
||||||
background-color alpha($ui-button--active-backgroundColor, 60%)
|
background-color alpha($ui-button--active-backgroundColor, 60%)
|
||||||
|
.tagList-item-count
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.tagList-item
|
.tagList-item
|
||||||
@@ -82,3 +107,5 @@ body[data-theme="dark"]
|
|||||||
&:hover
|
&:hover
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
|
||||||
|
.tagList-item-count
|
||||||
|
color $ui-dark-button--active-color
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
* @fileoverview Percentage of todo achievement.
|
* @fileoverview Percentage of todo achievement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TodoListPercentage.styl'
|
import styles from './TodoListPercentage.styl'
|
||||||
|
|
||||||
@@ -15,9 +16,11 @@ const TodoListPercentage = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
||||||
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
||||||
|
<div styleName='progressBarInner'>
|
||||||
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
TodoListPercentage.propTypes = {
|
TodoListPercentage.propTypes = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.percentageBar
|
.percentageBar
|
||||||
position absolute
|
position absolute
|
||||||
top 50px
|
top 72px
|
||||||
right 0px
|
right 0px
|
||||||
left 0px
|
left 0px
|
||||||
background-color #DADFE1
|
background-color #DADFE1
|
||||||
@@ -16,17 +16,46 @@
|
|||||||
border-radius 2px
|
border-radius 2px
|
||||||
transition 0.4s cubic-bezier(0.4, 0.4, 0, 1)
|
transition 0.4s cubic-bezier(0.4, 0.4, 0, 1)
|
||||||
|
|
||||||
|
.progressBarInner
|
||||||
|
padding 0 10px
|
||||||
|
min-width 1px
|
||||||
|
height 100%
|
||||||
|
display -webkit-box
|
||||||
|
display box
|
||||||
|
justify-content center
|
||||||
|
align-items center
|
||||||
|
|
||||||
|
|
||||||
.percentageText
|
.percentageText
|
||||||
color #f4f4f4
|
color #f4f4f4
|
||||||
padding: 2px 43%
|
|
||||||
font-weight 600
|
font-weight 600
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.percentageBar
|
.percentageBar
|
||||||
background-color #363A3D
|
background-color #444444
|
||||||
|
|
||||||
.progressBar
|
.progressBar
|
||||||
background-color: alpha(#939395, 50%)
|
background-color: #1EC38B
|
||||||
|
|
||||||
.percentageText
|
.percentageText
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.percentageBar
|
||||||
|
background-color #002b36
|
||||||
|
|
||||||
|
.progressBar
|
||||||
|
background-color: #2aa198
|
||||||
|
|
||||||
|
.percentageText
|
||||||
|
color #fdf6e3
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.percentageBar
|
||||||
|
background-color #f92672
|
||||||
|
|
||||||
|
.progressBar
|
||||||
|
background-color: #373831
|
||||||
|
|
||||||
|
.percentageText
|
||||||
|
color #fdf6e3
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
* @fileoverview Percentage of todo achievement.
|
* @fileoverview Percentage of todo achievement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TodoProcess.styl'
|
import styles from './TodoProcess.styl'
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ body
|
|||||||
.katex
|
.katex
|
||||||
font 400 1.2em 'KaTeX_Main'
|
font 400 1.2em 'KaTeX_Main'
|
||||||
line-height 1.2em
|
line-height 1.2em
|
||||||
white-space nowrap
|
white-space initial
|
||||||
text-indent 0
|
text-indent 0
|
||||||
.katex .mfrac>.vlist>span:nth-child(2)
|
.katex .mfrac>.vlist>span:nth-child(2)
|
||||||
top 0 !important
|
top 0 !important
|
||||||
@@ -76,7 +76,10 @@ body
|
|||||||
justify-content left
|
justify-content left
|
||||||
li
|
li
|
||||||
label.taskListItem
|
label.taskListItem
|
||||||
margin-left -2em
|
margin-left -1.8em
|
||||||
|
&.checked
|
||||||
|
text-decoration line-through
|
||||||
|
opacity 0.5
|
||||||
div.math-rendered
|
div.math-rendered
|
||||||
text-align center
|
text-align center
|
||||||
.math-failed
|
.math-failed
|
||||||
@@ -102,7 +105,6 @@ a
|
|||||||
border-radius 5px
|
border-radius 5px
|
||||||
margin -5px
|
margin -5px
|
||||||
transition .1s
|
transition .1s
|
||||||
display inline-block
|
|
||||||
img
|
img
|
||||||
vertical-align sub
|
vertical-align sub
|
||||||
&:hover
|
&:hover
|
||||||
@@ -117,6 +119,7 @@ hr
|
|||||||
margin 15px 0
|
margin 15px 0
|
||||||
h1, h2, h3, h4, h5, h6
|
h1, h2, h3, h4, h5, h6
|
||||||
font-weight bold
|
font-weight bold
|
||||||
|
word-wrap break-word
|
||||||
h1
|
h1
|
||||||
font-size 2.55em
|
font-size 2.55em
|
||||||
padding-bottom 0.3em
|
padding-bottom 0.3em
|
||||||
@@ -154,6 +157,7 @@ p
|
|||||||
line-height 1.6em
|
line-height 1.6em
|
||||||
margin 0 0 1em
|
margin 0 0 1em
|
||||||
white-space pre-line
|
white-space pre-line
|
||||||
|
word-wrap break-word
|
||||||
img
|
img
|
||||||
max-width 100%
|
max-width 100%
|
||||||
strong, b
|
strong, b
|
||||||
@@ -174,6 +178,8 @@ ul
|
|||||||
margin-bottom 1em
|
margin-bottom 1em
|
||||||
li
|
li
|
||||||
display list-item
|
display list-item
|
||||||
|
&.taskListItem
|
||||||
|
list-style none
|
||||||
p
|
p
|
||||||
margin 0
|
margin 0
|
||||||
&>li>ul, &>li>ol
|
&>li>ul, &>li>ol
|
||||||
@@ -193,7 +199,6 @@ ol
|
|||||||
&>li>ul, &>li>ol
|
&>li>ul, &>li>ol
|
||||||
margin 0
|
margin 0
|
||||||
code
|
code
|
||||||
color #CC305F
|
|
||||||
padding 0.2em 0.4em
|
padding 0.2em 0.4em
|
||||||
background-color #f7f7f7
|
background-color #f7f7f7
|
||||||
border-radius 3px
|
border-radius 3px
|
||||||
@@ -214,6 +219,7 @@ pre
|
|||||||
background-color white
|
background-color white
|
||||||
&.CodeMirror
|
&.CodeMirror
|
||||||
height initial
|
height initial
|
||||||
|
flex-wrap wrap
|
||||||
&>code
|
&>code
|
||||||
flex 1
|
flex 1
|
||||||
overflow-x auto
|
overflow-x auto
|
||||||
@@ -223,6 +229,13 @@ pre
|
|||||||
padding 0
|
padding 0
|
||||||
border none
|
border none
|
||||||
border-radius 0
|
border-radius 0
|
||||||
|
&>span.filename
|
||||||
|
width 100%
|
||||||
|
border-radius: 5px 0px 0px 0px
|
||||||
|
margin -8px 100% 8px -8px
|
||||||
|
padding 0px 6px
|
||||||
|
background-color #777;
|
||||||
|
color white
|
||||||
&>span.lineNumber
|
&>span.lineNumber
|
||||||
display none
|
display none
|
||||||
font-size 1em
|
font-size 1em
|
||||||
@@ -280,6 +293,82 @@ kbd
|
|||||||
line-height 1
|
line-height 1
|
||||||
padding 3px 5px
|
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-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: {border-color: #448aff, title-color: rgba(68,138,255,.1), icon: "note"},
|
||||||
|
hint: {border-color: #00bfa5, title-color: rgba(0,191,165,.1), icon: "info"},
|
||||||
|
danger: {border-color: #ff1744, title-color: rgba(255,23,68,.1), icon: "block"},
|
||||||
|
caution: {border-color: #ff9100, title-color: rgba(255,145,0,.1), icon: "warning"},
|
||||||
|
error: {border-color: #ff1744, title-color: rgba(255,23,68,.1), icon: "error"},
|
||||||
|
attention: {border-color: #64dd17, title-color: rgba(100,221,23,.1), icon: "priority_high"}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, val in admonition_types
|
||||||
|
.admonition.{name}
|
||||||
|
@extend $admonition
|
||||||
|
border-left-color: val[border-color]
|
||||||
|
|
||||||
|
.admonition.{name}>.admonition-title
|
||||||
|
@extend $admonition-title
|
||||||
|
border-bottom-color: .1rem solid val[title-color]
|
||||||
|
background-color: val[title-color]
|
||||||
|
|
||||||
|
.admonition.{name}>.admonition-title:before
|
||||||
|
@extend $admonition-icon
|
||||||
|
color: val[border-color]
|
||||||
|
content: val[icon]
|
||||||
|
|
||||||
themeDarkBackground = darken(#21252B, 10%)
|
themeDarkBackground = darken(#21252B, 10%)
|
||||||
themeDarkText = #f9f9f9
|
themeDarkText = #f9f9f9
|
||||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||||
@@ -330,3 +419,59 @@ body[data-theme="dark"]
|
|||||||
kbd
|
kbd
|
||||||
background-color themeDarkBorder
|
background-color themeDarkBorder
|
||||||
color themeDarkText
|
color themeDarkText
|
||||||
|
|
||||||
|
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
||||||
|
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
|
||||||
|
themeSolarizedDarkTableBorder = themeDarkBorder
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
border-color themeDarkBorder
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
table
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
background-color themeSolarizedDarkTableHead
|
||||||
|
th
|
||||||
|
border-color themeSolarizedDarkTableBorder
|
||||||
|
&:last-child
|
||||||
|
border-right solid 1px themeSolarizedDarkTableBorder
|
||||||
|
tbody
|
||||||
|
tr:nth-child(2n + 1)
|
||||||
|
background-color themeSolarizedDarkTableOdd
|
||||||
|
tr:nth-child(2n)
|
||||||
|
background-color themeSolarizedDarkTableEven
|
||||||
|
td
|
||||||
|
border-color themeSolarizedDarkTableBorder
|
||||||
|
&:last-child
|
||||||
|
border-right solid 1px themeSolarizedDarkTableBorder
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,118 +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
|
|
||||||
@@ -1,209 +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 { 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 () {
|
|
||||||
let { 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 () {
|
|
||||||
let { note } = this.props
|
|
||||||
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
|
||||||
this.setState({
|
|
||||||
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveToClipboard () {
|
|
||||||
let { 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 () {
|
|
||||||
let { 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') {
|
|
||||||
let tabList = note.snippets.map((snippet, index) => {
|
|
||||||
let 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>
|
|
||||||
})
|
|
||||||
|
|
||||||
let viewList = note.snippets.map((snippet, index) => {
|
|
||||||
let 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}
|
|
||||||
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,98 +0,0 @@
|
|||||||
@import('../main/Detail/DetailVars.styl')
|
|
||||||
|
|
||||||
.root
|
|
||||||
absolute top bottom left right
|
|
||||||
bottom 30px
|
|
||||||
left $note-detail-left-margin
|
|
||||||
right $note-detail-right-margin
|
|
||||||
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
|
|
||||||
@@ -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 () {
|
|
||||||
let { index } = this.props
|
|
||||||
|
|
||||||
if (index > -1) {
|
|
||||||
let list = this.refs.root
|
|
||||||
let item = list.childNodes[index]
|
|
||||||
if (item == null) return null
|
|
||||||
|
|
||||||
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
|
||||||
if (overflowBelow) {
|
|
||||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
|
||||||
}
|
|
||||||
let overflowAbove = list.scrollTop > item.offsetTop
|
|
||||||
if (overflowAbove) {
|
|
||||||
list.scrollTop = item.offsetTop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetScroll () {
|
|
||||||
this.refs.root.scrollTop = 0
|
|
||||||
this.setState({
|
|
||||||
range: 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScroll (e) {
|
|
||||||
let { 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 () {
|
|
||||||
let { notes, index } = this.props
|
|
||||||
|
|
||||||
let 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) {
|
|
||||||
let { storage } = this.props
|
|
||||||
this.props.handleStorageButtonClick(e, storage.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFolderClick (e, folder) {
|
|
||||||
let { storage } = this.props
|
|
||||||
this.props.handleFolderButtonClick(e, storage.key, folder.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let { storage, filter } = this.props
|
|
||||||
let 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,356 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import { connect, Provider } from 'react-redux'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import ipc from './ipcClient'
|
|
||||||
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')
|
|
||||||
|
|
||||||
const electron = require('electron')
|
|
||||||
const { remote } = electron
|
|
||||||
const { Menu } = remote
|
|
||||||
|
|
||||||
function hideFinder () {
|
|
||||||
let 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 || e.metaKey) {
|
|
||||||
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) {
|
|
||||||
let { filter } = this.state
|
|
||||||
filter.includeSnippet = e.target.checked
|
|
||||||
this.setState({
|
|
||||||
filter: filter,
|
|
||||||
index: 0
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOnlyMarkdownCheckboxChange (e) {
|
|
||||||
let { filter } = this.state
|
|
||||||
filter.includeMarkdown = e.target.checked
|
|
||||||
this.refs.list.resetScroll()
|
|
||||||
this.setState({
|
|
||||||
filter: filter,
|
|
||||||
index: 0
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAllNotesButtonClick (e) {
|
|
||||||
let { filter } = this.state
|
|
||||||
filter.type = 'ALL'
|
|
||||||
this.refs.list.resetScroll()
|
|
||||||
this.setState({
|
|
||||||
filter,
|
|
||||||
index: 0
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleStarredButtonClick (e) {
|
|
||||||
let { filter } = this.state
|
|
||||||
filter.type = 'STARRED'
|
|
||||||
this.refs.list.resetScroll()
|
|
||||||
this.setState({
|
|
||||||
filter,
|
|
||||||
index: 0
|
|
||||||
}, () => {
|
|
||||||
this.refs.search.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleStorageButtonClick (e, storage) {
|
|
||||||
let { 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) {
|
|
||||||
let { 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 () {
|
|
||||||
let { data, config } = this.props
|
|
||||||
let { filter, search } = this.state
|
|
||||||
let storageList = []
|
|
||||||
for (let key in data.storageMap) {
|
|
||||||
let storage = data.storageMap[key]
|
|
||||||
let 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 (let 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) {
|
|
||||||
let 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))
|
|
||||||
|
|
||||||
let 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,124 +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 () {
|
|
||||||
let 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 () {
|
|
||||||
let 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 {
|
|
||||||
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'
|
|
||||||
|
|
||||||
let 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
|
|
||||||
}
|
|
||||||
|
|
||||||
let reducer = combineReducers({
|
|
||||||
data,
|
|
||||||
config,
|
|
||||||
routing: routerReducer
|
|
||||||
})
|
|
||||||
|
|
||||||
let store = createStore(reducer)
|
|
||||||
|
|
||||||
export default store
|
|
||||||
78
browser/lib/Languages.js
Normal file
78
browser/lib/Languages.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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',
|
||||||
|
locale: 'pt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Russian',
|
||||||
|
locale: 'ru'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Spanish',
|
||||||
|
locale: 'es-ES'
|
||||||
|
}, {
|
||||||
|
name: 'Turkish',
|
||||||
|
locale: 'tr'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getLocales () {
|
||||||
|
return languages.reduce(function (localeList, locale) {
|
||||||
|
localeList.push(locale.locale)
|
||||||
|
return localeList
|
||||||
|
}, [])
|
||||||
|
},
|
||||||
|
getLanguages () {
|
||||||
|
return languages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -38,15 +38,15 @@ class MutableMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
map (cb) {
|
map (cb) {
|
||||||
let result = []
|
const result = []
|
||||||
for (let [key, value] of this._map) {
|
for (const [key, value] of this._map) {
|
||||||
result.push(cb(value, key))
|
result.push(cb(value, key))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
toJS () {
|
toJS () {
|
||||||
let result = {}
|
const result = {}
|
||||||
for (let [key, value] of this._map) {
|
for (let [key, value] of this._map) {
|
||||||
if (value instanceof MutableSet || value instanceof MutableMap) {
|
if (value instanceof MutableSet || value instanceof MutableMap) {
|
||||||
value = value.toJS()
|
value = value.toJS()
|
||||||
@@ -85,7 +85,7 @@ class MutableSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
map (cb) {
|
map (cb) {
|
||||||
let result = []
|
const result = []
|
||||||
this._set.forEach(function (value, key) {
|
this._set.forEach(function (value, key) {
|
||||||
result.push(cb(value, key))
|
result.push(cb(value, key))
|
||||||
})
|
})
|
||||||
|
|||||||
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')
|
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 = {
|
const consts = {
|
||||||
FOLDER_COLORS: [
|
FOLDER_COLORS: [
|
||||||
'#E10051',
|
'#E10051',
|
||||||
@@ -31,7 +35,8 @@ const consts = {
|
|||||||
'Dodger Blue',
|
'Dodger Blue',
|
||||||
'Violet Eggplant'
|
'Violet Eggplant'
|
||||||
],
|
],
|
||||||
THEMES: ['default'].concat(themes)
|
THEMES: ['default'].concat(themes),
|
||||||
|
SNIPPET_FILE: snippetFile
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = consts
|
module.exports = consts
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const { remote } = require('electron')
|
|||||||
const { Menu, MenuItem } = remote
|
const { Menu, MenuItem } = remote
|
||||||
|
|
||||||
function popup (templates) {
|
function popup (templates) {
|
||||||
let menu = new Menu()
|
const menu = new Menu()
|
||||||
templates.forEach((item) => {
|
templates.forEach((item) => {
|
||||||
menu.append(new MenuItem(item))
|
menu.append(new MenuItem(item))
|
||||||
})
|
})
|
||||||
|
|||||||
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,3 +1,5 @@
|
|||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
|
import 'codemirror-mode-elixir'
|
||||||
|
|
||||||
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
|
||||||
|
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export function findNoteTitle (value) {
|
export function findNoteTitle (value) {
|
||||||
let splitted = value.split('\n')
|
const splitted = value.split('\n')
|
||||||
let title = null
|
let title = null
|
||||||
let isInsideCodeBlock = false
|
let isInsideCodeBlock = false
|
||||||
|
|
||||||
splitted.some((line, index) => {
|
splitted.some((line, index) => {
|
||||||
let trimmedLine = line.trim()
|
const trimmedLine = line.trim()
|
||||||
let trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
||||||
if (trimmedLine.match('```')) {
|
if (trimmedLine.match('```')) {
|
||||||
isInsideCodeBlock = !isInsideCodeBlock
|
isInsideCodeBlock = !isInsideCodeBlock
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export function getTodoStatus (content) {
|
export function getTodoStatus (content) {
|
||||||
let splitted = content.split('\n')
|
const splitted = content.split('\n')
|
||||||
let numberOfTodo = 0
|
let numberOfTodo = 0
|
||||||
let numberOfCompletedTodo = 0
|
let numberOfCompletedTodo = 0
|
||||||
|
|
||||||
splitted.forEach((line) => {
|
splitted.forEach((line) => {
|
||||||
let trimmedLine = line.trim()
|
const trimmedLine = line.trim()
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[\s|x\] ./)) {
|
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) {
|
||||||
numberOfTodo++
|
numberOfTodo++
|
||||||
}
|
}
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
||||||
|
|||||||
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 crypto = require('crypto')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
|
const uuidv4 = require('uuid/v4')
|
||||||
|
|
||||||
module.exports = function (length) {
|
module.exports = function (uuid) {
|
||||||
if (!_.isFinite(length)) length = 10
|
if (typeof uuid === typeof true && uuid) {
|
||||||
|
return uuidv4()
|
||||||
|
}
|
||||||
|
const length = 10
|
||||||
return crypto.randomBytes(length).toString('hex')
|
return crypto.randomBytes(length).toString('hex')
|
||||||
}
|
}
|
||||||
|
|||||||
23
browser/lib/markdown-it-sanitize-html.js
Normal file
23
browser/lib/markdown-it-sanitize-html.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
|
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 === 'inline') {
|
||||||
|
const inlineTokens = state.tokens[tokenIdx].children
|
||||||
|
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
||||||
|
if (inlineTokens[childIdx].type === 'html_inline') {
|
||||||
|
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,44 +1,116 @@
|
|||||||
import markdownit from 'markdown-it'
|
import markdownit from 'markdown-it'
|
||||||
|
import sanitize from './markdown-it-sanitize-html'
|
||||||
import emoji from 'markdown-it-emoji'
|
import emoji from 'markdown-it-emoji'
|
||||||
import math from '@rokt33r/markdown-it-math'
|
import math from '@rokt33r/markdown-it-math'
|
||||||
|
import smartArrows from 'markdown-it-smartarrows'
|
||||||
import _ from 'lodash'
|
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.
|
function createGutter (str, firstLineNumber) {
|
||||||
const katex = window.katex
|
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||||
|
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
|
||||||
function createGutter (str) {
|
const lines = []
|
||||||
let lc = (str.match(/\n/g) || []).length
|
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
|
||||||
let lines = []
|
|
||||||
for (let i = 1; i <= lc; i++) {
|
|
||||||
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
||||||
}
|
}
|
||||||
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
||||||
}
|
}
|
||||||
|
|
||||||
var md = markdownit({
|
class Markdown {
|
||||||
typographer: true,
|
constructor (options = {}) {
|
||||||
|
const config = ConfigManager.get()
|
||||||
|
const defaultOptions = {
|
||||||
|
typographer: config.preview.smartQuotes,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
html: true,
|
html: true,
|
||||||
xhtmlOut: true,
|
xhtmlOut: true,
|
||||||
breaks: true,
|
breaks: config.preview.breaks,
|
||||||
highlight: function (str, lang) {
|
highlight: function (str, lang) {
|
||||||
if (lang === 'flowchart') {
|
const delimiter = ':'
|
||||||
|
const langInfo = lang.split(delimiter)
|
||||||
|
const langType = langInfo[0]
|
||||||
|
const fileName = langInfo[1] || ''
|
||||||
|
const firstLineNumber = parseInt(langInfo[2], 10)
|
||||||
|
|
||||||
|
if (langType === 'flowchart') {
|
||||||
return `<pre class="flowchart">${str}</pre>`
|
return `<pre class="flowchart">${str}</pre>`
|
||||||
}
|
}
|
||||||
if (lang === 'sequence') {
|
if (langType === 'sequence') {
|
||||||
return `<pre class="sequence">${str}</pre>`
|
return `<pre class="sequence">${str}</pre>`
|
||||||
}
|
}
|
||||||
return '<pre class="code">' +
|
return '<pre class="code CodeMirror">' +
|
||||||
createGutter(str) +
|
'<span class="filename">' + fileName + '</span>' +
|
||||||
'<code class="' + lang + '">' +
|
createGutter(str, firstLineNumber) +
|
||||||
|
'<code class="' + langType + '">' +
|
||||||
str +
|
str +
|
||||||
'</code></pre>'
|
'</code></pre>'
|
||||||
|
},
|
||||||
|
sanitize: 'STRICT'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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']
|
||||||
})
|
})
|
||||||
md.use(emoji, {
|
}
|
||||||
|
|
||||||
|
this.md.use(emoji, {
|
||||||
shortcuts: {}
|
shortcuts: {}
|
||||||
})
|
})
|
||||||
md.use(math, {
|
this.md.use(math, {
|
||||||
|
inlineOpen: config.preview.latexInlineOpen,
|
||||||
|
inlineClose: config.preview.latexInlineClose,
|
||||||
|
blockOpen: config.preview.latexBlockOpen,
|
||||||
|
blockClose: config.preview.latexBlockClose,
|
||||||
inlineRenderer: function (str) {
|
inlineRenderer: function (str) {
|
||||||
let output = ''
|
let output = ''
|
||||||
try {
|
try {
|
||||||
@@ -58,10 +130,10 @@ md.use(math, {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
md.use(require('markdown-it-imsize'))
|
this.md.use(require('markdown-it-imsize'))
|
||||||
md.use(require('markdown-it-footnote'))
|
this.md.use(require('markdown-it-footnote'))
|
||||||
md.use(require('markdown-it-multimd-table'))
|
this.md.use(require('markdown-it-multimd-table'))
|
||||||
md.use(require('markdown-it-named-headers'), {
|
this.md.use(require('markdown-it-named-headers'), {
|
||||||
slugify: (header) => {
|
slugify: (header) => {
|
||||||
return encodeURI(header.trim()
|
return encodeURI(header.trim()
|
||||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||||
@@ -69,15 +141,28 @@ md.use(require('markdown-it-named-headers'), {
|
|||||||
.replace(/\-+$/, '')
|
.replace(/\-+$/, '')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
md.use(require('markdown-it-kbd'))
|
this.md.use(require('markdown-it-kbd'))
|
||||||
md.use(require('markdown-it-plantuml'))
|
this.md.use(require('markdown-it-admonition'))
|
||||||
|
|
||||||
|
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||||
|
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 `${serverAddress}/${zippedCode}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Override task item
|
// 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 content, terminate, i, l, token
|
||||||
let nextLine = startLine + 1
|
let nextLine = startLine + 1
|
||||||
let terminatorRules = state.md.block.ruler.getRules('paragraph')
|
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||||
let endLine = state.lineMax
|
const endLine = state.lineMax
|
||||||
|
|
||||||
// jump line-by-line until empty one or EOF
|
// jump line-by-line until empty one or EOF
|
||||||
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
||||||
@@ -107,9 +192,16 @@ md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
|||||||
token.map = [startLine, state.line]
|
token.map = [startLine, state.line]
|
||||||
|
|
||||||
if (state.parentType === 'list') {
|
if (state.parentType === 'list') {
|
||||||
let match = content.match(/^\[( |x)\] ?(.+)/i)
|
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||||
if (match) {
|
if (match) {
|
||||||
content = `<label class='taskListItem' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
|
||||||
|
if (liToken) {
|
||||||
|
if (!liToken.attrs) {
|
||||||
|
liToken.attrs = []
|
||||||
|
}
|
||||||
|
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>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,9 +215,13 @@ md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (config.preview.smartArrows) {
|
||||||
|
this.md.use(smartArrows)
|
||||||
|
}
|
||||||
|
|
||||||
// Add line number attribute for scrolling
|
// Add line number attribute for scrolling
|
||||||
let originalRender = md.renderer.render
|
const originalRender = this.md.renderer.render
|
||||||
md.renderer.render = function render (tokens, options, env) {
|
this.md.renderer.render = (tokens, options, env) => {
|
||||||
tokens.forEach((token) => {
|
tokens.forEach((token) => {
|
||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
case 'heading_open':
|
case 'heading_open':
|
||||||
@@ -135,23 +231,18 @@ md.renderer.render = function render (tokens, options, env) {
|
|||||||
token.attrPush(['data-line', token.map[0]])
|
token.attrPush(['data-line', token.map[0]])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
let result = originalRender.call(md.renderer, tokens, options, env)
|
const result = originalRender.call(this.md.renderer, tokens, options, env)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
// FIXME We should not depend on global variable.
|
// FIXME We should not depend on global variable.
|
||||||
window.md = md
|
window.md = this.md
|
||||||
|
|
||||||
function normalizeLinkText (linkText) {
|
|
||||||
return md.normalizeLinkText(linkText)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const markdown = {
|
render (content) {
|
||||||
render: function markdown (content) {
|
|
||||||
if (!_.isString(content)) content = ''
|
if (!_.isString(content)) content = ''
|
||||||
const renderedContent = md.render(content)
|
return this.md.render(content)
|
||||||
return renderedContent
|
}
|
||||||
},
|
|
||||||
normalizeLinkText
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default markdown
|
export default Markdown
|
||||||
|
|
||||||
|
|||||||
0
browser/lib/markdown2.js
Normal file
0
browser/lib/markdown2.js
Normal file
@@ -4,39 +4,28 @@ export default function searchFromNotes (notes, search) {
|
|||||||
if (search.trim().length === 0) return []
|
if (search.trim().length === 0) return []
|
||||||
const searchBlocks = search.split(' ').filter(block => { return block !== '' })
|
const searchBlocks = search.split(' ').filter(block => { return block !== '' })
|
||||||
|
|
||||||
let foundNotes = findByWord(notes, searchBlocks[0])
|
let foundNotes = notes
|
||||||
searchBlocks.forEach((block) => {
|
searchBlocks.forEach((block) => {
|
||||||
foundNotes = findByWord(foundNotes, block)
|
foundNotes = findByWordOrTag(foundNotes, block)
|
||||||
if (block.match(/^#.+/)) {
|
|
||||||
foundNotes = foundNotes.concat(findByTag(notes, block))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return foundNotes
|
return foundNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
function findByTag (notes, block) {
|
function findByWordOrTag (notes, block) {
|
||||||
const tag = block.match(/#(.+)/)[1]
|
let tag = block
|
||||||
let regExp = new RegExp(_.escapeRegExp(tag), 'i')
|
if (tag.match(/^#.+/)) {
|
||||||
return notes.filter((note) => {
|
tag = tag.match(/#(.+)/)[1]
|
||||||
if (!_.isArray(note.tags)) return false
|
|
||||||
return note.tags.some((_tag) => {
|
|
||||||
return _tag.match(regExp)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||||
function findByWord (notes, block) {
|
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||||
let regExp = new RegExp(_.escapeRegExp(block), 'i')
|
|
||||||
return notes.filter((note) => {
|
return notes.filter((note) => {
|
||||||
if (_.isArray(note.tags) && note.tags.some((_tag) => {
|
if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) {
|
||||||
return _tag.match(regExp)
|
|
||||||
})) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (note.type === 'SNIPPET_NOTE') {
|
if (note.type === 'SNIPPET_NOTE') {
|
||||||
return note.description.match(regExp)
|
return note.description.match(wordRegExp)
|
||||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||||
return note.content.match(regExp)
|
return note.content.match(wordRegExp)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|||||||
78
browser/lib/utils.js
Normal file
78
browser/lib/utils.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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 (text) {
|
||||||
|
const matchHtmlRegExp = /["'&<>]/
|
||||||
|
const str = '' + text
|
||||||
|
const match = matchHtmlRegExp.exec(str)
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
let escape
|
||||||
|
let html = ''
|
||||||
|
let index = 0
|
||||||
|
let lastIndex = 0
|
||||||
|
|
||||||
|
for (index = match.index; index < str.length; index++) {
|
||||||
|
switch (str.charCodeAt(index)) {
|
||||||
|
case 34: // "
|
||||||
|
escape = '"'
|
||||||
|
break
|
||||||
|
case 38: // &
|
||||||
|
escape = '&'
|
||||||
|
break
|
||||||
|
case 39: // '
|
||||||
|
escape = '''
|
||||||
|
break
|
||||||
|
case 60: // <
|
||||||
|
escape = '<'
|
||||||
|
break
|
||||||
|
case 62: // >
|
||||||
|
escape = '>'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex !== index) {
|
||||||
|
html += str.substring(lastIndex, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = index + 1
|
||||||
|
html += escape
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastIndex !== index
|
||||||
|
? html + str.substring(lastIndex, index)
|
||||||
|
: 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
|
||||||
|
}
|
||||||
@@ -20,5 +20,20 @@
|
|||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
background-color $ui-dark-backgroundColor
|
background-color $ui-dark-backgroundColor
|
||||||
|
border-left 1px solid $ui-dark-borderColor
|
||||||
.empty-message
|
.empty-message
|
||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-inactive-text-color
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './FolderSelect.styl'
|
import styles from './FolderSelect.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class FolderSelect extends React.Component {
|
class FolderSelect extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -73,8 +75,8 @@ class FolderSelect extends React.Component {
|
|||||||
case 9:
|
case 9:
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
let tabbable = document.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])')
|
const tabbable = document.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])')
|
||||||
let previousEl = tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
|
const previousEl = tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
|
||||||
if (previousEl != null) previousEl.focus()
|
if (previousEl != null) previousEl.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,9 +91,9 @@ class FolderSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSearchInputChange (e) {
|
handleSearchInputChange (e) {
|
||||||
let { folders } = this.props
|
const { folders } = this.props
|
||||||
let search = this.refs.search.value
|
const search = this.refs.search.value
|
||||||
let optionIndex = search.length > 0
|
const optionIndex = search.length > 0
|
||||||
? _.findIndex(folders, (folder) => {
|
? _.findIndex(folders, (folder) => {
|
||||||
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i'))
|
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i'))
|
||||||
})
|
})
|
||||||
@@ -129,7 +131,7 @@ class FolderSelect extends React.Component {
|
|||||||
|
|
||||||
nextOption () {
|
nextOption () {
|
||||||
let { optionIndex } = this.state
|
let { optionIndex } = this.state
|
||||||
let { folders } = this.props
|
const { folders } = this.props
|
||||||
|
|
||||||
optionIndex++
|
optionIndex++
|
||||||
if (optionIndex >= folders.length) optionIndex = 0
|
if (optionIndex >= folders.length) optionIndex = 0
|
||||||
@@ -140,7 +142,7 @@ class FolderSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
previousOption () {
|
previousOption () {
|
||||||
let { folders } = this.props
|
const { folders } = this.props
|
||||||
let { optionIndex } = this.state
|
let { optionIndex } = this.state
|
||||||
|
|
||||||
optionIndex--
|
optionIndex--
|
||||||
@@ -152,10 +154,10 @@ class FolderSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectOption () {
|
selectOption () {
|
||||||
let { folders } = this.props
|
const { folders } = this.props
|
||||||
let optionIndex = this.state.optionIndex
|
const optionIndex = this.state.optionIndex
|
||||||
|
|
||||||
let folder = folders[optionIndex]
|
const folder = folders[optionIndex]
|
||||||
if (folder != null) {
|
if (folder != null) {
|
||||||
this.setState({
|
this.setState({
|
||||||
status: 'FOCUS'
|
status: 'FOCUS'
|
||||||
@@ -184,10 +186,10 @@ class FolderSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { className, data, value } = this.props
|
const { className, data, value } = this.props
|
||||||
let splitted = value.split('-')
|
const splitted = value.split('-')
|
||||||
let storageKey = splitted.shift()
|
const storageKey = splitted.shift()
|
||||||
let folderKey = splitted.shift()
|
const folderKey = splitted.shift()
|
||||||
let options = []
|
let options = []
|
||||||
data.storageMap.forEach((storage, index) => {
|
data.storageMap.forEach((storage, index) => {
|
||||||
storage.folders.forEach((folder) => {
|
storage.folders.forEach((folder) => {
|
||||||
@@ -198,14 +200,14 @@ class FolderSelect extends React.Component {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
||||||
|
|
||||||
if (this.state.search.trim().length > 0) {
|
if (this.state.search.trim().length > 0) {
|
||||||
let filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
|
const filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
|
||||||
options = options.filter((option) => filter.test(option.folder.name))
|
options = options.filter((option) => filter.test(option.folder.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
let optionList = options
|
const optionList = options
|
||||||
.map((option, index) => {
|
.map((option, index) => {
|
||||||
return (
|
return (
|
||||||
<div styleName={index === this.state.optionIndex
|
<div styleName={index === this.state.optionIndex
|
||||||
@@ -248,7 +250,7 @@ class FolderSelect extends React.Component {
|
|||||||
<input styleName='search-input'
|
<input styleName='search-input'
|
||||||
ref='search'
|
ref='search'
|
||||||
value={this.state.search}
|
value={this.state.search}
|
||||||
placeholder='Folder...'
|
placeholder={i18n.__('Folder...')}
|
||||||
onChange={(e) => this.handleSearchInputChange(e)}
|
onChange={(e) => this.handleSearchInputChange(e)}
|
||||||
onBlur={(e) => this.handleSearchInputBlur(e)}
|
onBlur={(e) => this.handleSearchInputBlur(e)}
|
||||||
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
|
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
|
||||||
|
|||||||
@@ -3,20 +3,14 @@
|
|||||||
border solid 1px transparent
|
border solid 1px transparent
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
|
height 30px
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
user-select none
|
user-select none
|
||||||
margin-right 10px
|
margin-right 10px
|
||||||
&:hover
|
|
||||||
background-color $ui-button--hover-backgroundColor
|
|
||||||
|
|
||||||
.root--search, .root--focus
|
.root--search, .root--focus
|
||||||
@extend .root
|
@extend .root
|
||||||
background-color $ui-noteDetail-backgroundColor = #fff
|
|
||||||
border-color $ui-input--focus-borderColor
|
border-color $ui-input--focus-borderColor
|
||||||
width 154px
|
|
||||||
height 30px
|
|
||||||
&:hover
|
|
||||||
border-color $ui-input--focus-borderColor = #fff
|
|
||||||
|
|
||||||
.idle
|
.idle
|
||||||
position relative
|
position relative
|
||||||
@@ -139,3 +133,29 @@ body[data-theme="dark"]
|
|||||||
color $ui-dark-button--active-color
|
color $ui-dark-button--active-color
|
||||||
.search-optionList-item-name-surfix
|
.search-optionList-item-name-surfix
|
||||||
color $ui-dark-inactive-text-color
|
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
|
||||||
|
|||||||
20
browser/main/Detail/FullscreenButton.js
Normal file
20
browser/main/Detail/FullscreenButton.js
Normal file
@@ -0,0 +1,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 FullscreenButton = ({
|
||||||
|
onClick
|
||||||
|
}) => (
|
||||||
|
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
||||||
|
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
FullscreenButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(FullscreenButton, styles)
|
||||||
22
browser/main/Detail/FullscreenButton.styl
Normal file
22
browser/main/Detail/FullscreenButton.styl
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.control-fullScreenButton
|
||||||
|
top 80px
|
||||||
|
topBarButtonRight()
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 70px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.control-fullScreenButton
|
||||||
|
topBarButtonDark()
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './InfoButton.styl'
|
import styles from './InfoButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoButton = ({
|
const InfoButton = ({
|
||||||
onClick
|
onClick
|
||||||
@@ -9,6 +11,7 @@ const InfoButton = ({
|
|||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img className='infoButton' src='../resources/icon/icon-info.svg' />
|
<img className='infoButton' src='../resources/icon/icon-info.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Info')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
.control-infoButton
|
.control-infoButton
|
||||||
top 10px
|
top 10px
|
||||||
margin-bottom 10px
|
topBarButtonRight()
|
||||||
topBarButtonLight()
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 20px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
.infoButton
|
.infoButton
|
||||||
padding 0px
|
padding 0px
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './InfoPanel.styl'
|
import styles from './InfoPanel.styl'
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoPanel = ({
|
class InfoPanel extends React.Component {
|
||||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, wordCount, letterCount, type, print
|
copyNoteLink () {
|
||||||
}) => (
|
const {noteLink} = this.props
|
||||||
|
this.refs.noteLink.select()
|
||||||
|
copy(noteLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print
|
||||||
|
} = this.props
|
||||||
|
return (
|
||||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
||||||
<div>
|
<div>
|
||||||
<p styleName='modification-date'>{updatedAt}</p>
|
<p styleName='modification-date'>{updatedAt}</p>
|
||||||
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
|
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
@@ -18,11 +30,11 @@ const InfoPanel = ({
|
|||||||
: <div styleName='count-wrap'>
|
: <div styleName='count-wrap'>
|
||||||
<div styleName='count-number'>
|
<div styleName='count-number'>
|
||||||
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
|
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
|
||||||
<p styleName='infoPanel-sub-count'>Words</p>
|
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='count-number'>
|
<div styleName='count-number'>
|
||||||
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
|
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
|
||||||
<p styleName='infoPanel-sub-count'>Letters</p>
|
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -34,44 +46,54 @@ const InfoPanel = ({
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'>{storageName}</p>
|
<p styleName='infoPanel-default'>{storageName}</p>
|
||||||
<p styleName='infoPanel-sub'>STORAGE</p>
|
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'>{folderName}</p>
|
<p styleName='infoPanel-default'>{folderName}</p>
|
||||||
<p styleName='infoPanel-sub'>FOLDER</p>
|
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'>{createdAt}</p>
|
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||||
<p styleName='infoPanel-sub'>CREATION DATE</p>
|
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<input styleName='infoPanel-noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
|
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
|
||||||
<p styleName='infoPanel-sub'>NOTE LINK</p>
|
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
||||||
|
<i className='fa fa-clipboard' />
|
||||||
|
</button>
|
||||||
|
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div id='export-wrap'>
|
<div id='export-wrap'>
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||||
<i className='fa fa-file-code-o fa-fw' />
|
<i className='fa fa-file-code-o' />
|
||||||
<p>.md</p>
|
<p>{i18n.__('.md')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||||
<i className='fa fa-file-text-o fa-fw' />
|
<i className='fa fa-file-text-o' />
|
||||||
<p>.txt</p>
|
<p>{i18n.__('.txt')}</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||||
|
<i className='fa fa-html5' />
|
||||||
|
<p>{i18n.__('.html')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => print(e)}>
|
<button styleName='export--enable' onClick={(e) => print(e)}>
|
||||||
<i className='fa fa-print fa-fw' />
|
<i className='fa fa-print' />
|
||||||
<p>Print</p>
|
<p>{i18n.__('Print')}</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
InfoPanel.propTypes = {
|
InfoPanel.propTypes = {
|
||||||
storageName: PropTypes.string.isRequired,
|
storageName: PropTypes.string.isRequired,
|
||||||
@@ -81,6 +103,7 @@ InfoPanel.propTypes = {
|
|||||||
createdAt: PropTypes.string.isRequired,
|
createdAt: PropTypes.string.isRequired,
|
||||||
exportAsMd: PropTypes.func.isRequired,
|
exportAsMd: PropTypes.func.isRequired,
|
||||||
exportAsTxt: PropTypes.func.isRequired,
|
exportAsTxt: PropTypes.func.isRequired,
|
||||||
|
exportAsHtml: PropTypes.func.isRequired,
|
||||||
wordCount: PropTypes.number,
|
wordCount: PropTypes.number,
|
||||||
letterCount: PropTypes.number,
|
letterCount: PropTypes.number,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
.control-infoButton-panel
|
.control-infoButton-panel
|
||||||
z-index 200
|
z-index 200
|
||||||
margin-top 0px
|
margin-top 0px
|
||||||
right 0
|
right 25px
|
||||||
position absolute
|
position absolute
|
||||||
padding 20px 25px 0 25px
|
padding 20px 25px 0 25px
|
||||||
width 300px
|
width 300px
|
||||||
height 350px
|
|
||||||
overflow auto
|
overflow auto
|
||||||
background-color $ui-noteList-backgroundColor
|
background-color $ui-noteList-backgroundColor
|
||||||
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
|
||||||
@@ -70,15 +69,30 @@
|
|||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
|
|
||||||
.infoPanel-sub
|
.infoPanel-sub
|
||||||
font-size 14px
|
font-size 12px
|
||||||
|
font-weight 600
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
padding-bottom 8px
|
padding-bottom 8px
|
||||||
|
|
||||||
.infoPanel-noteLink
|
.infoPanel-noteLink
|
||||||
padding-right 5px
|
padding-right 5px
|
||||||
width 200px
|
width 210px
|
||||||
height 25px
|
height 25px
|
||||||
margin-bottom 6px
|
margin 6px 0
|
||||||
|
|
||||||
|
.infoPanel-copyButton
|
||||||
|
outline none
|
||||||
|
font-size 16px
|
||||||
|
color #A0A0A0
|
||||||
|
background-color transparent
|
||||||
|
border none
|
||||||
|
margin 0 5px
|
||||||
|
border-radius 5px
|
||||||
|
cursor pointer
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
background-color alpha($ui-button--hover-backgroundColor, 30%)
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
.infoPanel-trash
|
.infoPanel-trash
|
||||||
color #EA4447
|
color #EA4447
|
||||||
@@ -161,3 +175,83 @@ body[data-theme="dark"]
|
|||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-inactive-text-color
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.control-infoButton-panel
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control-infoButton-panel-trash
|
||||||
|
background-color $ui-solarized-ark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.modification-date
|
||||||
|
color $ui-solarized-ark-text-color
|
||||||
|
|
||||||
|
.modification-date-desc
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.infoPanel-defaul-count
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.infoPanel-sub-count
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.infoPanel-default
|
||||||
|
color $ui-solarized-ark-text-color
|
||||||
|
|
||||||
|
.infoPanel-sub
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.infoPanel-noteLink
|
||||||
|
background-color alpha($ui-solarized-dark-borderColor, 20%)
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
[id=export-wrap]
|
||||||
|
button
|
||||||
|
color $ui-dark-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-solarized-dark-borderColor, 20%)
|
||||||
|
color $ui-solarized-ark-text-color
|
||||||
|
p
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,46 +1,53 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './InfoPanel.styl'
|
import styles from './InfoPanel.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoPanelTrashed = ({
|
const InfoPanelTrashed = ({
|
||||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt
|
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
|
||||||
}) => (
|
}) => (
|
||||||
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
||||||
<div>
|
<div>
|
||||||
<p styleName='modification-date'>{updatedAt}</p>
|
<p styleName='modification-date'>{updatedAt}</p>
|
||||||
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
|
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'>{storageName}</p>
|
<p styleName='infoPanel-default'>{storageName}</p>
|
||||||
<p styleName='infoPanel-sub'>STORAGE</p>
|
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
|
<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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p styleName='infoPanel-default'>{createdAt}</p>
|
<p styleName='infoPanel-default'>{createdAt}</p>
|
||||||
<p styleName='infoPanel-sub'>CREATION DATE</p>
|
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id='export-wrap'>
|
<div id='export-wrap'>
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||||
<i className='fa fa-file-code-o fa-fw' />
|
<i className='fa fa-file-code-o' />
|
||||||
<p>.md</p>
|
<p>.md</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||||
<i className='fa fa-file-text-o fa-fw' />
|
<i className='fa fa-file-text-o' />
|
||||||
<p>.txt</p>
|
<p>.txt</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||||
|
<i className='fa fa-html5' />
|
||||||
|
<p>.html</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button styleName='export--unable'>
|
<button styleName='export--unable'>
|
||||||
<i className='fa fa-file-pdf-o fa-fw' />
|
<i className='fa fa-file-pdf-o' />
|
||||||
<p>.pdf</p>
|
<p>.pdf</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,7 +60,8 @@ InfoPanelTrashed.propTypes = {
|
|||||||
updatedAt: PropTypes.string.isRequired,
|
updatedAt: PropTypes.string.isRequired,
|
||||||
createdAt: PropTypes.string.isRequired,
|
createdAt: PropTypes.string.isRequired,
|
||||||
exportAsMd: PropTypes.func.isRequired,
|
exportAsMd: PropTypes.func.isRequired,
|
||||||
exportAsTxt: PropTypes.func.isRequired
|
exportAsTxt: PropTypes.func.isRequired,
|
||||||
|
exportAsHtml: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(InfoPanelTrashed, styles)
|
export default CSSModules(InfoPanelTrashed, styles)
|
||||||
|
|||||||
184
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
184
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
@@ -1,7 +1,9 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './MarkdownNoteDetail.styl'
|
import styles from './MarkdownNoteDetail.styl'
|
||||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||||
|
import MarkdownSplitEditor from 'browser/components/MarkdownSplitEditor'
|
||||||
import TodoListPercentage from 'browser/components/TodoListPercentage'
|
import TodoListPercentage from 'browser/components/TodoListPercentage'
|
||||||
import StarButton from './StarButton'
|
import StarButton from './StarButton'
|
||||||
import TagSelect from './TagSelect'
|
import TagSelect from './TagSelect'
|
||||||
@@ -14,18 +16,19 @@ import StatusBar from '../StatusBar'
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import TrashButton from './TrashButton'
|
import TrashButton from './TrashButton'
|
||||||
|
import FullscreenButton from './FullscreenButton'
|
||||||
|
import RestoreButton from './RestoreButton'
|
||||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||||
import InfoButton from './InfoButton'
|
import InfoButton from './InfoButton'
|
||||||
|
import ToggleModeButton from './ToggleModeButton'
|
||||||
import InfoPanel from './InfoPanel'
|
import InfoPanel from './InfoPanel'
|
||||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||||
import { formatDate } from 'browser/lib/date-formatter'
|
import { formatDate } from 'browser/lib/date-formatter'
|
||||||
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||||
import striptags from 'striptags'
|
import striptags from 'striptags'
|
||||||
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
const electron = require('electron')
|
|
||||||
const { remote } = electron
|
|
||||||
const { Menu, MenuItem, dialog } = remote
|
|
||||||
|
|
||||||
class MarkdownNoteDetail extends React.Component {
|
class MarkdownNoteDetail extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -38,7 +41,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
content: ''
|
content: ''
|
||||||
}, props.note),
|
}, props.note),
|
||||||
isLockButtonShown: false,
|
isLockButtonShown: false,
|
||||||
isLocked: false
|
isLocked: false,
|
||||||
|
editorType: props.config.editor.type
|
||||||
}
|
}
|
||||||
this.dispatchTimer = null
|
this.dispatchTimer = null
|
||||||
|
|
||||||
@@ -51,10 +55,14 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
||||||
|
ee.on('topbar:togglemodebutton', () => {
|
||||||
|
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
||||||
|
this.handleSwitchMode(reversedType)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
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()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
this.setState({
|
this.setState({
|
||||||
note: Object.assign({}, nextProps.note)
|
note: Object.assign({}, nextProps.note)
|
||||||
@@ -66,24 +74,26 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||||
if (this.saveQueue != null) this.saveNow()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUnmount () {
|
handleUpdateTag () {
|
||||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
const { note } = this.state
|
||||||
|
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||||
|
this.updateNote(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange (e) {
|
handleUpdateContent () {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
note.content = this.refs.content.value
|
note.content = this.refs.content.value
|
||||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
|
||||||
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
|
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
|
||||||
note.updatedAt = new Date()
|
this.updateNote(note)
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
updateNote (note) {
|
||||||
note
|
note.updatedAt = new Date()
|
||||||
}, () => {
|
this.setState({note}, () => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -96,7 +106,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveNow () {
|
saveNow () {
|
||||||
let { note, dispatch } = this.props
|
const { note, dispatch } = this.props
|
||||||
clearTimeout(this.saveQueue)
|
clearTimeout(this.saveQueue)
|
||||||
this.saveQueue = null
|
this.saveQueue = null
|
||||||
|
|
||||||
@@ -112,11 +122,11 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFolderChange (e) {
|
handleFolderChange (e) {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
let value = this.refs.folder.value
|
const value = this.refs.folder.value
|
||||||
let splitted = value.split('-')
|
const splitted = value.split('-')
|
||||||
let newStorageKey = splitted.shift()
|
const newStorageKey = splitted.shift()
|
||||||
let newFolderKey = splitted.shift()
|
const newFolderKey = splitted.shift()
|
||||||
|
|
||||||
dataApi
|
dataApi
|
||||||
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
||||||
@@ -125,7 +135,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
isMovingNote: true,
|
isMovingNote: true,
|
||||||
note: Object.assign({}, newNote)
|
note: Object.assign({}, newNote)
|
||||||
}, () => {
|
}, () => {
|
||||||
let { dispatch, location } = this.props
|
const { dispatch, location } = this.props
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'MOVE_NOTE',
|
type: 'MOVE_NOTE',
|
||||||
originNote: note,
|
originNote: note,
|
||||||
@@ -134,7 +144,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
hashHistory.replace({
|
hashHistory.replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: newNote.storage + '-' + newNote.key
|
key: newNote.key
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -145,7 +155,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleStarButtonClick (e) {
|
handleStarButtonClick (e) {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
||||||
|
|
||||||
note.isStarred = !note.isStarred
|
note.isStarred = !note.isStarred
|
||||||
@@ -169,32 +179,34 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ee.emit('export:save-text')
|
ee.emit('export:save-text')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportAsHtml () {
|
||||||
|
ee.emit('export:save-html')
|
||||||
|
}
|
||||||
|
|
||||||
handleTrashButtonClick (e) {
|
handleTrashButtonClick (e) {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
const { isTrashed } = note
|
const { isTrashed } = note
|
||||||
|
const { confirmDeletion } = this.props.config.ui
|
||||||
|
|
||||||
if (isTrashed) {
|
if (isTrashed) {
|
||||||
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
if (confirmDeleteNote(confirmDeletion, true)) {
|
||||||
type: 'warning',
|
const {note, dispatch} = this.props
|
||||||
message: 'Confirm note deletion',
|
|
||||||
detail: 'This will permanently remove this note.',
|
|
||||||
buttons: ['Confirm', 'Cancel']
|
|
||||||
})
|
|
||||||
if (dialogueButtonIndex === 1) return
|
|
||||||
let { note, dispatch } = this.props
|
|
||||||
dataApi
|
dataApi
|
||||||
.deleteNote(note.storage, note.key)
|
.deleteNote(note.storage, note.key)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
let dispatchHandler = () => {
|
const dispatchHandler = () => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'DELETE_NOTE',
|
type: 'DELETE_NOTE',
|
||||||
storageKey: data.storageKey,
|
storageKey: data.storageKey,
|
||||||
noteKey: data.noteKey
|
noteKey: data.noteKey
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ee.once('list:moved', dispatchHandler)
|
ee.once('list:next', dispatchHandler)
|
||||||
})
|
})
|
||||||
|
.then(() => ee.emit('list:next'))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (confirmDeleteNote(confirmDeletion, false)) {
|
||||||
note.isTrashed = true
|
note.isTrashed = true
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -202,12 +214,14 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}, () => {
|
}, () => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
})
|
||||||
}
|
|
||||||
ee.emit('list:next')
|
ee.emit('list:next')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleUndoButtonClick (e) {
|
handleUndoButtonClick (e) {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
note.isTrashed = false
|
note.isTrashed = false
|
||||||
|
|
||||||
@@ -232,7 +246,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getToggleLockButton () {
|
getToggleLockButton () {
|
||||||
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
|
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteKeyDown (e) {
|
handleDeleteKeyDown (e) {
|
||||||
@@ -261,13 +275,48 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ee.emit('print')
|
ee.emit('print')
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
handleSwitchMode (type) {
|
||||||
let { data, config, location } = this.props
|
this.setState({ editorType: type }, () => {
|
||||||
let { note } = this.state
|
const newConfig = Object.assign({}, this.props.config)
|
||||||
let storageKey = note.storage
|
newConfig.editor.type = type
|
||||||
let folderKey = note.folder
|
ConfigManager.set(newConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let options = []
|
renderEditor () {
|
||||||
|
const { config, ignorePreviewPointerEvents } = this.props
|
||||||
|
const { note } = this.state
|
||||||
|
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
||||||
|
return <MarkdownEditor
|
||||||
|
ref='content'
|
||||||
|
styleName='body-noteEditor'
|
||||||
|
config={config}
|
||||||
|
value={note.content}
|
||||||
|
storageKey={note.storage}
|
||||||
|
noteKey={note.key}
|
||||||
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
return <MarkdownSplitEditor
|
||||||
|
ref='content'
|
||||||
|
config={config}
|
||||||
|
value={note.content}
|
||||||
|
storageKey={note.storage}
|
||||||
|
noteKey={note.key}
|
||||||
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { data, location } = this.props
|
||||||
|
const { note, editorType } = this.state
|
||||||
|
const storageKey = note.storage
|
||||||
|
const folderKey = note.folder
|
||||||
|
|
||||||
|
const options = []
|
||||||
data.storageMap.forEach((storage, index) => {
|
data.storageMap.forEach((storage, index) => {
|
||||||
storage.folders.forEach((folder) => {
|
storage.folders.forEach((folder) => {
|
||||||
options.push({
|
options.push({
|
||||||
@@ -276,14 +325,11 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
||||||
|
|
||||||
const trashTopBar = <div styleName='info'>
|
const trashTopBar = <div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<i styleName='undo-button'
|
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||||
className='fa fa-undo fa-fw'
|
|
||||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
@@ -295,6 +341,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
|
exportAsHtml={this.exportAsHtml}
|
||||||
exportAsMd={this.exportAsMd}
|
exportAsMd={this.exportAsMd}
|
||||||
exportAsTxt={this.exportAsTxt}
|
exportAsTxt={this.exportAsTxt}
|
||||||
/>
|
/>
|
||||||
@@ -315,17 +362,12 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
<TagSelect
|
<TagSelect
|
||||||
ref='tags'
|
ref='tags'
|
||||||
value={this.state.note.tags}
|
value={this.state.note.tags}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={this.handleUpdateTag.bind(this)}
|
||||||
/>
|
|
||||||
<TodoListPercentage
|
|
||||||
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
|
|
||||||
/>
|
/>
|
||||||
|
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<InfoButton
|
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StarButton
|
<StarButton
|
||||||
onClick={(e) => this.handleStarButtonClick(e)}
|
onClick={(e) => this.handleStarButtonClick(e)}
|
||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
@@ -339,6 +381,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src={imgSrc} />
|
<img styleName='iconInfo' src={imgSrc} />
|
||||||
|
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -346,22 +389,23 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<button styleName='control-fullScreenButton'
|
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
|
||||||
onMouseDown={(e) => this.handleFullScreenButton(e)}
|
|
||||||
>
|
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
|
|
||||||
|
<InfoButton
|
||||||
|
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||||
|
/>
|
||||||
|
|
||||||
<InfoPanel
|
<InfoPanel
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
noteLink={`[${note.title}](${location.query.key})`}
|
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.exportAsMd}
|
exportAsMd={this.exportAsMd}
|
||||||
exportAsTxt={this.exportAsTxt}
|
exportAsTxt={this.exportAsTxt}
|
||||||
|
exportAsHtml={this.exportAsHtml}
|
||||||
wordCount={note.content.split(' ').length}
|
wordCount={note.content.split(' ').length}
|
||||||
letterCount={note.content.replace(/\r?\n/g, '').length}
|
letterCount={note.content.replace(/\r?\n/g, '').length}
|
||||||
type={note.type}
|
type={note.type}
|
||||||
@@ -379,15 +423,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||||
|
|
||||||
<div styleName='body'>
|
<div styleName='body'>
|
||||||
<MarkdownEditor
|
{this.renderEditor()}
|
||||||
ref='content'
|
|
||||||
styleName='body-noteEditor'
|
|
||||||
config={config}
|
|
||||||
value={this.state.note.content}
|
|
||||||
storageKey={this.state.note.storage}
|
|
||||||
onChange={(e) => this.handleChange(e)}
|
|
||||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusBar
|
<StatusBar
|
||||||
|
|||||||
@@ -7,31 +7,42 @@
|
|||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
box-shadow none
|
box-shadow none
|
||||||
padding 20px 40px
|
padding 20px 40px
|
||||||
|
overflow hidden
|
||||||
|
|
||||||
.lock-button
|
.lock-button
|
||||||
padding-bottom 3px
|
padding-bottom 3px
|
||||||
|
|
||||||
.control-lockButton
|
.control-lockButton
|
||||||
top 160px
|
topBarButtonRight()
|
||||||
margin-bottom 10px
|
position absolute
|
||||||
topBarButtonLight()
|
right 225px
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 35px
|
||||||
|
right -10px
|
||||||
|
width 50px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
.trashed-infopanel
|
.trashed-infopanel
|
||||||
top 40px
|
|
||||||
position relative
|
position relative
|
||||||
|
|
||||||
.control-fullScreenButton
|
|
||||||
top 80px
|
|
||||||
topBarButtonLight()
|
|
||||||
|
|
||||||
.body
|
.body
|
||||||
absolute left right
|
absolute left right
|
||||||
left 0
|
left 0
|
||||||
right 0
|
right 0
|
||||||
top $info-height + $info-margin-under-border
|
top $info-height + $info-margin-under-border
|
||||||
bottom $statusBar-height
|
bottom $statusBar-height
|
||||||
max-width 600px
|
margin 0 30px
|
||||||
margin 0 auto
|
|
||||||
.body-noteEditor
|
.body-noteEditor
|
||||||
absolute top bottom left right
|
absolute top bottom left right
|
||||||
|
|
||||||
@@ -44,7 +55,7 @@ body[data-theme="dark"]
|
|||||||
.root
|
.root
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
box-shadow none
|
box-shadow none
|
||||||
border none
|
border-left 1px solid $ui-dark-borderColor
|
||||||
|
|
||||||
.control-lockButton
|
.control-lockButton
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
@@ -54,3 +65,14 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.control-fullScreenButton
|
.control-fullScreenButton
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@import('DetailVars')
|
@import('DetailVars')
|
||||||
|
|
||||||
$info-height = 50px
|
$info-height = 60px
|
||||||
$info-margin-under-border = 30px
|
$info-margin-under-border = 30px
|
||||||
|
|
||||||
.info
|
.info
|
||||||
@@ -8,11 +8,11 @@ $info-margin-under-border = 30px
|
|||||||
left 0
|
left 0
|
||||||
right 0
|
right 0
|
||||||
height $info-height
|
height $info-height
|
||||||
border-bottom 1px solid #eee
|
|
||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
width 100%
|
width 100%
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
|
padding 0 20px
|
||||||
|
|
||||||
.info-left
|
.info-left
|
||||||
padding 0 10px
|
padding 0 10px
|
||||||
@@ -20,7 +20,6 @@ $info-margin-under-border = 30px
|
|||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
|
|
||||||
|
|
||||||
.info-left-top-folderSelect
|
.info-left-top-folderSelect
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
@@ -45,12 +44,9 @@ $info-margin-under-border = 30px
|
|||||||
color $ui-button--color
|
color $ui-button--color
|
||||||
|
|
||||||
.info-right
|
.info-right
|
||||||
position absolute
|
|
||||||
right 40px
|
|
||||||
top 60px
|
|
||||||
bottom 1px
|
|
||||||
padding-left 30px
|
|
||||||
z-index 101
|
z-index 101
|
||||||
|
display inline-flex
|
||||||
|
margin-top 3px
|
||||||
|
|
||||||
.undo-button
|
.undo-button
|
||||||
width 34px
|
width 34px
|
||||||
@@ -96,3 +92,13 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.undo-button
|
.undo-button
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.info
|
||||||
|
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
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TrashButton.styl'
|
import styles from './TrashButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const PermanentDeleteButton = ({
|
const PermanentDeleteButton = ({
|
||||||
onClick
|
onClick
|
||||||
@@ -9,6 +11,7 @@ const PermanentDeleteButton = ({
|
|||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
||||||
</button>
|
</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()
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './SnippetNoteDetail.styl'
|
import styles from './SnippetNoteDetail.styl'
|
||||||
import CodeEditor from 'browser/components/CodeEditor'
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
@@ -10,34 +11,24 @@ import dataApi from 'browser/main/lib/dataApi'
|
|||||||
import {hashHistory} from 'react-router'
|
import {hashHistory} from 'react-router'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
|
import 'codemirror-mode-elixir'
|
||||||
import SnippetTab from 'browser/components/SnippetTab'
|
import SnippetTab from 'browser/components/SnippetTab'
|
||||||
import StatusBar from '../StatusBar'
|
import StatusBar from '../StatusBar'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
import {findNoteTitle} from 'browser/lib/findNoteTitle'
|
||||||
|
import convertModeName from 'browser/lib/convertModeName'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import TrashButton from './TrashButton'
|
import TrashButton from './TrashButton'
|
||||||
|
import RestoreButton from './RestoreButton'
|
||||||
import PermanentDeleteButton from './PermanentDeleteButton'
|
import PermanentDeleteButton from './PermanentDeleteButton'
|
||||||
import InfoButton from './InfoButton'
|
import InfoButton from './InfoButton'
|
||||||
import InfoPanel from './InfoPanel'
|
import InfoPanel from './InfoPanel'
|
||||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||||
import { formatDate } from 'browser/lib/date-formatter'
|
import { formatDate } from 'browser/lib/date-formatter'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
function pass (name) {
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote } = electron
|
const { remote } = electron
|
||||||
@@ -50,18 +41,36 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
isMovingNote: false,
|
isMovingNote: false,
|
||||||
snippetIndex: 0,
|
snippetIndex: 0,
|
||||||
|
showArrows: false,
|
||||||
|
enableLeftArrow: false,
|
||||||
|
enableRightArrow: false,
|
||||||
note: Object.assign({
|
note: Object.assign({
|
||||||
description: ''
|
description: ''
|
||||||
}, props.note, {
|
}, props.note, {
|
||||||
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.scrollToNextTabThreshold = 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
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()
|
if (this.saveQueue != null) this.saveNow()
|
||||||
let nextNote = Object.assign({
|
const nextNote = Object.assign({
|
||||||
description: ''
|
description: ''
|
||||||
}, nextProps.note, {
|
}, nextProps.note, {
|
||||||
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
|
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||||
@@ -70,11 +79,12 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
snippetIndex: 0,
|
snippetIndex: 0,
|
||||||
note: nextNote
|
note: nextNote
|
||||||
}, () => {
|
}, () => {
|
||||||
let { snippets } = this.state.note
|
const { snippets } = this.state.note
|
||||||
snippets.forEach((snippet, index) => {
|
snippets.forEach((snippet, index) => {
|
||||||
this.refs['code-' + index].reload()
|
this.refs['code-' + index].reload()
|
||||||
})
|
})
|
||||||
if (this.refs.tags) this.refs.tags.reset()
|
if (this.refs.tags) this.refs.tags.reset()
|
||||||
|
this.setState(this.getArrowsState())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +94,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleChange (e) {
|
handleChange (e) {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||||
note.description = this.refs.description.value
|
note.description = this.refs.description.value
|
||||||
@@ -106,7 +116,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveNow () {
|
saveNow () {
|
||||||
let { note, dispatch } = this.props
|
const { note, dispatch } = this.props
|
||||||
clearTimeout(this.saveQueue)
|
clearTimeout(this.saveQueue)
|
||||||
this.saveQueue = null
|
this.saveQueue = null
|
||||||
|
|
||||||
@@ -122,11 +132,11 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFolderChange (e) {
|
handleFolderChange (e) {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
let value = this.refs.folder.value
|
const value = this.refs.folder.value
|
||||||
let splitted = value.split('-')
|
const splitted = value.split('-')
|
||||||
let newStorageKey = splitted.shift()
|
const newStorageKey = splitted.shift()
|
||||||
let newFolderKey = splitted.shift()
|
const newFolderKey = splitted.shift()
|
||||||
|
|
||||||
dataApi
|
dataApi
|
||||||
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
|
||||||
@@ -135,7 +145,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
isMovingNote: true,
|
isMovingNote: true,
|
||||||
note: Object.assign({}, newNote)
|
note: Object.assign({}, newNote)
|
||||||
}, () => {
|
}, () => {
|
||||||
let { dispatch, location } = this.props
|
const { dispatch, location } = this.props
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'MOVE_NOTE',
|
type: 'MOVE_NOTE',
|
||||||
originNote: note,
|
originNote: note,
|
||||||
@@ -144,7 +154,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
hashHistory.replace({
|
hashHistory.replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: newNote.storage + '-' + newNote.key
|
key: newNote.key
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -155,7 +165,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleStarButtonClick (e) {
|
handleStarButtonClick (e) {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
||||||
|
|
||||||
note.isStarred = !note.isStarred
|
note.isStarred = !note.isStarred
|
||||||
@@ -172,31 +182,29 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTrashButtonClick (e) {
|
handleTrashButtonClick (e) {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
const { isTrashed } = note
|
const { isTrashed } = note
|
||||||
|
const { confirmDeletion } = this.props.config.ui
|
||||||
|
|
||||||
if (isTrashed) {
|
if (isTrashed) {
|
||||||
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
if (confirmDeleteNote(confirmDeletion, true)) {
|
||||||
type: 'warning',
|
const {note, dispatch} = this.props
|
||||||
message: 'Confirm note deletion',
|
|
||||||
detail: 'This will permanently remove this note.',
|
|
||||||
buttons: ['Confirm', 'Cancel']
|
|
||||||
})
|
|
||||||
if (dialogueButtonIndex === 1) return
|
|
||||||
let { note, dispatch } = this.props
|
|
||||||
dataApi
|
dataApi
|
||||||
.deleteNote(note.storage, note.key)
|
.deleteNote(note.storage, note.key)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
let dispatchHandler = () => {
|
const dispatchHandler = () => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'DELETE_NOTE',
|
type: 'DELETE_NOTE',
|
||||||
storageKey: data.storageKey,
|
storageKey: data.storageKey,
|
||||||
noteKey: data.noteKey
|
noteKey: data.noteKey
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ee.once('list:moved', dispatchHandler)
|
ee.once('list:next', dispatchHandler)
|
||||||
})
|
})
|
||||||
|
.then(() => ee.emit('list:next'))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (confirmDeleteNote(confirmDeletion, false)) {
|
||||||
note.isTrashed = true
|
note.isTrashed = true
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -204,12 +212,14 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}, () => {
|
}, () => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
})
|
||||||
}
|
|
||||||
ee.emit('list:next')
|
ee.emit('list:next')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleUndoButtonClick (e) {
|
handleUndoButtonClick (e) {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
note.isTrashed = false
|
note.isTrashed = false
|
||||||
|
|
||||||
@@ -225,6 +235,51 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
ee.emit('editor:fullscreen')
|
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) {
|
handleTabPlusButtonClick (e) {
|
||||||
this.addSnippet()
|
this.addSnippet()
|
||||||
}
|
}
|
||||||
@@ -235,14 +290,35 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTabDragStart (e, index) {
|
||||||
|
e.dataTransfer.setData('text/plain', index)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabDrop (e, index) {
|
||||||
|
const oldIndex = parseInt(e.dataTransfer.getData('text'))
|
||||||
|
|
||||||
|
const snippets = this.state.note.snippets.slice()
|
||||||
|
const draggedSnippet = snippets[oldIndex]
|
||||||
|
snippets[oldIndex] = snippets[index]
|
||||||
|
snippets[index] = draggedSnippet
|
||||||
|
const snippetIndex = index
|
||||||
|
|
||||||
|
const note = Object.assign({}, this.state.note, {snippets})
|
||||||
|
this.setState({ note, snippetIndex }, () => {
|
||||||
|
this.save()
|
||||||
|
this.refs['code-' + index].reload()
|
||||||
|
this.refs['code-' + oldIndex].reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleTabDeleteButtonClick (e, index) {
|
handleTabDeleteButtonClick (e, index) {
|
||||||
if (this.state.note.snippets.length > 1) {
|
if (this.state.note.snippets.length > 1) {
|
||||||
if (this.state.note.snippets[index].content.trim().length > 0) {
|
if (this.state.note.snippets[index].content.trim().length > 0) {
|
||||||
let dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Delete a snippet',
|
message: i18n.__('Delete a snippet'),
|
||||||
detail: 'This work cannot be undone.',
|
detail: i18n.__('This work cannot be undone.'),
|
||||||
buttons: ['Confirm', 'Cancel']
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
if (dialogIndex === 0) {
|
if (dialogIndex === 0) {
|
||||||
this.deleteSnippetByIndex(index)
|
this.deleteSnippetByIndex(index)
|
||||||
@@ -263,6 +339,21 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.setState({ note, snippetIndex }, () => {
|
this.setState({ note, snippetIndex }, () => {
|
||||||
this.save()
|
this.save()
|
||||||
this.refs['code-' + this.state.snippetIndex].reload()
|
this.refs['code-' + this.state.snippetIndex].reload()
|
||||||
|
|
||||||
|
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
|
||||||
|
console.log('no need for arrows')
|
||||||
|
this.moveTabBarBy(0)
|
||||||
|
} else {
|
||||||
|
const lastTab = this.allTabs.lastChild
|
||||||
|
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
|
||||||
|
console.log('need to scroll')
|
||||||
|
const width = this.visibleTabs.offsetWidth
|
||||||
|
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
|
||||||
|
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
|
||||||
|
} else {
|
||||||
|
this.setState(this.getArrowsState())
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,24 +368,24 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
name: mode
|
name: mode
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
|
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||||
|
|
||||||
this.setState({
|
this.setState(state => ({
|
||||||
note: this.state.note
|
note: state.note
|
||||||
}, () => {
|
}), () => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModeOptionClick (index, name) {
|
handleModeOptionClick (index, name) {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
let snippets = this.state.note.snippets.slice()
|
const snippets = this.state.note.snippets.slice()
|
||||||
snippets[index].mode = name
|
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({
|
this.setState(state => ({
|
||||||
note: this.state.note
|
note: state.note
|
||||||
}, () => {
|
}), () => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -306,12 +397,12 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleCodeChange (index) {
|
handleCodeChange (index) {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
let snippets = this.state.note.snippets.slice()
|
const snippets = this.state.note.snippets.slice()
|
||||||
snippets[index].content = this.refs['code-' + index].value
|
snippets[index].content = this.refs['code-' + index].value
|
||||||
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
|
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
|
||||||
this.setState({
|
this.setState(state => ({
|
||||||
note: this.state.note
|
note: state.note
|
||||||
}, () => {
|
}), () => {
|
||||||
this.save()
|
this.save()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -319,6 +410,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
switch (e.keyCode) {
|
switch (e.keyCode) {
|
||||||
|
// tab key
|
||||||
case 9:
|
case 9:
|
||||||
if (e.ctrlKey && !e.shiftKey) {
|
if (e.ctrlKey && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -331,9 +423,10 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
// L key
|
||||||
case 76:
|
case 76:
|
||||||
{
|
{
|
||||||
let isSuper = global.process.platform === 'darwin'
|
const isSuper = global.process.platform === 'darwin'
|
||||||
? e.metaKey
|
? e.metaKey
|
||||||
: e.ctrlKey
|
: e.ctrlKey
|
||||||
if (isSuper) {
|
if (isSuper) {
|
||||||
@@ -342,9 +435,10 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
// T key
|
||||||
case 84:
|
case 84:
|
||||||
{
|
{
|
||||||
let isSuper = global.process.platform === 'darwin'
|
const isSuper = global.process.platform === 'darwin'
|
||||||
? e.metaKey
|
? e.metaKey
|
||||||
: e.ctrlKey
|
: e.ctrlKey
|
||||||
if (isSuper) {
|
if (isSuper) {
|
||||||
@@ -357,8 +451,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleModeButtonClick (e, index) {
|
handleModeButtonClick (e, index) {
|
||||||
let menu = new Menu()
|
const menu = new Menu()
|
||||||
CodeMirror.modeInfo.forEach((mode) => {
|
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: mode.name,
|
label: mode.name,
|
||||||
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
||||||
@@ -398,8 +492,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleIndentSizeItemClick (e, indentSize) {
|
handleIndentSizeItemClick (e, indentSize) {
|
||||||
let { config, dispatch } = this.props
|
const { config, dispatch } = this.props
|
||||||
let editor = Object.assign({}, config.editor, {
|
const editor = Object.assign({}, config.editor, {
|
||||||
indentSize
|
indentSize
|
||||||
})
|
})
|
||||||
ConfigManager.set({
|
ConfigManager.set({
|
||||||
@@ -414,8 +508,8 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleIndentTypeItemClick (e, indentType) {
|
handleIndentTypeItemClick (e, indentType) {
|
||||||
let { config, dispatch } = this.props
|
const { config, dispatch } = this.props
|
||||||
let editor = Object.assign({}, config.editor, {
|
const editor = Object.assign({}, config.editor, {
|
||||||
indentType
|
indentType
|
||||||
})
|
})
|
||||||
ConfigManager.set({
|
ConfigManager.set({
|
||||||
@@ -433,36 +527,87 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.refs.description.focus()
|
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 () {
|
addSnippet () {
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
note.snippets = note.snippets.concat([{
|
note.snippets = note.snippets.concat([{
|
||||||
name: '',
|
name: '',
|
||||||
mode: 'Plain Text',
|
mode: 'Plain Text',
|
||||||
content: ''
|
content: ''
|
||||||
}])
|
}])
|
||||||
let snippetIndex = note.snippets.length - 1
|
const snippetIndex = note.snippets.length - 1
|
||||||
|
|
||||||
this.setState({
|
this.setState(Object.assign({
|
||||||
note,
|
note,
|
||||||
snippetIndex
|
snippetIndex
|
||||||
}, () => {
|
}, this.getArrowsState()), () => {
|
||||||
|
if (this.state.showArrows) {
|
||||||
|
const tabs = this.allTabs.querySelectorAll('div')
|
||||||
|
if (tabs) {
|
||||||
|
this.moveToTab(tabs[snippetIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
this.refs['tab-' + snippetIndex].startRenaming()
|
this.refs['tab-' + snippetIndex].startRenaming()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
jumpNextTab () {
|
jumpNextTab () {
|
||||||
this.setState({
|
this.setState(state => ({
|
||||||
snippetIndex: (this.state.snippetIndex + 1) % this.state.note.snippets.length
|
snippetIndex: (state.snippetIndex + 1) % state.note.snippets.length
|
||||||
}, () => {
|
}), () => {
|
||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
jumpPrevTab () {
|
jumpPrevTab () {
|
||||||
this.setState({
|
this.setState(state => ({
|
||||||
snippetIndex: (this.state.snippetIndex - 1 + this.state.note.snippets.length) % this.state.note.snippets.length
|
snippetIndex: (state.snippetIndex - 1 + state.note.snippets.length) % state.note.snippets.length
|
||||||
}, () => {
|
}), () => {
|
||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -480,26 +625,26 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
showWarning () {
|
showWarning () {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Sorry!',
|
message: i18n.__('Sorry!'),
|
||||||
detail: 'md/text import is available only a markdown note.',
|
detail: i18n.__('md/text import is available only a markdown note.'),
|
||||||
buttons: ['OK']
|
buttons: [i18n.__('OK')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { data, config, location } = this.props
|
const { data, config, location } = this.props
|
||||||
let { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
let storageKey = note.storage
|
const storageKey = note.storage
|
||||||
let folderKey = note.folder
|
const folderKey = note.folder
|
||||||
|
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||||
|
|
||||||
let tabList = note.snippets.map((snippet, index) => {
|
const tabList = note.snippets.map((snippet, index) => {
|
||||||
let isActive = this.state.snippetIndex === index
|
const isActive = this.state.snippetIndex === index
|
||||||
|
|
||||||
return <SnippetTab
|
return <SnippetTab
|
||||||
key={index}
|
key={index}
|
||||||
@@ -510,13 +655,15 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
|
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
|
||||||
onRename={(name) => this.renameSnippetByIndex(index, name)}
|
onRename={(name) => this.renameSnippetByIndex(index, name)}
|
||||||
isDeletable={note.snippets.length > 1}
|
isDeletable={note.snippets.length > 1}
|
||||||
|
onDragStart={(e) => this.handleTabDragStart(e, index)}
|
||||||
|
onDrop={(e) => this.handleTabDrop(e, index)}
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
|
|
||||||
let viewList = note.snippets.map((snippet, index) => {
|
const viewList = note.snippets.map((snippet, index) => {
|
||||||
let isActive = this.state.snippetIndex === index
|
const isActive = this.state.snippetIndex === index
|
||||||
|
|
||||||
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
|
let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
|
||||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||||
|
|
||||||
return <div styleName='tabView'
|
return <div styleName='tabView'
|
||||||
@@ -540,7 +687,10 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
fontSize={editorFontSize}
|
fontSize={editorFontSize}
|
||||||
indentType={config.editor.indentType}
|
indentType={config.editor.indentType}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
keyMap={config.editor.keyMap}
|
keyMap={config.editor.keyMap}
|
||||||
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||||
ref={'code-' + index}
|
ref={'code-' + index}
|
||||||
/>
|
/>
|
||||||
@@ -548,7 +698,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
})
|
})
|
||||||
|
|
||||||
let options = []
|
const options = []
|
||||||
data.storageMap.forEach((storage, index) => {
|
data.storageMap.forEach((storage, index) => {
|
||||||
storage.folders.forEach((folder) => {
|
storage.folders.forEach((folder) => {
|
||||||
options.push({
|
options.push({
|
||||||
@@ -557,14 +707,11 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
||||||
|
|
||||||
const trashTopBar = <div styleName='info'>
|
const trashTopBar = <div styleName='info'>
|
||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
<i styleName='undo-button'
|
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
|
||||||
className='fa fa-undo fa-fw'
|
|
||||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
@@ -578,6 +725,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.showWarning}
|
exportAsMd={this.showWarning}
|
||||||
exportAsTxt={this.showWarning}
|
exportAsTxt={this.showWarning}
|
||||||
|
exportAsHtml={this.showWarning}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -600,25 +748,27 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<InfoButton
|
|
||||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StarButton
|
<StarButton
|
||||||
onClick={(e) => this.handleStarButtonClick(e)}
|
onClick={(e) => this.handleStarButtonClick(e)}
|
||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button styleName='control-fullScreenButton'
|
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
|
||||||
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
|
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
|
|
||||||
|
<InfoButton
|
||||||
|
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||||
|
/>
|
||||||
|
|
||||||
<InfoPanel
|
<InfoPanel
|
||||||
storageName={currentOption.storage.name}
|
storageName={currentOption.storage.name}
|
||||||
folderName={currentOption.folder.name}
|
folderName={currentOption.folder.name}
|
||||||
noteLink={`[${note.title}](${location.query.key})`}
|
noteLink={`[${note.title}](:note:${location.query.key})`}
|
||||||
updatedAt={formatDate(note.updatedAt)}
|
updatedAt={formatDate(note.updatedAt)}
|
||||||
createdAt={formatDate(note.createdAt)}
|
createdAt={formatDate(note.createdAt)}
|
||||||
exportAsMd={this.showWarning}
|
exportAsMd={this.showWarning}
|
||||||
@@ -644,16 +794,32 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
fontSize: parseInt(config.preview.fontSize, 10)
|
fontSize: parseInt(config.preview.fontSize, 10)
|
||||||
}}
|
}}
|
||||||
ref='description'
|
ref='description'
|
||||||
placeholder='Description...'
|
placeholder={i18n.__('Description...')}
|
||||||
value={this.state.note.description}
|
value={this.state.note.description}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='tabList'>
|
<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}
|
{tabList}
|
||||||
</div>
|
</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)}
|
onClick={(e) => this.handleTabPlusButtonClick(e)}
|
||||||
>
|
>
|
||||||
<i className='fa fa-plus' />
|
<i className='fa fa-plus' />
|
||||||
@@ -667,7 +833,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
|
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
|
||||||
>
|
>
|
||||||
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
||||||
? 'Select Syntax...'
|
? i18n.__('Select Syntax...')
|
||||||
: this.state.note.snippets[this.state.snippetIndex].mode
|
: this.state.note.snippets[this.state.snippetIndex].mode
|
||||||
}
|
}
|
||||||
<i className='fa fa-caret-down' />
|
<i className='fa fa-caret-down' />
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
.body
|
.body
|
||||||
absolute left right
|
absolute left right
|
||||||
left $snippet-note-detail-left-margin
|
margin 0 30px
|
||||||
right $snippet-note-detail-right-margin
|
|
||||||
top $info-height + $info-margin-under-border
|
top $info-height + $info-margin-under-border
|
||||||
bottom $statusBar-height
|
bottom $statusBar-height
|
||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
@@ -36,13 +35,26 @@
|
|||||||
height 30px
|
height 30px
|
||||||
display flex
|
display flex
|
||||||
background-color $ui-noteDetail-backgroundColor
|
background-color $ui-noteDetail-backgroundColor
|
||||||
|
overflow hidden
|
||||||
|
|
||||||
.tabList .list
|
.tabList .list
|
||||||
flex 1
|
flex 1
|
||||||
display flex
|
|
||||||
overflow hidden
|
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()
|
navWhiteButtonColor()
|
||||||
width 30px
|
width 30px
|
||||||
|
|
||||||
@@ -69,7 +81,22 @@
|
|||||||
.control-fullScreenButton
|
.control-fullScreenButton
|
||||||
top 80px
|
top 80px
|
||||||
margin-bottom 10px
|
margin-bottom 10px
|
||||||
topBarButtonLight()
|
topBarButtonRight()
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 70px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.root
|
.root
|
||||||
@@ -78,7 +105,7 @@ body[data-theme="white"]
|
|||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root
|
.root
|
||||||
border none
|
border-left 1px solid $ui-dark-borderColor
|
||||||
background-color $ui-dark-noteDetail-backgroundColor
|
background-color $ui-dark-noteDetail-backgroundColor
|
||||||
box-shadow none
|
box-shadow none
|
||||||
|
|
||||||
@@ -109,3 +136,37 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.control-fullScreenButton
|
.control-fullScreenButton
|
||||||
topBarButtonDark()
|
topBarButtonDark()
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
border-left 1px solid $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
.body
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
.body .description textarea
|
||||||
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
border 1px solid $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
.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
|
||||||
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
|
color $ui-monokai-text-color
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './StarButton.styl'
|
import styles from './StarButton.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class StarButton extends React.Component {
|
class StarButton extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -31,7 +33,7 @@ class StarButton extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { className } = this.props
|
const { className } = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={_.isString(className)
|
<button className={_.isString(className)
|
||||||
@@ -45,14 +47,14 @@ class StarButton extends React.Component {
|
|||||||
onMouseDown={(e) => this.handleMouseDown(e)}
|
onMouseDown={(e) => this.handleMouseDown(e)}
|
||||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||||
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
onMouseLeave={(e) => this.handleMouseLeave(e)}
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}>
|
||||||
>
|
|
||||||
<img styleName='icon'
|
<img styleName='icon'
|
||||||
src={this.state.isActive || this.props.isActive
|
src={this.state.isActive || this.props.isActive
|
||||||
? '../resources/icon/icon-starred.svg'
|
? '../resources/icon/icon-starred.svg'
|
||||||
: '../resources/icon/icon-star.svg'
|
: '../resources/icon/icon-star.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<span styleName='tooltip'>{i18n.__('Star')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
.root
|
.root
|
||||||
top 45px
|
top 45px
|
||||||
topBarButtonLight()
|
topBarButtonRight()
|
||||||
&:hover
|
&:hover
|
||||||
transition 0.2s
|
transition 0.2s
|
||||||
color alpha($ui-favorite-star-button-color, 0.6)
|
color alpha($ui-favorite-star-button-color, 0.6)
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 115px
|
||||||
|
width 40px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
.root--active
|
.root--active
|
||||||
@extend .root
|
@extend .root
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TagSelect.styl'
|
import styles from './TagSelect.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class TagSelect extends React.Component {
|
class TagSelect extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -37,17 +39,14 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNewTagBlur (e) {
|
||||||
|
this.submitTag()
|
||||||
|
}
|
||||||
|
|
||||||
removeLastTag () {
|
removeLastTag () {
|
||||||
let { value } = this.props
|
this.removeTagByCallback((value) => {
|
||||||
|
|
||||||
value = _.isArray(value)
|
|
||||||
? value.slice()
|
|
||||||
: []
|
|
||||||
value.pop()
|
value.pop()
|
||||||
value = _.uniq(value)
|
})
|
||||||
|
|
||||||
this.value = value
|
|
||||||
this.props.onChange()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset () {
|
reset () {
|
||||||
@@ -60,6 +59,7 @@ class TagSelect extends React.Component {
|
|||||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
||||||
let { value } = this.props
|
let { value } = this.props
|
||||||
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||||
|
newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
|
||||||
|
|
||||||
if (newTag.length <= 0) {
|
if (newTag.length <= 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -89,21 +89,28 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTagRemoveButtonClick (tag) {
|
handleTagRemoveButtonClick (tag) {
|
||||||
return (e) => {
|
this.removeTagByCallback((value, tag) => {
|
||||||
|
value.splice(value.indexOf(tag), 1)
|
||||||
|
}, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTagByCallback (callback, tag = null) {
|
||||||
let { value } = this.props
|
let { value } = this.props
|
||||||
|
|
||||||
value.splice(value.indexOf(tag), 1)
|
value = _.isArray(value)
|
||||||
|
? value.slice()
|
||||||
|
: []
|
||||||
|
callback(value, tag)
|
||||||
value = _.uniq(value)
|
value = _.uniq(value)
|
||||||
|
|
||||||
this.value = value
|
this.value = value
|
||||||
this.props.onChange()
|
this.props.onChange()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { value, className } = this.props
|
const { value, className } = this.props
|
||||||
|
|
||||||
let tagList = _.isArray(value)
|
const tagList = _.isArray(value)
|
||||||
? value.map((tag) => {
|
? value.map((tag) => {
|
||||||
return (
|
return (
|
||||||
<span styleName='tag'
|
<span styleName='tag'
|
||||||
@@ -111,7 +118,7 @@ class TagSelect extends React.Component {
|
|||||||
>
|
>
|
||||||
<span styleName='tag-label'>#{tag}</span>
|
<span styleName='tag-label'>#{tag}</span>
|
||||||
<button styleName='tag-removeButton'
|
<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='../resources/icon/icon-x.svg' width='8px' />
|
||||||
</button>
|
</button>
|
||||||
@@ -131,9 +138,10 @@ class TagSelect extends React.Component {
|
|||||||
<input styleName='newTag'
|
<input styleName='newTag'
|
||||||
ref='newTag'
|
ref='newTag'
|
||||||
value={this.state.newTag}
|
value={this.state.newTag}
|
||||||
placeholder='Add tag...'
|
placeholder={i18n.__('Add tag...')}
|
||||||
onChange={(e) => this.handleNewTagInputChange(e)}
|
onChange={(e) => this.handleNewTagInputChange(e)}
|
||||||
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
||||||
|
onBlur={(e) => this.handleNewTagBlur(e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
width 100%
|
width 100%
|
||||||
overflow-x scroll
|
overflow-x scroll
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
|
margin-top 31px
|
||||||
|
position absolute
|
||||||
|
|
||||||
.root::-webkit-scrollbar
|
.root::-webkit-scrollbar
|
||||||
display none
|
display none
|
||||||
@@ -64,3 +66,35 @@ body[data-theme="dark"]
|
|||||||
border-color none
|
border-color none
|
||||||
background-color transparent
|
background-color transparent
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.tag
|
||||||
|
background-color $ui-solarized-dark-tag-backgroundColor
|
||||||
|
|
||||||
|
.tag-removeButton
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
.tag-label
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.newTag
|
||||||
|
border-color none
|
||||||
|
background-color transparent
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.tag
|
||||||
|
background-color $ui-monokai-button-backgroundColor
|
||||||
|
|
||||||
|
.tag-removeButton
|
||||||
|
border-color $ui-button--focus-borderColor
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
.tag-label
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
.newTag
|
||||||
|
border-color none
|
||||||
|
background-color transparent
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
|||||||
26
browser/main/Detail/ToggleModeButton.js
Normal file
26
browser/main/Detail/ToggleModeButton.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
|
}) => (
|
||||||
|
<div styleName='control-toggleModeButton'>
|
||||||
|
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
||||||
|
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
||||||
|
</div>
|
||||||
|
<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'>{i18n.__('Toggle Mode')}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
ToggleModeButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
editorType: PropTypes.string.Required
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(ToggleModeButton, styles)
|
||||||
65
browser/main/Detail/ToggleModeButton.styl
Normal file
65
browser/main/Detail/ToggleModeButton.styl
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.control-toggleModeButton
|
||||||
|
height 25px
|
||||||
|
border-radius 50px
|
||||||
|
background-color #F4F4F4
|
||||||
|
width 52px
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
position: relative
|
||||||
|
top 2px
|
||||||
|
.active
|
||||||
|
background-color #1EC38B
|
||||||
|
width 33px
|
||||||
|
height 24px
|
||||||
|
box-shadow 2px 0px 7px #eee
|
||||||
|
z-index 1
|
||||||
|
|
||||||
|
div
|
||||||
|
width 40px
|
||||||
|
height 100%
|
||||||
|
border-radius 50%
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
cursor pointer
|
||||||
|
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 33px
|
||||||
|
left -10px
|
||||||
|
z-index 200
|
||||||
|
width 80px
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.control-fullScreenButton
|
||||||
|
topBarButtonDark()
|
||||||
|
|
||||||
|
.control-toggleModeButton
|
||||||
|
background-color #3A404C
|
||||||
|
.active
|
||||||
|
background-color #1EC38B
|
||||||
|
box-shadow 2px 0px 7px #444444
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.control-toggleModeButton
|
||||||
|
background-color #002B36
|
||||||
|
.active
|
||||||
|
background-color #1EC38B
|
||||||
|
box-shadow 2px 0px 7px #222222
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.control-toggleModeButton
|
||||||
|
background-color #272822
|
||||||
|
.active
|
||||||
|
background-color #1EC38B
|
||||||
|
box-shadow 2px 0px 7px #222222
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TrashButton.styl'
|
import styles from './TrashButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const TrashButton = ({
|
const TrashButton = ({
|
||||||
onClick
|
onClick
|
||||||
@@ -9,6 +11,7 @@ const TrashButton = ({
|
|||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
.control-trashButton
|
.control-trashButton
|
||||||
top 120px
|
top 115px
|
||||||
margin-bottom 10px
|
topBarButtonRight()
|
||||||
topBarButtonLight()
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 50px
|
||||||
|
right 50px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
|
||||||
.control-trashButton--in-trash
|
.control-trashButton--in-trash
|
||||||
top 60px
|
top 60px
|
||||||
topBarButtonLight()
|
topBarButtonRight()
|
||||||
|
|
||||||
.trashButton
|
.trashButton
|
||||||
padding 0px
|
padding 0px
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './Detail.styl'
|
import styles from './Detail.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
@@ -6,6 +7,8 @@ import MarkdownNoteDetail from './MarkdownNoteDetail'
|
|||||||
import SnippetNoteDetail from './SnippetNoteDetail'
|
import SnippetNoteDetail from './SnippetNoteDetail'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import StatusBar from '../StatusBar'
|
import StatusBar from '../StatusBar'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import debounceRender from 'react-debounce-render'
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
@@ -32,14 +35,11 @@ class Detail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { location, data, config } = this.props
|
const { location, data, config } = this.props
|
||||||
let note = null
|
let note = null
|
||||||
if (location.query.key != null) {
|
if (location.query.key != null) {
|
||||||
let splitted = location.query.key.split('-')
|
const noteKey = location.query.key
|
||||||
let storageKey = splitted.shift()
|
note = data.noteMap.get(noteKey)
|
||||||
let noteKey = splitted.shift()
|
|
||||||
|
|
||||||
note = data.noteMap.get(storageKey + '-' + noteKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
@@ -49,7 +49,7 @@ class Detail extends React.Component {
|
|||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
>
|
>
|
||||||
<div styleName='empty'>
|
<div styleName='empty'>
|
||||||
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new note</div>
|
<div styleName='empty-message'>{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />{i18n.__('to create a new note')}</div>
|
||||||
</div>
|
</div>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||||
@@ -100,4 +100,4 @@ Detail.propTypes = {
|
|||||||
ignorePreviewPointerEvents: PropTypes.bool
|
ignorePreviewPointerEvents: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(Detail, styles)
|
export default debounceRender(CSSModules(Detail, styles))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './Main.styl'
|
import styles from './Main.styl'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
@@ -9,10 +10,16 @@ import Detail from './Detail'
|
|||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import modal from 'browser/main/lib/modal'
|
|
||||||
import InitModal from 'browser/main/modals/InitModal'
|
|
||||||
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
|
import { hashHistory } from 'react-router'
|
||||||
|
import store from 'browser/main/store'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import { getLocales } from 'browser/lib/Languages'
|
||||||
|
import applyShortcuts from 'browser/main/lib/shortcutManager'
|
||||||
|
const path = require('path')
|
||||||
|
const electron = require('electron')
|
||||||
|
const { remote } = electron
|
||||||
|
|
||||||
class Main extends React.Component {
|
class Main extends React.Component {
|
||||||
|
|
||||||
@@ -23,7 +30,7 @@ class Main extends React.Component {
|
|||||||
mobileAnalytics.initAwsMobileAnalytics()
|
mobileAnalytics.initAwsMobileAnalytics()
|
||||||
}
|
}
|
||||||
|
|
||||||
let { config } = props
|
const { config } = props
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isRightSliderFocused: false,
|
isRightSliderFocused: false,
|
||||||
@@ -39,7 +46,7 @@ class Main extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getChildContext () {
|
getChildContext () {
|
||||||
let { status, config } = this.props
|
const { status, config } = this.props
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
@@ -47,17 +54,113 @@ class Main extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
init () {
|
||||||
let { dispatch, config } = this.props
|
dataApi
|
||||||
|
.addStorage({
|
||||||
|
name: 'My Storage',
|
||||||
|
path: path.join(remote.app.getPath('home'), 'Boostnote')
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.storage.folders[0] != null) {
|
||||||
|
return data
|
||||||
|
} else {
|
||||||
|
return dataApi
|
||||||
|
.createFolder(data.storage.key, {
|
||||||
|
color: '#1278BD',
|
||||||
|
name: 'Default'
|
||||||
|
})
|
||||||
|
.then((_data) => {
|
||||||
|
return {
|
||||||
|
storage: _data.storage,
|
||||||
|
notes: data.notes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
console.log(data)
|
||||||
|
store.dispatch({
|
||||||
|
type: 'ADD_STORAGE',
|
||||||
|
storage: data.storage,
|
||||||
|
notes: data.notes
|
||||||
|
})
|
||||||
|
|
||||||
if (config.ui.theme === 'dark') {
|
const defaultSnippetNote = dataApi
|
||||||
document.body.setAttribute('data-theme', 'dark')
|
.createNote(data.storage.key, {
|
||||||
} else if (config.ui.theme === 'white') {
|
type: 'SNIPPET_NOTE',
|
||||||
document.body.setAttribute('data-theme', 'white')
|
folder: data.storage.folders[0].key,
|
||||||
|
title: 'Snippet note example',
|
||||||
|
description: 'Snippet note example\nYou can store a series of snippets as a single note, like Gist.',
|
||||||
|
snippets: [
|
||||||
|
{
|
||||||
|
name: 'example.html',
|
||||||
|
mode: 'html',
|
||||||
|
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'example.js',
|
||||||
|
mode: 'javascript',
|
||||||
|
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then((note) => {
|
||||||
|
store.dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const defaultMarkdownNote = dataApi
|
||||||
|
.createNote(data.storage.key, {
|
||||||
|
type: 'MARKDOWN_NOTE',
|
||||||
|
folder: data.storage.folders[0].key,
|
||||||
|
title: 'Welcome to Boostnote!',
|
||||||
|
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
||||||
|
})
|
||||||
|
.then((note) => {
|
||||||
|
store.dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.resolve(defaultSnippetNote)
|
||||||
|
.then(defaultMarkdownNote)
|
||||||
|
.then(() => data.storage)
|
||||||
|
})
|
||||||
|
.then((storage) => {
|
||||||
|
hashHistory.push('/storages/' + storage.key)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch, config } = this.props
|
||||||
|
|
||||||
|
const supportedThemes = [
|
||||||
|
'dark',
|
||||||
|
'white',
|
||||||
|
'solarized-dark',
|
||||||
|
'monokai'
|
||||||
|
]
|
||||||
|
|
||||||
|
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
|
||||||
|
document.body.setAttribute('data-theme', config.ui.theme)
|
||||||
} else {
|
} else {
|
||||||
document.body.setAttribute('data-theme', 'default')
|
document.body.setAttribute('data-theme', 'default')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (getLocales().indexOf(config.ui.language) !== -1) {
|
||||||
|
i18n.setLocale(config.ui.language)
|
||||||
|
} else {
|
||||||
|
i18n.setLocale('en')
|
||||||
|
}
|
||||||
|
applyShortcuts()
|
||||||
// Reload all data
|
// Reload all data
|
||||||
dataApi.init()
|
dataApi.init()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -68,7 +171,7 @@ class Main extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (data.storages.length < 1) {
|
if (data.storages.length < 1) {
|
||||||
modal.open(InitModal)
|
this.init()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -99,8 +202,8 @@ class Main extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
isRightSliderFocused: false
|
isRightSliderFocused: false
|
||||||
}, () => {
|
}, () => {
|
||||||
let { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
let newListWidth = this.state.listWidth
|
const newListWidth = this.state.listWidth
|
||||||
// TODO: ConfigManager should dispatch itself.
|
// TODO: ConfigManager should dispatch itself.
|
||||||
ConfigManager.set({listWidth: newListWidth})
|
ConfigManager.set({listWidth: newListWidth})
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -115,8 +218,8 @@ class Main extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
isLeftSliderFocused: false
|
isLeftSliderFocused: false
|
||||||
}, () => {
|
}, () => {
|
||||||
let { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
let navWidth = this.state.navWidth
|
const navWidth = this.state.navWidth
|
||||||
// TODO: ConfigManager should dispatch itself.
|
// TODO: ConfigManager should dispatch itself.
|
||||||
ConfigManager.set({ navWidth })
|
ConfigManager.set({ navWidth })
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -129,7 +232,7 @@ class Main extends React.Component {
|
|||||||
|
|
||||||
handleMouseMove (e) {
|
handleMouseMove (e) {
|
||||||
if (this.state.isRightSliderFocused) {
|
if (this.state.isRightSliderFocused) {
|
||||||
let offset = this.refs.body.getBoundingClientRect().left
|
const offset = this.refs.body.getBoundingClientRect().left
|
||||||
let newListWidth = e.pageX - offset
|
let newListWidth = e.pageX - offset
|
||||||
if (newListWidth < 10) {
|
if (newListWidth < 10) {
|
||||||
newListWidth = 10
|
newListWidth = 10
|
||||||
@@ -182,7 +285,7 @@ class Main extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { config } = this.props
|
const { config } = this.props
|
||||||
|
|
||||||
// the width of the navigation bar when it is folded/collapsed
|
// the width of the navigation bar when it is folded/collapsed
|
||||||
const foldedNavigationWidth = 44
|
const foldedNavigationWidth = 44
|
||||||
|
|||||||
@@ -71,3 +71,11 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
.control-newNoteButton-tooltip
|
.control-newNoteButton-tooltip
|
||||||
darkTooltip()
|
darkTooltip()
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root, .root--expanded
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.root, .root--expanded
|
||||||
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './NewNoteButton.styl'
|
import styles from './NewNoteButton.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import modal from 'browser/main/lib/modal'
|
import modal from 'browser/main/lib/modal'
|
||||||
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
||||||
import { hashHistory } from 'react-router'
|
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
@@ -34,7 +34,7 @@ class NewNoteButton extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNewNoteButtonClick (e) {
|
handleNewNoteButtonClick (e) {
|
||||||
const { config, location, dispatch } = this.props
|
const { location, dispatch } = this.props
|
||||||
const { storage, folder } = this.resolveTargetFolder()
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
|
|
||||||
modal.open(NewNoteModal, {
|
modal.open(NewNoteModal, {
|
||||||
@@ -51,15 +51,15 @@ class NewNoteButton extends React.Component {
|
|||||||
|
|
||||||
// Find first storage
|
// Find first storage
|
||||||
if (storage == null) {
|
if (storage == null) {
|
||||||
for (let kv of data.storageMap) {
|
for (const kv of data.storageMap) {
|
||||||
storage = kv[1]
|
storage = kv[1]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storage == null) this.showMessageBox('No storage to create a note')
|
if (storage == null) this.showMessageBox(i18n.__('No storage to create a note'))
|
||||||
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
|
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
|
||||||
if (folder == null) this.showMessageBox('No folder to create a note')
|
if (folder == null) this.showMessageBox(i18n.__('No folder to create a note'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
storage,
|
storage,
|
||||||
@@ -87,7 +87,7 @@ class NewNoteButton extends React.Component {
|
|||||||
onClick={(e) => this.handleNewNoteButtonClick(e)}>
|
onClick={(e) => this.handleNewNoteButtonClick(e)}>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
|
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
|
||||||
<span styleName='control-newNoteButton-tooltip'>
|
<span styleName='control-newNoteButton-tooltip'>
|
||||||
Make a Note {OSX ? '⌘' : '^'} + n
|
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -89,3 +89,52 @@ body[data-theme="dark"]
|
|||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
&:active
|
&:active
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
|
|
||||||
|
body[data-theme="solarized-dark"]
|
||||||
|
.root
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control
|
||||||
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
border-color $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
.control-sortBy-select
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.control-button
|
||||||
|
color $ui-solarized-dark-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
.control-button--active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
&:active
|
||||||
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.root
|
||||||
|
border-color $ui-monokai-borderColor
|
||||||
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control
|
||||||
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
|
border-color $ui-monokai-borderColor
|
||||||
|
|
||||||
|
.control-sortBy-select
|
||||||
|
&:hover
|
||||||
|
transition 0.2s
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
.control-button
|
||||||
|
color $ui-monokai-inactive-text-color
|
||||||
|
&:hover
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
|
||||||
|
.control-button--active
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
&:active
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
import React, { PropTypes } from 'react'
|
/* global electron */
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import debounceRender from 'react-debounce-render'
|
||||||
import styles from './NoteList.styl'
|
import styles from './NoteList.styl'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import NoteItem from 'browser/components/NoteItem'
|
import NoteItem from 'browser/components/NoteItem'
|
||||||
import NoteItemSimple from 'browser/components/NoteItemSimple'
|
import NoteItemSimple from 'browser/components/NoteItemSimple'
|
||||||
import searchFromNotes from 'browser/lib/search'
|
import searchFromNotes from 'browser/lib/search'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
import { hashHistory } from 'react-router'
|
import { hashHistory } from 'react-router'
|
||||||
import markdown from 'browser/lib/markdown'
|
import copy from 'copy-to-clipboard'
|
||||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import stripgtags from 'striptags'
|
import Markdown from '../../lib/markdown'
|
||||||
import store from 'browser/main/store'
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { Menu, MenuItem, dialog } = remote
|
const { Menu, MenuItem, dialog } = remote
|
||||||
|
const WP_POST_PATH = '/wp/v2/posts'
|
||||||
|
|
||||||
function sortByCreatedAt (a, b) {
|
function sortByCreatedAt (a, b) {
|
||||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||||
@@ -31,6 +38,18 @@ function sortByUpdatedAt (a, b) {
|
|||||||
return new Date(b.updatedAt) - new Date(a.updatedAt)
|
return new Date(b.updatedAt) - new Date(a.updatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findNoteByKey (notes, noteKey) {
|
||||||
|
return notes.find((note) => note.key === noteKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNotesByKeys (notes, noteKeys) {
|
||||||
|
return notes.filter((note) => noteKeys.includes(getNoteKey(note)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNoteKey (note) {
|
||||||
|
return note.key
|
||||||
|
}
|
||||||
|
|
||||||
class NoteList extends React.Component {
|
class NoteList extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -50,8 +69,21 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
this.importFromFileHandler = this.importFromFile.bind(this)
|
this.importFromFileHandler = this.importFromFile.bind(this)
|
||||||
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
||||||
|
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
|
||||||
|
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
|
||||||
|
this.deleteNote = this.deleteNote.bind(this)
|
||||||
|
this.focusNote = this.focusNote.bind(this)
|
||||||
|
this.pinToTop = this.pinToTop.bind(this)
|
||||||
|
this.getNoteStorage = this.getNoteStorage.bind(this)
|
||||||
|
this.getNoteFolder = this.getNoteFolder.bind(this)
|
||||||
|
this.getViewType = this.getViewType.bind(this)
|
||||||
|
this.restoreNote = this.restoreNote.bind(this)
|
||||||
|
this.copyNoteLink = this.copyNoteLink.bind(this)
|
||||||
|
|
||||||
|
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||||
this.state = {
|
this.state = {
|
||||||
|
shiftKeyDown: false,
|
||||||
|
selectedNoteKeys: []
|
||||||
}
|
}
|
||||||
|
|
||||||
this.contextNotes = []
|
this.contextNotes = []
|
||||||
@@ -89,15 +121,28 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
let { location } = this.props
|
const { location } = this.props
|
||||||
|
const { selectedNoteKeys } = this.state
|
||||||
|
const visibleNoteKeys = this.notes.map(note => note.key)
|
||||||
|
const note = this.notes[0]
|
||||||
|
const prevKey = prevProps.location.query.key
|
||||||
|
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
|
||||||
|
|
||||||
if (this.notes.length > 0 && location.query.key == null) {
|
if (note && location.query.key == null) {
|
||||||
let { router } = this.context
|
const { router } = this.context
|
||||||
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
|
||||||
|
|
||||||
|
// A visible note is an active note
|
||||||
|
if (!selectedNoteKeys.includes(noteKey)) {
|
||||||
|
if (selectedNoteKeys.length === 1) selectedNoteKeys.pop()
|
||||||
|
selectedNoteKeys.push(noteKey)
|
||||||
|
ee.emit('list:moved')
|
||||||
|
}
|
||||||
|
|
||||||
router.replace({
|
router.replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: this.notes[0].storage + '-' + this.notes[0].key
|
key: noteKey
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -107,16 +152,16 @@ class NoteList extends React.Component {
|
|||||||
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
|
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
|
||||||
const targetIndex = this.getTargetIndex()
|
const targetIndex = this.getTargetIndex()
|
||||||
if (targetIndex > -1) {
|
if (targetIndex > -1) {
|
||||||
let list = this.refs.list
|
const list = this.refs.list
|
||||||
let item = list.childNodes[targetIndex]
|
const item = list.childNodes[targetIndex]
|
||||||
|
|
||||||
if (item == null) return false
|
if (item == null) return false
|
||||||
|
|
||||||
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
const overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
||||||
if (overflowBelow) {
|
if (overflowBelow) {
|
||||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
||||||
}
|
}
|
||||||
let overflowAbove = list.scrollTop > item.offsetTop
|
const overflowAbove = list.scrollTop > item.offsetTop
|
||||||
if (overflowAbove) {
|
if (overflowAbove) {
|
||||||
list.scrollTop = item.offsetTop
|
list.scrollTop = item.offsetTop
|
||||||
}
|
}
|
||||||
@@ -124,12 +169,34 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
focusNote (selectedNoteKeys, noteKey) {
|
||||||
|
const { router } = this.context
|
||||||
|
const { location } = this.props
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedNoteKeys
|
||||||
|
})
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {
|
||||||
|
key: noteKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getNoteKeyFromTargetIndex (targetIndex) {
|
||||||
|
const note = Object.assign({}, this.notes[targetIndex])
|
||||||
|
const noteKey = getNoteKey(note)
|
||||||
|
return noteKey
|
||||||
|
}
|
||||||
|
|
||||||
selectPriorNote () {
|
selectPriorNote () {
|
||||||
if (this.notes == null || this.notes.length === 0) {
|
if (this.notes == null || this.notes.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let { router } = this.context
|
let { selectedNoteKeys } = this.state
|
||||||
let { location } = this.props
|
const { shiftKeyDown } = this.state
|
||||||
|
|
||||||
let targetIndex = this.getTargetIndex()
|
let targetIndex = this.getTargetIndex()
|
||||||
|
|
||||||
@@ -137,39 +204,50 @@ class NoteList extends React.Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetIndex--
|
targetIndex--
|
||||||
if (targetIndex < 0) targetIndex = 0
|
|
||||||
|
|
||||||
router.push({
|
if (!shiftKeyDown) { selectedNoteKeys = [] }
|
||||||
pathname: location.pathname,
|
const priorNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
|
||||||
query: {
|
if (selectedNoteKeys.includes(priorNoteKey)) {
|
||||||
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
|
selectedNoteKeys.pop()
|
||||||
|
} else {
|
||||||
|
selectedNoteKeys.push(priorNoteKey)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
this.focusNote(selectedNoteKeys, priorNoteKey)
|
||||||
|
|
||||||
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
|
|
||||||
selectNextNote () {
|
selectNextNote () {
|
||||||
if (this.notes == null || this.notes.length === 0) {
|
if (this.notes == null || this.notes.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let { router } = this.context
|
let { selectedNoteKeys } = this.state
|
||||||
let { location } = this.props
|
const { shiftKeyDown } = this.state
|
||||||
|
|
||||||
let targetIndex = this.getTargetIndex()
|
let targetIndex = this.getTargetIndex()
|
||||||
|
const isTargetLastNote = targetIndex === this.notes.length - 1
|
||||||
|
|
||||||
if (targetIndex === this.notes.length - 1) {
|
if (isTargetLastNote && shiftKeyDown) {
|
||||||
|
return
|
||||||
|
} else if (isTargetLastNote) {
|
||||||
targetIndex = 0
|
targetIndex = 0
|
||||||
} else {
|
} else {
|
||||||
targetIndex++
|
targetIndex++
|
||||||
if (targetIndex < 0) targetIndex = 0
|
if (targetIndex < 0) targetIndex = 0
|
||||||
else if (targetIndex > this.notes.length - 1) targetIndex === this.notes.length - 1
|
else if (targetIndex > this.notes.length - 1) targetIndex = this.notes.length - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push({
|
if (!shiftKeyDown) { selectedNoteKeys = [] }
|
||||||
pathname: location.pathname,
|
const nextNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
|
||||||
query: {
|
if (selectedNoteKeys.includes(nextNoteKey)) {
|
||||||
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
|
selectedNoteKeys.pop()
|
||||||
|
} else {
|
||||||
|
selectedNoteKeys.push(nextNoteKey)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
this.focusNote(selectedNoteKeys, nextNoteKey)
|
||||||
|
|
||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,19 +257,8 @@ class NoteList extends React.Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { router } = this.context
|
const selectedNoteKeys = [noteHash]
|
||||||
const { location } = this.props
|
this.focusNote(selectedNoteKeys, noteHash)
|
||||||
|
|
||||||
let targetIndex = this.getTargetIndex()
|
|
||||||
|
|
||||||
if (targetIndex < 0) targetIndex = 0
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
pathname: location.pathname,
|
|
||||||
query: {
|
|
||||||
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ee.emit('list:moved')
|
ee.emit('list:moved')
|
||||||
}
|
}
|
||||||
@@ -199,37 +266,57 @@ class NoteList extends React.Component {
|
|||||||
handleNoteListKeyDown (e) {
|
handleNoteListKeyDown (e) {
|
||||||
if (e.metaKey || e.ctrlKey) return true
|
if (e.metaKey || e.ctrlKey) return true
|
||||||
|
|
||||||
|
// A key
|
||||||
if (e.keyCode === 65 && !e.shiftKey) {
|
if (e.keyCode === 65 && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ee.emit('top:new-note')
|
ee.emit('top:new-note')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// D key
|
||||||
if (e.keyCode === 68) {
|
if (e.keyCode === 68) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ee.emit('detail:delete')
|
this.deleteNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// E key
|
||||||
if (e.keyCode === 69) {
|
if (e.keyCode === 69) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.keyCode === 38) {
|
// L or S key
|
||||||
|
if (e.keyCode === 76 || e.keyCode === 83) {
|
||||||
|
e.preventDefault()
|
||||||
|
ee.emit('top:focus-search')
|
||||||
|
}
|
||||||
|
|
||||||
|
// UP or K key
|
||||||
|
if (e.keyCode === 38 || e.keyCode === 75) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.selectPriorNote()
|
this.selectPriorNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.keyCode === 40) {
|
// DOWN or J key
|
||||||
|
if (e.keyCode === 40 || e.keyCode === 74) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.selectNextNote()
|
this.selectNextNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
this.setState({ shiftKeyDown: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNoteListKeyUp (e) {
|
||||||
|
if (!e.shiftKey) {
|
||||||
|
this.setState({ shiftKeyDown: false })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNotes () {
|
getNotes () {
|
||||||
let { data, params, location } = this.props
|
const { data, params, location } = this.props
|
||||||
let { router } = this.context
|
|
||||||
|
|
||||||
if (location.pathname.match(/\/home/) || location.pathname.match(/\alltags/)) {
|
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
|
||||||
const allNotes = data.noteMap.map((note) => note)
|
const allNotes = data.noteMap.map((note) => note)
|
||||||
this.contextNotes = allNotes
|
this.contextNotes = allNotes
|
||||||
return allNotes
|
return allNotes
|
||||||
@@ -242,8 +329,10 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname.match(/\/searched/)) {
|
if (location.pathname.match(/\/searched/)) {
|
||||||
const searchInputText = document.getElementsByClassName('searchInput')[0].value
|
const searchInputText = params.searchword
|
||||||
if (searchInputText === '') {
|
const allNotes = data.noteMap.map((note) => note)
|
||||||
|
this.contextNotes = allNotes
|
||||||
|
if (searchInputText === undefined || searchInputText === '') {
|
||||||
return this.sortByPin(this.contextNotes)
|
return this.sortByPin(this.contextNotes)
|
||||||
}
|
}
|
||||||
return searchFromNotes(this.contextNotes, searchInputText)
|
return searchFromNotes(this.contextNotes, searchInputText)
|
||||||
@@ -256,11 +345,10 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (location.pathname.match(/\/tags/)) {
|
if (location.pathname.match(/\/tags/)) {
|
||||||
|
const listOfTags = params.tagname.split(' ')
|
||||||
return data.noteMap.map(note => {
|
return data.noteMap.map(note => {
|
||||||
return note
|
return note
|
||||||
}).filter(note => {
|
}).filter(note => listOfTags.every(tag => note.tags.includes(tag)))
|
||||||
return note.tags.includes(params.tagname)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getContextNotes()
|
return this.getContextNotes()
|
||||||
@@ -300,8 +388,25 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNoteClick (e, uniqueKey) {
|
handleNoteClick (e, uniqueKey) {
|
||||||
let { router } = this.context
|
const { router } = this.context
|
||||||
let { location } = this.props
|
const { location } = this.props
|
||||||
|
let { selectedNoteKeys } = this.state
|
||||||
|
const { shiftKeyDown } = this.state
|
||||||
|
|
||||||
|
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||||
|
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
||||||
|
this.setState({
|
||||||
|
selectedNoteKeys: newSelectedNoteKeys
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!shiftKeyDown) {
|
||||||
|
selectedNoteKeys = []
|
||||||
|
}
|
||||||
|
selectedNoteKeys.push(uniqueKey)
|
||||||
|
this.setState({
|
||||||
|
selectedNoteKeys
|
||||||
|
})
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
@@ -312,9 +417,9 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSortByChange (e) {
|
handleSortByChange (e) {
|
||||||
let { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
|
|
||||||
let config = {
|
const config = {
|
||||||
sortBy: e.target.value
|
sortBy: e.target.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,9 +431,9 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleListStyleButtonClick (e, style) {
|
handleListStyleButtonClick (e, style) {
|
||||||
let { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
|
|
||||||
let config = {
|
const config = {
|
||||||
listStyle: style
|
listStyle: style
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,73 +449,355 @@ class NoteList extends React.Component {
|
|||||||
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Sorry!',
|
message: i18n.__('Sorry!'),
|
||||||
detail: 'md/text import is available only a markdown note.',
|
detail: i18n.__('md/text import is available only a markdown note.'),
|
||||||
buttons: ['OK', 'Cancel']
|
buttons: [i18n.__('OK'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragStart (e, note) {
|
handleDragStart (e, note) {
|
||||||
const noteData = JSON.stringify(note)
|
let { selectedNoteKeys } = this.state
|
||||||
|
const noteKey = getNoteKey(note)
|
||||||
|
|
||||||
|
if (!selectedNoteKeys.includes(noteKey)) {
|
||||||
|
selectedNoteKeys = []
|
||||||
|
selectedNoteKeys.push(noteKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
|
const noteData = JSON.stringify(selectedNotes)
|
||||||
e.dataTransfer.setData('note', noteData)
|
e.dataTransfer.setData('note', noteData)
|
||||||
|
this.selectNextNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNoteContextMenu (e, uniqueKey) {
|
handleNoteContextMenu (e, uniqueKey) {
|
||||||
const { location } = this.props
|
const { location } = this.props
|
||||||
const note = this.notes.find((note) => {
|
const { selectedNoteKeys } = this.state
|
||||||
const noteKey = `${note.storage}-${note.key}`
|
const note = findNoteByKey(this.notes, uniqueKey)
|
||||||
return noteKey === uniqueKey
|
const noteKey = getNoteKey(note)
|
||||||
})
|
|
||||||
|
|
||||||
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top'
|
if (selectedNoteKeys.length === 0 || !selectedNoteKeys.includes(noteKey)) {
|
||||||
const deleteLabel = 'Delete Note'
|
this.handleNoteClick(e, uniqueKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinLabel = note.isPinned ? i18n.__('Remove pin') : i18n.__('Pin to Top')
|
||||||
|
const deleteLabel = i18n.__('Delete Note')
|
||||||
|
const cloneNote = i18n.__('Clone Note')
|
||||||
|
const restoreNote = i18n.__('Restore Note')
|
||||||
|
const copyNoteLink = i18n.__('Copy Note Link')
|
||||||
|
const publishLabel = i18n.__('Publish Blog')
|
||||||
|
const updateLabel = i18n.__('Update Blog')
|
||||||
|
const openBlogLabel = i18n.__('Open Blog')
|
||||||
|
|
||||||
const menu = new Menu()
|
const menu = new Menu()
|
||||||
if (!location.pathname.match(/\/home|\/starred|\/trash/)) {
|
|
||||||
|
if (location.pathname.match(/\/trash/)) {
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: restoreNote,
|
||||||
|
click: this.restoreNote
|
||||||
|
}))
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: deleteLabel,
|
||||||
|
click: this.deleteNote
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
if (!location.pathname.match(/\/starred/)) {
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: pinLabel,
|
label: pinLabel,
|
||||||
click: (e) => this.pinToTop(e, uniqueKey)
|
click: this.pinToTop
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: deleteLabel,
|
label: deleteLabel,
|
||||||
click: (e) => this.deleteNote(e, uniqueKey)
|
click: this.deleteNote
|
||||||
}))
|
}))
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: cloneNote,
|
||||||
|
click: this.cloneNote.bind(this)
|
||||||
|
}))
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: copyNoteLink,
|
||||||
|
click: this.copyNoteLink(note)
|
||||||
|
}))
|
||||||
|
if (note.type === 'MARKDOWN_NOTE') {
|
||||||
|
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: updateLabel,
|
||||||
|
click: this.publishMarkdown.bind(this)
|
||||||
|
}))
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: openBlogLabel,
|
||||||
|
click: () => this.openBlog.bind(this)(note)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: publishLabel,
|
||||||
|
click: this.publishMarkdown.bind(this)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
menu.popup()
|
menu.popup()
|
||||||
}
|
}
|
||||||
|
|
||||||
pinToTop (e, uniqueKey) {
|
updateSelectedNotes (updateFunc, cleanSelection = true) {
|
||||||
const { data, params } = this.props
|
const { selectedNoteKeys } = this.state
|
||||||
const storageKey = params.storageKey
|
const { dispatch } = this.props
|
||||||
const folderKey = params.folderKey
|
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
|
|
||||||
const currentStorage = data.storageMap.get(storageKey)
|
if (!_.isFunction(updateFunc)) {
|
||||||
const currentFolder = _.find(currentStorage.folders, {key: folderKey})
|
console.warn('Update function is not defined. No update will happen')
|
||||||
|
updateFunc = (note) => { return note }
|
||||||
|
}
|
||||||
|
|
||||||
this.handleNoteClick(e, uniqueKey)
|
Promise.all(
|
||||||
const targetIndex = this.getTargetIndex()
|
selectedNotes.map((note) => {
|
||||||
let note = this.notes[targetIndex]
|
note = updateFunc(note)
|
||||||
|
return dataApi
|
||||||
|
.updateNote(note.storage, note.key, note)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then((updatedNotes) => {
|
||||||
|
updatedNotes.forEach((note) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (cleanSelection) {
|
||||||
|
this.selectNextNote()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pinToTop () {
|
||||||
|
this.updateSelectedNotes((note) => {
|
||||||
note.isPinned = !note.isPinned
|
note.isPinned = !note.isPinned
|
||||||
|
return note
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreNote () {
|
||||||
|
this.updateSelectedNotes((note) => {
|
||||||
|
note.isTrashed = false
|
||||||
|
return note
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNote () {
|
||||||
|
const { dispatch } = this.props
|
||||||
|
const { selectedNoteKeys } = this.state
|
||||||
|
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
|
const firstNote = selectedNotes[0]
|
||||||
|
const { confirmDeletion } = this.props.config.ui
|
||||||
|
|
||||||
|
if (firstNote.isTrashed) {
|
||||||
|
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
selectedNotes.map((note) => {
|
||||||
|
return dataApi
|
||||||
|
.deleteNote(note.storage, note.key)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then((data) => {
|
||||||
|
data.forEach((item) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'DELETE_NOTE',
|
||||||
|
storageKey: item.storageKey,
|
||||||
|
noteKey: item.noteKey
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Cannot Delete note: ' + err)
|
||||||
|
})
|
||||||
|
console.log('Notes were all deleted')
|
||||||
|
} else {
|
||||||
|
if (!confirmDeleteNote(confirmDeletion, false)) return
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
selectedNotes.map((note) => {
|
||||||
|
note.isTrashed = true
|
||||||
|
|
||||||
|
return dataApi
|
||||||
|
.updateNote(note.storage, note.key, note)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then((newNotes) => {
|
||||||
|
newNotes.forEach((newNote) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: newNote
|
||||||
|
})
|
||||||
|
})
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||||
|
console.log('Notes went to trash')
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Notes could not go to trash: ' + err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.setState({ selectedNoteKeys: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneNote () {
|
||||||
|
const { selectedNoteKeys } = this.state
|
||||||
|
const { dispatch, location } = this.props
|
||||||
|
const { storage, folder } = this.resolveTargetFolder()
|
||||||
|
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
|
const firstNote = selectedNotes[0]
|
||||||
|
const eventName = firstNote.type === 'MARKDOWN_NOTE' ? 'ADD_MARKDOWN' : 'ADD_SNIPPET'
|
||||||
|
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent(eventName)
|
||||||
|
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||||
|
dataApi
|
||||||
|
.createNote(storage.key, {
|
||||||
|
type: firstNote.type,
|
||||||
|
folder: folder.key,
|
||||||
|
title: firstNote.title + ' ' + i18n.__('copy'),
|
||||||
|
content: firstNote.content
|
||||||
|
})
|
||||||
|
.then((note) => {
|
||||||
|
attachmentManagement.cloneAttachments(firstNote, note)
|
||||||
|
return note
|
||||||
|
})
|
||||||
|
.then((note) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_NOTE',
|
||||||
|
note: note
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedNoteKeys: [note.key]
|
||||||
|
})
|
||||||
|
|
||||||
|
hashHistory.push({
|
||||||
|
pathname: location.pathname,
|
||||||
|
query: {key: note.key}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
copyNoteLink (note) {
|
||||||
|
const noteLink = `[${note.title}](:note:${note.key})`
|
||||||
|
return copy(noteLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
save (note) {
|
||||||
|
const { dispatch } = this.props
|
||||||
dataApi
|
dataApi
|
||||||
.updateNote(note.storage, note.key, note)
|
.updateNote(note.storage, note.key, note)
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
store.dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteNote (e, uniqueKey) {
|
publishMarkdown () {
|
||||||
this.handleNoteClick(e, uniqueKey)
|
if (this.pendingPublish) {
|
||||||
ee.emit('detail:delete')
|
clearTimeout(this.pendingPublish)
|
||||||
|
}
|
||||||
|
this.pendingPublish = setTimeout(() => {
|
||||||
|
this.publishMarkdownNow()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishMarkdownNow () {
|
||||||
|
const {selectedNoteKeys} = this.state
|
||||||
|
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
|
const firstNote = selectedNotes[0]
|
||||||
|
const config = ConfigManager.get()
|
||||||
|
const {address, token, authMethod, username, password} = config.blog
|
||||||
|
let authToken = ''
|
||||||
|
if (authMethod === 'USER') {
|
||||||
|
authToken = `Basic ${window.btoa(`${username}:${password}`)}`
|
||||||
|
} else {
|
||||||
|
authToken = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
const contentToRender = firstNote.content.replace(`# ${firstNote.title}`, '')
|
||||||
|
const markdown = new Markdown()
|
||||||
|
const data = {
|
||||||
|
title: firstNote.title,
|
||||||
|
content: markdown.render(contentToRender),
|
||||||
|
status: 'publish'
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = ''
|
||||||
|
let method = ''
|
||||||
|
if (firstNote.blog && firstNote.blog.blogId) {
|
||||||
|
url = `${address}${WP_POST_PATH}/${firstNote.blog.blogId}`
|
||||||
|
method = 'PUT'
|
||||||
|
} else {
|
||||||
|
url = `${address}${WP_POST_PATH}`
|
||||||
|
method = 'POST'
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
fetch(url, {
|
||||||
|
method: method,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: {
|
||||||
|
'Authorization': authToken,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(res => res.json())
|
||||||
|
.then(response => {
|
||||||
|
if (_.isNil(response.link) || _.isNil(response.id)) {
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
firstNote.blog = {
|
||||||
|
blogLink: response.link,
|
||||||
|
blogId: response.id
|
||||||
|
}
|
||||||
|
this.save(firstNote)
|
||||||
|
this.confirmPublish(firstNote)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
this.confirmPublishError()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmPublishError () {
|
||||||
|
const { remote } = electron
|
||||||
|
const { dialog } = remote
|
||||||
|
const alertError = {
|
||||||
|
type: 'warning',
|
||||||
|
message: i18n.__('Publish Failed'),
|
||||||
|
detail: i18n.__('Check and update your blog setting and try again.'),
|
||||||
|
buttons: [i18n.__('Confirm')]
|
||||||
|
}
|
||||||
|
dialog.showMessageBox(remote.getCurrentWindow(), alertError)
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmPublish (note) {
|
||||||
|
const buttonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
type: 'warning',
|
||||||
|
message: i18n.__('Publish Succeeded'),
|
||||||
|
detail: `${note.title} is published at ${note.blog.blogLink}`,
|
||||||
|
buttons: [i18n.__('Confirm'), i18n.__('Open Blog')]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (buttonIndex === 1) {
|
||||||
|
this.openBlog(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openBlog (note) {
|
||||||
|
const { shell } = electron
|
||||||
|
shell.openExternal(note.blog.blogLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
importFromFile () {
|
importFromFile () {
|
||||||
const { dispatch, location } = this.props
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
filters: [
|
filters: [
|
||||||
{ name: 'Documents', extensions: ['md', 'txt'] }
|
{ name: 'Documents', extensions: ['md', 'txt'] }
|
||||||
@@ -439,12 +826,18 @@ class NoteList extends React.Component {
|
|||||||
filepaths.forEach((filepath) => {
|
filepaths.forEach((filepath) => {
|
||||||
fs.readFile(filepath, (err, data) => {
|
fs.readFile(filepath, (err, data) => {
|
||||||
if (err) throw Error('File reading error: ', err)
|
if (err) throw Error('File reading error: ', err)
|
||||||
|
|
||||||
|
fs.stat(filepath, (err, {mtime, birthtime}) => {
|
||||||
|
if (err) throw Error('File stat reading error: ', err)
|
||||||
|
|
||||||
const content = data.toString()
|
const content = data.toString()
|
||||||
const newNote = {
|
const newNote = {
|
||||||
content: content,
|
content: content,
|
||||||
folder: folder.key,
|
folder: folder.key,
|
||||||
title: markdown.strip(findNoteTitle(content)),
|
title: path.basename(filepath, path.extname(filepath)),
|
||||||
type: 'MARKDOWN_NOTE'
|
type: 'MARKDOWN_NOTE',
|
||||||
|
createdAt: birthtime,
|
||||||
|
updatedAt: mtime
|
||||||
}
|
}
|
||||||
dataApi.createNote(storage.key, newNote)
|
dataApi.createNote(storage.key, newNote)
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
@@ -454,7 +847,8 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
hashHistory.push({
|
hashHistory.push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {key: `${note.storage}-${note.key}`}
|
query: {key: getNoteKey(note)}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -464,7 +858,7 @@ class NoteList extends React.Component {
|
|||||||
getTargetIndex () {
|
getTargetIndex () {
|
||||||
const { location } = this.props
|
const { location } = this.props
|
||||||
const targetIndex = _.findIndex(this.notes, (note) => {
|
const targetIndex = _.findIndex(this.notes, (note) => {
|
||||||
return `${note.storage}-${note.key}` === location.query.key
|
return getNoteKey(note) === location.query.key
|
||||||
})
|
})
|
||||||
return targetIndex
|
return targetIndex
|
||||||
}
|
}
|
||||||
@@ -475,7 +869,7 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
// Find first storage
|
// Find first storage
|
||||||
if (storage == null) {
|
if (storage == null) {
|
||||||
for (let kv of data.storageMap) {
|
for (const kv of data.storageMap) {
|
||||||
storage = kv[1]
|
storage = kv[1]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -495,18 +889,38 @@ class NoteList extends React.Component {
|
|||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: message,
|
message: message,
|
||||||
buttons: ['OK']
|
buttons: [i18n.__('OK')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNoteStorage (note) { // note.storage = storage key
|
||||||
|
return this.props.data.storageMap.toJS()[note.storage]
|
||||||
|
}
|
||||||
|
|
||||||
|
getNoteFolder (note) { // note.folder = folder key
|
||||||
|
return _.find(this.getNoteStorage(note).folders, ({ key }) => key === note.folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewType () {
|
||||||
|
const { pathname } = this.props.location
|
||||||
|
const folder = /\/folders\/[a-zA-Z0-9]+/.test(pathname)
|
||||||
|
const storage = /\/storages\/[a-zA-Z0-9]+/.test(pathname) && !folder
|
||||||
|
const allNotes = pathname === '/home'
|
||||||
|
if (allNotes) return 'ALL'
|
||||||
|
if (folder) return 'FOLDER'
|
||||||
|
if (storage) return 'STORAGE'
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { location, notes, config, dispatch } = this.props
|
const { location, config } = this.props
|
||||||
let sortFunc = config.sortBy === 'CREATED_AT'
|
let { notes } = this.props
|
||||||
|
const { selectedNoteKeys } = this.state
|
||||||
|
const sortFunc = config.sortBy === 'CREATED_AT'
|
||||||
? sortByCreatedAt
|
? sortByCreatedAt
|
||||||
: config.sortBy === 'ALPHABETICAL'
|
: config.sortBy === 'ALPHABETICAL'
|
||||||
? sortByAlphabetical
|
? sortByAlphabetical
|
||||||
: sortByUpdatedAt
|
: sortByUpdatedAt
|
||||||
const sortedNotes = location.pathname.match(/\/home|\/starred|\/trash/)
|
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||||
? this.getNotes().sort(sortFunc)
|
? this.getNotes().sort(sortFunc)
|
||||||
: this.sortByPin(this.getNotes().sort(sortFunc))
|
: this.sortByPin(this.getNotes().sort(sortFunc))
|
||||||
this.notes = notes = sortedNotes.filter((note) => {
|
this.notes = notes = sortedNotes.filter((note) => {
|
||||||
@@ -514,7 +928,7 @@ class NoteList extends React.Component {
|
|||||||
if (note.isTrashed !== true || location.pathname === '/trashed') return true
|
if (note.isTrashed !== true || location.pathname === '/trashed') return true
|
||||||
})
|
})
|
||||||
|
|
||||||
moment.locale('en', {
|
moment.updateLocale('en', {
|
||||||
relativeTime: {
|
relativeTime: {
|
||||||
future: 'in %s',
|
future: 'in %s',
|
||||||
past: '%s ago',
|
past: '%s ago',
|
||||||
@@ -533,19 +947,30 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let noteList = notes
|
const viewType = this.getViewType()
|
||||||
.map(note => {
|
|
||||||
|
const autoSelectFirst =
|
||||||
|
notes.length === 1 ||
|
||||||
|
selectedNoteKeys.length === 0 ||
|
||||||
|
notes.every(note => !selectedNoteKeys.includes(note.key))
|
||||||
|
|
||||||
|
const noteList = notes
|
||||||
|
.map((note, index) => {
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDefault = config.listStyle === 'DEFAULT'
|
const isDefault = config.listStyle === 'DEFAULT'
|
||||||
const isActive = location.query.key === note.storage + '-' + note.key
|
const uniqueKey = getNoteKey(note)
|
||||||
|
|
||||||
|
const isActive =
|
||||||
|
selectedNoteKeys.includes(uniqueKey) ||
|
||||||
|
notes.length === 1 ||
|
||||||
|
(autoSelectFirst && index === 0)
|
||||||
const dateDisplay = moment(
|
const dateDisplay = moment(
|
||||||
config.sortBy === 'CREATED_AT'
|
config.sortBy === 'CREATED_AT'
|
||||||
? note.createdAt : note.updatedAt
|
? note.createdAt : note.updatedAt
|
||||||
).fromNow('D')
|
).fromNow('D')
|
||||||
const key = `${note.storage}-${note.key}`
|
|
||||||
|
|
||||||
if (isDefault) {
|
if (isDefault) {
|
||||||
return (
|
return (
|
||||||
@@ -553,11 +978,14 @@ class NoteList extends React.Component {
|
|||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
note={note}
|
note={note}
|
||||||
dateDisplay={dateDisplay}
|
dateDisplay={dateDisplay}
|
||||||
key={key}
|
key={uniqueKey}
|
||||||
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
||||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||||
handleDragStart={this.handleDragStart.bind(this)}
|
handleDragStart={this.handleDragStart.bind(this)}
|
||||||
pathname={location.pathname}
|
pathname={location.pathname}
|
||||||
|
folderName={this.getNoteFolder(note).name}
|
||||||
|
storageName={this.getNoteStorage(note).name}
|
||||||
|
viewType={viewType}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -566,10 +994,14 @@ class NoteList extends React.Component {
|
|||||||
<NoteItemSimple
|
<NoteItemSimple
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
note={note}
|
note={note}
|
||||||
key={key}
|
key={uniqueKey}
|
||||||
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
||||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||||
handleDragStart={this.handleDragStart.bind(this)}
|
handleDragStart={this.handleDragStart.bind(this)}
|
||||||
|
pathname={location.pathname}
|
||||||
|
folderName={this.getNoteFolder(note).name}
|
||||||
|
storageName={this.getNoteStorage(note).name}
|
||||||
|
viewType={viewType}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -584,16 +1016,17 @@ class NoteList extends React.Component {
|
|||||||
<div styleName='control-sortBy'>
|
<div styleName='control-sortBy'>
|
||||||
<i className='fa fa-angle-down' />
|
<i className='fa fa-angle-down' />
|
||||||
<select styleName='control-sortBy-select'
|
<select styleName='control-sortBy-select'
|
||||||
|
title={i18n.__('Select filter mode')}
|
||||||
value={config.sortBy}
|
value={config.sortBy}
|
||||||
onChange={(e) => this.handleSortByChange(e)}
|
onChange={(e) => this.handleSortByChange(e)}
|
||||||
>
|
>
|
||||||
<option value='UPDATED_AT'>Updated</option>
|
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
|
||||||
<option value='CREATED_AT'>Created</option>
|
<option title='Sort by create time' value='CREATED_AT'>{i18n.__('Created')}</option>
|
||||||
<option value='ALPHABETICAL'>Alphabetically</option>
|
<option title='Sort alphabetically' value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='control-button-area'>
|
<div styleName='control-button-area'>
|
||||||
<button styleName={config.listStyle === 'DEFAULT'
|
<button title={i18n.__('Default View')} styleName={config.listStyle === 'DEFAULT'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
: 'control-button'
|
: 'control-button'
|
||||||
}
|
}
|
||||||
@@ -601,7 +1034,7 @@ class NoteList extends React.Component {
|
|||||||
>
|
>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
|
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
|
||||||
</button>
|
</button>
|
||||||
<button styleName={config.listStyle === 'SMALL'
|
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
: 'control-button'
|
: 'control-button'
|
||||||
}
|
}
|
||||||
@@ -615,6 +1048,7 @@ class NoteList extends React.Component {
|
|||||||
ref='list'
|
ref='list'
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
|
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
|
||||||
|
onKeyUp={this.handleNoteListKeyUp}
|
||||||
>
|
>
|
||||||
{noteList}
|
{noteList}
|
||||||
</div>
|
</div>
|
||||||
@@ -634,4 +1068,4 @@ NoteList.propTypes = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(NoteList, styles)
|
export default debounceRender(CSSModules(NoteList, styles))
|
||||||
|
|||||||
25
browser/main/SideNav/ListButton.js
Normal file
25
browser/main/SideNav/ListButton.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './SwitchButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const ListButton = ({
|
||||||
|
onClick, isTagActive
|
||||||
|
}) => (
|
||||||
|
<button styleName={isTagActive ? 'non-active-button' : 'active-button'} onClick={onClick}>
|
||||||
|
<img src={isTagActive
|
||||||
|
? '../resources/icon/icon-list.svg'
|
||||||
|
: '../resources/icon/icon-list-active.svg'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span styleName='tooltip'>{i18n.__('Notes')}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
ListButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
isTagActive: PropTypes.bool.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(ListButton, styles)
|
||||||
20
browser/main/SideNav/PreferenceButton.js
Normal file
20
browser/main/SideNav/PreferenceButton.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './PreferenceButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const PreferenceButton = ({
|
||||||
|
onClick
|
||||||
|
}) => (
|
||||||
|
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
|
||||||
|
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
|
||||||
|
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
PreferenceButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(PreferenceButton, styles)
|
||||||
52
browser/main/SideNav/PreferenceButton.styl
Normal file
52
browser/main/SideNav/PreferenceButton.styl
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
.top-menu-preference
|
||||||
|
navButtonColor()
|
||||||
|
position absolute
|
||||||
|
top 22px
|
||||||
|
right 10px
|
||||||
|
width 2em
|
||||||
|
background-color transparent
|
||||||
|
&:hover
|
||||||
|
color $ui-button-default--active-backgroundColor
|
||||||
|
background-color transparent
|
||||||
|
.tooltip
|
||||||
|
opacity 1
|
||||||
|
&:active, &:active:hover
|
||||||
|
color $ui-button-default--active-backgroundColor
|
||||||
|
|
||||||
|
body[data-theme="white"]
|
||||||
|
.top-menu-preference
|
||||||
|
navWhiteButtonColor()
|
||||||
|
background-color transparent
|
||||||
|
&:hover
|
||||||
|
color #0B99F1
|
||||||
|
background-color transparent
|
||||||
|
&:active, &:active:hover
|
||||||
|
color #0B99F1
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
body[data-theme="dark"]
|
||||||
|
.top-menu-preference
|
||||||
|
navDarkButtonColor()
|
||||||
|
background-color transparent
|
||||||
|
&:active
|
||||||
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
background-color transparent
|
||||||
|
&:hover
|
||||||
|
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
position absolute
|
||||||
|
pointer-events none
|
||||||
|
top 26px
|
||||||
|
left -20px
|
||||||
|
z-index 200
|
||||||
|
padding 5px
|
||||||
|
line-height normal
|
||||||
|
border-radius 2px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
white-space nowrap
|
||||||
@@ -11,19 +11,6 @@
|
|||||||
.top
|
.top
|
||||||
padding-bottom 15px
|
padding-bottom 15px
|
||||||
|
|
||||||
.top-menu-preference
|
|
||||||
navButtonColor()
|
|
||||||
position absolute
|
|
||||||
top 22px
|
|
||||||
right 10px
|
|
||||||
width 2em
|
|
||||||
background-color transparent
|
|
||||||
&:hover
|
|
||||||
color $ui-button-default--active-backgroundColor
|
|
||||||
background-color transparent
|
|
||||||
&:active, &:active:hover
|
|
||||||
color $ui-button-default--active-backgroundColor
|
|
||||||
|
|
||||||
.switch-buttons
|
.switch-buttons
|
||||||
background-color transparent
|
background-color transparent
|
||||||
border 0
|
border 0
|
||||||
@@ -31,21 +18,7 @@
|
|||||||
display flex
|
display flex
|
||||||
text-align center
|
text-align center
|
||||||
|
|
||||||
.non-active-button
|
|
||||||
color $ui-inactive-text-color
|
|
||||||
font-size 16px
|
|
||||||
border 0
|
|
||||||
background-color transparent
|
|
||||||
transition 0.2s
|
|
||||||
display flex
|
|
||||||
text-align center
|
|
||||||
margin-right 4px;
|
|
||||||
&:hover
|
|
||||||
color alpha(#239F86, 60%)
|
|
||||||
|
|
||||||
.active-button
|
|
||||||
@extend .non-active-button
|
|
||||||
color $ui-button-default--active-backgroundColor
|
|
||||||
|
|
||||||
.top-menu-label
|
.top-menu-label
|
||||||
margin-left 5px
|
margin-left 5px
|
||||||
@@ -57,11 +30,33 @@
|
|||||||
display flex
|
display flex
|
||||||
flex-direction column
|
flex-direction column
|
||||||
|
|
||||||
.tag-title
|
.tag-control
|
||||||
|
display flex
|
||||||
|
height 30px
|
||||||
|
line-height 25px
|
||||||
|
overflow hidden
|
||||||
|
.tag-control-title
|
||||||
padding-left 15px
|
padding-left 15px
|
||||||
padding-bottom 13px
|
padding-bottom 13px
|
||||||
|
flex 1
|
||||||
p
|
p
|
||||||
color $ui-button-default-color
|
color $ui-button-default-color
|
||||||
|
.tag-control-sortTagsBy
|
||||||
|
user-select none
|
||||||
|
font-size 12px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
margin-left 12px
|
||||||
|
margin-right 12px
|
||||||
|
.tag-control-sortTagsBy-select
|
||||||
|
appearance: none;
|
||||||
|
margin-left 5px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
padding 0
|
||||||
|
border none
|
||||||
|
background-color transparent
|
||||||
|
outline none
|
||||||
|
cursor pointer
|
||||||
|
font-size 12px
|
||||||
|
|
||||||
.tagList
|
.tagList
|
||||||
overflow-y auto
|
overflow-y auto
|
||||||
@@ -102,64 +97,28 @@
|
|||||||
font-size 13px
|
font-size 13px
|
||||||
.top-menu-preference
|
.top-menu-preference
|
||||||
position absolute
|
position absolute
|
||||||
left 11px
|
left 7px
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.root, .root--folded
|
.root, .root--folded
|
||||||
background-color #f9f9f9
|
background-color #f9f9f9
|
||||||
color $ui-text-color
|
color $ui-text-color
|
||||||
|
|
||||||
.top-menu-preference
|
|
||||||
navWhiteButtonColor()
|
|
||||||
background-color transparent
|
|
||||||
&:hover
|
|
||||||
color #0B99F1
|
|
||||||
background-color transparent
|
|
||||||
&:active, &:active:hover
|
|
||||||
color #0B99F1
|
|
||||||
background-color transparent
|
|
||||||
|
|
||||||
.non-active-button
|
|
||||||
color $ui-inactive-text-color
|
|
||||||
&:hover
|
|
||||||
color alpha(#0B99F1, 60%)
|
|
||||||
|
|
||||||
.tag-title
|
|
||||||
p
|
|
||||||
color $ui-text-color
|
|
||||||
|
|
||||||
.non-active-button
|
|
||||||
&:hover
|
|
||||||
color alpha(#0B99F1, 60%)
|
|
||||||
|
|
||||||
.active-button
|
|
||||||
@extend .non-active-button
|
|
||||||
color #0B99F1
|
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
.root, .root--folded
|
.root, .root--folded
|
||||||
border-color $ui-dark-borderColor
|
border-right 1px solid $ui-dark-borderColor
|
||||||
background-color $ui-dark-backgroundColor
|
background-color $ui-dark-backgroundColor
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
|
|
||||||
.top
|
.top
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
|
|
||||||
.top-menu-preference
|
body[data-theme="solarized-dark"]
|
||||||
navDarkButtonColor()
|
.root, .root--folded
|
||||||
background-color transparent
|
background-color $ui-solarized-dark-backgroundColor
|
||||||
&:active
|
border-right 1px solid $ui-solarized-dark-borderColor
|
||||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
|
||||||
background-color transparent
|
|
||||||
&:hover
|
|
||||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
|
||||||
background-color transparent
|
|
||||||
|
|
||||||
.non-active-button
|
body[data-theme="monokai"]
|
||||||
color alpha($ui-dark-text-color, 60%)
|
.root, .root--folded
|
||||||
&:hover
|
background-color $ui-monokai-backgroundColor
|
||||||
color alpha(#0B99F1, 60%)
|
border-right 1px solid $ui-monokai-borderColor
|
||||||
|
|
||||||
.tag-title
|
|
||||||
p
|
|
||||||
color alpha($ui-dark-text-color, 60%)
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './StorageItem.styl'
|
import styles from './StorageItem.styl'
|
||||||
import { hashHistory } from 'react-router'
|
import { hashHistory } from 'react-router'
|
||||||
@@ -7,10 +8,14 @@ import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
|
|||||||
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import StorageItemChild from 'browser/components/StorageItem'
|
import StorageItemChild from 'browser/components/StorageItem'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import _ from 'lodash'
|
||||||
|
import { SortableElement } from 'react-sortable-hoc'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { Menu, MenuItem, dialog } = remote
|
const { Menu, dialog } = remote
|
||||||
|
const escapeStringRegexp = require('escape-string-regexp')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
class StorageItem extends React.Component {
|
class StorageItem extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -22,31 +27,33 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleHeaderContextMenu (e) {
|
handleHeaderContextMenu (e) {
|
||||||
let menu = new Menu()
|
const menu = Menu.buildFromTemplate([
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
label: 'Add Folder',
|
label: i18n.__('Add Folder'),
|
||||||
click: (e) => this.handleAddFolderButtonClick(e)
|
click: (e) => this.handleAddFolderButtonClick(e)
|
||||||
}))
|
},
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}))
|
},
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
label: 'Unlink Storage',
|
label: i18n.__('Unlink Storage'),
|
||||||
click: (e) => this.handleUnlinkStorageClick(e)
|
click: (e) => this.handleUnlinkStorageClick(e)
|
||||||
}))
|
}
|
||||||
|
])
|
||||||
|
|
||||||
menu.popup()
|
menu.popup()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnlinkStorageClick (e) {
|
handleUnlinkStorageClick (e) {
|
||||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Unlink Storage',
|
message: i18n.__('Unlink Storage'),
|
||||||
detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)',
|
detail: i18n.__('This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)'),
|
||||||
buttons: ['Confirm', 'Cancel']
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
let { storage, dispatch } = this.props
|
const { storage, dispatch } = this.props
|
||||||
dataApi.removeStorage(storage.key)
|
dataApi.removeStorage(storage.key)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -67,7 +74,7 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleAddFolderButtonClick (e) {
|
handleAddFolderButtonClick (e) {
|
||||||
let { storage } = this.props
|
const { storage } = this.props
|
||||||
|
|
||||||
modal.open(CreateFolderModal, {
|
modal.open(CreateFolderModal, {
|
||||||
storage
|
storage
|
||||||
@@ -75,51 +82,94 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleHeaderInfoClick (e) {
|
handleHeaderInfoClick (e) {
|
||||||
let { storage } = this.props
|
const { storage } = this.props
|
||||||
hashHistory.push('/storages/' + storage.key)
|
hashHistory.push('/storages/' + storage.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFolderButtonClick (folderKey) {
|
handleFolderButtonClick (folderKey) {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
let { storage } = this.props
|
const { storage } = this.props
|
||||||
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
|
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFolderButtonContextMenu (e, folder) {
|
handleFolderButtonContextMenu (e, folder) {
|
||||||
let menu = new Menu()
|
const menu = Menu.buildFromTemplate([
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
label: 'Rename Folder',
|
label: i18n.__('Rename Folder'),
|
||||||
click: (e) => this.handleRenameFolderClick(e, folder)
|
click: (e) => this.handleRenameFolderClick(e, folder)
|
||||||
}))
|
},
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}))
|
},
|
||||||
menu.append(new MenuItem({
|
{
|
||||||
label: 'Delete Folder',
|
label: i18n.__('Export Folder'),
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: i18n.__('Export as txt'),
|
||||||
|
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.__('Export as md'),
|
||||||
|
click: (e) => this.handleExportFolderClick(e, folder, 'md')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.__('Delete Folder'),
|
||||||
click: (e) => this.handleFolderDeleteClick(e, folder)
|
click: (e) => this.handleFolderDeleteClick(e, folder)
|
||||||
}))
|
}
|
||||||
|
])
|
||||||
|
|
||||||
menu.popup()
|
menu.popup()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRenameFolderClick (e, folder) {
|
handleRenameFolderClick (e, folder) {
|
||||||
let { storage } = this.props
|
const { storage } = this.props
|
||||||
modal.open(RenameFolderModal, {
|
modal.open(RenameFolderModal, {
|
||||||
storage,
|
storage,
|
||||||
folder
|
folder
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleExportFolderClick (e, folder, fileType) {
|
||||||
|
const options = {
|
||||||
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
|
buttonLabel: i18n.__('Select directory'),
|
||||||
|
title: i18n.__('Select a folder to export the files to'),
|
||||||
|
multiSelections: false
|
||||||
|
}
|
||||||
|
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
||||||
|
(paths) => {
|
||||||
|
if (paths && paths.length === 1) {
|
||||||
|
const { storage, dispatch } = this.props
|
||||||
|
dataApi
|
||||||
|
.exportFolder(storage.key, folder.key, fileType, paths[0])
|
||||||
|
.then((data) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'EXPORT_FOLDER',
|
||||||
|
storage: data.storage,
|
||||||
|
folderKey: data.folderKey,
|
||||||
|
fileType: data.fileType
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleFolderDeleteClick (e, folder) {
|
handleFolderDeleteClick (e, folder) {
|
||||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Delete Folder',
|
message: i18n.__('Delete Folder'),
|
||||||
detail: 'This will delete all notes in the folder and can not be undone.',
|
detail: i18n.__('This will delete all notes in the folder and can not be undone.'),
|
||||||
buttons: ['Confirm', 'Cancel']
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
let { storage, dispatch } = this.props
|
const { storage, dispatch } = this.props
|
||||||
dataApi
|
dataApi
|
||||||
.deleteFolder(storage.key, folder.key)
|
.deleteFolder(storage.key, folder.key)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -142,48 +192,42 @@ class StorageItem extends React.Component {
|
|||||||
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dropNote (storage, folder, dispatch, location, noteData) {
|
||||||
|
noteData = noteData.filter((note) => folder.key !== note.folder)
|
||||||
|
if (noteData.length === 0) return
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
noteData.map((note) => dataApi.moveNote(note.storage, note.key, storage.key, folder.key))
|
||||||
|
)
|
||||||
|
.then((createdNoteData) => {
|
||||||
|
createdNoteData.forEach((newNote) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'MOVE_NOTE',
|
||||||
|
originNote: noteData.find((note) => note.content === newNote.oldContent),
|
||||||
|
note: newNote
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`error on delete notes: ${err}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleDrop (e, storage, folder, dispatch, location) {
|
handleDrop (e, storage, folder, dispatch, location) {
|
||||||
e.target.style.opacity = '1'
|
e.target.style.opacity = '1'
|
||||||
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
||||||
const noteData = JSON.parse(e.dataTransfer.getData('note'))
|
const noteData = JSON.parse(e.dataTransfer.getData('note'))
|
||||||
const newNoteData = Object.assign({}, noteData, {storage: storage, folder: folder.key})
|
this.dropNote(storage, folder, dispatch, location, noteData)
|
||||||
if (folder.key === noteData.folder) return
|
|
||||||
dataApi
|
|
||||||
.createNote(storage.key, newNoteData)
|
|
||||||
.then((note) => {
|
|
||||||
dataApi
|
|
||||||
.deleteNote(noteData.storage, noteData.key)
|
|
||||||
.then((data) => {
|
|
||||||
let dispatchHandler = () => {
|
|
||||||
dispatch({
|
|
||||||
type: 'DELETE_NOTE',
|
|
||||||
storageKey: data.storageKey,
|
|
||||||
noteKey: data.noteKey
|
|
||||||
})
|
|
||||||
}
|
|
||||||
eventEmitter.once('list:moved', dispatchHandler)
|
|
||||||
eventEmitter.emit('list:next')
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_NOTE',
|
|
||||||
note: note
|
|
||||||
})
|
|
||||||
hashHistory.push({
|
|
||||||
pathname: location.pathname,
|
|
||||||
query: {key: `${note.storage}-${note.key}`}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { storage, location, isFolded, data, dispatch } = this.props
|
const { storage, location, isFolded, data, dispatch } = this.props
|
||||||
let { folderNoteMap, trashedSet } = data
|
const { folderNoteMap, trashedSet } = data
|
||||||
let folderList = storage.folders.map((folder) => {
|
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||||
let isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
|
const folderList = storage.folders.map((folder, index) => {
|
||||||
let noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
||||||
|
const isActive = !!(location.pathname.match(folderRegex))
|
||||||
|
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||||
|
|
||||||
let noteCount = 0
|
let noteCount = 0
|
||||||
if (noteSet) {
|
if (noteSet) {
|
||||||
@@ -195,8 +239,9 @@ class StorageItem extends React.Component {
|
|||||||
noteCount = noteSet.size - trashedNoteCount
|
noteCount = noteSet.size - trashedNoteCount
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<StorageItemChild
|
<SortableStorageItemChild
|
||||||
key={folder.key}
|
key={folder.key}
|
||||||
|
index={index}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||||
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
||||||
@@ -211,7 +256,7 @@ class StorageItem extends React.Component {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
|
const isActive = location.pathname.match(new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + '$'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div styleName={isFolded ? 'root--folded' : 'root'}
|
<div styleName={isFolded ? 'root--folded' : 'root'}
|
||||||
@@ -245,7 +290,7 @@ class StorageItem extends React.Component {
|
|||||||
onClick={(e) => this.handleHeaderInfoClick(e)}
|
onClick={(e) => this.handleHeaderInfoClick(e)}
|
||||||
>
|
>
|
||||||
<span styleName='header-info-name'>
|
<span styleName='header-info-name'>
|
||||||
{isFolded ? storage.name.substring(0, 1) : storage.name}
|
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
|
||||||
</span>
|
</span>
|
||||||
{isFolded &&
|
{isFolded &&
|
||||||
<span styleName='header-info--folded-tooltip'>
|
<span styleName='header-info--folded-tooltip'>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user