mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 02:06:29 +00:00
Compare commits
493 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64ca875cfd | ||
|
|
743c97940e | ||
|
|
d4dd74e820 | ||
|
|
3f77cb2214 | ||
|
|
7c839a1df9 | ||
|
|
fddeaa966d | ||
|
|
e706ec0ffe | ||
|
|
d44a76bae3 | ||
|
|
4488de9add | ||
|
|
4061866042 | ||
|
|
6d4aa27e15 | ||
|
|
d70d3b439f | ||
|
|
56851eb91f | ||
|
|
b224c72e98 | ||
|
|
6c57ac8f01 | ||
|
|
cf35dc5345 | ||
|
|
102d13bbae | ||
|
|
faf2aff959 | ||
|
|
5d54ad3342 | ||
|
|
0f5d753910 | ||
|
|
b8a295713c | ||
|
|
fdb54b5cdc | ||
|
|
df20662005 | ||
|
|
df3b2cd8fe | ||
|
|
a39da481e0 | ||
|
|
830ade9596 | ||
|
|
2aa296ff33 | ||
|
|
9050035c74 | ||
|
|
c245855bbf | ||
|
|
ef66e71feb | ||
|
|
c33058ae2b | ||
|
|
d95d282f39 | ||
|
|
aac075be06 | ||
|
|
bf288fdaeb | ||
|
|
a6eddb5798 | ||
|
|
78ae7b847c | ||
|
|
c7bae93b48 | ||
|
|
938cf238ff | ||
|
|
5188846e2e | ||
|
|
d874a3a493 | ||
|
|
f95284622e | ||
|
|
0b6c0e6b94 | ||
|
|
b021bb73ed | ||
|
|
c76b653737 | ||
|
|
3414e2daf0 | ||
|
|
92e2cd102e | ||
|
|
b703c42ee3 | ||
|
|
94f7533ee7 | ||
|
|
101e5d5035 | ||
|
|
ab78af0691 | ||
|
|
af14b682b1 | ||
|
|
a26d4fb499 | ||
|
|
f644d21c61 | ||
|
|
2af52c193c | ||
|
|
7717cda52a | ||
|
|
c13746f10e | ||
|
|
49abfac98e | ||
|
|
aa26e5ac2a | ||
|
|
336f007fa1 | ||
|
|
a3e59d43a7 | ||
|
|
ae840ea689 | ||
|
|
79ee674ad9 | ||
|
|
4d77053313 | ||
|
|
235e88f115 | ||
|
|
021bac5b68 | ||
|
|
60c4cacfbc | ||
|
|
30e6fc516b | ||
|
|
1388e7d7f4 | ||
|
|
b79b123c65 | ||
|
|
d3a6ff6b6a | ||
|
|
10d3adbe40 | ||
|
|
e100e080da | ||
|
|
e2c7a8c384 | ||
|
|
d97bbe7918 | ||
|
|
3864d73d1a | ||
|
|
82987cd53c | ||
|
|
aa56aad46e | ||
|
|
ad7b155a99 | ||
|
|
f1ca06daf5 | ||
|
|
3df743f47b | ||
|
|
637225c2bc | ||
|
|
849104f530 | ||
|
|
3a4bc33d53 | ||
|
|
480c05650b | ||
|
|
fa265d769c | ||
|
|
4a197a5c90 | ||
|
|
23702056fd | ||
|
|
2a774d20f4 | ||
|
|
70b86907f3 | ||
|
|
d0d813552c | ||
|
|
c6f1f97a57 | ||
|
|
fdb1ef540d | ||
|
|
62a8ffb4ae | ||
|
|
1e3cf6f374 | ||
|
|
48c29dd7d9 | ||
|
|
e30727ab27 | ||
|
|
157eb5f87b | ||
|
|
8f290c2a6d | ||
|
|
47f6a217e8 | ||
|
|
69691bdf2a | ||
|
|
522a37b8e3 | ||
|
|
53aa142d94 | ||
|
|
61bd591ee8 | ||
|
|
fd2b438c67 | ||
|
|
5855cdfb26 | ||
|
|
a63266b0e9 | ||
|
|
fe19d96088 | ||
|
|
37933782d2 | ||
|
|
1dcc51e5a4 | ||
|
|
eb61ce2cf2 | ||
|
|
c5bcfe6ab3 | ||
|
|
6c7ed82fa9 | ||
|
|
00ed0d79ec | ||
|
|
cfc84f3e78 | ||
|
|
27e5010d8e | ||
|
|
efa00728d6 | ||
|
|
106caf2b3e | ||
|
|
3dca6c1fd6 | ||
|
|
bd56055593 | ||
|
|
dbd2c7049e | ||
|
|
07aa775de1 | ||
|
|
88ac2a98e8 | ||
|
|
d0c5592855 | ||
|
|
b03ad0cfe0 | ||
|
|
a43b0e6a57 | ||
|
|
12176473ff | ||
|
|
1614c1452f | ||
|
|
d38f16d732 | ||
|
|
d7a2296909 | ||
|
|
4550d888bb | ||
|
|
d62d1d670b | ||
|
|
f18e8c5a38 | ||
|
|
6cb6cd3f26 | ||
|
|
c5554e8f1e | ||
|
|
5144c0ecdc | ||
|
|
642c62d8ce | ||
|
|
f92da16544 | ||
|
|
6e8b370e54 | ||
|
|
1ecf1e0413 | ||
|
|
1ea1482aad | ||
|
|
aeded9ac0e | ||
|
|
1cec872273 | ||
|
|
e550295644 | ||
|
|
70da43eeb7 | ||
|
|
7e9fb6be32 | ||
|
|
258ed4e866 | ||
|
|
b8992362c2 | ||
|
|
60fcf2734a | ||
|
|
ae9e83cf66 | ||
|
|
b8d1e37cce | ||
|
|
6ecd1e5ea5 | ||
|
|
52956503f1 | ||
|
|
3b7bedbbe8 | ||
|
|
148feac43e | ||
|
|
3272033c63 | ||
|
|
73d1bdf84b | ||
|
|
0e38f61c85 | ||
|
|
9d84fe7719 | ||
|
|
5aaecfc0fe | ||
|
|
ff3026686f | ||
|
|
83243b61a6 | ||
|
|
80c4601fdc | ||
|
|
fa3700df7c | ||
|
|
6244e44033 | ||
|
|
d64dafc715 | ||
|
|
9e67880456 | ||
|
|
7484f6e6a6 | ||
|
|
d8d5810d7c | ||
|
|
4a167aa3d7 | ||
|
|
ced3460673 | ||
|
|
d75dd874ca | ||
|
|
1da477d1d1 | ||
|
|
a4ad3896f0 | ||
|
|
e536d203d2 | ||
|
|
ca038937e9 | ||
|
|
342d0862c6 | ||
|
|
3b11285bd5 | ||
|
|
3c404f3678 | ||
|
|
eb37be1381 | ||
|
|
0e29e8ac76 | ||
|
|
71ae42056f | ||
|
|
a9bad53209 | ||
|
|
eee340366e | ||
|
|
a2bc1a5d2d | ||
|
|
3610d5ea93 | ||
|
|
a75f8e5fdf | ||
|
|
1821a5c678 | ||
|
|
5c3a62b9c5 | ||
|
|
851f57c1f5 | ||
|
|
2cd3f8c6ee | ||
|
|
a331d82cb5 | ||
|
|
6ef2ec4ed2 | ||
|
|
29ed26a503 | ||
|
|
7b3d5ab1ae | ||
|
|
6f2f6e9567 | ||
|
|
9a0bc984d4 | ||
|
|
1922c8dbf8 | ||
|
|
a841449771 | ||
|
|
ff4ef16375 | ||
|
|
33f6926916 | ||
|
|
0b9635c160 | ||
|
|
555ae327b6 | ||
|
|
944c79ec61 | ||
|
|
623688c28e | ||
|
|
8936d8b517 | ||
|
|
14fec7473a | ||
|
|
33b40bf35f | ||
|
|
52bea8f808 | ||
|
|
39ce706f3e | ||
|
|
d0171a8933 | ||
|
|
9f75d2fe4b | ||
|
|
9dfc6c2bc1 | ||
|
|
025e778252 | ||
|
|
c761f631a1 | ||
|
|
e173117a44 | ||
|
|
1ff4206bed | ||
|
|
babc5626a9 | ||
|
|
b624c9a4d2 | ||
|
|
d83feafcb2 | ||
|
|
cc1cb5fbc7 | ||
|
|
80666fed1a | ||
|
|
db2c6c99f7 | ||
|
|
3f1fa44ee7 | ||
|
|
4717e4fe3f | ||
|
|
ffc390d49d | ||
|
|
002e7ab9dd | ||
|
|
ca69bd69f2 | ||
|
|
3e2a366dc6 | ||
|
|
d365aaf270 | ||
|
|
818ee16e39 | ||
|
|
533caba717 | ||
|
|
cae6fd45b3 | ||
|
|
8c268be823 | ||
|
|
17845428bd | ||
|
|
efd1b3cd3c | ||
|
|
2ccd00a378 | ||
|
|
0123a99b5d | ||
|
|
ac744fbd90 | ||
|
|
10a1104073 | ||
|
|
a39a856f69 | ||
|
|
e6e69b4fd2 | ||
|
|
b4de1b49f2 | ||
|
|
c47428b27f | ||
|
|
d267a78416 | ||
|
|
04bb04a6a9 | ||
|
|
8eb535169f | ||
|
|
e0e0fbf739 | ||
|
|
4f8e8ae7b9 | ||
|
|
15b77482ac | ||
|
|
7420363adf | ||
|
|
1666e3a58a | ||
|
|
adbe85cc33 | ||
|
|
89d8d36ec3 | ||
|
|
a631adacb5 | ||
|
|
3384d1b7c3 | ||
|
|
f70de60672 | ||
|
|
cfd54c3f0e | ||
|
|
b29c0fe8cb | ||
|
|
5c1e5e0fcc | ||
|
|
8ec56390c4 | ||
|
|
2ad27e175c | ||
|
|
da1bd3f1fd | ||
|
|
dd913279d7 | ||
|
|
1246a677d1 | ||
|
|
43f2fc0740 | ||
|
|
b821209807 | ||
|
|
39fc5da98f | ||
|
|
504b6af3f6 | ||
|
|
b3ede3230c | ||
|
|
7035503fa7 | ||
|
|
7d147fd040 | ||
|
|
2a44e0b7eb | ||
|
|
686b9bc82c | ||
|
|
0c1497a255 | ||
|
|
496090610f | ||
|
|
16177754d5 | ||
|
|
f41f4939bc | ||
|
|
610503472a | ||
|
|
5dcd74b3b0 | ||
|
|
305825da78 | ||
|
|
205451a31d | ||
|
|
f1ae04fd07 | ||
|
|
4ba82275b9 | ||
|
|
093920173e | ||
|
|
2c3d95a4db | ||
|
|
76928e43a3 | ||
|
|
a816c5dc23 | ||
|
|
5ef84e4bfc | ||
|
|
fbdc9c9f8d | ||
|
|
6510152138 | ||
|
|
df0c6a3b94 | ||
|
|
dfe0d74845 | ||
|
|
b9edd0238d | ||
|
|
bf72237b38 | ||
|
|
a24f6e80c7 | ||
|
|
297c764fe1 | ||
|
|
b03c2a1f80 | ||
|
|
7af77384e7 | ||
|
|
bacbfc8615 | ||
|
|
c8466e9fa6 | ||
|
|
4a231d6fdb | ||
|
|
4e80e1dd03 | ||
|
|
e0b18c6868 | ||
|
|
15b9f8e13f | ||
|
|
03b8dbbc44 | ||
|
|
80af8dcf80 | ||
|
|
62b2856d29 | ||
|
|
7ab3ce91a1 | ||
|
|
e040aeef55 | ||
|
|
46df5a8fa7 | ||
|
|
f61fbbaead | ||
|
|
480f515114 | ||
|
|
e206e6babf | ||
|
|
eae4b52aa1 | ||
|
|
933f75f1ee | ||
|
|
f3c72e561a | ||
|
|
44d6374cfe | ||
|
|
eba13800ff | ||
|
|
3942492f32 | ||
|
|
fe323d5764 | ||
|
|
4740edfb1f | ||
|
|
671dff060d | ||
|
|
4b0dc08426 | ||
|
|
10ffa35b29 | ||
|
|
57fadacda0 | ||
|
|
5c186f30a8 | ||
|
|
0a205f77b0 | ||
|
|
75a0f4373c | ||
|
|
189b245b1d | ||
|
|
0eae47c8be | ||
|
|
00607cb704 | ||
|
|
d79b6e094a | ||
|
|
f9d5c86245 | ||
|
|
a6a1291d0e | ||
|
|
49db1c8244 | ||
|
|
dbe1721d50 | ||
|
|
b55420e935 | ||
|
|
2706df2b24 | ||
|
|
90f791de1b | ||
|
|
c9db3f98d1 | ||
|
|
bdfe233472 | ||
|
|
d340aeb77d | ||
|
|
2da1105ff8 | ||
|
|
e6e5036474 | ||
|
|
609a1709c5 | ||
|
|
2bb9607eea | ||
|
|
ca32d05bb2 | ||
|
|
67a016add0 | ||
|
|
ea41dbb3bc | ||
|
|
637090d259 | ||
|
|
911d65131a | ||
|
|
f4203263bb | ||
|
|
79df5249ef | ||
|
|
ac71093888 | ||
|
|
d15e0f9fe5 | ||
|
|
d3861caf28 | ||
|
|
1c8e379fdd | ||
|
|
d9783490ec | ||
|
|
bb32c3a8d3 | ||
|
|
bab7ec388c | ||
|
|
e2957192d0 | ||
|
|
3994c78365 | ||
|
|
bfd4d7ffe1 | ||
|
|
cb2f18c078 | ||
|
|
69a032c1cf | ||
|
|
a287afb3e9 | ||
|
|
da81f10e04 | ||
|
|
786675a99b | ||
|
|
6603f46678 | ||
|
|
fdfa3bb8f5 | ||
|
|
da204a27c5 | ||
|
|
e11a68afba | ||
|
|
168b0f82dd | ||
|
|
6715a54da2 | ||
|
|
6b32b3ae80 | ||
|
|
6a2242725d | ||
|
|
e9070fadab | ||
|
|
1117e1b724 | ||
|
|
d015b18c66 | ||
|
|
01641b5af4 | ||
|
|
7c0c81207b | ||
|
|
c844b60941 | ||
|
|
9f8246a26a | ||
|
|
6d5141b60f | ||
|
|
8b4a9dd325 | ||
|
|
5006aaae38 | ||
|
|
1bb841d5c5 | ||
|
|
f57c4f390d | ||
|
|
646151e020 | ||
|
|
20f573c477 | ||
|
|
094e4c5da8 | ||
|
|
7716880a6c | ||
|
|
71605fb8fe | ||
|
|
fa9d8b8881 | ||
|
|
aa0566b8ca | ||
|
|
2a838ebb0b | ||
|
|
fabc975b20 | ||
|
|
3bdc88cecb | ||
|
|
a591001761 | ||
|
|
54717ea6f2 | ||
|
|
ceca4c98a3 | ||
|
|
39b4287c5e | ||
|
|
53923c9c87 | ||
|
|
ede733888d | ||
|
|
a79db03093 | ||
|
|
5c8254a9c4 | ||
|
|
2f7b62f710 | ||
|
|
a19c13eb3c | ||
|
|
64d4cd84af | ||
|
|
734db58d85 | ||
|
|
2bbcb8ca89 | ||
|
|
07e810a231 | ||
|
|
48beb184df | ||
|
|
f195e87568 | ||
|
|
13d44ae56a | ||
|
|
00b4874d09 | ||
|
|
5bb90babbc | ||
|
|
7cde30d352 | ||
|
|
73fbf49ba4 | ||
|
|
b39ef5948b | ||
|
|
7cf9dda821 | ||
|
|
657806c8cf | ||
|
|
d61a218808 | ||
|
|
039f73711a | ||
|
|
6e885acf8c | ||
|
|
9ef07cea7a | ||
|
|
9e3b321aaf | ||
|
|
21560701ea | ||
|
|
4556375174 | ||
|
|
91b5398b5a | ||
|
|
eeb8016992 | ||
|
|
736106be3a | ||
|
|
f400568dc0 | ||
|
|
0ca96cba6e | ||
|
|
df4d837026 | ||
|
|
760f84d7fa | ||
|
|
ab35c3557f | ||
|
|
174a315e3f | ||
|
|
0834313456 | ||
|
|
ce3b29085f | ||
|
|
b93d7a204f | ||
|
|
df931e10c0 | ||
|
|
9572cb2d33 | ||
|
|
51e836f32a | ||
|
|
7fefbd88d0 | ||
|
|
cb956c5508 | ||
|
|
47b0086bf8 | ||
|
|
3c14cc219e | ||
|
|
b8d66e4a95 | ||
|
|
7804a22984 | ||
|
|
bfc1c93153 | ||
|
|
5bf3824f28 | ||
|
|
dac23e38d9 | ||
|
|
404faf8a0b | ||
|
|
4a7b0f4711 | ||
|
|
dd62fca45d | ||
|
|
79fb04126c | ||
|
|
39c9574ae3 | ||
|
|
38af257adf | ||
|
|
5aae9a4722 | ||
|
|
cfe3cae88d | ||
|
|
612de84ac6 | ||
|
|
33be597ef0 | ||
|
|
cc26fd80d7 | ||
|
|
c227a1ffec | ||
|
|
f0df787bbe | ||
|
|
09188bed48 | ||
|
|
4a3bcaba06 | ||
|
|
1d1ab65edd | ||
|
|
7330cdaf1c | ||
|
|
050a1fb6cf | ||
|
|
1e8397cf17 | ||
|
|
59b53ece2b | ||
|
|
16c62cd46f | ||
|
|
eff56c2514 | ||
|
|
ee6b9a223f | ||
|
|
acc6ea434a | ||
|
|
1e5a7356f4 | ||
|
|
4c8342c19d | ||
|
|
dad5232ecb | ||
|
|
be972781ee | ||
|
|
58fbc298b1 | ||
|
|
7de7772339 | ||
|
|
ad847a2f5d | ||
|
|
4307db11c5 | ||
|
|
4f79f52524 | ||
|
|
83f8151ca4 | ||
|
|
2c7f24cb8c | ||
|
|
398ebae2ba | ||
|
|
5de176757d | ||
|
|
342575a576 | ||
|
|
785272540e | ||
|
|
82178055af |
@@ -22,7 +22,9 @@
|
||||
"fontSize": "14",
|
||||
"lineNumber": true
|
||||
},
|
||||
"sortBy": "UPDATED_AT",
|
||||
"sortBy": {
|
||||
"default": "UPDATED_AT"
|
||||
},
|
||||
"sortTagsBy": "ALPHABETICAL",
|
||||
"ui": {
|
||||
"defaultNote": "ALWAYS_ASK",
|
||||
|
||||
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Space indentation
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# The indent size used in the `package.json` file cannot be changed
|
||||
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
|
||||
[{*.yml,*.yaml,package.json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
41
.vscode/launch.json
vendored
Normal file
41
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "BoostNote Main",
|
||||
"protocol": "inspector",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9223",
|
||||
"--hot",
|
||||
"${workspaceFolder}/index.js"
|
||||
],
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modeules/.bin/electron.cmd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"name": "BoostNote Renderer",
|
||||
"port": 9223,
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///./~/*": "${webRoot}/node_modules/*",
|
||||
"webpack:///*": "${webRoot}/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "BostNote All",
|
||||
"configurations": ["BoostNote Main", "BoostNote Renderer"]
|
||||
}
|
||||
]
|
||||
}
|
||||
27
.vscode/tasks.json
vendored
Normal file
27
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build Boostnote",
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
},
|
||||
"problemMatcher": {
|
||||
"pattern":[
|
||||
{
|
||||
"regexp": "^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
# Current behavior
|
||||
|
||||
<!--
|
||||
Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug.
|
||||
Let us know what is currently happening.
|
||||
|
||||
If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile.
|
||||
Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug.
|
||||
|
||||
If your issue is regarding Boostnote mobile, please open an issue in the Boostnote Mobile repo 👉 https://github.com/BoostIO/boostnote-mobile.
|
||||
-->
|
||||
|
||||
# Expected behavior
|
||||
|
||||
<!--
|
||||
Let us know what you think should happen!
|
||||
-->
|
||||
|
||||
# Steps to reproduce
|
||||
|
||||
<!--
|
||||
Please be thorough, issues we can reproduce are easier to fix!
|
||||
-->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
36
PULL_REQUEST_TEMPLATE.md
Normal file
36
PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
Before submitting this PR, please make sure that:
|
||||
- You have read and understand the contributing.md
|
||||
- You have checked docs/code_style.md for information on code style
|
||||
-->
|
||||
## Description
|
||||
<!--
|
||||
Tell us what your PR does.
|
||||
Please attach a screenshot/ video/gif image describing your PR if possible.
|
||||
-->
|
||||
|
||||
## Issue fixed
|
||||
<!--
|
||||
Please list out all issue fixed with this PR here.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Please make sure you fill in these checkboxes,
|
||||
your PR will be reviewed faster if we know exactly what it does.
|
||||
|
||||
Change :white_circle: to :radio_button: in all the options that apply
|
||||
-->
|
||||
## Type of changes
|
||||
|
||||
- :white_circle: Bug fix (Change that fixed an issue)
|
||||
- :white_circle: Breaking change (Change that can cause existing functionality to change)
|
||||
- :white_circle: Improvement (Change that improves the code. Maybe performance or development improvement)
|
||||
- :white_circle: Feature (Change that adds new functionality)
|
||||
- :white_circle: Documentation change (Change that modifies documentation. Maybe typo fixes)
|
||||
|
||||
## Checklist:
|
||||
|
||||
- :white_circle: My code follows [the project code style](docs/code_style.md)
|
||||
- :white_circle: I have written test for my code and it has been tested
|
||||
- :white_circle: All existing tests have been passed
|
||||
- :white_circle: I have attached a screenshot/video to visualize my change if possible
|
||||
@@ -5,27 +5,39 @@ import CodeMirror from 'codemirror'
|
||||
import 'codemirror-mode-elixir'
|
||||
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||
import convertModeName from 'browser/lib/convertModeName'
|
||||
import { options, TableEditor } from '@susisu/mte-kernel'
|
||||
import { options, TableEditor, Alignment } from '@susisu/mte-kernel'
|
||||
import TextEditorInterface from 'browser/lib/TextEditorInterface'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import iconv from 'iconv-lite'
|
||||
import crypto from 'crypto'
|
||||
import consts from 'browser/lib/consts'
|
||||
import styles from '../components/CodeEditor.styl'
|
||||
import fs from 'fs'
|
||||
const { ipcRenderer } = require('electron')
|
||||
const { ipcRenderer, remote, clipboard } = require('electron')
|
||||
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
||||
const spellcheck = require('browser/lib/spellcheck')
|
||||
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
|
||||
import TurndownService from 'turndown'
|
||||
import { gfm } from 'turndown-plugin-gfm'
|
||||
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
const buildCMRulers = (rulers, enableRulers) =>
|
||||
enableRulers ? rulers.map(ruler => ({column: ruler})) : []
|
||||
(enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
|
||||
|
||||
function translateHotkey (hotkey) {
|
||||
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||
this.changeHandler = (e) => this.handleChange(e)
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
|
||||
leading: false,
|
||||
trailing: true
|
||||
})
|
||||
this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
|
||||
this.focusHandler = () => {
|
||||
ipcRenderer.send('editor:focused', true)
|
||||
}
|
||||
@@ -41,17 +53,39 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
this.props.onBlur != null && this.props.onBlur(e)
|
||||
|
||||
const {storageKey, noteKey} = this.props
|
||||
attachmentManagement.deleteAttachmentsNotPresentInNote(this.editor.getValue(), storageKey, noteKey)
|
||||
const { storageKey, noteKey } = this.props
|
||||
attachmentManagement.deleteAttachmentsNotPresentInNote(
|
||||
this.editor.getValue(),
|
||||
storageKey,
|
||||
noteKey
|
||||
)
|
||||
}
|
||||
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
|
||||
this.loadStyleHandler = (e) => {
|
||||
this.pasteHandler = (editor, e) => {
|
||||
e.preventDefault()
|
||||
|
||||
this.handlePaste(editor, false)
|
||||
}
|
||||
this.loadStyleHandler = e => {
|
||||
this.editor.refresh()
|
||||
}
|
||||
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||
this.searchState = null
|
||||
this.scrollToLineHandeler = this.scrollToLine.bind(this)
|
||||
|
||||
this.formatTable = () => this.handleFormatTable()
|
||||
|
||||
if (props.switchPreview !== 'RIGHTCLICK') {
|
||||
this.contextMenuHandler = function (editor, event) {
|
||||
const menu = buildEditorContextMenu(editor, event)
|
||||
if (menu != null) {
|
||||
setTimeout(() => menu.popup(remote.getCurrentWindow()), 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.editorActivityHandler = () => this.handleEditorActivity()
|
||||
|
||||
this.turndownService = new TurndownService()
|
||||
}
|
||||
|
||||
handleSearch (msg) {
|
||||
@@ -66,7 +100,10 @@ export default class CodeEditor extends React.Component {
|
||||
cm.addOverlay(component.searchState)
|
||||
|
||||
function makeOverlay (query, style) {
|
||||
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi')
|
||||
query = new RegExp(
|
||||
query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
|
||||
'gi'
|
||||
)
|
||||
return {
|
||||
token: function (stream) {
|
||||
query.lastIndex = stream.pos
|
||||
@@ -89,9 +126,92 @@ export default class CodeEditor extends React.Component {
|
||||
this.tableEditor.formatAll(options({textWidthOptions: {}}))
|
||||
}
|
||||
|
||||
handleEditorActivity () {
|
||||
if (!this.textEditorInterface.transaction) {
|
||||
this.updateTableEditorState()
|
||||
}
|
||||
}
|
||||
|
||||
updateDefaultKeyMap () {
|
||||
const { hotkey } = this.props
|
||||
const expandSnippet = this.expandSnippet.bind(this)
|
||||
|
||||
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
|
||||
Tab: function (cm) {
|
||||
const cursor = cm.getCursor()
|
||||
const line = cm.getLine(cursor.line)
|
||||
const cursorPosition = cursor.ch
|
||||
const charBeforeCursor = line.substr(cursorPosition - 1, 1)
|
||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||
else {
|
||||
const tabs = cm.getOption('indentWithTabs')
|
||||
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
|
||||
cm.execCommand('goLineStart')
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
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 {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
},
|
||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': cm => {
|
||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||
document.execCommand('copy')
|
||||
}
|
||||
return CodeMirror.Pass
|
||||
},
|
||||
[translateHotkey(hotkey.pasteSmartly)]: cm => {
|
||||
this.handlePaste(cm, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateTableEditorState () {
|
||||
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
|
||||
if (active) {
|
||||
if (this.extraKeysMode !== 'editor') {
|
||||
this.extraKeysMode = 'editor'
|
||||
this.editor.setOption('extraKeys', this.editorKeyMap)
|
||||
}
|
||||
} else {
|
||||
if (this.extraKeysMode !== 'default') {
|
||||
this.extraKeysMode = 'default'
|
||||
this.editor.setOption('extraKeys', this.defaultKeyMap)
|
||||
this.tableEditor.resetSmartCursor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { rulers, enableRulers } = this.props
|
||||
const expandSnippet = this.expandSnippet.bind(this)
|
||||
eventEmitter.on('line:jump', this.scrollToLineHandeler)
|
||||
|
||||
const defaultSnippet = [
|
||||
{
|
||||
@@ -102,9 +222,15 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
]
|
||||
if (!fs.existsSync(consts.SNIPPET_FILE)) {
|
||||
fs.writeFileSync(consts.SNIPPET_FILE, JSON.stringify(defaultSnippet, null, 4), 'utf8')
|
||||
fs.writeFileSync(
|
||||
consts.SNIPPET_FILE,
|
||||
JSON.stringify(defaultSnippet, null, 4),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
|
||||
this.updateDefaultKeyMap()
|
||||
|
||||
this.value = this.props.value
|
||||
this.editor = CodeMirror(this.refs.root, {
|
||||
rulers: buildCMRulers(rulers, enableRulers),
|
||||
@@ -127,53 +253,7 @@ export default class CodeEditor extends React.Component {
|
||||
explode: '[]{}``$$',
|
||||
override: true
|
||||
},
|
||||
extraKeys: {
|
||||
Tab: function (cm) {
|
||||
const cursor = cm.getCursor()
|
||||
const line = cm.getLine(cursor.line)
|
||||
const cursorPosition = cursor.ch
|
||||
const charBeforeCursor = line.substr(cursorPosition - 1, 1)
|
||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||
else {
|
||||
const tabs = cm.getOption('indentWithTabs')
|
||||
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
|
||||
cm.execCommand('goLineStart')
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
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 {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
},
|
||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': (cm) => {
|
||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||
document.execCommand('copy')
|
||||
}
|
||||
return CodeMirror.Pass
|
||||
}
|
||||
}
|
||||
extraKeys: this.defaultKeyMap
|
||||
})
|
||||
|
||||
this.setMode(this.props.mode)
|
||||
@@ -182,6 +262,9 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.on('blur', this.blurHandler)
|
||||
this.editor.on('change', this.changeHandler)
|
||||
this.editor.on('paste', this.pasteHandler)
|
||||
if (this.props.switchPreview !== 'RIGHTCLICK') {
|
||||
this.editor.on('contextmenu', this.contextMenuHandler)
|
||||
}
|
||||
eventEmitter.on('top:search', this.searchHandler)
|
||||
|
||||
eventEmitter.emit('code:init')
|
||||
@@ -196,12 +279,74 @@ export default class CodeEditor extends React.Component {
|
||||
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
|
||||
CodeMirror.Vim.map('ZZ', ':q', 'normal')
|
||||
|
||||
this.tableEditor = new TableEditor(new TextEditorInterface(this.editor))
|
||||
this.textEditorInterface = new TextEditorInterface(this.editor)
|
||||
this.tableEditor = new TableEditor(this.textEditorInterface)
|
||||
if (this.props.spellCheck) {
|
||||
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
|
||||
}
|
||||
|
||||
eventEmitter.on('code:format-table', this.formatTable)
|
||||
|
||||
this.tableEditorOptions = options({
|
||||
smartCursor: true
|
||||
})
|
||||
|
||||
this.editorKeyMap = CodeMirror.normalizeKeyMap({
|
||||
'Tab': () => { this.tableEditor.nextCell(this.tableEditorOptions) },
|
||||
'Shift-Tab': () => { this.tableEditor.previousCell(this.tableEditorOptions) },
|
||||
'Enter': () => { this.tableEditor.nextRow(this.tableEditorOptions) },
|
||||
'Ctrl-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) },
|
||||
'Cmd-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) },
|
||||
'Ctrl-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) },
|
||||
'Cmd-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) },
|
||||
'Ctrl-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) },
|
||||
'Cmd-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) },
|
||||
'Ctrl-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) },
|
||||
'Cmd-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) },
|
||||
'Ctrl-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) },
|
||||
'Cmd-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) },
|
||||
'Ctrl-K Ctrl-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) },
|
||||
'Cmd-K Cmd-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) },
|
||||
'Ctrl-L Ctrl-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) },
|
||||
'Cmd-L Cmd-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) },
|
||||
'Ctrl-K Ctrl-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) },
|
||||
'Cmd-K Cmd-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) },
|
||||
'Ctrl-L Ctrl-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) },
|
||||
'Cmd-L Cmd-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) }
|
||||
})
|
||||
|
||||
if (this.props.enableTableEditor) {
|
||||
this.editor.on('cursorActivity', this.editorActivityHandler)
|
||||
this.editor.on('changes', this.editorActivityHandler)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
clientWidth: this.refs.root.clientWidth
|
||||
})
|
||||
}
|
||||
|
||||
expandSnippet (line, cursor, cm, snippets) {
|
||||
const wordBeforeCursor = this.getWordBeforeCursor(line, cursor.line, cursor.ch)
|
||||
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) {
|
||||
@@ -209,19 +354,28 @@ export default class CodeEditor extends React.Component {
|
||||
const snippetLines = snippets[i].content.split('\n')
|
||||
let cursorLineNumber = 0
|
||||
let cursorLinePosition = 0
|
||||
|
||||
let cursorIndex
|
||||
for (let j = 0; j < snippetLines.length; j++) {
|
||||
const cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||
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 })
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cm.replaceRange(
|
||||
snippets[i].content.replace(templateCursorString, ''),
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
cm.setCursor({
|
||||
line: cursor.line + cursorLineNumber,
|
||||
ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
|
||||
})
|
||||
} else {
|
||||
cm.replaceRange(
|
||||
snippets[i].content,
|
||||
@@ -261,8 +415,8 @@ export default class CodeEditor extends React.Component {
|
||||
return {
|
||||
text: wordBeforeCursor,
|
||||
range: {
|
||||
from: {line: lineNumber, ch: originCursorPosition},
|
||||
to: {line: lineNumber, ch: cursorPosition}
|
||||
from: { line: lineNumber, ch: originCursorPosition },
|
||||
to: { line: lineNumber, ch: cursorPosition }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,15 +432,17 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.off('paste', this.pasteHandler)
|
||||
eventEmitter.off('top:search', this.searchHandler)
|
||||
this.editor.off('scroll', this.scrollHandler)
|
||||
this.editor.off('contextmenu', this.contextMenuHandler)
|
||||
const editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||
|
||||
spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED)
|
||||
eventEmitter.off('code:format-table', this.formatTable)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
let needRefresh = false
|
||||
const {rulers, enableRulers} = this.props
|
||||
const { rulers, enableRulers } = this.props
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.setMode(this.props.mode)
|
||||
}
|
||||
@@ -304,7 +460,10 @@ export default class CodeEditor extends React.Component {
|
||||
needRefresh = true
|
||||
}
|
||||
|
||||
if (prevProps.enableRulers !== enableRulers || prevProps.rulers !== rulers) {
|
||||
if (
|
||||
prevProps.enableRulers !== enableRulers ||
|
||||
prevProps.rulers !== rulers
|
||||
) {
|
||||
this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
|
||||
}
|
||||
|
||||
@@ -324,6 +483,45 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
|
||||
}
|
||||
|
||||
if (prevProps.enableTableEditor !== this.props.enableTableEditor) {
|
||||
if (this.props.enableTableEditor) {
|
||||
this.editor.on('cursorActivity', this.editorActivityHandler)
|
||||
this.editor.on('changes', this.editorActivityHandler)
|
||||
} else {
|
||||
this.editor.off('cursorActivity', this.editorActivityHandler)
|
||||
this.editor.off('changes', this.editorActivityHandler)
|
||||
}
|
||||
|
||||
this.extraKeysMode = 'default'
|
||||
this.editor.setOption('extraKeys', this.defaultKeyMap)
|
||||
}
|
||||
|
||||
if (prevProps.hotkey !== this.props.hotkey) {
|
||||
this.updateDefaultKeyMap()
|
||||
|
||||
if (this.extraKeysMode === 'default') {
|
||||
this.editor.setOption('extraKeys', this.defaultKeyMap)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.clientWidth !== this.refs.root.clientWidth) {
|
||||
this.setState({
|
||||
clientWidth: this.refs.root.clientWidth
|
||||
})
|
||||
|
||||
needRefresh = true
|
||||
}
|
||||
|
||||
if (prevProps.spellCheck !== this.props.spellCheck) {
|
||||
if (this.props.spellCheck === false) {
|
||||
spellcheck.setLanguage(this.editor, spellcheck.SPELLCHECK_DISABLED)
|
||||
let elem = document.getElementById('editor-bottom-panel')
|
||||
elem.parentNode.removeChild(elem)
|
||||
} else {
|
||||
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
|
||||
}
|
||||
}
|
||||
|
||||
if (needRefresh) {
|
||||
this.editor.refresh()
|
||||
}
|
||||
@@ -337,17 +535,22 @@ export default class CodeEditor extends React.Component {
|
||||
CodeMirror.autoLoadMode(this.editor, syntax.mode)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.value = this.editor.getValue()
|
||||
handleChange (editor, changeObject) {
|
||||
spellcheck.handleChange(editor, changeObject)
|
||||
this.value = editor.getValue()
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(e)
|
||||
this.props.onChange(editor)
|
||||
}
|
||||
}
|
||||
|
||||
moveCursorTo (row, col) {
|
||||
}
|
||||
moveCursorTo (row, col) {}
|
||||
|
||||
scrollToLine (num) {
|
||||
scrollToLine (event, num) {
|
||||
const cursor = {
|
||||
line: num,
|
||||
ch: 1
|
||||
}
|
||||
this.editor.setCursor(cursor)
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -376,47 +579,108 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
handleDropImage (dropEvent) {
|
||||
dropEvent.preventDefault()
|
||||
const {storageKey, noteKey} = this.props
|
||||
attachmentManagement.handleAttachmentDrop(this, storageKey, noteKey, dropEvent)
|
||||
const { storageKey, noteKey } = this.props
|
||||
attachmentManagement.handleAttachmentDrop(
|
||||
this,
|
||||
storageKey,
|
||||
noteKey,
|
||||
dropEvent
|
||||
)
|
||||
}
|
||||
|
||||
insertAttachmentMd (imageMd) {
|
||||
this.editor.replaceSelection(imageMd)
|
||||
}
|
||||
|
||||
handlePaste (editor, e) {
|
||||
const clipboardData = e.clipboardData
|
||||
const {storageKey, noteKey} = this.props
|
||||
const dataTransferItem = clipboardData.items[0]
|
||||
const pastedTxt = clipboardData.getData('text')
|
||||
const isURL = (str) => {
|
||||
handlePaste (editor, forceSmartPaste) {
|
||||
const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
|
||||
|
||||
const isURL = str => {
|
||||
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
||||
return matcher.test(str)
|
||||
}
|
||||
const isInLinkTag = (editor) => {
|
||||
|
||||
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}
|
||||
{ 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}
|
||||
{ line: endCursor.line, ch: endCursor.ch },
|
||||
{ line: endCursor.line, ch: endCursor.ch + 1 }
|
||||
)
|
||||
return prevChar === '](' && nextChar === ')'
|
||||
}
|
||||
if (dataTransferItem.type.match('image')) {
|
||||
attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem)
|
||||
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
this.handlePasteUrl(e, editor, pastedTxt)
|
||||
|
||||
const isInFencedCodeBlock = editor => {
|
||||
const cursor = editor.getCursor()
|
||||
|
||||
let token = editor.getTokenAt(cursor)
|
||||
if (token.state.fencedState) {
|
||||
return true
|
||||
}
|
||||
|
||||
let line = line = cursor.line - 1
|
||||
while (line >= 0) {
|
||||
token = editor.getTokenAt({
|
||||
ch: 3,
|
||||
line
|
||||
})
|
||||
|
||||
if (token.start === token.end) {
|
||||
--line
|
||||
} else if (token.type === 'comment') {
|
||||
if (line > 0) {
|
||||
token = editor.getTokenAt({
|
||||
ch: 3,
|
||||
line: line - 1
|
||||
})
|
||||
|
||||
return token.type !== 'comment'
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||
attachmentManagement.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
|
||||
.then((modifiedText) => {
|
||||
|
||||
const pastedTxt = clipboard.readText()
|
||||
|
||||
if (isInFencedCodeBlock(editor)) {
|
||||
this.handlePasteText(editor, pastedTxt)
|
||||
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
this.handlePasteUrl(editor, pastedTxt)
|
||||
} else if (enableSmartPaste || forceSmartPaste) {
|
||||
const image = clipboard.readImage()
|
||||
if (!image.isEmpty()) {
|
||||
attachmentManagement.handlePastNativeImage(
|
||||
this,
|
||||
storageKey,
|
||||
noteKey,
|
||||
image
|
||||
)
|
||||
} else {
|
||||
const pastedHtml = clipboard.readHTML()
|
||||
if (pastedHtml.length > 0) {
|
||||
this.handlePasteHtml(editor, pastedHtml)
|
||||
} else {
|
||||
this.handlePasteText(editor, pastedTxt)
|
||||
}
|
||||
}
|
||||
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||
attachmentManagement
|
||||
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
|
||||
.then(modifiedText => {
|
||||
this.editor.replaceSelection(modifiedText)
|
||||
})
|
||||
e.preventDefault()
|
||||
} else {
|
||||
this.handlePasteText(editor, pastedTxt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,45 +690,66 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handlePasteUrl (e, editor, pastedTxt) {
|
||||
e.preventDefault()
|
||||
handlePasteUrl (editor, pastedTxt) {
|
||||
const taggedUrl = `<${pastedTxt}>`
|
||||
editor.replaceSelection(taggedUrl)
|
||||
|
||||
const isImageReponse = (response) => {
|
||||
return response.headers.has('content-type') &&
|
||||
const isImageReponse = response => {
|
||||
return (
|
||||
response.headers.has('content-type') &&
|
||||
response.headers.get('content-type').match(/^image\/.+$/)
|
||||
)
|
||||
}
|
||||
const replaceTaggedUrl = (replacement) => {
|
||||
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 })
|
||||
const newCursor = Object.assign({}, cursor, {
|
||||
ch: cursor.ch + newValue.length - value.length
|
||||
})
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(newCursor)
|
||||
}
|
||||
|
||||
fetch(pastedTxt, {
|
||||
method: 'get'
|
||||
}).then((response) => {
|
||||
if (isImageReponse(response)) {
|
||||
return this.mapImageResponse(response, pastedTxt)
|
||||
} else {
|
||||
return this.mapNormalResponse(response, pastedTxt)
|
||||
}
|
||||
}).then((replacement) => {
|
||||
replaceTaggedUrl(replacement)
|
||||
}).catch((e) => {
|
||||
replaceTaggedUrl(pastedTxt)
|
||||
})
|
||||
.then(response => {
|
||||
if (isImageReponse(response)) {
|
||||
return this.mapImageResponse(response, pastedTxt)
|
||||
} else {
|
||||
return this.mapNormalResponse(response, pastedTxt)
|
||||
}
|
||||
})
|
||||
.then(replacement => {
|
||||
replaceTaggedUrl(replacement)
|
||||
})
|
||||
.catch(e => {
|
||||
replaceTaggedUrl(pastedTxt)
|
||||
})
|
||||
}
|
||||
|
||||
handlePasteHtml (editor, pastedHtml) {
|
||||
const markdown = this.turndownService.turndown(pastedHtml)
|
||||
editor.replaceSelection(markdown)
|
||||
}
|
||||
|
||||
handlePasteText (editor, pastedTxt) {
|
||||
editor.replaceSelection(pastedTxt)
|
||||
}
|
||||
|
||||
mapNormalResponse (response, pastedTxt) {
|
||||
return this.decodeResponse(response).then((body) => {
|
||||
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})`
|
||||
const parsedBody = new window.DOMParser().parseFromString(
|
||||
body,
|
||||
'text/html'
|
||||
)
|
||||
const escapePipe = (str) => {
|
||||
return str.replace('|', '\\|')
|
||||
}
|
||||
const linkWithTitle = `[${escapePipe(parsedBody.title)}](${pastedTxt})`
|
||||
resolve(linkWithTitle)
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
@@ -491,10 +776,13 @@ export default class CodeEditor extends React.Component {
|
||||
const _charset = headers.has('content-type')
|
||||
? this.extractContentTypeCharset(headers.get('content-type'))
|
||||
: undefined
|
||||
return response.arrayBuffer().then((buff) => {
|
||||
return response.arrayBuffer().then(buff => {
|
||||
return new Promise((resolve, reject) => {
|
||||
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())
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
@@ -504,11 +792,14 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
extractContentTypeCharset (contentType) {
|
||||
return contentType.split(';').filter((str) => {
|
||||
return str.trim().toLowerCase().startsWith('charset')
|
||||
}).map((str) => {
|
||||
return str.replace(/['"]/g, '').split('=')[1]
|
||||
})[0]
|
||||
return contentType
|
||||
.split(';')
|
||||
.filter(str => {
|
||||
return str.trim().toLowerCase().startsWith('charset')
|
||||
})
|
||||
.map(str => {
|
||||
return str.replace(/['"]/g, '').split('=')[1]
|
||||
})[0]
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -517,10 +808,7 @@ export default class CodeEditor extends React.Component {
|
||||
const width = this.props.width
|
||||
return (
|
||||
<div
|
||||
className={className == null
|
||||
? 'CodeEditor'
|
||||
: `CodeEditor ${className}`
|
||||
}
|
||||
className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
|
||||
ref='root'
|
||||
tabIndex='-1'
|
||||
style={{
|
||||
@@ -528,10 +816,29 @@ export default class CodeEditor extends React.Component {
|
||||
fontSize: fontSize,
|
||||
width: width
|
||||
}}
|
||||
onDrop={(e) => this.handleDropImage(e)}
|
||||
onDrop={e => this.handleDropImage(e)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
createSpellCheckPanel () {
|
||||
const panel = document.createElement('div')
|
||||
panel.className = 'panel bottom'
|
||||
panel.id = 'editor-bottom-panel'
|
||||
const dropdown = document.createElement('select')
|
||||
dropdown.title = 'Spellcheck'
|
||||
dropdown.className = styles['spellcheck-select']
|
||||
dropdown.addEventListener('change', (e) => spellcheck.setLanguage(this.editor, dropdown.value))
|
||||
const options = spellcheck.getAvailableDictionaries()
|
||||
for (const op of options) {
|
||||
const option = document.createElement('option')
|
||||
option.value = op.value
|
||||
option.innerHTML = op.label
|
||||
dropdown.appendChild(option)
|
||||
}
|
||||
panel.appendChild(dropdown)
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
@@ -542,7 +849,8 @@ CodeEditor.propTypes = {
|
||||
className: PropTypes.string,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
readOnly: PropTypes.bool
|
||||
readOnly: PropTypes.bool,
|
||||
spellCheck: PropTypes.bool
|
||||
}
|
||||
|
||||
CodeEditor.defaultProps = {
|
||||
@@ -552,5 +860,6 @@ CodeEditor.defaultProps = {
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space'
|
||||
indentType: 'space',
|
||||
spellCheck: false
|
||||
}
|
||||
|
||||
6
browser/components/CodeEditor.styl
Normal file
6
browser/components/CodeEditor.styl
Normal file
@@ -0,0 +1,6 @@
|
||||
.codeEditor-typo
|
||||
text-decoration underline wavy red
|
||||
|
||||
.spellcheck-select
|
||||
border: none
|
||||
text-decoration underline wavy red
|
||||
@@ -6,6 +6,7 @@ import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
|
||||
class MarkdownEditor extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -18,7 +19,7 @@ class MarkdownEditor extends React.Component {
|
||||
this.supportMdSelectionBold = [16, 17, 186]
|
||||
|
||||
this.state = {
|
||||
status: 'PREVIEW',
|
||||
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW',
|
||||
renderValue: props.value,
|
||||
keyPressed: new Set(),
|
||||
isLocked: false
|
||||
@@ -64,6 +65,10 @@ class MarkdownEditor extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
setValue (value) {
|
||||
this.refs.code.setValue(value)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.value = this.refs.code.value
|
||||
this.props.onChange(e)
|
||||
@@ -72,9 +77,7 @@ class MarkdownEditor extends React.Component {
|
||||
handleContextMenu (e) {
|
||||
const { config } = this.props
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||
const newStatus = this.state.status === 'PREVIEW'
|
||||
? 'CODE'
|
||||
: 'PREVIEW'
|
||||
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
|
||||
this.setState({
|
||||
status: newStatus
|
||||
}, () => {
|
||||
@@ -84,6 +87,10 @@ class MarkdownEditor extends React.Component {
|
||||
this.refs.preview.focus()
|
||||
}
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
|
||||
const newConfig = Object.assign({}, config)
|
||||
newConfig.editor.delfaultStatus = newStatus
|
||||
ConfigManager.set(newConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,8 +147,10 @@ class MarkdownEditor extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /\[x\]/i
|
||||
const uncheckedMatch = /\[ \]/
|
||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||
const checkReplace = /\[x\]/i
|
||||
const uncheckReplace = /\[ \]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
@@ -150,10 +159,10 @@ class MarkdownEditor extends React.Component {
|
||||
const targetLine = lines[lineIndex]
|
||||
|
||||
if (targetLine.match(checkedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
|
||||
}
|
||||
if (targetLine.match(uncheckedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
||||
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
|
||||
}
|
||||
this.refs.code.setValue(lines.join('\n'))
|
||||
}
|
||||
@@ -250,7 +259,7 @@ class MarkdownEditor extends React.Component {
|
||||
: 'codeEditor--hide'
|
||||
}
|
||||
ref='code'
|
||||
mode='GitHub Flavored Markdown'
|
||||
mode='Boost Flavored Markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
@@ -265,8 +274,13 @@ class MarkdownEditor extends React.Component {
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
spellCheck={config.editor.spellcheck}
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
switchPreview={config.editor.switchPreview}
|
||||
/>
|
||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||
? 'preview'
|
||||
@@ -299,6 +313,7 @@ class MarkdownEditor extends React.Component {
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
.preview
|
||||
display block
|
||||
absolute top bottom left right
|
||||
z-index 100
|
||||
background-color white
|
||||
height 100%
|
||||
width 100%
|
||||
|
||||
@@ -17,8 +17,13 @@ import copy from 'copy-to-clipboard'
|
||||
import mdurl from 'mdurl'
|
||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||
import yaml from 'js-yaml'
|
||||
import context from 'browser/lib/context'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import fs from 'fs'
|
||||
import ConfigManager from '../main/lib/ConfigManager'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { remote, shell } = require('electron')
|
||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||
|
||||
const { app } = remote
|
||||
@@ -27,16 +32,27 @@ const fileUrl = require('file-url')
|
||||
|
||||
const dialog = remote.dialog
|
||||
|
||||
const uri2path = require('file-uri-to-path')
|
||||
|
||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||
const appPath = fileUrl(process.env.NODE_ENV === 'production'
|
||||
? app.getAppPath()
|
||||
: path.resolve())
|
||||
const appPath = fileUrl(
|
||||
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
|
||||
)
|
||||
const CSS_FILES = [
|
||||
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
||||
]
|
||||
|
||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) {
|
||||
function buildStyle (
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
) {
|
||||
return `
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
@@ -66,7 +82,6 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scro
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||
}
|
||||
${allowCustomCSS ? customCSS : ''}
|
||||
${markdownStyle}
|
||||
|
||||
body {
|
||||
@@ -74,6 +89,11 @@ body {
|
||||
font-size: ${fontSize}px;
|
||||
${scrollPastEnd && 'padding-bottom: 90vh;'}
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
padding-bottom: initial;
|
||||
}
|
||||
}
|
||||
code {
|
||||
font-family: '${codeBlockFontFamily.join("','")}';
|
||||
background-color: rgba(0,0,0,0.04);
|
||||
@@ -130,6 +150,8 @@ body p {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
${allowCustomCSS ? customCSS : ''}
|
||||
`
|
||||
}
|
||||
|
||||
@@ -152,7 +174,6 @@ const scrollBarDarkStyle = `
|
||||
}
|
||||
`
|
||||
|
||||
const { shell } = require('electron')
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||
@@ -160,17 +181,27 @@ if (!OSX) {
|
||||
defaultFontFamily.unshift('Microsoft YaHei')
|
||||
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 {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||
this.contextMenuHandler = e => this.handleContextMenu(e)
|
||||
this.mouseDownHandler = e => this.handleMouseDown(e)
|
||||
this.mouseUpHandler = e => this.handleMouseUp(e)
|
||||
this.DoubleClickHandler = e => this.handleDoubleClick(e)
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
|
||||
leading: false,
|
||||
trailing: true
|
||||
})
|
||||
this.checkboxClickHandler = e => this.handleCheckboxClick(e)
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||
@@ -200,8 +231,32 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
this.props.onContextMenu(e)
|
||||
handleContextMenu (event) {
|
||||
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return
|
||||
if (_.isFunction(this.props.onContextMenu)) {
|
||||
this.props.onContextMenu(event)
|
||||
return
|
||||
}
|
||||
// No contextMenu was passed to us -> execute our own link-opener
|
||||
if (event.target.tagName.toLowerCase() === 'a') {
|
||||
const href = event.target.href
|
||||
const isLocalFile = href.startsWith('file:')
|
||||
if (isLocalFile) {
|
||||
const absPath = uri2path(href)
|
||||
try {
|
||||
if (fs.lstatSync(absPath).isFile()) {
|
||||
context.popup([
|
||||
{
|
||||
label: i18n.__('Show in explorer'),
|
||||
click: (e) => shell.showItemInFolder(absPath)
|
||||
}
|
||||
])
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error while evaluating if the file is locally available', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDoubleClick (e) {
|
||||
@@ -209,6 +264,10 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
const config = ConfigManager.get()
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
|
||||
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
||||
}
|
||||
if (e.target != null) {
|
||||
switch (e.target.tagName) {
|
||||
case 'A':
|
||||
@@ -237,31 +296,44 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
handleSaveAsHtml () {
|
||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
||||
|
||||
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
||||
let body = this.markdown.render(escapeHtmlCharacters(noteContent, { detectCodeBlock: true }))
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = this.getStyleParams()
|
||||
|
||||
const inlineStyles = buildStyle(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
)
|
||||
let body = this.markdown.render(noteContent)
|
||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath)
|
||||
|
||||
files.forEach((file) => {
|
||||
file = file.replace('file://', '')
|
||||
files.forEach(file => {
|
||||
if (global.process.platform === 'win32') {
|
||||
file = file.replace('file:///', '')
|
||||
} else {
|
||||
file = file.replace('file://', '')
|
||||
}
|
||||
exportTasks.push({
|
||||
src: file,
|
||||
dst: 'css'
|
||||
})
|
||||
})
|
||||
attachmentsAbsolutePaths.forEach((attachment) => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey)
|
||||
|
||||
let styles = ''
|
||||
files.forEach((file) => {
|
||||
files.forEach(file => {
|
||||
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||
})
|
||||
|
||||
@@ -283,48 +355,54 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
exportAsDocument (fileType, contentFormatter) {
|
||||
const options = {
|
||||
filters: [
|
||||
{name: 'Documents', extensions: [fileType]}
|
||||
],
|
||||
filters: [{ name: 'Documents', extensions: [fileType] }],
|
||||
properties: ['openFile', 'createDirectory']
|
||||
}
|
||||
|
||||
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
||||
(filename) => {
|
||||
if (filename) {
|
||||
const content = this.props.value
|
||||
const storage = this.props.storagePath
|
||||
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
|
||||
if (filename) {
|
||||
const content = this.props.value
|
||||
const storage = this.props.storagePath
|
||||
const nodeKey = this.props.noteKey
|
||||
|
||||
exportNote(storage, content, filename, contentFormatter)
|
||||
.then((res) => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
|
||||
}).catch((err) => {
|
||||
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
exportNote(nodeKey, storage, content, filename, contentFormatter)
|
||||
.then(res => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: `Exported to ${filename}`
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
dialog.showErrorBox(
|
||||
'Export error',
|
||||
err ? err.message || err : 'Unexpected error during export'
|
||||
)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
node.innerText = mdurl.decode(href) === innerText
|
||||
? href
|
||||
: innerText
|
||||
node.innerText = mdurl.decode(href) === innerText ? href : innerText
|
||||
}
|
||||
}
|
||||
|
||||
getScrollBarStyle () {
|
||||
const {
|
||||
theme
|
||||
} = this.props
|
||||
const { theme } = this.props
|
||||
|
||||
switch (theme) {
|
||||
case 'dark':
|
||||
case 'solarized-dark':
|
||||
case 'monokai':
|
||||
case 'dracula':
|
||||
return scrollBarDarkStyle
|
||||
default:
|
||||
return scrollBarStyle
|
||||
@@ -333,7 +411,10 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
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 = `
|
||||
<style id='style'></style>
|
||||
@@ -344,7 +425,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
</style>
|
||||
`
|
||||
|
||||
CSS_FILES.forEach((file) => {
|
||||
CSS_FILES.forEach(file => {
|
||||
styles += `<link rel="stylesheet" href="${file}">`
|
||||
})
|
||||
|
||||
@@ -352,12 +433,30 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.rewriteIframe()
|
||||
this.applyStyle()
|
||||
|
||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'mousedown',
|
||||
this.mouseDownHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'mouseup',
|
||||
this.mouseUpHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'dblclick',
|
||||
this.DoubleClickHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'drop',
|
||||
this.preventImageDroppedHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'dragover',
|
||||
this.preventImageDroppedHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'scroll',
|
||||
this.scrollHandler
|
||||
)
|
||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||
@@ -365,13 +464,34 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('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)
|
||||
this.refs.root.contentWindow.document.body.removeEventListener(
|
||||
'contextmenu',
|
||||
this.contextMenuHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.removeEventListener(
|
||||
'mousedown',
|
||||
this.mouseDownHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.removeEventListener(
|
||||
'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-md', this.saveAsMdHandler)
|
||||
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||
@@ -380,14 +500,18 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||
if (prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||
prevProps.sanitize !== this.props.sanitize ||
|
||||
prevProps.smartArrows !== this.props.smartArrows ||
|
||||
prevProps.breaks !== this.props.breaks) {
|
||||
if (
|
||||
prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||
prevProps.sanitize !== this.props.sanitize ||
|
||||
prevProps.smartArrows !== this.props.smartArrows ||
|
||||
prevProps.breaks !== this.props.breaks ||
|
||||
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
|
||||
) {
|
||||
this.initMarkdown()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
if (prevProps.fontFamily !== this.props.fontFamily ||
|
||||
if (
|
||||
prevProps.fontFamily !== this.props.fontFamily ||
|
||||
prevProps.fontSize !== this.props.fontSize ||
|
||||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
||||
@@ -396,34 +520,82 @@ export default class MarkdownPreview extends React.Component {
|
||||
prevProps.theme !== this.props.theme ||
|
||||
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
|
||||
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
|
||||
prevProps.customCSS !== this.props.customCSS) {
|
||||
prevProps.customCSS !== this.props.customCSS
|
||||
) {
|
||||
this.applyStyle()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
}
|
||||
|
||||
getStyleParams () {
|
||||
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS } = this.props
|
||||
const {
|
||||
fontSize,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = this.props
|
||||
let { fontFamily, codeBlockFontFamily } = this.props
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
? fontFamily
|
||||
.split(',')
|
||||
.map(fontName => fontName.trim())
|
||||
.concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
|
||||
codeBlockFontFamily.trim().length > 0
|
||||
? codeBlockFontFamily
|
||||
.split(',')
|
||||
.map(fontName => fontName.trim())
|
||||
.concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
|
||||
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS}
|
||||
return {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
}
|
||||
}
|
||||
|
||||
applyStyle () {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = this.getStyleParams()
|
||||
|
||||
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
||||
this.getWindow().document.getElementById(
|
||||
'codeTheme'
|
||||
).href = this.GetCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
)
|
||||
}
|
||||
|
||||
GetCodeThemeLink (theme) {
|
||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
||||
theme = consts.THEMES.some(_theme => _theme === theme) &&
|
||||
theme !== 'default'
|
||||
? theme
|
||||
: 'elegant'
|
||||
return theme.startsWith('solarized')
|
||||
@@ -432,71 +604,92 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
rewriteIframe () {
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.removeEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll(
|
||||
'input[type="checkbox"]'
|
||||
),
|
||||
el => {
|
||||
el.removeEventListener('click', this.checkboxClickHandler)
|
||||
}
|
||||
)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.removeEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('a'),
|
||||
el => {
|
||||
el.removeEventListener('click', this.linkClickHandler)
|
||||
}
|
||||
)
|
||||
|
||||
const { theme, indentSize, showCopyNotification, storagePath, noteKey } = this.props
|
||||
const {
|
||||
theme,
|
||||
indentSize,
|
||||
showCopyNotification,
|
||||
storagePath,
|
||||
noteKey
|
||||
} = this.props
|
||||
let { value, codeBlockTheme } = this.props
|
||||
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
|
||||
const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g)
|
||||
if (codeBlocks !== null) {
|
||||
codeBlocks.forEach((codeBlock) => {
|
||||
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
||||
})
|
||||
}
|
||||
const renderedHTML = this.markdown.render(value)
|
||||
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
||||
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)
|
||||
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
|
||||
renderedHTML,
|
||||
storagePath
|
||||
)
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll(
|
||||
'input[type="checkbox"]'
|
||||
),
|
||||
el => {
|
||||
el.addEventListener('click', this.checkboxClickHandler)
|
||||
}
|
||||
)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.addEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('a'),
|
||||
el => {
|
||||
this.fixDecodedURI(el)
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
}
|
||||
)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
this.fixDecodedURI(el)
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
||||
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
|
||||
? codeBlockTheme
|
||||
: 'default'
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(el.className))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
CodeMirror.requireMode(syntax.mode, () => {
|
||||
const content = htmlTextHelper.decodeEntities(el.innerHTML)
|
||||
const copyIcon = document.createElement('i')
|
||||
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||
copyIcon.onclick = (e) => {
|
||||
copy(content)
|
||||
if (showCopyNotification) {
|
||||
this.notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
||||
el => {
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(el.className))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
CodeMirror.requireMode(syntax.mode, () => {
|
||||
const content = htmlTextHelper.decodeEntities(el.innerHTML)
|
||||
const copyIcon = document.createElement('i')
|
||||
copyIcon.innerHTML =
|
||||
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||
copyIcon.onclick = e => {
|
||||
copy(content)
|
||||
if (showCopyNotification) {
|
||||
this.notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
el.parentNode.appendChild(copyIcon)
|
||||
el.innerHTML = ''
|
||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||
const [refThema, color] = codeBlockTheme.split(' ')
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||
} else {
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||
}
|
||||
CodeMirror.runMode(content, syntax.mime, el, {
|
||||
tabSize: indentSize
|
||||
el.parentNode.appendChild(copyIcon)
|
||||
el.innerHTML = ''
|
||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||
const [refThema, color] = codeBlockTheme.split(' ')
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||
} else {
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||
}
|
||||
CodeMirror.runMode(content, syntax.mime, el, {
|
||||
tabSize: indentSize
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
const opts = {}
|
||||
// if (this.props.theme === 'dark') {
|
||||
// opts['font-color'] = '#DDD'
|
||||
@@ -504,50 +697,65 @@ export default class MarkdownPreview extends React.Component {
|
||||
// opts['element-color'] = '#DDD'
|
||||
// opts['fill'] = '#3A404C'
|
||||
// }
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => {
|
||||
Raphael.setWindow(this.getWindow())
|
||||
try {
|
||||
const diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, opts)
|
||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
el.className = 'flowchart-error'
|
||||
el.innerHTML = 'Flowchart parse error: ' + e.message
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
|
||||
el => {
|
||||
Raphael.setWindow(this.getWindow())
|
||||
try {
|
||||
const diagram = flowchart.parse(
|
||||
htmlTextHelper.decodeEntities(el.innerHTML)
|
||||
)
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, opts)
|
||||
_.forEach(el.querySelectorAll('a'), el => {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
el.className = 'flowchart-error'
|
||||
el.innerHTML = 'Flowchart parse error: ' + e.message
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => {
|
||||
Raphael.setWindow(this.getWindow())
|
||||
try {
|
||||
const diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, {theme: 'simple'})
|
||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
el.className = 'sequence-error'
|
||||
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.sequence'),
|
||||
el => {
|
||||
Raphael.setWindow(this.getWindow())
|
||||
try {
|
||||
const diagram = SequenceDiagram.parse(
|
||||
htmlTextHelper.decodeEntities(el.innerHTML)
|
||||
)
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, { theme: 'simple' })
|
||||
_.forEach(el.querySelectorAll('a'), el => {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
el.className = 'sequence-error'
|
||||
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.chart'),
|
||||
(el) => {
|
||||
el => {
|
||||
try {
|
||||
const chartConfig = JSON.parse(el.innerHTML)
|
||||
const format = el.attributes.getNamedItem('data-format').value
|
||||
const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
|
||||
el.innerHTML = ''
|
||||
var canvas = document.createElement('canvas')
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
el.appendChild(canvas)
|
||||
/* eslint-disable no-new */
|
||||
new Chart(canvas, chartConfig)
|
||||
|
||||
const height = el.attributes.getNamedItem('data-height')
|
||||
if (height && height.value !== 'undefined') {
|
||||
el.style.height = height.value + 'vh'
|
||||
canvas.height = height.value + 'vh'
|
||||
}
|
||||
|
||||
const chart = new Chart(canvas, chartConfig)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
el.className = 'chart-error'
|
||||
el.innerHTML = 'chartjs diagram parse error: ' + e.message
|
||||
}
|
||||
@@ -555,8 +763,8 @@ export default class MarkdownPreview extends React.Component {
|
||||
)
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
||||
(el) => {
|
||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML))
|
||||
el => {
|
||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -570,7 +778,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
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++) {
|
||||
let block = blocks[index]
|
||||
@@ -590,7 +800,11 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
notify (title, options) {
|
||||
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)
|
||||
}
|
||||
@@ -605,7 +819,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
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)
|
||||
const targetElement = this.refs.root.contentWindow.document.getElementById(
|
||||
targetId
|
||||
)
|
||||
|
||||
if (targetElement != null) {
|
||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
||||
@@ -623,6 +839,15 @@ export default class MarkdownPreview extends React.Component {
|
||||
return
|
||||
}
|
||||
|
||||
const regexIsLine = /^:line:[0-9]/
|
||||
if (regexIsLine.test(linkHash)) {
|
||||
const numberPattern = /\d+/g
|
||||
|
||||
const lineNumber = parseInt(linkHash.match(numberPattern)[0])
|
||||
eventEmitter.emit('line:jump', lineNumber)
|
||||
return
|
||||
}
|
||||
|
||||
// this will match the old link format storage.key-note.key
|
||||
// e.g.
|
||||
// 877f99c3268608328037-1c211eb7dcb463de6490
|
||||
@@ -639,9 +864,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
render () {
|
||||
const { className, style, tabIndex } = this.props
|
||||
return (
|
||||
<iframe className={className != null
|
||||
? 'MarkdownPreview ' + className
|
||||
: 'MarkdownPreview'
|
||||
<iframe
|
||||
className={
|
||||
className != null ? 'MarkdownPreview ' + className : 'MarkdownPreview'
|
||||
}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
|
||||
@@ -20,12 +20,18 @@ class MarkdownSplitEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
setValue (value) {
|
||||
this.refs.code.setValue(value)
|
||||
}
|
||||
|
||||
handleOnChange () {
|
||||
this.value = this.refs.code.value
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
if (!this.props.config.preview.scrollSync) return
|
||||
|
||||
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
|
||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||
let srcTop, srcHeight, targetTop, targetHeight
|
||||
@@ -72,8 +78,10 @@ class MarkdownSplitEditor extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /\[x\]/i
|
||||
const uncheckedMatch = /\[ \]/
|
||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||
const checkReplace = /\[x\]/i
|
||||
const uncheckReplace = /\[ \]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
@@ -82,10 +90,10 @@ class MarkdownSplitEditor extends React.Component {
|
||||
const targetLine = lines[lineIndex]
|
||||
|
||||
if (targetLine.match(checkedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
|
||||
}
|
||||
if (targetLine.match(uncheckedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
||||
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
|
||||
}
|
||||
this.refs.code.setValue(lines.join('\n'))
|
||||
}
|
||||
@@ -145,7 +153,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
styleName='codeEditor'
|
||||
ref='code'
|
||||
width={this.state.codeEditorWidthInPercent + '%'}
|
||||
mode='GitHub Flavored Markdown'
|
||||
mode='Boost Flavored Markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
@@ -158,10 +166,15 @@ class MarkdownSplitEditor extends React.Component {
|
||||
rulers={config.editor.rulers}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
onChange={this.handleOnChange.bind(this)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
spellCheck={config.editor.spellcheck}
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
switchPreview={config.editor.switchPreview}
|
||||
/>
|
||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||
<div styleName='slider-hitbox' />
|
||||
@@ -191,6 +204,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -24,18 +24,19 @@ const TagElement = ({ tagName }) => (
|
||||
/**
|
||||
* @description Tag element list component.
|
||||
* @param {Array|null} tags
|
||||
* @param {boolean} showTagsAlphabetically
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const TagElementList = (tags) => {
|
||||
const TagElementList = (tags, showTagsAlphabetically) => {
|
||||
if (!isArray(tags)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tagElements = tags.map(tag => (
|
||||
TagElement({tagName: tag})
|
||||
))
|
||||
|
||||
return tagElements
|
||||
if (showTagsAlphabetically) {
|
||||
return _.sortBy(tags).map(tag => TagElement({ tagName: tag }))
|
||||
} else {
|
||||
return tags.map(tag => TagElement({ tagName: tag }))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,12 +58,11 @@ const NoteItem = ({
|
||||
pathname,
|
||||
storageName,
|
||||
folderName,
|
||||
viewType
|
||||
viewType,
|
||||
showTagsAlphabetically
|
||||
}) => (
|
||||
<div styleName={isActive
|
||||
? 'item--active'
|
||||
: 'item'
|
||||
}
|
||||
<div
|
||||
styleName={isActive ? 'item--active' : 'item'}
|
||||
key={note.key}
|
||||
onClick={e => handleNoteClick(e, note.key)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||
@@ -72,42 +72,52 @@ const NoteItem = ({
|
||||
<div styleName='item-wrapper'>
|
||||
{note.type === 'SNIPPET_NOTE'
|
||||
? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
|
||||
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />
|
||||
}
|
||||
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
|
||||
<div styleName='item-title'>
|
||||
{note.title.trim().length > 0
|
||||
? note.title
|
||||
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>
|
||||
}
|
||||
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
|
||||
</div>
|
||||
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'>
|
||||
<div styleName='item-middle'>
|
||||
<div styleName='item-middle-time'>{dateDisplay}</div>
|
||||
<div styleName='item-middle-app-meta'>
|
||||
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'>
|
||||
<div
|
||||
title={
|
||||
viewType === 'ALL'
|
||||
? storageName
|
||||
: viewType === 'STORAGE' ? folderName : null
|
||||
}
|
||||
styleName='item-middle-app-meta-label'
|
||||
>
|
||||
{viewType === 'ALL' && storageName}
|
||||
{viewType === 'STORAGE' && folderName}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
<div styleName='item-bottom'>
|
||||
<div styleName='item-bottom-tagList'>
|
||||
{note.tags.length > 0
|
||||
? TagElementList(note.tags)
|
||||
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>{i18n.__('No tags')}</span>
|
||||
}
|
||||
? TagElementList(note.tags, showTagsAlphabetically)
|
||||
: <span
|
||||
style={{ fontStyle: 'italic', opacity: 0.5 }}
|
||||
styleName='item-bottom-tagList-empty'
|
||||
>
|
||||
{i18n.__('No tags')}
|
||||
</span>}
|
||||
</div>
|
||||
<div>
|
||||
{note.isStarred
|
||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||
}
|
||||
? <img
|
||||
styleName='item-star'
|
||||
src='../resources/icon/icon-starred.svg'
|
||||
/>
|
||||
: ''}
|
||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||
}
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||
: ''}
|
||||
{note.type === 'MARKDOWN_NOTE'
|
||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||
: ''
|
||||
}
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -368,13 +368,13 @@ body[data-theme="monokai"]
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
color $ui-monokai-text-color
|
||||
color $ui-monokai-active-color
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-monokai-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
color #f92672
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
|
||||
@@ -394,3 +394,76 @@ body[data-theme="monokai"]
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.item
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
// background-color alpha($ui-dracula-noteList-backgroundColor, 20%)
|
||||
color $ui-dracula-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-dracula-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-dracula-noteList-backgroundColor, 20%)
|
||||
color $ui-dracula-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-dracula-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-dracula-noteList-backgroundColor, 10%)
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.item-wrapper
|
||||
border-color alpha($ui-dracula-button-backgroundColor, 60%)
|
||||
|
||||
.item--active
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
.item-wrapper
|
||||
border-color transparent
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
color $ui-dracula-active-color
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(#f8f8f2, 10%)
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-dracula-button--active-backgroundColor, 60%)
|
||||
color #ff79c6
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(#f8f8f2, 20%)
|
||||
|
||||
.item-title
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-icon
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-empty
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
@@ -240,7 +240,7 @@ body[data-theme="monokai"]
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(#fff, 20%)
|
||||
@@ -286,3 +286,67 @@ body[data-theme="monokai"]
|
||||
.item-simple-right-storageName
|
||||
padding-left 4px
|
||||
opacity 0.4
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.item-simple
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
background-color alpha($ui-dracula-button-backgroundColor, 60%)
|
||||
color $ui-dracula-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-dracula-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(#f8f8f2, 20%)
|
||||
color $ui-dracula-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-dracula-button--active-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-dracula-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(#f8f8f2, 10%)
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.item-simple--active
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-button--active-backgroundColor
|
||||
.item-simple-wrapper
|
||||
border-color transparent
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
color $ui-dracula-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(#f8f8f2, 10%)
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(#f8f8f2, 20%)
|
||||
.item-simple-title
|
||||
color $ui-dark-text-color
|
||||
border-bottom $ui-dark-borderColor
|
||||
.item-simple-right
|
||||
float right
|
||||
.item-simple-right-storageName
|
||||
padding-left 4px
|
||||
opacity 0.4
|
||||
@@ -51,4 +51,15 @@ body[data-theme="monokai"]
|
||||
border none
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:hover
|
||||
color #5CB85C
|
||||
color #5CB85C
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.notification-area
|
||||
background-color none
|
||||
|
||||
.notification-link
|
||||
color $ui-dracula-text-color
|
||||
border none
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
&:hover
|
||||
color #ff79c6
|
||||
@@ -263,4 +263,46 @@ body[data-theme="monokai"]
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.menu-button
|
||||
&:active
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.menu-button--active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
.menu-button-label
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.menu-button-star--active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
.menu-button-label
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.menu-button-trash--active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
.menu-button-label
|
||||
color $ui-dracula-text-color
|
||||
@@ -136,4 +136,92 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-solarized-dark-text-color, 30%)
|
||||
color alpha($ui-solarized-dark-text-color, 30%)
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.root--active
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-monokai-borderColor
|
||||
&:hover
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.button
|
||||
border none
|
||||
color $ui-monokai-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-monokai-text-color, 30%)
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.root--active
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dracula-borderColor
|
||||
&:hover
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.button
|
||||
border none
|
||||
color $ui-dracula-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-dracula-text-color, 30%)
|
||||
@@ -54,9 +54,8 @@ const StorageItem = ({
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
{!isFolded && (
|
||||
<DraggableIcon className={styles['folderList-item-reorder']} />
|
||||
)}
|
||||
{!isFolded &&
|
||||
<DraggableIcon className={styles['folderList-item-reorder']} />}
|
||||
<span
|
||||
styleName={
|
||||
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||
@@ -72,12 +71,10 @@ const StorageItem = ({
|
||||
: folderName}
|
||||
</span>
|
||||
{!isFolded &&
|
||||
_.isNumber(noteCount) && (
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||
)}
|
||||
{isFolded && (
|
||||
<span styleName='folderList-item-tooltip'>{folderName}</span>
|
||||
)}
|
||||
_.isNumber(noteCount) &&
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>}
|
||||
{isFolded &&
|
||||
<span styleName='folderList-item-tooltip'>{folderName}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,4 +156,23 @@ body[data-theme="monokai"]
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.folderList-item
|
||||
&:hover
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
&:active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
&:active
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
@@ -7,18 +7,18 @@ import styles from './StorageList.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
|
||||
/**
|
||||
* @param {Array} storgaeList
|
||||
* @param {Array} storageList
|
||||
*/
|
||||
|
||||
const StorageList = ({storageList, isFolded}) => (
|
||||
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
|
||||
{storageList.length > 0 ? storageList : (
|
||||
<div styleName='storgaeList-empty'>No storage mount.</div>
|
||||
<div styleName='storageList-empty'>No storage mount.</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
StorageList.propTypes = {
|
||||
storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired
|
||||
storageList: PropTypes.arrayOf(PropTypes.element).isRequired
|
||||
}
|
||||
export default CSSModules(StorageList, styles)
|
||||
|
||||
@@ -14,8 +14,8 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
* @param {bool} isRelated
|
||||
*/
|
||||
|
||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
|
||||
<div styleName='tagList-itemContainer'>
|
||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => (
|
||||
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}>
|
||||
{isRelated
|
||||
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
||||
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
|
||||
@@ -25,7 +25,7 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isAc
|
||||
<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 styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import styles from './TodoListPercentage.styl'
|
||||
*/
|
||||
|
||||
const TodoListPercentage = ({
|
||||
percentageOfTodo
|
||||
percentageOfTodo, onClearCheckboxClick
|
||||
}) => (
|
||||
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
||||
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
||||
@@ -20,11 +20,15 @@ const TodoListPercentage = ({
|
||||
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='todoClear'>
|
||||
<p styleName='todoClearText' onClick={(e) => onClearCheckboxClick(e)}>clear</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
TodoListPercentage.propTypes = {
|
||||
percentageOfTodo: PropTypes.number.isRequired
|
||||
percentageOfTodo: PropTypes.number.isRequired,
|
||||
onClearCheckboxClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(TodoListPercentage, styles)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.percentageBar
|
||||
display: flex
|
||||
position absolute
|
||||
top 72px
|
||||
right 0px
|
||||
@@ -30,6 +31,20 @@
|
||||
color #f4f4f4
|
||||
font-weight 600
|
||||
|
||||
.todoClear
|
||||
display flex
|
||||
justify-content: flex-end
|
||||
position absolute
|
||||
z-index 120
|
||||
width 100%
|
||||
height 100%
|
||||
padding 2px 10px
|
||||
|
||||
.todoClearText
|
||||
color #f4f4f4
|
||||
cursor pointer
|
||||
font-weight 500
|
||||
|
||||
body[data-theme="dark"]
|
||||
.percentageBar
|
||||
background-color #444444
|
||||
@@ -39,7 +54,10 @@ body[data-theme="dark"]
|
||||
|
||||
.percentageText
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
.todoClearText
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.percentageBar
|
||||
background-color #002b36
|
||||
@@ -50,12 +68,28 @@ body[data-theme="solarized-dark"]
|
||||
.percentageText
|
||||
color #fdf6e3
|
||||
|
||||
.todoClearText
|
||||
color #fdf6e3
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.percentageBar
|
||||
background-color #f92672
|
||||
background-color: $ui-monokai-borderColor
|
||||
|
||||
.progressBar
|
||||
background-color: #373831
|
||||
background-color $ui-monokai-active-color
|
||||
|
||||
.percentageText
|
||||
color #fdf6e3
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.percentageBar
|
||||
background-color $ui-dracula-borderColor
|
||||
|
||||
.progressBar
|
||||
background-color: $ui-dracula-active-color
|
||||
|
||||
.percentageText
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.percentageText
|
||||
color $ui-dracula-text-color
|
||||
|
||||
@@ -80,6 +80,9 @@ li
|
||||
&.checked
|
||||
text-decoration line-through
|
||||
opacity 0.5
|
||||
&.taskListItem.checked
|
||||
text-decoration line-through
|
||||
opacity 0.5
|
||||
div.math-rendered
|
||||
text-align center
|
||||
.math-failed
|
||||
@@ -206,41 +209,39 @@ code
|
||||
text-decoration none
|
||||
margin-right 2px
|
||||
pre
|
||||
padding 0.5em !important
|
||||
padding 0.5rem !important
|
||||
border solid 1px #D1D1D1
|
||||
border-radius 5px
|
||||
overflow-x auto
|
||||
margin 0 0 1em
|
||||
margin 0 0 1rem
|
||||
display flex
|
||||
line-height 1.4em
|
||||
&.flowchart, &.sequence, &.chart
|
||||
display flex
|
||||
justify-content center
|
||||
background-color white
|
||||
&.CodeMirror
|
||||
height initial
|
||||
flex-wrap wrap
|
||||
&>code
|
||||
flex 1
|
||||
overflow-x auto
|
||||
code
|
||||
background-color inherit
|
||||
margin 0
|
||||
padding 0
|
||||
border none
|
||||
border-radius 0
|
||||
&.CodeMirror
|
||||
height initial
|
||||
flex-wrap wrap
|
||||
&>code
|
||||
flex 1
|
||||
overflow-x auto
|
||||
&.mermaid svg
|
||||
max-width 100% !important
|
||||
&>span.filename
|
||||
width 100%
|
||||
border-radius: 5px 0px 0px 0px
|
||||
margin -8px 100% 8px -8px
|
||||
padding 0px 6px
|
||||
margin -0.5rem 100% 0.5rem -0.5rem
|
||||
padding 0.125rem 0.375rem
|
||||
background-color #777;
|
||||
color white
|
||||
&:empty
|
||||
display none
|
||||
&>span.lineNumber
|
||||
display none
|
||||
font-size 1em
|
||||
padding 0.5em 0
|
||||
margin -0.5em 0.5em -0.5em -0.5em
|
||||
padding 0.5rem 0
|
||||
margin -0.5rem 0.5rem -0.5rem -0.5rem
|
||||
border-right 1px solid
|
||||
text-align right
|
||||
border-top-left-radius 4px
|
||||
@@ -257,6 +258,7 @@ table
|
||||
display block
|
||||
width 100%
|
||||
margin 0 0 1em
|
||||
overflow-x auto
|
||||
thead
|
||||
tr
|
||||
background-color tableHeadBgColor
|
||||
@@ -360,7 +362,7 @@ 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)
|
||||
@@ -371,6 +373,49 @@ for name, val in admonition_types
|
||||
color: val[color]
|
||||
content: val[icon]
|
||||
|
||||
dl
|
||||
margin 2rem 0
|
||||
padding 0
|
||||
display flex
|
||||
width 100%
|
||||
flex-wrap wrap
|
||||
align-items flex-start
|
||||
border-bottom 1px solid borderColor
|
||||
background-color tableHeadBgColor
|
||||
|
||||
dt
|
||||
border-top 1px solid borderColor
|
||||
font-weight bold
|
||||
text-align right
|
||||
overflow hidden
|
||||
flex-basis 20%
|
||||
padding 0.4rem 0.9rem
|
||||
box-sizing border-box
|
||||
|
||||
dd
|
||||
border-top 1px solid borderColor
|
||||
flex-basis 80%
|
||||
padding 0.4rem 0.9rem
|
||||
min-height 2.5rem
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
box-sizing border-box
|
||||
|
||||
dd + dd
|
||||
margin-left 20%
|
||||
|
||||
pre.fence
|
||||
flex-wrap wrap
|
||||
|
||||
.chart, .flowchart, .mermaid, .sequence
|
||||
display flex
|
||||
justify-content center
|
||||
background-color white
|
||||
max-width 100%
|
||||
flex-grow 1
|
||||
|
||||
canvas, svg
|
||||
max-width 100% !important
|
||||
|
||||
themeDarkBackground = darken(#21252B, 10%)
|
||||
themeDarkText = #f9f9f9
|
||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||
@@ -421,6 +466,14 @@ body[data-theme="dark"]
|
||||
kbd
|
||||
background-color themeDarkBorder
|
||||
color themeDarkText
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeDarkTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color themeDarkPreview
|
||||
|
||||
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
||||
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
||||
@@ -448,6 +501,14 @@ body[data-theme="solarized-dark"]
|
||||
border-color themeSolarizedDarkTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeSolarizedDarkTableBorder
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeSolarizedDarkTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
|
||||
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
||||
@@ -476,4 +537,49 @@ body[data-theme="monokai"]
|
||||
&:last-child
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
kbd
|
||||
background-color themeDarkBackground
|
||||
background-color themeDarkBackground
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeMonokaiTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
|
||||
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
|
||||
themeDraculaTableHead = themeDraculaTableEven
|
||||
themeDraculaTableBorder = themeDarkBorder
|
||||
|
||||
body[data-theme="dracula"]
|
||||
color $ui-dracula-text-color
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
table
|
||||
thead
|
||||
tr
|
||||
background-color themeDraculaTableHead
|
||||
th
|
||||
border-color themeDraculaTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeDraculaTableBorder
|
||||
tbody
|
||||
tr:nth-child(2n + 1)
|
||||
background-color themeDraculaTableOdd
|
||||
tr:nth-child(2n)
|
||||
background-color themeDraculaTableEven
|
||||
td
|
||||
border-color themeDraculaTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeDraculaTableBorder
|
||||
kbd
|
||||
background-color themeDarkBackground
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeDraculaTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
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++) {
|
||||
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
let id = 'm-'
|
||||
for (let i = 0; i < 7; i++) {
|
||||
id += pool[getRandomInt(0, 16)]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
function render (element, content) {
|
||||
function render (element, content, theme) {
|
||||
try {
|
||||
const height = element.attributes.getNamedItem('data-height')
|
||||
if (height && height.value !== 'undefined') {
|
||||
element.style.height = height.value + 'vh'
|
||||
}
|
||||
const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
|
||||
mermaidAPI.initialize({
|
||||
theme: isDarkTheme ? 'dark' : 'default',
|
||||
themeCSS: isDarkTheme ? darkThemeStyling : '',
|
||||
useMaxWidth: false
|
||||
})
|
||||
mermaidAPI.render(getId(), content, (svgGraph) => {
|
||||
element.innerHTML = svgGraph
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
element.className = 'mermaid-error'
|
||||
element.innerHTML = 'mermaid diagram parse error: ' + e.message
|
||||
}
|
||||
|
||||
@@ -48,8 +48,12 @@ const languages = [
|
||||
locale: 'pl'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese',
|
||||
locale: 'pt'
|
||||
name: 'Portuguese (PT-BR)',
|
||||
locale: 'pt-BR'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (PT-PT)',
|
||||
locale: 'pt-PT'
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
@@ -61,6 +65,9 @@ const languages = [
|
||||
}, {
|
||||
name: 'Turkish',
|
||||
locale: 'tr'
|
||||
}, {
|
||||
name: 'Thai',
|
||||
locale: 'th'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,53 +1,113 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
import { Point } from '@susisu/mte-kernel'
|
||||
|
||||
export default class TextEditorInterface {
|
||||
constructor (editor) {
|
||||
this.editor = editor
|
||||
this.doc = editor.getDoc()
|
||||
this.transaction = false
|
||||
}
|
||||
|
||||
getCursorPosition () {
|
||||
const { line, ch } = this.doc.getCursor()
|
||||
return new Point(line, ch)
|
||||
}
|
||||
|
||||
setCursorPosition (pos) {
|
||||
this.doc.setCursor({
|
||||
line: pos.row,
|
||||
ch: pos.column
|
||||
})
|
||||
}
|
||||
|
||||
setSelectionRange (range) {
|
||||
this.doc.setSelection(
|
||||
{ line: range.start.row, ch: range.start.column },
|
||||
{ line: range.end.row, ch: range.end.column }
|
||||
)
|
||||
}
|
||||
|
||||
getLastRow () {
|
||||
return this.doc.lineCount() - 1
|
||||
}
|
||||
|
||||
acceptsTableEdit () {
|
||||
return true
|
||||
}
|
||||
|
||||
getLine (row) {
|
||||
return this.doc.getLine(row)
|
||||
}
|
||||
|
||||
insertLine (row, line) {
|
||||
const lastRow = this.getLastRow()
|
||||
if (row > lastRow) {
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
'\n' + line,
|
||||
{ line: lastRow, ch: lastLine.length },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
} else {
|
||||
this.doc.replaceRange(
|
||||
line + '\n',
|
||||
{ line: row, ch: 0 },
|
||||
{ line: row, ch: 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deleteLine (row) {
|
||||
const lastRow = this.getLastRow()
|
||||
if (row >= lastRow) {
|
||||
if (lastRow > 0) {
|
||||
const preLastLine = this.getLine(lastRow - 1)
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
'',
|
||||
{ line: lastRow - 1, ch: preLastLine.length },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
} else {
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
'',
|
||||
{ line: lastRow, ch: 0 },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this.doc.replaceRange(
|
||||
'',
|
||||
{ line: row, ch: 0 },
|
||||
{ line: row + 1, ch: 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
replaceLines (startRow, endRow, lines) {
|
||||
const lastRow = this.getLastRow()
|
||||
if (endRow > lastRow) {
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
lines.join('\n'),
|
||||
{ line: startRow, ch: 0 },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
} else {
|
||||
this.doc.replaceRange(
|
||||
lines.join('\n') + '\n',
|
||||
{ line: startRow, ch: 0 },
|
||||
{ line: endRow, ch: 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
transact (func) {
|
||||
this.transaction = true
|
||||
func()
|
||||
this.transaction = false
|
||||
if (this.onDidFinishTransaction) {
|
||||
this.onDidFinishTransaction.call(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
65
browser/lib/contextMenuBuilder.js
Normal file
65
browser/lib/contextMenuBuilder.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const {remote} = require('electron')
|
||||
const {Menu} = remote.require('electron')
|
||||
const spellcheck = require('./spellcheck')
|
||||
|
||||
/**
|
||||
* Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
|
||||
* If the word is does not contains a spelling error (determined by the 'error style'), no suggestions for corrections are requested
|
||||
* => they are not visible in the context menu
|
||||
* @param editor CodeMirror editor
|
||||
* @param {MouseEvent} event that has triggered the creation of the context menu
|
||||
* @returns {Electron.Menu} The created electron context menu
|
||||
*/
|
||||
const buildEditorContextMenu = function (editor, event) {
|
||||
if (editor == null || event == null || event.pageX == null || event.pageY == null) {
|
||||
return null
|
||||
}
|
||||
const cursor = editor.coordsChar({left: event.pageX, top: event.pageY})
|
||||
const wordRange = editor.findWordAt(cursor)
|
||||
const word = editor.getRange(wordRange.anchor, wordRange.head)
|
||||
const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || []
|
||||
let isMisspelled = false
|
||||
for (const mark of existingMarks) {
|
||||
if (mark.className === spellcheck.getCSSClassName()) {
|
||||
isMisspelled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
let suggestion = []
|
||||
if (isMisspelled) {
|
||||
suggestion = spellcheck.getSpellingSuggestion(word)
|
||||
}
|
||||
|
||||
const selection = {
|
||||
isMisspelled: isMisspelled,
|
||||
spellingSuggestions: suggestion
|
||||
}
|
||||
const template = [{
|
||||
role: 'cut'
|
||||
}, {
|
||||
role: 'copy'
|
||||
}, {
|
||||
role: 'paste'
|
||||
}, {
|
||||
role: 'selectall'
|
||||
}]
|
||||
|
||||
if (selection.isMisspelled) {
|
||||
const suggestions = selection.spellingSuggestions
|
||||
template.unshift.apply(template, suggestions.map(function (suggestion) {
|
||||
return {
|
||||
label: suggestion,
|
||||
click: function (suggestion) {
|
||||
if (editor != null) {
|
||||
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).concat({
|
||||
type: 'separator'
|
||||
}))
|
||||
}
|
||||
return Menu.buildFromTemplate(template)
|
||||
}
|
||||
|
||||
module.exports = buildEditorContextMenu
|
||||
@@ -1,19 +1,37 @@
|
||||
export function findNoteTitle (value) {
|
||||
export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') {
|
||||
const splitted = value.split('\n')
|
||||
let title = null
|
||||
let isInsideCodeBlock = false
|
||||
|
||||
splitted.some((line, index) => {
|
||||
const trimmedLine = line.trim()
|
||||
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
||||
if (trimmedLine.match('```')) {
|
||||
isInsideCodeBlock = !isInsideCodeBlock
|
||||
if (splitted[0] === '---') {
|
||||
let line = 0
|
||||
while (++line < splitted.length) {
|
||||
if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) {
|
||||
title = splitted[line].substring(frontMatterTitleField.length + 1).trim()
|
||||
|
||||
break
|
||||
}
|
||||
if (splitted[line] === '---') {
|
||||
splitted.splice(0, line + 1)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
|
||||
title = trimmedLine
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (title === null) {
|
||||
splitted.some((line, index) => {
|
||||
const trimmedLine = line.trim()
|
||||
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
||||
if (trimmedLine.match('```')) {
|
||||
isInsideCodeBlock = !isInsideCodeBlock
|
||||
}
|
||||
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
|
||||
title = trimmedLine
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (title === null) {
|
||||
title = ''
|
||||
|
||||
@@ -5,10 +5,10 @@ export function getTodoStatus (content) {
|
||||
|
||||
splitted.forEach((line) => {
|
||||
const trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) {
|
||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
|
||||
numberOfTodo++
|
||||
}
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
|
||||
numberOfCompletedTodo++
|
||||
}
|
||||
})
|
||||
|
||||
232
browser/lib/markdown-it-deflist.js
Normal file
232
browser/lib/markdown-it-deflist.js
Normal file
@@ -0,0 +1,232 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = function definitionListPlugin (md) {
|
||||
var isSpace = md.utils.isSpace
|
||||
|
||||
// Search `[:~][\n ]`, returns next pos after marker on success
|
||||
// or -1 on fail.
|
||||
function skipMarker (state, line) {
|
||||
let start = state.bMarks[line] + state.tShift[line]
|
||||
const max = state.eMarks[line]
|
||||
|
||||
if (start >= max) { return -1 }
|
||||
|
||||
// Check bullet
|
||||
const marker = state.src.charCodeAt(start++)
|
||||
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 }
|
||||
|
||||
const pos = state.skipSpaces(start)
|
||||
|
||||
// require space after ":"
|
||||
if (start === pos) { return -1 }
|
||||
|
||||
return start
|
||||
}
|
||||
|
||||
function markTightParagraphs (state, idx) {
|
||||
const level = state.level + 2
|
||||
|
||||
let i
|
||||
let l
|
||||
for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
|
||||
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
|
||||
state.tokens[i + 2].hidden = true
|
||||
state.tokens[i].hidden = true
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deflist (state, startLine, endLine, silent) {
|
||||
var ch,
|
||||
contentStart,
|
||||
ddLine,
|
||||
dtLine,
|
||||
itemLines,
|
||||
listLines,
|
||||
listTokIdx,
|
||||
max,
|
||||
newEndLine,
|
||||
nextLine,
|
||||
offset,
|
||||
oldDDIndent,
|
||||
oldIndent,
|
||||
oldLineMax,
|
||||
oldParentType,
|
||||
oldSCount,
|
||||
oldTShift,
|
||||
oldTight,
|
||||
pos,
|
||||
prevEmptyEnd,
|
||||
tight,
|
||||
token
|
||||
|
||||
if (silent) {
|
||||
// quirk: validation mode validates a dd block only, not a whole deflist
|
||||
if (state.ddIndent < 0) { return false }
|
||||
return skipMarker(state, startLine) >= 0
|
||||
}
|
||||
|
||||
nextLine = startLine + 1
|
||||
if (nextLine >= endLine) { return false }
|
||||
|
||||
if (state.isEmpty(nextLine)) {
|
||||
nextLine++
|
||||
if (nextLine >= endLine) { return false }
|
||||
}
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { return false }
|
||||
contentStart = skipMarker(state, nextLine)
|
||||
if (contentStart < 0) { return false }
|
||||
|
||||
// Start list
|
||||
listTokIdx = state.tokens.length
|
||||
tight = true
|
||||
|
||||
token = state.push('dl_open', 'dl', 1)
|
||||
token.map = listLines = [ startLine, 0 ]
|
||||
|
||||
//
|
||||
// Iterate list items
|
||||
//
|
||||
|
||||
dtLine = startLine
|
||||
ddLine = nextLine
|
||||
|
||||
// One definition list can contain multiple DTs,
|
||||
// and one DT can be followed by multiple DDs.
|
||||
//
|
||||
// Thus, there is two loops here, and label is
|
||||
// needed to break out of the second one
|
||||
//
|
||||
/* eslint no-labels:0,block-scoped-var:0 */
|
||||
OUTER:
|
||||
for (;;) {
|
||||
prevEmptyEnd = false
|
||||
|
||||
token = state.push('dt_open', 'dt', 1)
|
||||
token.map = [ dtLine, dtLine ]
|
||||
|
||||
token = state.push('inline', '', 0)
|
||||
token.map = [ dtLine, dtLine ]
|
||||
token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim()
|
||||
token.children = []
|
||||
|
||||
token = state.push('dt_close', 'dt', -1)
|
||||
|
||||
for (;;) {
|
||||
token = state.push('dd_open', 'dd', 1)
|
||||
token.map = itemLines = [ ddLine, 0 ]
|
||||
|
||||
pos = contentStart
|
||||
max = state.eMarks[ddLine]
|
||||
offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine])
|
||||
|
||||
while (pos < max) {
|
||||
ch = state.src.charCodeAt(pos)
|
||||
|
||||
if (isSpace(ch)) {
|
||||
if (ch === 0x09) {
|
||||
offset += 4 - offset % 4
|
||||
} else {
|
||||
offset++
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
pos++
|
||||
}
|
||||
|
||||
contentStart = pos
|
||||
|
||||
oldTight = state.tight
|
||||
oldDDIndent = state.ddIndent
|
||||
oldIndent = state.blkIndent
|
||||
oldTShift = state.tShift[ddLine]
|
||||
oldSCount = state.sCount[ddLine]
|
||||
oldParentType = state.parentType
|
||||
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2
|
||||
state.tShift[ddLine] = contentStart - state.bMarks[ddLine]
|
||||
state.sCount[ddLine] = offset
|
||||
state.tight = true
|
||||
state.parentType = 'deflist'
|
||||
|
||||
newEndLine = ddLine
|
||||
while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) {
|
||||
}
|
||||
|
||||
oldLineMax = state.lineMax
|
||||
state.lineMax = newEndLine
|
||||
|
||||
state.md.block.tokenize(state, ddLine, newEndLine, true)
|
||||
|
||||
state.lineMax = oldLineMax
|
||||
|
||||
// If any of list item is tight, mark list as tight
|
||||
if (!state.tight || prevEmptyEnd) {
|
||||
tight = false
|
||||
}
|
||||
// Item become loose if finish with empty line,
|
||||
// but we should filter last element, because it means list finish
|
||||
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1)
|
||||
|
||||
state.tShift[ddLine] = oldTShift
|
||||
state.sCount[ddLine] = oldSCount
|
||||
state.tight = oldTight
|
||||
state.parentType = oldParentType
|
||||
state.blkIndent = oldIndent
|
||||
state.ddIndent = oldDDIndent
|
||||
|
||||
token = state.push('dd_close', 'dd', -1)
|
||||
|
||||
itemLines[1] = nextLine = state.line
|
||||
|
||||
if (nextLine >= endLine) { break OUTER }
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { break OUTER }
|
||||
contentStart = skipMarker(state, nextLine)
|
||||
if (contentStart < 0) { break }
|
||||
|
||||
ddLine = nextLine
|
||||
|
||||
// go to the next loop iteration:
|
||||
// insert DD tag and repeat checking
|
||||
}
|
||||
|
||||
if (nextLine >= endLine) { break }
|
||||
dtLine = nextLine
|
||||
|
||||
if (state.isEmpty(dtLine)) { break }
|
||||
if (state.sCount[dtLine] < state.blkIndent) { break }
|
||||
|
||||
ddLine = dtLine + 1
|
||||
if (ddLine >= endLine) { break }
|
||||
if (state.isEmpty(ddLine)) { ddLine++ }
|
||||
if (ddLine >= endLine) { break }
|
||||
|
||||
if (state.sCount[ddLine] < state.blkIndent) { break }
|
||||
contentStart = skipMarker(state, ddLine)
|
||||
if (contentStart < 0) { break }
|
||||
|
||||
// go to the next loop iteration:
|
||||
// insert DT and DD tags and repeat checking
|
||||
}
|
||||
|
||||
// Finilize list
|
||||
token = state.push('dl_close', 'dl', -1)
|
||||
|
||||
listLines[1] = nextLine
|
||||
|
||||
state.line = nextLine
|
||||
|
||||
// mark paragraphs tight if needed
|
||||
if (tight) {
|
||||
markTightParagraphs(state, listTokIdx)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] })
|
||||
}
|
||||
136
browser/lib/markdown-it-fence.js
Normal file
136
browser/lib/markdown-it-fence.js
Normal file
@@ -0,0 +1,136 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = function (md, renderers, defaultRenderer) {
|
||||
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
|
||||
|
||||
function fence (state, startLine, endLine, silent) {
|
||||
let pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||
let max = state.eMarks[startLine]
|
||||
|
||||
if (state.sCount[startLine] - state.blkIndent >= 4 || pos + 3 > max) {
|
||||
return false
|
||||
}
|
||||
|
||||
const marker = state.src.charCodeAt(pos)
|
||||
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) {
|
||||
return false
|
||||
}
|
||||
|
||||
let mem = pos
|
||||
pos = state.skipChars(pos, marker)
|
||||
|
||||
let len = pos - mem
|
||||
if (len < 3) {
|
||||
return false
|
||||
}
|
||||
|
||||
const markup = state.src.slice(mem, pos)
|
||||
const params = state.src.slice(pos, max)
|
||||
|
||||
if (silent) {
|
||||
return true
|
||||
}
|
||||
|
||||
let nextLine = startLine
|
||||
let haveEndMarker = false
|
||||
|
||||
while (true) {
|
||||
nextLine++
|
||||
if (nextLine >= endLine) {
|
||||
break
|
||||
}
|
||||
|
||||
pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
|
||||
max = state.eMarks[nextLine]
|
||||
|
||||
if (pos < max && state.sCount[nextLine] < state.blkIndent) {
|
||||
break
|
||||
}
|
||||
if (state.src.charCodeAt(pos) !== marker || state.sCount[nextLine] - state.blkIndent >= 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
pos = state.skipChars(pos, marker)
|
||||
|
||||
if (pos - mem < len) {
|
||||
continue
|
||||
}
|
||||
|
||||
pos = state.skipSpaces(pos)
|
||||
|
||||
if (pos >= max) {
|
||||
haveEndMarker = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
len = state.sCount[startLine]
|
||||
state.line = nextLine + (haveEndMarker ? 1 : 0)
|
||||
|
||||
const parameters = {}
|
||||
let langType = ''
|
||||
let fileName = ''
|
||||
let firstLineNumber = 1
|
||||
|
||||
let match = paramsRE.exec(params)
|
||||
if (match) {
|
||||
if (match[1]) {
|
||||
langType = match[1]
|
||||
}
|
||||
if (match[3]) {
|
||||
fileName = match[3]
|
||||
}
|
||||
if (match[4]) {
|
||||
firstLineNumber = parseInt(match[4], 10)
|
||||
}
|
||||
|
||||
if (match[2]) {
|
||||
const params = match[2]
|
||||
const regex = /(\w[-\w]*)(?:=(?:'(.*?[^\\])?'|"(.*?[^\\])?"|([^'"][^\s]*)))?/g
|
||||
|
||||
let name, value
|
||||
while ((match = regex.exec(params))) {
|
||||
name = match[1]
|
||||
value = match[2] || match[3] || match[4] || null
|
||||
|
||||
const height = /^(\d+)h$/.exec(name)
|
||||
if (height && !value) {
|
||||
parameters.height = height[1]
|
||||
} else {
|
||||
parameters[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let token
|
||||
if (renderers[langType]) {
|
||||
token = state.push(`${langType}_fence`, 'div', 0)
|
||||
} else {
|
||||
token = state.push('_fence', 'code', 0)
|
||||
}
|
||||
|
||||
token.langType = langType
|
||||
token.fileName = fileName
|
||||
token.firstLineNumber = firstLineNumber
|
||||
token.parameters = parameters
|
||||
|
||||
token.content = state.getLines(startLine + 1, nextLine, len, true)
|
||||
token.markup = markup
|
||||
token.map = [startLine, state.line]
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
md.block.ruler.before('fence', '_fence', fence, {
|
||||
alt: ['paragraph', 'reference', 'blockquote', 'list']
|
||||
})
|
||||
|
||||
for (const name in renderers) {
|
||||
md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index])
|
||||
}
|
||||
|
||||
if (defaultRenderer) {
|
||||
md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index])
|
||||
}
|
||||
}
|
||||
24
browser/lib/markdown-it-frontmatter.js
Normal file
24
browser/lib/markdown-it-frontmatter.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = function frontMatterPlugin (md) {
|
||||
function frontmatter (state, startLine, endLine, silent) {
|
||||
if (startLine !== 0 || state.src.substr(startLine, state.eMarks[0]) !== '---') {
|
||||
return false
|
||||
}
|
||||
|
||||
let line = 0
|
||||
while (++line < state.lineMax) {
|
||||
if (state.src.substring(state.bMarks[line], state.eMarks[line]) === '---') {
|
||||
state.line = line + 1
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
md.block.ruler.before('table', 'frontmatter', frontmatter, {
|
||||
alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
import { escapeHtmlCharacters } from './utils'
|
||||
import url from 'url'
|
||||
|
||||
module.exports = function sanitizePlugin (md, options) {
|
||||
options = options || {}
|
||||
@@ -8,19 +10,115 @@ module.exports = function sanitizePlugin (md, options) {
|
||||
md.core.ruler.after('linkify', 'sanitize_inline', state => {
|
||||
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
|
||||
if (state.tokens[tokenIdx].type === 'html_block') {
|
||||
state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options)
|
||||
state.tokens[tokenIdx].content = sanitizeHtml(
|
||||
state.tokens[tokenIdx].content,
|
||||
options
|
||||
)
|
||||
}
|
||||
if (state.tokens[tokenIdx].type === 'fence') {
|
||||
state.tokens[tokenIdx].content = state.tokens[tokenIdx].content.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
if (state.tokens[tokenIdx].type === '_fence') {
|
||||
// escapeHtmlCharacters has better performance
|
||||
state.tokens[tokenIdx].content = escapeHtmlCharacters(
|
||||
state.tokens[tokenIdx].content,
|
||||
{ skipSingleQuote: true }
|
||||
)
|
||||
}
|
||||
if (state.tokens[tokenIdx].type === 'inline') {
|
||||
const inlineTokens = state.tokens[tokenIdx].children
|
||||
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
||||
if (inlineTokens[childIdx].type === 'html_inline') {
|
||||
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options)
|
||||
inlineTokens[childIdx].content = sanitizeInline(
|
||||
inlineTokens[childIdx].content,
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
|
||||
const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig
|
||||
|
||||
function sanitizeInline (html, options) {
|
||||
let match = tagRegex.exec(html)
|
||||
if (!match) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options
|
||||
|
||||
if (match[1] !== undefined) {
|
||||
// opening tag
|
||||
const tag = match[1].toLowerCase()
|
||||
if (allowedTags.indexOf(tag) === -1) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const attributes = match[2]
|
||||
|
||||
let attrs = ''
|
||||
let name
|
||||
let value
|
||||
|
||||
while ((match = attributesRegex.exec(attributes))) {
|
||||
name = match[1].toLowerCase()
|
||||
value = match[3]
|
||||
|
||||
if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) {
|
||||
if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
|
||||
if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
attrs += ` ${name}`
|
||||
if (match[2]) {
|
||||
attrs += `="${value}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selfClosing.indexOf(tag) === -1) {
|
||||
return '<' + tag + attrs + '>'
|
||||
} else {
|
||||
return '<' + tag + attrs + ' />'
|
||||
}
|
||||
} else {
|
||||
// closing tag
|
||||
if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) {
|
||||
return html
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function naughtyHRef (href, options) {
|
||||
// href = href.replace(/[\x00-\x20]+/g, '')
|
||||
href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
|
||||
|
||||
const matches = href.match(/^([a-zA-Z]+)\:/)
|
||||
if (!matches) {
|
||||
if (href.match(/^[\/\\]{2}/)) {
|
||||
return !options.allowProtocolRelative
|
||||
}
|
||||
|
||||
// No scheme
|
||||
return false
|
||||
}
|
||||
|
||||
const scheme = matches[1].toLowerCase()
|
||||
|
||||
return options.allowedSchemes.indexOf(scheme) === -1
|
||||
}
|
||||
|
||||
function naughtyIFrame (src, options) {
|
||||
try {
|
||||
const parsed = url.parse(src, false, true)
|
||||
|
||||
return options.allowedIframeHostnames.index(parsed.hostname) === -1
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
107
browser/lib/markdown-toc-generator.js
Normal file
107
browser/lib/markdown-toc-generator.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @fileoverview Markdown table of contents generator
|
||||
*/
|
||||
|
||||
import toc from 'markdown-toc'
|
||||
import diacritics from 'diacritics-map'
|
||||
import stripColor from 'strip-color'
|
||||
import mdlink from 'markdown-link'
|
||||
|
||||
const EOL = require('os').EOL
|
||||
|
||||
/**
|
||||
* @caseSensitiveSlugify Custom slugify function
|
||||
* Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js),
|
||||
* but keeps original case to properly handle https://github.com/BoostIO/Boostnote/issues/2067
|
||||
*/
|
||||
function caseSensitiveSlugify (str) {
|
||||
function replaceDiacritics (str) {
|
||||
return str.replace(/[À-ž]/g, function (ch) {
|
||||
return diacritics[ch] || ch
|
||||
})
|
||||
}
|
||||
|
||||
function getTitle (str) {
|
||||
if (/^\[[^\]]+\]\(/.test(str)) {
|
||||
var m = /^\[([^\]]+)\]/.exec(str)
|
||||
if (m) return m[1]
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
str = getTitle(str)
|
||||
str = stripColor(str)
|
||||
// str = str.toLowerCase() //let's be case sensitive
|
||||
|
||||
// `.split()` is often (but not always) faster than `.replace()`
|
||||
str = str.split(' ').join('-')
|
||||
str = str.split(/\t/).join('--')
|
||||
str = str.split(/<\/?[^>]+>/).join('')
|
||||
str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('')
|
||||
str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('')
|
||||
str = replaceDiacritics(str)
|
||||
return str
|
||||
}
|
||||
|
||||
function linkify (tok, text, slug, opts) {
|
||||
var uniqeID = opts.num === 0 ? '' : '-' + opts.num
|
||||
tok.content = mdlink(text, '#' + slug + uniqeID)
|
||||
return tok
|
||||
}
|
||||
|
||||
const TOC_MARKER_START = '<!-- toc -->'
|
||||
const TOC_MARKER_END = '<!-- tocstop -->'
|
||||
|
||||
/**
|
||||
* Takes care of proper updating given editor with TOC.
|
||||
* If TOC doesn't exit in the editor, it's inserted at current caret position.
|
||||
* Otherwise,TOC is updated in place.
|
||||
* @param editor CodeMirror editor to be updated with TOC
|
||||
*/
|
||||
export function generateInEditor (editor) {
|
||||
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
|
||||
|
||||
function tocExistsInEditor () {
|
||||
return tocRegex.test(editor.getValue())
|
||||
}
|
||||
|
||||
function updateExistingToc () {
|
||||
const toc = generate(editor.getValue())
|
||||
const search = editor.getSearchCursor(tocRegex)
|
||||
while (search.findNext()) {
|
||||
search.replace(toc)
|
||||
}
|
||||
}
|
||||
|
||||
function addTocAtCursorPosition () {
|
||||
const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity}))
|
||||
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
|
||||
}
|
||||
|
||||
if (tocExistsInEditor()) {
|
||||
updateExistingToc()
|
||||
} else {
|
||||
addTocAtCursorPosition()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates MD TOC based on MD document passed as string.
|
||||
* @param markdownText MD document
|
||||
* @returns generatedTOC String containing generated TOC
|
||||
*/
|
||||
export function generate (markdownText) {
|
||||
const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify, linkify: linkify})
|
||||
return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END
|
||||
}
|
||||
|
||||
function wrapTocWithEol (toc, editor) {
|
||||
const leftWrap = editor.getCursor().ch === 0 ? '' : EOL
|
||||
const rightWrap = editor.getLine(editor.getCursor().line).length === editor.getCursor().ch ? '' : EOL
|
||||
return leftWrap + toc + rightWrap
|
||||
}
|
||||
|
||||
export default {
|
||||
generate,
|
||||
generateInEditor
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import katex from 'katex'
|
||||
import { lastFindInArray } from './utils'
|
||||
import anchor from '@enyaxu/markdown-it-anchor'
|
||||
|
||||
function createGutter (str, firstLineNumber) {
|
||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||
@@ -27,32 +28,6 @@ class Markdown {
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: config.preview.breaks,
|
||||
highlight: function (str, lang) {
|
||||
const delimiter = ':'
|
||||
const langInfo = lang.split(delimiter)
|
||||
const langType = langInfo[0]
|
||||
const fileName = langInfo[1] || ''
|
||||
const firstLineNumber = parseInt(langInfo[2], 10)
|
||||
|
||||
if (langType === 'flowchart') {
|
||||
return `<pre class="flowchart">${str}</pre>`
|
||||
}
|
||||
if (langType === 'sequence') {
|
||||
return `<pre class="sequence">${str}</pre>`
|
||||
}
|
||||
if (langType === 'chart') {
|
||||
return `<pre class="chart">${str}</pre>`
|
||||
}
|
||||
if (langType === 'mermaid') {
|
||||
return `<pre class="mermaid">${str}</pre>`
|
||||
}
|
||||
return '<pre class="code CodeMirror">' +
|
||||
'<span class="filename">' + fileName + '</span>' +
|
||||
createGutter(str, firstLineNumber) +
|
||||
'<code class="' + langType + '">' +
|
||||
str +
|
||||
'</code></pre>'
|
||||
},
|
||||
sanitize: 'STRICT'
|
||||
}
|
||||
|
||||
@@ -105,7 +80,11 @@ class Markdown {
|
||||
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
||||
'input': ['type', 'id', 'checked']
|
||||
},
|
||||
allowedIframeHostnames: ['www.youtube.com']
|
||||
allowedIframeHostnames: ['www.youtube.com'],
|
||||
selfClosing: [ 'img', 'br', 'hr', 'input' ],
|
||||
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
|
||||
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
|
||||
allowProtocolRelative: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -139,16 +118,59 @@ class Markdown {
|
||||
this.md.use(require('markdown-it-imsize'))
|
||||
this.md.use(require('markdown-it-footnote'))
|
||||
this.md.use(require('markdown-it-multimd-table'))
|
||||
this.md.use(require('markdown-it-named-headers'), {
|
||||
slugify: (header) => {
|
||||
return encodeURI(header.trim()
|
||||
this.md.use(anchor, {
|
||||
slugify: (title) => {
|
||||
var slug = encodeURI(title.trim()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||
.replace(/\s+/g, '-'))
|
||||
.replace(/\-+$/, '')
|
||||
return slug
|
||||
}
|
||||
})
|
||||
this.md.use(require('markdown-it-kbd'))
|
||||
this.md.use(require('markdown-it-admonition'))
|
||||
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
|
||||
this.md.use(require('markdown-it-abbr'))
|
||||
this.md.use(require('markdown-it-sub'))
|
||||
this.md.use(require('markdown-it-sup'))
|
||||
this.md.use(require('./markdown-it-deflist'))
|
||||
this.md.use(require('./markdown-it-frontmatter'))
|
||||
|
||||
this.md.use(require('./markdown-it-fence'), {
|
||||
chart: token => {
|
||||
if (token.parameters.hasOwnProperty('yaml')) {
|
||||
token.parameters.format = 'yaml'
|
||||
}
|
||||
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="chart" data-height="${token.parameters.height}" data-format="${token.parameters.format || 'json'}">${token.content}</div>
|
||||
</pre>`
|
||||
},
|
||||
flowchart: token => {
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
|
||||
</pre>`
|
||||
},
|
||||
mermaid: token => {
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="mermaid" data-height="${token.parameters.height}">${token.content}</div>
|
||||
</pre>`
|
||||
},
|
||||
sequence: token => {
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="sequence" data-height="${token.parameters.height}">${token.content}</div>
|
||||
</pre>`
|
||||
}
|
||||
}, token => {
|
||||
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
${createGutter(token.content, token.firstLineNumber)}
|
||||
<code class="${token.langType}">${token.content}</code>
|
||||
</pre>`
|
||||
})
|
||||
|
||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||
this.md.use(require('markdown-it-plantuml'), '', {
|
||||
@@ -163,6 +185,22 @@ class Markdown {
|
||||
}
|
||||
})
|
||||
|
||||
// Ditaa support
|
||||
this.md.use(require('markdown-it-plantuml'), {
|
||||
openMarker: '@startditaa',
|
||||
closeMarker: '@endditaa',
|
||||
generateSource: function (umlCode) {
|
||||
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
||||
// Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
|
||||
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
|
||||
const s = unescape(encodeURIComponent(umlCode))
|
||||
const zippedCode = deflate.encode64(
|
||||
deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9)
|
||||
)
|
||||
return `${serverAddress}/${zippedCode}`
|
||||
}
|
||||
})
|
||||
|
||||
// Override task item
|
||||
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||
let content, terminate, i, l, token
|
||||
@@ -205,7 +243,11 @@ class Markdown {
|
||||
if (!liToken.attrs) {
|
||||
liToken.attrs = []
|
||||
}
|
||||
liToken.attrs.push(['class', 'taskListItem'])
|
||||
if (config.preview.lineThroughCheckbox) {
|
||||
liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`])
|
||||
} else {
|
||||
liToken.attrs.push(['class', 'taskListItem'])
|
||||
}
|
||||
}
|
||||
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
||||
}
|
||||
@@ -230,9 +272,12 @@ class Markdown {
|
||||
this.md.renderer.render = (tokens, options, env) => {
|
||||
tokens.forEach((token) => {
|
||||
switch (token.type) {
|
||||
case 'heading_open':
|
||||
case 'paragraph_open':
|
||||
case 'blockquote_open':
|
||||
case 'dd_open':
|
||||
case 'dt_open':
|
||||
case 'heading_open':
|
||||
case 'list_item_open':
|
||||
case 'paragraph_open':
|
||||
case 'table_open':
|
||||
token.attrPush(['data-line', token.map[0]])
|
||||
}
|
||||
|
||||
76
browser/lib/newNote.js
Normal file
76
browser/lib/newNote.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { hashHistory } from 'react-router'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
|
||||
export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
|
||||
let tags = []
|
||||
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
|
||||
tags = params.tagname.split(' ')
|
||||
}
|
||||
|
||||
return dataApi
|
||||
.createNote(storage, {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: folder,
|
||||
title: '',
|
||||
tags,
|
||||
content: ''
|
||||
})
|
||||
.then(note => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: { key: noteHash }
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
})
|
||||
}
|
||||
|
||||
export function createSnippetNote (storage, folder, dispatch, location, params, config) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
|
||||
let tags = []
|
||||
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
|
||||
tags = params.tagname.split(' ')
|
||||
}
|
||||
|
||||
return dataApi
|
||||
.createNote(storage, {
|
||||
type: 'SNIPPET_NOTE',
|
||||
folder: folder,
|
||||
title: '',
|
||||
tags,
|
||||
description: '',
|
||||
snippets: [
|
||||
{
|
||||
name: '',
|
||||
mode: config.editor.snippetDefaultLanguage || 'text',
|
||||
content: ''
|
||||
}
|
||||
]
|
||||
})
|
||||
.then(note => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: { key: noteHash }
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
})
|
||||
}
|
||||
@@ -23,7 +23,9 @@ function findByWordOrTag (notes, block) {
|
||||
return true
|
||||
}
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
return note.description.match(wordRegExp)
|
||||
return note.description.match(wordRegExp) || note.snippets.some((snippet) => {
|
||||
return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
|
||||
})
|
||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||
return note.content.match(wordRegExp)
|
||||
}
|
||||
|
||||
232
browser/lib/spellcheck.js
Normal file
232
browser/lib/spellcheck.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import styles from '../components/CodeEditor.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const Typo = require('typo-js')
|
||||
const _ = require('lodash')
|
||||
|
||||
const CSS_ERROR_CLASS = 'codeEditor-typo'
|
||||
const SPELLCHECK_DISABLED = 'NONE'
|
||||
const DICTIONARY_PATH = '../dictionaries'
|
||||
const MILLISECONDS_TILL_LIVECHECK = 500
|
||||
|
||||
let dictionary = null
|
||||
let self
|
||||
|
||||
function getAvailableDictionaries () {
|
||||
return [
|
||||
{label: i18n.__('Disabled'), value: SPELLCHECK_DISABLED},
|
||||
{label: i18n.__('English'), value: 'en_GB'},
|
||||
{label: i18n.__('German'), value: 'de_DE'},
|
||||
{label: i18n.__('French'), value: 'fr_FR'}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Only to be used in the tests :)
|
||||
*/
|
||||
function setDictionaryForTestsOnly (newDictionary) {
|
||||
dictionary = newDictionary
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Initializes the spellcheck. It removes all existing marks of the current editor.
|
||||
* If a language was given (i.e. lang !== this.SPELLCHECK_DISABLED) it will load the stated dictionary and use it to check the whole document.
|
||||
* @param {Codemirror} editor CodeMirror-Editor
|
||||
* @param {String} lang on of the values from getAvailableDictionaries()-Method
|
||||
*/
|
||||
function setLanguage (editor, lang) {
|
||||
self = this
|
||||
dictionary = null
|
||||
|
||||
if (editor == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const existingMarks = editor.getAllMarks() || []
|
||||
for (const mark of existingMarks) {
|
||||
mark.clear()
|
||||
}
|
||||
if (lang !== SPELLCHECK_DISABLED) {
|
||||
dictionary = new Typo(lang, false, false, {
|
||||
dictionaryPath: DICTIONARY_PATH,
|
||||
asyncLoad: true,
|
||||
loadedCallback: () =>
|
||||
checkWholeDocument(editor)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the whole content of the editor for typos
|
||||
* @param {Codemirror} editor CodeMirror-Editor
|
||||
*/
|
||||
function checkWholeDocument (editor) {
|
||||
const lastLine = editor.lineCount() - 1
|
||||
const textOfLastLine = editor.getLine(lastLine) || ''
|
||||
const lastChar = textOfLastLine.length
|
||||
const from = {line: 0, ch: 0}
|
||||
const to = {line: lastLine, ch: lastChar}
|
||||
checkMultiLineRange(editor, from, to)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given range for typos
|
||||
* @param {Codemirror} editor CodeMirror-Editor
|
||||
* @param {line, ch} from starting position of the spellcheck
|
||||
* @param {line, ch} to end position of the spellcheck
|
||||
*/
|
||||
function checkMultiLineRange (editor, from, to) {
|
||||
function sortRange (pos1, pos2) {
|
||||
if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) {
|
||||
return {from: pos2, to: pos1}
|
||||
}
|
||||
return {from: pos1, to: pos2}
|
||||
}
|
||||
|
||||
const {from: smallerPos, to: higherPos} = sortRange(from, to)
|
||||
for (let l = smallerPos.line; l <= higherPos.line; l++) {
|
||||
const line = editor.getLine(l) || ''
|
||||
let w = 0
|
||||
if (l === smallerPos.line) {
|
||||
w = smallerPos.ch
|
||||
}
|
||||
let wEnd = line.length
|
||||
if (l === higherPos.line) {
|
||||
wEnd = higherPos.ch
|
||||
}
|
||||
while (w <= wEnd) {
|
||||
const wordRange = editor.findWordAt({line: l, ch: w})
|
||||
self.checkWord(editor, wordRange)
|
||||
w += (wordRange.head.ch - wordRange.anchor.ch) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Checks whether a certain range of characters in the editor (i.e. a word) contains a typo.
|
||||
* If so the ranged will be marked with the class CSS_ERROR_CLASS.
|
||||
* Note: Due to performance considerations, only words with more then 3 signs are checked.
|
||||
* @param {Codemirror} editor CodeMirror-Editor
|
||||
* @param wordRange Object specifying the range that should be checked.
|
||||
* Having the following structure: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code>
|
||||
*/
|
||||
function checkWord (editor, wordRange) {
|
||||
const word = editor.getRange(wordRange.anchor, wordRange.head)
|
||||
if (word == null || word.length <= 3) {
|
||||
return
|
||||
}
|
||||
if (!dictionary.check(word)) {
|
||||
editor.markText(wordRange.anchor, wordRange.head, {className: styles[CSS_ERROR_CLASS]})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the changes recently made (aka live check)
|
||||
* @param {Codemirror} editor CodeMirror-Editor
|
||||
* @param fromChangeObject codeMirror changeObject describing the start of the editing
|
||||
* @param toChangeObject codeMirror changeObject describing the end of the editing
|
||||
*/
|
||||
function checkChangeRange (editor, fromChangeObject, toChangeObject) {
|
||||
/**
|
||||
* Calculate the smallest respectively largest position as a start, resp. end, position and return it
|
||||
* @param start CodeMirror change object
|
||||
* @param end CodeMirror change object
|
||||
* @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
|
||||
*/
|
||||
function getStartAndEnd (start, end) {
|
||||
const possiblePositions = [start.from, start.to, end.from, end.to]
|
||||
let smallest = start.from
|
||||
let biggest = end.to
|
||||
for (const currentPos of possiblePositions) {
|
||||
if (currentPos.line < smallest.line || (currentPos.line === smallest.line && currentPos.ch < smallest.ch)) {
|
||||
smallest = currentPos
|
||||
}
|
||||
if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) {
|
||||
biggest = currentPos
|
||||
}
|
||||
}
|
||||
return {start: smallest, end: biggest}
|
||||
}
|
||||
|
||||
if (dictionary === null || editor == null) { return }
|
||||
|
||||
try {
|
||||
const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject)
|
||||
|
||||
// Expand the range to include words after/before whitespaces
|
||||
start.ch = Math.max(start.ch - 1, 0)
|
||||
end.ch = end.ch + 1
|
||||
|
||||
// clean existing marks
|
||||
const existingMarks = editor.findMarks(start, end) || []
|
||||
for (const mark of existingMarks) {
|
||||
mark.clear()
|
||||
}
|
||||
|
||||
self.checkMultiLineRange(editor, start, end)
|
||||
} catch (e) {
|
||||
console.info('Error during the spell check. It might be due to problems figuring out the range of the new text..', e)
|
||||
}
|
||||
}
|
||||
|
||||
function saveLiveSpellCheckFrom (changeObject) {
|
||||
liveSpellCheckFrom = changeObject
|
||||
}
|
||||
let liveSpellCheckFrom
|
||||
const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, {
|
||||
'leading': true,
|
||||
'trailing': false
|
||||
})
|
||||
const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, {
|
||||
'leading': false,
|
||||
'trailing': true
|
||||
})
|
||||
|
||||
/**
|
||||
* Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
|
||||
* @param {Codemirror} editor CodeMirror-Editor
|
||||
* @param changeObject codeMirror changeObject
|
||||
*/
|
||||
function handleChange (editor, changeObject) {
|
||||
if (dictionary === null) {
|
||||
return
|
||||
}
|
||||
debouncedSpellCheckLeading(changeObject)
|
||||
debouncedSpellCheck(editor, liveSpellCheckFrom, changeObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of spelling suggestions for the given (wrong written) word.
|
||||
* Returns an empty array if the dictionary is null (=> spellcheck is disabled) or the given word was null
|
||||
* @param word word to be checked
|
||||
* @returns {String[]} Array of suggestions
|
||||
*/
|
||||
function getSpellingSuggestion (word) {
|
||||
if (dictionary == null || word == null) {
|
||||
return []
|
||||
}
|
||||
return dictionary.suggest(word)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the CSS class used for errors
|
||||
*/
|
||||
function getCSSClassName () {
|
||||
return styles[CSS_ERROR_CLASS]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DICTIONARY_PATH,
|
||||
CSS_ERROR_CLASS,
|
||||
SPELLCHECK_DISABLED,
|
||||
getAvailableDictionaries,
|
||||
setLanguage,
|
||||
checkChangeRange,
|
||||
handleChange,
|
||||
getSpellingSuggestion,
|
||||
checkWord,
|
||||
checkMultiLineRange,
|
||||
checkWholeDocument,
|
||||
setDictionaryForTestsOnly,
|
||||
getCSSClassName
|
||||
}
|
||||
@@ -6,8 +6,12 @@ export function lastFindInArray (array, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
||||
export function escapeHtmlCharacters (
|
||||
html,
|
||||
opt = { detectCodeBlock: false, skipSingleQuote: false }
|
||||
) {
|
||||
const matchHtmlRegExp = /["'&<>]/g
|
||||
const matchCodeBlockRegExp = /```/g
|
||||
const escapes = ['"', '&', ''', '<', '>']
|
||||
let match = null
|
||||
const replaceAt = (str, index, replace) =>
|
||||
@@ -15,11 +19,18 @@ export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
||||
replace +
|
||||
str.substr(index + replace.length - (replace.length - 1))
|
||||
|
||||
// detecting code block
|
||||
while ((match = matchHtmlRegExp.exec(html)) != null) {
|
||||
while ((match = matchHtmlRegExp.exec(html)) !== null) {
|
||||
const current = { char: match[0], index: match.index }
|
||||
const 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) {
|
||||
// position of the nearest line start
|
||||
// The first type of code block is lines that start with 4 spaces
|
||||
// Here we check for the \n character located before the character that
|
||||
// needed to be escape. It means we check for the begining of the line that
|
||||
// contain that character, then we check if there are 4 spaces next to the
|
||||
// \n character (the line start with 4 spaces)
|
||||
let previousLineEnd = current.index - 1
|
||||
while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) {
|
||||
previousLineEnd--
|
||||
@@ -31,16 +42,54 @@ export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
||||
html[previousLineEnd + 3] === ' ' &&
|
||||
html[previousLineEnd + 4] === ' '
|
||||
) {
|
||||
// so skip it
|
||||
// skip the current character
|
||||
continue
|
||||
}
|
||||
// The second type of code block is lines that wrapped in ```
|
||||
// We will get the position of each ```
|
||||
// then push it into an array
|
||||
// then the array returned will be like this:
|
||||
// [startCodeblock, endCodeBlock, startCodeBlock, endCodeBlock]
|
||||
while ((openCodeBlock = matchCodeBlockRegExp.exec(html)) !== null) {
|
||||
codeBlockIndexs.push(openCodeBlock.index)
|
||||
}
|
||||
let shouldSkipChar = false
|
||||
// we loop through the array of positions
|
||||
// we skip 2 element as the i index position is the position of ``` that
|
||||
// open the codeblock and the i + 1 is the position of the ``` that close
|
||||
// the code block
|
||||
for (let i = 0; i < codeBlockIndexs.length; i += 2) {
|
||||
// the i index position is the position of the ``` that open code block
|
||||
// so we have to + 2 as that position is the position of the first ` in the ````
|
||||
// but we need to make sure that the position current character is larger
|
||||
// that the last ` in the ``` that open the code block so we have to take
|
||||
// the position of the first ` and + 2
|
||||
// the i + 1 index position is the closing ``` so the char must less than it
|
||||
if (
|
||||
current.index > codeBlockIndexs[i] + 2 &&
|
||||
current.index < codeBlockIndexs[i + 1]
|
||||
) {
|
||||
// skip it
|
||||
shouldSkipChar = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (shouldSkipChar) {
|
||||
// skip the current character
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 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 escape string is 5. For example ('"')
|
||||
// maximum length of an escaped string is 5. For example ('"')
|
||||
// we take the next 5 character of the next string if it is one of the string:
|
||||
// ['"', '&', ''', '<', '>'] then we will not escape the & character
|
||||
// as it is a part of the escaped string and should not be escaped
|
||||
while (nextStr.length <= 5) {
|
||||
nextStr += html[nextIndex]
|
||||
nextIndex++
|
||||
@@ -55,7 +104,7 @@ export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
||||
}
|
||||
} else if (current.char === '"') {
|
||||
html = replaceAt(html, current.index, '"')
|
||||
} else if (current.char === "'") {
|
||||
} else if (current.char === "'" && !opt.skipSingleQuote) {
|
||||
html = replaceAt(html, current.index, ''')
|
||||
} else if (current.char === '<') {
|
||||
html = replaceAt(html, current.index, '<')
|
||||
|
||||
@@ -23,7 +23,7 @@ body[data-theme="dark"]
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
.empty-message
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
@@ -37,3 +37,10 @@ body[data-theme="monokai"]
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
.empty-message
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
border-left 1px solid $ui-dracula-borderColor
|
||||
.empty-message
|
||||
color $ui-dracula-text-color
|
||||
@@ -36,7 +36,7 @@
|
||||
height 34px
|
||||
width 20px
|
||||
line-height 34px
|
||||
|
||||
|
||||
.search-input
|
||||
vertical-align middle
|
||||
position relative
|
||||
@@ -71,7 +71,7 @@
|
||||
overflow ellipsis
|
||||
cursor pointer
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.search-optionList-item--active
|
||||
@extend .search-optionList-item
|
||||
@@ -159,3 +159,29 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-monokai-inactive-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
color #f8f8f2
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.search-optionList
|
||||
color #f8f8f2
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
|
||||
.search-optionList-item
|
||||
&:hover
|
||||
background-color lighten($ui-dracula-button--hover-backgroundColor, 15%)
|
||||
|
||||
.search-optionList-item--active
|
||||
background-color $ui-dracula-button--active-backgroundColor
|
||||
color $ui-dracula-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
color $ui-dracula-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-dracula-inactive-text-color
|
||||
|
||||
@@ -70,22 +70,22 @@ class InfoPanel extends React.Component {
|
||||
<hr />
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
|
||||
<i className='fa fa-file-code-o' />
|
||||
<p>{i18n.__('.md')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
|
||||
<i className='fa fa-file-text-o' />
|
||||
<p>{i18n.__('.txt')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
||||
<i className='fa fa-html5' />
|
||||
<p>{i18n.__('.html')}</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => print(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
|
||||
<i className='fa fa-print' />
|
||||
<p>{i18n.__('Print')}</p>
|
||||
</button>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
.control-infoButton-panel-trash
|
||||
z-index 200
|
||||
margin-top 0px
|
||||
top 50px
|
||||
right 0px
|
||||
position absolute
|
||||
padding 20px 25px 0 25px
|
||||
@@ -256,3 +257,43 @@ body[data-theme="monokai"]
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.control-infoButton-panel
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control-infoButton-panel-trash
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.modification-date
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.modification-date-desc
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-defaul-count
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.infoPanel-sub-count
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-default
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.infoPanel-sub
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-noteLink
|
||||
background-color alpha($ui-dracula-borderColor, 20%)
|
||||
color $ui-dracula-text-color
|
||||
|
||||
[id=export-wrap]
|
||||
button
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-dracula-borderColor, 20%)
|
||||
color $ui-dracula-text-color
|
||||
p
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
@@ -31,17 +31,17 @@ const InfoPanelTrashed = ({
|
||||
</div>
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
|
||||
<i className='fa fa-file-code-o' />
|
||||
<p>.md</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
|
||||
<i className='fa fa-file-text-o' />
|
||||
<p>.txt</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
|
||||
<i className='fa fa-html5' />
|
||||
<p>.html</p>
|
||||
</button>
|
||||
|
||||
@@ -29,6 +29,7 @@ import { formatDate } from 'browser/lib/date-formatter'
|
||||
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||
import striptags from 'striptags'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||
|
||||
class MarkdownNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -47,6 +48,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
this.dispatchTimer = null
|
||||
|
||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||
this.generateToc = () => this.handleGenerateToc()
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -59,10 +61,14 @@ class MarkdownNoteDetail extends React.Component {
|
||||
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
||||
this.handleSwitchMode(reversedType)
|
||||
})
|
||||
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
|
||||
ee.on('code:generate-toc', this.generateToc)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
|
||||
const isNewNote = nextProps.note.key !== this.props.note.key
|
||||
const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length
|
||||
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
this.setState({
|
||||
note: Object.assign({}, nextProps.note)
|
||||
@@ -75,6 +81,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
componentWillUnmount () {
|
||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||
ee.off('code:generate-toc', this.generateToc)
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
@@ -87,7 +94,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
handleUpdateContent () {
|
||||
const { note } = this.state
|
||||
note.content = this.refs.content.value
|
||||
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
|
||||
note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
|
||||
this.updateNote(note)
|
||||
}
|
||||
|
||||
@@ -183,6 +190,36 @@ class MarkdownNoteDetail extends React.Component {
|
||||
ee.emit('export:save-html')
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
// tab key
|
||||
case 9:
|
||||
if (e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.jumpNextTab()
|
||||
} else if (e.ctrlKey && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.jumpPrevTab()
|
||||
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
|
||||
e.preventDefault()
|
||||
this.focusEditor()
|
||||
}
|
||||
break
|
||||
// I key
|
||||
case 73:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
e.preventDefault()
|
||||
this.handleInfoButtonClick(e)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
handleTrashButtonClick (e) {
|
||||
const { note } = this.state
|
||||
const { isTrashed } = note
|
||||
@@ -262,6 +299,11 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleGenerateToc () {
|
||||
const editor = this.refs.content.refs.code.editor
|
||||
markdownToc.generateInEditor(editor)
|
||||
}
|
||||
|
||||
handleFocus (e) {
|
||||
this.focus()
|
||||
}
|
||||
@@ -284,9 +326,33 @@ class MarkdownNoteDetail extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleDeleteNote () {
|
||||
this.handleTrashButtonClick()
|
||||
}
|
||||
|
||||
handleClearTodo () {
|
||||
const { note } = this.state
|
||||
const splitted = note.content.split('\n')
|
||||
|
||||
const clearTodoContent = splitted.map((line) => {
|
||||
const trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/\[x\]/i)) {
|
||||
return line.replace(/\[x\]/i, '[ ]')
|
||||
} else {
|
||||
return line
|
||||
}
|
||||
}).join('\n')
|
||||
|
||||
note.content = clearTodoContent
|
||||
this.refs.content.setValue(note.content)
|
||||
|
||||
this.updateNote(note)
|
||||
}
|
||||
|
||||
renderEditor () {
|
||||
const { config, ignorePreviewPointerEvents } = this.props
|
||||
const { note } = this.state
|
||||
|
||||
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
||||
return <MarkdownEditor
|
||||
ref='content'
|
||||
@@ -312,7 +378,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, location } = this.props
|
||||
const { data, location, config } = this.props
|
||||
const { note, editorType } = this.state
|
||||
const storageKey = note.storage
|
||||
const folderKey = note.folder
|
||||
@@ -363,9 +429,12 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
data={data}
|
||||
onChange={this.handleUpdateTag.bind(this)}
|
||||
/>
|
||||
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||
@@ -419,6 +488,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
|
||||
@@ -76,3 +76,8 @@ body[data-theme="monokai"]
|
||||
.root
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-left 1px solid $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
@@ -13,6 +13,7 @@ $info-margin-under-border = 30px
|
||||
display flex
|
||||
align-items center
|
||||
padding 0 20px
|
||||
z-index 99
|
||||
|
||||
.info-left
|
||||
padding 0 10px
|
||||
@@ -97,8 +98,13 @@ body[data-theme="solarized-dark"]
|
||||
.info
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.info
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.info
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
@@ -29,6 +29,7 @@ import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
import { formatDate } from 'browser/lib/date-formatter'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
@@ -52,6 +53,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
this.scrollToNextTabThreshold = 0.7
|
||||
this.generateToc = () => this.handleGenerateToc()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -65,6 +67,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
enableLeftArrow: allTabs.offsetLeft !== 0
|
||||
})
|
||||
}
|
||||
ee.on('code:generate-toc', this.generateToc)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@@ -91,6 +94,16 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
ee.off('code:generate-toc', this.generateToc)
|
||||
}
|
||||
|
||||
handleGenerateToc () {
|
||||
const { note, snippetIndex } = this.state
|
||||
const currentMode = note.snippets[snippetIndex].mode
|
||||
if (currentMode.includes('Markdown')) {
|
||||
const currentEditor = this.refs[`code-${snippetIndex}`].refs.code.editor
|
||||
markdownToc.generateInEditor(currentEditor)
|
||||
}
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
@@ -99,7 +112,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
note.description = this.refs.description.value
|
||||
note.updatedAt = new Date()
|
||||
note.title = findNoteTitle(note.description)
|
||||
note.title = findNoteTitle(note.description, false)
|
||||
|
||||
this.setState({
|
||||
note
|
||||
@@ -341,12 +354,10 @@ class SnippetNoteDetail extends React.Component {
|
||||
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)
|
||||
@@ -423,6 +434,18 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.focusEditor()
|
||||
}
|
||||
break
|
||||
// I key
|
||||
case 73:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
e.preventDefault()
|
||||
this.handleInfoButtonClick(e)
|
||||
}
|
||||
}
|
||||
break
|
||||
// L key
|
||||
case 76:
|
||||
{
|
||||
@@ -441,7 +464,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
if (isSuper && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault()
|
||||
this.addSnippet()
|
||||
}
|
||||
@@ -573,11 +596,12 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
addSnippet () {
|
||||
const { config } = this.props
|
||||
const { note } = this.state
|
||||
|
||||
note.snippets = note.snippets.concat([{
|
||||
name: '',
|
||||
mode: 'Plain Text',
|
||||
mode: config.editor.snippetDefaultLanguage || 'text',
|
||||
content: ''
|
||||
}])
|
||||
const snippetIndex = note.snippets.length - 1
|
||||
@@ -613,7 +637,6 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
focusEditor () {
|
||||
console.log('code-' + this.state.snippetIndex)
|
||||
this.refs['code-' + this.state.snippetIndex].focus()
|
||||
}
|
||||
|
||||
@@ -622,11 +645,18 @@ class SnippetNoteDetail extends React.Component {
|
||||
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
||||
}
|
||||
|
||||
showWarning () {
|
||||
showWarning (e, msg) {
|
||||
const warningMessage = (msg) => ({
|
||||
'export-txt': 'Text export',
|
||||
'export-md': 'Markdown export',
|
||||
'export-html': 'HTML export',
|
||||
'print': 'Print'
|
||||
})[msg]
|
||||
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: i18n.__('Sorry!'),
|
||||
detail: i18n.__('md/text import is available only a markdown note.'),
|
||||
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
||||
buttons: [i18n.__('OK')]
|
||||
})
|
||||
}
|
||||
@@ -691,8 +721,11 @@ class SnippetNoteDetail extends React.Component {
|
||||
keyMap={config.editor.keyMap}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -744,6 +777,9 @@ class SnippetNoteDetail extends React.Component {
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
data={data}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
@@ -773,7 +809,9 @@ class SnippetNoteDetail extends React.Component {
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
exportAsHtml={this.showWarning}
|
||||
type={note.type}
|
||||
print={this.showWarning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -169,4 +169,21 @@ body[data-theme="monokai"]
|
||||
|
||||
.tabList
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-left 1px solid $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
.body
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
.body .description textarea
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
border 1px solid $ui-dracula-borderColor
|
||||
|
||||
.tabList
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
@@ -5,61 +5,34 @@ import styles from './TagSelect.styl'
|
||||
import _ from 'lodash'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import Autosuggest from 'react-autosuggest'
|
||||
|
||||
class TagSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
newTag: ''
|
||||
newTag: '',
|
||||
suggestions: []
|
||||
}
|
||||
|
||||
this.handleAddTag = this.handleAddTag.bind(this)
|
||||
this.onInputBlur = this.onInputBlur.bind(this)
|
||||
this.onInputChange = this.onInputChange.bind(this)
|
||||
this.onInputKeyDown = this.onInputKeyDown.bind(this)
|
||||
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this)
|
||||
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this)
|
||||
this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
handleNewTagInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 9:
|
||||
e.preventDefault()
|
||||
this.submitTag()
|
||||
break
|
||||
case 13:
|
||||
this.submitTag()
|
||||
break
|
||||
case 8:
|
||||
if (this.refs.newTag.value.length === 0) {
|
||||
this.removeLastTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleNewTagBlur (e) {
|
||||
this.submitTag()
|
||||
}
|
||||
|
||||
removeLastTag () {
|
||||
this.removeTagByCallback((value) => {
|
||||
value.pop()
|
||||
})
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.setState({
|
||||
newTag: ''
|
||||
})
|
||||
}
|
||||
|
||||
submitTag () {
|
||||
addNewTag (newTag) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
||||
let { value } = this.props
|
||||
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||
newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
|
||||
|
||||
newTag = newTag.trim().replace(/ +/g, '_')
|
||||
if (newTag.charAt(0) === '#') {
|
||||
newTag.substring(1)
|
||||
}
|
||||
|
||||
if (newTag.length <= 0) {
|
||||
this.setState({
|
||||
@@ -68,6 +41,7 @@ class TagSelect extends React.Component {
|
||||
return
|
||||
}
|
||||
|
||||
let { value } = this.props
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
@@ -82,10 +56,41 @@ class TagSelect extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleNewTagInputChange (e) {
|
||||
this.setState({
|
||||
newTag: this.refs.newTag.value
|
||||
})
|
||||
buildSuggestions () {
|
||||
this.suggestions = _.sortBy(this.props.data.tagNoteMap.map(
|
||||
(tag, name) => ({
|
||||
name,
|
||||
nameLC: name.toLowerCase(),
|
||||
size: tag.size
|
||||
})
|
||||
).filter(
|
||||
tag => tag.size > 0
|
||||
), ['name'])
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.props.value
|
||||
|
||||
this.buildSuggestions()
|
||||
|
||||
ee.on('editor:add-tag', this.handleAddTag)
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ee.off('editor:add-tag', this.handleAddTag)
|
||||
}
|
||||
|
||||
handleAddTag () {
|
||||
this.refs.newTag.input.focus()
|
||||
}
|
||||
|
||||
handleTagLabelClick (tag) {
|
||||
const { router } = this.context
|
||||
router.push(`/tags/${tag}`)
|
||||
}
|
||||
|
||||
handleTagRemoveButtonClick (tag) {
|
||||
@@ -94,6 +99,60 @@ class TagSelect extends React.Component {
|
||||
}, tag)
|
||||
}
|
||||
|
||||
onInputBlur (e) {
|
||||
this.submitNewTag()
|
||||
}
|
||||
|
||||
onInputChange (e, { newValue, method }) {
|
||||
this.setState({
|
||||
newTag: newValue
|
||||
})
|
||||
}
|
||||
|
||||
onInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 9:
|
||||
e.preventDefault()
|
||||
this.submitNewTag()
|
||||
break
|
||||
case 13:
|
||||
this.submitNewTag()
|
||||
break
|
||||
case 8:
|
||||
if (this.state.newTag.length === 0) {
|
||||
this.removeLastTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested () {
|
||||
this.setState({
|
||||
suggestions: []
|
||||
})
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested ({ value }) {
|
||||
const valueLC = value.toLowerCase()
|
||||
const suggestions = _.filter(
|
||||
this.suggestions,
|
||||
tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
|
||||
)
|
||||
|
||||
this.setState({
|
||||
suggestions
|
||||
})
|
||||
}
|
||||
|
||||
onSuggestionSelected (event, { suggestion, suggestionValue }) {
|
||||
this.addNewTag(suggestionValue)
|
||||
}
|
||||
|
||||
removeLastTag () {
|
||||
this.removeTagByCallback((value) => {
|
||||
value.pop()
|
||||
})
|
||||
}
|
||||
|
||||
removeTagByCallback (callback, tag = null) {
|
||||
let { value } = this.props
|
||||
|
||||
@@ -107,16 +166,28 @@ class TagSelect extends React.Component {
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.buildSuggestions()
|
||||
|
||||
this.setState({
|
||||
newTag: ''
|
||||
})
|
||||
}
|
||||
|
||||
submitNewTag () {
|
||||
this.addNewTag(this.refs.newTag.input.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, className } = this.props
|
||||
const { value, className, showTagsAlphabetically } = this.props
|
||||
|
||||
const tagList = _.isArray(value)
|
||||
? value.map((tag) => {
|
||||
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
|
||||
return (
|
||||
<span styleName='tag'
|
||||
key={tag}
|
||||
>
|
||||
<span styleName='tag-label'>#{tag}</span>
|
||||
<span styleName='tag-label' onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
|
||||
<button styleName='tag-removeButton'
|
||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
|
||||
>
|
||||
@@ -127,6 +198,8 @@ class TagSelect extends React.Component {
|
||||
})
|
||||
: []
|
||||
|
||||
const { newTag, suggestions } = this.state
|
||||
|
||||
return (
|
||||
<div className={_.isString(className)
|
||||
? 'TagSelect ' + className
|
||||
@@ -135,24 +208,39 @@ class TagSelect extends React.Component {
|
||||
styleName='root'
|
||||
>
|
||||
{tagList}
|
||||
<input styleName='newTag'
|
||||
<Autosuggest
|
||||
ref='newTag'
|
||||
value={this.state.newTag}
|
||||
placeholder={i18n.__('Add tag...')}
|
||||
onChange={(e) => this.handleNewTagInputChange(e)}
|
||||
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
||||
onBlur={(e) => this.handleNewTagBlur(e)}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
getSuggestionValue={suggestion => suggestion.name}
|
||||
renderSuggestion={suggestion => (
|
||||
<div>
|
||||
{suggestion.name}
|
||||
</div>
|
||||
)}
|
||||
inputProps={{
|
||||
placeholder: i18n.__('Add tag...'),
|
||||
value: newTag,
|
||||
onChange: this.onInputChange,
|
||||
onKeyDown: this.onInputKeyDown,
|
||||
onBlur: this.onInputBlur
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TagSelect.contextTypes = {
|
||||
router: PropTypes.shape({})
|
||||
}
|
||||
|
||||
TagSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func
|
||||
|
||||
}
|
||||
|
||||
export default CSSModules(TagSelect, styles)
|
||||
|
||||
@@ -39,16 +39,9 @@
|
||||
|
||||
.tag-label
|
||||
font-size 13px
|
||||
color: $ui-text-color
|
||||
color $ui-text-color
|
||||
padding 4px 16px 4px 8px
|
||||
|
||||
.newTag
|
||||
box-sizing border-box
|
||||
border none
|
||||
background-color transparent
|
||||
outline none
|
||||
padding 0 4px
|
||||
font-size 13px
|
||||
cursor pointer
|
||||
|
||||
body[data-theme="dark"]
|
||||
.tag
|
||||
@@ -62,11 +55,6 @@ body[data-theme="dark"]
|
||||
.tag-label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.tag
|
||||
background-color $ui-solarized-dark-tag-backgroundColor
|
||||
@@ -78,14 +66,9 @@ body[data-theme="solarized-dark"]
|
||||
.tag-label
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.tag
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
background-color $ui-monokai-tag-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-button--focus-borderColor
|
||||
@@ -93,8 +76,14 @@ body[data-theme="monokai"]
|
||||
|
||||
.tag-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.tag
|
||||
background-color $ui-dracula-tag-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-dracula-button--focus-borderColor
|
||||
background-color transparent
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.tag-label
|
||||
color $ui-dracula-borderColor
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ToggleModeButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const ToggleModeButton = ({
|
||||
onClick, editorType
|
||||
}) => (
|
||||
<div styleName='control-toggleModeButton'>
|
||||
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
||||
</div>
|
||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||
</div>
|
||||
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
ToggleModeButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
editorType: PropTypes.string.Required
|
||||
}
|
||||
|
||||
export default CSSModules(ToggleModeButton, styles)
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ToggleModeButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const ToggleModeButton = ({
|
||||
onClick, editorType
|
||||
}) => (
|
||||
<div styleName='control-toggleModeButton'>
|
||||
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
||||
</div>
|
||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||
</div>
|
||||
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
ToggleModeButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
editorType: PropTypes.string.Required
|
||||
}
|
||||
|
||||
export default CSSModules(ToggleModeButton, styles)
|
||||
|
||||
@@ -59,7 +59,14 @@ body[data-theme="solarized-dark"]
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.control-toggleModeButton
|
||||
background-color #272822
|
||||
background-color #373831
|
||||
.active
|
||||
background-color #1EC38B
|
||||
background-color #f92672
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.control-toggleModeButton
|
||||
background-color #44475a
|
||||
.active
|
||||
background-color #bd93f9
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
@@ -22,7 +22,6 @@ const electron = require('electron')
|
||||
const { remote } = electron
|
||||
|
||||
class Main extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
@@ -57,13 +56,13 @@ class Main extends React.Component {
|
||||
init () {
|
||||
dataApi
|
||||
.addStorage({
|
||||
name: 'My Storage',
|
||||
name: 'My Storage Location',
|
||||
path: path.join(remote.app.getPath('home'), 'Boostnote')
|
||||
})
|
||||
.then((data) => {
|
||||
.then(data => {
|
||||
return data
|
||||
})
|
||||
.then((data) => {
|
||||
.then(data => {
|
||||
if (data.storage.folders[0] != null) {
|
||||
return data
|
||||
} else {
|
||||
@@ -72,7 +71,7 @@ class Main extends React.Component {
|
||||
color: '#1278BD',
|
||||
name: 'Default'
|
||||
})
|
||||
.then((_data) => {
|
||||
.then(_data => {
|
||||
return {
|
||||
storage: _data.storage,
|
||||
notes: data.notes
|
||||
@@ -80,8 +79,7 @@ class Main extends React.Component {
|
||||
})
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
console.log(data)
|
||||
.then(data => {
|
||||
store.dispatch({
|
||||
type: 'ADD_STORAGE',
|
||||
storage: data.storage,
|
||||
@@ -98,16 +96,16 @@ class Main extends React.Component {
|
||||
{
|
||||
name: 'example.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',
|
||||
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({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
@@ -120,7 +118,7 @@ class Main extends React.Component {
|
||||
title: 'Welcome to Boostnote!',
|
||||
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
||||
})
|
||||
.then((note) => {
|
||||
.then(note => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
@@ -131,10 +129,10 @@ class Main extends React.Component {
|
||||
.then(defaultMarkdownNote)
|
||||
.then(() => data.storage)
|
||||
})
|
||||
.then((storage) => {
|
||||
.then(storage => {
|
||||
hashHistory.push('/storages/' + storage.key)
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
@@ -142,12 +140,7 @@ class Main extends React.Component {
|
||||
componentDidMount () {
|
||||
const { dispatch, config } = this.props
|
||||
|
||||
const supportedThemes = [
|
||||
'dark',
|
||||
'white',
|
||||
'solarized-dark',
|
||||
'monokai'
|
||||
]
|
||||
const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai', 'dracula']
|
||||
|
||||
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
|
||||
document.body.setAttribute('data-theme', config.ui.theme)
|
||||
@@ -162,19 +155,20 @@ class Main extends React.Component {
|
||||
}
|
||||
applyShortcuts()
|
||||
// Reload all data
|
||||
dataApi.init()
|
||||
.then((data) => {
|
||||
dispatch({
|
||||
type: 'INIT_ALL',
|
||||
storages: data.storages,
|
||||
notes: data.notes
|
||||
})
|
||||
|
||||
if (data.storages.length < 1) {
|
||||
this.init()
|
||||
}
|
||||
dataApi.init().then(data => {
|
||||
dispatch({
|
||||
type: 'INIT_ALL',
|
||||
storages: data.storages,
|
||||
notes: data.notes
|
||||
})
|
||||
|
||||
if (data.storages.length < 1) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
|
||||
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
||||
|
||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||
}
|
||||
|
||||
@@ -199,34 +193,40 @@ class Main extends React.Component {
|
||||
handleMouseUp (e) {
|
||||
// Change width of NoteList component.
|
||||
if (this.state.isRightSliderFocused) {
|
||||
this.setState({
|
||||
isRightSliderFocused: false
|
||||
}, () => {
|
||||
const { dispatch } = this.props
|
||||
const newListWidth = this.state.listWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({listWidth: newListWidth})
|
||||
dispatch({
|
||||
type: 'SET_LIST_WIDTH',
|
||||
listWidth: newListWidth
|
||||
})
|
||||
})
|
||||
this.setState(
|
||||
{
|
||||
isRightSliderFocused: false
|
||||
},
|
||||
() => {
|
||||
const { dispatch } = this.props
|
||||
const newListWidth = this.state.listWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({ listWidth: newListWidth })
|
||||
dispatch({
|
||||
type: 'SET_LIST_WIDTH',
|
||||
listWidth: newListWidth
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Change width of SideNav component.
|
||||
if (this.state.isLeftSliderFocused) {
|
||||
this.setState({
|
||||
isLeftSliderFocused: false
|
||||
}, () => {
|
||||
const { dispatch } = this.props
|
||||
const navWidth = this.state.navWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({ navWidth })
|
||||
dispatch({
|
||||
type: 'SET_NAV_WIDTH',
|
||||
navWidth
|
||||
})
|
||||
})
|
||||
this.setState(
|
||||
{
|
||||
isLeftSliderFocused: false
|
||||
},
|
||||
() => {
|
||||
const { dispatch } = this.props
|
||||
const navWidth = this.state.navWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({ navWidth })
|
||||
dispatch({
|
||||
type: 'SET_NAV_WIDTH',
|
||||
navWidth
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,8 +271,8 @@ class Main extends React.Component {
|
||||
}
|
||||
|
||||
hideLeftLists (noteDetail, noteList, mainBody) {
|
||||
this.setState({noteDetailWidth: noteDetail.style.left})
|
||||
this.setState({mainBodyWidth: mainBody.style.left})
|
||||
this.setState({ noteDetailWidth: noteDetail.style.left })
|
||||
this.setState({ mainBodyWidth: mainBody.style.left })
|
||||
noteDetail.style.left = '0px'
|
||||
mainBody.style.left = '0px'
|
||||
noteList.style.display = 'none'
|
||||
@@ -294,33 +294,36 @@ class Main extends React.Component {
|
||||
<div
|
||||
className='Main'
|
||||
styleName='root'
|
||||
onMouseMove={(e) => this.handleMouseMove(e)}
|
||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||
onMouseMove={e => this.handleMouseMove(e)}
|
||||
onMouseUp={e => this.handleMouseUp(e)}
|
||||
>
|
||||
<SideNav
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'location'
|
||||
])}
|
||||
{..._.pick(this.props, ['dispatch', 'data', 'config', 'params', 'location'])}
|
||||
width={this.state.navWidth}
|
||||
/>
|
||||
{!config.isSideNavFolded &&
|
||||
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'}
|
||||
style={{left: this.state.navWidth}}
|
||||
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)}
|
||||
<div
|
||||
styleName={
|
||||
this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
|
||||
}
|
||||
style={{ left: this.state.navWidth }}
|
||||
onMouseDown={e => this.handleLeftSlideMouseDown(e)}
|
||||
draggable='false'
|
||||
>
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
}
|
||||
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||
</div>}
|
||||
<div
|
||||
styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||
id='main-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, [
|
||||
'dispatch',
|
||||
'config',
|
||||
@@ -329,7 +332,8 @@ class Main extends React.Component {
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<NoteList style={{width: this.state.listWidth}}
|
||||
<NoteList
|
||||
style={{ width: this.state.listWidth }}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
@@ -338,15 +342,20 @@ class Main extends React.Component {
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<div styleName={this.state.isRightSliderFocused ? 'slider-right--active' : 'slider-right'}
|
||||
style={{left: this.state.listWidth - 1}}
|
||||
onMouseDown={(e) => this.handleRightSlideMouseDown(e)}
|
||||
<div
|
||||
styleName={
|
||||
this.state.isRightSliderFocused
|
||||
? 'slider-right--active'
|
||||
: 'slider-right'
|
||||
}
|
||||
style={{ left: this.state.listWidth - 1 }}
|
||||
onMouseDown={e => this.handleRightSlideMouseDown(e)}
|
||||
draggable='false'
|
||||
>
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
<Detail
|
||||
style={{left: this.state.listWidth}}
|
||||
style={{ left: this.state.listWidth }}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
@@ -374,4 +383,4 @@ Main.propTypes = {
|
||||
data: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
export default connect((x) => x)(CSSModules(Main, styles))
|
||||
export default connect(x => x)(CSSModules(Main, styles))
|
||||
|
||||
@@ -79,3 +79,7 @@ body[data-theme="solarized-dark"]
|
||||
body[data-theme="monokai"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
@@ -7,6 +7,7 @@ import modal from 'browser/main/lib/modal'
|
||||
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { dialog } = remote
|
||||
@@ -34,15 +35,23 @@ class NewNoteButton extends React.Component {
|
||||
}
|
||||
|
||||
handleNewNoteButtonClick (e) {
|
||||
const { location, dispatch } = this.props
|
||||
const { location, params, dispatch, config } = this.props
|
||||
const { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
modal.open(NewNoteModal, {
|
||||
storage: storage.key,
|
||||
folder: folder.key,
|
||||
dispatch,
|
||||
location
|
||||
})
|
||||
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
||||
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
|
||||
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
||||
createSnippetNote(storage.key, folder.key, dispatch, location, params, config)
|
||||
} else {
|
||||
modal.open(NewNoteModal, {
|
||||
storage: storage.key,
|
||||
folder: folder.key,
|
||||
dispatch,
|
||||
location,
|
||||
params,
|
||||
config
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resolveTargetFolder () {
|
||||
|
||||
@@ -84,7 +84,7 @@ body[data-theme="dark"]
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
.control-button--active
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
@@ -109,7 +109,7 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
|
||||
.control-button--active
|
||||
color $ui-solarized-dark-text-color
|
||||
&:active
|
||||
@@ -138,3 +138,27 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.control-sortBy-select
|
||||
&:hover
|
||||
transition 0.2s
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-button
|
||||
color $ui-dracula-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-button--active
|
||||
color $ui-dracula-text-color
|
||||
&:active
|
||||
color $ui-dracula-text-color
|
||||
@@ -56,7 +56,6 @@ class NoteList extends React.Component {
|
||||
super(props)
|
||||
|
||||
this.selectNextNoteHandler = () => {
|
||||
console.log('fired next')
|
||||
this.selectNextNote()
|
||||
}
|
||||
this.selectPriorNoteHandler = () => {
|
||||
@@ -65,13 +64,14 @@ class NoteList extends React.Component {
|
||||
this.focusHandler = () => {
|
||||
this.refs.list.focus()
|
||||
}
|
||||
this.alertIfSnippetHandler = () => {
|
||||
this.alertIfSnippet()
|
||||
this.alertIfSnippetHandler = (event, msg) => {
|
||||
this.alertIfSnippet(msg)
|
||||
}
|
||||
this.importFromFileHandler = this.importFromFile.bind(this)
|
||||
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
||||
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
|
||||
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
|
||||
this.cloneNote = this.cloneNote.bind(this)
|
||||
this.deleteNote = this.deleteNote.bind(this)
|
||||
this.focusNote = this.focusNote.bind(this)
|
||||
this.pinToTop = this.pinToTop.bind(this)
|
||||
@@ -80,10 +80,13 @@ class NoteList extends React.Component {
|
||||
this.getViewType = this.getViewType.bind(this)
|
||||
this.restoreNote = this.restoreNote.bind(this)
|
||||
this.copyNoteLink = this.copyNoteLink.bind(this)
|
||||
this.navigate = this.navigate.bind(this)
|
||||
|
||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||
this.state = {
|
||||
ctrlKeyDown: false,
|
||||
shiftKeyDown: false,
|
||||
prevShiftNoteIndex: -1,
|
||||
selectedNoteKeys: []
|
||||
}
|
||||
|
||||
@@ -94,10 +97,12 @@ class NoteList extends React.Component {
|
||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||
ee.on('list:next', this.selectNextNoteHandler)
|
||||
ee.on('list:prior', this.selectPriorNoteHandler)
|
||||
ee.on('list:clone', this.cloneNote)
|
||||
ee.on('list:focus', this.focusHandler)
|
||||
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||
ee.on('import:file', this.importFromFileHandler)
|
||||
ee.on('list:jump', this.jumpNoteByHash)
|
||||
ee.on('list:navigate', this.navigate)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@@ -115,6 +120,7 @@ class NoteList extends React.Component {
|
||||
|
||||
ee.off('list:next', this.selectNextNoteHandler)
|
||||
ee.off('list:prior', this.selectPriorNoteHandler)
|
||||
ee.off('list:clone', this.cloneNote)
|
||||
ee.off('list:focus', this.focusHandler)
|
||||
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||
ee.off('import:file', this.importFromFileHandler)
|
||||
@@ -170,16 +176,15 @@ class NoteList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
focusNote (selectedNoteKeys, noteKey) {
|
||||
focusNote (selectedNoteKeys, noteKey, pathname) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys
|
||||
})
|
||||
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
pathname,
|
||||
query: {
|
||||
key: noteKey
|
||||
}
|
||||
@@ -198,6 +203,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
const { location } = this.props
|
||||
|
||||
let targetIndex = this.getTargetIndex()
|
||||
|
||||
@@ -214,7 +220,7 @@ class NoteList extends React.Component {
|
||||
selectedNoteKeys.push(priorNoteKey)
|
||||
}
|
||||
|
||||
this.focusNote(selectedNoteKeys, priorNoteKey)
|
||||
this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname)
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
@@ -225,6 +231,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
const { location } = this.props
|
||||
|
||||
let targetIndex = this.getTargetIndex()
|
||||
const isTargetLastNote = targetIndex === this.notes.length - 1
|
||||
@@ -247,7 +254,7 @@ class NoteList extends React.Component {
|
||||
selectedNoteKeys.push(nextNoteKey)
|
||||
}
|
||||
|
||||
this.focusNote(selectedNoteKeys, nextNoteKey)
|
||||
this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname)
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
@@ -259,13 +266,13 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
const selectedNoteKeys = [noteHash]
|
||||
this.focusNote(selectedNoteKeys, noteHash)
|
||||
this.focusNote(selectedNoteKeys, noteHash, '/home')
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
|
||||
handleNoteListKeyDown (e) {
|
||||
if (e.metaKey || e.ctrlKey) return true
|
||||
if (e.metaKey) return true
|
||||
|
||||
// A key
|
||||
if (e.keyCode === 65 && !e.shiftKey) {
|
||||
@@ -273,12 +280,6 @@ class NoteList extends React.Component {
|
||||
ee.emit('top:new-note')
|
||||
}
|
||||
|
||||
// D key
|
||||
if (e.keyCode === 68) {
|
||||
e.preventDefault()
|
||||
this.deleteNote()
|
||||
}
|
||||
|
||||
// E key
|
||||
if (e.keyCode === 69) {
|
||||
e.preventDefault()
|
||||
@@ -305,6 +306,8 @@ class NoteList extends React.Component {
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.setState({ shiftKeyDown: true })
|
||||
} else if (e.ctrlKey) {
|
||||
this.setState({ ctrlKeyDown: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +315,10 @@ class NoteList extends React.Component {
|
||||
if (!e.shiftKey) {
|
||||
this.setState({ shiftKeyDown: false })
|
||||
}
|
||||
|
||||
if (!e.ctrlKey) {
|
||||
this.setState({ ctrlKeyDown: false })
|
||||
}
|
||||
}
|
||||
|
||||
getNotes () {
|
||||
@@ -388,25 +395,65 @@ class NoteList extends React.Component {
|
||||
return pinnedNotes.concat(unpinnedNotes)
|
||||
}
|
||||
|
||||
getNoteIndexByKey (noteKey) {
|
||||
return this.notes.findIndex((note) => {
|
||||
if (!note) return -1
|
||||
|
||||
return note.key === noteKey
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteClick (e, uniqueKey) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
||||
const { ctrlKeyDown, shiftKeyDown } = this.state
|
||||
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
||||
|
||||
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
if (ctrlKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
||||
this.setState({
|
||||
selectedNoteKeys: newSelectedNoteKeys
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!shiftKeyDown) {
|
||||
if (!ctrlKeyDown && !shiftKeyDown) {
|
||||
selectedNoteKeys = []
|
||||
}
|
||||
|
||||
if (!shiftKeyDown) {
|
||||
prevShiftNoteIndex = -1
|
||||
}
|
||||
|
||||
selectedNoteKeys.push(uniqueKey)
|
||||
|
||||
if (shiftKeyDown && hasSelectedNoteKey) {
|
||||
let firstShiftNoteIndex = this.getNoteIndexByKey(selectedNoteKeys[0])
|
||||
// Shift selection can either start from first note in the exisiting selectedNoteKeys
|
||||
// or previous first shift note index
|
||||
firstShiftNoteIndex = firstShiftNoteIndex > prevShiftNoteIndex
|
||||
? firstShiftNoteIndex : prevShiftNoteIndex
|
||||
|
||||
const lastShiftNoteIndex = this.getNoteIndexByKey(uniqueKey)
|
||||
|
||||
const startIndex = firstShiftNoteIndex < lastShiftNoteIndex
|
||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||
const endIndex = firstShiftNoteIndex > lastShiftNoteIndex
|
||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||
|
||||
selectedNoteKeys = []
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
selectedNoteKeys.push(this.notes[i].key)
|
||||
}
|
||||
|
||||
if (prevShiftNoteIndex < 0) {
|
||||
prevShiftNoteIndex = firstShiftNoteIndex
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys
|
||||
selectedNoteKeys,
|
||||
prevShiftNoteIndex
|
||||
})
|
||||
|
||||
router.push({
|
||||
@@ -418,10 +465,10 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
handleSortByChange (e) {
|
||||
const { dispatch } = this.props
|
||||
const { dispatch, params: { folderKey } } = this.props
|
||||
|
||||
const config = {
|
||||
sortBy: e.target.value
|
||||
[folderKey]: { sortBy: e.target.value }
|
||||
}
|
||||
|
||||
ConfigManager.set(config)
|
||||
@@ -445,14 +492,21 @@ class NoteList extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
alertIfSnippet () {
|
||||
alertIfSnippet (msg) {
|
||||
const warningMessage = (msg) => ({
|
||||
'export-txt': 'Text export',
|
||||
'export-md': 'Markdown export',
|
||||
'export-html': 'HTML export',
|
||||
'print': 'Print'
|
||||
})[msg]
|
||||
|
||||
const targetIndex = this.getTargetIndex()
|
||||
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: i18n.__('Sorry!'),
|
||||
detail: i18n.__('md/text import is available only a markdown note.'),
|
||||
buttons: [i18n.__('OK'), i18n.__('Cancel')]
|
||||
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
|
||||
buttons: [i18n.__('OK')]
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -517,7 +571,7 @@ class NoteList extends React.Component {
|
||||
click: this.cloneNote.bind(this)
|
||||
}, {
|
||||
label: copyNoteLink,
|
||||
click: this.copyNoteLink(note)
|
||||
click: this.copyNoteLink.bind(this, note)
|
||||
})
|
||||
if (note.type === 'MARKDOWN_NOTE') {
|
||||
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||
@@ -614,7 +668,6 @@ class NoteList extends React.Component {
|
||||
.catch((err) => {
|
||||
console.error('Cannot Delete note: ' + err)
|
||||
})
|
||||
console.log('Notes were all deleted')
|
||||
} else {
|
||||
if (!confirmDeleteNote(confirmDeletion, false)) return
|
||||
|
||||
@@ -634,7 +687,6 @@ class NoteList extends React.Component {
|
||||
})
|
||||
})
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||
console.log('Notes went to trash')
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Notes could not go to trash: ' + err)
|
||||
@@ -687,6 +739,16 @@ class NoteList extends React.Component {
|
||||
return copy(noteLink)
|
||||
}
|
||||
|
||||
navigate (sender, pathname) {
|
||||
const { router } = this.context
|
||||
router.push({
|
||||
pathname,
|
||||
query: {
|
||||
// key: noteKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
save (note) {
|
||||
const { dispatch } = this.props
|
||||
dataApi
|
||||
@@ -909,12 +971,13 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { location, config } = this.props
|
||||
const { location, config, params: { folderKey } } = this.props
|
||||
let { notes } = this.props
|
||||
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
|
||||
: config.sortBy === 'ALPHABETICAL'
|
||||
: sortBy === 'ALPHABETICAL'
|
||||
? sortByAlphabetical
|
||||
: sortByUpdatedAt
|
||||
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||
@@ -965,7 +1028,7 @@ class NoteList extends React.Component {
|
||||
notes.length === 1 ||
|
||||
(autoSelectFirst && index === 0)
|
||||
const dateDisplay = moment(
|
||||
config.sortBy === 'CREATED_AT'
|
||||
sortBy === 'CREATED_AT'
|
||||
? note.createdAt : note.updatedAt
|
||||
).fromNow('D')
|
||||
|
||||
@@ -983,6 +1046,7 @@ class NoteList extends React.Component {
|
||||
folderName={this.getNoteFolder(note).name}
|
||||
storageName={this.getNoteStorage(note).name}
|
||||
viewType={viewType}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1014,7 +1078,7 @@ class NoteList extends React.Component {
|
||||
<i className='fa fa-angle-down' />
|
||||
<select styleName='control-sortBy-select'
|
||||
title={i18n.__('Select filter mode')}
|
||||
value={config.sortBy}
|
||||
value={sortBy}
|
||||
onChange={(e) => this.handleSortByChange(e)}
|
||||
>
|
||||
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
text-align center
|
||||
|
||||
|
||||
|
||||
|
||||
.top-menu-label
|
||||
margin-left 5px
|
||||
overflow ellipsis
|
||||
@@ -122,3 +122,8 @@ body[data-theme="monokai"]
|
||||
.root, .root--folded
|
||||
background-color $ui-monokai-backgroundColor
|
||||
border-right 1px solid $ui-monokai-borderColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root, .root--folded
|
||||
background-color $ui-dracula-backgroundColor
|
||||
border-right 1px solid $ui-dracula-borderColor
|
||||
@@ -25,7 +25,8 @@ class StorageItem extends React.Component {
|
||||
const { storage } = this.props
|
||||
|
||||
this.state = {
|
||||
isOpen: !!storage.isOpen
|
||||
isOpen: !!storage.isOpen,
|
||||
draggedOver: null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +39,22 @@ class StorageItem extends React.Component {
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export Storage'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.__('Export as txt'),
|
||||
click: (e) => this.handleExportStorageClick(e, 'txt')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as md'),
|
||||
click: (e) => this.handleExportStorageClick(e, 'md')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Unlink Storage'),
|
||||
click: (e) => this.handleUnlinkStorageClick(e)
|
||||
@@ -68,6 +85,30 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleExportStorageClick (e, fileType) {
|
||||
const options = {
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
buttonLabel: i18n.__('Select directory'),
|
||||
title: i18n.__('Select a folder to export the files to'),
|
||||
multiSelections: false
|
||||
}
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
||||
(paths) => {
|
||||
if (paths && paths.length === 1) {
|
||||
const { storage, dispatch } = this.props
|
||||
dataApi
|
||||
.exportStorage(storage.key, fileType, paths[0])
|
||||
.then(data => {
|
||||
dispatch({
|
||||
type: 'EXPORT_STORAGE',
|
||||
storage: data.storage,
|
||||
fileType: data.fileType
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
const { storage, dispatch } = this.props
|
||||
const isOpen = !this.state.isOpen
|
||||
@@ -164,6 +205,20 @@ class StorageItem extends React.Component {
|
||||
folderKey: data.folderKey,
|
||||
fileType: data.fileType
|
||||
})
|
||||
return data
|
||||
})
|
||||
.then(data => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: 'Exported to "' + data.exportDir + '"'
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
dialog.showErrorBox(
|
||||
'Export error',
|
||||
err ? err.message || err : 'Unexpected error during export'
|
||||
)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -191,14 +246,20 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleDragEnter (e) {
|
||||
e.dataTransfer.setData('defaultColor', e.target.style.backgroundColor)
|
||||
e.target.style.backgroundColor = 'rgba(129, 130, 131, 0.08)'
|
||||
handleDragEnter (e, key) {
|
||||
e.preventDefault()
|
||||
if (this.state.draggedOver === key) { return }
|
||||
this.setState({
|
||||
draggedOver: key
|
||||
})
|
||||
}
|
||||
|
||||
handleDragLeave (e) {
|
||||
e.target.style.opacity = '1'
|
||||
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
||||
e.preventDefault()
|
||||
if (this.state.draggedOver === null) { return }
|
||||
this.setState({
|
||||
draggedOver: null
|
||||
})
|
||||
}
|
||||
|
||||
dropNote (storage, folder, dispatch, location, noteData) {
|
||||
@@ -223,8 +284,12 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
|
||||
handleDrop (e, storage, folder, dispatch, location) {
|
||||
e.target.style.opacity = '1'
|
||||
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
|
||||
e.preventDefault()
|
||||
if (this.state.draggedOver !== null) {
|
||||
this.setState({
|
||||
draggedOver: null
|
||||
})
|
||||
}
|
||||
const noteData = JSON.parse(e.dataTransfer.getData('note'))
|
||||
this.dropNote(storage, folder, dispatch, location, noteData)
|
||||
}
|
||||
@@ -234,7 +299,7 @@ class StorageItem extends React.Component {
|
||||
const { folderNoteMap, trashedSet } = data
|
||||
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||
const folderList = storage.folders.map((folder, index) => {
|
||||
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
||||
const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
||||
const isActive = !!(location.pathname.match(folderRegex))
|
||||
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||
|
||||
@@ -251,16 +316,22 @@ class StorageItem extends React.Component {
|
||||
<SortableStorageItemChild
|
||||
key={folder.key}
|
||||
index={index}
|
||||
isActive={isActive}
|
||||
isActive={isActive || folder.key === this.state.draggedOver}
|
||||
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
||||
folderName={folder.name}
|
||||
folderColor={folder.color}
|
||||
isFolded={isFolded}
|
||||
noteCount={noteCount}
|
||||
handleDrop={(e) => this.handleDrop(e, storage, folder, dispatch, location)}
|
||||
handleDragEnter={this.handleDragEnter}
|
||||
handleDragLeave={this.handleDragLeave}
|
||||
handleDrop={(e) => {
|
||||
this.handleDrop(e, storage, folder, dispatch, location)
|
||||
}}
|
||||
handleDragEnter={(e) => {
|
||||
this.handleDragEnter(e, folder.key)
|
||||
}}
|
||||
handleDragLeave={(e) => {
|
||||
this.handleDragLeave(e, folder)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -18,6 +18,12 @@ import TagButton from './TagButton'
|
||||
import {SortableContainer} from 'react-sortable-hoc'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import context from 'browser/lib/context'
|
||||
import { remote } from 'electron'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
|
||||
function matchActiveTags (tags, activeTags) {
|
||||
return _.every(activeTags, v => tags.indexOf(v) >= 0)
|
||||
}
|
||||
|
||||
class SideNav extends React.Component {
|
||||
// TODO: should not use electron stuff v0.7
|
||||
@@ -30,6 +36,52 @@ class SideNav extends React.Component {
|
||||
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
|
||||
}
|
||||
|
||||
deleteTag (tag) {
|
||||
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
ype: 'warning',
|
||||
message: i18n.__('Confirm tag deletion'),
|
||||
detail: i18n.__('This will permanently remove this tag.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
})
|
||||
|
||||
if (selectedButton === 0) {
|
||||
const { data, dispatch, location, params } = this.props
|
||||
|
||||
const notes = data.noteMap
|
||||
.map(note => note)
|
||||
.filter(note => note.tags.indexOf(tag) !== -1)
|
||||
.map(note => {
|
||||
note = Object.assign({}, note)
|
||||
note.tags = note.tags.slice()
|
||||
|
||||
note.tags.splice(note.tags.indexOf(tag), 1)
|
||||
|
||||
return note
|
||||
})
|
||||
|
||||
Promise
|
||||
.all(notes.map(note => dataApi.updateNote(note.storage, note.key, note)))
|
||||
.then(updatedNotes => {
|
||||
updatedNotes.forEach(note => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note
|
||||
})
|
||||
})
|
||||
|
||||
if (location.pathname.match('/tags')) {
|
||||
const tags = params.tagname.split(' ')
|
||||
const index = tags.indexOf(tag)
|
||||
if (index !== -1) {
|
||||
tags.splice(index, 1)
|
||||
|
||||
this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleMenuButtonClick (e) {
|
||||
openModal(PreferencesModal)
|
||||
}
|
||||
@@ -44,6 +96,17 @@ class SideNav extends React.Component {
|
||||
router.push('/starred')
|
||||
}
|
||||
|
||||
handleTagContextMenu (e, tag) {
|
||||
const menu = []
|
||||
|
||||
menu.push({
|
||||
label: i18n.__('Delete Tag'),
|
||||
click: this.deleteTag.bind(this, tag)
|
||||
})
|
||||
|
||||
context.popup(menu)
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
const { dispatch, config } = this.props
|
||||
|
||||
@@ -144,12 +207,20 @@ class SideNav extends React.Component {
|
||||
|
||||
tagListComponent () {
|
||||
const { data, location, config } = this.props
|
||||
const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
|
||||
const activeTags = this.getActiveTags(location.pathname)
|
||||
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
||||
let tagList = _.sortBy(data.tagNoteMap.map(
|
||||
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
||||
), ['name']).filter(
|
||||
).filter(
|
||||
tag => tag.size > 0
|
||||
)
|
||||
), ['name'])
|
||||
if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
|
||||
const notesTags = data.noteMap.map(note => note.tags)
|
||||
tagList = tagList.map(tag => {
|
||||
tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length
|
||||
return tag
|
||||
})
|
||||
}
|
||||
if (config.sortTagsBy === 'COUNTER') {
|
||||
tagList = _.sortBy(tagList, item => (0 - item.size))
|
||||
}
|
||||
@@ -165,6 +236,7 @@ class SideNav extends React.Component {
|
||||
name={tag.name}
|
||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
||||
handleContextMenu={this.handleTagContextMenu.bind(this)}
|
||||
isActive={this.getTagActive(location.pathname, tag.name)}
|
||||
isRelated={tag.related}
|
||||
key={tag.name}
|
||||
@@ -198,12 +270,12 @@ class SideNav extends React.Component {
|
||||
const tags = pathSegments[pathSegments.length - 1]
|
||||
return (tags === 'alltags')
|
||||
? []
|
||||
: tags.split(' ')
|
||||
: decodeURIComponent(tags).split(' ')
|
||||
}
|
||||
|
||||
handleClickTagListItem (name) {
|
||||
const { router } = this.context
|
||||
router.push(`/tags/${name}`)
|
||||
router.push(`/tags/${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
handleSortTagsByChange (e) {
|
||||
@@ -230,7 +302,7 @@ class SideNav extends React.Component {
|
||||
} else {
|
||||
listOfTags.push(tag)
|
||||
}
|
||||
router.push(`/tags/${listOfTags.join(' ')}`)
|
||||
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
|
||||
}
|
||||
|
||||
emptyTrash (entries) {
|
||||
@@ -238,6 +310,8 @@ class SideNav extends React.Component {
|
||||
const deletionPromises = entries.map((note) => {
|
||||
return dataApi.deleteNote(note.storage, note.key)
|
||||
})
|
||||
const { confirmDeletion } = this.props.config.ui
|
||||
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||
Promise.all(deletionPromises)
|
||||
.then((arrayOfStorageAndNoteKeys) => {
|
||||
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||
@@ -247,7 +321,6 @@ class SideNav extends React.Component {
|
||||
.catch((err) => {
|
||||
console.error('Cannot Delete note: ' + err)
|
||||
})
|
||||
console.log('Trash emptied')
|
||||
}
|
||||
|
||||
handleFilterButtonContextMenu (event) {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
color $ui-active-color
|
||||
span
|
||||
margin-left 5px
|
||||
|
||||
|
||||
.update
|
||||
navButtonColor()
|
||||
height 24px
|
||||
@@ -47,6 +47,14 @@
|
||||
.update-icon
|
||||
color $brand-color
|
||||
|
||||
body[data-theme="default"]
|
||||
.zoom
|
||||
color $ui-text-color
|
||||
|
||||
body[data-theme="white"]
|
||||
.zoom
|
||||
color $ui-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
@@ -80,3 +88,14 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-active-color
|
||||
&:active
|
||||
color $ui-monokai-active-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
navButtonColor()
|
||||
.zoom
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
transition 0.15s
|
||||
color $ui-dracula-active-color
|
||||
&:active
|
||||
color $ui-dracula-active-color
|
||||
@@ -5,6 +5,7 @@ import styles from './StatusBar.styl'
|
||||
import ZoomManager from 'browser/main/lib/ZoomManager'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import context from 'browser/lib/context'
|
||||
import EventEmitter from 'browser/main/lib/eventEmitter'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote, ipcRenderer } = electron
|
||||
@@ -13,6 +14,26 @@ const { dialog } = remote
|
||||
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
|
||||
|
||||
class StatusBar extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this)
|
||||
this.handleZoomOutMenuItem = this.handleZoomOutMenuItem.bind(this)
|
||||
this.handleZoomResetMenuItem = this.handleZoomResetMenuItem.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
EventEmitter.on('status:zoomin', this.handleZoomInMenuItem)
|
||||
EventEmitter.on('status:zoomout', this.handleZoomOutMenuItem)
|
||||
EventEmitter.on('status:zoomreset', this.handleZoomResetMenuItem)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
EventEmitter.off('status:zoomin', this.handleZoomInMenuItem)
|
||||
EventEmitter.off('status:zoomout', this.handleZoomOutMenuItem)
|
||||
EventEmitter.off('status:zoomreset', this.handleZoomResetMenuItem)
|
||||
}
|
||||
|
||||
updateApp () {
|
||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
@@ -48,6 +69,20 @@ class StatusBar extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleZoomInMenuItem () {
|
||||
const zoomFactor = ZoomManager.getZoom() + 0.1
|
||||
this.handleZoomMenuItemClick(zoomFactor)
|
||||
}
|
||||
|
||||
handleZoomOutMenuItem () {
|
||||
const zoomFactor = ZoomManager.getZoom() - 0.1
|
||||
this.handleZoomMenuItemClick(zoomFactor)
|
||||
}
|
||||
|
||||
handleZoomResetMenuItem () {
|
||||
this.handleZoomMenuItemClick(1.0)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, status } = this.context
|
||||
|
||||
|
||||
@@ -256,3 +256,25 @@ body[data-theme="monokai"]
|
||||
input
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
border-color $ui-dracula-borderColor
|
||||
.control-search
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control-search-icon
|
||||
absolute top bottom left
|
||||
line-height 32px
|
||||
width 35px
|
||||
color $ui-dracula-inactive-text-color
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control-search-input
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
input
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
@@ -18,6 +18,9 @@ body
|
||||
::-webkit-scrollbar
|
||||
width 12px
|
||||
|
||||
::-webkit-scrollbar-corner
|
||||
background-color: transparent;
|
||||
|
||||
::-webkit-scrollbar-thumb
|
||||
background-color rgba(0, 0, 0, 0.15)
|
||||
|
||||
@@ -132,6 +135,15 @@ body[data-theme="dark"]
|
||||
.CodeMirror-foldgutter-folded:after
|
||||
content: "\25B8"
|
||||
|
||||
.CodeMirror-hover
|
||||
padding 2px 4px 0 4px
|
||||
position absolute
|
||||
z-index 99
|
||||
|
||||
.CodeMirror-hyperlink
|
||||
cursor pointer
|
||||
|
||||
|
||||
.sortableItemHelper
|
||||
z-index modalZIndex + 5
|
||||
|
||||
@@ -152,3 +164,18 @@ body[data-theme="monokai"]
|
||||
background-color $ui-monokai-backgroundColor
|
||||
.sortableItemHelper
|
||||
color: $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
::-webkit-scrollbar-thumb
|
||||
background-color rgba(0, 0, 0, 0.3)
|
||||
.ModalBase
|
||||
.modalBack
|
||||
background-color $ui-dracula-backgroundColor
|
||||
.sortableItemHelper
|
||||
color: $ui-dracula-text-color
|
||||
|
||||
body[data-theme="default"]
|
||||
.SideNav ::-webkit-scrollbar-thumb
|
||||
background-color rgba(255, 255, 255, 0.3)
|
||||
|
||||
@import '../styles/Detail/TagSelect.styl'
|
||||
@@ -45,7 +45,6 @@ function initAwsMobileAnalytics () {
|
||||
if (getSendEventCond()) return
|
||||
AWS.config.credentials.get((err) => {
|
||||
if (!err) {
|
||||
console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
|
||||
recordDynamicCustomEvent('APP_STARTED')
|
||||
recordStaticCustomEvent()
|
||||
}
|
||||
@@ -58,7 +57,7 @@ function recordDynamicCustomEvent (type, options = {}) {
|
||||
mobileAnalyticsClient.recordEvent(type, options)
|
||||
} catch (analyticsError) {
|
||||
if (analyticsError instanceof ReferenceError) {
|
||||
console.log(analyticsError.name + ': ' + analyticsError.message)
|
||||
console.error(analyticsError.name + ': ' + analyticsError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +70,7 @@ function recordStaticCustomEvent () {
|
||||
})
|
||||
} catch (analyticsError) {
|
||||
if (analyticsError instanceof ReferenceError) {
|
||||
console.log(analyticsError.name + ': ' + analyticsError.message)
|
||||
console.error(analyticsError.name + ': ' + analyticsError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,17 @@ export const DEFAULT_CONFIG = {
|
||||
isSideNavFolded: false,
|
||||
listWidth: 280,
|
||||
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'
|
||||
amaEnabled: true,
|
||||
hotkey: {
|
||||
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
||||
toggleMode: OSX ? 'Command + M' : 'Ctrl + M'
|
||||
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
||||
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
|
||||
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V'
|
||||
},
|
||||
ui: {
|
||||
language: 'en',
|
||||
@@ -41,10 +45,16 @@ export const DEFAULT_CONFIG = {
|
||||
enableRulers: false,
|
||||
rulers: [80, 120],
|
||||
displayLineNumbers: true,
|
||||
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
|
||||
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
||||
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
|
||||
scrollPastEnd: false,
|
||||
type: 'SPLIT',
|
||||
fetchUrlTitle: true
|
||||
type: 'SPLIT', // 'SPLIT', 'EDITOR_PREVIEW'
|
||||
fetchUrlTitle: true,
|
||||
enableTableEditor: false,
|
||||
enableFrontMatterTitle: true,
|
||||
frontMatterTitleField: 'title',
|
||||
spellcheck: false,
|
||||
enableSmartPaste: false
|
||||
},
|
||||
preview: {
|
||||
fontSize: '14',
|
||||
@@ -57,12 +67,14 @@ export const DEFAULT_CONFIG = {
|
||||
latexBlockClose: '$$',
|
||||
plantUMLServerAddress: 'http://www.plantuml.com/plantuml',
|
||||
scrollPastEnd: false,
|
||||
scrollSync: true,
|
||||
smartQuotes: true,
|
||||
breaks: true,
|
||||
smartArrows: false,
|
||||
allowCustomCSS: false,
|
||||
customCSS: '',
|
||||
sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||
lineThroughCheckbox: true
|
||||
},
|
||||
blog: {
|
||||
type: 'wordpress', // Available value: wordpress, add more types in the future plz
|
||||
@@ -144,6 +156,8 @@ function set (updates) {
|
||||
document.body.setAttribute('data-theme', 'solarized-dark')
|
||||
} else if (newConfig.ui.theme === 'monokai') {
|
||||
document.body.setAttribute('data-theme', 'monokai')
|
||||
} else if (newConfig.ui.theme === 'dracula') {
|
||||
document.body.setAttribute('data-theme', 'dracula')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
@@ -192,6 +206,7 @@ function rewriteHotkey (config) {
|
||||
const keys = [...Object.keys(config.hotkey)]
|
||||
keys.forEach(key => {
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
|
||||
})
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -11,6 +11,118 @@ 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
|
||||
* Create a Image element to get the real size of image.
|
||||
* @param {File} file the File object dropped.
|
||||
* @returns {Promise<Image>} Image element created
|
||||
*/
|
||||
function getImage (file) {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader()
|
||||
const img = new Image()
|
||||
img.onload = () => resolve(img)
|
||||
reader.onload = e => {
|
||||
img.src = e.target.result
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
* Get the orientation info from iamges's EXIF data.
|
||||
* case 1: The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
|
||||
* case 2: The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
|
||||
* case 3: The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
|
||||
* case 4: The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
|
||||
* case 5: The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
|
||||
* case 6: The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
|
||||
* case 7: The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
|
||||
* case 8: The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
|
||||
* Other: reserved
|
||||
* ref: http://sylvana.net/jpegcrop/exif_orientation.html
|
||||
* @param {File} file the File object dropped.
|
||||
* @returns {Promise<Number>} Orientation info
|
||||
*/
|
||||
function getOrientation (file) {
|
||||
const getData = arrayBuffer => {
|
||||
const view = new DataView(arrayBuffer)
|
||||
|
||||
// Not start with SOI(Start of image) Marker return fail value
|
||||
if (view.getUint16(0, false) !== 0xFFD8) return -2
|
||||
const length = view.byteLength
|
||||
let offset = 2
|
||||
while (offset < length) {
|
||||
const marker = view.getUint16(offset, false)
|
||||
offset += 2
|
||||
// Loop and seed for APP1 Marker
|
||||
if (marker === 0xFFE1) {
|
||||
// return fail value if it isn't EXIF data
|
||||
if (view.getUint32(offset += 2, false) !== 0x45786966) {
|
||||
return -1
|
||||
}
|
||||
// Read TIFF header,
|
||||
// First 2bytes defines byte align of TIFF data.
|
||||
// If it is 0x4949="II", it means "Intel" type byte align.
|
||||
// If it is 0x4d4d="MM", it means "Motorola" type byte align
|
||||
const little = view.getUint16(offset += 6, false) === 0x4949
|
||||
offset += view.getUint32(offset + 4, little)
|
||||
const tags = view.getUint16(offset, little) // Get TAG number
|
||||
offset += 2
|
||||
for (let i = 0; i < tags; i++) {
|
||||
// Loop to find Orientation TAG and return the value
|
||||
if (view.getUint16(offset + (i * 12), little) === 0x0112) {
|
||||
return view.getUint16(offset + (i * 12) + 8, little)
|
||||
}
|
||||
}
|
||||
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker
|
||||
break
|
||||
} else {
|
||||
offset += view.getUint16(offset, false)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = event => resolve(getData(event.target.result))
|
||||
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
|
||||
})
|
||||
}
|
||||
/**
|
||||
* @description
|
||||
* Rotate image file to correct direction.
|
||||
* Create a canvas and draw the image with correct direction, then export to base64 format.
|
||||
* @param {*} file the File object dropped.
|
||||
* @return {String} Base64 encoded image.
|
||||
*/
|
||||
function fixRotate (file) {
|
||||
return Promise.all([getImage(file), getOrientation(file)])
|
||||
.then(([img, orientation]) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (orientation > 4 && orientation < 9) {
|
||||
canvas.width = img.height
|
||||
canvas.height = img.width
|
||||
} else {
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
}
|
||||
switch (orientation) {
|
||||
case 2: ctx.transform(-1, 0, 0, 1, img.width, 0); break
|
||||
case 3: ctx.transform(-1, 0, 0, -1, img.width, img.height); break
|
||||
case 4: ctx.transform(1, 0, 0, -1, 0, img.height); break
|
||||
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break
|
||||
case 6: ctx.transform(0, 1, -1, 0, img.height, 0); break
|
||||
case 7: ctx.transform(0, -1, -1, 0, img.height, img.width); break
|
||||
case 8: ctx.transform(0, -1, 1, 0, 0, img.width); break
|
||||
default: break
|
||||
}
|
||||
ctx.drawImage(img, 0, 0)
|
||||
return canvas.toDataURL()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
@@ -38,26 +150,34 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(sourceFilePath)) {
|
||||
reject('source file does not exist')
|
||||
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
|
||||
if (!fs.existsSync(sourceFilePath) && !isBase64) {
|
||||
return 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)}`
|
||||
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath.sourceFilePath || sourceFilePath)}`
|
||||
} else {
|
||||
destinationName = path.basename(sourceFilePath)
|
||||
destinationName = path.basename(sourceFilePath.sourceFilePath || 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)
|
||||
})
|
||||
|
||||
if (isBase64) {
|
||||
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
|
||||
const dataBuffer = new Buffer(base64Data, 'base64')
|
||||
outputFile.write(dataBuffer, () => {
|
||||
resolve(destinationName)
|
||||
})
|
||||
} else {
|
||||
const inputFileStream = fs.createReadStream(sourceFilePath)
|
||||
inputFileStream.pipe(outputFile)
|
||||
inputFileStream.on('end', () => {
|
||||
resolve(destinationName)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
return reject(e)
|
||||
}
|
||||
@@ -82,9 +202,9 @@ function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
|
||||
* @param noteKey Key of the current note
|
||||
*/
|
||||
function migrateAttachments (markdownContent, storagePath, noteKey) {
|
||||
if (sander.existsSync(path.join(storagePath, 'images'))) {
|
||||
if (noteKey !== undefined && sander.existsSync(path.join(storagePath, 'images'))) {
|
||||
const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
|
||||
if (attachments !== []) {
|
||||
if (attachments.length) {
|
||||
createAttachmentDestinationFolder(storagePath, noteKey)
|
||||
}
|
||||
for (const attachment of attachments) {
|
||||
@@ -137,10 +257,17 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
||||
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)
|
||||
const isImage = fileType.startsWith('image')
|
||||
let promise
|
||||
if (isImage) {
|
||||
promise = fixRotate(file).then(base64data => {
|
||||
return copyAttachment({type: 'base64', data: base64data, sourceFilePath: filePath}, storageKey, noteKey)
|
||||
})
|
||||
} else {
|
||||
promise = copyAttachment(filePath, storageKey, noteKey)
|
||||
}
|
||||
promise.then((fileName) => {
|
||||
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), isImage)
|
||||
codeEditor.insertAttachmentMd(imageMd)
|
||||
})
|
||||
}
|
||||
@@ -189,6 +316,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
|
||||
reader.readAsDataURL(blob)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
|
||||
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
|
||||
* @param {String} storageKey Key of the current storage
|
||||
* @param {String} noteKey Key of the current note
|
||||
* @param {NativeImage} image The native image
|
||||
*/
|
||||
function handlePastNativeImage (codeEditor, storageKey, noteKey, image) {
|
||||
if (!codeEditor) {
|
||||
throw new Error('codeEditor has to be given')
|
||||
}
|
||||
if (!storageKey) {
|
||||
throw new Error('storageKey has to be given')
|
||||
}
|
||||
|
||||
if (!noteKey) {
|
||||
throw new Error('noteKey has to be given')
|
||||
}
|
||||
if (!image) {
|
||||
throw new Error('image has to be given')
|
||||
}
|
||||
|
||||
const targetStorage = findStorage.findStorage(storageKey)
|
||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||
|
||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||
|
||||
const imageName = `${uniqueSlug()}.png`
|
||||
const imagePath = path.join(destinationDir, imageName)
|
||||
|
||||
const binaryData = image.toPNG()
|
||||
fs.writeFileSync(imagePath, binaryData, 'binary')
|
||||
|
||||
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
|
||||
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
|
||||
codeEditor.insertAttachmentMd(imageMd)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns all attachment paths of the given markdown
|
||||
* @param {String} markdownContent content in which the attachment paths should be found
|
||||
@@ -402,7 +567,6 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
|
||||
return modifiedLinkText
|
||||
})
|
||||
} else {
|
||||
console.log('One if the parameters was null -> Do nothing..')
|
||||
return Promise.resolve(linkText)
|
||||
}
|
||||
}
|
||||
@@ -413,6 +577,7 @@ module.exports = {
|
||||
generateAttachmentMarkdown,
|
||||
handleAttachmentDrop,
|
||||
handlePastImageEvent,
|
||||
handlePastNativeImage,
|
||||
getAttachmentsInMarkdownContent,
|
||||
getAbsolutePathsOfAttachmentsInContent,
|
||||
removeStorageAndNoteReferences,
|
||||
|
||||
@@ -16,7 +16,7 @@ function copyFile (srcPath, dstPath) {
|
||||
const dstFolder = path.dirname(dstPath)
|
||||
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
||||
|
||||
const input = fs.createReadStream(srcPath)
|
||||
const input = fs.createReadStream(decodeURI(srcPath))
|
||||
const output = fs.createWriteStream(dstPath)
|
||||
|
||||
output.on('error', reject)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import resolveStorageData from './resolveStorageData'
|
||||
import resolveStorageNotes from './resolveStorageNotes'
|
||||
import exportNote from './exportNote'
|
||||
import filenamify from 'filenamify'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
@@ -45,9 +45,9 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
||||
|
||||
notes
|
||||
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
||||
.forEach(snippet => {
|
||||
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
|
||||
fs.writeFileSync(notePath, snippet.content)
|
||||
.forEach(note => {
|
||||
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
|
||||
exportNote(note.key, storage.path, note.content, notePath, null)
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,27 +4,43 @@ import { findStorage } from 'browser/lib/findStorage'
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const attachmentManagement = require('./attachmentManagement')
|
||||
|
||||
/**
|
||||
* Export note together with images
|
||||
* Export note together with attachments
|
||||
*
|
||||
* If images is stored in the storage, creates 'images' subfolder in target directory
|
||||
* and copies images to it. Changes links to images in the content of the note
|
||||
* If attachments are stored in the storage, creates 'attachments' subfolder in target directory
|
||||
* and copies attachments to it. Changes links to images in the content of the note
|
||||
*
|
||||
* @param {String} nodeKey key of the node that should be exported
|
||||
* @param {String} storageKey or storage path
|
||||
* @param {String} noteContent Content to export
|
||||
* @param {String} targetPath Path to exported file
|
||||
* @param {function} outputFormatter
|
||||
* @return {Promise.<*[]>}
|
||||
*/
|
||||
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
||||
function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) {
|
||||
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
||||
const exportTasks = []
|
||||
|
||||
if (!storagePath) {
|
||||
throw new Error('Storage path is not found')
|
||||
}
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||
noteContent,
|
||||
storagePath
|
||||
)
|
||||
attachmentsAbsolutePaths.forEach(attachment => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
|
||||
let exportedData = noteContent
|
||||
let exportedData = attachmentManagement.removeStorageAndNoteReferences(
|
||||
noteContent,
|
||||
nodeKey
|
||||
)
|
||||
|
||||
if (outputFormatter) {
|
||||
exportedData = outputFormatter(exportedData, exportTasks)
|
||||
|
||||
63
browser/main/lib/dataApi/exportStorage.js
Normal file
63
browser/main/lib/dataApi/exportStorage.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import resolveStorageData from './resolveStorageData'
|
||||
import resolveStorageNotes from './resolveStorageNotes'
|
||||
import filenamify from 'filenamify'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {String} fileType
|
||||
* @param {String} exportDir
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
* {
|
||||
* storage: Object,
|
||||
* fileType: String,
|
||||
* exportDir: String
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
function exportStorage (storageKey, fileType, exportDir) {
|
||||
let targetStorage
|
||||
try {
|
||||
targetStorage = findStorage(storageKey)
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(storage => (
|
||||
resolveStorageNotes(storage).then(notes => ({storage, notes}))
|
||||
))
|
||||
.then(function exportNotes (data) {
|
||||
const { storage, notes } = data
|
||||
const folderNamesMapping = {}
|
||||
storage.folders.forEach(folder => {
|
||||
const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
|
||||
folderNamesMapping[folder.key] = folderExportedDir
|
||||
// make sure directory exists
|
||||
try {
|
||||
fs.mkdirSync(folderExportedDir)
|
||||
} catch (e) {}
|
||||
})
|
||||
notes
|
||||
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
|
||||
.forEach(markdownNote => {
|
||||
const folderExportedDir = folderNamesMapping[markdownNote.folder]
|
||||
const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}`
|
||||
const notePath = path.join(folderExportedDir, snippetName)
|
||||
fs.writeFileSync(notePath, markdownNote.content)
|
||||
})
|
||||
|
||||
return {
|
||||
storage,
|
||||
fileType,
|
||||
exportDir
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = exportStorage
|
||||
@@ -9,6 +9,7 @@ const dataApi = {
|
||||
deleteFolder: require('./deleteFolder'),
|
||||
reorderFolder: require('./reorderFolder'),
|
||||
exportFolder: require('./exportFolder'),
|
||||
exportStorage: require('./exportStorage'),
|
||||
createNote: require('./createNote'),
|
||||
updateNote: require('./updateNote'),
|
||||
deleteNote: require('./deleteNote'),
|
||||
|
||||
@@ -14,7 +14,6 @@ function renameStorage (key, name) {
|
||||
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
||||
} catch (err) {
|
||||
console.log('error got')
|
||||
console.error(err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
@@ -31,13 +31,9 @@ function resolveStorageData (storageCache) {
|
||||
|
||||
const version = parseInt(storage.version, 10)
|
||||
if (version >= 1) {
|
||||
if (version > 1) {
|
||||
console.log('The repository version is newer than one of current app.')
|
||||
}
|
||||
return Promise.resolve(storage)
|
||||
}
|
||||
|
||||
console.log('Transform Legacy storage', storage.path)
|
||||
return migrateFromV6Storage(storage.path)
|
||||
.then(() => storage)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ function resolveStorageNotes (storage) {
|
||||
notePathList = sander.readdirSync(notesDirPath)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log(notesDirPath, ' doesn\'t exist.')
|
||||
console.error(notesDirPath, ' doesn\'t exist.')
|
||||
sander.mkdirSync(notesDirPath)
|
||||
} else {
|
||||
console.warn('Failed to find note dir', notesDirPath, err)
|
||||
|
||||
@@ -12,7 +12,6 @@ function toggleStorage (key, isOpen) {
|
||||
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
||||
} catch (err) {
|
||||
console.log('error got')
|
||||
console.error(err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ function once (name, listener) {
|
||||
}
|
||||
|
||||
function emit (name, ...args) {
|
||||
console.log(name)
|
||||
remote.getCurrentWindow().webContents.send(name, ...args)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,13 @@ nodeIpc.connectTo(
|
||||
path.join(app.getPath('userData'), 'boostnote.service'),
|
||||
function () {
|
||||
nodeIpc.of.node.on('error', function (err) {
|
||||
console.log(err)
|
||||
console.error(err)
|
||||
})
|
||||
nodeIpc.of.node.on('connect', function () {
|
||||
console.log('Connected successfully')
|
||||
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
|
||||
})
|
||||
nodeIpc.of.node.on('disconnect', function () {
|
||||
console.log('disconnected')
|
||||
return
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,5 +3,8 @@ import ee from 'browser/main/lib/eventEmitter'
|
||||
module.exports = {
|
||||
'toggleMode': () => {
|
||||
ee.emit('topbar:togglemodebutton')
|
||||
},
|
||||
'deleteNote': () => {
|
||||
ee.emit('hotkey:deletenote')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,3 +128,29 @@ body[data-theme="monokai"]
|
||||
|
||||
.control-confirmButton
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
modalDracula()
|
||||
width 500px
|
||||
height 270px
|
||||
overflow hidden
|
||||
position relative
|
||||
|
||||
.header
|
||||
background-color transparent
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-folder-label
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-folder-input
|
||||
border 1px solid $ui-input--create-folder-modal
|
||||
color white
|
||||
|
||||
.description
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.control-confirmButton
|
||||
colorDraculaPrimaryButton()
|
||||
@@ -1,12 +1,9 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NewNoteModal.styl'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import { hashHistory } from 'react-router'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
|
||||
|
||||
class NewNoteModal extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -24,31 +21,10 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleMarkdownNoteButtonClick (e) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
const { storage, folder, dispatch, location } = this.props
|
||||
dataApi
|
||||
.createNote(storage, {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: folder,
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
.then(note => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: { key: noteHash }
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
const { storage, folder, dispatch, location, params, config } = this.props
|
||||
createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => {
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
}
|
||||
|
||||
handleMarkdownNoteButtonKeyDown (e) {
|
||||
@@ -59,38 +35,10 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleSnippetNoteButtonClick (e) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
const { storage, folder, dispatch, location } = this.props
|
||||
|
||||
dataApi
|
||||
.createNote(storage, {
|
||||
type: 'SNIPPET_NOTE',
|
||||
folder: folder,
|
||||
title: '',
|
||||
description: '',
|
||||
snippets: [
|
||||
{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}
|
||||
]
|
||||
})
|
||||
.then(note => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: { key: noteHash }
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
const { storage, folder, dispatch, location, params, config } = this.props
|
||||
createSnippetNote(storage, folder, dispatch, location, params, config).then(() => {
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
}
|
||||
|
||||
handleSnippetNoteButtonKeyDown (e) {
|
||||
|
||||
@@ -97,3 +97,20 @@ body[data-theme="monokai"]
|
||||
|
||||
.description
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
background-color transparent
|
||||
|
||||
.header
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-button
|
||||
border-color $ui-dracula-borderColor
|
||||
color $ui-dracula-text-color
|
||||
background-color transparent
|
||||
&:focus
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.description
|
||||
color $ui-dracula-text-color
|
||||
@@ -43,7 +43,7 @@ class Blog extends React.Component {
|
||||
this.handleSettingError = (err) => {
|
||||
this.setState({BlogAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('Error occurs!')
|
||||
message: err.message != null ? err.message : i18n.__('An error occurred!')
|
||||
}})
|
||||
}
|
||||
this.oldBlog = this.state.config.blog
|
||||
@@ -70,7 +70,7 @@ class Blog extends React.Component {
|
||||
this.props.haveToSave({
|
||||
tab: 'Blog',
|
||||
type: 'warning',
|
||||
message: i18n.__('You have to save!')
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
line-height 30px
|
||||
|
||||
.group-section-label
|
||||
width 150px
|
||||
width 200px
|
||||
text-align left
|
||||
margin-right 10px
|
||||
font-size 14px
|
||||
@@ -68,9 +68,9 @@
|
||||
:global
|
||||
.alert
|
||||
display inline-block
|
||||
position absolute
|
||||
top 60px
|
||||
right 15px
|
||||
position fixed
|
||||
top 130px
|
||||
right 100px
|
||||
font-size 14px
|
||||
.success
|
||||
color #1EC38B
|
||||
@@ -127,7 +127,7 @@ colorDarkControl()
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
colorSolarizedDarkControl()
|
||||
border none
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
@@ -138,6 +138,10 @@ colorMonokaiControl()
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
colorDraculaControl()
|
||||
border none
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
@@ -165,27 +169,27 @@ body[data-theme="dark"]
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorDarkControl()
|
||||
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.group-header
|
||||
color $ui-solarized-dark-text-color
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
|
||||
.group-header2
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.group-section-control-input
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
|
||||
.group-control
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
.group-control-leftButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
.group-control-rightButton
|
||||
colorSolarizedDarkPrimaryButton()
|
||||
.group-hint
|
||||
@@ -200,19 +204,19 @@ body[data-theme="monokai"]
|
||||
|
||||
.group-header
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.group-header2
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.group-section-control-input
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.group-control
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
.group-control-leftButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
.group-control-rightButton
|
||||
colorMonokaiPrimaryButton()
|
||||
.group-hint
|
||||
@@ -220,3 +224,30 @@ body[data-theme="monokai"]
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorMonokaiControl()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.group-header
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.group-header2
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.group-section-control-input
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.group-control
|
||||
border-color $ui-dracula-borderColor
|
||||
.group-control-leftButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dracula-borderColor
|
||||
.group-control-rightButton
|
||||
colorDraculaPrimaryButton()
|
||||
.group-hint
|
||||
colorDraculaControl()
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorDraculaControl()
|
||||
@@ -23,21 +23,29 @@ class Crowdfunding extends React.Component {
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='header'>{i18n.__('Crowdfunding')}</div>
|
||||
<p>{i18n.__('Dear everyone,')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('Thank you for using Boostnote!')}</p>
|
||||
<p>{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('To continue supporting this growth, and to satisfy community expectations,')}</p>
|
||||
<p>{i18n.__('we would like to invest more time and resources in this project.')}</p>
|
||||
<p>{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}</p>
|
||||
<p>{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('If you like this project and see its potential, you can help by supporting us on OpenCollective!')}</p>
|
||||
<p>{i18n.__('### Sustainable Open Source Ecosystem')}</p>
|
||||
<p>{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}</p>
|
||||
<p>{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}</p>
|
||||
<p>{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('Thanks,')}</p>
|
||||
<p>{i18n.__('Boostnote maintainers')}</p>
|
||||
<p>{i18n.__('### We believe Meritocracy')}</p>
|
||||
<p>{i18n.__('We think developers who has skill and did great things must be rewarded properly.')}</p>
|
||||
<p>{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}</p>
|
||||
<p>{i18n.__('It sometimes looks like exploitation.')}</p>
|
||||
<p>{i18n.__('We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('As same as issues of Boostnote are already funded on IssueHunt, your open-source projects can be also started funding from now.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('Thank you,')}</p>
|
||||
<p>{i18n.__('The Boostnote Team')}</p>
|
||||
<br />
|
||||
<button styleName='cf-link'>
|
||||
<a href='https://opencollective.com/boostnoteio' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('Support via OpenCollective')}</a>
|
||||
<a href='http://bit.ly/issuehunt-from-boostnote-app' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('See IssueHunt')}</a>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ p
|
||||
body[data-theme="dark"]
|
||||
p
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
color $ui-solarized-dark-text-color
|
||||
@@ -41,3 +41,9 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
p
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
p
|
||||
color $ui-dracula-text-color
|
||||
@@ -2,6 +2,7 @@
|
||||
height 35px
|
||||
box-sizing border-box
|
||||
padding 2.5px 15px
|
||||
display flex
|
||||
&:hover
|
||||
background-color darken(white, 3%)
|
||||
|
||||
@@ -18,7 +19,10 @@
|
||||
border-left solid 2px transparent
|
||||
padding 0 10px
|
||||
line-height 30px
|
||||
float left
|
||||
flex 1
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
.folderItem-left-danger
|
||||
color $danger-color
|
||||
font-weight bold
|
||||
@@ -52,7 +56,8 @@
|
||||
outline none
|
||||
|
||||
.folderItem-right
|
||||
float right
|
||||
-webkit-box-flex: 1
|
||||
white-space nowrap
|
||||
|
||||
.folderItem-right-button
|
||||
vertical-align middle
|
||||
@@ -149,3 +154,26 @@ body[data-theme="monokai"]
|
||||
|
||||
.folderItem-right-dangerButton
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.folderItem
|
||||
&:hover
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
|
||||
.folderItem-left-danger
|
||||
color $danger-color
|
||||
|
||||
.folderItem-left-key
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.folderItem-left-colorButton
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.folderItem-right-button
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.folderItem-right-confirmButton
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.folderItem-right-dangerButton
|
||||
colorDraculaPrimaryButton()
|
||||
@@ -28,10 +28,20 @@ class HotkeyTab extends React.Component {
|
||||
}})
|
||||
}
|
||||
this.handleSettingError = (err) => {
|
||||
this.setState({keymapAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('Error occurs!')
|
||||
}})
|
||||
if (
|
||||
this.state.config.hotkey.toggleMain === '' ||
|
||||
this.state.config.hotkey.toggleMode === ''
|
||||
) {
|
||||
this.setState({keymapAlert: {
|
||||
type: 'success',
|
||||
message: i18n.__('Successfully applied!')
|
||||
}})
|
||||
} else {
|
||||
this.setState({keymapAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('An error occurred!')
|
||||
}})
|
||||
}
|
||||
}
|
||||
this.oldHotkey = this.state.config.hotkey
|
||||
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||
@@ -68,7 +78,9 @@ class HotkeyTab extends React.Component {
|
||||
const { config } = this.state
|
||||
config.hotkey = {
|
||||
toggleMain: this.refs.toggleMain.value,
|
||||
toggleMode: this.refs.toggleMode.value
|
||||
toggleMode: this.refs.toggleMode.value,
|
||||
deleteNote: this.refs.deleteNote.value,
|
||||
pasteSmartly: this.refs.pasteSmartly.value
|
||||
}
|
||||
this.setState({
|
||||
config
|
||||
@@ -79,7 +91,7 @@ class HotkeyTab extends React.Component {
|
||||
this.props.haveToSave({
|
||||
tab: 'Hotkey',
|
||||
type: 'warning',
|
||||
message: i18n.__('You have to save!')
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -117,7 +129,7 @@ class HotkeyTab extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Toggle editor mode')}</div>
|
||||
<div styleName='group-section-label'>{i18n.__('Toggle Editor Mode')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleHotkeyChange(e)}
|
||||
@@ -127,6 +139,28 @@ class HotkeyTab extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Delete Note')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleHotkeyChange(e)}
|
||||
ref='deleteNote'
|
||||
value={config.hotkey.deleteNote}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Paste Smartly')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleHotkeyChange(e)}
|
||||
ref='pasteSmartly'
|
||||
value={config.hotkey.pasteSmartly}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-control'>
|
||||
<button styleName='group-control-leftButton'
|
||||
onClick={(e) => this.handleHintToggleButtonClick(e)}
|
||||
|
||||
@@ -84,7 +84,7 @@ class InfoTab extends React.Component {
|
||||
>{i18n.__('GitHub')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://boostlog.io/@junp1234'
|
||||
<a href='https://medium.com/boostnote'
|
||||
onClick={(e) => this.handleLinkClick(e)}
|
||||
>{i18n.__('Blog')}</a>
|
||||
</li>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
color alpha($tab--dark-text-color, 80%)
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
@@ -75,3 +75,10 @@ body[data-theme="monokai"]
|
||||
.list
|
||||
a
|
||||
color $ui-monokai-active-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
.list
|
||||
a
|
||||
color $ui-dracula-active-color
|
||||
|
||||
@@ -90,7 +90,7 @@ body[data-theme="dark"]
|
||||
background-color $dark-primary-button-background--active
|
||||
&:hover
|
||||
color white
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
@@ -139,3 +139,27 @@ body[data-theme="monokai"]
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
&:hover
|
||||
color white
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
background-color transparent
|
||||
.top-bar
|
||||
background-color transparent
|
||||
border-color $ui-dracula-borderColor
|
||||
p
|
||||
color $ui-dracula-text-color
|
||||
.nav
|
||||
background-color transparent
|
||||
border-color $ui-dracula-borderColor
|
||||
.nav-button
|
||||
background-color transparent
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.nav-button--active
|
||||
@extend .nav-button
|
||||
color $ui-dracula-button--active-color
|
||||
background-color $ui-dracula-button--active-backgroundColor
|
||||
&:hover
|
||||
color #f8f8f2
|
||||
@@ -22,7 +22,7 @@ class SnippetList extends React.Component {
|
||||
reloadSnippetList () {
|
||||
dataApi.fetchSnippet().then(snippets => {
|
||||
this.setState({snippets})
|
||||
this.props.onSnippetSelect(snippets[0])
|
||||
this.props.onSnippetSelect(this.props.currentSnippet)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,7 +55,11 @@ class SnippetList extends React.Component {
|
||||
|
||||
defineSnippetStyleName (snippet) {
|
||||
const { currentSnippet } = this.props
|
||||
if (currentSnippet == null) return
|
||||
|
||||
if (currentSnippet == null) {
|
||||
return 'snippet-item'
|
||||
}
|
||||
|
||||
if (currentSnippet.id === snippet.id) {
|
||||
return 'snippet-item-selected'
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,9 @@ import i18n from 'browser/lib/i18n'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import SnippetList from './SnippetList'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
const path = require('path')
|
||||
|
||||
class SnippetTab extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -16,6 +19,17 @@ class SnippetTab extends React.Component {
|
||||
this.changeDelay = null
|
||||
}
|
||||
|
||||
notify (title, options) {
|
||||
if (global.process.platform === 'win32') {
|
||||
options.icon = path.join(
|
||||
'file://',
|
||||
global.__dirname,
|
||||
'../../resources/app.png'
|
||||
)
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
handleSnippetNameOrPrefixChange () {
|
||||
clearTimeout(this.changeDelay)
|
||||
this.changeDelay = setTimeout(() => {
|
||||
@@ -27,12 +41,14 @@ class SnippetTab extends React.Component {
|
||||
|
||||
handleSnippetSelect (snippet) {
|
||||
const { currentSnippet } = this.state
|
||||
if (currentSnippet === null || currentSnippet.id !== snippet.id) {
|
||||
dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
|
||||
// notify the snippet editor to load the content of the new snippet
|
||||
this.snippetEditor.onSnippetChanged(changedSnippet)
|
||||
this.setState({currentSnippet: changedSnippet})
|
||||
})
|
||||
if (snippet !== null) {
|
||||
if (currentSnippet === null || currentSnippet.id !== snippet.id) {
|
||||
dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
|
||||
// notify the snippet editor to load the content of the new snippet
|
||||
this.snippetEditor.onSnippetChanged(changedSnippet)
|
||||
this.setState({currentSnippet: changedSnippet})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +70,17 @@ class SnippetTab extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleCopySnippet (e) {
|
||||
const showCopyNotification = this.props.config.ui.showCopyNotification
|
||||
copy(this.state.currentSnippet.content)
|
||||
if (showCopyNotification) {
|
||||
this.notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, storageKey } = this.props
|
||||
const { currentSnippet } = this.state
|
||||
@@ -70,6 +97,13 @@ class SnippetTab extends React.Component {
|
||||
onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
|
||||
currentSnippet={currentSnippet} />
|
||||
<div styleName='snippet-detail' style={{visibility: currentSnippet ? 'visible' : 'hidden'}}>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-control'>
|
||||
<button styleName='group-control-rightButton'
|
||||
onClick={e => this.handleCopySnippet(e)}>{i18n.__('Copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Snippet name')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
|
||||
@@ -196,3 +196,19 @@ body[data-theme="monokai"]
|
||||
color white
|
||||
.group-control-button
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.snippets
|
||||
background $ui-dracula-backgroundColor
|
||||
.snippet-item
|
||||
color #f8f8f2
|
||||
&::after
|
||||
background $ui-dracula-borderColor
|
||||
&:hover
|
||||
background darken($ui-dracula-backgroundColor, 5)
|
||||
.snippet-item-selected
|
||||
background darken($ui-dracula-backgroundColor, 5)
|
||||
.snippet-detail
|
||||
color #f8f8f2
|
||||
.group-control-button
|
||||
colorDraculaPrimaryButton()
|
||||
@@ -9,13 +9,17 @@
|
||||
box-sizing border-box
|
||||
border-bottom $default-border
|
||||
margin-bottom 5px
|
||||
display flex
|
||||
|
||||
.header-label
|
||||
float left
|
||||
cursor pointer
|
||||
&:hover
|
||||
.header-label-editButton
|
||||
opacity 1
|
||||
flex 1
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
|
||||
.header-label-path
|
||||
color $ui-inactive-text-color
|
||||
@@ -38,8 +42,8 @@
|
||||
outline none
|
||||
|
||||
.header-control
|
||||
float right
|
||||
|
||||
-webkit-box-flex: 1
|
||||
white-space nowrap
|
||||
.header-control-button
|
||||
width 30px
|
||||
height 25px
|
||||
|
||||
@@ -70,7 +70,7 @@ class StoragesTab extends React.Component {
|
||||
})
|
||||
return (
|
||||
<div styleName='list'>
|
||||
<div styleName='header'>{i18n.__('Storages')}</div>
|
||||
<div styleName='header'>{i18n.__('Storage Locations')}</div>
|
||||
{storageList.length > 0
|
||||
? storageList
|
||||
: <div styleName='list-empty'>{i18n.__('No storage found.')}</div>
|
||||
|
||||
@@ -158,7 +158,7 @@ body[data-theme="dark"]
|
||||
.addStorage-body-control-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
@@ -236,3 +236,41 @@ body[data-theme="monokai"]
|
||||
.addStorage-body-control-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.folderList-item
|
||||
border-bottom $ui-dracula-borderColor
|
||||
|
||||
.folderList-empty
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.list-empty
|
||||
color $ui-dracula-text-color
|
||||
.list-control-addStorageButton
|
||||
border-color $ui-dracula-button-backgroundColor
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.addStorage-header
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.addStorage-body-section-name-input
|
||||
border-color $$ui-dracula-borderColor
|
||||
|
||||
.addStorage-body-section-type-description
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.addStorage-body-section-path-button
|
||||
colorPrimaryButton()
|
||||
.addStorage-body-control
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.addStorage-body-control-createButton
|
||||
colorDarkPrimaryButton()
|
||||
.addStorage-body-control-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dracula-borderColor
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user