mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 10:16:26 +00:00
Compare commits
606 Commits
v0.11.2
...
revert-fli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d015b18c66 | ||
|
|
039f73711a | ||
|
|
6e885acf8c | ||
|
|
9ef07cea7a | ||
|
|
9e3b321aaf | ||
|
|
21560701ea | ||
|
|
4556375174 | ||
|
|
91b5398b5a | ||
|
|
eeb8016992 | ||
|
|
736106be3a | ||
|
|
f400568dc0 | ||
|
|
0ca96cba6e | ||
|
|
df4d837026 | ||
|
|
760f84d7fa | ||
|
|
174a315e3f | ||
|
|
0834313456 | ||
|
|
df931e10c0 | ||
|
|
9572cb2d33 | ||
|
|
51e836f32a | ||
|
|
7fefbd88d0 | ||
|
|
cb956c5508 | ||
|
|
47b0086bf8 | ||
|
|
b8d66e4a95 | ||
|
|
bfc1c93153 | ||
|
|
404faf8a0b | ||
|
|
4a7b0f4711 | ||
|
|
dd62fca45d | ||
|
|
79fb04126c | ||
|
|
39c9574ae3 | ||
|
|
38af257adf | ||
|
|
5aae9a4722 | ||
|
|
cfe3cae88d | ||
|
|
612de84ac6 | ||
|
|
33be597ef0 | ||
|
|
cc26fd80d7 | ||
|
|
c227a1ffec | ||
|
|
f0df787bbe | ||
|
|
09188bed48 | ||
|
|
4a3bcaba06 | ||
|
|
1d1ab65edd | ||
|
|
7330cdaf1c | ||
|
|
050a1fb6cf | ||
|
|
1e8397cf17 | ||
|
|
59b53ece2b | ||
|
|
16c62cd46f | ||
|
|
eff56c2514 | ||
|
|
ee6b9a223f | ||
|
|
acc6ea434a | ||
|
|
1e5a7356f4 | ||
|
|
4c8342c19d | ||
|
|
dad5232ecb | ||
|
|
6cad2ab4df | ||
|
|
be972781ee | ||
|
|
58fbc298b1 | ||
|
|
7de7772339 | ||
|
|
ad847a2f5d | ||
|
|
856d52891c | ||
|
|
8de3b3bd8d | ||
|
|
0414483be2 | ||
|
|
22939aa472 | ||
|
|
0cb7c44985 | ||
|
|
b18a09e5eb | ||
|
|
ef3649b1d6 | ||
|
|
ac70a0d94d | ||
|
|
3b91f9b88b | ||
|
|
c37b780ca4 | ||
|
|
20061d2c65 | ||
|
|
f18fa77c1c | ||
|
|
a4c6869d4d | ||
|
|
f9a0070c82 | ||
|
|
5cc52f91cb | ||
|
|
a46b9fb2be | ||
|
|
933e38eca9 | ||
|
|
e182390480 | ||
|
|
563fdcba94 | ||
|
|
bc640834cd | ||
|
|
0e9e7d644a | ||
|
|
1d9b3ac2b5 | ||
|
|
aebed4a644 | ||
|
|
7bfb094a40 | ||
|
|
f90a44c1d0 | ||
|
|
dfcf6d2729 | ||
|
|
806a5daa86 | ||
|
|
4a3602099a | ||
|
|
c69be54655 | ||
|
|
680eaa1d4a | ||
|
|
9cc7b8bcc6 | ||
|
|
55d86d853a | ||
|
|
7d9f309e04 | ||
|
|
c2f0147cff | ||
|
|
05488e66ae | ||
|
|
09eac89086 | ||
|
|
866a0e7534 | ||
|
|
3c8337cf54 | ||
|
|
883b4c4c26 | ||
|
|
6bc42c564d | ||
|
|
4f79f52524 | ||
|
|
d2b2e76a6a | ||
|
|
9d9109e9e5 | ||
|
|
18efb89b9a | ||
|
|
cefe883025 | ||
|
|
0ffa0b96d3 | ||
|
|
0429acfa1b | ||
|
|
827e3c1829 | ||
|
|
aa756ef194 | ||
|
|
d8aad65b24 | ||
|
|
1038e86196 | ||
|
|
47845fd4e3 | ||
|
|
294c3f10ab | ||
|
|
f6afc756dc | ||
|
|
64407e5ca6 | ||
|
|
0a42b0f61f | ||
|
|
ae493cbd0e | ||
|
|
ddd339851b | ||
|
|
82db986bd7 | ||
|
|
7bacd6f8f0 | ||
|
|
f0941f47dd | ||
|
|
c9d05b1117 | ||
|
|
7ee12752ec | ||
|
|
b44772441d | ||
|
|
72e3784fa5 | ||
|
|
2c7f24cb8c | ||
|
|
8a6c86bf65 | ||
|
|
cc52cf60dc | ||
|
|
398ebae2ba | ||
|
|
0095735841 | ||
|
|
95c10a1de7 | ||
|
|
8f4c92e251 | ||
|
|
58354061d8 | ||
|
|
5de176757d | ||
|
|
7414d52dc2 | ||
|
|
c42b5c8806 | ||
|
|
5c60da0f8f | ||
|
|
70d02e9a6d | ||
|
|
6401016424 | ||
|
|
cd031a89fb | ||
|
|
aadba79002 | ||
|
|
4a017fd8ae | ||
|
|
e6072c8fe9 | ||
|
|
f7fd99ec20 | ||
|
|
836fc13449 | ||
|
|
e1f78cd682 | ||
|
|
c69f34836a | ||
|
|
48a0db28d7 | ||
|
|
58eeb90158 | ||
|
|
c36689934d | ||
|
|
653b985018 | ||
|
|
40d90a53b2 | ||
|
|
7c3aaff635 | ||
|
|
4bb7929229 | ||
|
|
bdd5b7b3a7 | ||
|
|
a2ddb56540 | ||
|
|
7c0097951c | ||
|
|
7970016fbf | ||
|
|
129e3b283d | ||
|
|
868eefd2cf | ||
|
|
c42e1a0ab4 | ||
|
|
720475dae5 | ||
|
|
259df880ba | ||
|
|
68a328a364 | ||
|
|
3bc21cdb09 | ||
|
|
9e8ef70510 | ||
|
|
5fd822a24d | ||
|
|
8ee4dbbb5c | ||
|
|
9522a4d5d9 | ||
|
|
eefecdefbe | ||
|
|
84e670e71c | ||
|
|
c22b69234f | ||
|
|
29cd63d3a7 | ||
|
|
214ed388f2 | ||
|
|
0bd3445370 | ||
|
|
002dc9b017 | ||
|
|
ebf5a03f56 | ||
|
|
2797faafe5 | ||
|
|
84ac739993 | ||
|
|
13c37f046f | ||
|
|
2631cc3747 | ||
|
|
5873e8e896 | ||
|
|
6dc633c2a1 | ||
|
|
8b07126285 | ||
|
|
93a120543a | ||
|
|
af0aa4a567 | ||
|
|
58fd1f0c46 | ||
|
|
030041932e | ||
|
|
8dbf456398 | ||
|
|
3a90a078ce | ||
|
|
c30957fc9f | ||
|
|
f6db946c9a | ||
|
|
c6c0d4c62a | ||
|
|
57befc4ccb | ||
|
|
e716af75ed | ||
|
|
efe2bea64b | ||
|
|
9b926326ef | ||
|
|
de71033fe2 | ||
|
|
2c10bf251d | ||
|
|
8af50aa5bd | ||
|
|
05bedfe3d4 | ||
|
|
a1929dac8a | ||
|
|
834ecc643a | ||
|
|
60baabf7e7 | ||
|
|
185a149d74 | ||
|
|
5d62dd2002 | ||
|
|
9eaa6b5cec | ||
|
|
6ff03bbb95 | ||
|
|
9fac6bca64 | ||
|
|
88856b788a | ||
|
|
eda4e46d9f | ||
|
|
0ae1263d9d | ||
|
|
31f1ebe801 | ||
|
|
c6a9c9c57d | ||
|
|
35bcbbbae4 | ||
|
|
22e2c3da1f | ||
|
|
b526d48946 | ||
|
|
91a95b7c20 | ||
|
|
d634e1124a | ||
|
|
309e159df1 | ||
|
|
ffae53326a | ||
|
|
ddd1522e19 | ||
|
|
4bc0cccb24 | ||
|
|
72fbefa300 | ||
|
|
30378eeb50 | ||
|
|
03293c0d25 | ||
|
|
2e3f6e39f6 | ||
|
|
e4d4041c6b | ||
|
|
166a5c10a7 | ||
|
|
1fec81cc3e | ||
|
|
9c247bcb22 | ||
|
|
707356bffe | ||
|
|
fc88a49acc | ||
|
|
8ccf490e9b | ||
|
|
ea768f982e | ||
|
|
172ea82954 | ||
|
|
10500c3c1c | ||
|
|
225916fbba | ||
|
|
2fce78422b | ||
|
|
8132dd6847 | ||
|
|
4caee1e103 | ||
|
|
ca0b03e97c | ||
|
|
f03178bb8d | ||
|
|
d083a86138 | ||
|
|
8216b992ea | ||
|
|
8e74ee7dde | ||
|
|
b207fe14df | ||
|
|
92cfa21be6 | ||
|
|
5fd482428a | ||
|
|
98b09f7edc | ||
|
|
7b83a34777 | ||
|
|
aeb63ec901 | ||
|
|
436093e0b6 | ||
|
|
d7ee06ce6d | ||
|
|
e72003009d | ||
|
|
cfe8235a36 | ||
|
|
8c1ac9c5b3 | ||
|
|
77e3a7d43a | ||
|
|
53728a0f4a | ||
|
|
06f33d9a63 | ||
|
|
ed2698ecc3 | ||
|
|
0cb5554ae5 | ||
|
|
89850c0b22 | ||
|
|
d78b94f4e8 | ||
|
|
c2c50817f1 | ||
|
|
680c2a2904 | ||
|
|
a1085e3863 | ||
|
|
37f6a05170 | ||
|
|
0d296c3b25 | ||
|
|
73caa2508e | ||
|
|
69e012a6f0 | ||
|
|
21251a1915 | ||
|
|
67143ba2d5 | ||
|
|
d399cba4c0 | ||
|
|
9cad7cd025 | ||
|
|
a593842265 | ||
|
|
2f4eb595f6 | ||
|
|
bfcf349ffe | ||
|
|
2bd78cd47f | ||
|
|
713615e28b | ||
|
|
2b2f17525e | ||
|
|
cd6233a3d7 | ||
|
|
f76224bd17 | ||
|
|
1516807ed5 | ||
|
|
8afa373726 | ||
|
|
199f2202e0 | ||
|
|
86a6311f75 | ||
|
|
9893fd9ae5 | ||
|
|
d6c28da3a8 | ||
|
|
52f694a714 | ||
|
|
0ca4e6ca4f | ||
|
|
50bce4892f | ||
|
|
ca345cf008 | ||
|
|
e0e1290fae | ||
|
|
a84b2611e4 | ||
|
|
ec31fab344 | ||
|
|
1b96eee4de | ||
|
|
a6af5de3e1 | ||
|
|
52b3068330 | ||
|
|
0934c08dfe | ||
|
|
f0428fde66 | ||
|
|
b3f57a67c4 | ||
|
|
8bc2b1262b | ||
|
|
fb24efd3de | ||
|
|
ce594b0b5a | ||
|
|
7b39ab4ec4 | ||
|
|
f717ed9f66 | ||
|
|
d3091a5384 | ||
|
|
2a6d950a4b | ||
|
|
b03b9d5334 | ||
|
|
c9cb31bd02 | ||
|
|
905d6860fc | ||
|
|
6f52744b0f | ||
|
|
007d3e52c5 | ||
|
|
f10fa632ca | ||
|
|
266323b90b | ||
|
|
60707a8f45 | ||
|
|
b89896a4e7 | ||
|
|
d69fd12fb9 | ||
|
|
0bdcfa6028 | ||
|
|
55b8488901 | ||
|
|
8ddbf2067b | ||
|
|
fa5cebda6d | ||
|
|
4fd01b4234 | ||
|
|
0f354f4f06 | ||
|
|
f94a197828 | ||
|
|
a26ff660b0 | ||
|
|
50cc648799 | ||
|
|
a7946805ae | ||
|
|
f3b2969b42 | ||
|
|
d6c3490165 | ||
|
|
6ee594d4d1 | ||
|
|
7e1596de30 | ||
|
|
67bba043ed | ||
|
|
26d7f4923d | ||
|
|
e9218d1088 | ||
|
|
03fd1e29e3 | ||
|
|
ff59af6b51 | ||
|
|
73ba8b8b13 | ||
|
|
ffc3fb770c | ||
|
|
d58ea70a95 | ||
|
|
90e8dd038d | ||
|
|
56d1e3edaa | ||
|
|
83a9e54896 | ||
|
|
ab038b1f31 | ||
|
|
f5a9d3928c | ||
|
|
2bc0bce1b5 | ||
|
|
372933fd99 | ||
|
|
9112347e95 | ||
|
|
30548a68e4 | ||
|
|
ce052d1691 | ||
|
|
765ba8c867 | ||
|
|
a20c0cd49e | ||
|
|
e06ca9a056 | ||
|
|
d04048c749 | ||
|
|
2d0f7589ea | ||
|
|
dc60be404a | ||
|
|
9ff5cc51f9 | ||
|
|
e9de8f42e5 | ||
|
|
5bd0499ae4 | ||
|
|
99e706bcd2 | ||
|
|
239edb0605 | ||
|
|
bf3f5a5971 | ||
|
|
92be3f32d6 | ||
|
|
106f5a53ff | ||
|
|
8c43f3d567 | ||
|
|
2e09501c8a | ||
|
|
a2592e48c8 | ||
|
|
291d76674b | ||
|
|
78957cf128 | ||
|
|
e88694b049 | ||
|
|
5e7bdf7354 | ||
|
|
2e3e0bc1d8 | ||
|
|
b33e6b232c | ||
|
|
df6b083670 | ||
|
|
05009d40c4 | ||
|
|
ea27a3b449 | ||
|
|
2831b0bd2a | ||
|
|
32e22dd507 | ||
|
|
2ee9951853 | ||
|
|
b1912135ed | ||
|
|
bed3d42923 | ||
|
|
c4ec69a43f | ||
|
|
24b004bb2d | ||
|
|
84925b24b5 | ||
|
|
c02b91dfd4 | ||
|
|
066d97220f | ||
|
|
61ed47dda0 | ||
|
|
68c0f210cc | ||
|
|
6c542750f4 | ||
|
|
25440a26ee | ||
|
|
ab393b1f6d | ||
|
|
e643147b69 | ||
|
|
6c4aa71cbc | ||
|
|
9930ba8748 | ||
|
|
fbb8b4687b | ||
|
|
01b1c49738 | ||
|
|
c9c28eda1b | ||
|
|
744bcba599 | ||
|
|
d76db726c4 | ||
|
|
71ec528a87 | ||
|
|
fbbc93900e | ||
|
|
33b45737c9 | ||
|
|
4a55f78a48 | ||
|
|
a82a79e25c | ||
|
|
6ec2124a9c | ||
|
|
1f1ef1440e | ||
|
|
a7d0a4bdac | ||
|
|
d2129ffac6 | ||
|
|
a4782f0663 | ||
|
|
16794b9d78 | ||
|
|
a76aed2d4e | ||
|
|
d2163dacf9 | ||
|
|
158305346f | ||
|
|
e692432242 | ||
|
|
a7b85b123e | ||
|
|
ddcd722598 | ||
|
|
358458a937 | ||
|
|
8925f7c381 | ||
|
|
ff2e39901a | ||
|
|
90ff0f43ea | ||
|
|
442c352c8d | ||
|
|
e91b7fb082 | ||
|
|
88de66a31f | ||
|
|
d3b3e45800 | ||
|
|
50d2f90621 | ||
|
|
8ccf6cb8a3 | ||
|
|
2e9b478824 | ||
|
|
89b2d54725 | ||
|
|
3d0af2d8ca | ||
|
|
813b433f4d | ||
|
|
0bce96b0c6 | ||
|
|
1d4f1764fc | ||
|
|
2994420160 | ||
|
|
65d8d7282f | ||
|
|
47af3f09fc | ||
|
|
f4024f4683 | ||
|
|
ee0ed6df7a | ||
|
|
d3fbba3572 | ||
|
|
4a6b22f5b7 | ||
|
|
d070305002 | ||
|
|
a8500150b0 | ||
|
|
02fb1d01ad | ||
|
|
497dee038f | ||
|
|
a4af77f91e | ||
|
|
f2a4e1d230 | ||
|
|
8560901f80 | ||
|
|
daea604c60 | ||
|
|
7aedb59f26 | ||
|
|
0be1c2f464 | ||
|
|
0dbfaf0e79 | ||
|
|
4147399cda | ||
|
|
cc667ac738 | ||
|
|
022915ffc9 | ||
|
|
4b9cf775ff | ||
|
|
56879924bd | ||
|
|
6142f2d641 | ||
|
|
0d53f799b7 | ||
|
|
65e77e9669 | ||
|
|
2c8f3b56ae | ||
|
|
8ab190affc | ||
|
|
eafccc4fc4 | ||
|
|
ce440351a5 | ||
|
|
be94edde0f | ||
|
|
4fcc9af933 | ||
|
|
e2b4ac6ee8 | ||
|
|
c151049cc2 | ||
|
|
ac778d3f65 | ||
|
|
1aed0cb4b9 | ||
|
|
5836b65aad | ||
|
|
46f750efba | ||
|
|
b33c9e23ce | ||
|
|
14694f1cb0 | ||
|
|
75575348cd | ||
|
|
f6ad0a235c | ||
|
|
bbf6c60888 | ||
|
|
f5915f3e10 | ||
|
|
a32cfc8aff | ||
|
|
c90a878c9e | ||
|
|
b46b958105 | ||
|
|
6943b06a88 | ||
|
|
27a9def88c | ||
|
|
11f8cfe0e6 | ||
|
|
e1e3cc7999 | ||
|
|
254c8816f1 | ||
|
|
9a445e34fd | ||
|
|
ee78e113de | ||
|
|
0dfb14962a | ||
|
|
d493df4295 | ||
|
|
f0144233f9 | ||
|
|
90f21f4ed1 | ||
|
|
646ebe592e | ||
|
|
b098a15e9c | ||
|
|
4f98995fe4 | ||
|
|
56231edc3a | ||
|
|
871ab428c2 | ||
|
|
a9b75f752e | ||
|
|
9590559b81 | ||
|
|
24bd2eddf1 | ||
|
|
f4ba06c401 | ||
|
|
cd405d1df9 | ||
|
|
9d6dbc1a6f | ||
|
|
6d57712fca | ||
|
|
191f2cacbf | ||
|
|
080448af3a | ||
|
|
6e2272d043 | ||
|
|
71078dea4f | ||
|
|
333f0be879 | ||
|
|
3b0f664a3b | ||
|
|
2a23d19321 | ||
|
|
5ee4237510 | ||
|
|
bdb906c26d | ||
|
|
02095ac155 | ||
|
|
80d1ca81ac | ||
|
|
3bbabbc80b | ||
|
|
f8ff3d4bf5 | ||
|
|
3a40f9ebd6 | ||
|
|
cf776088e6 | ||
|
|
7ab81608e8 | ||
|
|
017e40fcb9 | ||
|
|
29309fbaa3 | ||
|
|
b766b08bf5 | ||
|
|
e796e00963 | ||
|
|
10136df977 | ||
|
|
44ec107ce8 | ||
|
|
c7fb5b0475 | ||
|
|
785735ccf5 | ||
|
|
f0736ccf8d | ||
|
|
a24a7e03be | ||
|
|
be235e5204 | ||
|
|
57ec598672 | ||
|
|
2627c09cda | ||
|
|
36e63bb8a9 | ||
|
|
5ea24f650b | ||
|
|
8c792ce7a1 | ||
|
|
055969f5c6 | ||
|
|
145ae10a79 | ||
|
|
bdb9349b52 | ||
|
|
8b11b57ec5 | ||
|
|
29888c89ad | ||
|
|
281fb2afd3 | ||
|
|
fbb7839f83 | ||
|
|
d3f9c170ac | ||
|
|
4f9a0b0040 | ||
|
|
aae584106a | ||
|
|
2a784deb4b | ||
|
|
5f5a7880a6 | ||
|
|
ab8b6d806d | ||
|
|
6c4206a2dd | ||
|
|
0d61d9cee4 | ||
|
|
48bdb9e818 | ||
|
|
cfe447a403 | ||
|
|
2c30f0e487 | ||
|
|
147211478c | ||
|
|
35b7674e69 | ||
|
|
55075b2a96 | ||
|
|
6577599959 | ||
|
|
0c4e72e507 | ||
|
|
9bf96e943d | ||
|
|
2811843e70 | ||
|
|
842ece2a8a | ||
|
|
222de03ea3 | ||
|
|
d31ae9f3fc | ||
|
|
1dd7644e12 | ||
|
|
2cee54f70a | ||
|
|
a8431fae96 | ||
|
|
03d11b7b58 | ||
|
|
316e2eeefb | ||
|
|
c21e19337a | ||
|
|
b9cab0dae8 | ||
|
|
33b3299ca2 | ||
|
|
00ba38beba | ||
|
|
433fce286e | ||
|
|
b36322bba4 | ||
|
|
c147e0a789 | ||
|
|
8bf5d02624 | ||
|
|
35616c1ccd | ||
|
|
7c8939ecb8 | ||
|
|
ccb0302d3f | ||
|
|
0cfd048013 | ||
|
|
f72b4f0249 | ||
|
|
b5cb209f14 | ||
|
|
1af374439d | ||
|
|
bbcd674516 | ||
|
|
3cba71b7a8 | ||
|
|
6a1e9c5818 | ||
|
|
ede41d01b4 | ||
|
|
826a67b550 | ||
|
|
f3d59a9b61 | ||
|
|
65434453b8 | ||
|
|
847ad2d781 | ||
|
|
ecabd37cbb | ||
|
|
4f6c35713e | ||
|
|
4d41c7f37f | ||
|
|
a222fd0786 | ||
|
|
40b5472866 | ||
|
|
a413e273ca | ||
|
|
0f82085cae | ||
|
|
7472019422 | ||
|
|
aa3597881a | ||
|
|
a36841e501 | ||
|
|
fe9afc8952 | ||
|
|
63d39a81aa | ||
|
|
b11dc2ca20 | ||
|
|
a35f876f4f | ||
|
|
8ae7d96cc7 | ||
|
|
a3ec6f470a | ||
|
|
8e85a33dac | ||
|
|
1769dd959e | ||
|
|
1253b81a01 |
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" } ]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,7 +22,10 @@
|
|||||||
"fontSize": "14",
|
"fontSize": "14",
|
||||||
"lineNumber": true
|
"lineNumber": true
|
||||||
},
|
},
|
||||||
"sortBy": "UPDATED_AT",
|
"sortBy": {
|
||||||
|
"default": "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/
|
||||||
@@ -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",
|
||||||
@@ -17,5 +19,8 @@
|
|||||||
"FileReader": true,
|
"FileReader": true,
|
||||||
"localStorage": true,
|
"localStorage": true,
|
||||||
"fetch": true
|
"fetch": true
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 6
|
- 7
|
||||||
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
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
|
# 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 on IssueHunt:
|
||||||
👉 https://opencollective.com/boostnoteio
|
👉 https://issuehunt.io/repos/53266139
|
||||||
-->
|
-->
|
||||||
|
|||||||
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()
|
||||||
|
}
|
||||||
@@ -3,39 +3,32 @@ import React from 'react'
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import CodeMirror from 'codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
import 'codemirror-mode-elixir'
|
import 'codemirror-mode-elixir'
|
||||||
import path from 'path'
|
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||||
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
import convertModeName from 'browser/lib/convertModeName'
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { options, TableEditor } from '@susisu/mte-kernel'
|
||||||
import fs from 'fs'
|
import TextEditorInterface from 'browser/lib/TextEditorInterface'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import iconv from 'iconv-lite'
|
import iconv from 'iconv-lite'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import consts from 'browser/lib/consts'
|
||||||
|
import fs from 'fs'
|
||||||
const { ipcRenderer } = require('electron')
|
const { ipcRenderer } = require('electron')
|
||||||
|
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
||||||
|
|
||||||
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 buildCMRulers = (rulers, enableRulers) =>
|
||||||
|
(enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
|
||||||
this.changeHandler = (e) => this.handleChange(e)
|
leading: false,
|
||||||
|
trailing: true
|
||||||
|
})
|
||||||
|
this.changeHandler = e => this.handleChange(e)
|
||||||
this.focusHandler = () => {
|
this.focusHandler = () => {
|
||||||
ipcRenderer.send('editor:focused', true)
|
ipcRenderer.send('editor:focused', true)
|
||||||
}
|
}
|
||||||
@@ -50,13 +43,22 @@ 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.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||||
this.searchState = null
|
this.searchState = null
|
||||||
|
|
||||||
|
this.formatTable = () => this.handleFormatTable()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearch (msg) {
|
handleSearch (msg) {
|
||||||
@@ -71,7 +73,10 @@ export default class CodeEditor extends React.Component {
|
|||||||
cm.addOverlay(component.searchState)
|
cm.addOverlay(component.searchState)
|
||||||
|
|
||||||
function makeOverlay (query, style) {
|
function makeOverlay (query, style) {
|
||||||
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi')
|
query = new RegExp(
|
||||||
|
query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
|
||||||
|
'gi'
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
token: function (stream) {
|
token: function (stream) {
|
||||||
query.lastIndex = stream.pos
|
query.lastIndex = stream.pos
|
||||||
@@ -90,10 +95,33 @@ export default class CodeEditor extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
handleFormatTable () {
|
||||||
this.value = this.props.value
|
this.tableEditor.formatAll(options({textWidthOptions: {}}))
|
||||||
|
}
|
||||||
|
|
||||||
|
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.editor = CodeMirror(this.refs.root, {
|
this.editor = CodeMirror(this.refs.root, {
|
||||||
|
rulers: buildCMRulers(rulers, enableRulers),
|
||||||
value: this.props.value,
|
value: this.props.value,
|
||||||
lineNumbers: this.props.displayLineNumbers,
|
lineNumbers: this.props.displayLineNumbers,
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
@@ -105,11 +133,20 @@ export default class CodeEditor extends React.Component {
|
|||||||
scrollPastEnd: this.props.scrollPastEnd,
|
scrollPastEnd: this.props.scrollPastEnd,
|
||||||
inputStyle: 'textarea',
|
inputStyle: 'textarea',
|
||||||
dragDrop: false,
|
dragDrop: false,
|
||||||
autoCloseBrackets: true,
|
foldGutter: true,
|
||||||
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||||
|
autoCloseBrackets: {
|
||||||
|
pairs: '()[]{}\'\'""$$**``',
|
||||||
|
triples: '```"""\'\'\'',
|
||||||
|
explode: '[]{}``$$',
|
||||||
|
override: 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')
|
||||||
@@ -121,6 +158,21 @@ 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')
|
||||||
@@ -134,7 +186,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
// Do nothing
|
// Do nothing
|
||||||
},
|
},
|
||||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
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')
|
||||||
}
|
}
|
||||||
@@ -162,6 +214,83 @@ export default class CodeEditor extends React.Component {
|
|||||||
CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor)
|
CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor)
|
||||||
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
|
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
|
||||||
CodeMirror.Vim.map('ZZ', ':q', 'normal')
|
CodeMirror.Vim.map('ZZ', ':q', 'normal')
|
||||||
|
|
||||||
|
this.tableEditor = new TableEditor(new TextEditorInterface(this.editor))
|
||||||
|
eventEmitter.on('code:format-table', this.formatTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 () {
|
||||||
@@ -177,10 +306,13 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.off('scroll', this.scrollHandler)
|
this.editor.off('scroll', this.scrollHandler)
|
||||||
const editorTheme = document.getElementById('editorTheme')
|
const editorTheme = document.getElementById('editorTheme')
|
||||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||||
|
|
||||||
|
eventEmitter.off('code:format-table', this.formatTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -198,6 +330,13 @@ 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)
|
||||||
@@ -220,7 +359,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -234,11 +373,9 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveCursorTo (row, col) {
|
moveCursorTo (row, col) {}
|
||||||
}
|
|
||||||
|
|
||||||
scrollToLine (num) {
|
scrollToLine (num) {}
|
||||||
}
|
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
this.editor.focus()
|
this.editor.focus()
|
||||||
@@ -264,51 +401,65 @@ export default class CodeEditor extends React.Component {
|
|||||||
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,
|
||||||
copyImage(imagePath, this.props.storageKey).then((imagePath) => {
|
storageKey,
|
||||||
const imageMd = `})`
|
noteKey,
|
||||||
this.insertImageMd(imageMd)
|
dropEvent
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
insertImageMd (imageMd) {
|
insertAttachmentMd (imageMd) {
|
||||||
this.editor.replaceSelection(imageMd)
|
this.editor.replaceSelection(imageMd)
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePaste (editor, e) {
|
handlePaste (editor, e) {
|
||||||
const clipboardData = e.clipboardData
|
const clipboardData = e.clipboardData
|
||||||
|
const { storageKey, noteKey } = this.props
|
||||||
const dataTransferItem = clipboardData.items[0]
|
const dataTransferItem = clipboardData.items[0]
|
||||||
const pastedTxt = clipboardData.getData('text')
|
const pastedTxt = clipboardData.getData('text')
|
||||||
const isURL = (str) => {
|
const isURL = str => {
|
||||||
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
||||||
return matcher.test(str)
|
return matcher.test(str)
|
||||||
}
|
}
|
||||||
|
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')) {
|
if (dataTransferItem.type.match('image')) {
|
||||||
const blob = dataTransferItem.getAsFile()
|
attachmentManagement.handlePastImageEvent(
|
||||||
const reader = new FileReader()
|
this,
|
||||||
let base64data
|
storageKey,
|
||||||
|
noteKey,
|
||||||
reader.readAsDataURL(blob)
|
dataTransferItem
|
||||||
reader.onloadend = () => {
|
)
|
||||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
} else if (
|
||||||
base64data += base64data.replace('+', ' ')
|
this.props.fetchUrlTitle &&
|
||||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
isURL(pastedTxt) &&
|
||||||
const imageName = Math.random().toString(36).slice(-16)
|
!isInLinkTag(editor)
|
||||||
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)
|
|
||||||
}
|
|
||||||
} else if (this.props.fetchUrlTitle && isURL(pastedTxt)) {
|
|
||||||
this.handlePasteUrl(e, editor, pastedTxt)
|
this.handlePasteUrl(e, editor, pastedTxt)
|
||||||
}
|
}
|
||||||
|
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||||
|
attachmentManagement
|
||||||
|
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
|
||||||
|
.then(modifiedText => {
|
||||||
|
this.editor.replaceSelection(modifiedText)
|
||||||
|
})
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleScroll (e) {
|
handleScroll (e) {
|
||||||
@@ -322,24 +473,68 @@ export default class CodeEditor extends React.Component {
|
|||||||
const taggedUrl = `<${pastedTxt}>`
|
const taggedUrl = `<${pastedTxt}>`
|
||||||
editor.replaceSelection(taggedUrl)
|
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, {
|
fetch(pastedTxt, {
|
||||||
method: 'get'
|
method: 'get'
|
||||||
}).then((response) => {
|
})
|
||||||
return this.decodeResponse(response)
|
.then(response => {
|
||||||
}).then((response) => {
|
if (isImageReponse(response)) {
|
||||||
const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html')
|
return this.mapImageResponse(response, pastedTxt)
|
||||||
const value = editor.getValue()
|
} else {
|
||||||
const cursor = editor.getCursor()
|
return this.mapNormalResponse(response, pastedTxt)
|
||||||
const LinkWithTitle = `[${parsedResponse.title}](${pastedTxt})`
|
}
|
||||||
const newValue = value.replace(taggedUrl, LinkWithTitle)
|
})
|
||||||
editor.setValue(newValue)
|
.then(replacement => {
|
||||||
editor.setCursor(cursor)
|
replaceTaggedUrl(replacement)
|
||||||
}).catch((e) => {
|
})
|
||||||
const value = editor.getValue()
|
.catch(e => {
|
||||||
const newValue = value.replace(taggedUrl, pastedTxt)
|
replaceTaggedUrl(pastedTxt)
|
||||||
const cursor = editor.getCursor()
|
})
|
||||||
editor.setValue(newValue)
|
}
|
||||||
editor.setCursor(cursor)
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,10 +543,13 @@ export default class CodeEditor extends React.Component {
|
|||||||
const _charset = headers.has('content-type')
|
const _charset = headers.has('content-type')
|
||||||
? this.extractContentTypeCharset(headers.get('content-type'))
|
? this.extractContentTypeCharset(headers.get('content-type'))
|
||||||
: undefined
|
: undefined
|
||||||
return response.arrayBuffer().then((buff) => {
|
return response.arrayBuffer().then(buff => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8'
|
const charset = _charset !== undefined &&
|
||||||
|
iconv.encodingExists(_charset)
|
||||||
|
? _charset
|
||||||
|
: 'utf-8'
|
||||||
resolve(iconv.decode(new Buffer(buff), charset).toString())
|
resolve(iconv.decode(new Buffer(buff), charset).toString())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e)
|
reject(e)
|
||||||
@@ -361,32 +559,31 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extractContentTypeCharset (contentType) {
|
extractContentTypeCharset (contentType) {
|
||||||
return contentType.split(';').filter((str) => {
|
return contentType
|
||||||
return str.trim().toLowerCase().startsWith('charset')
|
.split(';')
|
||||||
}).map((str) => {
|
.filter(str => {
|
||||||
return str.replace(/['"]/g, '').split('=')[1]
|
return str.trim().toLowerCase().startsWith('charset')
|
||||||
})[0]
|
})
|
||||||
|
.map(str => {
|
||||||
|
return str.replace(/['"]/g, '').split('=')[1]
|
||||||
|
})[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { className, fontSize } = this.props
|
const {className, fontSize} = this.props
|
||||||
let fontFamily = this.props.fontFamily
|
const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
|
||||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
const width = this.props.width
|
||||||
? [fontFamily].concat(defaultEditorFontFamily)
|
|
||||||
: defaultEditorFontFamily
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={className == null
|
className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
|
||||||
? 'CodeEditor'
|
|
||||||
: `CodeEditor ${className}`
|
|
||||||
}
|
|
||||||
ref='root'
|
ref='root'
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
style={{
|
style={{
|
||||||
fontFamily: fontFamily.join(', '),
|
fontFamily,
|
||||||
fontSize: fontSize
|
fontSize: fontSize,
|
||||||
|
width: width
|
||||||
}}
|
}}
|
||||||
onDrop={(e) => this.handleDropImage(e)}
|
onDrop={e => this.handleDropImage(e)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -394,6 +591,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,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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'
|
||||||
|
|
||||||
class MarkdownEditor extends React.Component {
|
class MarkdownEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -223,7 +223,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { 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
|
||||||
@@ -258,9 +258,12 @@ 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}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
|
noteKey={noteKey}
|
||||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
onBlur={(e) => this.handleBlur(e)}
|
onBlur={(e) => this.handleBlur(e)}
|
||||||
@@ -280,6 +283,9 @@ class MarkdownEditor extends React.Component {
|
|||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
scrollPastEnd={config.preview.scrollPastEnd}
|
scrollPastEnd={config.preview.scrollPastEnd}
|
||||||
smartQuotes={config.preview.smartQuotes}
|
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)}
|
onDoubleClick={(e) => this.handleDoubleClick(e)}
|
||||||
@@ -290,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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,28 +7,45 @@ 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 mermaidRender from './render/MermaidRender'
|
||||||
import SequenceDiagram from 'js-sequence-diagrams'
|
import SequenceDiagram from 'js-sequence-diagrams'
|
||||||
|
import Chart from 'chart.js'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
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 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 fileUrl = require('file-url')
|
||||||
|
|
||||||
const dialog = remote.dialog
|
const dialog = remote.dialog
|
||||||
|
|
||||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||||
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
const appPath = fileUrl(
|
||||||
? app.getAppPath()
|
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
|
||||||
: path.resolve())
|
)
|
||||||
const CSS_FILES = [
|
const CSS_FILES = [
|
||||||
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
||||||
]
|
]
|
||||||
|
|
||||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd) {
|
function buildStyle (
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
) {
|
||||||
return `
|
return `
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
@@ -48,7 +65,19 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scro
|
|||||||
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;
|
||||||
@@ -100,9 +129,38 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollBarStyle = `
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const scrollBarDarkStyle = `
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const { shell } = require('electron')
|
const { shell } = require('electron')
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
@@ -111,19 +169,27 @@ if (!OSX) {
|
|||||||
defaultFontFamily.unshift('Microsoft YaHei')
|
defaultFontFamily.unshift('Microsoft YaHei')
|
||||||
defaultFontFamily.unshift('meiryo')
|
defaultFontFamily.unshift('meiryo')
|
||||||
}
|
}
|
||||||
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
const defaultCodeBlockFontFamily = [
|
||||||
|
'Monaco',
|
||||||
|
'Menlo',
|
||||||
|
'Ubuntu Mono',
|
||||||
|
'Consolas',
|
||||||
|
'source-code-pro',
|
||||||
|
'monospace'
|
||||||
|
]
|
||||||
export default class MarkdownPreview extends React.Component {
|
export default class MarkdownPreview extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
this.contextMenuHandler = e => this.handleContextMenu(e)
|
||||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
this.mouseDownHandler = e => this.handleMouseDown(e)
|
||||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
this.mouseUpHandler = e => this.handleMouseUp(e)
|
||||||
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
|
this.DoubleClickHandler = e => this.handleDoubleClick(e)
|
||||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
|
||||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
leading: false,
|
||||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
trailing: true
|
||||||
|
})
|
||||||
|
this.checkboxClickHandler = e => this.handleCheckboxClick(e)
|
||||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||||
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||||
@@ -135,24 +201,12 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initMarkdown () {
|
initMarkdown () {
|
||||||
const { smartQuotes } = this.props
|
const { smartQuotes, sanitize, breaks } = this.props
|
||||||
this.markdown = new Markdown({ typographer: smartQuotes })
|
this.markdown = new Markdown({
|
||||||
}
|
typographer: smartQuotes,
|
||||||
|
sanitize,
|
||||||
handlePreviewAnchorClick (e) {
|
breaks
|
||||||
e.preventDefault()
|
})
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
const anchor = e.target.closest('a')
|
|
||||||
const href = anchor.getAttribute('href')
|
|
||||||
if (_.isString(href) && href.match(/^#/)) {
|
|
||||||
const 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) {
|
||||||
@@ -197,27 +251,85 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsMd () {
|
handleSaveAsMd () {
|
||||||
this.exportAsDocument('md')
|
this.exportAsDocument('md', (noteContent, exportTasks) => {
|
||||||
|
let result = noteContent
|
||||||
|
if (this.props && this.props.storagePath && this.props.noteKey) {
|
||||||
|
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||||
|
noteContent,
|
||||||
|
this.props.storagePath
|
||||||
|
)
|
||||||
|
attachmentsAbsolutePaths.forEach(attachment => {
|
||||||
|
exportTasks.push({
|
||||||
|
src: attachment,
|
||||||
|
dst: attachmentManagement.DESTINATION_FOLDER
|
||||||
|
})
|
||||||
|
})
|
||||||
|
result = attachmentManagement.removeStorageAndNoteReferences(
|
||||||
|
noteContent,
|
||||||
|
this.props.noteKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsHtml () {
|
handleSaveAsHtml () {
|
||||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
|
const {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
codeBlockTheme,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
} = this.getStyleParams()
|
||||||
|
|
||||||
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
const inlineStyles = buildStyle(
|
||||||
const body = this.markdown.render(noteContent)
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
)
|
||||||
|
let body = this.markdown.render(
|
||||||
|
escapeHtmlCharacters(noteContent, { detectCodeBlock: true })
|
||||||
|
)
|
||||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||||
|
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||||
|
noteContent,
|
||||||
|
this.props.storagePath
|
||||||
|
)
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach(file => {
|
||||||
file = file.replace('file://', '')
|
if (global.process.platform === 'win32') {
|
||||||
|
file = file.replace('file:///', '')
|
||||||
|
} else {
|
||||||
|
file = file.replace('file://', '')
|
||||||
|
}
|
||||||
exportTasks.push({
|
exportTasks.push({
|
||||||
src: file,
|
src: file,
|
||||||
dst: 'css'
|
dst: 'css'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
attachmentsAbsolutePaths.forEach(attachment => {
|
||||||
|
exportTasks.push({
|
||||||
|
src: attachment,
|
||||||
|
dst: attachmentManagement.DESTINATION_FOLDER
|
||||||
|
})
|
||||||
|
})
|
||||||
|
body = attachmentManagement.removeStorageAndNoteReferences(
|
||||||
|
body,
|
||||||
|
this.props.noteKey
|
||||||
|
)
|
||||||
|
|
||||||
let styles = ''
|
let styles = ''
|
||||||
files.forEach((file) => {
|
files.forEach(file => {
|
||||||
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -239,50 +351,75 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
exportAsDocument (fileType, contentFormatter) {
|
exportAsDocument (fileType, contentFormatter) {
|
||||||
const options = {
|
const options = {
|
||||||
filters: [
|
filters: [{ name: 'Documents', extensions: [fileType] }],
|
||||||
{name: 'Documents', extensions: [fileType]}
|
|
||||||
],
|
|
||||||
properties: ['openFile', 'createDirectory']
|
properties: ['openFile', 'createDirectory']
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
|
||||||
(filename) => {
|
if (filename) {
|
||||||
if (filename) {
|
const content = this.props.value
|
||||||
const content = this.props.value
|
const storage = this.props.storagePath
|
||||||
const storage = this.props.storagePath
|
|
||||||
|
|
||||||
exportNote(storage, content, filename, contentFormatter)
|
exportNote(storage, content, filename, contentFormatter)
|
||||||
.then((res) => {
|
.then(res => {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
}).catch((err) => {
|
type: 'info',
|
||||||
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
|
message: `Exported to ${filename}`
|
||||||
throw err
|
})
|
||||||
})
|
})
|
||||||
}
|
.catch(err => {
|
||||||
})
|
dialog.showErrorBox(
|
||||||
|
'Export error',
|
||||||
|
err ? err.message || err : 'Unexpected error during export'
|
||||||
|
)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fixDecodedURI (node) {
|
fixDecodedURI (node) {
|
||||||
if (node && node.children.length === 1 && typeof node.children[0] === 'string') {
|
if (
|
||||||
|
node &&
|
||||||
|
node.children.length === 1 &&
|
||||||
|
typeof node.children[0] === 'string'
|
||||||
|
) {
|
||||||
const { innerText, href } = node
|
const { innerText, href } = node
|
||||||
|
|
||||||
node.innerText = mdurl.decode(href) === innerText
|
node.innerText = mdurl.decode(href) === innerText ? href : innerText
|
||||||
? href
|
}
|
||||||
: innerText
|
}
|
||||||
|
|
||||||
|
getScrollBarStyle () {
|
||||||
|
const { theme } = this.props
|
||||||
|
|
||||||
|
switch (theme) {
|
||||||
|
case 'dark':
|
||||||
|
case 'solarized-dark':
|
||||||
|
case 'monokai':
|
||||||
|
return scrollBarDarkStyle
|
||||||
|
default:
|
||||||
|
return scrollBarStyle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||||
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
this.refs.root.contentWindow.document.body.addEventListener(
|
||||||
|
'contextmenu',
|
||||||
|
this.contextMenuHandler
|
||||||
|
)
|
||||||
|
|
||||||
let styles = `
|
let styles = `
|
||||||
<style id='style'></style>
|
<style id='style'></style>
|
||||||
<link rel="stylesheet" id="codeTheme">
|
<link rel="stylesheet" id="codeTheme">
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<style>
|
||||||
|
${this.getScrollBarStyle()}
|
||||||
|
</style>
|
||||||
`
|
`
|
||||||
|
|
||||||
CSS_FILES.forEach((file) => {
|
CSS_FILES.forEach(file => {
|
||||||
styles += `<link rel="stylesheet" href="${file}">`
|
styles += `<link rel="stylesheet" href="${file}">`
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -290,12 +427,30 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
'mousedown',
|
||||||
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler)
|
this.mouseDownHandler
|
||||||
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
)
|
||||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler)
|
'mouseup',
|
||||||
|
this.mouseUpHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
|
'dblclick',
|
||||||
|
this.DoubleClickHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
|
'drop',
|
||||||
|
this.preventImageDroppedHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
|
'dragover',
|
||||||
|
this.preventImageDroppedHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.addEventListener(
|
||||||
|
'scroll',
|
||||||
|
this.scrollHandler
|
||||||
|
)
|
||||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||||
@@ -303,13 +458,34 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
this.refs.root.contentWindow.document.body.removeEventListener(
|
||||||
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
'contextmenu',
|
||||||
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
this.contextMenuHandler
|
||||||
this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler)
|
)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
'mousedown',
|
||||||
this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler)
|
this.mouseDownHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'mouseup',
|
||||||
|
this.mouseUpHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'dblclick',
|
||||||
|
this.DoubleClickHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'drop',
|
||||||
|
this.preventImageDroppedHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'dragover',
|
||||||
|
this.preventImageDroppedHandler
|
||||||
|
)
|
||||||
|
this.refs.root.contentWindow.document.removeEventListener(
|
||||||
|
'scroll',
|
||||||
|
this.scrollHandler
|
||||||
|
)
|
||||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||||
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||||
@@ -318,45 +494,101 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||||
if (prevProps.smartQuotes !== this.props.smartQuotes) {
|
if (
|
||||||
|
prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||||
|
prevProps.sanitize !== this.props.sanitize ||
|
||||||
|
prevProps.smartArrows !== this.props.smartArrows ||
|
||||||
|
prevProps.breaks !== this.props.breaks
|
||||||
|
) {
|
||||||
this.initMarkdown()
|
this.initMarkdown()
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
}
|
}
|
||||||
if (prevProps.fontFamily !== this.props.fontFamily ||
|
if (
|
||||||
|
prevProps.fontFamily !== this.props.fontFamily ||
|
||||||
prevProps.fontSize !== this.props.fontSize ||
|
prevProps.fontSize !== this.props.fontSize ||
|
||||||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
||||||
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.scrollPastEnd !== this.props.scrollPastEnd ||
|
||||||
|
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
|
||||||
|
prevProps.customCSS !== this.props.customCSS
|
||||||
|
) {
|
||||||
this.applyStyle()
|
this.applyStyle()
|
||||||
this.rewriteIframe()
|
this.rewriteIframe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStyleParams () {
|
getStyleParams () {
|
||||||
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd } = this.props
|
const {
|
||||||
|
fontSize,
|
||||||
|
lineNumber,
|
||||||
|
codeBlockTheme,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
} = this.props
|
||||||
let { fontFamily, codeBlockFontFamily } = this.props
|
let { fontFamily, codeBlockFontFamily } = this.props
|
||||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||||
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
? fontFamily
|
||||||
: defaultFontFamily
|
.split(',')
|
||||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
.map(fontName => fontName.trim())
|
||||||
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
.concat(defaultFontFamily)
|
||||||
: defaultCodeBlockFontFamily
|
: defaultFontFamily
|
||||||
|
codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
|
||||||
|
codeBlockFontFamily.trim().length > 0
|
||||||
|
? codeBlockFontFamily
|
||||||
|
.split(',')
|
||||||
|
.map(fontName => fontName.trim())
|
||||||
|
.concat(defaultCodeBlockFontFamily)
|
||||||
|
: defaultCodeBlockFontFamily
|
||||||
|
|
||||||
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd}
|
return {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
codeBlockTheme,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStyle () {
|
applyStyle () {
|
||||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd} = this.getStyleParams()
|
const {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
codeBlockTheme,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
} = this.getStyleParams()
|
||||||
|
|
||||||
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
|
this.getWindow().document.getElementById(
|
||||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd)
|
'codeTheme'
|
||||||
|
).href = this.GetCodeThemeLink(codeBlockTheme)
|
||||||
|
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
GetCodeThemeLink (theme) {
|
GetCodeThemeLink (theme) {
|
||||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
theme = consts.THEMES.some(_theme => _theme === theme) &&
|
||||||
|
theme !== 'default'
|
||||||
? theme
|
? theme
|
||||||
: 'elegant'
|
: 'elegant'
|
||||||
return theme.startsWith('solarized')
|
return theme.startsWith('solarized')
|
||||||
@@ -365,82 +597,92 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rewriteIframe () {
|
rewriteIframe () {
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
_.forEach(
|
||||||
el.removeEventListener('click', this.anchorClickHandler)
|
this.refs.root.contentWindow.document.querySelectorAll(
|
||||||
})
|
'input[type="checkbox"]'
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
),
|
||||||
el.removeEventListener('click', this.checkboxClickHandler)
|
el => {
|
||||||
})
|
el.removeEventListener('click', this.checkboxClickHandler)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
_.forEach(
|
||||||
el.removeEventListener('click', this.linkClickHandler)
|
this.refs.root.contentWindow.document.querySelectorAll('a'),
|
||||||
})
|
el => {
|
||||||
|
el.removeEventListener('click', this.linkClickHandler)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const { theme, indentSize, showCopyNotification, storagePath } = this.props
|
const {
|
||||||
|
theme,
|
||||||
|
indentSize,
|
||||||
|
showCopyNotification,
|
||||||
|
storagePath,
|
||||||
|
noteKey
|
||||||
|
} = this.props
|
||||||
let { value, codeBlockTheme } = this.props
|
let { value, codeBlockTheme } = this.props
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||||
|
const renderedHTML = this.markdown.render(value)
|
||||||
|
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
||||||
|
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
|
||||||
|
renderedHTML,
|
||||||
|
storagePath
|
||||||
|
)
|
||||||
|
_.forEach(
|
||||||
|
this.refs.root.contentWindow.document.querySelectorAll(
|
||||||
|
'input[type="checkbox"]'
|
||||||
|
),
|
||||||
|
el => {
|
||||||
|
el.addEventListener('click', this.checkboxClickHandler)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g)
|
_.forEach(
|
||||||
if (codeBlocks !== null) {
|
this.refs.root.contentWindow.document.querySelectorAll('a'),
|
||||||
codeBlocks.forEach((codeBlock) => {
|
el => {
|
||||||
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
this.fixDecodedURI(el)
|
||||||
})
|
el.addEventListener('click', this.linkClickHandler)
|
||||||
}
|
}
|
||||||
this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value)
|
)
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
|
||||||
this.fixDecodedURI(el)
|
|
||||||
el.addEventListener('click', this.anchorClickHandler)
|
|
||||||
})
|
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
|
||||||
el.addEventListener('click', this.checkboxClickHandler)
|
|
||||||
})
|
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
|
||||||
el.addEventListener('click', this.linkClickHandler)
|
|
||||||
})
|
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
|
|
||||||
el.src = this.markdown.normalizeLinkText(el.src)
|
|
||||||
if (!/\/:storage/.test(el.src)) return
|
|
||||||
el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
|
|
||||||
})
|
|
||||||
|
|
||||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
|
||||||
? codeBlockTheme
|
? codeBlockTheme
|
||||||
: 'default'
|
: 'default'
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
|
_.forEach(
|
||||||
let syntax = CodeMirror.findModeByName(el.className)
|
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
||||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
el => {
|
||||||
CodeMirror.requireMode(syntax.mode, () => {
|
let syntax = CodeMirror.findModeByName(convertModeName(el.className))
|
||||||
const content = htmlTextHelper.decodeEntities(el.innerHTML)
|
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||||
const copyIcon = document.createElement('i')
|
CodeMirror.requireMode(syntax.mode, () => {
|
||||||
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
const content = htmlTextHelper.decodeEntities(el.innerHTML)
|
||||||
copyIcon.onclick = (e) => {
|
const copyIcon = document.createElement('i')
|
||||||
copy(content)
|
copyIcon.innerHTML =
|
||||||
if (showCopyNotification) {
|
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||||
this.notify('Saved to Clipboard!', {
|
copyIcon.onclick = e => {
|
||||||
body: 'Paste it wherever you want!',
|
copy(content)
|
||||||
silent: true
|
if (showCopyNotification) {
|
||||||
})
|
this.notify('Saved to Clipboard!', {
|
||||||
|
body: 'Paste it wherever you want!',
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
el.parentNode.appendChild(copyIcon)
|
||||||
el.parentNode.appendChild(copyIcon)
|
el.innerHTML = ''
|
||||||
el.innerHTML = ''
|
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
const [refThema, color] = codeBlockTheme.split(' ')
|
||||||
const [refThema, color] = codeBlockTheme.split(' ')
|
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
} else {
|
||||||
} else {
|
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
}
|
||||||
}
|
CodeMirror.runMode(content, syntax.mime, el, {
|
||||||
CodeMirror.runMode(content, syntax.mime, el, {
|
tabSize: indentSize
|
||||||
tabSize: indentSize
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
})
|
)
|
||||||
const opts = {}
|
const opts = {}
|
||||||
// if (this.props.theme === 'dark') {
|
// if (this.props.theme === 'dark') {
|
||||||
// opts['font-color'] = '#DDD'
|
// opts['font-color'] = '#DDD'
|
||||||
@@ -448,37 +690,71 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
// opts['element-color'] = '#DDD'
|
// opts['element-color'] = '#DDD'
|
||||||
// opts['fill'] = '#3A404C'
|
// opts['fill'] = '#3A404C'
|
||||||
// }
|
// }
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => {
|
_.forEach(
|
||||||
Raphael.setWindow(this.getWindow())
|
this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
|
||||||
try {
|
el => {
|
||||||
const diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
Raphael.setWindow(this.getWindow())
|
||||||
el.innerHTML = ''
|
try {
|
||||||
diagram.drawSVG(el, opts)
|
const diagram = flowchart.parse(
|
||||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
htmlTextHelper.decodeEntities(el.innerHTML)
|
||||||
el.addEventListener('click', this.anchorClickHandler)
|
)
|
||||||
})
|
el.innerHTML = ''
|
||||||
} catch (e) {
|
diagram.drawSVG(el, opts)
|
||||||
console.error(e)
|
_.forEach(el.querySelectorAll('a'), el => {
|
||||||
el.className = 'flowchart-error'
|
el.addEventListener('click', this.linkClickHandler)
|
||||||
el.innerHTML = 'Flowchart parse error: ' + e.message
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
el.className = 'flowchart-error'
|
||||||
|
el.innerHTML = 'Flowchart parse error: ' + e.message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => {
|
_.forEach(
|
||||||
Raphael.setWindow(this.getWindow())
|
this.refs.root.contentWindow.document.querySelectorAll('.sequence'),
|
||||||
try {
|
el => {
|
||||||
const diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
Raphael.setWindow(this.getWindow())
|
||||||
el.innerHTML = ''
|
try {
|
||||||
diagram.drawSVG(el, {theme: 'simple'})
|
const diagram = SequenceDiagram.parse(
|
||||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
htmlTextHelper.decodeEntities(el.innerHTML)
|
||||||
el.addEventListener('click', this.anchorClickHandler)
|
)
|
||||||
})
|
el.innerHTML = ''
|
||||||
} catch (e) {
|
diagram.drawSVG(el, { theme: 'simple' })
|
||||||
console.error(e)
|
_.forEach(el.querySelectorAll('a'), el => {
|
||||||
el.className = 'sequence-error'
|
el.addEventListener('click', this.linkClickHandler)
|
||||||
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
el.className = 'sequence-error'
|
||||||
|
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
|
_.forEach(
|
||||||
|
this.refs.root.contentWindow.document.querySelectorAll('.chart'),
|
||||||
|
el => {
|
||||||
|
try {
|
||||||
|
const chartConfig = JSON.parse(el.innerHTML)
|
||||||
|
el.innerHTML = ''
|
||||||
|
var canvas = document.createElement('canvas')
|
||||||
|
el.appendChild(canvas)
|
||||||
|
/* eslint-disable no-new */
|
||||||
|
new Chart(canvas, chartConfig)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
el.className = 'chart-error'
|
||||||
|
el.innerHTML = 'chartjs diagram parse error: ' + e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_.forEach(
|
||||||
|
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
||||||
|
el => {
|
||||||
|
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
@@ -490,7 +766,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollTo (targetRow) {
|
scrollTo (targetRow) {
|
||||||
const blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
|
const blocks = this.getWindow().document.querySelectorAll(
|
||||||
|
'body>[data-line]'
|
||||||
|
)
|
||||||
|
|
||||||
for (let index = 0; index < blocks.length; index++) {
|
for (let index = 0; index < blocks.length; index++) {
|
||||||
let block = blocks[index]
|
let block = blocks[index]
|
||||||
@@ -510,36 +788,64 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
notify (title, options) {
|
notify (title, options) {
|
||||||
if (global.process.platform === 'win32') {
|
if (global.process.platform === 'win32') {
|
||||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
options.icon = path.join(
|
||||||
|
'file://',
|
||||||
|
global.__dirname,
|
||||||
|
'../../resources/app.png'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return new window.Notification(title, options)
|
return new window.Notification(title, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
handlelinkClick (e) {
|
handlelinkClick (e) {
|
||||||
const noteHash = e.target.href.split('/').pop()
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
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
|
// this will match the new uuid v4 hash and the old hash
|
||||||
// e.g.
|
// e.g.
|
||||||
// :note:1c211eb7dcb463de6490 and
|
// :note:1c211eb7dcb463de6490 and
|
||||||
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
|
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
|
||||||
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
|
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
|
||||||
if (regexIsNoteLink.test(noteHash)) {
|
if (regexIsNoteLink.test(linkHash)) {
|
||||||
eventEmitter.emit('list:jump', noteHash.replace(':note:', ''))
|
eventEmitter.emit('list:jump', linkHash.replace(':note:', ''))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// this will match the old link format storage.key-note.key
|
// this will match the old link format storage.key-note.key
|
||||||
// e.g.
|
// e.g.
|
||||||
// 877f99c3268608328037-1c211eb7dcb463de6490
|
// 877f99c3268608328037-1c211eb7dcb463de6490
|
||||||
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
|
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
|
||||||
if (regexIsLegacyNoteLink.test(noteHash)) {
|
if (regexIsLegacyNoteLink.test(linkHash)) {
|
||||||
eventEmitter.emit('list:jump', noteHash.split('-')[1])
|
eventEmitter.emit('list:jump', linkHash.split('-')[1])
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// other case
|
||||||
|
shell.openExternal(href)
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { className, style, tabIndex } = this.props
|
const { className, style, tabIndex } = this.props
|
||||||
return (
|
return (
|
||||||
<iframe className={className != null
|
<iframe
|
||||||
? 'MarkdownPreview ' + className
|
className={
|
||||||
: 'MarkdownPreview'
|
className != null ? 'MarkdownPreview ' + className : 'MarkdownPreview'
|
||||||
}
|
}
|
||||||
style={style}
|
style={style}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
@@ -554,9 +860,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
|
smartQuotes: PropTypes.bool,
|
||||||
|
smartArrows: PropTypes.bool,
|
||||||
|
breaks: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
this.focus = () => this.refs.code.focus()
|
this.focus = () => this.refs.code.focus()
|
||||||
this.reload = () => this.refs.code.reload()
|
this.reload = () => this.refs.code.reload()
|
||||||
this.userScroll = true
|
this.userScroll = true
|
||||||
|
this.state = {
|
||||||
|
isSliderFocused: false,
|
||||||
|
codeEditorWidthInPercent: 50
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOnChange () {
|
handleOnChange () {
|
||||||
@@ -87,20 +91,60 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 () {
|
render () {
|
||||||
const { config, value, storageKey } = this.props
|
const {config, value, storageKey, noteKey} = this.props
|
||||||
const storage = findStorage(storageKey)
|
const storage = findStorage(storageKey)
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
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
|
||||||
const previewStyle = {}
|
const previewStyle = {}
|
||||||
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%'
|
||||||
|
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none'
|
||||||
return (
|
return (
|
||||||
<div styleName='root'>
|
<div styleName='root' ref='root'
|
||||||
|
onMouseMove={e => this.handleMouseMove(e)}
|
||||||
|
onMouseUp={e => this.handleMouseUp(e)}>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
styleName='codeEditor'
|
styleName='codeEditor'
|
||||||
ref='code'
|
ref='code'
|
||||||
|
width={this.state.codeEditorWidthInPercent + '%'}
|
||||||
mode='GitHub Flavored Markdown'
|
mode='GitHub Flavored Markdown'
|
||||||
value={value}
|
value={value}
|
||||||
theme={config.editor.theme}
|
theme={config.editor.theme}
|
||||||
@@ -110,12 +154,18 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
displayLineNumbers={config.editor.displayLineNumbers}
|
displayLineNumbers={config.editor.displayLineNumbers}
|
||||||
indentType={config.editor.indentType}
|
indentType={config.editor.indentType}
|
||||||
indentSize={editorIndentSize}
|
indentSize={editorIndentSize}
|
||||||
|
enableRulers={config.editor.enableRulers}
|
||||||
|
rulers={config.editor.rulers}
|
||||||
scrollPastEnd={config.editor.scrollPastEnd}
|
scrollPastEnd={config.editor.scrollPastEnd}
|
||||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
|
noteKey={noteKey}
|
||||||
onChange={this.handleOnChange.bind(this)}
|
onChange={this.handleOnChange.bind(this)}
|
||||||
onScroll={this.handleScroll.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
|
<MarkdownPreview
|
||||||
style={previewStyle}
|
style={previewStyle}
|
||||||
styleName='preview'
|
styleName='preview'
|
||||||
@@ -128,6 +178,9 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
lineNumber={config.preview.lineNumber}
|
lineNumber={config.preview.lineNumber}
|
||||||
scrollPastEnd={config.preview.scrollPastEnd}
|
scrollPastEnd={config.preview.scrollPastEnd}
|
||||||
smartQuotes={config.preview.smartQuotes}
|
smartQuotes={config.preview.smartQuotes}
|
||||||
|
smartArrows={config.preview.smartArrows}
|
||||||
|
breaks={config.preview.breaks}
|
||||||
|
sanitize={config.preview.sanitize}
|
||||||
ref='preview'
|
ref='preview'
|
||||||
tabInde='0'
|
tabInde='0'
|
||||||
value={value}
|
value={value}
|
||||||
@@ -135,6 +188,9 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
onScroll={this.handleScroll.bind(this)}
|
onScroll={this.handleScroll.bind(this)}
|
||||||
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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,14 @@
|
|||||||
height 100%
|
height 100%
|
||||||
font-size 30px
|
font-size 30px
|
||||||
display flex
|
display flex
|
||||||
.codeEditor
|
.slider
|
||||||
width 50%
|
absolute top bottom
|
||||||
.preview
|
top -2px
|
||||||
width 50%
|
width 0
|
||||||
|
z-index 0
|
||||||
|
.slider-hitbox
|
||||||
|
absolute top bottom left right
|
||||||
|
width 7px
|
||||||
|
left -3px
|
||||||
|
z-index 10
|
||||||
|
cursor col-resize
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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.
|
||||||
@@ -25,14 +26,12 @@ const TagElement = ({ tagName }) => (
|
|||||||
* @param {Array|null} tags
|
* @param {Array|null} tags
|
||||||
* @return {React.Component}
|
* @return {React.Component}
|
||||||
*/
|
*/
|
||||||
const TagElementList = (tags) => {
|
const TagElementList = tags => {
|
||||||
if (!isArray(tags)) {
|
if (!isArray(tags)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagElements = tags.map(tag => (
|
const tagElements = tags.map(tag => TagElement({ tagName: tag }))
|
||||||
TagElement({tagName: tag})
|
|
||||||
))
|
|
||||||
|
|
||||||
return tagElements
|
return tagElements
|
||||||
}
|
}
|
||||||
@@ -58,10 +57,8 @@ const NoteItem = ({
|
|||||||
folderName,
|
folderName,
|
||||||
viewType
|
viewType
|
||||||
}) => (
|
}) => (
|
||||||
<div styleName={isActive
|
<div
|
||||||
? 'item--active'
|
styleName={isActive ? 'item--active' : 'item'}
|
||||||
: 'item'
|
|
||||||
}
|
|
||||||
key={note.key}
|
key={note.key}
|
||||||
onClick={e => handleNoteClick(e, note.key)}
|
onClick={e => handleNoteClick(e, note.key)}
|
||||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||||
@@ -71,42 +68,54 @@ const NoteItem = ({
|
|||||||
<div styleName='item-wrapper'>
|
<div styleName='item-wrapper'>
|
||||||
{note.type === 'SNIPPET_NOTE'
|
{note.type === 'SNIPPET_NOTE'
|
||||||
? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
|
? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
|
||||||
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />
|
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
|
||||||
}
|
|
||||||
<div styleName='item-title'>
|
<div styleName='item-title'>
|
||||||
{note.title.trim().length > 0
|
{note.title.trim().length > 0
|
||||||
? note.title
|
? note.title
|
||||||
: <span styleName='item-title-empty'>Empty</span>
|
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'>
|
{['ALL', 'STORAGE'].includes(viewType) &&
|
||||||
<div styleName='item-middle-time'>{dateDisplay}</div>
|
<div styleName='item-middle'>
|
||||||
<div styleName='item-middle-app-meta'>
|
<div styleName='item-middle-time'>{dateDisplay}</div>
|
||||||
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'>
|
<div styleName='item-middle-app-meta'>
|
||||||
{viewType === 'ALL' && storageName}
|
<div
|
||||||
{viewType === 'STORAGE' && folderName}
|
title={
|
||||||
|
viewType === 'ALL'
|
||||||
|
? storageName
|
||||||
|
: viewType === 'STORAGE' ? folderName : null
|
||||||
|
}
|
||||||
|
styleName='item-middle-app-meta-label'
|
||||||
|
>
|
||||||
|
{viewType === 'ALL' && storageName}
|
||||||
|
{viewType === 'STORAGE' && folderName}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>}
|
||||||
</div>}
|
|
||||||
|
|
||||||
<div styleName='item-bottom'>
|
<div styleName='item-bottom'>
|
||||||
<div styleName='item-bottom-tagList'>
|
<div styleName='item-bottom-tagList'>
|
||||||
{note.tags.length > 0
|
{note.tags.length > 0
|
||||||
? TagElementList(note.tags)
|
? TagElementList(note.tags)
|
||||||
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>No tags</span>
|
: <span
|
||||||
}
|
style={{ fontStyle: 'italic', opacity: 0.5 }}
|
||||||
|
styleName='item-bottom-tagList-empty'
|
||||||
|
>
|
||||||
|
{i18n.__('No tags')}
|
||||||
|
</span>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{note.isStarred
|
{note.isStarred
|
||||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
? <img
|
||||||
}
|
styleName='item-star'
|
||||||
|
src='../resources/icon/icon-starred.svg'
|
||||||
|
/>
|
||||||
|
: ''}
|
||||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||||
}
|
: ''}
|
||||||
{note.type === 'MARKDOWN_NOTE'
|
{note.type === 'MARKDOWN_NOTE'
|
||||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||||
: ''
|
: ''}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -321,3 +321,76 @@ body[data-theme="solarized-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="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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './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.
|
||||||
@@ -45,7 +46,7 @@ const NoteItemSimple = ({
|
|||||||
}
|
}
|
||||||
{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'>
|
{isAllNotesView && <div styleName='item-simple-right'>
|
||||||
<span styleName='item-simple-right-storageName'>
|
<span styleName='item-simple-right-storageName'>
|
||||||
|
|||||||
@@ -104,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
|
||||||
@@ -117,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
|
||||||
@@ -132,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
|
||||||
@@ -165,9 +168,10 @@ body[data-theme="solarized-dark"]
|
|||||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
&:hover
|
&:hover
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
// background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-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
|
||||||
@@ -178,9 +182,10 @@ body[data-theme="solarized-dark"]
|
|||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
&:active
|
&:active
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
background-color $ui-solarized-dark-button--active-backgroundColor
|
// background-color $ui-solarized-dark-button--active-backgroundColor
|
||||||
color $ui-solarized-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
|
||||||
@@ -192,11 +197,13 @@ body[data-theme="solarized-dark"]
|
|||||||
|
|
||||||
.item-simple--active
|
.item-simple--active
|
||||||
border-color $ui-solarized-dark-borderColor
|
border-color $ui-solarized-dark-borderColor
|
||||||
background-color $ui-solarized-dark-button--active-backgroundColor
|
background-color $ui-solarized-dark-tag-backgroundColor
|
||||||
.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
|
||||||
|
color $ui-dark-text-color
|
||||||
.item-simple-bottom-time
|
.item-simple-bottom-time
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
.item-simple-bottom-tagList-item
|
.item-simple-bottom-tagList-item
|
||||||
@@ -207,8 +214,75 @@ body[data-theme="solarized-dark"]
|
|||||||
color #c0392b
|
color #c0392b
|
||||||
.item-simple-bottom-tagList-item
|
.item-simple-bottom-tagList-item
|
||||||
background-color alpha(#fff, 20%)
|
background-color alpha(#fff, 20%)
|
||||||
.item-simple-right
|
.item-simple-title
|
||||||
float right
|
color $ui-dark-text-color
|
||||||
.item-simple-right-storageName
|
border-bottom $ui-dark-borderColor
|
||||||
padding-left 4px
|
.item-simple-right
|
||||||
opacity 0.4
|
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
|
||||||
|
|||||||
@@ -41,3 +41,14 @@ body[data-theme="solarized-dark"]
|
|||||||
background-color $ui-solarized-dark-button-backgroundColor
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
&:hover
|
&:hover
|
||||||
color #5CB85C
|
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
|
||||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './SideNavFilter.styl'
|
import styles from './SideNavFilter.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} isFolded
|
* @param {boolean} isFolded
|
||||||
@@ -31,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>
|
||||||
|
|
||||||
@@ -45,12 +46,12 @@ const SideNavFilter = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</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
|
||||||
@@ -59,7 +60,7 @@ const SideNavFilter = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span onContextMenu={handleFilterButtonContextMenu} 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>
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
.iconWrap
|
.iconWrap
|
||||||
width 20px
|
width 20px
|
||||||
text-align center
|
text-align center
|
||||||
|
|
||||||
.counters
|
.counters
|
||||||
float right
|
float right
|
||||||
color $ui-inactive-text-color
|
color $ui-inactive-text-color
|
||||||
@@ -68,10 +68,9 @@
|
|||||||
.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
|
||||||
@@ -222,4 +221,46 @@ body[data-theme="solarized-dark"]
|
|||||||
background-color $ui-solarized-dark-button-backgroundColor
|
background-color $ui-solarized-dark-button-backgroundColor
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
.menu-button-label
|
.menu-button-label
|
||||||
color $ui-solarized-dark-text-color
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +115,7 @@ class SnippetTab extends React.Component {
|
|||||||
{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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -54,9 +54,8 @@ const StorageItem = ({
|
|||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
>
|
>
|
||||||
{!isFolded && (
|
{!isFolded &&
|
||||||
<DraggableIcon className={styles['folderList-item-reorder']} />
|
<DraggableIcon className={styles['folderList-item-reorder']} />}
|
||||||
)}
|
|
||||||
<span
|
<span
|
||||||
styleName={
|
styleName={
|
||||||
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||||
@@ -72,12 +71,10 @@ const StorageItem = ({
|
|||||||
: folderName}
|
: folderName}
|
||||||
</span>
|
</span>
|
||||||
{!isFolded &&
|
{!isFolded &&
|
||||||
_.isNumber(noteCount) && (
|
_.isNumber(noteCount) &&
|
||||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
<span styleName='folderList-item-noteCount'>{noteCount}</span>}
|
||||||
)}
|
{isFolded &&
|
||||||
{isFolded && (
|
<span styleName='folderList-item-tooltip'>{folderName}</span>}
|
||||||
<span styleName='folderList-item-tooltip'>{folderName}</span>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,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
|
||||||
@@ -138,3 +138,22 @@ body[data-theme="solarized-dark"]
|
|||||||
&:hover
|
&:hover
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
background-color $ui-solarized-dark-button-backgroundColor
|
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
|
||||||
@@ -10,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
|
||||||
|
|||||||
@@ -9,16 +9,26 @@ 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, count}) => (
|
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
|
||||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
<div styleName='tagList-itemContainer'>
|
||||||
<span styleName='tagList-item-name'>
|
{isRelated
|
||||||
{`# ${name}`}
|
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
||||||
<span styleName='tagList-item-count'> {count}</span>
|
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
|
||||||
</span>
|
</button>
|
||||||
</button>
|
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
|
||||||
|
}
|
||||||
|
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||||
|
<span styleName='tagList-item-name'>
|
||||||
|
{`# ${name}`}
|
||||||
|
<span styleName='tagList-item-count'>{count}</span>
|
||||||
|
</span>
|
||||||
|
</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
|
||||||
@@ -49,7 +66,10 @@
|
|||||||
text-overflow ellipsis
|
text-overflow ellipsis
|
||||||
|
|
||||||
.tagList-item-count
|
.tagList-item-count
|
||||||
padding 0 3px
|
float right
|
||||||
|
line-height 26px
|
||||||
|
padding-right 15px
|
||||||
|
font-size 13px
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.tagList-item
|
.tagList-item
|
||||||
|
|||||||
@@ -47,5 +47,15 @@ body[data-theme="solarized-dark"]
|
|||||||
.progressBar
|
.progressBar
|
||||||
background-color: #2aa198
|
background-color: #2aa198
|
||||||
|
|
||||||
|
.percentageText
|
||||||
|
color #fdf6e3
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.percentageBar
|
||||||
|
background-color #f92672
|
||||||
|
|
||||||
|
.progressBar
|
||||||
|
background-color: #373831
|
||||||
|
|
||||||
.percentageText
|
.percentageText
|
||||||
color #fdf6e3
|
color #fdf6e3
|
||||||
@@ -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
|
||||||
@@ -68,7 +68,7 @@ body
|
|||||||
padding 5px
|
padding 5px
|
||||||
margin -5px
|
margin -5px
|
||||||
border-radius 5px
|
border-radius 5px
|
||||||
.flowchart-error, .sequence-error
|
.flowchart-error, .sequence-error .chart-error
|
||||||
background-color errorBackgroundColor
|
background-color errorBackgroundColor
|
||||||
color errorTextColor
|
color errorTextColor
|
||||||
padding 5px
|
padding 5px
|
||||||
@@ -199,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,7 +213,7 @@ pre
|
|||||||
margin 0 0 1em
|
margin 0 0 1em
|
||||||
display flex
|
display flex
|
||||||
line-height 1.4em
|
line-height 1.4em
|
||||||
&.flowchart, &.sequence
|
&.flowchart, &.sequence, &.chart
|
||||||
display flex
|
display flex
|
||||||
justify-content center
|
justify-content center
|
||||||
background-color white
|
background-color white
|
||||||
@@ -294,6 +293,84 @@ 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-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 24px
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
direction: ltr;
|
||||||
|
|
||||||
|
/* Support for all WebKit browsers. */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
/* Support for Safari and Chrome. */
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
|
||||||
|
/* Support for Firefox. */
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
/* Support for IE. */
|
||||||
|
font-feature-settings: 'liga';
|
||||||
|
|
||||||
|
$admonition-title
|
||||||
|
margin 0 -1.2rem
|
||||||
|
padding .8rem 1.2rem .8rem 4rem
|
||||||
|
border-bottom .1rem solid rgba(68,138,255,.1)
|
||||||
|
background-color rgba(68,138,255,.1)
|
||||||
|
font-weight 700
|
||||||
|
|
||||||
|
.admonition>.admonition-title:last-child
|
||||||
|
margin-bottom 0
|
||||||
|
|
||||||
|
admonition_types = {
|
||||||
|
note: {color: #0288D1, icon: "note"},
|
||||||
|
hint: {color: #009688, icon: "info_outline"},
|
||||||
|
danger: {color: #c2185b, icon: "block"},
|
||||||
|
caution: {color: #ffa726, icon: "warning"},
|
||||||
|
error: {color: #d32f2f, icon: "error_outline"},
|
||||||
|
attention: {color: #455a64, icon: "priority_high"}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, val in admonition_types
|
||||||
|
.admonition.{name}
|
||||||
|
@extend $admonition
|
||||||
|
border-left-color: val[color]
|
||||||
|
|
||||||
|
.admonition.{name}>.admonition-title
|
||||||
|
@extend $admonition-title
|
||||||
|
border-bottom-color: .1rem solid rgba(val[color], 0.2)
|
||||||
|
background-color: rgba(val[color], 0.2)
|
||||||
|
|
||||||
|
.admonition.{name}>.admonition-title:before
|
||||||
|
@extend $admonition-icon
|
||||||
|
color: val[color]
|
||||||
|
content: val[icon]
|
||||||
|
|
||||||
themeDarkBackground = darken(#21252B, 10%)
|
themeDarkBackground = darken(#21252B, 10%)
|
||||||
themeDarkText = #f9f9f9
|
themeDarkText = #f9f9f9
|
||||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||||
@@ -371,3 +448,32 @@ body[data-theme="solarized-dark"]
|
|||||||
border-color themeSolarizedDarkTableBorder
|
border-color themeSolarizedDarkTableBorder
|
||||||
&:last-child
|
&:last-child
|
||||||
border-right solid 1px themeSolarizedDarkTableBorder
|
border-right solid 1px themeSolarizedDarkTableBorder
|
||||||
|
|
||||||
|
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
|
||||||
39
browser/components/render/MermaidRender.js
Normal file
39
browser/components/render/MermaidRender.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import mermaidAPI from 'mermaid'
|
||||||
|
|
||||||
|
// fixes bad styling in the mermaid dark theme
|
||||||
|
const darkThemeStyling = `
|
||||||
|
.loopText tspan {
|
||||||
|
fill: white;
|
||||||
|
}`
|
||||||
|
|
||||||
|
function getRandomInt (min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min)) + min
|
||||||
|
}
|
||||||
|
|
||||||
|
function getId () {
|
||||||
|
var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||||
|
var id = 'm-'
|
||||||
|
for (var i = 0; i < 7; i++) {
|
||||||
|
id += pool[getRandomInt(0, 16)]
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
function render (element, content, theme) {
|
||||||
|
try {
|
||||||
|
let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai'
|
||||||
|
mermaidAPI.initialize({
|
||||||
|
theme: isDarkTheme ? 'dark' : 'default',
|
||||||
|
themeCSS: isDarkTheme ? darkThemeStyling : ''
|
||||||
|
})
|
||||||
|
mermaidAPI.render(getId(), content, (svgGraph) => {
|
||||||
|
element.innerHTML = svgGraph
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
element.className = 'mermaid-error'
|
||||||
|
element.innerHTML = 'mermaid diagram parse error: ' + e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default render
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
53
browser/lib/TextEditorInterface.js
Normal file
53
browser/lib/TextEditorInterface.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Point } from '@susisu/mte-kernel'
|
||||||
|
|
||||||
|
export default class TextEditorInterface {
|
||||||
|
constructor (editor) {
|
||||||
|
this.editor = editor
|
||||||
|
}
|
||||||
|
|
||||||
|
getCursorPosition () {
|
||||||
|
const pos = this.editor.getCursor()
|
||||||
|
return new Point(pos.line, pos.ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCursorPosition (pos) {
|
||||||
|
this.editor.setCursor({line: pos.row, ch: pos.column})
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectionRange (range) {
|
||||||
|
this.editor.setSelection({
|
||||||
|
anchor: {line: range.start.row, ch: range.start.column},
|
||||||
|
head: {line: range.end.row, ch: range.end.column}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastRow () {
|
||||||
|
return this.editor.lastLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptsTableEdit (row) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getLine (row) {
|
||||||
|
return this.editor.getLine(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
insertLine (row, line) {
|
||||||
|
this.editor.replaceRange(line, {line: row, ch: 0})
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteLine (row) {
|
||||||
|
this.editor.replaceRange('', {line: row, ch: 0}, {line: row, ch: this.editor.getLine(row).length})
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceLines (startRow, endRow, lines) {
|
||||||
|
endRow-- // because endRow is a first line after a table.
|
||||||
|
const endRowCh = this.editor.getLine(endRow).length
|
||||||
|
this.editor.replaceRange(lines, {line: startRow, ch: 0}, {line: endRow, ch: endRowCh})
|
||||||
|
}
|
||||||
|
|
||||||
|
transact (func) {
|
||||||
|
func()
|
||||||
|
}
|
||||||
|
}
|
||||||
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,16 @@ const consts = {
|
|||||||
'Dodger Blue',
|
'Dodger Blue',
|
||||||
'Violet Eggplant'
|
'Violet Eggplant'
|
||||||
],
|
],
|
||||||
THEMES: ['default'].concat(themes)
|
THEMES: ['default'].concat(themes),
|
||||||
|
SNIPPET_FILE: snippetFile,
|
||||||
|
DEFAULT_EDITOR_FONT_FAMILY: [
|
||||||
|
'Monaco',
|
||||||
|
'Menlo',
|
||||||
|
'Ubuntu Mono',
|
||||||
|
'Consolas',
|
||||||
|
'source-code-pro',
|
||||||
|
'monospace'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = consts
|
module.exports = consts
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,10 @@ export function getTodoStatus (content) {
|
|||||||
|
|
||||||
splitted.forEach((line) => {
|
splitted.forEach((line) => {
|
||||||
const trimmedLine = line.trim()
|
const trimmedLine = line.trim()
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) {
|
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
|
||||||
numberOfTodo++
|
numberOfTodo++
|
||||||
}
|
}
|
||||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
|
||||||
numberOfCompletedTodo++
|
numberOfCompletedTodo++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
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,6 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
import sanitizeHtml from 'sanitize-html'
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
import { escapeHtmlCharacters } from './utils'
|
||||||
|
|
||||||
module.exports = function sanitizePlugin (md, options) {
|
module.exports = function sanitizePlugin (md, options) {
|
||||||
options = options || {}
|
options = options || {}
|
||||||
@@ -8,13 +9,26 @@ module.exports = function sanitizePlugin (md, options) {
|
|||||||
md.core.ruler.after('linkify', 'sanitize_inline', state => {
|
md.core.ruler.after('linkify', 'sanitize_inline', state => {
|
||||||
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
|
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
|
||||||
if (state.tokens[tokenIdx].type === 'html_block') {
|
if (state.tokens[tokenIdx].type === 'html_block') {
|
||||||
state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options)
|
state.tokens[tokenIdx].content = sanitizeHtml(
|
||||||
|
state.tokens[tokenIdx].content,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (state.tokens[tokenIdx].type === 'fence') {
|
||||||
|
// escapeHtmlCharacters has better performance
|
||||||
|
state.tokens[tokenIdx].content = escapeHtmlCharacters(
|
||||||
|
state.tokens[tokenIdx].content,
|
||||||
|
{ skipSingleQuote: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (state.tokens[tokenIdx].type === 'inline') {
|
if (state.tokens[tokenIdx].type === 'inline') {
|
||||||
const inlineTokens = state.tokens[tokenIdx].children
|
const inlineTokens = state.tokens[tokenIdx].children
|
||||||
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
||||||
if (inlineTokens[childIdx].type === 'html_inline') {
|
if (inlineTokens[childIdx].type === 'html_inline') {
|
||||||
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options)
|
inlineTokens[childIdx].content = sanitizeHtml(
|
||||||
|
inlineTokens[childIdx].content,
|
||||||
|
options
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import markdownit from 'markdown-it'
|
|||||||
import sanitize from './markdown-it-sanitize-html'
|
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 ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
import {lastFindInArray} from './utils'
|
import katex from 'katex'
|
||||||
|
import { lastFindInArray } from './utils'
|
||||||
// FIXME We should not depend on global variable.
|
|
||||||
const katex = window.katex
|
|
||||||
const config = ConfigManager.get()
|
|
||||||
|
|
||||||
function createGutter (str, firstLineNumber) {
|
function createGutter (str, firstLineNumber) {
|
||||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||||
@@ -22,12 +20,13 @@ function createGutter (str, firstLineNumber) {
|
|||||||
|
|
||||||
class Markdown {
|
class Markdown {
|
||||||
constructor (options = {}) {
|
constructor (options = {}) {
|
||||||
|
const config = ConfigManager.get()
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
typographer: true,
|
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) {
|
||||||
const delimiter = ':'
|
const delimiter = ':'
|
||||||
const langInfo = lang.split(delimiter)
|
const langInfo = lang.split(delimiter)
|
||||||
@@ -41,57 +40,74 @@ class Markdown {
|
|||||||
if (langType === 'sequence') {
|
if (langType === 'sequence') {
|
||||||
return `<pre class="sequence">${str}</pre>`
|
return `<pre class="sequence">${str}</pre>`
|
||||||
}
|
}
|
||||||
|
if (langType === 'chart') {
|
||||||
|
return `<pre class="chart">${str}</pre>`
|
||||||
|
}
|
||||||
|
if (langType === 'mermaid') {
|
||||||
|
return `<pre class="mermaid">${str}</pre>`
|
||||||
|
}
|
||||||
return '<pre class="code CodeMirror">' +
|
return '<pre class="code CodeMirror">' +
|
||||||
'<span class="filename">' + fileName + '</span>' +
|
'<span class="filename">' + fileName + '</span>' +
|
||||||
createGutter(str, firstLineNumber) +
|
createGutter(str, firstLineNumber) +
|
||||||
'<code class="' + langType + '">' +
|
'<code class="' + langType + '">' +
|
||||||
str +
|
str +
|
||||||
'</code></pre>'
|
'</code></pre>'
|
||||||
}
|
},
|
||||||
|
sanitize: 'STRICT'
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedOptions = Object.assign(defaultOptions, options)
|
const updatedOptions = Object.assign(defaultOptions, options)
|
||||||
this.md = markdownit(updatedOptions)
|
this.md = markdownit(updatedOptions)
|
||||||
|
|
||||||
// Sanitize use rinput before other plugins
|
if (updatedOptions.sanitize !== 'NONE') {
|
||||||
this.md.use(sanitize, {
|
const allowedTags = ['iframe', 'input', 'b',
|
||||||
allowedTags: ['iframe', 'input',
|
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
|
'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',
|
'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'
|
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details'
|
||||||
],
|
]
|
||||||
allowedAttributes: {
|
const allowedAttributes = [
|
||||||
'*': [
|
'abbr', 'accept', 'accept-charset',
|
||||||
'abbr', 'accept', 'accept-charset',
|
'accesskey', 'action', 'align', 'alt', 'axis',
|
||||||
'accesskey', 'action', 'align', 'alt', 'axis',
|
'border', 'cellpadding', 'cellspacing', 'char',
|
||||||
'border', 'cellpadding', 'cellspacing', 'char',
|
'charoff', 'charset', 'checked',
|
||||||
'charoff', 'charset', 'checked',
|
'clear', 'cols', 'colspan', 'color',
|
||||||
'clear', 'cols', 'colspan', 'color',
|
'compact', 'coords', 'datetime', 'dir',
|
||||||
'compact', 'coords', 'datetime', 'dir',
|
'disabled', 'enctype', 'for', 'frame',
|
||||||
'disabled', 'enctype', 'for', 'frame',
|
'headers', 'height', 'hreflang',
|
||||||
'headers', 'height', 'hreflang',
|
'hspace', 'ismap', 'label', 'lang',
|
||||||
'hspace', 'ismap', 'label', 'lang',
|
'maxlength', 'media', 'method',
|
||||||
'maxlength', 'media', 'method',
|
'multiple', 'name', 'nohref', 'noshade',
|
||||||
'multiple', 'name', 'nohref', 'noshade',
|
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
|
||||||
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
|
'rows', 'rowspan', 'rules', 'scope',
|
||||||
'rows', 'rowspan', 'rules', 'scope',
|
'selected', 'shape', 'size', 'span',
|
||||||
'selected', 'shape', 'size', 'span',
|
'start', 'summary', 'tabindex', 'target',
|
||||||
'start', 'summary', 'tabindex', 'target',
|
'title', 'type', 'usemap', 'valign', 'value',
|
||||||
'title', 'type', 'usemap', 'valign', 'value',
|
'vspace', 'width', 'itemprop'
|
||||||
'vspace', 'width', 'itemprop'
|
]
|
||||||
],
|
|
||||||
'a': ['href'],
|
if (updatedOptions.sanitize === 'ALLOW_STYLES') {
|
||||||
'div': ['itemscope', 'itemtype'],
|
allowedTags.push('style')
|
||||||
'blockquote': ['cite'],
|
allowedAttributes.push('style')
|
||||||
'del': ['cite'],
|
}
|
||||||
'ins': ['cite'],
|
|
||||||
'q': ['cite'],
|
// Sanitize use rinput before other plugins
|
||||||
'img': ['src', 'width', 'height'],
|
this.md.use(sanitize, {
|
||||||
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
allowedTags,
|
||||||
'input': ['type', 'id', 'checked']
|
allowedAttributes: {
|
||||||
},
|
'*': allowedAttributes,
|
||||||
allowedIframeHostnames: ['www.youtube.com']
|
'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']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.md.use(emoji, {
|
this.md.use(emoji, {
|
||||||
shortcuts: {}
|
shortcuts: {}
|
||||||
@@ -132,15 +148,34 @@ class Markdown {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.md.use(require('markdown-it-kbd'))
|
this.md.use(require('markdown-it-kbd'))
|
||||||
|
this.md.use(require('markdown-it-admonition'))
|
||||||
|
|
||||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||||
this.md.use(require('markdown-it-plantuml'), '', {
|
this.md.use(require('markdown-it-plantuml'), '', {
|
||||||
generateSource: function (umlCode) {
|
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 s = unescape(encodeURIComponent(umlCode))
|
||||||
const zippedCode = deflate.encode64(
|
const zippedCode = deflate.encode64(
|
||||||
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
|
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
|
||||||
)
|
)
|
||||||
return `http://www.plantuml.com/plantuml/svg/${zippedCode}`
|
return `${serverAddress}/${zippedCode}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ditaa support
|
||||||
|
this.md.use(require('markdown-it-plantuml'), {
|
||||||
|
openMarker: '@startditaa',
|
||||||
|
closeMarker: '@endditaa',
|
||||||
|
generateSource: function (umlCode) {
|
||||||
|
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
||||||
|
// Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
|
||||||
|
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
|
||||||
|
const s = unescape(encodeURIComponent(umlCode))
|
||||||
|
const zippedCode = deflate.encode64(
|
||||||
|
deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9)
|
||||||
|
)
|
||||||
|
return `${serverAddress}/${zippedCode}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -202,6 +237,10 @@ class Markdown {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (config.preview.smartArrows) {
|
||||||
|
this.md.use(smartArrows)
|
||||||
|
}
|
||||||
|
|
||||||
// Add line number attribute for scrolling
|
// Add line number attribute for scrolling
|
||||||
const originalRender = this.md.renderer.render
|
const originalRender = this.md.renderer.render
|
||||||
this.md.renderer.render = (tokens, options, env) => {
|
this.md.renderer.render = (tokens, options, env) => {
|
||||||
@@ -225,11 +264,6 @@ class Markdown {
|
|||||||
if (!_.isString(content)) content = ''
|
if (!_.isString(content)) content = ''
|
||||||
return this.md.render(content)
|
return this.md.render(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeLinkText (linkText) {
|
|
||||||
return this.md.normalizeLinkText(linkText)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Markdown
|
export default Markdown
|
||||||
|
|
||||||
|
|||||||
0
browser/lib/markdown2.js
Normal file
0
browser/lib/markdown2.js
Normal file
9
browser/lib/normalizeEditorFontFamily.js
Normal file
9
browser/lib/normalizeEditorFontFamily.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import consts from 'browser/lib/consts'
|
||||||
|
import isString from 'lodash/isString'
|
||||||
|
|
||||||
|
export default function normalizeEditorFontFamily (fontFamily) {
|
||||||
|
const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
|
||||||
|
return isString(fontFamily) && fontFamily.length > 0
|
||||||
|
? [fontFamily].concat(defaultEditorFontFamily).join(', ')
|
||||||
|
: defaultEditorFontFamily.join(', ')
|
||||||
|
}
|
||||||
@@ -4,39 +4,30 @@ export default function searchFromNotes (notes, search) {
|
|||||||
if (search.trim().length === 0) return []
|
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
|
||||||
const regExp = new RegExp(_.escapeRegExp(tag), 'i')
|
if (tag.match(/^#.+/)) {
|
||||||
|
tag = tag.match(/#(.+)/)[1]
|
||||||
|
}
|
||||||
|
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||||
|
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||||
return notes.filter((note) => {
|
return notes.filter((note) => {
|
||||||
if (!_.isArray(note.tags)) return false
|
if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) {
|
||||||
return note.tags.some((_tag) => {
|
|
||||||
return _tag.match(regExp)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function findByWord (notes, block) {
|
|
||||||
const regExp = new RegExp(_.escapeRegExp(block), 'i')
|
|
||||||
return notes.filter((note) => {
|
|
||||||
if (_.isArray(note.tags) && note.tags.some((_tag) => {
|
|
||||||
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) || note.snippets.some((snippet) => {
|
||||||
|
return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
|
||||||
|
})
|
||||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||||
return note.content.match(regExp)
|
return note.content.match(wordRegExp)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,134 @@ export function lastFindInArray (array, callback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export function escapeHtmlCharacters (
|
||||||
lastFindInArray
|
html,
|
||||||
|
opt = { detectCodeBlock: false, skipSingleQuote: false }
|
||||||
|
) {
|
||||||
|
const matchHtmlRegExp = /["'&<>]/g
|
||||||
|
const matchCodeBlockRegExp = /```/g
|
||||||
|
const escapes = ['"', '&', ''', '<', '>']
|
||||||
|
let match = null
|
||||||
|
const replaceAt = (str, index, replace) =>
|
||||||
|
str.substr(0, index) +
|
||||||
|
replace +
|
||||||
|
str.substr(index + replace.length - (replace.length - 1))
|
||||||
|
|
||||||
|
while ((match = matchHtmlRegExp.exec(html)) !== null) {
|
||||||
|
const current = { char: match[0], index: match.index }
|
||||||
|
const codeBlockIndexs = []
|
||||||
|
let openCodeBlock = null
|
||||||
|
// if the detectCodeBlock option is activated then this function should skip
|
||||||
|
// characters that needed to be escape but located in code block
|
||||||
|
if (opt.detectCodeBlock) {
|
||||||
|
// The first type of code block is lines that start with 4 spaces
|
||||||
|
// Here we check for the \n character located before the character that
|
||||||
|
// needed to be escape. It means we check for the begining of the line that
|
||||||
|
// contain that character, then we check if there are 4 spaces next to the
|
||||||
|
// \n character (the line start with 4 spaces)
|
||||||
|
let previousLineEnd = current.index - 1
|
||||||
|
while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) {
|
||||||
|
previousLineEnd--
|
||||||
|
}
|
||||||
|
// 4 spaces means this character is in a code block
|
||||||
|
if (
|
||||||
|
html[previousLineEnd + 1] === ' ' &&
|
||||||
|
html[previousLineEnd + 2] === ' ' &&
|
||||||
|
html[previousLineEnd + 3] === ' ' &&
|
||||||
|
html[previousLineEnd + 4] === ' '
|
||||||
|
) {
|
||||||
|
// skip the current character
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// The second type of code block is lines that wrapped in ```
|
||||||
|
// We will get the position of each ```
|
||||||
|
// then push it into an array
|
||||||
|
// then the array returned will be like this:
|
||||||
|
// [startCodeblock, endCodeBlock, startCodeBlock, endCodeBlock]
|
||||||
|
while ((openCodeBlock = matchCodeBlockRegExp.exec(html)) !== null) {
|
||||||
|
codeBlockIndexs.push(openCodeBlock.index)
|
||||||
|
}
|
||||||
|
let shouldSkipChar = false
|
||||||
|
// we loop through the array of positions
|
||||||
|
// we skip 2 element as the i index position is the position of ``` that
|
||||||
|
// open the codeblock and the i + 1 is the position of the ``` that close
|
||||||
|
// the code block
|
||||||
|
for (let i = 0; i < codeBlockIndexs.length; i += 2) {
|
||||||
|
// the i index position is the position of the ``` that open code block
|
||||||
|
// so we have to + 2 as that position is the position of the first ` in the ````
|
||||||
|
// but we need to make sure that the position current character is larger
|
||||||
|
// that the last ` in the ``` that open the code block so we have to take
|
||||||
|
// the position of the first ` and + 2
|
||||||
|
// the i + 1 index position is the closing ``` so the char must less than it
|
||||||
|
if (
|
||||||
|
current.index > codeBlockIndexs[i] + 2 &&
|
||||||
|
current.index < codeBlockIndexs[i + 1]
|
||||||
|
) {
|
||||||
|
// skip it
|
||||||
|
shouldSkipChar = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldSkipChar) {
|
||||||
|
// skip the current character
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise, escape it !!!
|
||||||
|
if (current.char === '&') {
|
||||||
|
// when escaping character & we have to be becareful as the & could be a part
|
||||||
|
// of an escaped character like " will be came &quot;
|
||||||
|
let nextStr = ''
|
||||||
|
let nextIndex = current.index
|
||||||
|
let escapedStr = false
|
||||||
|
// maximum length of an escaped string is 5. For example ('"')
|
||||||
|
// we take the next 5 character of the next string if it is one of the string:
|
||||||
|
// ['"', '&', ''', '<', '>'] then we will not escape the & character
|
||||||
|
// as it is a part of the escaped string and should not be escaped
|
||||||
|
while (nextStr.length <= 5) {
|
||||||
|
nextStr += html[nextIndex]
|
||||||
|
nextIndex++
|
||||||
|
if (escapes.indexOf(nextStr) !== -1) {
|
||||||
|
escapedStr = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!escapedStr) {
|
||||||
|
// this & char is not a part of an escaped string
|
||||||
|
html = replaceAt(html, current.index, '&')
|
||||||
|
}
|
||||||
|
} else if (current.char === '"') {
|
||||||
|
html = replaceAt(html, current.index, '"')
|
||||||
|
} else if (current.char === "'" && !opt.skipSingleQuote) {
|
||||||
|
html = replaceAt(html, current.index, ''')
|
||||||
|
} else if (current.char === '<') {
|
||||||
|
html = replaceAt(html, current.index, '<')
|
||||||
|
} else if (current.char === '>') {
|
||||||
|
html = replaceAt(html, current.index, '>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObjectEqual (a, b) {
|
||||||
|
const aProps = Object.getOwnPropertyNames(a)
|
||||||
|
const bProps = Object.getOwnPropertyNames(b)
|
||||||
|
|
||||||
|
if (aProps.length !== bProps.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < aProps.length; i++) {
|
||||||
|
const propName = aProps[i]
|
||||||
|
if (a[propName] !== b[propName]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
lastFindInArray,
|
||||||
|
escapeHtmlCharacters,
|
||||||
|
isObjectEqual
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,3 +30,10 @@ body[data-theme="solarized-dark"]
|
|||||||
border-left 1px solid $ui-solarized-dark-borderColor
|
border-left 1px solid $ui-solarized-dark-borderColor
|
||||||
.empty-message
|
.empty-message
|
||||||
color $ui-solarized-dark-text-color
|
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
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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) {
|
||||||
@@ -249,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)}
|
||||||
|
|||||||
@@ -133,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
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './FullscreenButton.styl'
|
import styles from './FullscreenButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
||||||
const FullscreenButton = ({
|
const FullscreenButton = ({
|
||||||
onClick
|
onClick
|
||||||
}) => (
|
}) => (
|
||||||
<button styleName='control-fullScreenButton' title='Fullscreen' onMouseDown={(e) => onClick(e)}>
|
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||||
<span styleName='tooltip'>Fullscreen</span>
|
<span styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './InfoButton.styl'
|
import styles from './InfoButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoButton = ({
|
const InfoButton = ({
|
||||||
onClick
|
onClick
|
||||||
@@ -10,7 +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'>Info</span>
|
<span styleName='tooltip'>{i18n.__('Info')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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 copy from 'copy-to-clipboard'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class InfoPanel extends React.Component {
|
class InfoPanel extends React.Component {
|
||||||
copyNoteLink () {
|
copyNoteLink () {
|
||||||
@@ -19,7 +20,7 @@ class InfoPanel extends React.Component {
|
|||||||
<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 />
|
||||||
@@ -29,11 +30,11 @@ class InfoPanel extends React.Component {
|
|||||||
: <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>
|
||||||
}
|
}
|
||||||
@@ -45,17 +46,17 @@ class InfoPanel extends React.Component {
|
|||||||
|
|
||||||
<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>
|
||||||
@@ -63,7 +64,7 @@ class InfoPanel extends React.Component {
|
|||||||
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
|
||||||
<i className='fa fa-clipboard' />
|
<i className='fa fa-clipboard' />
|
||||||
</button>
|
</button>
|
||||||
<p styleName='infoPanel-sub'>NOTE LINK</p>
|
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
@@ -71,22 +72,22 @@ class InfoPanel extends React.Component {
|
|||||||
<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' />
|
<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' />
|
<i className='fa fa-file-text-o' />
|
||||||
<p>.txt</p>
|
<p>{i18n.__('.txt')}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||||
<i className='fa fa-html5' />
|
<i className='fa fa-html5' />
|
||||||
<p>.html</p>
|
<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' />
|
<i className='fa fa-print' />
|
||||||
<p>Print</p>
|
<p>{i18n.__('Print')}</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
.control-infoButton-panel
|
.control-infoButton-panel
|
||||||
z-index 200
|
z-index 200
|
||||||
margin-top 0px
|
margin-top 0px
|
||||||
|
top: 50px
|
||||||
right 25px
|
right 25px
|
||||||
position absolute
|
position absolute
|
||||||
padding 20px 25px 0 25px
|
padding 20px 25px 0 25px
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
.control-infoButton-panel-trash
|
.control-infoButton-panel-trash
|
||||||
z-index 200
|
z-index 200
|
||||||
margin-top 0px
|
margin-top 0px
|
||||||
|
top 50px
|
||||||
right 0px
|
right 0px
|
||||||
position absolute
|
position absolute
|
||||||
padding 20px 25px 0 25px
|
padding 20px 25px 0 25px
|
||||||
@@ -215,3 +217,43 @@ body[data-theme="solarized-dark"]
|
|||||||
color $ui-dark-inactive-text-color
|
color $ui-dark-inactive-text-color
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-solarized-ark-text-color
|
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
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './InfoPanel.styl'
|
import styles from './InfoPanel.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const InfoPanelTrashed = ({
|
const InfoPanelTrashed = ({
|
||||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
|
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
|
||||||
@@ -9,24 +10,24 @@ const InfoPanelTrashed = ({
|
|||||||
<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'>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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'
|
||||||
|
|
||||||
class MarkdownNoteDetail extends React.Component {
|
class MarkdownNoteDetail extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -54,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)
|
||||||
@@ -181,10 +186,10 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
handleTrashButtonClick (e) {
|
handleTrashButtonClick (e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
const { isTrashed } = note
|
const { isTrashed } = note
|
||||||
const { confirmDeletion } = this.props
|
const { confirmDeletion } = this.props.config.ui
|
||||||
|
|
||||||
if (isTrashed) {
|
if (isTrashed) {
|
||||||
if (confirmDeletion(true)) {
|
if (confirmDeleteNote(confirmDeletion, true)) {
|
||||||
const {note, dispatch} = this.props
|
const {note, dispatch} = this.props
|
||||||
dataApi
|
dataApi
|
||||||
.deleteNote(note.storage, note.key)
|
.deleteNote(note.storage, note.key)
|
||||||
@@ -201,7 +206,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
.then(() => ee.emit('list:next'))
|
.then(() => ee.emit('list:next'))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (confirmDeletion()) {
|
if (confirmDeleteNote(confirmDeletion, false)) {
|
||||||
note.isTrashed = true
|
note.isTrashed = true
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -272,6 +277,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleSwitchMode (type) {
|
handleSwitchMode (type) {
|
||||||
this.setState({ editorType: type }, () => {
|
this.setState({ editorType: type }, () => {
|
||||||
|
this.focus()
|
||||||
const newConfig = Object.assign({}, this.props.config)
|
const newConfig = Object.assign({}, this.props.config)
|
||||||
newConfig.editor.type = type
|
newConfig.editor.type = type
|
||||||
ConfigManager.set(newConfig)
|
ConfigManager.set(newConfig)
|
||||||
@@ -288,6 +294,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
config={config}
|
config={config}
|
||||||
value={note.content}
|
value={note.content}
|
||||||
storageKey={note.storage}
|
storageKey={note.storage}
|
||||||
|
noteKey={note.key}
|
||||||
onChange={this.handleUpdateContent.bind(this)}
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
/>
|
/>
|
||||||
@@ -297,6 +304,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
config={config}
|
config={config}
|
||||||
value={note.content}
|
value={note.content}
|
||||||
storageKey={note.storage}
|
storageKey={note.storage}
|
||||||
|
noteKey={note.key}
|
||||||
onChange={this.handleUpdateContent.bind(this)}
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
/>
|
/>
|
||||||
@@ -437,8 +445,7 @@ MarkdownNoteDetail.propTypes = {
|
|||||||
style: PropTypes.shape({
|
style: PropTypes.shape({
|
||||||
left: PropTypes.number
|
left: PropTypes.number
|
||||||
}),
|
}),
|
||||||
ignorePreviewPointerEvents: PropTypes.bool,
|
ignorePreviewPointerEvents: PropTypes.bool
|
||||||
confirmDeletion: PropTypes.bool.isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(MarkdownNoteDetail, styles)
|
export default CSSModules(MarkdownNoteDetail, styles)
|
||||||
|
|||||||
@@ -71,3 +71,8 @@ body[data-theme="solarized-dark"]
|
|||||||
.root
|
.root
|
||||||
border-left 1px solid $ui-solarized-dark-borderColor
|
border-left 1px solid $ui-solarized-dark-borderColor
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
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
|
||||||
|
|||||||
@@ -98,3 +98,7 @@ body[data-theme="solarized-dark"]
|
|||||||
border-color $ui-solarized-dark-borderColor
|
border-color $ui-solarized-dark-borderColor
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.info
|
||||||
|
border-color $ui-monokai-borderColor
|
||||||
|
background-color $ui-monokai-noteDetail-backgroundColor
|
||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TrashButton.styl'
|
import styles from './TrashButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const PermanentDeleteButton = ({
|
const PermanentDeleteButton = ({
|
||||||
onClick
|
onClick
|
||||||
@@ -10,7 +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'>Permanent Delete</span>
|
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './RestoreButton.styl'
|
import styles from './RestoreButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const RestoreButton = ({
|
const RestoreButton = ({
|
||||||
onClick
|
onClick
|
||||||
@@ -10,7 +11,7 @@ const RestoreButton = ({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
|
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
|
||||||
<span styleName='tooltip'>Restore</span>
|
<span styleName='tooltip'>{i18n.__('Restore')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import StarButton from './StarButton'
|
|||||||
import TagSelect from './TagSelect'
|
import TagSelect from './TagSelect'
|
||||||
import FolderSelect from './FolderSelect'
|
import FolderSelect from './FolderSelect'
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
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 'codemirror-mode-elixir'
|
||||||
@@ -17,7 +17,8 @@ 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 RestoreButton from './RestoreButton'
|
||||||
@@ -26,25 +27,12 @@ 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
|
||||||
const { Menu, MenuItem, dialog } = remote
|
const { dialog } = remote
|
||||||
|
|
||||||
class SnippetNoteDetail extends React.Component {
|
class SnippetNoteDetail extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -53,16 +41,34 @@ 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()
|
||||||
const nextNote = Object.assign({
|
const nextNote = Object.assign({
|
||||||
description: ''
|
description: ''
|
||||||
@@ -78,6 +84,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
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())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,10 +184,10 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
handleTrashButtonClick (e) {
|
handleTrashButtonClick (e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
const { isTrashed } = note
|
const { isTrashed } = note
|
||||||
const { confirmDeletion } = this.props
|
const { confirmDeletion } = this.props.config.ui
|
||||||
|
|
||||||
if (isTrashed) {
|
if (isTrashed) {
|
||||||
if (confirmDeletion(true)) {
|
if (confirmDeleteNote(confirmDeletion, true)) {
|
||||||
const {note, dispatch} = this.props
|
const {note, dispatch} = this.props
|
||||||
dataApi
|
dataApi
|
||||||
.deleteNote(note.storage, note.key)
|
.deleteNote(note.storage, note.key)
|
||||||
@@ -197,7 +204,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
.then(() => ee.emit('list:next'))
|
.then(() => ee.emit('list:next'))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (confirmDeletion()) {
|
if (confirmDeleteNote(confirmDeletion, false)) {
|
||||||
note.isTrashed = true
|
note.isTrashed = true
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -228,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()
|
||||||
}
|
}
|
||||||
@@ -264,9 +316,9 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
if (this.state.note.snippets[index].content.trim().length > 0) {
|
if (this.state.note.snippets[index].content.trim().length > 0) {
|
||||||
const 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)
|
||||||
@@ -287,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())
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,11 +368,11 @@ 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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -314,11 +381,11 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
return (e) => {
|
return (e) => {
|
||||||
const snippets = this.state.note.snippets.slice()
|
const snippets = this.state.note.snippets.slice()
|
||||||
snippets[index].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()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -332,10 +399,10 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
return (e) => {
|
return (e) => {
|
||||||
const snippets = this.state.note.snippets.slice()
|
const snippets = this.state.note.snippets.slice()
|
||||||
snippets[index].content = this.refs['code-' + index].value
|
snippets[index].content = this.refs['code-' + index].value
|
||||||
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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -374,7 +441,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const isSuper = global.process.platform === 'darwin'
|
const isSuper = global.process.platform === 'darwin'
|
||||||
? e.metaKey
|
? e.metaKey
|
||||||
: e.ctrlKey
|
: e.ctrlKey
|
||||||
if (isSuper) {
|
if (isSuper && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.addSnippet()
|
this.addSnippet()
|
||||||
}
|
}
|
||||||
@@ -384,14 +451,14 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleModeButtonClick (e, index) {
|
handleModeButtonClick (e, index) {
|
||||||
const menu = new Menu()
|
const templetes = []
|
||||||
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
|
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
|
||||||
menu.append(new MenuItem({
|
templetes.push({
|
||||||
label: mode.name,
|
label: mode.name,
|
||||||
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
||||||
}))
|
})
|
||||||
})
|
})
|
||||||
menu.popup(remote.getCurrentWindow())
|
context.popup(templetes)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleIndentTypeButtonClick (e) {
|
handleIndentTypeButtonClick (e) {
|
||||||
@@ -460,6 +527,51 @@ 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 () {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
@@ -470,26 +582,32 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}])
|
}])
|
||||||
const 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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -507,9 +625,9 @@ 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')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,7 +663,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
const viewList = note.snippets.map((snippet, index) => {
|
const viewList = note.snippets.map((snippet, index) => {
|
||||||
const isActive = this.state.snippetIndex === index
|
const isActive = this.state.snippetIndex === index
|
||||||
|
|
||||||
let syntax = CodeMirror.findModeByName(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'
|
||||||
@@ -635,10 +753,10 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
isActive={note.isStarred}
|
isActive={note.isStarred}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button styleName='control-fullScreenButton' title='Fullscreen'
|
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
|
||||||
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
onMouseDown={(e) => this.handleFullScreenButton(e)}>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||||
<span styleName='tooltip'>Fullscreen</span>
|
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||||
@@ -676,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'
|
||||||
{tabList}
|
hidden={!this.state.showArrows}
|
||||||
|
disabled={!this.state.enableLeftArrow}
|
||||||
|
onClick={(e) => this.handleTabMoveLeftButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-chevron-left' />
|
||||||
|
</button>
|
||||||
|
<div styleName='list' onScroll={(e) => { this.setState(this.getArrowsState()) }} ref={(tabs) => { this.visibleTabs = tabs }}>
|
||||||
|
<div styleName='allTabs' ref={(tabs) => { this.allTabs = tabs }}>
|
||||||
|
{tabList}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button styleName='plusButton'
|
<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' />
|
||||||
@@ -699,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' />
|
||||||
@@ -736,8 +870,7 @@ SnippetNoteDetail.propTypes = {
|
|||||||
style: PropTypes.shape({
|
style: PropTypes.shape({
|
||||||
left: PropTypes.number
|
left: PropTypes.number
|
||||||
}),
|
}),
|
||||||
ignorePreviewPointerEvents: PropTypes.bool,
|
ignorePreviewPointerEvents: PropTypes.bool
|
||||||
confirmDeletion: PropTypes.bool.isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(SnippetNoteDetail, styles)
|
export default CSSModules(SnippetNoteDetail, styles)
|
||||||
|
|||||||
@@ -35,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
|
||||||
|
|
||||||
@@ -139,4 +152,21 @@ body[data-theme="solarized-dark"]
|
|||||||
|
|
||||||
.tabList
|
.tabList
|
||||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
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
|
||||||
@@ -3,6 +3,7 @@ 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) {
|
||||||
@@ -53,7 +54,7 @@ class StarButton extends React.Component {
|
|||||||
: '../resources/icon/icon-star.svg'
|
: '../resources/icon/icon-star.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span styleName='tooltip'>Star</span>
|
<span styleName='tooltip'>{i18n.__('Star')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
|
|||||||
import styles from './TagSelect.styl'
|
import styles from './TagSelect.styl'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
|
||||||
class TagSelect extends React.Component {
|
class TagSelect extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -12,16 +14,26 @@ class TagSelect extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
newTag: ''
|
newTag: ''
|
||||||
}
|
}
|
||||||
|
this.addtagHandler = this.handleAddTag.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.value = this.props.value
|
this.value = this.props.value
|
||||||
|
ee.on('editor:add-tag', this.addtagHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
componentDidUpdate () {
|
||||||
this.value = this.props.value
|
this.value = this.props.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
ee.off('editor:add-tag', this.addtagHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddTag () {
|
||||||
|
this.refs.newTag.focus()
|
||||||
|
}
|
||||||
|
|
||||||
handleNewTagInputKeyDown (e) {
|
handleNewTagInputKeyDown (e) {
|
||||||
switch (e.keyCode) {
|
switch (e.keyCode) {
|
||||||
case 9:
|
case 9:
|
||||||
@@ -43,16 +55,9 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeLastTag () {
|
removeLastTag () {
|
||||||
let { value } = this.props
|
this.removeTagByCallback((value) => {
|
||||||
|
value.pop()
|
||||||
value = _.isArray(value)
|
})
|
||||||
? value.slice()
|
|
||||||
: []
|
|
||||||
value.pop()
|
|
||||||
value = _.uniq(value)
|
|
||||||
|
|
||||||
this.value = value
|
|
||||||
this.props.onChange()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset () {
|
reset () {
|
||||||
@@ -95,15 +100,22 @@ class TagSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTagRemoveButtonClick (tag) {
|
handleTagRemoveButtonClick (tag) {
|
||||||
return (e) => {
|
this.removeTagByCallback((value, tag) => {
|
||||||
let { value } = this.props
|
|
||||||
|
|
||||||
value.splice(value.indexOf(tag), 1)
|
value.splice(value.indexOf(tag), 1)
|
||||||
value = _.uniq(value)
|
}, tag)
|
||||||
|
}
|
||||||
|
|
||||||
this.value = value
|
removeTagByCallback (callback, tag = null) {
|
||||||
this.props.onChange()
|
let { value } = this.props
|
||||||
}
|
|
||||||
|
value = _.isArray(value)
|
||||||
|
? value.slice()
|
||||||
|
: []
|
||||||
|
callback(value, tag)
|
||||||
|
value = _.uniq(value)
|
||||||
|
|
||||||
|
this.value = value
|
||||||
|
this.props.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@@ -117,7 +129,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>
|
||||||
@@ -137,7 +149,7 @@ 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)}
|
onBlur={(e) => this.handleNewTagBlur(e)}
|
||||||
|
|||||||
@@ -81,4 +81,20 @@ body[data-theme="solarized-dark"]
|
|||||||
.newTag
|
.newTag
|
||||||
border-color none
|
border-color none
|
||||||
background-color transparent
|
background-color transparent
|
||||||
color $ui-solarized-dark-text-color
|
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
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './ToggleModeButton.styl'
|
import styles from './ToggleModeButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const ToggleModeButton = ({
|
const ToggleModeButton = ({
|
||||||
onClick, editorType
|
onClick, editorType
|
||||||
@@ -13,7 +14,7 @@ const ToggleModeButton = ({
|
|||||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||||
</div>
|
</div>
|
||||||
<span styleName='tooltip'>Toggle Mode</span>
|
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -56,3 +56,10 @@ body[data-theme="solarized-dark"]
|
|||||||
.active
|
.active
|
||||||
background-color #1EC38B
|
background-color #1EC38B
|
||||||
box-shadow 2px 0px 7px #222222
|
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
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TrashButton.styl'
|
import styles from './TrashButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const TrashButton = ({
|
const TrashButton = ({
|
||||||
onClick
|
onClick
|
||||||
@@ -10,7 +11,7 @@ const TrashButton = ({
|
|||||||
onClick={(e) => onClick(e)}
|
onClick={(e) => onClick(e)}
|
||||||
>
|
>
|
||||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||||
<span styleName='tooltip'>Trash</span>
|
<span styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ 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'
|
||||||
|
import searchFromNotes from 'browser/lib/search'
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
@@ -32,32 +35,39 @@ class Detail extends React.Component {
|
|||||||
ee.off('detail:delete', this.deleteHandler)
|
ee.off('detail:delete', this.deleteHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmDeletion (permanent) {
|
|
||||||
if (this.props.config.ui.confirmDeletion || permanent) {
|
|
||||||
const electron = require('electron')
|
|
||||||
const { remote } = electron
|
|
||||||
const { dialog } = remote
|
|
||||||
|
|
||||||
const alertConfig = {
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Confirm note deletion',
|
|
||||||
detail: 'This will permanently remove this note.',
|
|
||||||
buttons: ['Confirm', 'Cancel']
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), alertConfig)
|
|
||||||
return dialogueButtonIndex === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { location, data, config } = this.props
|
const { location, data, params, config } = this.props
|
||||||
let note = null
|
let note = null
|
||||||
|
|
||||||
if (location.query.key != null) {
|
if (location.query.key != null) {
|
||||||
const noteKey = location.query.key
|
const noteKey = location.query.key
|
||||||
note = data.noteMap.get(noteKey)
|
const allNotes = data.noteMap.map(note => note)
|
||||||
|
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||||
|
let displayedNotes = allNotes
|
||||||
|
|
||||||
|
if (location.pathname.match(/\/searched/)) {
|
||||||
|
const searchStr = params.searchword
|
||||||
|
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
|
||||||
|
: searchFromNotes(allNotes, searchStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.pathname.match(/\/tags/)) {
|
||||||
|
const listOfTags = params.tagname.split(' ')
|
||||||
|
displayedNotes = data.noteMap.map(note => note).filter(note =>
|
||||||
|
listOfTags.every(tag => note.tags.includes(tag))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.pathname.match(/\/trashed/)) {
|
||||||
|
displayedNotes = trashedNotes
|
||||||
|
} else {
|
||||||
|
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteKeys = displayedNotes.map(note => note.key)
|
||||||
|
if (noteKeys.includes(noteKey)) {
|
||||||
|
note = data.noteMap.get(noteKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
@@ -67,7 +77,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'])}
|
||||||
@@ -81,7 +91,6 @@ class Detail extends React.Component {
|
|||||||
<SnippetNoteDetail
|
<SnippetNoteDetail
|
||||||
note={note}
|
note={note}
|
||||||
config={config}
|
config={config}
|
||||||
confirmDeletion={(permanent) => this.confirmDeletion(permanent)}
|
|
||||||
ref='root'
|
ref='root'
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
@@ -98,7 +107,6 @@ class Detail extends React.Component {
|
|||||||
<MarkdownNoteDetail
|
<MarkdownNoteDetail
|
||||||
note={note}
|
note={note}
|
||||||
config={config}
|
config={config}
|
||||||
confirmDeletion={(permanent) => this.confirmDeletion(permanent)}
|
|
||||||
ref='root'
|
ref='root'
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
@@ -120,4 +128,4 @@ Detail.propTypes = {
|
|||||||
ignorePreviewPointerEvents: PropTypes.bool
|
ignorePreviewPointerEvents: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(Detail, styles)
|
export default debounceRender(CSSModules(Detail, styles))
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ 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 { hashHistory } from 'react-router'
|
||||||
import store from 'browser/main/store'
|
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 path = require('path')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote } = electron
|
const { remote } = electron
|
||||||
|
|
||||||
class Main extends React.Component {
|
class Main extends React.Component {
|
||||||
|
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
@@ -57,10 +59,10 @@ class Main extends React.Component {
|
|||||||
name: 'My Storage',
|
name: 'My Storage',
|
||||||
path: path.join(remote.app.getPath('home'), 'Boostnote')
|
path: path.join(remote.app.getPath('home'), 'Boostnote')
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
if (data.storage.folders[0] != null) {
|
if (data.storage.folders[0] != null) {
|
||||||
return data
|
return data
|
||||||
} else {
|
} else {
|
||||||
@@ -69,7 +71,7 @@ class Main extends React.Component {
|
|||||||
color: '#1278BD',
|
color: '#1278BD',
|
||||||
name: 'Default'
|
name: 'Default'
|
||||||
})
|
})
|
||||||
.then((_data) => {
|
.then(_data => {
|
||||||
return {
|
return {
|
||||||
storage: _data.storage,
|
storage: _data.storage,
|
||||||
notes: data.notes
|
notes: data.notes
|
||||||
@@ -77,7 +79,7 @@ class Main extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'ADD_STORAGE',
|
type: 'ADD_STORAGE',
|
||||||
@@ -95,16 +97,16 @@ class Main extends React.Component {
|
|||||||
{
|
{
|
||||||
name: 'example.html',
|
name: 'example.html',
|
||||||
mode: 'html',
|
mode: 'html',
|
||||||
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>'
|
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'example.js',
|
name: 'example.js',
|
||||||
mode: 'javascript',
|
mode: 'javascript',
|
||||||
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)'
|
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then(note => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
@@ -117,7 +119,7 @@ class Main extends React.Component {
|
|||||||
title: 'Welcome to Boostnote!',
|
title: 'Welcome to Boostnote!',
|
||||||
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then(note => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
note: note
|
note: note
|
||||||
@@ -128,10 +130,10 @@ class Main extends React.Component {
|
|||||||
.then(defaultMarkdownNote)
|
.then(defaultMarkdownNote)
|
||||||
.then(() => data.storage)
|
.then(() => data.storage)
|
||||||
})
|
})
|
||||||
.then((storage) => {
|
.then(storage => {
|
||||||
hashHistory.push('/storages/' + storage.key)
|
hashHistory.push('/storages/' + storage.key)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -139,30 +141,33 @@ class Main extends React.Component {
|
|||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, config } = this.props
|
const { dispatch, config } = this.props
|
||||||
|
|
||||||
if (config.ui.theme === 'dark') {
|
const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai']
|
||||||
document.body.setAttribute('data-theme', 'dark')
|
|
||||||
} else if (config.ui.theme === 'white') {
|
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
|
||||||
document.body.setAttribute('data-theme', 'white')
|
document.body.setAttribute('data-theme', config.ui.theme)
|
||||||
} else if (config.ui.theme === 'solarized-dark') {
|
|
||||||
document.body.setAttribute('data-theme', 'solarized-dark')
|
|
||||||
} 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) => {
|
dispatch({
|
||||||
dispatch({
|
type: 'INIT_ALL',
|
||||||
type: 'INIT_ALL',
|
storages: data.storages,
|
||||||
storages: data.storages,
|
notes: data.notes
|
||||||
notes: data.notes
|
|
||||||
})
|
|
||||||
|
|
||||||
if (data.storages.length < 1) {
|
|
||||||
this.init()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (data.storages.length < 1) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,34 +192,40 @@ class Main extends React.Component {
|
|||||||
handleMouseUp (e) {
|
handleMouseUp (e) {
|
||||||
// Change width of NoteList component.
|
// Change width of NoteList component.
|
||||||
if (this.state.isRightSliderFocused) {
|
if (this.state.isRightSliderFocused) {
|
||||||
this.setState({
|
this.setState(
|
||||||
isRightSliderFocused: false
|
{
|
||||||
}, () => {
|
isRightSliderFocused: false
|
||||||
const { dispatch } = this.props
|
},
|
||||||
const newListWidth = this.state.listWidth
|
() => {
|
||||||
// TODO: ConfigManager should dispatch itself.
|
const { dispatch } = this.props
|
||||||
ConfigManager.set({listWidth: newListWidth})
|
const newListWidth = this.state.listWidth
|
||||||
dispatch({
|
// TODO: ConfigManager should dispatch itself.
|
||||||
type: 'SET_LIST_WIDTH',
|
ConfigManager.set({ listWidth: newListWidth })
|
||||||
listWidth: newListWidth
|
dispatch({
|
||||||
})
|
type: 'SET_LIST_WIDTH',
|
||||||
})
|
listWidth: newListWidth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change width of SideNav component.
|
// Change width of SideNav component.
|
||||||
if (this.state.isLeftSliderFocused) {
|
if (this.state.isLeftSliderFocused) {
|
||||||
this.setState({
|
this.setState(
|
||||||
isLeftSliderFocused: false
|
{
|
||||||
}, () => {
|
isLeftSliderFocused: false
|
||||||
const { dispatch } = this.props
|
},
|
||||||
const navWidth = this.state.navWidth
|
() => {
|
||||||
// TODO: ConfigManager should dispatch itself.
|
const { dispatch } = this.props
|
||||||
ConfigManager.set({ navWidth })
|
const navWidth = this.state.navWidth
|
||||||
dispatch({
|
// TODO: ConfigManager should dispatch itself.
|
||||||
type: 'SET_NAV_WIDTH',
|
ConfigManager.set({ navWidth })
|
||||||
navWidth
|
dispatch({
|
||||||
})
|
type: 'SET_NAV_WIDTH',
|
||||||
})
|
navWidth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,8 +270,8 @@ class Main extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hideLeftLists (noteDetail, noteList, mainBody) {
|
hideLeftLists (noteDetail, noteList, mainBody) {
|
||||||
this.setState({noteDetailWidth: noteDetail.style.left})
|
this.setState({ noteDetailWidth: noteDetail.style.left })
|
||||||
this.setState({mainBodyWidth: mainBody.style.left})
|
this.setState({ mainBodyWidth: mainBody.style.left })
|
||||||
noteDetail.style.left = '0px'
|
noteDetail.style.left = '0px'
|
||||||
mainBody.style.left = '0px'
|
mainBody.style.left = '0px'
|
||||||
noteList.style.display = 'none'
|
noteList.style.display = 'none'
|
||||||
@@ -282,33 +293,36 @@ class Main extends React.Component {
|
|||||||
<div
|
<div
|
||||||
className='Main'
|
className='Main'
|
||||||
styleName='root'
|
styleName='root'
|
||||||
onMouseMove={(e) => this.handleMouseMove(e)}
|
onMouseMove={e => this.handleMouseMove(e)}
|
||||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
onMouseUp={e => this.handleMouseUp(e)}
|
||||||
>
|
>
|
||||||
<SideNav
|
<SideNav
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, ['dispatch', 'data', 'config', 'location'])}
|
||||||
'dispatch',
|
|
||||||
'data',
|
|
||||||
'config',
|
|
||||||
'location'
|
|
||||||
])}
|
|
||||||
width={this.state.navWidth}
|
width={this.state.navWidth}
|
||||||
/>
|
/>
|
||||||
{!config.isSideNavFolded &&
|
{!config.isSideNavFolded &&
|
||||||
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'}
|
<div
|
||||||
style={{left: this.state.navWidth}}
|
styleName={
|
||||||
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)}
|
this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
|
||||||
|
}
|
||||||
|
style={{ left: this.state.navWidth }}
|
||||||
|
onMouseDown={e => this.handleLeftSlideMouseDown(e)}
|
||||||
draggable='false'
|
draggable='false'
|
||||||
>
|
>
|
||||||
<div styleName='slider-hitbox' />
|
<div styleName='slider-hitbox' />
|
||||||
</div>
|
</div>}
|
||||||
}
|
<div
|
||||||
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||||
id='main-body'
|
id='main-body'
|
||||||
ref='body'
|
ref='body'
|
||||||
style={{left: config.isSideNavFolded ? foldedNavigationWidth : this.state.navWidth}}
|
style={{
|
||||||
|
left: config.isSideNavFolded
|
||||||
|
? foldedNavigationWidth
|
||||||
|
: this.state.navWidth
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<TopBar style={{width: this.state.listWidth}}
|
<TopBar
|
||||||
|
style={{ width: this.state.listWidth }}
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'config',
|
'config',
|
||||||
@@ -317,7 +331,8 @@ class Main extends React.Component {
|
|||||||
'location'
|
'location'
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
<NoteList style={{width: this.state.listWidth}}
|
<NoteList
|
||||||
|
style={{ width: this.state.listWidth }}
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
@@ -326,15 +341,20 @@ class Main extends React.Component {
|
|||||||
'location'
|
'location'
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
<div styleName={this.state.isRightSliderFocused ? 'slider-right--active' : 'slider-right'}
|
<div
|
||||||
style={{left: this.state.listWidth - 1}}
|
styleName={
|
||||||
onMouseDown={(e) => this.handleRightSlideMouseDown(e)}
|
this.state.isRightSliderFocused
|
||||||
|
? 'slider-right--active'
|
||||||
|
: 'slider-right'
|
||||||
|
}
|
||||||
|
style={{ left: this.state.listWidth - 1 }}
|
||||||
|
onMouseDown={e => this.handleRightSlideMouseDown(e)}
|
||||||
draggable='false'
|
draggable='false'
|
||||||
>
|
>
|
||||||
<div styleName='slider-hitbox' />
|
<div styleName='slider-hitbox' />
|
||||||
</div>
|
</div>
|
||||||
<Detail
|
<Detail
|
||||||
style={{left: this.state.listWidth}}
|
style={{ left: this.state.listWidth }}
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'data',
|
'data',
|
||||||
@@ -362,4 +382,4 @@ Main.propTypes = {
|
|||||||
data: PropTypes.shape({}).isRequired
|
data: PropTypes.shape({}).isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect((x) => x)(CSSModules(Main, styles))
|
export default connect(x => x)(CSSModules(Main, styles))
|
||||||
|
|||||||
@@ -74,4 +74,8 @@ body[data-theme="dark"]
|
|||||||
|
|
||||||
body[data-theme="solarized-dark"]
|
body[data-theme="solarized-dark"]
|
||||||
.root, .root--expanded
|
.root, .root--expanded
|
||||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.root, .root--expanded
|
||||||
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
@@ -56,9 +57,9 @@ class NewNoteButton extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -86,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 ? '⌘' : 'Ctrl'} + N
|
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -113,4 +113,28 @@ body[data-theme="solarized-dark"]
|
|||||||
.control-button--active
|
.control-button--active
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
&:active
|
&:active
|
||||||
color $ui-solarized-dark-text-color
|
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,11 +1,14 @@
|
|||||||
|
/* global electron */
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import debounceRender from 'react-debounce-render'
|
||||||
import styles from './NoteList.styl'
|
import styles from './NoteList.styl'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
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'
|
||||||
@@ -15,10 +18,13 @@ import path from 'path'
|
|||||||
import { hashHistory } from 'react-router'
|
import { hashHistory } from 'react-router'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||||
import markdown from '../../lib/markdown'
|
import Markdown from '../../lib/markdown'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
|
import context from 'browser/lib/context'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { Menu, MenuItem, dialog } = remote
|
const { dialog } = remote
|
||||||
const WP_POST_PATH = '/wp/v2/posts'
|
const WP_POST_PATH = '/wp/v2/posts'
|
||||||
|
|
||||||
function sortByCreatedAt (a, b) {
|
function sortByCreatedAt (a, b) {
|
||||||
@@ -279,8 +285,8 @@ class NoteList extends React.Component {
|
|||||||
ee.emit('detail:focus')
|
ee.emit('detail:focus')
|
||||||
}
|
}
|
||||||
|
|
||||||
// F or S key
|
// L or S key
|
||||||
if (e.keyCode === 70 || e.keyCode === 83) {
|
if (e.keyCode === 76 || e.keyCode === 83) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ee.emit('top:focus-search')
|
ee.emit('top:focus-search')
|
||||||
}
|
}
|
||||||
@@ -324,8 +330,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)
|
||||||
@@ -338,11 +346,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()
|
||||||
@@ -411,10 +418,10 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSortByChange (e) {
|
handleSortByChange (e) {
|
||||||
const { dispatch } = this.props
|
const { dispatch, params: { folderKey } } = this.props
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
sortBy: e.target.value
|
[folderKey]: { sortBy: e.target.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigManager.set(config)
|
ConfigManager.set(config)
|
||||||
@@ -443,20 +450,27 @@ 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 { selectedNoteKeys } = this.state
|
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 notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
const noteData = JSON.stringify(selectedNotes)
|
const noteData = JSON.stringify(selectedNotes)
|
||||||
e.dataTransfer.setData('note', noteData)
|
e.dataTransfer.setData('note', noteData)
|
||||||
this.setState({ selectedNoteKeys: [] })
|
this.selectNextNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNoteContextMenu (e, uniqueKey) {
|
handleNoteContextMenu (e, uniqueKey) {
|
||||||
@@ -469,61 +483,60 @@ class NoteList extends React.Component {
|
|||||||
this.handleNoteClick(e, uniqueKey)
|
this.handleNoteClick(e, uniqueKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top'
|
const pinLabel = note.isPinned ? i18n.__('Remove pin') : i18n.__('Pin to Top')
|
||||||
const deleteLabel = 'Delete Note'
|
const deleteLabel = i18n.__('Delete Note')
|
||||||
const cloneNote = 'Clone Note'
|
const cloneNote = i18n.__('Clone Note')
|
||||||
const restoreNote = 'Restore Note'
|
const restoreNote = i18n.__('Restore Note')
|
||||||
const copyNoteLink = 'Copy Note Link'
|
const copyNoteLink = i18n.__('Copy Note Link')
|
||||||
const publishLabel = 'Publish Blog'
|
const publishLabel = i18n.__('Publish Blog')
|
||||||
const updateLabel = 'Update Blog'
|
const updateLabel = i18n.__('Update Blog')
|
||||||
const openBlogLabel = 'Open Blog'
|
const openBlogLabel = i18n.__('Open Blog')
|
||||||
|
|
||||||
const menu = new Menu()
|
const templates = []
|
||||||
if (!location.pathname.match(/\/starred|\/trash/)) {
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: pinLabel,
|
|
||||||
click: this.pinToTop
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (location.pathname.match(/\/trash/)) {
|
if (location.pathname.match(/\/trash/)) {
|
||||||
menu.append(new MenuItem({
|
templates.push({
|
||||||
label: restoreNote,
|
label: restoreNote,
|
||||||
click: this.restoreNote
|
click: this.restoreNote
|
||||||
}))
|
}, {
|
||||||
}
|
label: deleteLabel,
|
||||||
|
click: this.deleteNote
|
||||||
menu.append(new MenuItem({
|
})
|
||||||
label: deleteLabel,
|
} else {
|
||||||
click: this.deleteNote
|
if (!location.pathname.match(/\/starred/)) {
|
||||||
}))
|
templates.push({
|
||||||
menu.append(new MenuItem({
|
label: pinLabel,
|
||||||
label: cloneNote,
|
click: this.pinToTop
|
||||||
click: this.cloneNote.bind(this)
|
})
|
||||||
}))
|
}
|
||||||
menu.append(new MenuItem({
|
templates.push({
|
||||||
label: copyNoteLink,
|
label: deleteLabel,
|
||||||
click: this.copyNoteLink(note)
|
click: this.deleteNote
|
||||||
}))
|
}, {
|
||||||
if (note.type === 'MARKDOWN_NOTE') {
|
label: cloneNote,
|
||||||
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
click: this.cloneNote.bind(this)
|
||||||
menu.append(new MenuItem({
|
}, {
|
||||||
label: updateLabel,
|
label: copyNoteLink,
|
||||||
click: this.publishMarkdown.bind(this)
|
click: this.copyNoteLink(note)
|
||||||
}))
|
})
|
||||||
menu.append(new MenuItem({
|
if (note.type === 'MARKDOWN_NOTE') {
|
||||||
label: openBlogLabel,
|
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||||
click: () => this.openBlog.bind(this)(note)
|
templates.push({
|
||||||
}))
|
label: updateLabel,
|
||||||
} else {
|
click: this.publishMarkdown.bind(this)
|
||||||
menu.append(new MenuItem({
|
}, {
|
||||||
label: publishLabel,
|
label: openBlogLabel,
|
||||||
click: this.publishMarkdown.bind(this)
|
click: () => this.openBlog.bind(this)(note)
|
||||||
}))
|
})
|
||||||
|
} else {
|
||||||
|
templates.push({
|
||||||
|
label: publishLabel,
|
||||||
|
click: this.publishMarkdown.bind(this)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
context.popup(templates)
|
||||||
menu.popup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedNotes (updateFunc, cleanSelection = true) {
|
updateSelectedNotes (updateFunc, cleanSelection = true) {
|
||||||
@@ -578,16 +591,11 @@ class NoteList extends React.Component {
|
|||||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||||
const firstNote = selectedNotes[0]
|
const firstNote = selectedNotes[0]
|
||||||
|
const { confirmDeletion } = this.props.config.ui
|
||||||
|
|
||||||
if (firstNote.isTrashed) {
|
if (firstNote.isTrashed) {
|
||||||
const noteExp = selectedNotes.length > 1 ? 'notes' : 'note'
|
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||||
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Confirm note deletion',
|
|
||||||
detail: `This will permanently remove ${selectedNotes.length} ${noteExp}.`,
|
|
||||||
buttons: ['Confirm', 'Cancel']
|
|
||||||
})
|
|
||||||
if (dialogueButtonIndex === 1) return
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
selectedNotes.map((note) => {
|
selectedNotes.map((note) => {
|
||||||
return dataApi
|
return dataApi
|
||||||
@@ -608,6 +616,8 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
console.log('Notes were all deleted')
|
console.log('Notes were all deleted')
|
||||||
} else {
|
} else {
|
||||||
|
if (!confirmDeleteNote(confirmDeletion, false)) return
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
selectedNotes.map((note) => {
|
selectedNotes.map((note) => {
|
||||||
note.isTrashed = true
|
note.isTrashed = true
|
||||||
@@ -648,9 +658,13 @@ class NoteList extends React.Component {
|
|||||||
.createNote(storage.key, {
|
.createNote(storage.key, {
|
||||||
type: firstNote.type,
|
type: firstNote.type,
|
||||||
folder: folder.key,
|
folder: folder.key,
|
||||||
title: firstNote.title + ' copy',
|
title: firstNote.title + ' ' + i18n.__('copy'),
|
||||||
content: firstNote.content
|
content: firstNote.content
|
||||||
})
|
})
|
||||||
|
.then((note) => {
|
||||||
|
attachmentManagement.cloneAttachments(firstNote, note)
|
||||||
|
return note
|
||||||
|
})
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
@@ -669,7 +683,7 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
copyNoteLink (note) {
|
copyNoteLink (note) {
|
||||||
const noteLink = `[${note.title}](${note.key})`
|
const noteLink = `[${note.title}](:note:${note.key})`
|
||||||
return copy(noteLink)
|
return copy(noteLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,7 +722,8 @@ class NoteList extends React.Component {
|
|||||||
authToken = `Bearer ${token}`
|
authToken = `Bearer ${token}`
|
||||||
}
|
}
|
||||||
const contentToRender = firstNote.content.replace(`# ${firstNote.title}`, '')
|
const contentToRender = firstNote.content.replace(`# ${firstNote.title}`, '')
|
||||||
var data = {
|
const markdown = new Markdown()
|
||||||
|
const data = {
|
||||||
title: firstNote.title,
|
title: firstNote.title,
|
||||||
content: markdown.render(contentToRender),
|
content: markdown.render(contentToRender),
|
||||||
status: 'publish'
|
status: 'publish'
|
||||||
@@ -754,9 +769,9 @@ class NoteList extends React.Component {
|
|||||||
const { dialog } = remote
|
const { dialog } = remote
|
||||||
const alertError = {
|
const alertError = {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Publish Failed',
|
message: i18n.__('Publish Failed'),
|
||||||
detail: 'Check and update your blog setting and try again.',
|
detail: i18n.__('Check and update your blog setting and try again.'),
|
||||||
buttons: ['Confirm']
|
buttons: [i18n.__('Confirm')]
|
||||||
}
|
}
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), alertError)
|
dialog.showMessageBox(remote.getCurrentWindow(), alertError)
|
||||||
}
|
}
|
||||||
@@ -764,9 +779,9 @@ class NoteList extends React.Component {
|
|||||||
confirmPublish (note) {
|
confirmPublish (note) {
|
||||||
const buttonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const buttonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Publish Succeeded',
|
message: i18n.__('Publish Succeeded'),
|
||||||
detail: `${note.title} is published at ${note.blog.blogLink}`,
|
detail: `${note.title} is published at ${note.blog.blogLink}`,
|
||||||
buttons: ['Confirm', 'Open Blog']
|
buttons: [i18n.__('Confirm'), i18n.__('Open Blog')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (buttonIndex === 1) {
|
if (buttonIndex === 1) {
|
||||||
@@ -871,7 +886,7 @@ 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')]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -894,12 +909,13 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { location, config } = this.props
|
const { location, config, params: { folderKey } } = this.props
|
||||||
let { notes } = this.props
|
let { notes } = this.props
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
const sortFunc = config.sortBy === 'CREATED_AT'
|
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
||||||
|
const sortFunc = sortBy === 'CREATED_AT'
|
||||||
? sortByCreatedAt
|
? sortByCreatedAt
|
||||||
: config.sortBy === 'ALPHABETICAL'
|
: sortBy === 'ALPHABETICAL'
|
||||||
? sortByAlphabetical
|
? sortByAlphabetical
|
||||||
: sortByUpdatedAt
|
: sortByUpdatedAt
|
||||||
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||||
@@ -910,7 +926,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',
|
||||||
@@ -931,17 +947,26 @@ class NoteList extends React.Component {
|
|||||||
|
|
||||||
const viewType = this.getViewType()
|
const viewType = this.getViewType()
|
||||||
|
|
||||||
|
const autoSelectFirst =
|
||||||
|
notes.length === 1 ||
|
||||||
|
selectedNoteKeys.length === 0 ||
|
||||||
|
notes.every(note => !selectedNoteKeys.includes(note.key))
|
||||||
|
|
||||||
const noteList = notes
|
const noteList = notes
|
||||||
.map(note => {
|
.map((note, index) => {
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDefault = config.listStyle === 'DEFAULT'
|
const isDefault = config.listStyle === 'DEFAULT'
|
||||||
const uniqueKey = getNoteKey(note)
|
const uniqueKey = getNoteKey(note)
|
||||||
const isActive = selectedNoteKeys.includes(uniqueKey)
|
|
||||||
|
const isActive =
|
||||||
|
selectedNoteKeys.includes(uniqueKey) ||
|
||||||
|
notes.length === 1 ||
|
||||||
|
(autoSelectFirst && index === 0)
|
||||||
const dateDisplay = moment(
|
const dateDisplay = moment(
|
||||||
config.sortBy === 'CREATED_AT'
|
sortBy === 'CREATED_AT'
|
||||||
? note.createdAt : note.updatedAt
|
? note.createdAt : note.updatedAt
|
||||||
).fromNow('D')
|
).fromNow('D')
|
||||||
|
|
||||||
@@ -989,17 +1014,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='Select filter mode'
|
title={i18n.__('Select filter mode')}
|
||||||
value={config.sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => this.handleSortByChange(e)}
|
onChange={(e) => this.handleSortByChange(e)}
|
||||||
>
|
>
|
||||||
<option title='Sort by update time' value='UPDATED_AT'>Updated</option>
|
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
|
||||||
<option title='Sort by create time' value='CREATED_AT'>Created</option>
|
<option title='Sort by create time' value='CREATED_AT'>{i18n.__('Created')}</option>
|
||||||
<option title='Sort alphabetically' 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 title='Default View' styleName={config.listStyle === 'DEFAULT'
|
<button title={i18n.__('Default View')} styleName={config.listStyle === 'DEFAULT'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
: 'control-button'
|
: 'control-button'
|
||||||
}
|
}
|
||||||
@@ -1007,7 +1032,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 title='Compressed View' styleName={config.listStyle === 'SMALL'
|
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
|
||||||
? 'control-button--active'
|
? 'control-button--active'
|
||||||
: 'control-button'
|
: 'control-button'
|
||||||
}
|
}
|
||||||
@@ -1041,4 +1066,4 @@ NoteList.propTypes = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(NoteList, styles)
|
export default debounceRender(CSSModules(NoteList, styles))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './SwitchButton.styl'
|
import styles from './SwitchButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const ListButton = ({
|
const ListButton = ({
|
||||||
onClick, isTagActive
|
onClick, isTagActive
|
||||||
@@ -12,7 +13,7 @@ const ListButton = ({
|
|||||||
: '../resources/icon/icon-list-active.svg'
|
: '../resources/icon/icon-list-active.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span styleName='tooltip'>Notes</span>
|
<span styleName='tooltip'>{i18n.__('Notes')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './PreferenceButton.styl'
|
import styles from './PreferenceButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const PreferenceButton = ({
|
const PreferenceButton = ({
|
||||||
onClick
|
onClick
|
||||||
}) => (
|
}) => (
|
||||||
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
|
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
|
||||||
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
|
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
|
||||||
<span styleName='tooltip'>Preferences</span>
|
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -48,4 +48,5 @@ body[data-theme="dark"]
|
|||||||
line-height normal
|
line-height normal
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
white-space nowrap
|
||||||
|
|||||||
@@ -30,11 +30,33 @@
|
|||||||
display flex
|
display flex
|
||||||
flex-direction column
|
flex-direction column
|
||||||
|
|
||||||
.tag-title
|
.tag-control
|
||||||
padding-left 15px
|
display flex
|
||||||
padding-bottom 13px
|
height 30px
|
||||||
p
|
line-height 25px
|
||||||
color $ui-button-default-color
|
overflow hidden
|
||||||
|
.tag-control-title
|
||||||
|
padding-left 15px
|
||||||
|
padding-bottom 13px
|
||||||
|
flex 1
|
||||||
|
p
|
||||||
|
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
|
||||||
@@ -95,3 +117,8 @@ body[data-theme="solarized-dark"]
|
|||||||
.root, .root--folded
|
.root, .root--folded
|
||||||
background-color $ui-solarized-dark-backgroundColor
|
background-color $ui-solarized-dark-backgroundColor
|
||||||
border-right 1px solid $ui-solarized-dark-borderColor
|
border-right 1px solid $ui-solarized-dark-borderColor
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.root, .root--folded
|
||||||
|
background-color $ui-monokai-backgroundColor
|
||||||
|
border-right 1px solid $ui-monokai-borderColor
|
||||||
|
|||||||
@@ -10,43 +10,47 @@ import dataApi from 'browser/main/lib/dataApi'
|
|||||||
import StorageItemChild from 'browser/components/StorageItem'
|
import StorageItemChild from 'browser/components/StorageItem'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { SortableElement } from 'react-sortable-hoc'
|
import { SortableElement } from 'react-sortable-hoc'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import context from 'browser/lib/context'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { Menu, dialog } = remote
|
const { 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) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
const { storage } = this.props
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isOpen: true
|
isOpen: !!storage.isOpen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHeaderContextMenu (e) {
|
handleHeaderContextMenu (e) {
|
||||||
const menu = Menu.buildFromTemplate([
|
context.popup([
|
||||||
{
|
{
|
||||||
label: 'Add Folder',
|
label: i18n.__('Add Folder'),
|
||||||
click: (e) => this.handleAddFolderButtonClick(e)
|
click: (e) => this.handleAddFolderButtonClick(e)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Unlink Storage',
|
label: i18n.__('Unlink Storage'),
|
||||||
click: (e) => this.handleUnlinkStorageClick(e)
|
click: (e) => this.handleUnlinkStorageClick(e)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
menu.popup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnlinkStorageClick (e) {
|
handleUnlinkStorageClick (e) {
|
||||||
const 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) {
|
||||||
@@ -65,8 +69,18 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleToggleButtonClick (e) {
|
handleToggleButtonClick (e) {
|
||||||
|
const { storage, dispatch } = this.props
|
||||||
|
const isOpen = !this.state.isOpen
|
||||||
|
dataApi.toggleStorage(storage.key, isOpen)
|
||||||
|
.then((storage) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'EXPAND_STORAGE',
|
||||||
|
storage,
|
||||||
|
isOpen
|
||||||
|
})
|
||||||
|
})
|
||||||
this.setState({
|
this.setState({
|
||||||
isOpen: !this.state.isOpen
|
isOpen: isOpen
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,23 +105,23 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFolderButtonContextMenu (e, folder) {
|
handleFolderButtonContextMenu (e, folder) {
|
||||||
const menu = Menu.buildFromTemplate([
|
context.popup([
|
||||||
{
|
{
|
||||||
label: 'Rename Folder',
|
label: i18n.__('Rename Folder'),
|
||||||
click: (e) => this.handleRenameFolderClick(e, folder)
|
click: (e) => this.handleRenameFolderClick(e, folder)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Export Folder',
|
label: i18n.__('Export Folder'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Export as txt',
|
label: i18n.__('Export as txt'),
|
||||||
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
|
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Export as md',
|
label: i18n.__('Export as md'),
|
||||||
click: (e) => this.handleExportFolderClick(e, folder, 'md')
|
click: (e) => this.handleExportFolderClick(e, folder, 'md')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -116,12 +130,10 @@ class StorageItem extends React.Component {
|
|||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Delete Folder',
|
label: i18n.__('Delete Folder'),
|
||||||
click: (e) => this.handleFolderDeleteClick(e, folder)
|
click: (e) => this.handleFolderDeleteClick(e, folder)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
menu.popup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRenameFolderClick (e, folder) {
|
handleRenameFolderClick (e, folder) {
|
||||||
@@ -135,8 +147,8 @@ class StorageItem extends React.Component {
|
|||||||
handleExportFolderClick (e, folder, fileType) {
|
handleExportFolderClick (e, folder, fileType) {
|
||||||
const options = {
|
const options = {
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
buttonLabel: 'Select directory',
|
buttonLabel: i18n.__('Select directory'),
|
||||||
title: 'Select a folder to export the files to',
|
title: i18n.__('Select a folder to export the files to'),
|
||||||
multiSelections: false
|
multiSelections: false
|
||||||
}
|
}
|
||||||
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
||||||
@@ -160,9 +172,9 @@ class StorageItem extends React.Component {
|
|||||||
handleFolderDeleteClick (e, folder) {
|
handleFolderDeleteClick (e, folder) {
|
||||||
const 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) {
|
||||||
@@ -200,7 +212,7 @@ class StorageItem extends React.Component {
|
|||||||
createdNoteData.forEach((newNote) => {
|
createdNoteData.forEach((newNote) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'MOVE_NOTE',
|
type: 'MOVE_NOTE',
|
||||||
originNote: noteData.find((note) => note.content === newNote.content),
|
originNote: noteData.find((note) => note.content === newNote.oldContent),
|
||||||
note: newNote
|
note: newNote
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -222,7 +234,8 @@ class StorageItem extends React.Component {
|
|||||||
const { folderNoteMap, trashedSet } = data
|
const { folderNoteMap, trashedSet } = data
|
||||||
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||||
const folderList = storage.folders.map((folder, index) => {
|
const folderList = storage.folders.map((folder, index) => {
|
||||||
const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + 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)
|
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||||
|
|
||||||
let noteCount = 0
|
let noteCount = 0
|
||||||
@@ -252,7 +265,7 @@ class StorageItem extends React.Component {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const 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'}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
height 36px
|
height 36px
|
||||||
padding-left 25px
|
padding-left 25px
|
||||||
padding-right 15px
|
padding-right 15px
|
||||||
line-height 22px
|
line-height 36px
|
||||||
cursor pointer
|
cursor pointer
|
||||||
font-size 14px
|
font-size 14px
|
||||||
border none
|
border none
|
||||||
@@ -147,7 +147,7 @@ body[data-theme="dark"]
|
|||||||
background-color $ui-dark-button--active-backgroundColor
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
&:active
|
&:active
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
|
||||||
.header--active
|
.header--active
|
||||||
.header-addFolderButton
|
.header-addFolderButton
|
||||||
@@ -180,7 +180,7 @@ body[data-theme="dark"]
|
|||||||
&:active, &:active:hover
|
&:active, &:active:hover
|
||||||
color $ui-dark-text-color
|
color $ui-dark-text-color
|
||||||
background-color $ui-dark-button--active-backgroundColor
|
background-color $ui-dark-button--active-backgroundColor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
border-radius 2px
|
border-radius 2px
|
||||||
opacity 0
|
opacity 0
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
white-space nowrap
|
||||||
|
|
||||||
body[data-theme="white"]
|
body[data-theme="white"]
|
||||||
.non-active-button
|
.non-active-button
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './SwitchButton.styl'
|
import styles from './SwitchButton.styl'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const TagButton = ({
|
const TagButton = ({
|
||||||
onClick, isTagActive
|
onClick, isTagActive
|
||||||
@@ -12,7 +13,7 @@ const TagButton = ({
|
|||||||
: '../resources/icon/icon-tag.svg'
|
: '../resources/icon/icon-tag.svg'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span styleName='tooltip'>Tags</span>
|
<span styleName='tooltip'>{i18n.__('Tags')}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
const { remote } = require('electron')
|
|
||||||
const { Menu } = remote
|
|
||||||
import dataApi from 'browser/main/lib/dataApi'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import styles from './SideNav.styl'
|
import styles from './SideNav.styl'
|
||||||
import { openModal } from 'browser/main/lib/modal'
|
import { openModal } from 'browser/main/lib/modal'
|
||||||
@@ -18,6 +16,8 @@ import PreferenceButton from './PreferenceButton'
|
|||||||
import ListButton from './ListButton'
|
import ListButton from './ListButton'
|
||||||
import TagButton from './TagButton'
|
import TagButton from './TagButton'
|
||||||
import {SortableContainer} from 'react-sortable-hoc'
|
import {SortableContainer} from 'react-sortable-hoc'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import context from 'browser/lib/context'
|
||||||
|
|
||||||
class SideNav extends React.Component {
|
class SideNav extends React.Component {
|
||||||
// TODO: should not use electron stuff v0.7
|
// TODO: should not use electron stuff v0.7
|
||||||
@@ -81,7 +81,7 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SideNavComponent (isFolded, storageList) {
|
SideNavComponent (isFolded, storageList) {
|
||||||
const { location, data } = this.props
|
const { location, data, config } = this.props
|
||||||
|
|
||||||
const isHomeActive = !!location.pathname.match(/^\/home$/)
|
const isHomeActive = !!location.pathname.match(/^\/home$/)
|
||||||
const isStarredActive = !!location.pathname.match(/^\/starred$/)
|
const isStarredActive = !!location.pathname.match(/^\/starred$/)
|
||||||
@@ -107,15 +107,30 @@ class SideNav extends React.Component {
|
|||||||
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
|
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StorageList storageList={storageList} />
|
<StorageList storageList={storageList} isFolded={isFolded} />
|
||||||
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
|
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
component = (
|
component = (
|
||||||
<div styleName='tabBody'>
|
<div styleName='tabBody'>
|
||||||
<div styleName='tag-title'>
|
<div styleName='tag-control'>
|
||||||
<p>Tags</p>
|
<div styleName='tag-control-title'>
|
||||||
|
<p>{i18n.__('Tags')}</p>
|
||||||
|
</div>
|
||||||
|
<div styleName='tag-control-sortTagsBy'>
|
||||||
|
<i className='fa fa-angle-down' />
|
||||||
|
<select styleName='tag-control-sortTagsBy-select'
|
||||||
|
title={i18n.__('Select filter mode')}
|
||||||
|
value={config.sortTagsBy}
|
||||||
|
onChange={(e) => this.handleSortTagsByChange(e)}
|
||||||
|
>
|
||||||
|
<option title='Sort alphabetically'
|
||||||
|
value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
|
||||||
|
<option title='Sort by update time'
|
||||||
|
value='COUNTER'>{i18n.__('Counter')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='tagList'>
|
<div styleName='tagList'>
|
||||||
{this.tagListComponent(data)}
|
{this.tagListComponent(data)}
|
||||||
@@ -128,17 +143,30 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tagListComponent () {
|
tagListComponent () {
|
||||||
const { data, location } = this.props
|
const { data, location, config } = this.props
|
||||||
const tagList = _.sortBy(data.tagNoteMap.map((tag, name) => {
|
const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
|
||||||
return { name, size: tag.size }
|
let tagList = _.sortBy(data.tagNoteMap.map(
|
||||||
}), ['name'])
|
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
||||||
|
), ['name']).filter(
|
||||||
|
tag => tag.size > 0
|
||||||
|
)
|
||||||
|
if (config.sortTagsBy === 'COUNTER') {
|
||||||
|
tagList = _.sortBy(tagList, item => (0 - item.size))
|
||||||
|
}
|
||||||
|
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
|
||||||
|
tagList = tagList.filter(
|
||||||
|
tag => tag.related
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
tagList.map(tag => {
|
tagList.map(tag => {
|
||||||
return (
|
return (
|
||||||
<TagListItem
|
<TagListItem
|
||||||
name={tag.name}
|
name={tag.name}
|
||||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||||
isActive={this.getTagActive(location.pathname, tag)}
|
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
||||||
|
isActive={this.getTagActive(location.pathname, tag.name)}
|
||||||
|
isRelated={tag.related}
|
||||||
key={tag.name}
|
key={tag.name}
|
||||||
count={tag.size}
|
count={tag.size}
|
||||||
/>
|
/>
|
||||||
@@ -147,10 +175,30 @@ class SideNav extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRelatedTags (activeTags, noteMap) {
|
||||||
|
if (activeTags.length === 0) {
|
||||||
|
return new Set()
|
||||||
|
}
|
||||||
|
const relatedNotes = noteMap.map(
|
||||||
|
note => ({key: note.key, tags: note.tags})
|
||||||
|
).filter(
|
||||||
|
note => activeTags.every(tag => note.tags.includes(tag))
|
||||||
|
)
|
||||||
|
const relatedTags = new Set()
|
||||||
|
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
|
||||||
|
return relatedTags
|
||||||
|
}
|
||||||
|
|
||||||
getTagActive (path, tag) {
|
getTagActive (path, tag) {
|
||||||
|
return this.getActiveTags(path).includes(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveTags (path) {
|
||||||
const pathSegments = path.split('/')
|
const pathSegments = path.split('/')
|
||||||
const pathTag = pathSegments[pathSegments.length - 1]
|
const tags = pathSegments[pathSegments.length - 1]
|
||||||
return pathTag === tag
|
return (tags === 'alltags')
|
||||||
|
? []
|
||||||
|
: tags.split(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickTagListItem (name) {
|
handleClickTagListItem (name) {
|
||||||
@@ -158,6 +206,33 @@ class SideNav extends React.Component {
|
|||||||
router.push(`/tags/${name}`)
|
router.push(`/tags/${name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSortTagsByChange (e) {
|
||||||
|
const { dispatch } = this.props
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
sortTagsBy: e.target.value
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigManager.set(config)
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_CONFIG',
|
||||||
|
config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickNarrowToTag (tag) {
|
||||||
|
const { router } = this.context
|
||||||
|
const { location } = this.props
|
||||||
|
const listOfTags = this.getActiveTags(location.pathname)
|
||||||
|
const indexOfTag = listOfTags.indexOf(tag)
|
||||||
|
if (indexOfTag > -1) {
|
||||||
|
listOfTags.splice(indexOfTag, 1)
|
||||||
|
} else {
|
||||||
|
listOfTags.push(tag)
|
||||||
|
}
|
||||||
|
router.push(`/tags/${listOfTags.join(' ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
emptyTrash (entries) {
|
emptyTrash (entries) {
|
||||||
const { dispatch } = this.props
|
const { dispatch } = this.props
|
||||||
const deletionPromises = entries.map((note) => {
|
const deletionPromises = entries.map((note) => {
|
||||||
@@ -178,10 +253,9 @@ class SideNav extends React.Component {
|
|||||||
handleFilterButtonContextMenu (event) {
|
handleFilterButtonContextMenu (event) {
|
||||||
const { data } = this.props
|
const { data } = this.props
|
||||||
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||||
const menu = Menu.buildFromTemplate([
|
context.popup([
|
||||||
{ label: 'Empty Trash', click: () => this.emptyTrash(trashedNotes) }
|
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
|
||||||
])
|
])
|
||||||
menu.popup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|||||||
@@ -69,3 +69,14 @@ body[data-theme="dark"]
|
|||||||
navDarkButtonColor()
|
navDarkButtonColor()
|
||||||
border-color $ui-dark-borderColor
|
border-color $ui-dark-borderColor
|
||||||
border-left 1px solid $ui-dark-borderColor
|
border-left 1px solid $ui-dark-borderColor
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
navButtonColor()
|
||||||
|
.zoom
|
||||||
|
border-color $ui-dark-borderColor
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
&:hover
|
||||||
|
transition 0.15s
|
||||||
|
color $ui-monokai-active-color
|
||||||
|
&:active
|
||||||
|
color $ui-monokai-active-color
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import React from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './StatusBar.styl'
|
import styles from './StatusBar.styl'
|
||||||
import ZoomManager from 'browser/main/lib/ZoomManager'
|
import ZoomManager from 'browser/main/lib/ZoomManager'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import context from 'browser/lib/context'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote, ipcRenderer } = electron
|
const { remote, ipcRenderer } = electron
|
||||||
const { Menu, MenuItem, dialog } = remote
|
const { dialog } = remote
|
||||||
|
|
||||||
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
|
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
|
||||||
|
|
||||||
@@ -14,9 +16,9 @@ class StatusBar extends React.Component {
|
|||||||
updateApp () {
|
updateApp () {
|
||||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Update Boostnote',
|
message: i18n.__('Update Boostnote'),
|
||||||
detail: 'New Boostnote is ready to be installed.',
|
detail: i18n.__('New Boostnote is ready to be installed.'),
|
||||||
buttons: ['Restart & Install', 'Not Now']
|
buttons: [i18n.__('Restart & Install'), i18n.__('Not Now')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
@@ -25,16 +27,16 @@ class StatusBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleZoomButtonClick (e) {
|
handleZoomButtonClick (e) {
|
||||||
const menu = new Menu()
|
const templates = []
|
||||||
|
|
||||||
zoomOptions.forEach((zoom) => {
|
zoomOptions.forEach((zoom) => {
|
||||||
menu.append(new MenuItem({
|
templates.push({
|
||||||
label: Math.floor(zoom * 100) + '%',
|
label: Math.floor(zoom * 100) + '%',
|
||||||
click: () => this.handleZoomMenuItemClick(zoom)
|
click: () => this.handleZoomMenuItemClick(zoom)
|
||||||
}))
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
menu.popup(remote.getCurrentWindow())
|
context.popup(templates)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleZoomMenuItemClick (zoomFactor) {
|
handleZoomMenuItemClick (zoomFactor) {
|
||||||
@@ -62,7 +64,7 @@ class StatusBar extends React.Component {
|
|||||||
|
|
||||||
{status.updateReady
|
{status.updateReady
|
||||||
? <button onClick={this.updateApp} styleName='update'>
|
? <button onClick={this.updateApp} styleName='update'>
|
||||||
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update!
|
<i styleName='update-icon' className='fa fa-cloud-download' /> {i18n.__('Ready to Update!')}
|
||||||
</button>
|
</button>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,3 +234,25 @@ body[data-theme="solarized-dark"]
|
|||||||
input
|
input
|
||||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||||
color $ui-solarized-dark-text-color
|
color $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
.root, .root--expanded
|
||||||
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control
|
||||||
|
border-color $ui-monokai-borderColor
|
||||||
|
.control-search
|
||||||
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control-search-icon
|
||||||
|
absolute top bottom left
|
||||||
|
line-height 32px
|
||||||
|
width 35px
|
||||||
|
color $ui-monokai-inactive-text-color
|
||||||
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
|
|
||||||
|
.control-search-input
|
||||||
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
|
input
|
||||||
|
background-color $ui-monokai-noteList-backgroundColor
|
||||||
|
color $ui-monokai-text-color
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import styles from './TopBar.styl'
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ee from 'browser/main/lib/eventEmitter'
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
import NewNoteButton from 'browser/main/NewNoteButton'
|
import NewNoteButton from 'browser/main/NewNoteButton'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
class TopBar extends React.Component {
|
class TopBar extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -27,6 +28,14 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
const { params } = this.props
|
||||||
|
const searchWord = params.searchword
|
||||||
|
if (searchWord !== undefined) {
|
||||||
|
this.setState({
|
||||||
|
search: searchWord,
|
||||||
|
isSearching: true
|
||||||
|
})
|
||||||
|
}
|
||||||
ee.on('top:focus-search', this.focusSearchHandler)
|
ee.on('top:focus-search', this.focusSearchHandler)
|
||||||
ee.on('code:init', this.codeInitHandler)
|
ee.on('code:init', this.codeInitHandler)
|
||||||
}
|
}
|
||||||
@@ -96,9 +105,10 @@ class TopBar extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
isConfirmTranslation: true
|
isConfirmTranslation: true
|
||||||
})
|
})
|
||||||
router.push('/searched')
|
const keyword = this.refs.searchInput.value
|
||||||
|
router.push(`/searched/${encodeURIComponent(keyword)}`)
|
||||||
this.setState({
|
this.setState({
|
||||||
search: this.refs.searchInput.value
|
search: keyword
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +117,7 @@ class TopBar extends React.Component {
|
|||||||
const { router } = this.context
|
const { router } = this.context
|
||||||
const keyword = this.refs.searchInput.value
|
const keyword = this.refs.searchInput.value
|
||||||
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
|
||||||
router.push('/searched')
|
router.push(`/searched/${encodeURIComponent(keyword)}`)
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
@@ -146,8 +156,7 @@ class TopBar extends React.Component {
|
|||||||
if (this.state.isSearching) {
|
if (this.state.isSearching) {
|
||||||
el.blur()
|
el.blur()
|
||||||
} else {
|
} else {
|
||||||
el.focus()
|
el.select()
|
||||||
el.setSelectionRange(0, el.value.length)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +185,7 @@ class TopBar extends React.Component {
|
|||||||
onChange={(e) => this.handleSearchChange(e)}
|
onChange={(e) => this.handleSearchChange(e)}
|
||||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||||
onKeyUp={(e) => this.handleKeyUp(e)}
|
onKeyUp={(e) => this.handleKeyUp(e)}
|
||||||
placeholder='Search'
|
placeholder={i18n.__('Search')}
|
||||||
type='text'
|
type='text'
|
||||||
className='searchInput'
|
className='searchInput'
|
||||||
/>
|
/>
|
||||||
@@ -185,7 +194,7 @@ class TopBar extends React.Component {
|
|||||||
onClick={(e) => this.handleSearchClearButton(e)}
|
onClick={(e) => this.handleSearchClearButton(e)}
|
||||||
>
|
>
|
||||||
<i className='fa fa-fw fa-times' />
|
<i className='fa fa-fw fa-times' />
|
||||||
<span styleName='control-search-input-clear-tooltip'>Clear Search</span>
|
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ body
|
|||||||
font-weight 200
|
font-weight 200
|
||||||
-webkit-font-smoothing antialiased
|
-webkit-font-smoothing antialiased
|
||||||
|
|
||||||
|
::-webkit-scrollbar
|
||||||
|
width 12px
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb
|
||||||
|
background-color rgba(0, 0, 0, 0.15)
|
||||||
|
|
||||||
button, input, select, textarea
|
button, input, select, textarea
|
||||||
font-family DEFAULT_FONTS
|
font-family DEFAULT_FONTS
|
||||||
|
|
||||||
@@ -85,9 +91,11 @@ modalBackColor = white
|
|||||||
absolute top left bottom right
|
absolute top left bottom right
|
||||||
background-color modalBackColor
|
background-color modalBackColor
|
||||||
z-index modalZIndex + 1
|
z-index modalZIndex + 1
|
||||||
|
|
||||||
|
|
||||||
body[data-theme="dark"]
|
body[data-theme="dark"]
|
||||||
|
::-webkit-scrollbar-thumb
|
||||||
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
.ModalBase
|
.ModalBase
|
||||||
.modalBack
|
.modalBack
|
||||||
background-color $ui-dark-backgroundColor
|
background-color $ui-dark-backgroundColor
|
||||||
@@ -108,15 +116,43 @@ body[data-theme="dark"]
|
|||||||
background #B1D7FE
|
background #B1D7FE
|
||||||
::selection
|
::selection
|
||||||
background #B1D7FE
|
background #B1D7FE
|
||||||
|
.CodeMirror-foldmarker
|
||||||
|
font-family: arial
|
||||||
|
|
||||||
|
.CodeMirror-foldgutter
|
||||||
|
width: .7em
|
||||||
|
|
||||||
|
.CodeMirror-foldgutter-open,
|
||||||
|
.CodeMirror-foldgutter-folded
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
.CodeMirror-foldgutter-open:after
|
||||||
|
content: "\25BE"
|
||||||
|
|
||||||
|
.CodeMirror-foldgutter-folded:after
|
||||||
|
content: "\25B8"
|
||||||
|
|
||||||
.sortableItemHelper
|
.sortableItemHelper
|
||||||
z-index modalZIndex + 5
|
z-index modalZIndex + 5
|
||||||
|
|
||||||
body[data-theme="solarized-dark"]
|
body[data-theme="solarized-dark"]
|
||||||
|
::-webkit-scrollbar-thumb
|
||||||
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
.ModalBase
|
.ModalBase
|
||||||
.modalBack
|
.modalBack
|
||||||
background-color $ui-solarized-dark-backgroundColor
|
background-color $ui-solarized-dark-backgroundColor
|
||||||
.sortableItemHelper
|
.sortableItemHelper
|
||||||
color: $ui-solarized-dark-text-color
|
color: $ui-solarized-dark-text-color
|
||||||
|
|
||||||
|
body[data-theme="monokai"]
|
||||||
|
::-webkit-scrollbar-thumb
|
||||||
|
background-color rgba(0, 0, 0, 0.3)
|
||||||
|
.ModalBase
|
||||||
|
.modalBack
|
||||||
|
background-color $ui-monokai-backgroundColor
|
||||||
|
.sortableItemHelper
|
||||||
|
color: $ui-monokai-text-color
|
||||||
|
|
||||||
|
body[data-theme="default"]
|
||||||
|
.SideNav ::-webkit-scrollbar-thumb
|
||||||
|
background-color rgba(255, 255, 255, 0.3)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-rou
|
|||||||
import { syncHistoryWithStore } from 'react-router-redux'
|
import { syncHistoryWithStore } from 'react-router-redux'
|
||||||
require('./lib/ipcClient')
|
require('./lib/ipcClient')
|
||||||
require('../lib/customMeta')
|
require('../lib/customMeta')
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
@@ -23,6 +24,45 @@ document.addEventListener('dragover', function (e) {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// prevent menu from popup when alt pressed
|
||||||
|
// but still able to toggle menu when only alt is pressed
|
||||||
|
let isAltPressing = false
|
||||||
|
let isAltWithMouse = false
|
||||||
|
let isAltWithOtherKey = false
|
||||||
|
let isOtherKey = false
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Alt') {
|
||||||
|
isAltPressing = true
|
||||||
|
if (isOtherKey) {
|
||||||
|
isAltWithOtherKey = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isAltPressing) {
|
||||||
|
isAltWithOtherKey = true
|
||||||
|
}
|
||||||
|
isOtherKey = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', function (e) {
|
||||||
|
if (isAltPressing) {
|
||||||
|
isAltWithMouse = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('keyup', function (e) {
|
||||||
|
if (e.key === 'Alt') {
|
||||||
|
if (isAltWithMouse || isAltWithOtherKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
isAltWithMouse = false
|
||||||
|
isAltWithOtherKey = false
|
||||||
|
isAltPressing = false
|
||||||
|
isOtherKey = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
const className = e.target.className
|
const className = e.target.className
|
||||||
if (!className && typeof (className) !== 'string') return
|
if (!className && typeof (className) !== 'string') return
|
||||||
@@ -46,9 +86,9 @@ function notify (...args) {
|
|||||||
function updateApp () {
|
function updateApp () {
|
||||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Update Boostnote',
|
message: i18n.__('Update Boostnote'),
|
||||||
detail: 'New Boostnote is ready to be installed.',
|
detail: i18n.__('New Boostnote is ready to be installed.'),
|
||||||
buttons: ['Restart & Install', 'Not Now']
|
buttons: [i18n.__('Restart & Install'), i18n.__('Not Now')]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
@@ -63,7 +103,9 @@ ReactDOM.render((
|
|||||||
<IndexRedirect to='/home' />
|
<IndexRedirect to='/home' />
|
||||||
<Route path='home' />
|
<Route path='home' />
|
||||||
<Route path='starred' />
|
<Route path='starred' />
|
||||||
<Route path='searched' />
|
<Route path='searched'>
|
||||||
|
<Route path=':searchword' />
|
||||||
|
</Route>
|
||||||
<Route path='trashed' />
|
<Route path='trashed' />
|
||||||
<Route path='alltags' />
|
<Route path='alltags' />
|
||||||
<Route path='tags'>
|
<Route path='tags'>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import RcParser from 'browser/lib/RcParser'
|
import RcParser from 'browser/lib/RcParser'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import ee from 'browser/main/lib/eventEmitter'
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
const win = global.process.platform === 'win32'
|
const win = global.process.platform === 'win32'
|
||||||
@@ -14,13 +16,18 @@ export const DEFAULT_CONFIG = {
|
|||||||
isSideNavFolded: false,
|
isSideNavFolded: false,
|
||||||
listWidth: 280,
|
listWidth: 280,
|
||||||
navWidth: 200,
|
navWidth: 200,
|
||||||
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
sortBy: {
|
||||||
|
default: 'UPDATED_AT' // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
||||||
|
},
|
||||||
|
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
|
||||||
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
||||||
amaEnabled: true,
|
amaEnabled: true,
|
||||||
hotkey: {
|
hotkey: {
|
||||||
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
|
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
||||||
|
toggleMode: OSX ? 'Command + M' : 'Ctrl + M'
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
|
language: 'en',
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
showCopyNotification: true,
|
showCopyNotification: true,
|
||||||
disableDirectWrite: false,
|
disableDirectWrite: false,
|
||||||
@@ -33,6 +40,8 @@ export const DEFAULT_CONFIG = {
|
|||||||
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
|
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
|
||||||
indentType: 'space',
|
indentType: 'space',
|
||||||
indentSize: '2',
|
indentSize: '2',
|
||||||
|
enableRulers: false,
|
||||||
|
rulers: [80, 120],
|
||||||
displayLineNumbers: true,
|
displayLineNumbers: true,
|
||||||
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
|
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
|
||||||
scrollPastEnd: false,
|
scrollPastEnd: false,
|
||||||
@@ -48,8 +57,14 @@ export const DEFAULT_CONFIG = {
|
|||||||
latexInlineClose: '$',
|
latexInlineClose: '$',
|
||||||
latexBlockOpen: '$$',
|
latexBlockOpen: '$$',
|
||||||
latexBlockClose: '$$',
|
latexBlockClose: '$$',
|
||||||
|
plantUMLServerAddress: 'http://www.plantuml.com/plantuml',
|
||||||
scrollPastEnd: false,
|
scrollPastEnd: false,
|
||||||
smartQuotes: true
|
smartQuotes: true,
|
||||||
|
breaks: true,
|
||||||
|
smartArrows: false,
|
||||||
|
allowCustomCSS: false,
|
||||||
|
customCSS: '',
|
||||||
|
sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
type: 'wordpress', // Available value: wordpress, add more types in the future plz
|
type: 'wordpress', // Available value: wordpress, add more types in the future plz
|
||||||
@@ -129,10 +144,14 @@ function set (updates) {
|
|||||||
document.body.setAttribute('data-theme', 'white')
|
document.body.setAttribute('data-theme', 'white')
|
||||||
} else if (newConfig.ui.theme === 'solarized-dark') {
|
} else if (newConfig.ui.theme === 'solarized-dark') {
|
||||||
document.body.setAttribute('data-theme', 'solarized-dark')
|
document.body.setAttribute('data-theme', 'solarized-dark')
|
||||||
|
} else if (newConfig.ui.theme === 'monokai') {
|
||||||
|
document.body.setAttribute('data-theme', 'monokai')
|
||||||
} else {
|
} else {
|
||||||
document.body.setAttribute('data-theme', 'default')
|
document.body.setAttribute('data-theme', 'default')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i18n.setLocale(newConfig.ui.language)
|
||||||
|
|
||||||
let editorTheme = document.getElementById('editorTheme')
|
let editorTheme = document.getElementById('editorTheme')
|
||||||
if (editorTheme == null) {
|
if (editorTheme == null) {
|
||||||
editorTheme = document.createElement('link')
|
editorTheme = document.createElement('link')
|
||||||
@@ -155,6 +174,7 @@ function set (updates) {
|
|||||||
ipcRenderer.send('config-renew', {
|
ipcRenderer.send('config-renew', {
|
||||||
config: get()
|
config: get()
|
||||||
})
|
})
|
||||||
|
ee.emit('config-renew')
|
||||||
}
|
}
|
||||||
|
|
||||||
function assignConfigValues (originalConfig, rcConfig) {
|
function assignConfigValues (originalConfig, rcConfig) {
|
||||||
@@ -164,6 +184,17 @@ function assignConfigValues (originalConfig, rcConfig) {
|
|||||||
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
|
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
|
||||||
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
|
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
|
||||||
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
|
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
|
||||||
|
|
||||||
|
rewriteHotkey(config)
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteHotkey (config) {
|
||||||
|
const keys = [...Object.keys(config.hotkey)]
|
||||||
|
keys.forEach(key => {
|
||||||
|
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
|
||||||
|
})
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ function addStorage (input) {
|
|||||||
key,
|
key,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
type: input.type,
|
type: input.type,
|
||||||
path: input.path
|
path: input.path,
|
||||||
|
isOpen: false
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(newStorage)
|
return Promise.resolve(newStorage)
|
||||||
@@ -48,7 +49,8 @@ function addStorage (input) {
|
|||||||
key: newStorage.key,
|
key: newStorage.key,
|
||||||
type: newStorage.type,
|
type: newStorage.type,
|
||||||
name: newStorage.name,
|
name: newStorage.name,
|
||||||
path: newStorage.path
|
path: newStorage.path,
|
||||||
|
isOpen: false
|
||||||
})
|
})
|
||||||
|
|
||||||
localStorage.setItem('storages', JSON.stringify(rawStorages))
|
localStorage.setItem('storages', JSON.stringify(rawStorages))
|
||||||
|
|||||||
429
browser/main/lib/dataApi/attachmentManagement.js
Normal file
429
browser/main/lib/dataApi/attachmentManagement.js
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
const uniqueSlug = require('unique-slug')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const findStorage = require('browser/lib/findStorage')
|
||||||
|
const mdurl = require('mdurl')
|
||||||
|
const fse = require('fs-extra')
|
||||||
|
const escapeStringRegexp = require('escape-string-regexp')
|
||||||
|
const sander = require('sander')
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
|
||||||
|
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
||||||
|
const DESTINATION_FOLDER = 'attachments'
|
||||||
|
const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description
|
||||||
|
* Copies a copy of an attachment to the storage folder specified by the given key and return the generated attachment name.
|
||||||
|
* Renames the file to match a unique file name.
|
||||||
|
*
|
||||||
|
* @param {String} sourceFilePath The source path of the attachment to be copied
|
||||||
|
* @param {String} storageKey Storage key of the destination storage
|
||||||
|
* @param {String} noteKey Key of the current note. Will be used as subfolder in :storage
|
||||||
|
* @param {boolean} useRandomName determines whether a random filename for the new file is used. If false the source file name is used
|
||||||
|
* @return {Promise<String>} name (inclusive extension) of the generated file
|
||||||
|
*/
|
||||||
|
function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = true) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!sourceFilePath) {
|
||||||
|
reject('sourceFilePath has to be given')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storageKey) {
|
||||||
|
reject('storageKey has to be given')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noteKey) {
|
||||||
|
reject('noteKey has to be given')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(sourceFilePath)) {
|
||||||
|
reject('source file does not exist')
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
|
|
||||||
|
const inputFileStream = fs.createReadStream(sourceFilePath)
|
||||||
|
let destinationName
|
||||||
|
if (useRandomName) {
|
||||||
|
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}`
|
||||||
|
} else {
|
||||||
|
destinationName = path.basename(sourceFilePath)
|
||||||
|
}
|
||||||
|
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||||
|
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||||
|
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
|
||||||
|
inputFileStream.pipe(outputFile)
|
||||||
|
inputFileStream.on('end', () => {
|
||||||
|
resolve(destinationName)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
return reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
|
||||||
|
let destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER)
|
||||||
|
if (!fs.existsSync(destinationDir)) {
|
||||||
|
fs.mkdirSync(destinationDir)
|
||||||
|
}
|
||||||
|
destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER, noteKey)
|
||||||
|
if (!fs.existsSync(destinationDir)) {
|
||||||
|
fs.mkdirSync(destinationDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Moves attachments from the old location ('/images') to the new one ('/attachments/noteKey)
|
||||||
|
* @param markdownContent of the current note
|
||||||
|
* @param storagePath Storage path of the current note
|
||||||
|
* @param noteKey Key of the current note
|
||||||
|
*/
|
||||||
|
function migrateAttachments (markdownContent, storagePath, noteKey) {
|
||||||
|
if (noteKey !== undefined && sander.existsSync(path.join(storagePath, 'images'))) {
|
||||||
|
const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
|
||||||
|
if (attachments.length) {
|
||||||
|
createAttachmentDestinationFolder(storagePath, noteKey)
|
||||||
|
}
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
const attachmentBaseName = path.basename(attachment)
|
||||||
|
const possibleLegacyPath = path.join(storagePath, 'images', attachmentBaseName)
|
||||||
|
if (sander.existsSync(possibleLegacyPath)) {
|
||||||
|
const destinationPath = path.join(storagePath, DESTINATION_FOLDER, attachmentBaseName)
|
||||||
|
if (!sander.existsSync(destinationPath)) {
|
||||||
|
sander.copyFileSync(possibleLegacyPath).to(destinationPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Fixes the URLs embedded in the generated HTML so that they again refer actual local files.
|
||||||
|
* @param {String} renderedHTML HTML in that the links should be fixed
|
||||||
|
* @param {String} storagePath Path of the current storage
|
||||||
|
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
|
||||||
|
*/
|
||||||
|
function fixLocalURLS (renderedHTML, storagePath) {
|
||||||
|
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) {
|
||||||
|
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
|
||||||
|
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Generates the markdown code for a given attachment
|
||||||
|
* @param {String} fileName Name of the attachment
|
||||||
|
* @param {String} path Path of the attachment
|
||||||
|
* @param {Boolean} showPreview Indicator whether the generated markdown should show a preview of the image. Note that at the moment only previews for images are supported
|
||||||
|
* @returns {String} Generated markdown code
|
||||||
|
*/
|
||||||
|
function generateAttachmentMarkdown (fileName, path, showPreview) {
|
||||||
|
return `${showPreview ? '!' : ''}[${fileName}](${path})`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Handles the drop-event of a file. Includes the necessary markdown code and copies the file to the corresponding storage folder.
|
||||||
|
* The method calls {CodeEditor#insertAttachmentMd()} to include the generated markdown at the needed place!
|
||||||
|
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
|
||||||
|
* @param {String} storageKey Key of the current storage
|
||||||
|
* @param {String} noteKey Key of the current note
|
||||||
|
* @param {Event} dropEvent DropEvent
|
||||||
|
*/
|
||||||
|
function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
||||||
|
const file = dropEvent.dataTransfer.files[0]
|
||||||
|
const filePath = file.path
|
||||||
|
const originalFileName = path.basename(filePath)
|
||||||
|
const fileType = file['type']
|
||||||
|
|
||||||
|
copyAttachment(filePath, storageKey, noteKey).then((fileName) => {
|
||||||
|
const showPreview = fileType.startsWith('image')
|
||||||
|
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview)
|
||||||
|
codeEditor.insertAttachmentMd(imageMd)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
|
||||||
|
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
|
||||||
|
* @param {String} storageKey Key of the current storage
|
||||||
|
* @param {String} noteKey Key of the current note
|
||||||
|
* @param {DataTransferItem} dataTransferItem Part of the past-event
|
||||||
|
*/
|
||||||
|
function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
|
||||||
|
if (!codeEditor) {
|
||||||
|
throw new Error('codeEditor has to be given')
|
||||||
|
}
|
||||||
|
if (!storageKey) {
|
||||||
|
throw new Error('storageKey has to be given')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noteKey) {
|
||||||
|
throw new Error('noteKey has to be given')
|
||||||
|
}
|
||||||
|
if (!dataTransferItem) {
|
||||||
|
throw new Error('dataTransferItem has to be given')
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = dataTransferItem.getAsFile()
|
||||||
|
const reader = new FileReader()
|
||||||
|
let base64data
|
||||||
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
|
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||||
|
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||||
|
|
||||||
|
const imageName = `${uniqueSlug()}.png`
|
||||||
|
const imagePath = path.join(destinationDir, imageName)
|
||||||
|
|
||||||
|
reader.onloadend = function () {
|
||||||
|
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
||||||
|
base64data += base64data.replace('+', ' ')
|
||||||
|
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||||
|
fs.writeFileSync(imagePath, binaryData, 'binary')
|
||||||
|
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
|
||||||
|
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
|
||||||
|
codeEditor.insertAttachmentMd(imageMd)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns all attachment paths of the given markdown
|
||||||
|
* @param {String} markdownContent content in which the attachment paths should be found
|
||||||
|
* @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown
|
||||||
|
*/
|
||||||
|
function getAttachmentsInMarkdownContent (markdownContent) {
|
||||||
|
const preparedInput = markdownContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
|
||||||
|
const regexp = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + ')' + '?([a-zA-Z0-9]|-)*' + '(' + escapeStringRegexp(path.sep) + ')' + '([a-zA-Z0-9]|\\.)+(\\.[a-zA-Z0-9]+)?', 'g')
|
||||||
|
return preparedInput.match(regexp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns an array of the absolute paths of the attachments referenced in the given markdown code
|
||||||
|
* @param {String} markdownContent content in which the attachment paths should be found
|
||||||
|
* @param {String} storagePath path of the current storage
|
||||||
|
* @returns {String[]} Absolute paths of the referenced attachments
|
||||||
|
*/
|
||||||
|
function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
|
||||||
|
const temp = getAttachmentsInMarkdownContent(markdownContent) || []
|
||||||
|
const result = []
|
||||||
|
for (const relativePath of temp) {
|
||||||
|
result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER)))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Moves the attachments of the current note to the new location.
|
||||||
|
* Returns a modified version of the given content so that the links to the attachments point to the new note key.
|
||||||
|
* @param {String} oldPath Source of the note to be moved
|
||||||
|
* @param {String} newPath Destination of the note to be moved
|
||||||
|
* @param {String} noteKey Old note key
|
||||||
|
* @param {String} newNoteKey New note key
|
||||||
|
* @param {String} noteContent Content of the note to be moved
|
||||||
|
* @returns {String} Modified version of noteContent in which the paths of the attachments are fixed
|
||||||
|
*/
|
||||||
|
function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) {
|
||||||
|
const src = path.join(oldPath, DESTINATION_FOLDER, noteKey)
|
||||||
|
const dest = path.join(newPath, DESTINATION_FOLDER, newNoteKey)
|
||||||
|
if (fse.existsSync(src)) {
|
||||||
|
fse.moveSync(src, dest)
|
||||||
|
}
|
||||||
|
return replaceNoteKeyWithNewNoteKey(noteContent, noteKey, newNoteKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the given content so that in all attachment references the oldNoteKey is replaced by the new one
|
||||||
|
* @param noteContent content that should be modified
|
||||||
|
* @param oldNoteKey note key to be replaced
|
||||||
|
* @param newNoteKey note key serving as a replacement
|
||||||
|
* @returns {String} modified note content
|
||||||
|
*/
|
||||||
|
function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
|
||||||
|
if (noteContent) {
|
||||||
|
const preparedInput = noteContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
|
||||||
|
return preparedInput.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey))
|
||||||
|
}
|
||||||
|
return noteContent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Deletes all :storage and noteKey references from the given input.
|
||||||
|
* @param input Input in which the references should be deleted
|
||||||
|
* @param noteKey Key of the current note
|
||||||
|
* @returns {String} Input without the references
|
||||||
|
*/
|
||||||
|
function removeStorageAndNoteReferences (input, noteKey) {
|
||||||
|
return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Deletes the attachment folder specified by the given storageKey and noteKey
|
||||||
|
* @param storageKey Key of the storage of the note to be deleted
|
||||||
|
* @param noteKey Key of the note to be deleted
|
||||||
|
*/
|
||||||
|
function deleteAttachmentFolder (storageKey, noteKey) {
|
||||||
|
const storagePath = findStorage.findStorage(storageKey)
|
||||||
|
const noteAttachmentPath = path.join(storagePath.path, DESTINATION_FOLDER, noteKey)
|
||||||
|
sander.rimrafSync(noteAttachmentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Deletes all attachments stored in the attachment folder of the give not that are not referenced in the markdownContent
|
||||||
|
* @param markdownContent Content of the note. All unreferenced notes will be deleted
|
||||||
|
* @param storageKey StorageKey of the current note. Is used to determine the belonging attachment folder.
|
||||||
|
* @param noteKey NoteKey of the current note. Is used to determine the belonging attachment folder.
|
||||||
|
*/
|
||||||
|
function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey) {
|
||||||
|
if (storageKey == null || noteKey == null || markdownContent == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const targetStorage = findStorage.findStorage(storageKey)
|
||||||
|
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||||
|
const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
|
||||||
|
const attachmentsInNoteOnlyFileNames = []
|
||||||
|
if (attachmentsInNote) {
|
||||||
|
for (let i = 0; i < attachmentsInNote.length; i++) {
|
||||||
|
attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), ''))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fs.existsSync(attachmentFolder)) {
|
||||||
|
fs.readdir(attachmentFolder, (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error reading directory "' + attachmentFolder + '". Error:')
|
||||||
|
console.error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
files.forEach(file => {
|
||||||
|
if (!attachmentsInNoteOnlyFileNames.includes(file)) {
|
||||||
|
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
|
||||||
|
fs.unlink(absolutePathOfFile, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Could not delete "%s"', absolutePathOfFile)
|
||||||
|
console.error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.info('File "' + absolutePathOfFile + '" deleted because it was not included in the content of the note')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.info('Attachment folder ("' + attachmentFolder + '") did not exist..')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones the attachments of a given note.
|
||||||
|
* Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination.
|
||||||
|
* @param oldNote Note that is being cloned
|
||||||
|
* @param newNote Clone of the note
|
||||||
|
*/
|
||||||
|
function cloneAttachments (oldNote, newNote) {
|
||||||
|
if (newNote.type === 'MARKDOWN_NOTE') {
|
||||||
|
const oldStorage = findStorage.findStorage(oldNote.storage)
|
||||||
|
const newStorage = findStorage.findStorage(newNote.storage)
|
||||||
|
const attachmentsPaths = getAbsolutePathsOfAttachmentsInContent(oldNote.content, oldStorage.path) || []
|
||||||
|
|
||||||
|
const destinationFolder = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key)
|
||||||
|
if (!sander.existsSync(destinationFolder)) {
|
||||||
|
sander.mkdirSync(destinationFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const attachment of attachmentsPaths) {
|
||||||
|
const destination = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key, path.basename(attachment))
|
||||||
|
sander.copyFileSync(attachment).to(destination)
|
||||||
|
}
|
||||||
|
newNote.content = replaceNoteKeyWithNewNoteKey(newNote.content, oldNote.key, newNote.key)
|
||||||
|
} else {
|
||||||
|
console.debug('Cloning of the attachment was skipped since it only works for MARKDOWN_NOTEs')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateFileNotFoundMarkdown () {
|
||||||
|
return '**' + i18n.__('⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠') + '**'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a given text is a link to an boostnote attachment
|
||||||
|
* @param text Text that might contain a attachment link
|
||||||
|
* @return {Boolean} Result of the test
|
||||||
|
*/
|
||||||
|
function isAttachmentLink (text) {
|
||||||
|
if (text) {
|
||||||
|
return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + '[' + PATH_SEPARATORS + ']' + '.*\\).*', 'gi')) != null
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Handles the paste of an attachment link. Copies the referenced attachment to the location belonging to the new note.
|
||||||
|
* Returns a modified version of the pasted text so that it matches the copied attachment (resp. the new location)
|
||||||
|
* @param storageKey StorageKey of the current note
|
||||||
|
* @param noteKey NoteKey of the currentNote
|
||||||
|
* @param linkText Text that was pasted
|
||||||
|
* @return {Promise<String>} Promise returning the modified text
|
||||||
|
*/
|
||||||
|
function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
|
||||||
|
if (storageKey != null && noteKey != null && linkText != null) {
|
||||||
|
const storagePath = findStorage.findStorage(storageKey).path
|
||||||
|
const attachments = getAttachmentsInMarkdownContent(linkText) || []
|
||||||
|
const replaceInstructions = []
|
||||||
|
const copies = []
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
const absPathOfAttachment = attachment.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER))
|
||||||
|
copies.push(
|
||||||
|
sander.exists(absPathOfAttachment)
|
||||||
|
.then((fileExists) => {
|
||||||
|
if (!fileExists) {
|
||||||
|
const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')'))
|
||||||
|
replaceInstructions.push({regexp: fileNotFoundRegexp, replacement: this.generateFileNotFoundMarkdown()})
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
return this.copyAttachment(absPathOfAttachment, storageKey, noteKey)
|
||||||
|
.then((fileName) => {
|
||||||
|
const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')'))
|
||||||
|
replaceInstructions.push({
|
||||||
|
regexp: replaceLinkRegExp,
|
||||||
|
replacement: '(' + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + ')'
|
||||||
|
})
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Promise.all(copies).then(() => {
|
||||||
|
let modifiedLinkText = linkText
|
||||||
|
for (const replaceInstruction of replaceInstructions) {
|
||||||
|
modifiedLinkText = modifiedLinkText.replace(replaceInstruction.regexp, replaceInstruction.replacement)
|
||||||
|
}
|
||||||
|
return modifiedLinkText
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('One if the parameters was null -> Do nothing..')
|
||||||
|
return Promise.resolve(linkText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
copyAttachment,
|
||||||
|
fixLocalURLS,
|
||||||
|
generateAttachmentMarkdown,
|
||||||
|
handleAttachmentDrop,
|
||||||
|
handlePastImageEvent,
|
||||||
|
getAttachmentsInMarkdownContent,
|
||||||
|
getAbsolutePathsOfAttachmentsInContent,
|
||||||
|
removeStorageAndNoteReferences,
|
||||||
|
deleteAttachmentFolder,
|
||||||
|
deleteAttachmentsNotPresentInNote,
|
||||||
|
moveAttachments,
|
||||||
|
cloneAttachments,
|
||||||
|
isAttachmentLink,
|
||||||
|
handleAttachmentLinkPaste,
|
||||||
|
generateFileNotFoundMarkdown,
|
||||||
|
migrateAttachments,
|
||||||
|
STORAGE_FOLDER_PLACEHOLDER,
|
||||||
|
DESTINATION_FOLDER
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const { findStorage } = require('browser/lib/findStorage')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Copy an image and return the path.
|
|
||||||
* @param {String} filePath
|
|
||||||
* @param {String} storageKey
|
|
||||||
* @param {Boolean} rename create new filename or leave the old one
|
|
||||||
* @return {Promise<any>} an image path
|
|
||||||
*/
|
|
||||||
function copyImage (filePath, storageKey, rename = true) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const targetStorage = findStorage(storageKey)
|
|
||||||
|
|
||||||
const inputImage = fs.createReadStream(filePath)
|
|
||||||
const imageExt = path.extname(filePath)
|
|
||||||
const imageName = rename ? Math.random().toString(36).slice(-16) : path.basename(filePath, imageExt)
|
|
||||||
const basename = `${imageName}${imageExt}`
|
|
||||||
const imageDir = path.join(targetStorage.path, 'images')
|
|
||||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
|
||||||
const outputImage = fs.createWriteStream(path.join(imageDir, basename))
|
|
||||||
inputImage.pipe(outputImage)
|
|
||||||
resolve(basename)
|
|
||||||
} catch (e) {
|
|
||||||
return reject(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = copyImage
|
|
||||||
26
browser/main/lib/dataApi/createSnippet.js
Normal file
26
browser/main/lib/dataApi/createSnippet.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import consts from 'browser/lib/consts'
|
||||||
|
import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
|
||||||
|
|
||||||
|
function createSnippet (snippetFile) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const newSnippet = {
|
||||||
|
id: crypto.randomBytes(16).toString('hex'),
|
||||||
|
name: 'Unnamed snippet',
|
||||||
|
prefix: [],
|
||||||
|
content: ''
|
||||||
|
}
|
||||||
|
fetchSnippet(null, snippetFile).then((snippets) => {
|
||||||
|
snippets.push(newSnippet)
|
||||||
|
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
resolve(newSnippet)
|
||||||
|
})
|
||||||
|
}).catch((err) => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createSnippet
|
||||||
@@ -5,6 +5,7 @@ const resolveStorageNotes = require('./resolveStorageNotes')
|
|||||||
const CSON = require('@rokt33r/season')
|
const CSON = require('@rokt33r/season')
|
||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
const { findStorage } = require('browser/lib/findStorage')
|
const { findStorage } = require('browser/lib/findStorage')
|
||||||
|
const deleteSingleNote = require('./deleteNote')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} storageKey
|
* @param {String} storageKey
|
||||||
@@ -49,11 +50,7 @@ function deleteFolder (storageKey, folderKey) {
|
|||||||
|
|
||||||
const deleteAllNotes = targetNotes
|
const deleteAllNotes = targetNotes
|
||||||
.map(function deleteNote (note) {
|
.map(function deleteNote (note) {
|
||||||
const notePath = path.join(storage.path, 'notes', note.key + '.cson')
|
return deleteSingleNote(storageKey, note.key)
|
||||||
return sander.unlink(notePath)
|
|
||||||
.catch(function (err) {
|
|
||||||
console.warn('Failed to delete', notePath, err)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
return Promise.all(deleteAllNotes)
|
return Promise.all(deleteAllNotes)
|
||||||
.then(() => storage)
|
.then(() => storage)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const resolveStorageData = require('./resolveStorageData')
|
const resolveStorageData = require('./resolveStorageData')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
|
const attachmentManagement = require('./attachmentManagement')
|
||||||
const { findStorage } = require('browser/lib/findStorage')
|
const { findStorage } = require('browser/lib/findStorage')
|
||||||
|
|
||||||
function deleteNote (storageKey, noteKey) {
|
function deleteNote (storageKey, noteKey) {
|
||||||
@@ -25,6 +26,10 @@ function deleteNote (storageKey, noteKey) {
|
|||||||
storageKey
|
storageKey
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.then(function deleteAttachments (storageInfo) {
|
||||||
|
attachmentManagement.deleteAttachmentFolder(storageInfo.storageKey, storageInfo.noteKey)
|
||||||
|
return storageInfo
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = deleteNote
|
module.exports = deleteNote
|
||||||
|
|||||||
17
browser/main/lib/dataApi/deleteSnippet.js
Normal file
17
browser/main/lib/dataApi/deleteSnippet.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import consts from 'browser/lib/consts'
|
||||||
|
import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
|
||||||
|
|
||||||
|
function deleteSnippet (snippet, snippetFile) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fetchSnippet(null, snippetFile).then((snippets) => {
|
||||||
|
snippets = snippets.filter(currentSnippet => currentSnippet.id !== snippet.id)
|
||||||
|
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
resolve(snippet)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = deleteSnippet
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
import resolveStorageData from './resolveStorageData'
|
import resolveStorageData from './resolveStorageData'
|
||||||
import resolveStorageNotes from './resolveStorageNotes'
|
import resolveStorageNotes from './resolveStorageNotes'
|
||||||
|
import filenamify from 'filenamify'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
|||||||
notes
|
notes
|
||||||
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
||||||
.forEach(snippet => {
|
.forEach(snippet => {
|
||||||
const notePath = path.join(exportDir, `${snippet.title}.${fileType}`)
|
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
|
||||||
fs.writeFileSync(notePath, snippet.content)
|
fs.writeFileSync(notePath, snippet.content)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import copyFile from 'browser/main/lib/dataApi/copyFile'
|
import copyFile from 'browser/main/lib/dataApi/copyFile'
|
||||||
import {findStorage} from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
const LOCAL_STORED_REGEX = /!\[(.*?)]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
|
||||||
const IMAGES_FOLDER_NAME = 'images'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export note together with images
|
* Export note together with images
|
||||||
*
|
*
|
||||||
@@ -27,20 +24,7 @@ function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
|||||||
throw new Error('Storage path is not found')
|
throw new Error('Storage path is not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => {
|
let exportedData = noteContent
|
||||||
if (!path.extname(dstFilename)) {
|
|
||||||
dstFilename += path.extname(srcFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename)
|
|
||||||
|
|
||||||
exportTasks.push({
|
|
||||||
src: path.join(IMAGES_FOLDER_NAME, srcFilename),
|
|
||||||
dst: dstRelativePath
|
|
||||||
})
|
|
||||||
|
|
||||||
return ``
|
|
||||||
})
|
|
||||||
|
|
||||||
if (outputFormatter) {
|
if (outputFormatter) {
|
||||||
exportedData = outputFormatter(exportedData, exportTasks)
|
exportedData = outputFormatter(exportedData, exportTasks)
|
||||||
|
|||||||
20
browser/main/lib/dataApi/fetchSnippet.js
Normal file
20
browser/main/lib/dataApi/fetchSnippet.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import consts from 'browser/lib/consts'
|
||||||
|
|
||||||
|
function fetchSnippet (id, snippetFile) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.readFile(snippetFile || consts.SNIPPET_FILE, 'utf8', (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
const snippets = JSON.parse(data)
|
||||||
|
if (id) {
|
||||||
|
const snippet = snippets.find(snippet => { return snippet.id === id })
|
||||||
|
resolve(snippet)
|
||||||
|
}
|
||||||
|
resolve(snippets)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = fetchSnippet
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const dataApi = {
|
const dataApi = {
|
||||||
init: require('./init'),
|
init: require('./init'),
|
||||||
|
toggleStorage: require('./toggleStorage'),
|
||||||
addStorage: require('./addStorage'),
|
addStorage: require('./addStorage'),
|
||||||
renameStorage: require('./renameStorage'),
|
renameStorage: require('./renameStorage'),
|
||||||
removeStorage: require('./removeStorage'),
|
removeStorage: require('./removeStorage'),
|
||||||
@@ -13,6 +14,10 @@ const dataApi = {
|
|||||||
deleteNote: require('./deleteNote'),
|
deleteNote: require('./deleteNote'),
|
||||||
moveNote: require('./moveNote'),
|
moveNote: require('./moveNote'),
|
||||||
migrateFromV5Storage: require('./migrateFromV5Storage'),
|
migrateFromV5Storage: require('./migrateFromV5Storage'),
|
||||||
|
createSnippet: require('./createSnippet'),
|
||||||
|
deleteSnippet: require('./deleteSnippet'),
|
||||||
|
updateSnippet: require('./updateSnippet'),
|
||||||
|
fetchSnippet: require('./fetchSnippet'),
|
||||||
|
|
||||||
_migrateFromV6Storage: require('./migrateFromV6Storage'),
|
_migrateFromV6Storage: require('./migrateFromV6Storage'),
|
||||||
_resolveStorageData: require('./resolveStorageData'),
|
_resolveStorageData: require('./resolveStorageData'),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const CSON = require('@rokt33r/season')
|
|||||||
const keygen = require('browser/lib/keygen')
|
const keygen = require('browser/lib/keygen')
|
||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
const { findStorage } = require('browser/lib/findStorage')
|
const { findStorage } = require('browser/lib/findStorage')
|
||||||
const copyImage = require('./copyImage')
|
const attachmentManagement = require('./attachmentManagement')
|
||||||
|
|
||||||
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
||||||
let oldStorage, newStorage
|
let oldStorage, newStorage
|
||||||
@@ -64,32 +64,20 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
|||||||
noteData.key = newNoteKey
|
noteData.key = newNoteKey
|
||||||
noteData.storage = newStorageKey
|
noteData.storage = newStorageKey
|
||||||
noteData.updatedAt = new Date()
|
noteData.updatedAt = new Date()
|
||||||
|
noteData.oldContent = noteData.content
|
||||||
|
|
||||||
return noteData
|
return noteData
|
||||||
})
|
})
|
||||||
.then(function moveImages (noteData) {
|
.then(function moveAttachments (noteData) {
|
||||||
const searchImagesRegex = /!\[.*?]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
if (oldStorage.path === newStorage.path) {
|
||||||
let match = searchImagesRegex.exec(noteData.content)
|
return noteData
|
||||||
|
|
||||||
const moveTasks = []
|
|
||||||
while (match != null) {
|
|
||||||
const [, filename] = match
|
|
||||||
const oldPath = path.join(oldStorage.path, 'images', filename)
|
|
||||||
moveTasks.push(
|
|
||||||
copyImage(oldPath, noteData.storage, false)
|
|
||||||
.then(() => {
|
|
||||||
fs.unlinkSync(oldPath)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// find next occurence
|
|
||||||
match = searchImagesRegex.exec(noteData.content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(moveTasks).then(() => noteData)
|
noteData.content = attachmentManagement.moveAttachments(oldStorage.path, newStorage.path, noteKey, newNoteKey, noteData.content)
|
||||||
|
return noteData
|
||||||
})
|
})
|
||||||
.then(function writeAndReturn (noteData) {
|
.then(function writeAndReturn (noteData) {
|
||||||
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage', 'oldContent']))
|
||||||
return noteData
|
return noteData
|
||||||
})
|
})
|
||||||
.then(function deleteOldNote (data) {
|
.then(function deleteOldNote (data) {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ function resolveStorageData (storageCache) {
|
|||||||
key: storageCache.key,
|
key: storageCache.key,
|
||||||
name: storageCache.name,
|
name: storageCache.name,
|
||||||
type: storageCache.type,
|
type: storageCache.type,
|
||||||
path: storageCache.path
|
path: storageCache.path,
|
||||||
|
isOpen: storageCache.isOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
|
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ function resolveStorageNotes (storage) {
|
|||||||
data.storage = storage.key
|
data.storage = storage.key
|
||||||
return data
|
return data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(notePath)
|
console.error(`error on note path: ${notePath}, error: ${err}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.filter(function filterOnlyNoteObject (noteObj) {
|
||||||
|
return typeof noteObj === 'object'
|
||||||
|
})
|
||||||
|
|
||||||
return Promise.resolve(notes)
|
return Promise.resolve(notes)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user