mirror of
https://github.com/zadam/trilium.git
synced 2025-12-16 05:09:54 +01:00
Compare commits
963 Commits
v0.96.0
...
feature/fs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6bc65471d | ||
|
|
eb07d4b0ed | ||
|
|
2c096f3080 | ||
|
|
bac95c97e5 | ||
|
|
fe6daac979 | ||
|
|
770281214b | ||
|
|
15bd5aa4e4 | ||
|
|
3da6838395 | ||
|
|
16cdd9e137 | ||
|
|
28f88f2407 | ||
|
|
e525a7a0ff | ||
|
|
3415f38e0a | ||
|
|
910c0faade | ||
|
|
4ad1bb5e3a | ||
|
|
97f6f0a945 | ||
|
|
bc78c17a11 | ||
|
|
b8e813f7bd | ||
|
|
db3581eb26 | ||
|
|
d23230df68 | ||
|
|
b29781b614 | ||
|
|
7d7c3e7cdb | ||
|
|
cbd8cb80ab | ||
|
|
bfcdc34faf | ||
|
|
c728e6047d | ||
|
|
4c53a9ba8c | ||
|
|
e10a7da7e3 | ||
|
|
5cc431b1bf | ||
|
|
734aa2fcb5 | ||
|
|
5e37319d9b | ||
|
|
2e9eb6e3e9 | ||
|
|
9ce57b123a | ||
|
|
e793168afa | ||
|
|
d1513424e7 | ||
|
|
1436a01dbe | ||
|
|
b9b936b92a | ||
|
|
adf14bec31 | ||
|
|
ca1403ffea | ||
|
|
06672e439e | ||
|
|
e851701a9e | ||
|
|
9589164008 | ||
|
|
a88b067081 | ||
|
|
b3777e6900 | ||
|
|
d2646e291d | ||
|
|
99ab9ee66b | ||
|
|
08678e74e6 | ||
|
|
62de52ab17 | ||
|
|
d9820d9725 | ||
|
|
fe8a8eeac9 | ||
|
|
dfeb414aff | ||
|
|
69f12a2916 | ||
|
|
2b062e938e | ||
|
|
e0299bd1ae | ||
|
|
ac2f1b56fe | ||
|
|
06d98f6fcf | ||
|
|
bb660d15b2 | ||
|
|
4d73cdefef | ||
|
|
313ba3df80 | ||
|
|
15377c32c2 | ||
|
|
22b52f7c4a | ||
|
|
7055f77c91 | ||
|
|
051fe67176 | ||
|
|
90accfcc48 | ||
|
|
4f99db0c90 | ||
|
|
aeb356bf54 | ||
|
|
0dffa0f333 | ||
|
|
d17f5b8447 | ||
|
|
b5a57b3c66 | ||
|
|
987a3404a9 | ||
|
|
eddc30769f | ||
|
|
4d455650ba | ||
|
|
e2157aab26 | ||
|
|
b277f4bf3f | ||
|
|
4047452b0f | ||
|
|
cb37724879 | ||
|
|
8890893412 | ||
|
|
d0cbda7c0d | ||
|
|
60e7b9ffb0 | ||
|
|
45457c6f76 | ||
|
|
737f41d92b | ||
|
|
180841f364 | ||
|
|
bea40d4c2f | ||
|
|
5f9a054441 | ||
|
|
f90bf1ce7c | ||
|
|
8c4ed2d4da | ||
|
|
0e590a1bbf | ||
|
|
218a096135 | ||
|
|
8407bce370 | ||
|
|
43229f0b99 | ||
|
|
84fa0002b9 | ||
|
|
e79c705b20 | ||
|
|
894d7ce15d | ||
|
|
5830880582 | ||
|
|
caab0f70ff | ||
|
|
641966fcdd | ||
|
|
24c22e9bbf | ||
|
|
795f597bda | ||
|
|
2228663a7e | ||
|
|
0c97df357d | ||
|
|
19f63f1be0 | ||
|
|
fc000caf73 | ||
|
|
78929e0293 | ||
|
|
71e22da987 | ||
|
|
24e99d9654 | ||
|
|
98299da424 | ||
|
|
7014af66b6 | ||
|
|
659bd90027 | ||
|
|
146b0c284b | ||
|
|
4a0ac8807f | ||
|
|
d67734832e | ||
|
|
1673bf026a | ||
|
|
1f29b000a9 | ||
|
|
a6d024123e | ||
|
|
fb1a7239ce | ||
|
|
4f71d508cb | ||
|
|
2072bd61d1 | ||
|
|
6021178b7d | ||
|
|
179b0be2bb | ||
|
|
bf2b45dd4a | ||
|
|
513561234c | ||
|
|
33da990ae7 | ||
|
|
4003946e68 | ||
|
|
21f8d40789 | ||
|
|
d6c698e1d6 | ||
|
|
6c227852ae | ||
|
|
29cb22c4fd | ||
|
|
d040bc9e2d | ||
|
|
abb92f23a6 | ||
|
|
da5c86bb69 | ||
|
|
a0d428b12c | ||
|
|
e22fe20e23 | ||
|
|
1e6659aff9 | ||
|
|
60b32d5b05 | ||
|
|
e2ee9053a0 | ||
|
|
d2f0422ecc | ||
|
|
bfd97da626 | ||
|
|
1fd163f0bb | ||
|
|
d15ce575df | ||
|
|
9999ff5a89 | ||
|
|
4653941082 | ||
|
|
fa509661ab | ||
|
|
d9a289bf18 | ||
|
|
98c76b713d | ||
|
|
05ed917a56 | ||
|
|
b833806ec7 | ||
|
|
7fdef3418a | ||
|
|
49e14ec542 | ||
|
|
efd9244684 | ||
|
|
318f2d1f8c | ||
|
|
92fa1cf052 | ||
|
|
17c6eb1680 | ||
|
|
7c6af568d8 | ||
|
|
23c9c6826e | ||
|
|
b08fda5e10 | ||
|
|
5ec3a49377 | ||
|
|
1c728ae432 | ||
|
|
fd25c735c1 | ||
|
|
7de33907c5 | ||
|
|
ec021be16c | ||
|
|
8b6826ffa4 | ||
|
|
00cc1ffe74 | ||
|
|
2384fdbaad | ||
|
|
08a93d81d7 | ||
|
|
86911100df | ||
|
|
ff01656268 | ||
|
|
d0ea6d9e8d | ||
|
|
96ca3d5e38 | ||
|
|
3a569499cb | ||
|
|
545b19f978 | ||
|
|
d98be19c9a | ||
|
|
4826898c55 | ||
|
|
482b592f77 | ||
|
|
939ebfe47b | ||
|
|
c6dee1339b | ||
|
|
23f8c3ad3c | ||
|
|
81c1b88376 | ||
|
|
c4a85db698 | ||
|
|
e6eda45c04 | ||
|
|
a3014434cf | ||
|
|
3ebab2c126 | ||
|
|
954619bd36 | ||
|
|
eb76362de4 | ||
|
|
1cde14859b | ||
|
|
c752b98995 | ||
|
|
1f792ca418 | ||
|
|
b22e08b1eb | ||
|
|
2b5029cc38 | ||
|
|
9e936cb57b | ||
|
|
e8fd2c1b3c | ||
|
|
977fbf54ee | ||
|
|
3e5c91415d | ||
|
|
d60b855f74 | ||
|
|
4146192b6d | ||
|
|
26ee0ff48f | ||
|
|
6995fbfd06 | ||
|
|
1763d80d5f | ||
|
|
a594e5147c | ||
|
|
e51ea1a619 | ||
|
|
83b72eafa6 | ||
|
|
757a6777be | ||
|
|
37c9260dca | ||
|
|
e1a8f4f5db | ||
|
|
b7b0b39afc | ||
|
|
af797489e8 | ||
|
|
d003e91b89 | ||
|
|
4a35df745a | ||
|
|
b1b756b179 | ||
|
|
9e3372df72 | ||
|
|
657df7a728 | ||
|
|
944f0b694b | ||
|
|
efd409da17 | ||
|
|
08d60c554c | ||
|
|
a428ea7beb | ||
|
|
f69878b082 | ||
|
|
c5ffc2882b | ||
|
|
765691751a | ||
|
|
f19e5977c2 | ||
|
|
8f8b9af862 | ||
|
|
3e7dc71995 | ||
|
|
2a25cd8686 | ||
|
|
7664839135 | ||
|
|
47daebc65a | ||
|
|
0d18b944b6 | ||
|
|
951b5384a3 | ||
|
|
11547ecaa3 | ||
|
|
713a0f5b09 | ||
|
|
2cf9c98b43 | ||
|
|
d7af196a0c | ||
|
|
c363be57b7 | ||
|
|
10645790de | ||
|
|
8b18cf382c | ||
|
|
7a131e0bcc | ||
|
|
3d264379cc | ||
|
|
f405682ec1 | ||
|
|
3debf3ce1c | ||
|
|
5a76883969 | ||
|
|
6f51c5e0cc | ||
|
|
2c730d1f0b | ||
|
|
d487da0b2f | ||
|
|
cb8a5cbb62 | ||
|
|
ceb08593d8 | ||
|
|
9dd0eb7b9b | ||
|
|
ebff644d24 | ||
|
|
beb1c15fa5 | ||
|
|
40a5eee211 | ||
|
|
8f393d0bae | ||
|
|
94dad49e2f | ||
|
|
409638151c | ||
|
|
0d3de92890 | ||
|
|
5d619131ec | ||
|
|
e2c8443778 | ||
|
|
daa4743967 | ||
|
|
56553078ef | ||
|
|
5584a06cb3 | ||
|
|
cfeb69ace6 | ||
|
|
b0c8f110de | ||
|
|
aba1266c45 | ||
|
|
c331e0103d | ||
|
|
13978574e0 | ||
|
|
be85963558 | ||
|
|
8c19261ced | ||
|
|
7ca17fa609 | ||
|
|
3d107572df | ||
|
|
f7488655a7 | ||
|
|
876e0a29d4 | ||
|
|
af74375695 | ||
|
|
896965fec5 | ||
|
|
ba5ef93c1a | ||
|
|
ef1153d336 | ||
|
|
0d347f8823 | ||
|
|
897cdc26ae | ||
|
|
aba621c099 | ||
|
|
839813ebde | ||
|
|
545e2ddbfc | ||
|
|
1d63a5903a | ||
|
|
2b34c00a0c | ||
|
|
123068062a | ||
|
|
9a668e8709 | ||
|
|
f6f8937d64 | ||
|
|
c9f53a2880 | ||
|
|
2887e712c3 | ||
|
|
5d3a0ed1b4 | ||
|
|
334b6319de | ||
|
|
4c118c0fd4 | ||
|
|
db00d60684 | ||
|
|
25b74af363 | ||
|
|
eb57cf97ad | ||
|
|
c92e24363f | ||
|
|
8d5d00ac0f | ||
|
|
8b457384ba | ||
|
|
fab2d53ece | ||
|
|
774f27d8d2 | ||
|
|
d7f02ef1b3 | ||
|
|
97eaa6294c | ||
|
|
dc02bb0850 | ||
|
|
2c8c041e1c | ||
|
|
874b1c6654 | ||
|
|
fb982c7097 | ||
|
|
b7f5ce600e | ||
|
|
91604c9e26 | ||
|
|
c874333a37 | ||
|
|
1298b968f2 | ||
|
|
6fe5a854a7 | ||
|
|
aba3b5cb19 | ||
|
|
282aed22b5 | ||
|
|
669a3d9dcf | ||
|
|
9d7455d28a | ||
|
|
4f0c8b081c | ||
|
|
a5db5298a0 | ||
|
|
876c6e9252 | ||
|
|
aef824d262 | ||
|
|
a25ce42490 | ||
|
|
8b0fdaccf4 | ||
|
|
bd840a2421 | ||
|
|
27d515f289 | ||
|
|
df3b9faf8d | ||
|
|
0f129734ae | ||
|
|
275aacfba9 | ||
|
|
e7f47a0663 | ||
|
|
66486541fe | ||
|
|
34f1a84769 | ||
|
|
2244f0368f | ||
|
|
9d85005255 | ||
|
|
ad8629dca6 | ||
|
|
cccfe0e05a | ||
|
|
a8874257e8 | ||
|
|
f689c55f56 | ||
|
|
853c7be8b8 | ||
|
|
823df1e12d | ||
|
|
7570f818e9 | ||
|
|
03aa5aea2c | ||
|
|
a4e86ac353 | ||
|
|
cf6efc050a | ||
|
|
3e0802176b | ||
|
|
697954d4d9 | ||
|
|
741f6c1114 | ||
|
|
b2237ffa51 | ||
|
|
7b6d11bffa | ||
|
|
97565e8f36 | ||
|
|
c0dfee8439 | ||
|
|
fc98240614 | ||
|
|
169d1203c2 | ||
|
|
f3350bc8f5 | ||
|
|
504a19275c | ||
|
|
14cdc52670 | ||
|
|
cf8063f311 | ||
|
|
aa8902f5b9 | ||
|
|
7cd0e664ac | ||
|
|
a04804d3fa | ||
|
|
86f90e6685 | ||
|
|
8131a4b3d2 | ||
|
|
b91a3e13b0 | ||
|
|
5a7a0d32d1 | ||
|
|
3f5df18d6c | ||
|
|
df2cede075 | ||
|
|
4321c161ac | ||
|
|
b1f0c64ef2 | ||
|
|
c9b37dcc77 | ||
|
|
ab093ed9a0 | ||
|
|
cf31367acd | ||
|
|
e3d306cac3 | ||
|
|
960d321019 | ||
|
|
2d4ac93221 | ||
|
|
d4a4f15416 | ||
|
|
504a842d37 | ||
|
|
ded5b1f5d2 | ||
|
|
fcbbc21a80 | ||
|
|
38fce25b86 | ||
|
|
4cc2fa5300 | ||
|
|
4a82c3f65a | ||
|
|
b255d70e18 | ||
|
|
caa842cd55 | ||
|
|
cd338085fb | ||
|
|
e703ce92a8 | ||
|
|
84479a2c2a | ||
|
|
c13969217c | ||
|
|
402540f483 | ||
|
|
8c56315313 | ||
|
|
b29c3eff6e | ||
|
|
ec7dacfc9b | ||
|
|
5f9a6a9f76 | ||
|
|
28f4aea3d5 | ||
|
|
8d29c5fe1b | ||
|
|
ccd935b562 | ||
|
|
d77a49857b | ||
|
|
e30478e5d4 | ||
|
|
71863752cd | ||
|
|
e4a2a8e56d | ||
|
|
0f1c505823 | ||
|
|
1ecce11113 | ||
|
|
2287d67fb5 | ||
|
|
5b4f17ef3d | ||
|
|
3720ab6df6 | ||
|
|
3c893d69e5 | ||
|
|
b93a4a3e42 | ||
|
|
23cef0ab94 | ||
|
|
c8ffb8d694 | ||
|
|
08e08d8920 | ||
|
|
7acd300163 | ||
|
|
d8d95db4ec | ||
|
|
af97d3ef1d | ||
|
|
c65ec14943 | ||
|
|
adfdc7edb4 | ||
|
|
8cced607eb | ||
|
|
5dd5af90c2 | ||
|
|
7a48333b4f | ||
|
|
7044533398 | ||
|
|
560aad8df6 | ||
|
|
36c2099b2e | ||
|
|
6c157675d7 | ||
|
|
458d66cb21 | ||
|
|
201e8911c5 | ||
|
|
1b1ed2408f | ||
|
|
62487d21d8 | ||
|
|
bc752bdb0b | ||
|
|
9e00d421fb | ||
|
|
e7f02fe22b | ||
|
|
6d694f8e53 | ||
|
|
977befd0a7 | ||
|
|
1566ae4fbd | ||
|
|
4e97490cc6 | ||
|
|
446d5a0fcc | ||
|
|
1fd6465012 | ||
|
|
6cea8e3b87 | ||
|
|
28a63e0326 | ||
|
|
b73da46111 | ||
|
|
abafa8c2d2 | ||
|
|
4ae3272cdf | ||
|
|
6aa3b8dbd7 | ||
|
|
395e9b2228 | ||
|
|
be33f68c52 | ||
|
|
29d96381fa | ||
|
|
da8eecf774 | ||
|
|
de91326c12 | ||
|
|
ee1c3c35d7 | ||
|
|
70eece1429 | ||
|
|
b4f2be332b | ||
|
|
23fe76989b | ||
|
|
275d07659d | ||
|
|
a901e92573 | ||
|
|
6ead31b45f | ||
|
|
d4ce12dca9 | ||
|
|
bb6e22cdb7 | ||
|
|
2c9fc4812e | ||
|
|
60f4554afa | ||
|
|
3c486bfd1b | ||
|
|
26b9a95bb2 | ||
|
|
f7c9217cea | ||
|
|
e92022b73c | ||
|
|
61ff2353c8 | ||
|
|
c8cca26ca4 | ||
|
|
aa556ed4d5 | ||
|
|
5d694a7bdf | ||
|
|
c4787dae23 | ||
|
|
9f5f329c53 | ||
|
|
f82b96fcc4 | ||
|
|
d4b24fa427 | ||
|
|
c852f67c59 | ||
|
|
92c228a3c9 | ||
|
|
42f948e2b3 | ||
|
|
13e8932117 | ||
|
|
910d34bd42 | ||
|
|
b204ba29e7 | ||
|
|
d49244cbc8 | ||
|
|
ef2f2f17b4 | ||
|
|
b9f21dcf4c | ||
|
|
808fe690cc | ||
|
|
901eec04e5 | ||
|
|
9272394ada | ||
|
|
4457982fae | ||
|
|
7f67b2b461 | ||
|
|
7f3934f4c3 | ||
|
|
a3b80a2cc4 | ||
|
|
6d967e5e51 | ||
|
|
b674ca90d1 | ||
|
|
95edb60a84 | ||
|
|
40add78ccb | ||
|
|
1029c24c06 | ||
|
|
94d94fe8fb | ||
|
|
49489c0f45 | ||
|
|
215833a2c9 | ||
|
|
a7471a3d47 | ||
|
|
909aaefbd7 | ||
|
|
15c2f56bf2 | ||
|
|
84cdfec415 | ||
|
|
91572ab8b9 | ||
|
|
ed758f4c92 | ||
|
|
f1fc15e115 | ||
|
|
22300e8151 | ||
|
|
292646e14a | ||
|
|
b4921a20d8 | ||
|
|
54be79a725 | ||
|
|
4fc47370fe | ||
|
|
9e30bcf233 | ||
|
|
e5712c54e6 | ||
|
|
2a4fe21a39 | ||
|
|
b259558f0f | ||
|
|
e2f6d9e0d6 | ||
|
|
4fc2b0fa5e | ||
|
|
8dca79ecf2 | ||
|
|
c7f49f0e21 | ||
|
|
bce2094fb2 | ||
|
|
65c33e1aa0 | ||
|
|
8e108bc5e2 | ||
|
|
4e75ce7fdb | ||
|
|
1e42574d28 | ||
|
|
85ebaf6afa | ||
|
|
661c7e4056 | ||
|
|
1e8ea54dbc | ||
|
|
ddbe7e9936 | ||
|
|
cab86175ef | ||
|
|
ec7414b174 | ||
|
|
8343a5d1dd | ||
|
|
18c55784c7 | ||
|
|
39eac83d38 | ||
|
|
55bd6fb57d | ||
|
|
6fdec52332 | ||
|
|
824a3c5fcc | ||
|
|
87da644027 | ||
|
|
4f42f543d8 | ||
|
|
97ea3ac3fc | ||
|
|
f04b75fd36 | ||
|
|
f5bffc38f1 | ||
|
|
27738acefc | ||
|
|
59ce2072c5 | ||
|
|
ed68dda70b | ||
|
|
892ab02f06 | ||
|
|
7d9196d5e1 | ||
|
|
dccdb5ceb7 | ||
|
|
f961698e44 | ||
|
|
278fe3262e | ||
|
|
1fc860b052 | ||
|
|
88a8311173 | ||
|
|
63dc5697dd | ||
|
|
b595d1fade | ||
|
|
d91c59b7d0 | ||
|
|
aa2ab0da31 | ||
|
|
91f94106fb | ||
|
|
308f319138 | ||
|
|
fa0c01591a | ||
|
|
cb5a771490 | ||
|
|
0c17a13462 | ||
|
|
04593cb2d7 | ||
|
|
b6f50b6af0 | ||
|
|
fc454cba03 | ||
|
|
6f165df29e | ||
|
|
d16468071d | ||
|
|
20a492523f | ||
|
|
1216f51c78 | ||
|
|
ea3ac1041b | ||
|
|
d838e8baf0 | ||
|
|
60a7347d7d | ||
|
|
4e05e79426 | ||
|
|
aa872f47f2 | ||
|
|
fbd833ad86 | ||
|
|
bee65ed32c | ||
|
|
5adca76a9a | ||
|
|
e7467f6446 | ||
|
|
e49473fbd3 | ||
|
|
bfec44aa5a | ||
|
|
55b3bf6036 | ||
|
|
c9c07f0cb0 | ||
|
|
e25727441d | ||
|
|
51b7955ccd | ||
|
|
196bba9cda | ||
|
|
430ed78d85 | ||
|
|
2d11ed805d | ||
|
|
f55426bdb0 | ||
|
|
87b5068fec | ||
|
|
9ddd1a4ae2 | ||
|
|
736bc9c9bd | ||
|
|
5a2da62992 | ||
|
|
1a72eb91ee | ||
|
|
0d3c5b06e2 | ||
|
|
035b72a08d | ||
|
|
fc4a595725 | ||
|
|
444969bcf4 | ||
|
|
2cb6b14eca | ||
|
|
468b5022a4 | ||
|
|
c1897563ca | ||
|
|
5e533896b9 | ||
|
|
d3ceb7cfc1 | ||
|
|
731f74f421 | ||
|
|
46d82651a3 | ||
|
|
b3108c7e2b | ||
|
|
0cb988470e | ||
|
|
5a030014b0 | ||
|
|
2a43ef4dae | ||
|
|
6b5f9fc6ff | ||
|
|
b3a156c20d | ||
|
|
24340d3a8e | ||
|
|
2fac2a8c5e | ||
|
|
decb0c702d | ||
|
|
d45ff6cca5 | ||
|
|
83833e668c | ||
|
|
2cc181d1ac | ||
|
|
a946ce3534 | ||
|
|
3e9f476b37 | ||
|
|
de65c748a4 | ||
|
|
8a2bfb9d7b | ||
|
|
a1ced31fea | ||
|
|
989a9f506e | ||
|
|
59d55e2489 | ||
|
|
2b312a9234 | ||
|
|
16d9b982c2 | ||
|
|
a5600e75f5 | ||
|
|
f91dea62b6 | ||
|
|
4915ffcf2a | ||
|
|
9dbea2aa18 | ||
|
|
45f6a70fb8 | ||
|
|
96b4c611cc | ||
|
|
4e559d6594 | ||
|
|
db1a599f95 | ||
|
|
040964bbb7 | ||
|
|
dc6a303154 | ||
|
|
f88f14c983 | ||
|
|
f870649256 | ||
|
|
ed4dc30a6e | ||
|
|
ce9010ff13 | ||
|
|
994e9fa852 | ||
|
|
9df7d6227e | ||
|
|
242a576548 | ||
|
|
c1a5808f37 | ||
|
|
5c6bb99d78 | ||
|
|
63c408c45b | ||
|
|
2a665dffbc | ||
|
|
6509acd6ee | ||
|
|
4853d45609 | ||
|
|
fe78c1fee3 | ||
|
|
8102172557 | ||
|
|
a1341e6036 | ||
|
|
d31af2ddc2 | ||
|
|
a563330136 | ||
|
|
a58e5789bc | ||
|
|
68e258f23b | ||
|
|
dd18866156 | ||
|
|
1b1f1957c3 | ||
|
|
ff6b4effbd | ||
|
|
06fa59239c | ||
|
|
557bfbd1d6 | ||
|
|
f5a6dfa629 | ||
|
|
ce33dfb003 | ||
|
|
7b1c058d29 | ||
|
|
04c8f8a123 | ||
|
|
d15fccb1d8 | ||
|
|
229dd9cd18 | ||
|
|
a4faaa406b | ||
|
|
b6d2de54b2 | ||
|
|
d5e81d77a2 | ||
|
|
939e99637f | ||
|
|
579a261612 | ||
|
|
6d03304cbb | ||
|
|
b8d41b3421 | ||
|
|
6a5bb1f5c8 | ||
|
|
cd742a4617 | ||
|
|
54063b97ad | ||
|
|
7abb67e737 | ||
|
|
00fd1ba137 | ||
|
|
7ea37b9eb9 | ||
|
|
b749de8fe1 | ||
|
|
8efef6842d | ||
|
|
dc206f38d5 | ||
|
|
29a00a6c0e | ||
|
|
fe678230a8 | ||
|
|
9cdbeb061f | ||
|
|
6c308f35c1 | ||
|
|
34b89cf2e8 | ||
|
|
b566a188dc | ||
|
|
998432e236 | ||
|
|
1af8edfe4d | ||
|
|
5bf01106c5 | ||
|
|
a45289e385 | ||
|
|
4ffd005b09 | ||
|
|
e6ca89fea8 | ||
|
|
2225aea756 | ||
|
|
bfc4a84020 | ||
|
|
5390bfdcab | ||
|
|
301211ff41 | ||
|
|
64139e4e08 | ||
|
|
e6485cde92 | ||
|
|
891f6ba66f | ||
|
|
5d3c1e3fec | ||
|
|
087e755390 | ||
|
|
025dc1ce75 | ||
|
|
703200338d | ||
|
|
377c93ca0b | ||
|
|
69394ffe29 | ||
|
|
f85231d74a | ||
|
|
b93d8b0159 | ||
|
|
67b9329903 | ||
|
|
c0edd4ea4f | ||
|
|
8eaf2786e8 | ||
|
|
25622df464 | ||
|
|
a48900e178 | ||
|
|
ac8b0535d2 | ||
|
|
6ce25a825b | ||
|
|
b3f56851b8 | ||
|
|
4b86fedce1 | ||
|
|
1ebb70c4d2 | ||
|
|
3de7b81be8 | ||
|
|
d08225339c | ||
|
|
ba22d0706f | ||
|
|
ef80f104c0 | ||
|
|
af296a1e4e | ||
|
|
28a755306a | ||
|
|
461e085eff | ||
|
|
fbda049c32 | ||
|
|
4ded5e2b98 | ||
|
|
63537aff20 | ||
|
|
0f7a2adf15 | ||
|
|
60963abe2c | ||
|
|
08cf95aa38 | ||
|
|
e5b10ab16a | ||
|
|
7f5a1ee45a | ||
|
|
15c593f68e | ||
|
|
5f8ef0395b | ||
|
|
513636e1e0 | ||
|
|
ae9b2c08a9 | ||
|
|
d5327b3b4a | ||
|
|
323e3d3cac | ||
|
|
01b2257063 | ||
|
|
c69ef611a0 | ||
|
|
dcad23316d | ||
|
|
e411f9932f | ||
|
|
854969e1b8 | ||
|
|
4ac7b6e9e8 | ||
|
|
ac70908c5a | ||
|
|
45ac70b78f | ||
|
|
a4664576fe | ||
|
|
b293643398 | ||
|
|
a2e197facd | ||
|
|
8614d39ef4 | ||
|
|
6456bb34ae | ||
|
|
f5dc4de1c1 | ||
|
|
d869056910 | ||
|
|
821e4b17cb | ||
|
|
d8cb5efd2d | ||
|
|
f90e2fb484 | ||
|
|
2c9a7144da | ||
|
|
88d1af7210 | ||
|
|
300e5a5528 | ||
|
|
4418fefe4b | ||
|
|
fe5d1cac9a | ||
|
|
49d17fff9b | ||
|
|
557c6d2d8b | ||
|
|
45fc62357d | ||
|
|
840e3cc22f | ||
|
|
c158c7fc88 | ||
|
|
bc6f8fc2dd | ||
|
|
117730acb2 | ||
|
|
595a7dac83 | ||
|
|
64a4d70df4 | ||
|
|
be36199fe1 | ||
|
|
e46ad25677 | ||
|
|
d5ee663922 | ||
|
|
a7ab4be055 | ||
|
|
6bbf29e75a | ||
|
|
0a06c60cb7 | ||
|
|
03658575eb | ||
|
|
38114bddb9 | ||
|
|
0711a197db | ||
|
|
f8f818b211 | ||
|
|
988932209c | ||
|
|
2aa56cec30 | ||
|
|
93d493650c | ||
|
|
c6162ddcb4 | ||
|
|
038517eda4 | ||
|
|
30a9db73ab | ||
|
|
a50aa41bdb | ||
|
|
cbb322fdb8 | ||
|
|
026e2a020d | ||
|
|
07aab1d005 | ||
|
|
26f0f7b188 | ||
|
|
1efde3b86b | ||
|
|
8c1318f379 | ||
|
|
40e67e8e17 | ||
|
|
04466f52fd | ||
|
|
06baa5fb57 | ||
|
|
04e1657628 | ||
|
|
7816c8cab0 | ||
|
|
6636e658a4 | ||
|
|
2a06f0daef | ||
|
|
883cfa588c | ||
|
|
68011a0b5a | ||
|
|
5247d1a371 | ||
|
|
cabdd528d4 | ||
|
|
2bacbb796b | ||
|
|
aa0ed6434a | ||
|
|
5b2215d646 | ||
|
|
0e760e25f2 | ||
|
|
acbb85b409 | ||
|
|
ea1d4b97ad | ||
|
|
a81839c13f | ||
|
|
b9d4668d4d | ||
|
|
42b27f5965 | ||
|
|
9cc8222b1c | ||
|
|
e8479338df | ||
|
|
fa9e6c9fc0 | ||
|
|
5366173b52 | ||
|
|
63520c55b3 | ||
|
|
86f6d9b14a | ||
|
|
5270cf6284 | ||
|
|
4f46d81e1b | ||
|
|
294a2e6fdb | ||
|
|
b20a8bc90b | ||
|
|
68bdd1336f | ||
|
|
e62ccd932d | ||
|
|
d6c188df6e | ||
|
|
004000b5d2 | ||
|
|
633c8a3444 | ||
|
|
2f59a20b6b | ||
|
|
593c435f75 | ||
|
|
20ec45be57 | ||
|
|
d2a0e12409 | ||
|
|
33eebe117b | ||
|
|
ef0cfc2e7c | ||
|
|
b6e17ae543 | ||
|
|
8a33e2be89 | ||
|
|
5f91097987 | ||
|
|
0fd4f02951 | ||
|
|
106e78ed62 | ||
|
|
8855868b27 | ||
|
|
bfc3e8a907 | ||
|
|
154371e052 | ||
|
|
ab4a4d3d72 | ||
|
|
5a4de02db7 | ||
|
|
43cbc8c6e8 | ||
|
|
5938aa7b50 | ||
|
|
a49252b2f5 | ||
|
|
0be885d9bf | ||
|
|
ae1e8353f2 | ||
|
|
98fe88581f | ||
|
|
d66475576f | ||
|
|
65ff7be776 | ||
|
|
190b079494 | ||
|
|
b020a30bd4 | ||
|
|
81f8453c38 | ||
|
|
533e3cf42d | ||
|
|
69ee73492d | ||
|
|
4a902d04b2 | ||
|
|
2e48e316c2 | ||
|
|
bbe5dddb83 | ||
|
|
7c943fe4ac | ||
|
|
2cbb49681a | ||
|
|
84db4ed57c | ||
|
|
e155642ce4 | ||
|
|
87c4df60d3 | ||
|
|
ff412835e4 | ||
|
|
ad15828157 | ||
|
|
b2fc7f934e | ||
|
|
2fac4d91d6 | ||
|
|
125cd96354 | ||
|
|
af02e6b714 | ||
|
|
0c87b25244 | ||
|
|
e87ada6e79 | ||
|
|
282c8e58bd | ||
|
|
475b66b115 | ||
|
|
5bb971e61a | ||
|
|
ebad9ba723 | ||
|
|
6ece2a839e | ||
|
|
8d6527fb75 | ||
|
|
6bfff38182 | ||
|
|
9e446717fa | ||
|
|
408b48f606 | ||
|
|
8d077ad46d | ||
|
|
db72465e0b | ||
|
|
ba9f5e1688 | ||
|
|
caf40cd272 | ||
|
|
3edccd224a | ||
|
|
f48931a969 | ||
|
|
84f23aa997 | ||
|
|
1965da6a85 | ||
|
|
441ae3e25b | ||
|
|
7f612711a0 | ||
|
|
92eb4aa822 | ||
|
|
08ec522ae7 | ||
|
|
c5cc1fcc1e | ||
|
|
cedf91ea1a | ||
|
|
51b462f043 | ||
|
|
727eeb6c74 | ||
|
|
a114fba062 | ||
|
|
cf322b5c2a | ||
|
|
92116f1671 | ||
|
|
bc479248d7 | ||
|
|
8ee12f2950 | ||
|
|
dcea4c30ef | ||
|
|
e7ca56e061 | ||
|
|
09b800b9ad | ||
|
|
9a6a8580de | ||
|
|
a31ac17792 | ||
|
|
0e27cd0801 | ||
|
|
bc36676fa1 | ||
|
|
3d2db23f33 | ||
|
|
56d366a286 | ||
|
|
4a26f30d65 | ||
|
|
8e51469de5 | ||
|
|
50ebcd552c | ||
|
|
ada39cd3c7 | ||
|
|
b2d20af51a | ||
|
|
f528fa25d1 | ||
|
|
e09a7fb6e0 | ||
|
|
30f7939616 | ||
|
|
16b9375b9d | ||
|
|
4ef93569a1 | ||
|
|
1ce2aaeaf1 | ||
|
|
6bfe8dfcf0 | ||
|
|
8d8f4795e2 | ||
|
|
6f6d06377b | ||
|
|
f22823fcf6 | ||
|
|
93ce57ee1a | ||
|
|
97dd747252 | ||
|
|
bc8c136458 | ||
|
|
0774252dc1 | ||
|
|
ae30ae4be6 | ||
|
|
a2b8935763 | ||
|
|
703efb74d3 | ||
|
|
b2c6062e9a | ||
|
|
c9e7e461b1 | ||
|
|
6aaddfc5a4 | ||
|
|
7f2c41940d | ||
|
|
d31ba39a91 | ||
|
|
c058673e33 | ||
|
|
44ce6a5169 | ||
|
|
0fb0be4ffc | ||
|
|
e70ba00929 | ||
|
|
fe1dbb4cbf | ||
|
|
31df2341c3 | ||
|
|
9d99da14e1 | ||
|
|
f8e10f36db | ||
|
|
bb0f384a39 | ||
|
|
6a0b24f032 | ||
|
|
80d5536503 | ||
|
|
9dcd79bd94 | ||
|
|
c5020b8884 | ||
|
|
0b74de275c | ||
|
|
e66aef17df | ||
|
|
19eff5e6d6 | ||
|
|
88b4fc73de | ||
|
|
70694542eb | ||
|
|
360e5e3102 | ||
|
|
6e89a232e6 | ||
|
|
ecd3b7039f | ||
|
|
4a22e3d2d4 | ||
|
|
dcb4ebe5d9 | ||
|
|
dd379bf18d | ||
|
|
c9b556160f | ||
|
|
168e224d3e | ||
|
|
9e57c14130 | ||
|
|
9c137a1c48 | ||
|
|
ccb9b7e5fb | ||
|
|
c7b16cd043 | ||
|
|
7e20e41521 | ||
|
|
66761a69d3 | ||
|
|
fb32d26479 | ||
|
|
b6398fdb5d | ||
|
|
d9443527ee | ||
|
|
7c175da9f1 | ||
|
|
05aa087851 | ||
|
|
592e968f9f | ||
|
|
894a26cc67 | ||
|
|
1b5dd4638d | ||
|
|
a19186c508 | ||
|
|
5450bdeae9 | ||
|
|
fcd71957ff |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -2,3 +2,5 @@
|
||||
|
||||
github: [eliandoran]
|
||||
custom: ["https://paypal.me/eliandoran"]
|
||||
liberapay: ElianDoran
|
||||
buy_me_a_coffee: eliandoran
|
||||
|
||||
40
.github/instructions/nx.instructions.md
vendored
Normal file
40
.github/instructions/nx.instructions.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
applyTo: '**'
|
||||
---
|
||||
|
||||
// This file is automatically generated by Nx Console
|
||||
|
||||
You are in an nx workspace using Nx 21.3.7 and pnpm as the package manager.
|
||||
|
||||
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
|
||||
|
||||
# General Guidelines
|
||||
- When answering questions, use the nx_workspace tool first to gain an understanding of the workspace architecture
|
||||
- For questions around nx configuration, best practices or if you're unsure, use the nx_docs tool to get relevant, up-to-date docs!! Always use this instead of assuming things about nx configuration
|
||||
- If the user needs help with an Nx configuration or project graph error, use the 'nx_workspace' tool to get any errors
|
||||
- To help answer questions about the workspace structure or simply help with demonstrating how tasks depend on each other, use the 'nx_visualize_graph' tool
|
||||
|
||||
# Generation Guidelines
|
||||
If the user wants to generate something, use the following flow:
|
||||
|
||||
- learn about the nx workspace and any specifics the user needs by using the 'nx_workspace' tool and the 'nx_project_details' tool if applicable
|
||||
- get the available generators using the 'nx_generators' tool
|
||||
- decide which generator to use. If no generators seem relevant, check the 'nx_available_plugins' tool to see if the user could install a plugin to help them
|
||||
- get generator details using the 'nx_generator_schema' tool
|
||||
- you may use the 'nx_docs' tool to learn more about a specific generator or technology if you're unsure
|
||||
- decide which options to provide in order to best complete the user's request. Don't make any assumptions and keep the options minimalistic
|
||||
- open the generator UI using the 'nx_open_generate_ui' tool
|
||||
- wait for the user to finish the generator
|
||||
- read the generator log file using the 'nx_read_generator_log' tool
|
||||
- use the information provided in the log file to answer the user's question or continue with what they were doing
|
||||
|
||||
# Running Tasks Guidelines
|
||||
If the user wants help with tasks or commands (which include keywords like "test", "build", "lint", or other similar actions), use the following flow:
|
||||
- Use the 'nx_current_running_tasks_details' tool to get the list of tasks (this can include tasks that were completed, stopped or failed).
|
||||
- If there are any tasks, ask the user if they would like help with a specific task then use the 'nx_current_running_task_output' tool to get the terminal output for that task/command
|
||||
- Use the terminal output from 'nx_current_running_task_output' to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary
|
||||
- If the user would like to rerun the task or command, always use `nx run <taskId>` to rerun in the terminal. This will ensure that the task will run in the nx context and will be run the same way it originally executed
|
||||
- If the task was marked as "continuous" do not offer to rerun the task. This task is already running and the user can see the output in the terminal. You can use 'nx_current_running_task_output' to get the output of the task to verify the output.
|
||||
|
||||
|
||||
|
||||
17
.github/workflows/checks.yml
vendored
Normal file
17
.github/workflows/checks.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Checks
|
||||
on:
|
||||
push:
|
||||
pull_request_target:
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check if PRs have conflicts
|
||||
uses: eps1lon/actions-label-merge-conflict@v3
|
||||
with:
|
||||
dirtyLabel: "merge-conflicts"
|
||||
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"
|
||||
4
.mailmap
4
.mailmap
@@ -1,2 +1,2 @@
|
||||
Adam Zivner <adam.zivner@gmail.com>
|
||||
Adam Zivner <zadam.apps@gmail.com>
|
||||
zadam <adam.zivner@gmail.com>
|
||||
zadam <zadam.apps@gmail.com>
|
||||
8
.vscode/mcp.json
vendored
Normal file
8
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"servers": {
|
||||
"nx-mcp": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:9461/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -28,5 +28,13 @@
|
||||
"typescript.validate.enable": true,
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"docs/**/*.html": true,
|
||||
"docs/**/*.png": true,
|
||||
"apps/server/src/assets/doc_notes/**": true,
|
||||
"apps/edit-docs/demo/**": true
|
||||
},
|
||||
"nxConsole.generateAiAgentRules": true
|
||||
}
|
||||
161
CLAUDE.md
Normal file
161
CLAUDE.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using NX, with multiple applications and shared packages.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Setup
|
||||
- `pnpm install` - Install all dependencies
|
||||
- `corepack enable` - Enable pnpm if not available
|
||||
|
||||
### Running Applications
|
||||
- `pnpm run server:start` - Start development server (http://localhost:8080)
|
||||
- `pnpm nx run server:serve` - Alternative server start command
|
||||
- `pnpm nx run desktop:serve` - Run desktop Electron app
|
||||
- `pnpm run server:start-prod` - Run server in production mode
|
||||
|
||||
### Building
|
||||
- `pnpm nx build <project>` - Build specific project (server, client, desktop, etc.)
|
||||
- `pnpm run client:build` - Build client application
|
||||
- `pnpm run server:build` - Build server application
|
||||
- `pnpm run electron:build` - Build desktop application
|
||||
|
||||
### Testing
|
||||
- `pnpm test:all` - Run all tests (parallel + sequential)
|
||||
- `pnpm test:parallel` - Run tests that can run in parallel
|
||||
- `pnpm test:sequential` - Run tests that must run sequentially (server, ckeditor5-mermaid, ckeditor5-math)
|
||||
- `pnpm nx test <project>` - Run tests for specific project
|
||||
- `pnpm coverage` - Generate coverage reports
|
||||
|
||||
### Linting & Type Checking
|
||||
- `pnpm nx run <project>:lint` - Lint specific project
|
||||
- `pnpm nx run <project>:typecheck` - Type check specific project
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Monorepo Structure
|
||||
- **apps/**: Runnable applications
|
||||
- `client/` - Frontend application (shared by server and desktop)
|
||||
- `server/` - Node.js server with web interface
|
||||
- `desktop/` - Electron desktop application
|
||||
- `web-clipper/` - Browser extension for saving web content
|
||||
- Additional tools: `db-compare`, `dump-db`, `edit-docs`
|
||||
|
||||
- **packages/**: Shared libraries
|
||||
- `commons/` - Shared interfaces and utilities
|
||||
- `ckeditor5/` - Custom rich text editor with Trilium-specific plugins
|
||||
- `codemirror/` - Code editor customizations
|
||||
- `highlightjs/` - Syntax highlighting
|
||||
- Custom CKEditor plugins: `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid`
|
||||
|
||||
### Core Architecture Patterns
|
||||
|
||||
#### Three-Layer Cache System
|
||||
- **Becca** (Backend Cache): Server-side entity cache (`apps/server/src/becca/`)
|
||||
- **Froca** (Frontend Cache): Client-side mirror of backend data (`apps/client/src/services/froca.ts`)
|
||||
- **Shaca** (Share Cache): Optimized cache for shared/published notes (`apps/server/src/share/`)
|
||||
|
||||
#### Entity System
|
||||
Core entities are defined in `apps/server/src/becca/entities/`:
|
||||
- `BNote` - Notes with content and metadata
|
||||
- `BBranch` - Hierarchical relationships between notes (allows multiple parents)
|
||||
- `BAttribute` - Key-value metadata attached to notes
|
||||
- `BRevision` - Note version history
|
||||
- `BOption` - Application configuration
|
||||
|
||||
#### Widget-Based UI
|
||||
Frontend uses a widget system (`apps/client/src/widgets/`):
|
||||
- `BasicWidget` - Base class for all UI components
|
||||
- `NoteContextAwareWidget` - Widgets that respond to note changes
|
||||
- `RightPanelWidget` - Widgets displayed in the right panel
|
||||
- Type-specific widgets in `type_widgets/` directory
|
||||
|
||||
#### API Architecture
|
||||
- **Internal API**: REST endpoints in `apps/server/src/routes/api/`
|
||||
- **ETAPI**: External API for third-party integrations (`apps/server/src/etapi/`)
|
||||
- **WebSocket**: Real-time synchronization (`apps/server/src/services/ws.ts`)
|
||||
|
||||
### Key Files for Understanding Architecture
|
||||
|
||||
1. **Application Entry Points**:
|
||||
- `apps/server/src/main.ts` - Server startup
|
||||
- `apps/client/src/desktop.ts` - Client initialization
|
||||
|
||||
2. **Core Services**:
|
||||
- `apps/server/src/becca/becca.ts` - Backend data management
|
||||
- `apps/client/src/services/froca.ts` - Frontend data synchronization
|
||||
- `apps/server/src/services/backend_script_api.ts` - Scripting API
|
||||
|
||||
3. **Database Schema**:
|
||||
- `apps/server/src/assets/db/schema.sql` - Core database structure
|
||||
|
||||
4. **Configuration**:
|
||||
- `nx.json` - NX workspace configuration
|
||||
- `package.json` - Project dependencies and scripts
|
||||
|
||||
## Note Types and Features
|
||||
|
||||
Trilium supports multiple note types, each with specialized widgets:
|
||||
- **Text**: Rich text with CKEditor5 (markdown import/export)
|
||||
- **Code**: Syntax-highlighted code editing with CodeMirror
|
||||
- **File**: Binary file attachments
|
||||
- **Image**: Image display with editing capabilities
|
||||
- **Canvas**: Drawing/diagramming with Excalidraw
|
||||
- **Mermaid**: Diagram generation
|
||||
- **Relation Map**: Visual note relationship mapping
|
||||
- **Web View**: Embedded web pages
|
||||
- **Doc/Book**: Hierarchical documentation structure
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Testing Strategy
|
||||
- Server tests run sequentially due to shared database
|
||||
- Client tests can run in parallel
|
||||
- E2E tests use Playwright for both server and desktop apps
|
||||
- Build validation tests check artifact integrity
|
||||
|
||||
### Scripting System
|
||||
Trilium provides powerful user scripting capabilities:
|
||||
- Frontend scripts run in browser context
|
||||
- Backend scripts run in Node.js context with full API access
|
||||
- Script API documentation available in `docs/Script API/`
|
||||
|
||||
### Internationalization
|
||||
- Translation files in `apps/client/src/translations/`
|
||||
- Supported languages: English, German, Spanish, French, Romanian, Chinese
|
||||
|
||||
### Security Considerations
|
||||
- Per-note encryption with granular protected sessions
|
||||
- CSRF protection for API endpoints
|
||||
- OpenID and TOTP authentication support
|
||||
- Sanitization of user-generated content
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding New Note Types
|
||||
1. Create widget in `apps/client/src/widgets/type_widgets/`
|
||||
2. Register in `apps/client/src/services/note_types.ts`
|
||||
3. Add backend handling in `apps/server/src/services/notes.ts`
|
||||
|
||||
### Extending Search
|
||||
- Search expressions handled in `apps/server/src/services/search/`
|
||||
- Add new search operators in search context files
|
||||
|
||||
### Custom CKEditor Plugins
|
||||
- Create new package in `packages/` following existing plugin structure
|
||||
- Register in `packages/ckeditor5/src/plugins.ts`
|
||||
|
||||
### Database Migrations
|
||||
- Add migration scripts in `apps/server/src/migrations/`
|
||||
- Update schema in `apps/server/src/assets/db/schema.sql`
|
||||
|
||||
## Build System Notes
|
||||
- Uses NX for monorepo management with build caching
|
||||
- Vite for fast development builds
|
||||
- ESBuild for production optimization
|
||||
- pnpm workspaces for dependency management
|
||||
- Docker support with multi-stage builds
|
||||
15
README.md
15
README.md
@@ -1,6 +1,7 @@
|
||||
# Trilium Notes
|
||||
|
||||

|
||||
Donate:  
|
||||
|
||||

|
||||

|
||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
|
||||
@@ -119,8 +120,8 @@ To install TriliumNext on your own server (including via Docker from [Dockerhub]
|
||||
|
||||
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
|
||||
```shell
|
||||
git clone https://github.com/TriliumNext/Notes.git
|
||||
cd Notes
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm run server:start
|
||||
```
|
||||
@@ -129,8 +130,8 @@ pnpm run server:start
|
||||
|
||||
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
|
||||
```shell
|
||||
git clone https://github.com/TriliumNext/Notes.git
|
||||
cd Notes
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm nx run edit-docs:edit-docs
|
||||
```
|
||||
@@ -138,8 +139,8 @@ pnpm nx run edit-docs:edit-docs
|
||||
### Building the Executable
|
||||
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
|
||||
```shell
|
||||
git clone https://github.com/TriliumNext/Notes.git
|
||||
cd Notes
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
|
||||
```
|
||||
|
||||
@@ -35,13 +35,13 @@
|
||||
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.53.1",
|
||||
"@stylistic/eslint-plugin": "5.0.0",
|
||||
"@playwright/test": "1.54.1",
|
||||
"@stylistic/eslint-plugin": "5.2.2",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/node": "22.15.33",
|
||||
"@types/node": "22.16.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"eslint": "9.29.0",
|
||||
"eslint": "9.32.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"esm": "3.2.25",
|
||||
"jsdoc": "4.0.4",
|
||||
@@ -49,7 +49,7 @@
|
||||
"rcedit": "4.0.1",
|
||||
"rimraf": "6.0.1",
|
||||
"tslib": "2.8.1",
|
||||
"typedoc": "0.28.5",
|
||||
"typedoc": "0.28.7",
|
||||
"typedoc-plugin-missing-exports": "4.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# The development license key for premium CKEditor features.
|
||||
# Note: This key must only be used for the Trilium Notes project.
|
||||
# Expires on: 2025-09-13
|
||||
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w
|
||||
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w
|
||||
VITE_CKEDITOR_ENABLE_INSPECTOR=false
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.96.0",
|
||||
"version": "0.97.1",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -10,16 +10,17 @@
|
||||
"url": "https://github.com/TriliumNext/Notes"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "9.29.0",
|
||||
"@eslint/js": "9.32.0",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@fullcalendar/core": "6.1.17",
|
||||
"@fullcalendar/daygrid": "6.1.17",
|
||||
"@fullcalendar/interaction": "6.1.17",
|
||||
"@fullcalendar/list": "6.1.17",
|
||||
"@fullcalendar/multimonth": "6.1.17",
|
||||
"@fullcalendar/timegrid": "6.1.17",
|
||||
"@fullcalendar/core": "6.1.18",
|
||||
"@fullcalendar/daygrid": "6.1.18",
|
||||
"@fullcalendar/interaction": "6.1.18",
|
||||
"@fullcalendar/list": "6.1.18",
|
||||
"@fullcalendar/multimonth": "6.1.18",
|
||||
"@fullcalendar/timegrid": "6.1.18",
|
||||
"@maplibre/maplibre-gl-leaflet": "0.1.2",
|
||||
"@mermaid-js/layout-elk": "0.1.8",
|
||||
"@mind-elixir/node-menu": "1.0.5",
|
||||
"@mind-elixir/node-menu": "5.0.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@triliumnext/codemirror": "workspace:*",
|
||||
@@ -33,9 +34,9 @@
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
"debounce": "2.2.0",
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.49.6",
|
||||
"globals": "16.2.0",
|
||||
"i18next": "25.2.1",
|
||||
"force-graph": "1.50.1",
|
||||
"globals": "16.3.0",
|
||||
"i18next": "25.3.2",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery-hotkeys": "0.2.2",
|
||||
@@ -46,27 +47,29 @@
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "15.0.12",
|
||||
"mermaid": "11.7.0",
|
||||
"mind-elixir": "4.6.1",
|
||||
"marked": "16.1.1",
|
||||
"mermaid": "11.9.0",
|
||||
"mind-elixir": "5.0.4",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.26.9",
|
||||
"split.js": "1.6.5",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
"vanilla-js-wheel-zoom": "9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-inspector": "4.1.0",
|
||||
"@ckeditor/ckeditor5-inspector": "5.0.0",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/jquery": "3.5.32",
|
||||
"@types/leaflet": "1.9.19",
|
||||
"@types/leaflet": "1.9.20",
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/tabulator-tables": "6.2.8",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"happy-dom": "18.0.1",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.0"
|
||||
"vite-plugin-static-copy": "3.1.1"
|
||||
},
|
||||
"nx": {
|
||||
"name": "client",
|
||||
|
||||
@@ -28,6 +28,8 @@ import TouchBarComponent from "./touch_bar.js";
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import type CodeMirror from "@triliumnext/codemirror";
|
||||
import { StartupChecks } from "./startup_checks.js";
|
||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
||||
@@ -122,6 +124,7 @@ export type CommandMappings = {
|
||||
showImportDialog: CommandData & { noteId: string };
|
||||
openNewNoteSplit: NoteCommandData;
|
||||
openInWindow: NoteCommandData;
|
||||
openInPopup: CommandData & { noteIdOrPath: string; };
|
||||
openNoteInNewTab: CommandData;
|
||||
openNoteInNewSplit: CommandData;
|
||||
openNoteInNewWindow: CommandData;
|
||||
@@ -140,6 +143,7 @@ export type CommandMappings = {
|
||||
};
|
||||
openInTab: ContextMenuCommandData;
|
||||
openNoteInSplit: ContextMenuCommandData;
|
||||
openNoteInPopup: ContextMenuCommandData;
|
||||
toggleNoteHoisting: ContextMenuCommandData;
|
||||
insertNoteAfter: ContextMenuCommandData;
|
||||
insertChildNote: ContextMenuCommandData;
|
||||
@@ -261,7 +265,6 @@ export type CommandMappings = {
|
||||
|
||||
// Geomap
|
||||
deleteFromMap: { noteId: string };
|
||||
openGeoLocation: { noteId: string; event: JQuery.MouseDownEvent };
|
||||
|
||||
toggleZenMode: CommandData;
|
||||
|
||||
@@ -275,6 +278,21 @@ export type CommandMappings = {
|
||||
|
||||
geoMapCreateChildNote: CommandData;
|
||||
|
||||
// Table view
|
||||
addNewRow: CommandData & {
|
||||
customOpts: CreateNoteOpts;
|
||||
parentNotePath?: string;
|
||||
};
|
||||
addNewTableColumn: CommandData & {
|
||||
columnToEdit?: ColumnComponent;
|
||||
referenceColumn?: ColumnComponent;
|
||||
direction?: "before" | "after";
|
||||
type?: "label" | "relation";
|
||||
};
|
||||
deleteTableColumn: CommandData & {
|
||||
columnToDelete?: ColumnComponent;
|
||||
};
|
||||
|
||||
buildTouchBar: CommandData & {
|
||||
TouchBar: typeof TouchBar;
|
||||
buildIcon(name: string): NativeImage;
|
||||
|
||||
@@ -93,11 +93,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
|
||||
if (fun) {
|
||||
return this.callMethod(fun, data);
|
||||
} else {
|
||||
if (!this.parent) {
|
||||
throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`);
|
||||
}
|
||||
|
||||
} else if (this.parent) {
|
||||
return this.parent.triggerCommand(name, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,14 +315,39 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
}
|
||||
|
||||
hasNoteList() {
|
||||
return (
|
||||
this.note &&
|
||||
["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "") &&
|
||||
(this.note.hasChildren() || this.note.getLabelValue("viewType") === "calendar") &&
|
||||
["book", "text", "code"].includes(this.note.type) &&
|
||||
this.note.mime !== "text/x-sqlite;schema=trilium" &&
|
||||
!this.note.isLabelTruthy("hideChildrenOverview")
|
||||
);
|
||||
const note = this.note;
|
||||
|
||||
if (!note) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collections must always display a note list, even if no children.
|
||||
const viewType = note.getLabelValue("viewType") ?? "grid";
|
||||
if (!["list", "grid"].includes(viewType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!note.hasChildren()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!["book", "text", "code"].includes(note.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (note.mime === "text/x-sqlite;schema=trilium") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (note.isLabelTruthy("hideChildrenOverview")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async getTextEditor(callback?: GetTextEditorCallback) {
|
||||
|
||||
@@ -27,7 +27,6 @@ const NOTE_TYPE_ICONS = {
|
||||
doc: "bx bxs-file-doc",
|
||||
contentWidget: "bx bxs-widget",
|
||||
mindMap: "bx bx-sitemap",
|
||||
geoMap: "bx bx-map-alt",
|
||||
aiChat: "bx bx-bot"
|
||||
};
|
||||
|
||||
@@ -36,7 +35,7 @@ const NOTE_TYPE_ICONS = {
|
||||
* end user. Those types should be used only for checking against, they are
|
||||
* not for direct use.
|
||||
*/
|
||||
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap" | "aiChat";
|
||||
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "aiChat";
|
||||
|
||||
export interface NotePathRecord {
|
||||
isArchived: boolean;
|
||||
@@ -257,6 +256,20 @@ class FNote {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
async getSubtreeNoteIds() {
|
||||
let noteIds: (string | string[])[] = [];
|
||||
for (const child of await this.getChildNotes()) {
|
||||
noteIds.push(child.noteId);
|
||||
noteIds.push(await child.getSubtreeNoteIds());
|
||||
}
|
||||
return noteIds.flat();
|
||||
}
|
||||
|
||||
async getSubtreeNotes() {
|
||||
const noteIds = await this.getSubtreeNoteIds();
|
||||
return this.froca.getNotes(noteIds);
|
||||
}
|
||||
|
||||
async getChildNotes() {
|
||||
return await this.froca.getNotes(this.children);
|
||||
}
|
||||
|
||||
@@ -46,28 +46,7 @@ import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import TocWidget from "../widgets/toc.js";
|
||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||
import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js";
|
||||
import AboutDialog from "../widgets/dialogs/about.js";
|
||||
import HelpDialog from "../widgets/dialogs/help.js";
|
||||
import RecentChangesDialog from "../widgets/dialogs/recent_changes.js";
|
||||
import BranchPrefixDialog from "../widgets/dialogs/branch_prefix.js";
|
||||
import SortChildNotesDialog from "../widgets/dialogs/sort_child_notes.js";
|
||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||
import IncludeNoteDialog from "../widgets/dialogs/include_note.js";
|
||||
import NoteTypeChooserDialog from "../widgets/dialogs/note_type_chooser.js";
|
||||
import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js";
|
||||
import AddLinkDialog from "../widgets/dialogs/add_link.js";
|
||||
import CloneToDialog from "../widgets/dialogs/clone_to.js";
|
||||
import MoveToDialog from "../widgets/dialogs/move_to.js";
|
||||
import ImportDialog from "../widgets/dialogs/import.js";
|
||||
import ExportDialog from "../widgets/dialogs/export.js";
|
||||
import MarkdownImportDialog from "../widgets/dialogs/markdown_import.js";
|
||||
import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
|
||||
import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||
import InfoDialog from "../widgets/dialogs/info.js";
|
||||
import ConfirmDialog from "../widgets/dialogs/confirm.js";
|
||||
import PromptDialog from "../widgets/dialogs/prompt.js";
|
||||
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
|
||||
@@ -83,7 +62,7 @@ import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_ref
|
||||
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||
import options from "../services/options.js";
|
||||
import utils, { hasTouchBar } from "../services/utils.js";
|
||||
import utils from "../services/utils.js";
|
||||
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
|
||||
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
|
||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
||||
@@ -229,7 +208,7 @@ export default class DesktopLayout {
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new SqlTableSchemasWidget())
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget())
|
||||
.child(new NoteListWidget(false))
|
||||
.child(new SearchResultWidget())
|
||||
.child(new SqlResultWidget())
|
||||
.child(new ScrollPaddingWidget())
|
||||
|
||||
@@ -22,6 +22,14 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||
import InfoDialog from "../widgets/dialogs/info.js";
|
||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
|
||||
export function applyModals(rootContainer: RootContainer) {
|
||||
rootContainer
|
||||
@@ -47,4 +55,15 @@ export function applyModals(rootContainer: RootContainer) {
|
||||
.child(new ConfirmDialog())
|
||||
.child(new PromptDialog())
|
||||
.child(new IncorrectCpuArchDialog())
|
||||
.child(new PopupEditorDialog()
|
||||
.child(new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.css("align-items", "center")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(new NoteIconWidget())
|
||||
.child(new NoteTitleWidget()))
|
||||
.child(new ClassicEditorToolbar())
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget(true)))
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ export default class MobileLayout {
|
||||
.filling()
|
||||
.contentSized()
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget())
|
||||
.child(new NoteListWidget(false))
|
||||
.child(new FilePropertiesWidget().css("font-size", "smaller"))
|
||||
)
|
||||
.child(new MobileEditorToolbar())
|
||||
|
||||
@@ -2,7 +2,7 @@ import keyboardActionService from "../services/keyboard_actions.js";
|
||||
import note_tooltip from "../services/note_tooltip.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
interface ContextMenuOptions<T> {
|
||||
export interface ContextMenuOptions<T> {
|
||||
x: number;
|
||||
y: number;
|
||||
orientation?: "left";
|
||||
@@ -17,17 +17,30 @@ interface MenuSeparatorItem {
|
||||
title: "----";
|
||||
}
|
||||
|
||||
export interface MenuItemBadge {
|
||||
title: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface MenuCommandItem<T> {
|
||||
title: string;
|
||||
command?: T;
|
||||
type?: string;
|
||||
/**
|
||||
* The icon to display in the menu item.
|
||||
*
|
||||
* If not set, no icon is displayed and the item will appear shifted slightly to the left if there are other items with icons. To avoid this, use `bx bx-empty`.
|
||||
*/
|
||||
uiIcon?: string;
|
||||
badges?: MenuItemBadge[];
|
||||
templateNoteId?: string;
|
||||
enabled?: boolean;
|
||||
handler?: MenuHandler<T>;
|
||||
items?: MenuItem<T>[] | null;
|
||||
shortcut?: string;
|
||||
spellingSuggestion?: string;
|
||||
checked?: boolean;
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||
@@ -146,10 +159,13 @@ class ContextMenu {
|
||||
} else {
|
||||
const $icon = $("<span>");
|
||||
|
||||
if ("uiIcon" in item && item.uiIcon) {
|
||||
$icon.addClass(item.uiIcon);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
if ("uiIcon" in item || "checked" in item) {
|
||||
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||
if (icon) {
|
||||
$icon.addClass(icon);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
const $link = $("<span>")
|
||||
@@ -157,6 +173,18 @@ class ContextMenu {
|
||||
.append(" ") // some space between icon and text
|
||||
.append(item.title);
|
||||
|
||||
if ("badges" in item && item.badges) {
|
||||
for (let badge of item.badges) {
|
||||
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
||||
|
||||
if (badge.className) {
|
||||
badgeElement.addClass(badge.className);
|
||||
}
|
||||
|
||||
$link.append(badgeElement);
|
||||
}
|
||||
}
|
||||
|
||||
if ("shortcut" in item && item.shortcut) {
|
||||
$link.append($("<kbd>").text(item.shortcut));
|
||||
}
|
||||
@@ -213,6 +241,9 @@ class ContextMenu {
|
||||
$link.addClass("dropdown-toggle");
|
||||
|
||||
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
||||
if (!this.isMobile && item.columns) {
|
||||
$subMenu.css("column-count", item.columns);
|
||||
}
|
||||
|
||||
this.addItems($subMenu, item.items);
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ function getItems(): MenuItem<CommandNames>[] {
|
||||
return [
|
||||
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
||||
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
||||
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }
|
||||
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
|
||||
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
|
||||
];
|
||||
}
|
||||
|
||||
@@ -40,6 +41,8 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string
|
||||
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
||||
} else if (command === "openNoteInNewWindow") {
|
||||
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||
} else if (command === "openNoteInPopup") {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
const items: (MenuItem<TreeCommandNames> | null)[] = [
|
||||
{ title: `${t("tree-context-menu.open-in-a-new-tab")}`, command: "openInTab", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
|
||||
|
||||
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes },
|
||||
|
||||
isHoisted
|
||||
? null
|
||||
@@ -92,7 +92,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
command: "insertNoteAfter",
|
||||
uiIcon: "bx bx-plus",
|
||||
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
|
||||
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptionsOrHelp
|
||||
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptionsOrHelp,
|
||||
columns: 2
|
||||
},
|
||||
|
||||
{
|
||||
@@ -100,7 +101,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
command: "insertChildNote",
|
||||
uiIcon: "bx bx-plus",
|
||||
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
|
||||
enabled: notSearch && noSelectedNotes && notOptionsOrHelp
|
||||
enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
|
||||
columns: 2
|
||||
},
|
||||
|
||||
{ title: "----" },
|
||||
@@ -127,13 +129,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
|
||||
},
|
||||
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
||||
{
|
||||
title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`,
|
||||
command: "duplicateSubtree",
|
||||
uiIcon: "bx bx-outline",
|
||||
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
|
||||
},
|
||||
|
||||
|
||||
{ title: "----" },
|
||||
|
||||
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||
@@ -186,6 +182,13 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
{ title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted },
|
||||
|
||||
{
|
||||
title: `${t("tree-context-menu.duplicate")} <kbd data-command="duplicateSubtree">`,
|
||||
command: "duplicateSubtree",
|
||||
uiIcon: "bx bx-outline",
|
||||
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
|
||||
},
|
||||
|
||||
{
|
||||
title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`,
|
||||
command: "deleteNotes",
|
||||
@@ -244,6 +247,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
||||
|
||||
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
|
||||
} else if (command === "openNoteInPopup") {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
|
||||
} else if (command === "convertNoteToAttachment") {
|
||||
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
|
||||
return;
|
||||
|
||||
@@ -3,19 +3,21 @@ import froca from "./froca.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { AttributeRow } from "./load_results.js";
|
||||
|
||||
async function addLabel(noteId: string, name: string, value: string = "") {
|
||||
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/attribute`, {
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value
|
||||
value: value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
|
||||
async function setLabel(noteId: string, name: string, value: string = "") {
|
||||
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value
|
||||
value: value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,7 +51,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
|
||||
* @param name the name of the attribute to set.
|
||||
* @param value the value of the attribute to set.
|
||||
*/
|
||||
async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||
if (value) {
|
||||
// Create or update the attribute.
|
||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
||||
|
||||
@@ -95,7 +95,15 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false) {
|
||||
/**
|
||||
* Shows the delete confirmation screen
|
||||
*
|
||||
* @param branchIdsToDelete the list of branch IDs to delete.
|
||||
* @param forceDeleteAllClones whether to check by default the "Delete also all clones" checkbox.
|
||||
* @param moveToParent whether to automatically go to the parent note path after a succesful delete. Usually makes sense if deleting the active note(s).
|
||||
* @returns promise that returns false if the operation was cancelled or there was nothing to delete, true if the operation succeeded.
|
||||
*/
|
||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true) {
|
||||
branchIdsToDelete = filterRootNote(branchIdsToDelete);
|
||||
|
||||
if (branchIdsToDelete.length === 0) {
|
||||
@@ -110,10 +118,12 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await activateParentNotePath();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (moveToParent) {
|
||||
try {
|
||||
await activateParentNotePath();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const taskId = utils.randomString(10);
|
||||
|
||||
@@ -15,6 +15,8 @@ import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation
|
||||
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
|
||||
import { t } from "./i18n.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import toast from "./toast.js";
|
||||
import { BulkAction } from "@triliumnext/commons";
|
||||
|
||||
const ACTION_GROUPS = [
|
||||
{
|
||||
@@ -89,6 +91,17 @@ function parseActions(note: FNote) {
|
||||
.filter((action) => !!action);
|
||||
}
|
||||
|
||||
export async function executeBulkActions(targetNoteIds: string[], actions: BulkAction[], includeDescendants = false) {
|
||||
await server.post("bulk-action/execute", {
|
||||
noteIds: targetNoteIds,
|
||||
includeDescendants,
|
||||
actions
|
||||
});
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
|
||||
}
|
||||
|
||||
export default {
|
||||
addAction,
|
||||
parseActions,
|
||||
|
||||
@@ -118,8 +118,17 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
|
||||
async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
|
||||
const blob = await note.getBlob();
|
||||
|
||||
let content = blob?.content || "";
|
||||
if (note.mime === "application/json") {
|
||||
try {
|
||||
content = JSON.stringify(JSON.parse(content), null, 4);
|
||||
} catch (e) {
|
||||
// Ignore JSON parsing errors.
|
||||
}
|
||||
}
|
||||
|
||||
const $codeBlock = $("<code>");
|
||||
$codeBlock.text(blob?.content || "");
|
||||
$codeBlock.text(content);
|
||||
$renderedContent.append($("<pre>").append($codeBlock));
|
||||
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
||||
}
|
||||
@@ -301,7 +310,7 @@ function getRenderingType(entity: FNote | FAttachment) {
|
||||
|
||||
if (type === "file" && mime === "application/pdf") {
|
||||
type = "pdf";
|
||||
} else if (type === "file" && mime && CODE_MIME_TYPES.has(mime)) {
|
||||
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime)) {
|
||||
type = "code";
|
||||
} else if (type === "file" && mime && mime.startsWith("audio/")) {
|
||||
type = "audio";
|
||||
|
||||
@@ -4,14 +4,14 @@ import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptio
|
||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
import { focusSavedElement, saveFocusedElement } from "./focus.js";
|
||||
|
||||
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
|
||||
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
|
||||
if (closeActDialog) {
|
||||
closeActiveDialog();
|
||||
glob.activeDialog = $dialog;
|
||||
}
|
||||
|
||||
saveFocusedElement();
|
||||
Modal.getOrCreateInstance($dialog[0]).show();
|
||||
Modal.getOrCreateInstance($dialog[0], config).show();
|
||||
|
||||
$dialog.on("hidden.bs.modal", () => {
|
||||
const $autocompleteEl = $(".aa-input");
|
||||
@@ -41,8 +41,14 @@ async function info(message: string) {
|
||||
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a confirmation dialog with the given message.
|
||||
*
|
||||
* @param message the message to display in the dialog.
|
||||
* @returns A promise that resolves to true if the user confirmed, false otherwise.
|
||||
*/
|
||||
async function confirm(message: string) {
|
||||
return new Promise((res) =>
|
||||
return new Promise<boolean>((res) =>
|
||||
appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{
|
||||
message,
|
||||
callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed)
|
||||
|
||||
@@ -35,7 +35,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
loadResults.addOption(attributeEntity.name);
|
||||
} else if (ec.entityName === "attachments") {
|
||||
processAttachment(loadResults, ec);
|
||||
} else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") {
|
||||
} else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens" || ec.entityName === "file_note_mappings" || ec.entityName === "file_system_mappings") {
|
||||
// NOOP - these entities are handled at the backend level and don't require frontend processing
|
||||
} else {
|
||||
throw new Error(`Unknown entityName '${ec.entityName}'`);
|
||||
|
||||
@@ -49,6 +49,13 @@ function setupGlobs() {
|
||||
const string = e?.reason?.message?.toLowerCase();
|
||||
|
||||
let message = "Uncaught error: ";
|
||||
let errorObjectString;
|
||||
|
||||
try {
|
||||
errorObjectString = JSON.stringify(e.reason)
|
||||
} catch (error: any) {
|
||||
errorObjectString = error.toString();
|
||||
}
|
||||
|
||||
if (string?.includes("script error")) {
|
||||
message += "No details available";
|
||||
@@ -57,7 +64,7 @@ function setupGlobs() {
|
||||
`Message: ${e.reason.message}`,
|
||||
`Line: ${e.reason.lineNumber}`,
|
||||
`Column: ${e.reason.columnNumber}`,
|
||||
`Error object: ${JSON.stringify(e.reason)}`,
|
||||
`Error object: ${errorObjectString}`,
|
||||
`Stack: ${e.reason && e.reason.stack}`
|
||||
].join(", ");
|
||||
}
|
||||
|
||||
@@ -231,6 +231,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
let ntxId: string | null = null;
|
||||
let hoistedNoteId: string | null = null;
|
||||
let searchString: string | null = null;
|
||||
let openInPopup = false;
|
||||
|
||||
if (paramString) {
|
||||
for (const pair of paramString.split("&")) {
|
||||
@@ -246,6 +247,8 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
|
||||
} else if (["viewMode", "attachmentId"].includes(name)) {
|
||||
(viewScope as any)[name] = value;
|
||||
} else if (name === "popup") {
|
||||
openInPopup = true;
|
||||
} else {
|
||||
console.warn(`Unrecognized hash parameter '${name}'.`);
|
||||
}
|
||||
@@ -266,7 +269,8 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
ntxId,
|
||||
hoistedNoteId,
|
||||
viewScope,
|
||||
searchString
|
||||
searchString,
|
||||
openInPopup
|
||||
};
|
||||
}
|
||||
|
||||
@@ -277,13 +281,21 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
|
||||
return goToLinkExt(evt, hrefLink, $link);
|
||||
}
|
||||
|
||||
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
|
||||
/**
|
||||
* Handles navigation to a link, which can be an internal note path (e.g., `#root/1234`) or an external URL (e.g., `https://example.com`).
|
||||
*
|
||||
* @param evt the event that triggered the link navigation, or `null` if the link was clicked programmatically. Used to determine if the link should be opened in a new tab/window, based on the button presses.
|
||||
* @param hrefLink the link to navigate to, which can be a note path (e.g., `#root/1234`) or an external URL with any supported protocol (e.g., `https://example.com`).
|
||||
* @param $link the jQuery element of the link that was clicked, used to determine if the link is an anchor link (e.g., `#fn1` or `#fnref1`) and to handle it accordingly.
|
||||
* @returns `true` if the link was handled (i.e., the element was found and scrolled to), or a falsy value otherwise.
|
||||
*/
|
||||
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
|
||||
if (hrefLink?.startsWith("data:")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
evt?.preventDefault();
|
||||
evt?.stopPropagation();
|
||||
|
||||
if (hrefLink && hrefLink.startsWith("#") && !hrefLink.startsWith("#root/") && $link) {
|
||||
if (handleAnchor(hrefLink, $link)) {
|
||||
@@ -291,19 +303,22 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
|
||||
}
|
||||
}
|
||||
|
||||
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
|
||||
const { notePath, viewScope, openInPopup } = parseNavigationStateFromUrl(hrefLink);
|
||||
|
||||
const ctrlKey = utils.isCtrlKey(evt);
|
||||
const shiftKey = evt.shiftKey;
|
||||
const isLeftClick = "which" in evt && evt.which === 1;
|
||||
const isMiddleClick = "which" in evt && evt.which === 2;
|
||||
const ctrlKey = evt && utils.isCtrlKey(evt);
|
||||
const shiftKey = evt?.shiftKey;
|
||||
const isLeftClick = !evt || ("which" in evt && evt.which === 1);
|
||||
// Right click is handled separately.
|
||||
const isMiddleClick = evt && "which" in evt && evt.which === 2;
|
||||
const targetIsBlank = ($link?.attr("target") === "_blank");
|
||||
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank;
|
||||
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
|
||||
const openInNewWindow = isLeftClick && evt.shiftKey && !ctrlKey;
|
||||
const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
|
||||
|
||||
if (notePath) {
|
||||
if (openInNewWindow) {
|
||||
if (isLeftClick && openInPopup) {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
||||
} else if (openInNewWindow) {
|
||||
appContext.triggerCommand("openInWindow", { notePath, viewScope });
|
||||
} else if (openInNewTab) {
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
||||
@@ -311,7 +326,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
|
||||
viewScope
|
||||
});
|
||||
} else if (isLeftClick) {
|
||||
const ntxId = $(evt.target as any)
|
||||
const ntxId = $(evt?.target as any)
|
||||
.closest("[data-ntx-id]")
|
||||
.attr("data-ntx-id");
|
||||
|
||||
@@ -379,6 +394,12 @@ function linkContextMenu(e: PointerEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.isCtrlKey(e) && e.button === 2) {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
linkContextMenuService.openContextMenu(notePath, e, viewScope, null);
|
||||
|
||||
@@ -40,7 +40,10 @@ interface Options {
|
||||
allowCreatingNotes?: boolean;
|
||||
allowJumpToSearchNotes?: boolean;
|
||||
allowExternalLinks?: boolean;
|
||||
/** If set, hides the right-side button corresponding to go to selected note. */
|
||||
hideGoToSelectedNoteButton?: boolean;
|
||||
/** If set, hides all right-side buttons in the autocomplete dropdown */
|
||||
hideAllButtons?: boolean;
|
||||
}
|
||||
|
||||
async function autocompleteSourceForCKEditor(queryText: string) {
|
||||
@@ -190,9 +193,11 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
||||
|
||||
const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right");
|
||||
|
||||
$el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton);
|
||||
if (!options.hideAllButtons) {
|
||||
$el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton);
|
||||
}
|
||||
|
||||
if (!options.hideGoToSelectedNoteButton) {
|
||||
if (!options.hideGoToSelectedNoteButton && !options.hideAllButtons) {
|
||||
$el.after($goToSelectedNoteButton);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import type FBranch from "../entities/fbranch.js";
|
||||
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
|
||||
interface CreateNoteOpts {
|
||||
export interface CreateNoteOpts {
|
||||
isProtected?: boolean;
|
||||
saveSelection?: boolean;
|
||||
title?: string | null;
|
||||
|
||||
@@ -1,38 +1,31 @@
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import BoardView from "../widgets/view_widgets/board_view/index.js";
|
||||
import CalendarView from "../widgets/view_widgets/calendar_view.js";
|
||||
import GeoView from "../widgets/view_widgets/geo_view/index.js";
|
||||
import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js";
|
||||
import TableView from "../widgets/view_widgets/table_view/index.js";
|
||||
import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js";
|
||||
import type ViewMode from "../widgets/view_widgets/view_mode.js";
|
||||
|
||||
export type ViewTypeOptions = "list" | "grid" | "calendar";
|
||||
const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board"] as const;
|
||||
export type ArgsWithoutNoteId = Omit<ViewModeArgs, "noteIds">;
|
||||
export type ViewTypeOptions = typeof allViewTypes[number];
|
||||
|
||||
export default class NoteListRenderer {
|
||||
|
||||
private viewType: ViewTypeOptions;
|
||||
public viewMode: ViewMode | null;
|
||||
private args: ArgsWithoutNoteId;
|
||||
public viewMode?: ViewMode<any>;
|
||||
|
||||
constructor($parent: JQuery<HTMLElement>, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) {
|
||||
this.viewType = this.#getViewType(parentNote);
|
||||
const args: ViewModeArgs = {
|
||||
$parent,
|
||||
parentNote,
|
||||
noteIds,
|
||||
showNotePath
|
||||
};
|
||||
|
||||
if (this.viewType === "list" || this.viewType === "grid") {
|
||||
this.viewMode = new ListOrGridView(this.viewType, args);
|
||||
} else if (this.viewType === "calendar") {
|
||||
this.viewMode = new CalendarView(args);
|
||||
} else {
|
||||
this.viewMode = null;
|
||||
}
|
||||
constructor(args: ArgsWithoutNoteId) {
|
||||
this.args = args;
|
||||
this.viewType = this.#getViewType(args.parentNote);
|
||||
}
|
||||
|
||||
#getViewType(parentNote: FNote): ViewTypeOptions {
|
||||
const viewType = parentNote.getLabelValue("viewType");
|
||||
|
||||
if (!["list", "grid", "calendar"].includes(viewType || "")) {
|
||||
if (!(allViewTypes as readonly string[]).includes(viewType || "")) {
|
||||
// when not explicitly set, decide based on the note type
|
||||
return parentNote.type === "search" ? "list" : "grid";
|
||||
} else {
|
||||
@@ -41,15 +34,38 @@ export default class NoteListRenderer {
|
||||
}
|
||||
|
||||
get isFullHeight() {
|
||||
return this.viewMode?.isFullHeight;
|
||||
switch (this.viewType) {
|
||||
case "list":
|
||||
case "grid":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
if (!this.viewMode) {
|
||||
return null;
|
||||
}
|
||||
const args = this.args;
|
||||
const viewMode = this.#buildViewMode(args);
|
||||
this.viewMode = viewMode;
|
||||
await viewMode.beforeRender();
|
||||
return await viewMode.renderList();
|
||||
}
|
||||
|
||||
return await this.viewMode.renderList();
|
||||
#buildViewMode(args: ViewModeArgs) {
|
||||
switch (this.viewType) {
|
||||
case "calendar":
|
||||
return new CalendarView(args);
|
||||
case "table":
|
||||
return new TableView(args);
|
||||
case "geoMap":
|
||||
return new GeoView(args);
|
||||
case "board":
|
||||
return new BoardView(args);
|
||||
case "list":
|
||||
case "grid":
|
||||
default:
|
||||
return new ListOrGridView(this.viewType, args);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ let dismissTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
function setupGlobalTooltip() {
|
||||
$(document).on("mouseenter", "a", mouseEnterHandler);
|
||||
$(document).on("mouseenter", "[data-href]", mouseEnterHandler);
|
||||
|
||||
// close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
|
||||
$(document).on("click", (e) => {
|
||||
@@ -167,7 +168,10 @@ async function renderTooltip(note: FNote | null) {
|
||||
if (isContentEmpty) {
|
||||
classes.push("note-no-content");
|
||||
}
|
||||
content = `<h5 class="${classes.join(" ")}"><a href="#${note.noteId}" data-no-context-menu="true">${noteTitleWithPathAsSuffix.prop("outerHTML")}</a></h5>`;
|
||||
content = `\
|
||||
<h5 class="${classes.join(" ")}">
|
||||
<a href="#${note.noteId}" data-no-context-menu="true">${noteTitleWithPathAsSuffix.prop("outerHTML")}</a>
|
||||
</h5>`;
|
||||
}
|
||||
|
||||
content = `${content}<div class="note-tooltip-attributes">${$renderedAttributes[0].outerHTML}</div>`;
|
||||
@@ -175,6 +179,7 @@ async function renderTooltip(note: FNote | null) {
|
||||
content += $renderedContent[0].outerHTML;
|
||||
}
|
||||
|
||||
content += `<a class="open-popup-button" title="${t("note_tooltip.quick-edit")}" href="#${note.noteId}?popup"><span class="bx bx-edit" /></a>`;
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,118 @@
|
||||
import server from "./server.js";
|
||||
import froca from "./froca.js";
|
||||
import { t } from "./i18n.js";
|
||||
import type { MenuItem } from "../menus/context_menu.js";
|
||||
import froca from "./froca.js";
|
||||
import server from "./server.js";
|
||||
import type { MenuCommandItem, MenuItem, MenuItemBadge } from "../menus/context_menu.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
|
||||
|
||||
export interface NoteTypeMapping {
|
||||
type: NoteType;
|
||||
mime?: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
/** Indicates whether this type should be marked as a newly introduced feature. */
|
||||
isNew?: boolean;
|
||||
/** Indicates that this note type is part of a beta feature. */
|
||||
isBeta?: boolean;
|
||||
/** Indicates that this note type cannot be created by the user. */
|
||||
reserved?: boolean;
|
||||
/** Indicates that once a note of this type is created, its type can no longer be changed. */
|
||||
static?: boolean;
|
||||
}
|
||||
|
||||
export const NOTE_TYPES: NoteTypeMapping[] = [
|
||||
// The suggested note type ordering method: insert the item into the corresponding group,
|
||||
// then ensure the items within the group are ordered alphabetically.
|
||||
|
||||
// The default note type (always the first item)
|
||||
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
|
||||
|
||||
// Text notes group
|
||||
{ type: "book", mime: "", title: t("note_types.book"), icon: "bx-book" },
|
||||
|
||||
// Graphic notes
|
||||
{ type: "canvas", mime: "application/json", title: t("note_types.canvas"), icon: "bx-pen" },
|
||||
{ type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), icon: "bx-selection" },
|
||||
|
||||
// Map notes
|
||||
{ type: "mindMap", mime: "application/json", title: t("note_types.mind-map"), icon: "bx-sitemap" },
|
||||
{ type: "noteMap", mime: "", title: t("note_types.note-map"), icon: "bxs-network-chart", static: true },
|
||||
{ type: "relationMap", mime: "application/json", title: t("note_types.relation-map"), icon: "bxs-network-chart" },
|
||||
|
||||
// Misc note types
|
||||
{ type: "render", mime: "", title: t("note_types.render-note"), icon: "bx-extension" },
|
||||
{ type: "search", title: t("note_types.saved-search"), icon: "bx-file-find", static: true },
|
||||
{ type: "webView", mime: "", title: t("note_types.web-view"), icon: "bx-globe-alt" },
|
||||
|
||||
// Code notes
|
||||
{ type: "code", mime: "text/plain", title: t("note_types.code"), icon: "bx-code" },
|
||||
|
||||
// Reserved types (cannot be created by the user)
|
||||
{ type: "contentWidget", mime: "", title: t("note_types.widget"), reserved: true },
|
||||
{ type: "doc", mime: "", title: t("note_types.doc"), reserved: true },
|
||||
{ type: "file", title: t("note_types.file"), reserved: true },
|
||||
{ type: "image", title: t("note_types.image"), reserved: true },
|
||||
{ type: "launcher", mime: "", title: t("note_types.launcher"), reserved: true },
|
||||
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), reserved: true }
|
||||
];
|
||||
|
||||
/** The maximum age in days for a template to be marked with the "New" badge */
|
||||
const NEW_TEMPLATE_MAX_AGE = 3;
|
||||
|
||||
/** The length of a day in milliseconds. */
|
||||
const DAY_LENGTH = 1000 * 60 * 60 * 24;
|
||||
|
||||
/** The menu item badge used to mark new note types and templates */
|
||||
const NEW_BADGE: MenuItemBadge = {
|
||||
title: t("note_types.new-feature"),
|
||||
className: "new-note-type-badge"
|
||||
};
|
||||
|
||||
/** The menu item badge used to mark note types that are part of a beta feature */
|
||||
const BETA_BADGE = {
|
||||
title: t("note_types.beta-feature")
|
||||
};
|
||||
|
||||
const SEPARATOR = { title: "----" };
|
||||
|
||||
const creationDateCache = new Map<string, Date>();
|
||||
let rootCreationDate: Date | undefined;
|
||||
|
||||
async function getNoteTypeItems(command?: TreeCommandNames) {
|
||||
const items: MenuItem<TreeCommandNames>[] = [
|
||||
{ title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" },
|
||||
{ title: t("note_types.code"), command, type: "code", uiIcon: "bx bx-code" },
|
||||
{ title: t("note_types.saved-search"), command, type: "search", uiIcon: "bx bx-file-find" },
|
||||
{ title: t("note_types.relation-map"), command, type: "relationMap", uiIcon: "bx bxs-network-chart" },
|
||||
{ title: t("note_types.note-map"), command, type: "noteMap", uiIcon: "bx bxs-network-chart" },
|
||||
{ title: t("note_types.render-note"), command, type: "render", uiIcon: "bx bx-extension" },
|
||||
{ title: t("note_types.book"), command, type: "book", uiIcon: "bx bx-book" },
|
||||
{ title: t("note_types.mermaid-diagram"), command, type: "mermaid", uiIcon: "bx bx-selection" },
|
||||
{ title: t("note_types.canvas"), command, type: "canvas", uiIcon: "bx bx-pen" },
|
||||
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
|
||||
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
|
||||
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
|
||||
...await getBuiltInTemplates(command),
|
||||
...getBlankNoteTypes(command),
|
||||
...await getBuiltInTemplates(t("note_types.collections"), command, true),
|
||||
...await getBuiltInTemplates(null, command, false),
|
||||
...await getUserTemplates(command)
|
||||
];
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandNames>[] {
|
||||
return NOTE_TYPES
|
||||
.filter((nt) => !nt.reserved && nt.type !== "book")
|
||||
.map((nt) => {
|
||||
const menuItem: MenuCommandItem<TreeCommandNames> = {
|
||||
title: nt.title,
|
||||
command,
|
||||
type: nt.type,
|
||||
uiIcon: "bx " + nt.icon,
|
||||
badges: []
|
||||
}
|
||||
|
||||
if (nt.isNew) {
|
||||
menuItem.badges?.push(NEW_BADGE);
|
||||
}
|
||||
|
||||
if (nt.isBeta) {
|
||||
menuItem.badges?.push(BETA_BADGE);
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
});
|
||||
}
|
||||
|
||||
async function getUserTemplates(command?: TreeCommandNames) {
|
||||
const templateNoteIds = await server.get<string[]>("search-templates");
|
||||
const templateNotes = await froca.getNotes(templateNoteIds);
|
||||
@@ -37,19 +123,26 @@ async function getUserTemplates(command?: TreeCommandNames) {
|
||||
const items: MenuItem<TreeCommandNames>[] = [
|
||||
SEPARATOR
|
||||
];
|
||||
|
||||
for (const templateNote of templateNotes) {
|
||||
items.push({
|
||||
const item: MenuItem<TreeCommandNames> = {
|
||||
title: templateNote.title,
|
||||
uiIcon: templateNote.getIcon(),
|
||||
command: command,
|
||||
type: templateNote.type,
|
||||
templateNoteId: templateNote.noteId
|
||||
});
|
||||
};
|
||||
|
||||
if (await isNewTemplate(templateNote.noteId)) {
|
||||
item.badges = [NEW_BADGE];
|
||||
}
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async function getBuiltInTemplates(command?: TreeCommandNames) {
|
||||
async function getBuiltInTemplates(title: string | null, command: TreeCommandNames | undefined, filterCollections: boolean) {
|
||||
const templatesRoot = await froca.getNote("_templates");
|
||||
if (!templatesRoot) {
|
||||
console.warn("Unable to find template root.");
|
||||
@@ -61,21 +154,85 @@ async function getBuiltInTemplates(command?: TreeCommandNames) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: MenuItem<TreeCommandNames>[] = [
|
||||
SEPARATOR
|
||||
];
|
||||
for (const templateNote of childNotes) {
|
||||
const items: MenuItem<TreeCommandNames>[] = [];
|
||||
if (title) {
|
||||
items.push({
|
||||
title: title,
|
||||
enabled: false,
|
||||
uiIcon: "bx bx-empty"
|
||||
});
|
||||
} else {
|
||||
items.push(SEPARATOR);
|
||||
}
|
||||
|
||||
for (const templateNote of childNotes) {
|
||||
if (templateNote.hasLabel("collection") !== filterCollections) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const item: MenuItem<TreeCommandNames> = {
|
||||
title: templateNote.title,
|
||||
uiIcon: templateNote.getIcon(),
|
||||
command: command,
|
||||
type: templateNote.type,
|
||||
templateNoteId: templateNote.noteId
|
||||
});
|
||||
};
|
||||
|
||||
if (await isNewTemplate(templateNote.noteId)) {
|
||||
item.badges = [NEW_BADGE];
|
||||
}
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async function isNewTemplate(templateNoteId) {
|
||||
if (rootCreationDate === undefined) {
|
||||
// Retrieve the root note creation date
|
||||
try {
|
||||
let rootNoteInfo: any = await server.get("notes/root");
|
||||
if ("dateCreated" in rootNoteInfo) {
|
||||
rootCreationDate = new Date(rootNoteInfo.dateCreated);
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to retrieve the template's creation date from the cache
|
||||
let creationDate: Date | undefined = creationDateCache.get(templateNoteId);
|
||||
|
||||
if (creationDate === undefined) {
|
||||
// The creation date isn't available in the cache, try to retrieve it from the server
|
||||
try {
|
||||
const noteInfo: any = await server.get("notes/" + templateNoteId);
|
||||
if ("dateCreated" in noteInfo) {
|
||||
creationDate = new Date(noteInfo.dateCreated);
|
||||
creationDateCache.set(templateNoteId, creationDate);
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (creationDate) {
|
||||
if (rootCreationDate && creationDate.getTime() - rootCreationDate.getTime() < 30000) {
|
||||
// Ignore templates created within 30 seconds after the root note is created.
|
||||
// This is useful to prevent predefined templates from being marked
|
||||
// as 'New' after setting up a new database.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine the difference in days between now and the template's creation date
|
||||
const age = (new Date().getTime() - creationDate.getTime()) / DAY_LENGTH;
|
||||
// Return true if the template is at most NEW_TEMPLATE_MAX_AGE days old
|
||||
return (age <= NEW_TEMPLATE_MAX_AGE);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getNoteTypeItems
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url";
|
||||
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
|
||||
type Multiplicity = "single" | "multi";
|
||||
|
||||
export interface DefinitionObject {
|
||||
@@ -17,7 +17,7 @@ function parse(value: string) {
|
||||
for (const token of tokens) {
|
||||
if (token === "promoted") {
|
||||
defObj.isPromoted = true;
|
||||
} else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
|
||||
} else if (["text", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
|
||||
defObj.labelType = token as LabelType;
|
||||
} else if (["single", "multi"].includes(token)) {
|
||||
defObj.multiplicity = token as Multiplicity;
|
||||
|
||||
@@ -51,6 +51,14 @@ export default class SpacedUpdate {
|
||||
this.lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the update interval for the spaced update.
|
||||
* @param interval The update interval in milliseconds.
|
||||
*/
|
||||
setUpdateInterval(interval: number) {
|
||||
this.updateInterval = interval;
|
||||
}
|
||||
|
||||
triggerUpdate() {
|
||||
if (!this.changed) {
|
||||
return;
|
||||
|
||||
@@ -29,6 +29,14 @@ async function formatCodeBlocks() {
|
||||
await formatCodeBlocks($("#content"));
|
||||
}
|
||||
|
||||
async function setupTextNote() {
|
||||
formatCodeBlocks();
|
||||
applyMath();
|
||||
|
||||
const setupMermaid = (await import("./share/mermaid.js")).default;
|
||||
setupMermaid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch note with given ID from backend
|
||||
*
|
||||
@@ -47,8 +55,11 @@ async function fetchNote(noteId: string | null = null) {
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
() => {
|
||||
formatCodeBlocks();
|
||||
applyMath();
|
||||
const noteType = determineNoteType();
|
||||
|
||||
if (noteType === "text") {
|
||||
setupTextNote();
|
||||
}
|
||||
|
||||
const toggleMenuButton = document.getElementById("toggleMenuButton");
|
||||
const layout = document.getElementById("layout");
|
||||
@@ -60,6 +71,12 @@ document.addEventListener(
|
||||
false
|
||||
);
|
||||
|
||||
function determineNoteType() {
|
||||
const bodyClass = document.body.className;
|
||||
const match = bodyClass.match(/type-([^\s]+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// workaround to prevent webpack from removing "fetchNote" as dead code:
|
||||
// add fetchNote as property to the window object
|
||||
Object.defineProperty(window, "fetchNote", {
|
||||
|
||||
17
apps/client/src/share/mermaid.ts
Normal file
17
apps/client/src/share/mermaid.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import mermaid from "mermaid";
|
||||
|
||||
export default function setupMermaid() {
|
||||
for (const codeBlock of document.querySelectorAll("#content pre code.language-mermaid")) {
|
||||
const parentPre = codeBlock.parentElement;
|
||||
if (!parentPre) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mermaidDiv = document.createElement("div");
|
||||
mermaidDiv.classList.add("mermaid");
|
||||
mermaidDiv.innerHTML = codeBlock.innerHTML;
|
||||
parentPre.replaceWith(mermaidDiv);
|
||||
}
|
||||
|
||||
mermaid.init();
|
||||
}
|
||||
@@ -81,8 +81,8 @@ body {
|
||||
|
||||
/* -- Overrides the default colors used by the ckeditor5-image package. --------------------- */
|
||||
|
||||
--ck-color-image-caption-background: var(--main-background-color);
|
||||
--ck-color-image-caption-text: var(--main-text-color);
|
||||
--ck-content-color-image-caption-background: var(--main-background-color);
|
||||
--ck-content-color-image-caption-text: var(--main-text-color);
|
||||
|
||||
/* -- Overrides the default colors used by the ckeditor5-widget package. -------------------- */
|
||||
|
||||
|
||||
@@ -139,12 +139,6 @@ textarea,
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
/* Restore default apperance */
|
||||
input[type="number"],
|
||||
input[type="checkbox"] {
|
||||
appearance: auto !important;
|
||||
}
|
||||
|
||||
/* Add a gap between consecutive radios / check boxes */
|
||||
label.tn-radio + label.tn-radio,
|
||||
label.tn-checkbox + label.tn-checkbox {
|
||||
@@ -192,6 +186,13 @@ samp {
|
||||
font-family: var(--monospace-font-family) !important;
|
||||
}
|
||||
|
||||
.badge {
|
||||
--bs-badge-color: var(--muted-text-color);
|
||||
|
||||
margin-left: 8px;
|
||||
background: var(--accented-background-color);
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: var(--accented-background-color) !important;
|
||||
color: var(--muted-text-color) !important;
|
||||
@@ -320,7 +321,8 @@ button kbd {
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
.dropdown-menu,
|
||||
.tabulator-popup-container {
|
||||
color: var(--menu-text-color) !important;
|
||||
font-size: inherit;
|
||||
background-color: var(--menu-background-color) !important;
|
||||
@@ -330,7 +332,13 @@ button kbd {
|
||||
--bs-dropdown-link-active-bg: var(--active-item-background-color) !important;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu {
|
||||
.dropdown-menu .dropdown-divider {
|
||||
break-before: avoid;
|
||||
break-after: avoid;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu,
|
||||
body.desktop .tabulator-popup-container {
|
||||
border: 1px solid var(--dropdown-border-color);
|
||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
||||
animation: dropdown-menu-opening 100ms ease-in;
|
||||
@@ -373,7 +381,8 @@ body.desktop .dropdown-menu {
|
||||
}
|
||||
|
||||
.dropdown-menu a:hover:not(.disabled),
|
||||
.dropdown-item:hover:not(.disabled, .dropdown-item-container) {
|
||||
.dropdown-item:hover:not(.disabled, .dropdown-item-container),
|
||||
.tabulator-menu-item:hover {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--hover-item-border-color) !important;
|
||||
@@ -528,6 +537,7 @@ button.btn-sm {
|
||||
/* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
pre:not(.hljs) {
|
||||
@@ -759,6 +769,14 @@ table.promoted-attributes-in-tooltip th {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.note-tooltip-content .open-popup-button {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
bottom: 8px;
|
||||
font-size: 1.2em;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.note-tooltip-attributes {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
@@ -900,6 +918,13 @@ div[data-notify="container"] {
|
||||
font-family: var(--monospace-font-family);
|
||||
}
|
||||
|
||||
.ck-content {
|
||||
--ck-content-font-family: var(--detail-font-family);
|
||||
--ck-content-font-size: 1.1em;
|
||||
--ck-content-font-color: var(--main-text-color);
|
||||
--ck-content-line-height: var(--bs-body-line-height);
|
||||
}
|
||||
|
||||
.ck-content .table table th {
|
||||
background-color: var(--accented-background-color);
|
||||
}
|
||||
@@ -1186,12 +1211,14 @@ body.mobile .dropdown-submenu > .dropdown-menu {
|
||||
}
|
||||
|
||||
#context-menu-container,
|
||||
#context-menu-container .dropdown-menu {
|
||||
padding: 3px 0 0;
|
||||
#context-menu-container .dropdown-menu,
|
||||
.tabulator-popup-container {
|
||||
padding: 3px 0;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
#context-menu-container .dropdown-item {
|
||||
#context-menu-container .dropdown-item,
|
||||
.tabulator-menu .tabulator-menu-item {
|
||||
padding: 0 7px 0 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@@ -2174,3 +2201,189 @@ footer.file-footer button {
|
||||
content: "\ec24";
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* File System Sync Modal Styles */
|
||||
.mapping-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1050;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mapping-modal .modal-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1051;
|
||||
}
|
||||
|
||||
.mapping-modal .modal-content {
|
||||
position: relative;
|
||||
background: var(--main-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
z-index: 1052;
|
||||
}
|
||||
|
||||
.mapping-modal .modal-header {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.mapping-modal .modal-title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mapping-modal .modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--muted-text-color);
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mapping-modal .modal-close:hover {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.mapping-modal .modal-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.mapping-modal .modal-footer {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* File System Sync Mapping Cards */
|
||||
.mapping-item.card {
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 5px;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.mapping-item.card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.mapping-item .mapping-path {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.mapping-item .mapping-details {
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.mapping-item .mapping-status {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mapping-item .mapping-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.mapping-item .mapping-actions .btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge.badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.status-badge.badge-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.badge-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.badge-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Path Validation Styles */
|
||||
.path-validation-result {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.path-validation-result .text-success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.path-validation-result .text-warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.path-validation-result .text-danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* Sync Status Section */
|
||||
.sync-status-container {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: var(--accented-background-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.sync-status-info .status-item,
|
||||
.sync-status-info .active-mappings-count {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Form Enhancements */
|
||||
.mapping-form .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mapping-form .subtree-options {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.mapping-form .help-block {
|
||||
font-size: 0.875rem;
|
||||
color: var(--muted-text-color);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
199
apps/client/src/stylesheets/table.css
Normal file
199
apps/client/src/stylesheets/table.css
Normal file
@@ -0,0 +1,199 @@
|
||||
.tabulator {
|
||||
--table-background-color: var(--main-background-color);
|
||||
|
||||
--col-header-background-color: var(--main-background-color);
|
||||
--col-header-hover-background-color: var(--accented-background-color);
|
||||
--col-header-text-color: var(--main-text-color);
|
||||
--col-header-arrow-active-color: var(--main-text-color);
|
||||
--col-header-arrow-inactive-color: var(--more-accented-background-color);
|
||||
--col-header-separator-border: none;
|
||||
--col-header-bottom-border: 2px solid var(--main-border-color);
|
||||
|
||||
--row-background-color: var(--main-background-color);
|
||||
--row-alternate-background-color: var(--main-background-color);
|
||||
--row-moving-background-color: var(--accented-background-color);
|
||||
--row-text-color: var(--main-text-color);
|
||||
--row-delimiter-color: var(--more-accented-background-color);
|
||||
|
||||
--cell-horiz-padding-size: 8px;
|
||||
--cell-vert-padding-size: 8px;
|
||||
|
||||
--cell-editable-hover-outline-color: var(--main-border-color);
|
||||
--cell-read-only-text-color: var(--muted-text-color);
|
||||
|
||||
--cell-editing-border-color: var(--main-border-color);
|
||||
--cell-editing-border-width: 2px;
|
||||
--cell-editing-background-color: var(--ck-color-selector-focused-cell-background);
|
||||
--cell-editing-text-color: initial;
|
||||
|
||||
background: unset;
|
||||
border: unset;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableholder .tabulator-table {
|
||||
background: var(--table-background-color);
|
||||
}
|
||||
|
||||
/* Column headers */
|
||||
|
||||
.tabulator div.tabulator-header {
|
||||
border-bottom: var(--col-header-bottom-border);
|
||||
background: var(--col-header-background-color);
|
||||
color: var(--col-header-text-color);
|
||||
}
|
||||
|
||||
.tabulator .tabulator-col-content {
|
||||
padding: 8px 4px !important;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover {
|
||||
background-color: var(--col-header-hover-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.tabulator div.tabulator-header .tabulator-col.tabulator-moving {
|
||||
border: none;
|
||||
background: var(--col-header-hover-background-color);
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
|
||||
border-bottom-color: var(--col-header-arrow-active-color);
|
||||
border-top-color: var(--col-header-arrow-active-color);
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="none"] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
|
||||
border-bottom-color: var(--col-header-arrow-inactive-color);
|
||||
}
|
||||
|
||||
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
|
||||
margin-left: var(--cell-editing-border-width);
|
||||
}
|
||||
|
||||
.tabulator div.tabulator-header .tabulator-col,
|
||||
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
|
||||
background: var(--col-header-background-color);
|
||||
border-right: var(--col-header-separator-border);
|
||||
}
|
||||
|
||||
/* Table body */
|
||||
|
||||
.tabulator-tableholder {
|
||||
padding-top: 10px;
|
||||
height: unset !important; /* Don't extend on the full height */
|
||||
}
|
||||
|
||||
/* Rows */
|
||||
|
||||
.tabulator-row .tabulator-cell {
|
||||
padding: var(--cell-vert-padding-size) var(--cell-horiz-padding-size);
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell input {
|
||||
padding-left: var(--cell-horiz-padding-size) !important;
|
||||
padding-right: var(--cell-horiz-padding-size) !important;
|
||||
}
|
||||
|
||||
.tabulator-row {
|
||||
background: transparent;
|
||||
border-top: none;
|
||||
border-bottom: 1px solid var(--row-delimiter-color);
|
||||
color: var(--row-text-color);
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-row-odd {
|
||||
background: var(--row-background-color);
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-row-even {
|
||||
background: var(--row-alternate-background-color);
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-moving {
|
||||
border-color: transparent;
|
||||
background-color: var(--row-moving-background-color);
|
||||
}
|
||||
|
||||
/* Cell */
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
|
||||
margin-right: var(--cell-editing-border-width);
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,
|
||||
.tabulator-row .tabulator-cell {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell:not(.tabulator-editable) {
|
||||
color: var(--cell-read-only-text-color);
|
||||
}
|
||||
|
||||
.tabulator:not(.tabulator-editing) .tabulator-row .tabulator-cell.tabulator-editable:hover {
|
||||
outline: 2px solid var(--cell-editable-hover-outline-color);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-editing {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.tabulator-row:not(.tabulator-moving) .tabulator-cell.tabulator-editing {
|
||||
outline: calc(var(--cell-editing-border-width) - 1px) solid var(--cell-editing-border-color);
|
||||
border-color: var(--cell-editing-border-color);
|
||||
background: var(--cell-editing-background-color);
|
||||
}
|
||||
|
||||
.tabulator-row:not(.tabulator-moving) .tabulator-cell.tabulator-editing > * {
|
||||
color: var(--cell-editing-text-color);
|
||||
}
|
||||
|
||||
.tabulator .tree-collapse,
|
||||
.tabulator .tree-expand {
|
||||
color: var(--row-text-color);
|
||||
}
|
||||
|
||||
/* Align items without children/expander to the ones with. */
|
||||
.tabulator-cell[tabulator-field="title"] > span:first-child, /* 1st level */
|
||||
.tabulator-cell[tabulator-field="title"] > div:first-child + span { /* sub-level */
|
||||
padding-left: 21px;
|
||||
}
|
||||
|
||||
/* Checkbox cells */
|
||||
|
||||
.tabulator .tabulator-cell:has(svg),
|
||||
.tabulator .tabulator-cell:has(input[type="checkbox"]) {
|
||||
padding-left: 8px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-cell input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
/* Context menus */
|
||||
|
||||
.tabulator-popup-container {
|
||||
min-width: 10em;
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.tabulator-menu .tabulator-menu-item {
|
||||
border: 1px solid transparent;
|
||||
color: var(--menu-text-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
:root .tabulator .tabulator-footer {
|
||||
border-top: unset;
|
||||
padding: 10px 0;
|
||||
}
|
||||
@@ -178,6 +178,9 @@
|
||||
|
||||
--alert-bar-background: #6b6b6b3b;
|
||||
|
||||
--badge-background-color: #ffffff1a;
|
||||
--badge-text-color: var(--muted-text-color);
|
||||
|
||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
||||
--promoted-attribute-card-shadow-color: #000000b3;
|
||||
|
||||
|
||||
@@ -171,6 +171,9 @@
|
||||
|
||||
--alert-bar-background: #32637b29;
|
||||
|
||||
--badge-background-color: #00000011;
|
||||
--badge-text-color: var(--muted-text-color);
|
||||
|
||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
||||
--promoted-attribute-card-shadow-color: #00000033;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@import url(./pages.css);
|
||||
@import url(./ribbon.css);
|
||||
@import url(./notes/text.css);
|
||||
@import url(./notes/collections/table.css);
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
@@ -171,9 +172,19 @@ html body .dropdown-item[disabled] {
|
||||
opacity: var(--menu-item-disabled-opacity);
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
:root .badge {
|
||||
--bs-badge-color: var(--badge-text-color);
|
||||
--bs-badge-font-weight: 500;
|
||||
|
||||
background: var(--badge-background-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .2pt;
|
||||
}
|
||||
|
||||
/* Menu item icon */
|
||||
.dropdown-item .bx {
|
||||
transform: translateY(var(--menu-item-icon-vert-offset));
|
||||
translate: 0 var(--menu-item-icon-vert-offset);
|
||||
color: var(--menu-item-icon-color) !important;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
@@ -382,6 +382,10 @@ div.tn-tool-dialog {
|
||||
|
||||
/* DELETE NOTE PREVIEW DIALOG */
|
||||
|
||||
.delete-notes-dialog .modal-dialog {
|
||||
--bs-modal-width: fit-content;
|
||||
}
|
||||
|
||||
.delete-notes-list .note-path {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
:root .tabulator {
|
||||
--col-header-hover-background-color: var(--hover-item-background-color);
|
||||
--col-header-arrow-active-color: var(--active-item-text-color);
|
||||
--col-header-arrow-inactive-color: var(--main-border-color);
|
||||
|
||||
--row-moving-background-color: var(--more-accented-background-color);
|
||||
|
||||
--cell-editable-hover-outline-color: var(--input-focus-outline-color);
|
||||
|
||||
--cell-editing-border-color: var(--input-focus-outline-color);
|
||||
--cell-editing-background-color: var(--input-background-color);
|
||||
--cell-editing-text-color: var(--input-text-color);
|
||||
}
|
||||
@@ -46,6 +46,12 @@ div.promoted-attributes-container {
|
||||
.image-properties > div:first-child > span > strong {
|
||||
opacity: 0.65;
|
||||
font-weight: 500;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.note-info-widget-table td,
|
||||
.file-properties-widget .file-table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.file-properties-widget {
|
||||
|
||||
@@ -71,12 +71,13 @@ body.background-effects.platform-win32.layout-vertical #vertical-main-container
|
||||
/* #endregion */
|
||||
|
||||
/* Matches when the left pane is collapsed */
|
||||
:has(.layout-vertical #left-pane.hidden-int) {
|
||||
#horizontal-main-container.left-pane-hidden {
|
||||
--center-pane-border-radius: 0;
|
||||
--tab-first-item-horiz-offset: 5px;
|
||||
}
|
||||
|
||||
:has(#left-pane.hidden-int) #launcher-pane.vertical {
|
||||
/* Add a border to the vertical launch bar if collapsed. */
|
||||
body.layout-vertical #horizontal-main-container.left-pane-hidden #launcher-pane.vertical {
|
||||
border-right: 2px solid var(--left-pane-collapsed-border-color);
|
||||
}
|
||||
|
||||
@@ -1300,9 +1301,9 @@ div.promoted-attribute-cell .tn-checkbox {
|
||||
height: 1cap;
|
||||
}
|
||||
|
||||
/* The <div> containing the checkbox for a promoted boolean attribute */
|
||||
div.promoted-attribute-cell div:has(input[type="checkbox"]) {
|
||||
order: -1; /* Relocate the checkbox before the label */
|
||||
/* Relocate the checkbox before the label */
|
||||
div.promoted-attribute-cell.promoted-attribute-label-boolean > div:first-of-type {
|
||||
order: -1;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
@@ -1677,4 +1678,42 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
|
||||
#right-pane .highlights-list li:active {
|
||||
background: transparent;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/** Canvas **/
|
||||
|
||||
.excalidraw {
|
||||
--border-radius-lg: 6px;
|
||||
}
|
||||
|
||||
.excalidraw .Island {
|
||||
backdrop-filter: var(--dropdown-backdrop-filter);
|
||||
}
|
||||
|
||||
.excalidraw .Island.App-toolbar {
|
||||
--island-bg-color: var(--floating-button-background-color);
|
||||
--shadow-island: 1px 1px 1px var(--floating-button-shadow-color);
|
||||
}
|
||||
|
||||
.excalidraw .dropdown-menu {
|
||||
border: unset !important;
|
||||
box-shadow: unset !important;
|
||||
background-color: transparent !important;
|
||||
--island-bg-color: var(--menu-background-color);
|
||||
--shadow-island: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
||||
--default-border-color: var(--bs-dropdown-divider-bg);
|
||||
--button-hover-bg: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
.excalidraw .dropdown-menu .dropdown-menu-container {
|
||||
border-radius: var(--dropdown-border-radius);
|
||||
}
|
||||
|
||||
.excalidraw .dropdown-menu .dropdown-menu-container > div:not([class]):not(:last-child) {
|
||||
margin-left: calc(var(--padding) * var(--space-factor) * -1) !important;
|
||||
margin-right: calc(var(--padding) * var(--space-factor) * -1) !important;
|
||||
}
|
||||
|
||||
.excalidraw .dropdown-menu:before {
|
||||
content: unset !important;
|
||||
}
|
||||
@@ -754,7 +754,7 @@
|
||||
"expand_all_children": "展开所有子项",
|
||||
"collapse": "折叠",
|
||||
"expand": "展开",
|
||||
"book_properties": "书籍属性",
|
||||
"book_properties": "",
|
||||
"invalid_view_type": "无效的查看类型 '{{type}}'",
|
||||
"calendar": "日历"
|
||||
},
|
||||
@@ -1431,7 +1431,6 @@
|
||||
"move-to": "移动到...",
|
||||
"paste-into": "粘贴到里面",
|
||||
"paste-after": "粘贴到后面",
|
||||
"duplicate-subtree": "复制子树",
|
||||
"export": "导出",
|
||||
"import-into-note": "导入到笔记",
|
||||
"apply-bulk-actions": "应用批量操作",
|
||||
@@ -1450,7 +1449,7 @@
|
||||
"relation-map": "关系图",
|
||||
"note-map": "笔记地图",
|
||||
"render-note": "渲染笔记",
|
||||
"book": "书",
|
||||
"book": "",
|
||||
"mermaid-diagram": "Mermaid 图",
|
||||
"canvas": "画布",
|
||||
"web-view": "网页视图",
|
||||
|
||||
@@ -750,7 +750,7 @@
|
||||
"expand_all_children": "Unternotizen ausklappen",
|
||||
"collapse": "Einklappen",
|
||||
"expand": "Ausklappen",
|
||||
"book_properties": "Bucheigenschaften",
|
||||
"book_properties": "",
|
||||
"invalid_view_type": "Ungültiger Ansichtstyp „{{type}}“",
|
||||
"calendar": "Kalender"
|
||||
},
|
||||
@@ -1384,7 +1384,7 @@
|
||||
"move-to": "Verschieben nach...",
|
||||
"paste-into": "Als Unternotiz einfügen",
|
||||
"paste-after": "Danach einfügen",
|
||||
"duplicate-subtree": "Notizbaum duplizieren",
|
||||
"duplicate": "Duplizieren",
|
||||
"export": "Exportieren",
|
||||
"import-into-note": "In Notiz importieren",
|
||||
"apply-bulk-actions": "Massenaktionen ausführen",
|
||||
@@ -1403,7 +1403,7 @@
|
||||
"relation-map": "Beziehungskarte",
|
||||
"note-map": "Notizkarte",
|
||||
"render-note": "Render Notiz",
|
||||
"book": "Buch",
|
||||
"book": "",
|
||||
"mermaid-diagram": "Mermaid Diagram",
|
||||
"canvas": "Canvas",
|
||||
"web-view": "Webansicht",
|
||||
|
||||
@@ -443,7 +443,8 @@
|
||||
"other_notes_with_name": "Other notes with {{attributeType}} name \"{{attributeName}}\"",
|
||||
"and_more": "... and {{count}} more.",
|
||||
"print_landscape": "When exporting to PDF, changes the orientation of the page to landscape instead of portrait.",
|
||||
"print_page_size": "When exporting to PDF, changes the size of the page. Supported values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>."
|
||||
"print_page_size": "When exporting to PDF, changes the size of the page. Supported values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
|
||||
"color_type": "Color"
|
||||
},
|
||||
"attribute_editor": {
|
||||
"help_text_body1": "To add label, just type e.g. <code>#rock</code> or if you want to add also value then e.g. <code>#year = 2020</code>",
|
||||
@@ -758,9 +759,12 @@
|
||||
"expand_all_children": "Expand all children",
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"book_properties": "Book Properties",
|
||||
"book_properties": "Collection Properties",
|
||||
"invalid_view_type": "Invalid view type '{{type}}'",
|
||||
"calendar": "Calendar"
|
||||
"calendar": "Calendar",
|
||||
"table": "Table",
|
||||
"geo-map": "Geo Map",
|
||||
"board": "Board"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "No edited notes on this day yet...",
|
||||
@@ -837,7 +841,8 @@
|
||||
"unknown_label_type": "Unknown label type '{{type}}'",
|
||||
"unknown_attribute_type": "Unknown attribute type '{{type}}'",
|
||||
"add_new_attribute": "Add new attribute",
|
||||
"remove_this_attribute": "Remove this attribute"
|
||||
"remove_this_attribute": "Remove this attribute",
|
||||
"remove_color": "Remove the color label"
|
||||
},
|
||||
"script_executor": {
|
||||
"query": "Query",
|
||||
@@ -960,7 +965,7 @@
|
||||
"no_attachments": "This note has no attachments."
|
||||
},
|
||||
"book": {
|
||||
"no_children_help": "This note of type Book doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details."
|
||||
"no_children_help": "This collection doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details."
|
||||
},
|
||||
"editable_code": {
|
||||
"placeholder": "Type the content of your code note here..."
|
||||
@@ -1023,7 +1028,7 @@
|
||||
"title": "Consistency Checks",
|
||||
"find_and_fix_button": "Find and fix consistency issues",
|
||||
"finding_and_fixing_message": "Finding and fixing consistency issues...",
|
||||
"issues_fixed_message": "Consistency issues should be fixed."
|
||||
"issues_fixed_message": "Any consistency issue which may have been found is now fixed."
|
||||
},
|
||||
"database_anonymization": {
|
||||
"title": "Database Anonymization",
|
||||
@@ -1593,12 +1598,13 @@
|
||||
"move-to": "Move to...",
|
||||
"paste-into": "Paste into",
|
||||
"paste-after": "Paste after",
|
||||
"duplicate-subtree": "Duplicate subtree",
|
||||
"duplicate": "Duplicate",
|
||||
"export": "Export",
|
||||
"import-into-note": "Import into note",
|
||||
"apply-bulk-actions": "Apply bulk actions",
|
||||
"converted-to-attachments": "{{count}} notes have been converted to attachments.",
|
||||
"convert-to-attachment-confirm": "Are you sure you want to convert note selected notes into attachments of their parent notes?"
|
||||
"convert-to-attachment-confirm": "Are you sure you want to convert note selected notes into attachments of their parent notes?",
|
||||
"open-in-popup": "Quick edit"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "This note is shared publicly on",
|
||||
@@ -1612,7 +1618,7 @@
|
||||
"relation-map": "Relation Map",
|
||||
"note-map": "Note Map",
|
||||
"render-note": "Render Note",
|
||||
"book": "Book",
|
||||
"book": "Collection",
|
||||
"mermaid-diagram": "Mermaid Diagram",
|
||||
"canvas": "Canvas",
|
||||
"web-view": "Web View",
|
||||
@@ -1626,7 +1632,9 @@
|
||||
"geo-map": "Geo Map",
|
||||
"beta-feature": "Beta",
|
||||
"ai-chat": "AI Chat",
|
||||
"task-list": "Task List"
|
||||
"task-list": "Task List",
|
||||
"new-feature": "New",
|
||||
"collections": "Collections"
|
||||
},
|
||||
"protect_note": {
|
||||
"toggle-on": "Protect the note",
|
||||
@@ -1828,7 +1836,8 @@
|
||||
"link_context_menu": {
|
||||
"open_note_in_new_tab": "Open note in a new tab",
|
||||
"open_note_in_new_split": "Open note in a new split",
|
||||
"open_note_in_new_window": "Open note in a new window"
|
||||
"open_note_in_new_window": "Open note in a new window",
|
||||
"open_note_in_popup": "Quick edit"
|
||||
},
|
||||
"electron_integration": {
|
||||
"desktop-application": "Desktop Application",
|
||||
@@ -1848,7 +1857,8 @@
|
||||
"full-text-search": "Full text search"
|
||||
},
|
||||
"note_tooltip": {
|
||||
"note-has-been-deleted": "Note has been deleted."
|
||||
"note-has-been-deleted": "Note has been deleted.",
|
||||
"quick-edit": "Quick edit"
|
||||
},
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Create a new child note and add it to the map",
|
||||
@@ -1857,7 +1867,8 @@
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Open location",
|
||||
"remove-from-map": "Remove from map"
|
||||
"remove-from-map": "Remove from map",
|
||||
"add-note": "Add a marker at this location"
|
||||
},
|
||||
"help-button": {
|
||||
"title": "Open the relevant help page"
|
||||
@@ -1933,5 +1944,48 @@
|
||||
"title": "Features",
|
||||
"emoji_completion_enabled": "Enable Emoji auto-completion",
|
||||
"note_completion_enabled": "Enable note auto-completion"
|
||||
},
|
||||
"table_view": {
|
||||
"new-row": "New row",
|
||||
"new-column": "New column",
|
||||
"sort-column-by": "Sort by \"{{title}}\"",
|
||||
"sort-column-ascending": "Ascending",
|
||||
"sort-column-descending": "Descending",
|
||||
"sort-column-clear": "Clear sorting",
|
||||
"hide-column": "Hide column \"{{title}}\"",
|
||||
"show-hide-columns": "Show/hide columns",
|
||||
"row-insert-above": "Insert row above",
|
||||
"row-insert-below": "Insert row below",
|
||||
"row-insert-child": "Insert child note",
|
||||
"add-column-to-the-left": "Add column to the left",
|
||||
"add-column-to-the-right": "Add column to the right",
|
||||
"edit-column": "Edit column",
|
||||
"delete_column_confirmation": "Are you sure you want to delete this column? The corresponding attribute will be removed from all notes.",
|
||||
"delete-column": "Delete column",
|
||||
"new-column-label": "Label",
|
||||
"new-column-relation": "Relation"
|
||||
},
|
||||
"book_properties_config": {
|
||||
"hide-weekends": "Hide weekends",
|
||||
"display-week-numbers": "Display week numbers",
|
||||
"map-style": "Map style:",
|
||||
"max-nesting-depth": "Max nesting depth:",
|
||||
"raster": "Raster",
|
||||
"vector_light": "Vector (Light)",
|
||||
"vector_dark": "Vector (Dark)",
|
||||
"show-scale": "Show scale"
|
||||
},
|
||||
"table_context_menu": {
|
||||
"delete_row": "Delete row"
|
||||
},
|
||||
"board_view": {
|
||||
"delete-note": "Delete Note",
|
||||
"move-to": "Move to",
|
||||
"insert-above": "Insert above",
|
||||
"insert-below": "Insert below",
|
||||
"delete-column": "Delete column",
|
||||
"delete-column-confirmation": "Are you sure you want to delete this column? The corresponding attribute will be deleted in the notes under this column as well.",
|
||||
"new-item": "New item",
|
||||
"add-column": "Add Column"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,7 +758,7 @@
|
||||
"expand_all_children": "Ampliar todas las subnotas",
|
||||
"collapse": "Colapsar",
|
||||
"expand": "Expandir",
|
||||
"book_properties": "Propiedades del libro",
|
||||
"book_properties": "",
|
||||
"invalid_view_type": "Tipo de vista inválida '{{type}}'",
|
||||
"calendar": "Calendario"
|
||||
},
|
||||
@@ -1593,7 +1593,7 @@
|
||||
"move-to": "Mover a...",
|
||||
"paste-into": "Pegar en",
|
||||
"paste-after": "Pegar después de",
|
||||
"duplicate-subtree": "Duplicar subárbol",
|
||||
"duplicate": "Duplicar",
|
||||
"export": "Exportar",
|
||||
"import-into-note": "Importar a nota",
|
||||
"apply-bulk-actions": "Aplicar acciones en lote",
|
||||
@@ -1612,7 +1612,7 @@
|
||||
"relation-map": "Mapa de Relaciones",
|
||||
"note-map": "Mapa de Notas",
|
||||
"render-note": "Nota de Renderizado",
|
||||
"book": "Libro",
|
||||
"book": "",
|
||||
"mermaid-diagram": "Diagrama Mermaid",
|
||||
"canvas": "Lienzo",
|
||||
"web-view": "Vista Web",
|
||||
|
||||
@@ -753,7 +753,7 @@
|
||||
"expand_all_children": "Développer tous les enfants",
|
||||
"collapse": "Réduire",
|
||||
"expand": "Développer",
|
||||
"book_properties": "Propriétés du livre",
|
||||
"book_properties": "",
|
||||
"invalid_view_type": "Type de vue non valide '{{type}}'",
|
||||
"calendar": "Calendrier"
|
||||
},
|
||||
@@ -1389,7 +1389,7 @@
|
||||
"move-to": "Déplacer vers...",
|
||||
"paste-into": "Coller dans",
|
||||
"paste-after": "Coller après",
|
||||
"duplicate-subtree": "Dupliquer le sous-arbre",
|
||||
"duplicate": "Dupliquer",
|
||||
"export": "Exporter",
|
||||
"import-into-note": "Importer dans la note",
|
||||
"apply-bulk-actions": "Appliquer des Actions groupées",
|
||||
@@ -1408,7 +1408,7 @@
|
||||
"relation-map": "Carte des relations",
|
||||
"note-map": "Carte de notes",
|
||||
"render-note": "Rendu Html",
|
||||
"book": "Livre",
|
||||
"book": "",
|
||||
"mermaid-diagram": "Diagramme Mermaid",
|
||||
"canvas": "Canevas",
|
||||
"web-view": "Affichage Web",
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
"no_children_help": "Această notiță de tip Carte nu are nicio subnotiță așadar nu este nimic de afișat. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> pentru detalii."
|
||||
},
|
||||
"book_properties": {
|
||||
"book_properties": "Proprietăți carte",
|
||||
"book_properties": "",
|
||||
"collapse": "Minimizează",
|
||||
"collapse_all_notes": "Minimizează toate notițele",
|
||||
"expand": "Expandează",
|
||||
@@ -1349,7 +1349,7 @@
|
||||
"copy-note-path-to-clipboard": "Copiază calea notiței în clipboard",
|
||||
"cut": "Decupează",
|
||||
"delete": "Șterge",
|
||||
"duplicate-subtree": "Dublifică ierarhia",
|
||||
"duplicate": "Dublifică",
|
||||
"edit-branch-prefix": "Editează prefixul ramurii",
|
||||
"expand-subtree": "Expandează subnotițele",
|
||||
"export": "Exportă",
|
||||
@@ -1377,7 +1377,7 @@
|
||||
"shared_publicly": "Această notiță este partajată public la"
|
||||
},
|
||||
"note_types": {
|
||||
"book": "Carte",
|
||||
"book": "Colecție",
|
||||
"canvas": "Schiță",
|
||||
"code": "Cod sursă",
|
||||
"mermaid-diagram": "Diagramă Mermaid",
|
||||
|
||||
@@ -718,7 +718,7 @@
|
||||
"expand_all_children": "展開所有子項",
|
||||
"collapse": "折疊",
|
||||
"expand": "展開",
|
||||
"book_properties": "書籍屬性",
|
||||
"book_properties": "",
|
||||
"invalid_view_type": "無效的查看類型 '{{type}}'"
|
||||
},
|
||||
"edited_notes": {
|
||||
@@ -1336,7 +1336,6 @@
|
||||
"move-to": "移動到...",
|
||||
"paste-into": "貼上到裡面",
|
||||
"paste-after": "貼上到後面",
|
||||
"duplicate-subtree": "複製子樹",
|
||||
"export": "匯出",
|
||||
"import-into-note": "匯入到筆記",
|
||||
"apply-bulk-actions": "應用批量操作",
|
||||
@@ -1355,7 +1354,7 @@
|
||||
"relation-map": "關係圖",
|
||||
"note-map": "筆記地圖",
|
||||
"render-note": "渲染筆記",
|
||||
"book": "書",
|
||||
"book": "",
|
||||
"mermaid-diagram": "美人魚圖(Mermaid)",
|
||||
"canvas": "畫布",
|
||||
"web-view": "網頁視圖",
|
||||
|
||||
5
apps/client/src/types-assets.d.ts
vendored
5
apps/client/src/types-assets.d.ts
vendored
@@ -3,6 +3,11 @@ declare module "*.png" {
|
||||
export default path;
|
||||
}
|
||||
|
||||
declare module "*.json" {
|
||||
var content: any;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module "*?url" {
|
||||
var path: string;
|
||||
export default path;
|
||||
|
||||
@@ -78,7 +78,7 @@ const TPL = /*html*/`
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
|
||||
<h5 class="attr-detail-title">${t("attribute_detail.attr_detail_title")}</h5>
|
||||
|
||||
<span class="bx bx-x close-attr-detail-button tn-tool-button" title="${t("attribute_detail.close_button_title")}"></span>
|
||||
@@ -142,6 +142,7 @@ const TPL = /*html*/`
|
||||
<option value="datetime">${t("attribute_detail.date_time")}</option>
|
||||
<option value="time">${t("attribute_detail.time")}</option>
|
||||
<option value="url">${t("attribute_detail.url")}</option>
|
||||
<option value="color">${t("attribute_detail.color_type")}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -295,6 +296,8 @@ interface AttributeDetailOpts {
|
||||
x: number;
|
||||
y: number;
|
||||
focus?: "name";
|
||||
parent?: HTMLElement;
|
||||
hideMultiplicity?: boolean;
|
||||
}
|
||||
|
||||
interface SearchRelatedResponse {
|
||||
@@ -477,7 +480,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
});
|
||||
}
|
||||
|
||||
async showAttributeDetail({ allAttributes, attribute, isOwned, x, y, focus }: AttributeDetailOpts) {
|
||||
async showAttributeDetail({ allAttributes, attribute, isOwned, x, y, focus, hideMultiplicity }: AttributeDetailOpts) {
|
||||
if (!attribute) {
|
||||
this.hide();
|
||||
|
||||
@@ -528,7 +531,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$rowPromotedAlias.toggle(!!definition.isPromoted);
|
||||
this.$inputPromotedAlias.val(definition.promotedAlias || "").attr("disabled", disabledFn);
|
||||
|
||||
this.$rowMultiplicity.toggle(["label-definition", "relation-definition"].includes(this.attrType || ""));
|
||||
this.$rowMultiplicity.toggle(["label-definition", "relation-definition"].includes(this.attrType || "") && !hideMultiplicity);
|
||||
this.$inputMultiplicity.val(definition.multiplicity || "").attr("disabled", disabledFn);
|
||||
|
||||
this.$rowLabelType.toggle(this.attrType === "label-definition");
|
||||
@@ -560,19 +563,22 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.toggleInt(true);
|
||||
|
||||
const offset = this.parent?.$widget.offset() || { top: 0, left: 0 };
|
||||
const offset = this.parent?.$widget?.offset() || { top: 0, left: 0 };
|
||||
const detPosition = this.getDetailPosition(x, offset);
|
||||
const outerHeight = this.$widget.outerHeight();
|
||||
const height = $(window).height();
|
||||
|
||||
if (detPosition && outerHeight && height) {
|
||||
this.$widget
|
||||
.css("left", detPosition.left)
|
||||
.css("right", detPosition.right)
|
||||
.css("top", y - offset.top + 70)
|
||||
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
|
||||
if (!detPosition || !outerHeight || !height) {
|
||||
console.warn("Can't position popup, is it attached?");
|
||||
return;
|
||||
}
|
||||
|
||||
this.$widget
|
||||
.css("left", detPosition.left)
|
||||
.css("right", detPosition.right)
|
||||
.css("top", y - offset.top + 70)
|
||||
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
|
||||
|
||||
if (focus === "name") {
|
||||
this.$inputName.trigger("focus").trigger("select");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import noteAutocompleteService, { type Suggestion } from "../../services/note_au
|
||||
import server from "../../services/server.js";
|
||||
import contextMenuService from "../../menus/context_menu.js";
|
||||
import attributeParser, { type Attribute } from "../../services/attribute_parser.js";
|
||||
import { AttributeEditor, type EditorConfig, type Element, type MentionFeed, type Node, type Position } from "@triliumnext/ckeditor5";
|
||||
import { AttributeEditor, type EditorConfig, type ModelElement, type MentionFeed, type ModelNode, type ModelPosition } from "@triliumnext/ckeditor5";
|
||||
import froca from "../../services/froca.js";
|
||||
import attributeRenderer from "../../services/attribute_renderer.js";
|
||||
import noteCreateService from "../../services/note_create.js";
|
||||
@@ -417,16 +417,16 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
|
||||
this.$editor.tooltip("show");
|
||||
}
|
||||
|
||||
getClickIndex(pos: Position) {
|
||||
getClickIndex(pos: ModelPosition) {
|
||||
let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0);
|
||||
|
||||
let curNode: Node | Text | Element | null = pos.textNode;
|
||||
let curNode: ModelNode | Text | ModelElement | null = pos.textNode;
|
||||
|
||||
while (curNode?.previousSibling) {
|
||||
curNode = curNode.previousSibling;
|
||||
|
||||
if ((curNode as Element).name === "reference") {
|
||||
clickIndex += (curNode.getAttribute("notePath") as string).length + 1;
|
||||
if ((curNode as ModelElement).name === "reference") {
|
||||
clickIndex += (curNode.getAttribute("href") as string).length + 1;
|
||||
} else if ("data" in curNode) {
|
||||
clickIndex += (curNode.data as string).length;
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
|
||||
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book", "mindMap"].includes(note.type));
|
||||
|
||||
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
|
||||
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type));
|
||||
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type));
|
||||
|
||||
const canPrint = ["text", "code"].includes(note.type);
|
||||
this.toggleDisabled(this.$printActiveNoteButton, canPrint);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { default as Component, TypedComponent } from "../../components/component.js";
|
||||
import BasicWidget, { TypedBasicWidget } from "../basic_widget.js";
|
||||
import type { TypedComponent } from "../../components/component.js";
|
||||
import { TypedBasicWidget } from "../basic_widget.js";
|
||||
|
||||
export default class Container<T extends TypedComponent<any>> extends TypedBasicWidget<T> {
|
||||
doRender() {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type Component from "../../components/component.js";
|
||||
|
||||
export default class LeftPaneContainer extends FlexContainer<Component> {
|
||||
private currentLeftPaneVisible: boolean;
|
||||
|
||||
|
||||
constructor() {
|
||||
super("column");
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class LeftPaneContainer extends FlexContainer<Component> {
|
||||
this.currentLeftPaneVisible = leftPaneVisible ?? !this.currentLeftPaneVisible;
|
||||
const visible = this.isEnabled();
|
||||
this.toggleInt(visible);
|
||||
this.parent?.$widget.toggleClass("left-pane-hidden", !visible);
|
||||
|
||||
if (visible) {
|
||||
this.triggerEvent("focusTree", {});
|
||||
|
||||
@@ -154,13 +154,21 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
||||
this.$noteTypeDropdown.append($('<h6 class="dropdown-header">').append(t("note_type_chooser.templates")));
|
||||
} else {
|
||||
const commandItem = noteType as MenuCommandItem<CommandNames>;
|
||||
this.$noteTypeDropdown.append(
|
||||
$('<a class="dropdown-item" tabindex="0">')
|
||||
.attr("data-note-type", commandItem.type || "")
|
||||
.attr("data-template-note-id", commandItem.templateNoteId || "")
|
||||
.append($("<span>").addClass(commandItem.uiIcon || ""))
|
||||
.append(` ${noteType.title}`)
|
||||
);
|
||||
const listItem = $('<a class="dropdown-item" tabindex="0">')
|
||||
.attr("data-note-type", commandItem.type || "")
|
||||
.attr("data-template-note-id", commandItem.templateNoteId || "")
|
||||
.append($("<span>").addClass(commandItem.uiIcon || ""))
|
||||
.append(` ${noteType.title}`);
|
||||
|
||||
if (commandItem.badges) {
|
||||
for (let badge of commandItem.badges) {
|
||||
listItem.append($(`<span class="badge">`)
|
||||
.addClass(badge.className || "")
|
||||
.text(badge.title));
|
||||
}
|
||||
}
|
||||
|
||||
this.$noteTypeDropdown.append(listItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
161
apps/client/src/widgets/dialogs/popup_editor.ts
Normal file
161
apps/client/src/widgets/dialogs/popup_editor.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { EventNames, EventData } from "../../components/app_context.js";
|
||||
import NoteContext from "../../components/note_context.js";
|
||||
import { openDialog } from "../../services/dialog.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import Container from "../containers/container.js";
|
||||
import TypeWidget from "../type_widgets/type_widget.js";
|
||||
|
||||
const TPL = /*html*/`\
|
||||
<div class="popup-editor-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<style>
|
||||
body.desktop .modal.popup-editor-dialog .modal-dialog {
|
||||
max-width: 75vw;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-header .modal-title {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-body {
|
||||
padding: 0;
|
||||
height: 75vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail-editable-text {
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .title-row,
|
||||
.modal.popup-editor-dialog .modal-title,
|
||||
.modal.popup-editor-dialog .note-icon-widget {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-icon-widget {
|
||||
width: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-icon-widget button.note-icon,
|
||||
.modal.popup-editor-dialog .note-title-widget input.note-title {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .classic-toolbar-widget {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--modal-background-color);
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail-file {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">
|
||||
<!-- This is where the first child will be injected -->
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- This is where all but the first child will be injected. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class PopupEditorDialog extends Container<BasicWidget> {
|
||||
|
||||
private noteContext: NoteContext;
|
||||
private $modalHeader!: JQuery<HTMLElement>;
|
||||
private $modalBody!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.noteContext = new NoteContext("_popup-editor");
|
||||
}
|
||||
|
||||
doRender() {
|
||||
// This will populate this.$widget with the content of the children.
|
||||
super.doRender();
|
||||
|
||||
// Now we wrap it in the modal.
|
||||
const $newWidget = $(TPL);
|
||||
this.$modalHeader = $newWidget.find(".modal-title");
|
||||
this.$modalBody = $newWidget.find(".modal-body");
|
||||
|
||||
const children = this.$widget.children();
|
||||
this.$modalHeader.append(children[0]);
|
||||
this.$modalBody.append(children.slice(1));
|
||||
this.$widget = $newWidget;
|
||||
this.setVisibility(false);
|
||||
}
|
||||
|
||||
async openInPopupEvent({ noteIdOrPath }: EventData<"openInPopup">) {
|
||||
const $dialog = await openDialog(this.$widget, false, {
|
||||
focus: false
|
||||
});
|
||||
|
||||
await this.noteContext.setNote(noteIdOrPath, {
|
||||
viewScope: {
|
||||
readOnlyTemporarilyDisabled: true
|
||||
}
|
||||
});
|
||||
|
||||
const activeEl = document.activeElement;
|
||||
if (activeEl && "blur" in activeEl) {
|
||||
(activeEl as HTMLElement).blur();
|
||||
}
|
||||
|
||||
$dialog.on("shown.bs.modal", async () => {
|
||||
// Reduce the z-index of modals so that ckeditor popups are properly shown on top of it.
|
||||
// The backdrop instance is not shared so it's OK to make a one-off modification.
|
||||
$("body > .modal-backdrop").css("z-index", "998");
|
||||
$dialog.css("z-index", "999");
|
||||
|
||||
await this.handleEventInChildren("activeContextChanged", { noteContext: this.noteContext });
|
||||
this.setVisibility(true);
|
||||
await this.handleEventInChildren("focusOnDetail", { ntxId: this.noteContext.ntxId });
|
||||
});
|
||||
$dialog.on("hidden.bs.modal", () => {
|
||||
const $typeWidgetEl = $dialog.find(".note-detail-printable");
|
||||
if ($typeWidgetEl.length) {
|
||||
const typeWidget = glob.getComponentByEl($typeWidgetEl[0]) as TypeWidget;
|
||||
typeWidget.cleanup();
|
||||
}
|
||||
|
||||
this.setVisibility(false);
|
||||
});
|
||||
}
|
||||
|
||||
setVisibility(visible: boolean) {
|
||||
const $bodyItems = this.$modalBody.find("> div");
|
||||
if (visible) {
|
||||
$bodyItems.fadeIn();
|
||||
this.$modalHeader.children().show();
|
||||
} else {
|
||||
$bodyItems.hide();
|
||||
this.$modalHeader.children().hide();
|
||||
}
|
||||
}
|
||||
|
||||
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
|
||||
// Avoid events related to the current tab interfere with our popup.
|
||||
if (["noteSwitched", "noteSwitchedAndActivated"].includes(name)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return super.handleEventInChildren(name, data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,7 +23,9 @@ const TPL = /*html*/`\
|
||||
export default class GeoMapButtons extends NoteContextAwareWidget {
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.note?.type === "geoMap";
|
||||
return super.isEnabled()
|
||||
&& this.note?.getLabelValue("viewType") === "geoMap"
|
||||
&& !this.note.hasLabel("readOnly");
|
||||
}
|
||||
|
||||
doRender() {
|
||||
|
||||
@@ -17,7 +17,6 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
contentWidget: null,
|
||||
doc: null,
|
||||
file: null,
|
||||
geoMap: "81SGnPGMk7Xc",
|
||||
image: null,
|
||||
launcher: null,
|
||||
mermaid: null,
|
||||
@@ -32,9 +31,12 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
};
|
||||
|
||||
export const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
list: null,
|
||||
grid: null,
|
||||
calendar: "xWbu3jpNWapp"
|
||||
list: "mULW0Q3VojwY",
|
||||
grid: "8QqnMzx393bx",
|
||||
calendar: "xWbu3jpNWapp",
|
||||
table: "2FvYrpmOXm29",
|
||||
geoMap: "81SGnPGMk7Xc",
|
||||
board: "CtBQqbwXDx1w"
|
||||
};
|
||||
|
||||
export default class ContextualHelpButton extends NoteContextAwareWidget {
|
||||
|
||||
@@ -39,10 +39,20 @@ export default class ToggleReadOnlyButton extends OnClickButtonWidget {
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled()
|
||||
&& this.note?.type === "mermaid"
|
||||
&& this.note?.isContentAvailable()
|
||||
&& this.noteContext?.viewScope?.viewMode === "default";
|
||||
if (!super.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this?.note?.isContentAvailable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.noteContext?.viewScope?.viewMode !== "default") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.note.type === "mermaid" ||
|
||||
(this.note.getLabelValue("viewType") === "geoMap");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { Map } from "leaflet";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
|
||||
const TPL = /*html*/`\
|
||||
<div class="geo-map-widget">
|
||||
<style>
|
||||
.note-detail-geo-map,
|
||||
.geo-map-widget,
|
||||
.geo-map-container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
z-index: 900;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="geo-map-container"></div>
|
||||
</div>`;
|
||||
|
||||
export type Leaflet = typeof L;
|
||||
export type InitCallback = (L: Leaflet) => void;
|
||||
|
||||
export default class GeoMapWidget extends NoteContextAwareWidget {
|
||||
|
||||
map?: Map;
|
||||
$container!: JQuery<HTMLElement>;
|
||||
private initCallback?: InitCallback;
|
||||
|
||||
constructor(widgetMode: "type", initCallback?: InitCallback) {
|
||||
super();
|
||||
this.initCallback = initCallback;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$container = this.$widget.find(".geo-map-container");
|
||||
|
||||
const map = L.map(this.$container[0], {
|
||||
worldCopyJump: true
|
||||
});
|
||||
|
||||
this.map = map;
|
||||
if (this.initCallback) {
|
||||
this.initCallback(L);
|
||||
}
|
||||
|
||||
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
detectRetina: true
|
||||
}).addTo(map);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
|
||||
import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
|
||||
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
|
||||
import MindMapWidget from "./type_widgets/mind_map.js";
|
||||
import GeoMapTypeWidget from "./type_widgets/geo_map.js";
|
||||
import utils from "../services/utils.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import type TypeWidget from "./type_widgets/type_widget.js";
|
||||
@@ -71,7 +70,6 @@ const typeWidgetClasses = {
|
||||
attachmentDetail: AttachmentDetailTypeWidget,
|
||||
attachmentList: AttachmentListTypeWidget,
|
||||
mindMap: MindMapWidget,
|
||||
geoMap: GeoMapTypeWidget,
|
||||
aiChat: AiChatTypeWidget,
|
||||
|
||||
// Split type editors
|
||||
@@ -197,7 +195,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
const isBackendNote = this.noteContext?.noteId === "_backendLog";
|
||||
const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
|
||||
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap", "mermaid"].includes(this.type ?? "");
|
||||
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "mermaid", "file"].includes(this.type ?? "");
|
||||
const isFullHeight = (!this.noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|
||||
|| this.noteContext?.viewScope?.viewMode === "attachments"
|
||||
|| isBackendNote;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import NoteListRenderer from "../services/note_list_renderer.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { CommandListener, CommandListenerData, EventData } from "../components/app_context.js";
|
||||
import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData, EventNames } from "../components/app_context.js";
|
||||
import type ViewMode from "./view_widgets/view_mode.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
@@ -36,10 +36,31 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
private isIntersecting?: boolean;
|
||||
private noteIdRefreshed?: string;
|
||||
private shownNoteId?: string | null;
|
||||
private viewMode?: ViewMode | null;
|
||||
private viewMode?: ViewMode<any> | null;
|
||||
private displayOnlyCollections: boolean;
|
||||
|
||||
/**
|
||||
* @param displayOnlyCollections if set to `true` then only collection-type views are displayed such as geo-map and the calendar. The original book types grid and list will be ignored.
|
||||
*/
|
||||
constructor(displayOnlyCollections: boolean) {
|
||||
super();
|
||||
|
||||
this.displayOnlyCollections = displayOnlyCollections;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.noteContext?.hasNoteList();
|
||||
if (!super.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.displayOnlyCollections && this.note?.type !== "book") {
|
||||
const viewType = this.note?.getLabelValue("viewType");
|
||||
if (!viewType || ["grid", "list"].includes(viewType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return this.noteContext?.hasNoteList();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
@@ -76,7 +97,11 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async renderNoteList(note: FNote) {
|
||||
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
|
||||
const noteListRenderer = new NoteListRenderer({
|
||||
$parent: this.$content,
|
||||
parentNote: note,
|
||||
parentNotePath: this.notePath
|
||||
});
|
||||
this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight);
|
||||
await noteListRenderer.renderList();
|
||||
this.viewMode = noteListRenderer.viewMode;
|
||||
@@ -120,12 +145,6 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
this.refresh();
|
||||
this.checkRenderStatus();
|
||||
}
|
||||
|
||||
// Inform the view mode of changes and refresh if needed.
|
||||
if (this.viewMode && this.viewMode.onEntitiesReloaded(e)) {
|
||||
this.refresh();
|
||||
this.checkRenderStatus();
|
||||
}
|
||||
}
|
||||
|
||||
buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) {
|
||||
@@ -134,4 +153,26 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
triggerCommand<K extends CommandNames>(name: K, data?: CommandMappings[K]): Promise<unknown> | undefined | null {
|
||||
// Pass the commands to the view mode, which is not actually attached to the hierarchy.
|
||||
if (this.viewMode?.triggerCommand(name, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return super.triggerCommand(name, data);
|
||||
}
|
||||
|
||||
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
|
||||
super.handleEventInChildren(name, data);
|
||||
|
||||
if (this.viewMode) {
|
||||
const ret = this.viewMode.handleEvent(name, data);
|
||||
if (ret) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -324,7 +324,13 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
const mapRootNoteId = this.getMapRootNoteId();
|
||||
const data = await this.loadNotesAndRelations(mapRootNoteId);
|
||||
|
||||
const labelValues = (name: string) => this.note?.getLabels(name).map(l => l.value) ?? [];
|
||||
|
||||
const excludeRelations = labelValues("mapExcludeRelation");
|
||||
const includeRelations = labelValues("mapIncludeRelation");
|
||||
|
||||
const data = await this.loadNotesAndRelations(mapRootNoteId, excludeRelations, includeRelations);
|
||||
|
||||
const nodeLinkRatio = data.nodes.length / data.links.length;
|
||||
const magnifiedRatio = Math.pow(nodeLinkRatio, 1.5);
|
||||
@@ -473,8 +479,10 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
async loadNotesAndRelations(mapRootNoteId: string): Promise<NotesAndRelationsData> {
|
||||
const resp = await server.post<PostNotesMapResponse>(`note-map/${mapRootNoteId}/${this.mapType}`);
|
||||
async loadNotesAndRelations(mapRootNoteId: string, excludeRelations: string[], includeRelations: string[]): Promise<NotesAndRelationsData> {
|
||||
const resp = await server.post<PostNotesMapResponse>(`note-map/${mapRootNoteId}/${this.mapType}`, {
|
||||
excludeRelations, includeRelations
|
||||
});
|
||||
|
||||
this.calculateNodeSizes(resp);
|
||||
|
||||
|
||||
@@ -186,6 +186,15 @@ interface RefreshContext {
|
||||
noteIdsToReload: Set<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The information contained within a drag event.
|
||||
*/
|
||||
export interface DragData {
|
||||
noteId: string;
|
||||
branchId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
private $tree!: JQuery<HTMLElement>;
|
||||
private $treeActions!: JQuery<HTMLElement>;
|
||||
@@ -231,15 +240,21 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
this.$tree.on("mousedown", ".fancytree-title", (e) => {
|
||||
if (e.which === 2) {
|
||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||
|
||||
const notePath = treeService.getNotePath(node);
|
||||
|
||||
if (notePath) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
||||
activate: e.shiftKey ? true : false
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
this.$tree.on("mouseup", ".fancytree-title", (e) => {
|
||||
// Prevent middle click from pasting in the editor.
|
||||
if (e.which === 2) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -698,7 +713,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
});
|
||||
} else {
|
||||
this.$tree.on("contextmenu", ".fancytree-node", (e) => {
|
||||
this.showContextMenu(e);
|
||||
if (!utils.isCtrlKey(e)) {
|
||||
this.showContextMenu(e);
|
||||
} else {
|
||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||
const notePath = treeService.getNotePath(node);
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
||||
}
|
||||
return false; // blocks default browser right click menu
|
||||
});
|
||||
|
||||
|
||||
@@ -1,59 +1,15 @@
|
||||
import server from "../services/server.js";
|
||||
import { Dropdown } from "bootstrap";
|
||||
import { NOTE_TYPES } from "../services/note_types.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import mimeTypesService from "../services/mime_types.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import server from "../services/server.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
import { Dropdown } from "bootstrap";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
|
||||
interface NoteTypeMapping {
|
||||
type: NoteType;
|
||||
mime?: string;
|
||||
title: string;
|
||||
isBeta?: boolean;
|
||||
selectable: boolean;
|
||||
}
|
||||
|
||||
const NOTE_TYPES: NoteTypeMapping[] = [
|
||||
// The suggested note type ordering method: insert the item into the corresponding group,
|
||||
// then ensure the items within the group are ordered alphabetically.
|
||||
|
||||
// The default note type (always the first item)
|
||||
{ type: "text", mime: "text/html", title: t("note_types.text"), selectable: true },
|
||||
|
||||
// Text notes group
|
||||
{ type: "book", mime: "", title: t("note_types.book"), selectable: true },
|
||||
|
||||
// Graphic notes
|
||||
{ type: "canvas", mime: "application/json", title: t("note_types.canvas"), selectable: true },
|
||||
{ type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), selectable: true },
|
||||
|
||||
// Map notes
|
||||
{ type: "geoMap", mime: "application/json", title: t("note_types.geo-map"), isBeta: true, selectable: true },
|
||||
{ type: "mindMap", mime: "application/json", title: t("note_types.mind-map"), selectable: true },
|
||||
{ type: "relationMap", mime: "application/json", title: t("note_types.relation-map"), selectable: true },
|
||||
|
||||
// Misc note types
|
||||
{ type: "render", mime: "", title: t("note_types.render-note"), selectable: true },
|
||||
{ type: "webView", mime: "", title: t("note_types.web-view"), selectable: true },
|
||||
|
||||
// Code notes
|
||||
{ type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true },
|
||||
|
||||
// Reserved types (cannot be created by the user)
|
||||
{ type: "contentWidget", mime: "", title: t("note_types.widget"), selectable: false },
|
||||
{ type: "doc", mime: "", title: t("note_types.doc"), selectable: false },
|
||||
{ type: "file", title: t("note_types.file"), selectable: false },
|
||||
{ type: "image", title: t("note_types.image"), selectable: false },
|
||||
{ type: "launcher", mime: "", title: t("note_types.launcher"), selectable: false },
|
||||
{ type: "noteMap", mime: "", title: t("note_types.note-map"), selectable: false },
|
||||
{ type: "search", title: t("note_types.saved-search"), selectable: false },
|
||||
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), selectable: false }
|
||||
];
|
||||
|
||||
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => !nt.selectable).map((nt) => nt.type);
|
||||
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type);
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="dropdown note-type-widget">
|
||||
@@ -63,13 +19,6 @@ const TPL = /*html*/`
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.note-type-dropdown .badge {
|
||||
margin-left: 8px;
|
||||
background: var(--accented-background-color);
|
||||
font-weight: normal;
|
||||
color: var(--menu-text-color);
|
||||
}
|
||||
</style>
|
||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button note-type-button">
|
||||
<span class="note-type-desc"></span>
|
||||
@@ -116,10 +65,15 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const noteType of NOTE_TYPES.filter((nt) => nt.selectable)) {
|
||||
for (const noteType of NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static)) {
|
||||
let $typeLink: JQuery<HTMLElement>;
|
||||
|
||||
const $title = $("<span>").text(noteType.title);
|
||||
|
||||
if (noteType.isNew) {
|
||||
$title.append($(`<span class="badge new-note-type-badge">`).text(t("note_types.new-feature")));
|
||||
}
|
||||
|
||||
if (noteType.isBeta) {
|
||||
$title.append($(`<span class="badge">`).text(t("note_types.beta-feature")));
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
}
|
||||
|
||||
#isFullWidthNote(note: FNote) {
|
||||
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap", "geoMap"].includes(note.type)) {
|
||||
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,18 @@ import attributeService from "../../services/attributes.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import { bookPropertiesConfig, BookProperty } from "./book_properties_config.js";
|
||||
import attributes from "../../services/attributes.js";
|
||||
import type { ViewTypeOptions } from "../../services/note_list_renderer.js";
|
||||
|
||||
const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||
grid: t("book_properties.grid"),
|
||||
list: t("book_properties.list"),
|
||||
calendar: t("book_properties.calendar"),
|
||||
table: t("book_properties.table"),
|
||||
geoMap: t("book_properties.geo-map"),
|
||||
board: t("book_properties.board")
|
||||
};
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="book-properties-widget">
|
||||
@@ -15,42 +27,56 @@ const TPL = /*html*/`
|
||||
.book-properties-widget > * {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.book-properties-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.book-properties-container > div {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.book-properties-container > .type-number > label {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.book-properties-container input[type="checkbox"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.book-properties-container label {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="display: flex; align-items: baseline">
|
||||
<span style="white-space: nowrap">${t("book_properties.view_type")}: </span>
|
||||
|
||||
<select class="view-type-select form-select form-select-sm">
|
||||
<option value="grid">${t("book_properties.grid")}</option>
|
||||
<option value="list">${t("book_properties.list")}</option>
|
||||
<option value="calendar">${t("book_properties.calendar")}</option>
|
||||
${Object.entries(VIEW_TYPE_MAPPINGS)
|
||||
.filter(([type]) => type !== "raster")
|
||||
.map(([type, label]) => `
|
||||
<option value="${type}">${label}</option>
|
||||
`).join("")}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
class="collapse-all-button btn btn-sm"
|
||||
title="${t("book_properties.collapse_all_notes")}">
|
||||
|
||||
<span class="bx bx-layer-minus"></span>
|
||||
|
||||
${t("book_properties.collapse")}
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="expand-children-button btn btn-sm"
|
||||
title="${t("book_properties.expand_all_children")}">
|
||||
<span class="bx bx-move-vertical"></span>
|
||||
|
||||
${t("book_properties.expand")}
|
||||
</button>
|
||||
<div class="book-properties-container">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $viewTypeSelect!: JQuery<HTMLElement>;
|
||||
private $expandChildrenButton!: JQuery<HTMLElement>;
|
||||
private $collapseAllButton!: JQuery<HTMLElement>;
|
||||
private $propertiesContainer!: JQuery<HTMLElement>;
|
||||
private labelsToWatch: string[] = [];
|
||||
|
||||
get name() {
|
||||
return "bookProperties";
|
||||
@@ -67,7 +93,6 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
activate: true,
|
||||
title: t("book_properties.book_properties"),
|
||||
icon: "bx bx-book"
|
||||
};
|
||||
@@ -80,32 +105,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
this.$viewTypeSelect = this.$widget.find(".view-type-select");
|
||||
this.$viewTypeSelect.on("change", () => this.toggleViewType(String(this.$viewTypeSelect.val())));
|
||||
|
||||
this.$expandChildrenButton = this.$widget.find(".expand-children-button");
|
||||
this.$expandChildrenButton.on("click", async () => {
|
||||
if (!this.noteId || !this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.note?.isLabelTruthy("expanded")) {
|
||||
await attributeService.addLabel(this.noteId, "expanded");
|
||||
}
|
||||
|
||||
this.triggerCommand("refreshNoteList", { noteId: this.noteId });
|
||||
});
|
||||
|
||||
this.$collapseAllButton = this.$widget.find(".collapse-all-button");
|
||||
this.$collapseAllButton.on("click", async () => {
|
||||
if (!this.noteId || !this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
// owned is important - we shouldn't remove inherited expanded labels
|
||||
for (const expandedAttr of this.note.getOwnedLabels("expanded")) {
|
||||
await attributeService.removeAttributeById(this.noteId, expandedAttr.attributeId);
|
||||
}
|
||||
|
||||
this.triggerCommand("refreshNoteList", { noteId: this.noteId });
|
||||
});
|
||||
this.$propertiesContainer = this.$widget.find(".book-properties-container");
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
@@ -117,8 +117,15 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$viewTypeSelect.val(viewType);
|
||||
|
||||
this.$expandChildrenButton.toggle(viewType === "list");
|
||||
this.$collapseAllButton.toggle(viewType === "list");
|
||||
this.$propertiesContainer.empty();
|
||||
|
||||
const bookPropertiesData = bookPropertiesConfig[viewType];
|
||||
if (bookPropertiesData) {
|
||||
for (const property of bookPropertiesData.properties) {
|
||||
this.$propertiesContainer.append(this.renderBookProperty(property));
|
||||
this.labelsToWatch.push(property.bindToLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async toggleViewType(type: string) {
|
||||
@@ -126,7 +133,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!["list", "grid", "calendar"].includes(type)) {
|
||||
if (!VIEW_TYPE_MAPPINGS.hasOwnProperty(type)) {
|
||||
throw new Error(t("book_properties.invalid_view_type", { type }));
|
||||
}
|
||||
|
||||
@@ -134,8 +141,122 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name === "viewType")) {
|
||||
if (loadResults.getAttributeRows().find((attr) =>
|
||||
attr.noteId === this.noteId
|
||||
&& (attr.name === "viewType" || this.labelsToWatch.includes(attr.name ?? "")))) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
renderBookProperty(property: BookProperty) {
|
||||
const $container = $("<div>");
|
||||
$container.addClass(`type-${property.type}`);
|
||||
const note = this.note;
|
||||
if (!note) {
|
||||
return $container;
|
||||
}
|
||||
switch (property.type) {
|
||||
case "checkbox":
|
||||
const $label = $("<label>").text(property.label);
|
||||
const $checkbox = $("<input>", {
|
||||
type: "checkbox",
|
||||
class: "form-check-input",
|
||||
});
|
||||
$checkbox.on("change", () => {
|
||||
if ($checkbox.prop("checked")) {
|
||||
attributes.setLabel(note.noteId, property.bindToLabel);
|
||||
} else {
|
||||
attributes.removeOwnedLabelByName(note, property.bindToLabel);
|
||||
}
|
||||
});
|
||||
$checkbox.prop("checked", note.hasOwnedLabel(property.bindToLabel));
|
||||
$label.prepend($checkbox);
|
||||
$container.append($label);
|
||||
break;
|
||||
case "button":
|
||||
const $button = $("<button>", {
|
||||
type: "button",
|
||||
class: "btn btn-sm"
|
||||
}).text(property.label);
|
||||
if (property.title) {
|
||||
$button.attr("title", property.title);
|
||||
}
|
||||
if (property.icon) {
|
||||
$button.prepend($("<span>", { class: property.icon }));
|
||||
}
|
||||
$button.on("click", () => {
|
||||
property.onClick({
|
||||
note,
|
||||
triggerCommand: this.triggerCommand.bind(this)
|
||||
});
|
||||
});
|
||||
$container.append($button);
|
||||
break;
|
||||
case "number":
|
||||
const $numberInput = $("<input>", {
|
||||
type: "number",
|
||||
class: "form-control form-control-sm",
|
||||
value: note.getLabelValue(property.bindToLabel) || "",
|
||||
width: property.width ?? 100,
|
||||
min: property.min ?? 0
|
||||
});
|
||||
$numberInput.on("change", () => {
|
||||
const value = $numberInput.val();
|
||||
if (value === "") {
|
||||
attributes.removeOwnedLabelByName(note, property.bindToLabel);
|
||||
} else {
|
||||
attributes.setLabel(note.noteId, property.bindToLabel, String(value));
|
||||
}
|
||||
});
|
||||
$container.append($("<label>")
|
||||
.text(property.label)
|
||||
.append(" ".repeat(2))
|
||||
.append($numberInput));
|
||||
break;
|
||||
case "combobox":
|
||||
const $select = $("<select>", {
|
||||
class: "form-select form-select-sm"
|
||||
});
|
||||
const actualValue = note.getLabelValue(property.bindToLabel) ?? property.defaultValue ?? "";
|
||||
for (const option of property.options) {
|
||||
if ("items" in option) {
|
||||
const $optGroup = $("<optgroup>", { label: option.name });
|
||||
for (const item of option.items) {
|
||||
buildComboBoxItem(item, actualValue).appendTo($optGroup);
|
||||
}
|
||||
$optGroup.appendTo($select);
|
||||
} else {
|
||||
buildComboBoxItem(option, actualValue).appendTo($select);
|
||||
}
|
||||
}
|
||||
$select.on("change", () => {
|
||||
const value = $select.val();
|
||||
if (value === null || value === "") {
|
||||
attributes.removeOwnedLabelByName(note, property.bindToLabel);
|
||||
} else {
|
||||
attributes.setLabel(note.noteId, property.bindToLabel, String(value));
|
||||
}
|
||||
});
|
||||
$container.append($("<label>")
|
||||
.text(property.label)
|
||||
.append(" ".repeat(2))
|
||||
.append($select));
|
||||
break;
|
||||
}
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function buildComboBoxItem({ value, label }: { value: string, label: string }, actualValue: string) {
|
||||
const $option = $("<option>", {
|
||||
value,
|
||||
text: label
|
||||
});
|
||||
if (actualValue === value) {
|
||||
$option.prop("selected", true);
|
||||
}
|
||||
return $option;
|
||||
}
|
||||
|
||||
169
apps/client/src/widgets/ribbon_widgets/book_properties_config.ts
Normal file
169
apps/client/src/widgets/ribbon_widgets/book_properties_config.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { t } from "i18next";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { ViewTypeOptions } from "../../services/note_list_renderer"
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, type MapLayer } from "../view_widgets/geo_view/map_layer";
|
||||
|
||||
interface BookConfig {
|
||||
properties: BookProperty[];
|
||||
}
|
||||
|
||||
interface CheckBoxProperty {
|
||||
type: "checkbox",
|
||||
label: string;
|
||||
bindToLabel: string
|
||||
}
|
||||
|
||||
interface ButtonProperty {
|
||||
type: "button",
|
||||
label: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
onClick: (context: BookContext) => void;
|
||||
}
|
||||
|
||||
interface NumberProperty {
|
||||
type: "number",
|
||||
label: string;
|
||||
bindToLabel: string;
|
||||
width?: number;
|
||||
min?: number;
|
||||
}
|
||||
|
||||
interface ComboBoxItem {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ComboBoxGroup {
|
||||
name: string;
|
||||
items: ComboBoxItem[];
|
||||
}
|
||||
|
||||
interface ComboBoxProperty {
|
||||
type: "combobox",
|
||||
label: string;
|
||||
bindToLabel: string;
|
||||
/**
|
||||
* The default value is used when the label is not set.
|
||||
*/
|
||||
defaultValue?: string;
|
||||
options: (ComboBoxItem | ComboBoxGroup)[];
|
||||
}
|
||||
|
||||
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty;
|
||||
|
||||
interface BookContext {
|
||||
note: FNote;
|
||||
triggerCommand: NoteContextAwareWidget["triggerCommand"];
|
||||
}
|
||||
|
||||
export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
||||
grid: {
|
||||
properties: []
|
||||
},
|
||||
list: {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties.collapse"),
|
||||
title: t("book_properties.collapse_all_notes"),
|
||||
type: "button",
|
||||
icon: "bx bx-layer-minus",
|
||||
async onClick({ note, triggerCommand }) {
|
||||
const { noteId } = note;
|
||||
|
||||
// owned is important - we shouldn't remove inherited expanded labels
|
||||
for (const expandedAttr of note.getOwnedLabels("expanded")) {
|
||||
await attributes.removeAttributeById(noteId, expandedAttr.attributeId);
|
||||
}
|
||||
|
||||
triggerCommand("refreshNoteList", { noteId: noteId });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("book_properties.expand"),
|
||||
title: t("book_properties.expand_all_children"),
|
||||
type: "button",
|
||||
icon: "bx bx-move-vertical",
|
||||
async onClick({ note, triggerCommand }) {
|
||||
const { noteId } = note;
|
||||
if (!note.isLabelTruthy("expanded")) {
|
||||
await attributes.addLabel(noteId, "expanded");
|
||||
}
|
||||
|
||||
triggerCommand("refreshNoteList", { noteId });
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
calendar: {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties_config.hide-weekends"),
|
||||
type: "checkbox",
|
||||
bindToLabel: "calendar:hideWeekends"
|
||||
},
|
||||
{
|
||||
label: t("book_properties_config.display-week-numbers"),
|
||||
type: "checkbox",
|
||||
bindToLabel: "calendar:weekNumbers"
|
||||
}
|
||||
]
|
||||
},
|
||||
geoMap: {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties_config.map-style"),
|
||||
type: "combobox",
|
||||
bindToLabel: "map:style",
|
||||
defaultValue: DEFAULT_MAP_LAYER_NAME,
|
||||
options: [
|
||||
{
|
||||
name: t("book_properties_config.raster"),
|
||||
items: Object.entries(MAP_LAYERS)
|
||||
.filter(([_, layer]) => layer.type === "raster")
|
||||
.map(buildMapLayer)
|
||||
},
|
||||
{
|
||||
name: t("book_properties_config.vector_light"),
|
||||
items: Object.entries(MAP_LAYERS)
|
||||
.filter(([_, layer]) => layer.type === "vector" && !layer.isDarkTheme)
|
||||
.map(buildMapLayer)
|
||||
},
|
||||
{
|
||||
name: t("book_properties_config.vector_dark"),
|
||||
items: Object.entries(MAP_LAYERS)
|
||||
.filter(([_, layer]) => layer.type === "vector" && layer.isDarkTheme)
|
||||
.map(buildMapLayer)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t("book_properties_config.show-scale"),
|
||||
type: "checkbox",
|
||||
bindToLabel: "map:scale"
|
||||
}
|
||||
]
|
||||
},
|
||||
table: {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties_config.max-nesting-depth"),
|
||||
type: "number",
|
||||
bindToLabel: "maxNestingDepth",
|
||||
width: 65
|
||||
}
|
||||
]
|
||||
},
|
||||
board: {
|
||||
properties: []
|
||||
}
|
||||
};
|
||||
|
||||
function buildMapLayer([ id, layer ]: [ string, MapLayer ]): ComboBoxItem {
|
||||
return {
|
||||
value: id,
|
||||
label: layer.name
|
||||
};
|
||||
}
|
||||
@@ -48,6 +48,18 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget {
|
||||
this.contentSized();
|
||||
}
|
||||
|
||||
isEnabled(): boolean | null | undefined {
|
||||
if (options.get("textNoteEditorType") !== "ckeditor-classic") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.note || this.note.type !== "text") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async getTitle() {
|
||||
return {
|
||||
show: await this.#shouldDisplay(),
|
||||
@@ -58,11 +70,7 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async #shouldDisplay() {
|
||||
if (options.get("textNoteEditorType") !== "ckeditor-classic") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.note || this.note.type !== "text") {
|
||||
if (!this.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,12 +53,56 @@ const TPL = /*html*/`
|
||||
word-break:keep-all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.promoted-attribute-cell input[type="checkbox"] {
|
||||
width: 22px !important;
|
||||
flex-grow: 0;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
/* Restore default apperance */
|
||||
.promoted-attribute-cell input[type="number"],
|
||||
.promoted-attribute-cell input[type="checkbox"] {
|
||||
appearance: auto;
|
||||
}
|
||||
|
||||
.promoted-attribute-cell input[type="color"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: 2px;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
border-radius: 25% !important;
|
||||
}
|
||||
|
||||
.promoted-attribute-cell input[type="color"]::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.promoted-attribute-cell input[type="color"]::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 25%;
|
||||
}
|
||||
|
||||
.promoted-attribute-label-color input[type="hidden"][value=""] + input[type="color"] {
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.promoted-attribute-label-color input[type="hidden"][value=""] + input[type="color"]:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0px;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
transform: rotate(45deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="promoted-attributes-container"></div>
|
||||
@@ -69,11 +113,6 @@ interface AttributeResult {
|
||||
attributeId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This widget is quite special because it's used in the desktop ribbon, but in mobile outside of ribbon.
|
||||
* This works without many issues (apart from autocomplete), but it should be kept in mind when changing things
|
||||
* and testing.
|
||||
*/
|
||||
export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $container!: JQuery<HTMLElement>;
|
||||
@@ -117,7 +156,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
// the order of attributes is important as well
|
||||
ownedAttributes.sort((a, b) => a.position - b.position);
|
||||
|
||||
if (promotedDefAttrs.length === 0) {
|
||||
if (promotedDefAttrs.length === 0 || note.getLabelValue("viewType") === "table") {
|
||||
this.toggleInt(false);
|
||||
return;
|
||||
}
|
||||
@@ -188,6 +227,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
.append($multiplicityCell);
|
||||
|
||||
if (valueAttr.type === "label") {
|
||||
$wrapper.addClass(`promoted-attribute-label-${definition.labelType}`);
|
||||
if (definition.labelType === "text") {
|
||||
$input.prop("type", "text");
|
||||
|
||||
@@ -262,6 +302,35 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
.on("click", () => window.open($input.val() as string, "_blank"));
|
||||
|
||||
$input.after($openButton);
|
||||
} else if (definition.labelType === "color") {
|
||||
const defaultColor = "#ffffff";
|
||||
$input.prop("type", "hidden");
|
||||
$input.val(valueAttr.value ?? "");
|
||||
|
||||
// We insert a separate input since the color input does not support empty value.
|
||||
// This is a workaround to allow clearing the color input.
|
||||
const $colorInput = $("<input>")
|
||||
.prop("type", "color")
|
||||
.prop("value", valueAttr.value || defaultColor)
|
||||
.addClass("form-control promoted-attribute-input")
|
||||
.on("change", e => setValue((e.target as HTMLInputElement).value, e));
|
||||
$input.after($colorInput);
|
||||
|
||||
const $clearButton = $("<span>")
|
||||
.addClass("input-group-text bx bxs-tag-x")
|
||||
.prop("title", t("promoted_attributes.remove_color"))
|
||||
.on("click", e => setValue("", e));
|
||||
|
||||
const setValue = (color: string, event: JQuery.TriggeredEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => {
|
||||
$input.val(color);
|
||||
if (!color) {
|
||||
$colorInput.val(defaultColor);
|
||||
}
|
||||
event.target = $input[0]; // Set the event target to the main input
|
||||
this.promotedAttributeChanged(event);
|
||||
};
|
||||
|
||||
$colorInput.after($clearButton);
|
||||
} else {
|
||||
ws.logError(t("promoted_attributes.unknown_label_type", { type: definition.labelType }));
|
||||
}
|
||||
|
||||
@@ -65,7 +65,11 @@ export default class SearchResultWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds(), true);
|
||||
const noteListRenderer = new NoteListRenderer({
|
||||
$parent: this.$content,
|
||||
parentNote: note,
|
||||
showNotePath: true
|
||||
});
|
||||
await noteListRenderer.renderList();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import appContext, { type CommandNames, type CommandListenerData, type EventData
|
||||
import froca from "../services/froca.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import type NoteContext from "../components/note_context.js";
|
||||
import { setupHorizontalScrollViaWheel } from "./widget_utils.js";
|
||||
|
||||
const isDesktop = utils.isDesktop();
|
||||
|
||||
@@ -386,15 +387,7 @@ export default class TabRowWidget extends BasicWidget {
|
||||
};
|
||||
|
||||
setupScrollEvents() {
|
||||
this.$tabScrollingContainer.on('wheel', (event) => {
|
||||
const wheelEvent = event.originalEvent as WheelEvent;
|
||||
if (utils.isCtrlKey(event) || event.altKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
event.currentTarget.scrollLeft += wheelEvent.deltaY + wheelEvent.deltaX;
|
||||
});
|
||||
setupHorizontalScrollViaWheel(this.$tabScrollingContainer);
|
||||
|
||||
this.$scrollButtonLeft[0].addEventListener('click', () => this.scrollTabContainer(-210));
|
||||
this.$scrollButtonRight[0].addEventListener('click', () => this.scrollTabContainer(210));
|
||||
|
||||
@@ -130,7 +130,8 @@ export default abstract class AbstractSplitTypeWidget extends TypeWidget {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.editorTypeWidget = new EditableCodeTypeWidget();
|
||||
|
||||
this.editorTypeWidget = new EditableCodeTypeWidget(true);
|
||||
this.editorTypeWidget.updateBackgroundColor = () => {};
|
||||
this.editorTypeWidget.isEnabled = () => true;
|
||||
|
||||
@@ -146,6 +147,8 @@ export default abstract class AbstractSplitTypeWidget extends TypeWidget {
|
||||
doRender(): void {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.spacedUpdate.setUpdateInterval(750);
|
||||
|
||||
// Preview pane
|
||||
this.$previewCol = this.$widget.find(".note-detail-split-preview-col");
|
||||
this.$preview = this.$widget.find(".note-detail-split-preview");
|
||||
|
||||
@@ -36,7 +36,21 @@ export default class BookTypeWidget extends TypeWidget {
|
||||
}
|
||||
|
||||
async doRefresh(note: FNote) {
|
||||
this.$helpNoChildren.toggle(!this.note?.hasChildren() && this.note?.getAttributeValue("label", "viewType") !== "calendar");
|
||||
this.$helpNoChildren.toggle(this.shouldDisplayNoChildrenWarning());
|
||||
}
|
||||
|
||||
shouldDisplayNoChildrenWarning() {
|
||||
if (this.note?.hasChildren()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.note?.getAttributeValue("label", "viewType")) {
|
||||
case "list":
|
||||
case "grid":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
|
||||
@@ -166,7 +166,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
||||
onChange: () => this.onChangeHandler(),
|
||||
viewModeEnabled: options.is("databaseReadonly"),
|
||||
zenModeEnabled: false,
|
||||
gridModeEnabled: false,
|
||||
isCollaborating: false,
|
||||
detectScroll: false,
|
||||
handleKeyboardGlobally: false,
|
||||
|
||||
@@ -153,7 +153,8 @@ export default class Canvas {
|
||||
appState: {
|
||||
scrollX: appState.scrollX,
|
||||
scrollY: appState.scrollY,
|
||||
zoom: appState.zoom
|
||||
zoom: appState.zoom,
|
||||
gridModeEnabled: appState.gridModeEnabled
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ async function handleContentUpdate(affectedNoteIds: string[]) {
|
||||
const templateNoteIds = new Set(templateCache.keys());
|
||||
const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds);
|
||||
|
||||
await froca.getNotes(affectedNoteIds);
|
||||
await froca.getNotes(affectedNoteIds, true);
|
||||
|
||||
let fullReloadNeeded = false;
|
||||
for (const affectedTemplateNoteId of affectedTemplateNoteIds) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import RevisionSnapshotsLimitOptions from "./options/other/revision_snapshots_li
|
||||
import NetworkConnectionsOptions from "./options/other/network_connections.js";
|
||||
import HtmlImportTagsOptions from "./options/other/html_import_tags.js";
|
||||
import AdvancedSyncOptions from "./options/advanced/sync.js";
|
||||
import FileSystemSyncOptions from "./options/advanced/file_system_sync.js";
|
||||
import DatabaseIntegrityCheckOptions from "./options/advanced/database_integrity_check.js";
|
||||
import VacuumDatabaseOptions from "./options/advanced/vacuum_database.js";
|
||||
import DatabaseAnonymizationOptions from "./options/advanced/database_anonymization.js";
|
||||
@@ -138,6 +139,7 @@ const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", (typeof NoteContextAw
|
||||
],
|
||||
_optionsAdvanced: [
|
||||
AdvancedSyncOptions,
|
||||
FileSystemSyncOptions,
|
||||
DatabaseIntegrityCheckOptions,
|
||||
DatabaseAnonymizationOptions,
|
||||
VacuumDatabaseOptions
|
||||
|
||||
@@ -16,6 +16,10 @@ const TPL = /*html*/`<div class="note-detail-doc note-detail-printable">
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.note-detail-doc-content code {
|
||||
font-variant: none;
|
||||
}
|
||||
|
||||
.note-detail-doc-content pre:not(.hljs) {
|
||||
background-color: var(--accented-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
|
||||
@@ -28,6 +28,16 @@ const TPL = /*html*/`
|
||||
|
||||
export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
|
||||
|
||||
private debounceUpdate: boolean;
|
||||
|
||||
/**
|
||||
* @param debounceUpdate if true, the update will be debounced to prevent excessive updates. Especially useful if the editor is linked to a live preview.
|
||||
*/
|
||||
constructor(debounceUpdate: boolean = false) {
|
||||
super();
|
||||
this.debounceUpdate = debounceUpdate;
|
||||
}
|
||||
|
||||
static getType() {
|
||||
return "editableCode";
|
||||
}
|
||||
@@ -46,7 +56,13 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
|
||||
return {
|
||||
placeholder: t("editable_code.placeholder"),
|
||||
vimKeybindings: options.is("vimKeymapEnabled"),
|
||||
onContentChanged: () => this.spacedUpdate.scheduleUpdate(),
|
||||
onContentChanged: () => {
|
||||
if (this.debounceUpdate) {
|
||||
this.spacedUpdate.resetUpdateTimer();
|
||||
}
|
||||
|
||||
this.spacedUpdate.scheduleUpdate();
|
||||
},
|
||||
tabIndex: 300
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,13 +178,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
});
|
||||
|
||||
if (isClassicEditor) {
|
||||
let $classicToolbarWidget;
|
||||
if (!utils.isMobile()) {
|
||||
const $parentSplit = this.$widget.parents(".note-split.type-text");
|
||||
$classicToolbarWidget = $parentSplit.find("> .ribbon-container .classic-toolbar-widget");
|
||||
} else {
|
||||
$classicToolbarWidget = $("body").find(".classic-toolbar-widget");
|
||||
}
|
||||
const $classicToolbarWidget = this.findClassicToolbar();
|
||||
|
||||
$classicToolbarWidget.empty();
|
||||
if ($classicToolbarWidget.length) {
|
||||
@@ -271,7 +265,12 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$editor.trigger("focus");
|
||||
const editor = this.watchdog.editor;
|
||||
if (editor) {
|
||||
editor.editing.view.focus();
|
||||
} else {
|
||||
this.$editor.trigger("focus");
|
||||
}
|
||||
}
|
||||
|
||||
scrollToEnd() {
|
||||
@@ -515,6 +514,22 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
}
|
||||
}
|
||||
|
||||
findClassicToolbar(): JQuery<HTMLElement> {
|
||||
if (!utils.isMobile()) {
|
||||
const $parentSplit = this.$widget.parents(".note-split.type-text");
|
||||
|
||||
if ($parentSplit.length) {
|
||||
// The editor is in a normal tab.
|
||||
return $parentSplit.find("> .ribbon-container .classic-toolbar-widget");
|
||||
} else {
|
||||
// The editor is in a popup.
|
||||
return this.$widget.closest(".modal-body").find(".classic-toolbar-widget");
|
||||
}
|
||||
} else {
|
||||
return $("body").find(".classic-toolbar-widget");
|
||||
}
|
||||
}
|
||||
|
||||
buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) {
|
||||
const { TouchBar, buildIcon } = data;
|
||||
const { TouchBarSegmentedControl, TouchBarGroup, TouchBarButton } = TouchBar;
|
||||
|
||||
@@ -3,7 +3,6 @@ import TypeWidget from "./type_widget.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import searchService from "../../services/search.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="note-detail-empty note-detail-printable">
|
||||
|
||||
@@ -22,7 +22,8 @@ const TPL = /*html*/`
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.note-split.full-content-width .note-detail-file[data-preview-type="video"] {
|
||||
.note-detail.full-height .note-detail-file[data-preview-type="pdf"],
|
||||
.note-detail.full-height .note-detail-file[data-preview-type="video"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
import { GPX, Marker, type LatLng, type LeafletMouseEvent } from "leaflet";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import GeoMapWidget, { type InitCallback, type Leaflet } from "../geo_map.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import server from "../../services/server.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import dialogService from "../../services/dialog.js";
|
||||
import type { CommandListenerData, EventData } from "../../components/app_context.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import attributes from "../../services/attributes.js";
|
||||
import openContextMenu from "./geo_map_context_menu.js";
|
||||
import link from "../../services/link.js";
|
||||
import note_tooltip from "../../services/note_tooltip.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
|
||||
import markerIcon from "leaflet/dist/images/marker-icon.png";
|
||||
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
|
||||
import { hasTouchBar } from "../../services/utils.js";
|
||||
|
||||
const TPL = /*html*/`\
|
||||
<div class="note-detail-geo-map note-detail-printable">
|
||||
<style>
|
||||
.leaflet-pane {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.geo-map-container.placing-note {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.geo-map-container .marker-pin {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .icon-shadow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .bx {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 2px;
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .title-label {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.75rem;
|
||||
height: 1rem;
|
||||
color: black;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
|
||||
white-space: no-wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</div>`;
|
||||
|
||||
const LOCATION_ATTRIBUTE = "geolocation";
|
||||
const CHILD_NOTE_ICON = "bx bx-pin";
|
||||
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
|
||||
const DEFAULT_ZOOM = 2;
|
||||
|
||||
interface MapData {
|
||||
view?: {
|
||||
center?: LatLng | [number, number];
|
||||
zoom?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Deduplicate
|
||||
interface CreateChildResponse {
|
||||
note: {
|
||||
noteId: string;
|
||||
};
|
||||
}
|
||||
|
||||
enum State {
|
||||
Normal,
|
||||
NewNote
|
||||
}
|
||||
|
||||
export default class GeoMapTypeWidget extends TypeWidget {
|
||||
|
||||
private geoMapWidget: GeoMapWidget;
|
||||
private _state: State;
|
||||
private L!: Leaflet;
|
||||
private currentMarkerData: Record<string, Marker>;
|
||||
private currentTrackData: Record<string, GPX>;
|
||||
private gpxLoaded?: boolean;
|
||||
private ignoreNextZoomEvent?: boolean;
|
||||
|
||||
static getType() {
|
||||
return "geoMap";
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.geoMapWidget = new GeoMapWidget("type", (L: Leaflet) => this.#onMapInitialized(L));
|
||||
this.currentMarkerData = {};
|
||||
this.currentTrackData = {};
|
||||
this._state = State.Normal;
|
||||
|
||||
this.child(this.geoMapWidget);
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
this.$widget = $(TPL);
|
||||
this.$widget.append(this.geoMapWidget.render());
|
||||
}
|
||||
|
||||
async #onMapInitialized(L: Leaflet) {
|
||||
this.L = L;
|
||||
const map = this.geoMapWidget.map;
|
||||
if (!map) {
|
||||
throw new Error(t("geo-map.unable-to-load-map"));
|
||||
}
|
||||
|
||||
this.#restoreViewportAndZoom();
|
||||
|
||||
// Restore markers.
|
||||
await this.#reloadMarkers();
|
||||
|
||||
// This fixes an issue with the map appearing cut off at the beginning, due to the container not being properly attached
|
||||
setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
}, 100);
|
||||
|
||||
const updateFn = () => this.spacedUpdate.scheduleUpdate();
|
||||
map.on("moveend", updateFn);
|
||||
map.on("zoomend", updateFn);
|
||||
map.on("click", (e) => this.#onMapClicked(e));
|
||||
|
||||
if (hasTouchBar) {
|
||||
map.on("zoom", () => {
|
||||
if (!this.ignoreNextZoomEvent) {
|
||||
this.triggerCommand("refreshTouchBar");
|
||||
}
|
||||
|
||||
this.ignoreNextZoomEvent = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async #restoreViewportAndZoom() {
|
||||
const map = this.geoMapWidget.map;
|
||||
if (!map || !this.note) {
|
||||
return;
|
||||
}
|
||||
const blob = await this.note.getBlob();
|
||||
|
||||
let parsedContent: MapData = {};
|
||||
if (blob && blob.content) {
|
||||
parsedContent = JSON.parse(blob.content);
|
||||
}
|
||||
|
||||
// Restore viewport position & zoom
|
||||
const center = parsedContent.view?.center ?? DEFAULT_COORDINATES;
|
||||
const zoom = parsedContent.view?.zoom ?? DEFAULT_ZOOM;
|
||||
map.setView(center, zoom);
|
||||
}
|
||||
|
||||
async #reloadMarkers() {
|
||||
if (!this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete all existing markers
|
||||
for (const marker of Object.values(this.currentMarkerData)) {
|
||||
marker.remove();
|
||||
}
|
||||
|
||||
// Delete all existing tracks
|
||||
for (const track of Object.values(this.currentTrackData)) {
|
||||
track.remove();
|
||||
}
|
||||
|
||||
// Add the new markers.
|
||||
this.currentMarkerData = {};
|
||||
const childNotes = await this.note.getChildNotes();
|
||||
for (const childNote of childNotes) {
|
||||
if (childNote.mime === "application/gpx+xml") {
|
||||
this.#processNoteWithGpxTrack(childNote);
|
||||
continue;
|
||||
}
|
||||
|
||||
const latLng = childNote.getAttributeValue("label", LOCATION_ATTRIBUTE);
|
||||
if (latLng) {
|
||||
this.#processNoteWithMarker(childNote, latLng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #processNoteWithGpxTrack(note: FNote) {
|
||||
if (!this.L || !this.geoMapWidget.map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.gpxLoaded) {
|
||||
await import("leaflet-gpx");
|
||||
this.gpxLoaded = true;
|
||||
}
|
||||
|
||||
const xmlResponse = await server.get<string | Uint8Array>(`notes/${note.noteId}/open`, undefined, true);
|
||||
let stringResponse: string;
|
||||
if (xmlResponse instanceof Uint8Array) {
|
||||
stringResponse = new TextDecoder().decode(xmlResponse);
|
||||
} else {
|
||||
stringResponse = xmlResponse;
|
||||
}
|
||||
|
||||
const track = new this.L.GPX(stringResponse, {
|
||||
markers: {
|
||||
startIcon: this.#buildIcon(note.getIcon(), note.getColorClass(), note.title),
|
||||
endIcon: this.#buildIcon("bxs-flag-checkered"),
|
||||
wptIcons: {
|
||||
"": this.#buildIcon("bx bx-pin")
|
||||
}
|
||||
},
|
||||
polyline_options: {
|
||||
color: note.getLabelValue("color") ?? "blue"
|
||||
}
|
||||
});
|
||||
track.addTo(this.geoMapWidget.map);
|
||||
this.currentTrackData[note.noteId] = track;
|
||||
}
|
||||
|
||||
#processNoteWithMarker(note: FNote, latLng: string) {
|
||||
const map = this.geoMapWidget.map;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lat, lng] = latLng.split(",", 2).map((el) => parseFloat(el));
|
||||
const L = this.L;
|
||||
const icon = this.#buildIcon(note.getIcon(), note.getColorClass(), note.title);
|
||||
|
||||
const marker = L.marker(L.latLng(lat, lng), {
|
||||
icon,
|
||||
draggable: true,
|
||||
autoPan: true,
|
||||
autoPanSpeed: 5
|
||||
})
|
||||
.addTo(map)
|
||||
.on("moveend", (e) => {
|
||||
this.moveMarker(note.noteId, (e.target as Marker).getLatLng());
|
||||
});
|
||||
marker.on("mousedown", ({ originalEvent }) => {
|
||||
// Middle click to open in new tab
|
||||
if (originalEvent.button === 1) {
|
||||
const hoistedNoteId = this.hoistedNoteId;
|
||||
//@ts-ignore, fix once tab manager is ported.
|
||||
appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
marker.on("contextmenu", (e) => {
|
||||
openContextMenu(note.noteId, e.originalEvent);
|
||||
});
|
||||
|
||||
const el = marker.getElement();
|
||||
if (el) {
|
||||
const $el = $(el);
|
||||
$el.attr("data-href", `#${note.noteId}`);
|
||||
note_tooltip.setupElementTooltip($($el));
|
||||
}
|
||||
|
||||
this.currentMarkerData[note.noteId] = marker;
|
||||
}
|
||||
|
||||
#buildIcon(bxIconClass: string, colorClass?: string, title?: string) {
|
||||
return this.L.divIcon({
|
||||
html: /*html*/`\
|
||||
<img class="icon" src="${markerIcon}" />
|
||||
<img class="icon-shadow" src="${markerIconShadow}" />
|
||||
<span class="bx ${bxIconClass} ${colorClass ?? ""}"></span>
|
||||
<span class="title-label">${title ?? ""}</span>`,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41]
|
||||
});
|
||||
}
|
||||
|
||||
#changeState(newState: State) {
|
||||
this._state = newState;
|
||||
this.geoMapWidget.$container.toggleClass("placing-note", newState === State.NewNote);
|
||||
if (hasTouchBar) {
|
||||
this.triggerCommand("refreshTouchBar");
|
||||
}
|
||||
}
|
||||
|
||||
async #onMapClicked(e: LeafletMouseEvent) {
|
||||
if (this._state !== State.NewNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.closePersistent("geo-new-note");
|
||||
const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
|
||||
|
||||
if (title?.trim()) {
|
||||
const { note } = await server.post<CreateChildResponse>(`notes/${this.noteId}/children?target=into`, {
|
||||
title,
|
||||
content: "",
|
||||
type: "text"
|
||||
});
|
||||
attributes.setLabel(note.noteId, "iconClass", CHILD_NOTE_ICON);
|
||||
this.moveMarker(note.noteId, e.latlng);
|
||||
}
|
||||
|
||||
this.#changeState(State.Normal);
|
||||
}
|
||||
|
||||
async moveMarker(noteId: string, latLng: LatLng | null) {
|
||||
const value = latLng ? [latLng.lat, latLng.lng].join(",") : "";
|
||||
await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value);
|
||||
}
|
||||
|
||||
getData(): any {
|
||||
const map = this.geoMapWidget.map;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: MapData = {
|
||||
view: {
|
||||
center: map.getBounds().getCenter(),
|
||||
zoom: map.getZoom()
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
content: JSON.stringify(data)
|
||||
};
|
||||
}
|
||||
|
||||
async geoMapCreateChildNoteEvent({ ntxId }: EventData<"geoMapCreateChildNote">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.showPersistent({
|
||||
icon: "plus",
|
||||
id: "geo-new-note",
|
||||
title: "New note",
|
||||
message: t("geo-map.create-child-note-instruction")
|
||||
});
|
||||
|
||||
this.#changeState(State.NewNote);
|
||||
|
||||
const globalKeyListener: (this: Window, ev: KeyboardEvent) => any = (e) => {
|
||||
if (e.key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#changeState(State.Normal);
|
||||
|
||||
window.removeEventListener("keydown", globalKeyListener);
|
||||
toastService.closePersistent("geo-new-note");
|
||||
};
|
||||
window.addEventListener("keydown", globalKeyListener);
|
||||
}
|
||||
|
||||
async doRefresh(note: FNote) {
|
||||
await this.geoMapWidget.refresh();
|
||||
this.#restoreViewportAndZoom();
|
||||
await this.#reloadMarkers();
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// If any of the children branches are altered.
|
||||
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.noteId)) {
|
||||
this.#reloadMarkers();
|
||||
return;
|
||||
}
|
||||
|
||||
// If any of note has its location attribute changed.
|
||||
// TODO: Should probably filter by parent here as well.
|
||||
const attributeRows = loadResults.getAttributeRows();
|
||||
if (attributeRows.find((at) => [LOCATION_ATTRIBUTE, "color"].includes(at.name ?? ""))) {
|
||||
this.#reloadMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
openGeoLocationEvent({ noteId, event }: EventData<"openGeoLocation">) {
|
||||
const marker = this.currentMarkerData[noteId];
|
||||
if (!marker) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latLng = this.currentMarkerData[noteId].getLatLng();
|
||||
const url = `geo:${latLng.lat},${latLng.lng}`;
|
||||
link.goToLinkExt(event, url);
|
||||
}
|
||||
|
||||
deleteFromMapEvent({ noteId }: EventData<"deleteFromMap">) {
|
||||
this.moveMarker(noteId, null);
|
||||
}
|
||||
|
||||
buildTouchBarCommand({ TouchBar }: CommandListenerData<"buildTouchBar">) {
|
||||
const map = this.geoMapWidget.map;
|
||||
const that = this;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [
|
||||
new TouchBar.TouchBarSlider({
|
||||
label: "Zoom",
|
||||
value: map.getZoom(),
|
||||
minValue: map.getMinZoom(),
|
||||
maxValue: map.getMaxZoom(),
|
||||
change(newValue) {
|
||||
that.ignoreNextZoomEvent = true;
|
||||
map.setZoom(newValue);
|
||||
},
|
||||
}),
|
||||
new TouchBar.TouchBarButton({
|
||||
label: "New geo note",
|
||||
click: () => this.triggerCommand("geoMapCreateChildNote", { ntxId: this.ntxId }),
|
||||
enabled: (this._state === State.Normal)
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import appContext from "../../components/app_context.js";
|
||||
import type { ContextMenuEvent } from "../../menus/context_menu.js";
|
||||
import contextMenu from "../../menus/context_menu.js";
|
||||
import linkContextMenu from "../../menus/link_context_menu.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
|
||||
export default function openContextMenu(noteId: string, e: ContextMenuEvent) {
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
...linkContextMenu.getItems(),
|
||||
{ title: t("geo-map-context.open-location"), command: "openGeoLocation", uiIcon: "bx bx-map-alt" },
|
||||
{ title: "----" },
|
||||
{ title: t("geo-map-context.remove-from-map"), command: "deleteFromMap", uiIcon: "bx bx-trash" }
|
||||
],
|
||||
selectMenuItemHandler: ({ command }, e) => {
|
||||
if (command === "deleteFromMap") {
|
||||
appContext.triggerCommand(command, { noteId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "openGeoLocation") {
|
||||
appContext.triggerCommand(command, { noteId, event: e });
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the events to the link context menu
|
||||
linkContextMenu.handleLinkContextMenuItem(command, noteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import type { MindElixirCtor, MindElixirInstance } from "mind-elixir";
|
||||
import type { MindElixirInstance } from "mind-elixir";
|
||||
import nodeMenu from "@mind-elixir/node-menu";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
// allow node-menu plugin css to be bundled by webpack
|
||||
import "mind-elixir/style";
|
||||
import "@mind-elixir/node-menu/dist/style.css";
|
||||
|
||||
const NEW_TOPIC_NAME = "";
|
||||
|
||||
@@ -0,0 +1,659 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import server from "../../../../services/server.js";
|
||||
import toastService from "../../../../services/toast.js";
|
||||
import noteAutocompleteService from "../../../../services/note_autocomplete.js";
|
||||
import type { OptionMap } from "@triliumnext/commons";
|
||||
import type { Suggestion } from "../../../../services/note_autocomplete.js";
|
||||
|
||||
interface FileSystemMapping {
|
||||
mappingId: string;
|
||||
noteId: string;
|
||||
filePath: string;
|
||||
syncDirection: 'bidirectional' | 'trilium_to_disk' | 'disk_to_trilium';
|
||||
isActive: boolean;
|
||||
includeSubtree: boolean;
|
||||
preserveHierarchy: boolean;
|
||||
contentFormat: 'auto' | 'markdown' | 'html' | 'raw';
|
||||
excludePatterns: string[] | null;
|
||||
lastSyncTime: string | null;
|
||||
syncErrors: string[] | null;
|
||||
dateCreated: string;
|
||||
dateModified: string;
|
||||
}
|
||||
|
||||
interface SyncStatus {
|
||||
enabled: boolean;
|
||||
initialized: boolean;
|
||||
status?: Record<string, any>;
|
||||
}
|
||||
|
||||
// API Request/Response interfaces
|
||||
interface PathValidationRequest {
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
interface PathValidationResponse {
|
||||
exists: boolean;
|
||||
stats?: {
|
||||
isDirectory: boolean;
|
||||
size: number;
|
||||
modified: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateMappingRequest {
|
||||
noteId: string;
|
||||
filePath: string;
|
||||
syncDirection: 'bidirectional' | 'trilium_to_disk' | 'disk_to_trilium';
|
||||
contentFormat: 'auto' | 'markdown' | 'html' | 'raw';
|
||||
includeSubtree: boolean;
|
||||
preserveHierarchy: boolean;
|
||||
excludePatterns: string[] | null;
|
||||
}
|
||||
|
||||
interface UpdateMappingRequest extends CreateMappingRequest {}
|
||||
|
||||
interface SyncMappingResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
success?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const TPL = /*html*/`
|
||||
<style>
|
||||
.modal-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.modal-visible {
|
||||
display: flex !important;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1050;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
<div class="options-section">
|
||||
<h4>File System Sync</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" class="file-sync-enabled-checkbox">
|
||||
Enable file system synchronization
|
||||
</label>
|
||||
<div class="help-block">
|
||||
Allows bidirectional synchronization between Trilium notes and files on your local file system.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-sync-controls" style="display: none;">
|
||||
<div class="alert alert-info">
|
||||
<strong>Note:</strong> File system sync creates mappings between notes and files/directories.
|
||||
Changes in either location will be synchronized automatically when enabled.
|
||||
</div>
|
||||
|
||||
<div class="sync-status-container">
|
||||
<h5>Sync Status</h5>
|
||||
<div class="sync-status-info">
|
||||
<div class="status-item">
|
||||
<strong>Status:</strong> <span class="sync-status-text">Loading...</span>
|
||||
</div>
|
||||
<div class="active-mappings-count">
|
||||
<strong>Active Mappings:</strong> <span class="mappings-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mappings-section">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5>File System Mappings</h5>
|
||||
<button class="btn btn-primary btn-sm create-mapping-button">
|
||||
<i class="bx bx-plus"></i> Create Mapping
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mappings-list">
|
||||
<!-- Mappings will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sync-actions mt-3">
|
||||
<button class="btn btn-secondary refresh-status-button">
|
||||
<i class="bx bx-refresh"></i> Refresh Status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Mapping Modal -->
|
||||
<div class="mapping-modal modal-hidden">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Create File System Mapping</h5>
|
||||
<button type="button" class="modal-close" aria-label="Close">
|
||||
<i class="bx bx-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="mapping-form">
|
||||
<div class="form-group">
|
||||
<label for="note-selector">Note:</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="note-selector" class="form-control note-selector"
|
||||
placeholder="Search for a note...">
|
||||
</div>
|
||||
<div class="help-block">Select the note to map to the file system.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="file-path">File/Directory Path:</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="file-path" class="form-control file-path-input"
|
||||
placeholder="/path/to/file/or/directory">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-secondary validate-path-button">
|
||||
<i class="bx bx-search"></i> Validate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="path-validation-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync-direction">Sync Direction:</label>
|
||||
<select id="sync-direction" class="form-control sync-direction-select">
|
||||
<option value="bidirectional">Bidirectional (default)</option>
|
||||
<option value="trilium_to_disk">Trilium → Disk only</option>
|
||||
<option value="disk_to_trilium">Disk → Trilium only</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="content-format">Content Format:</label>
|
||||
<select id="content-format" class="form-control content-format-select">
|
||||
<option value="auto">Auto-detect (default)</option>
|
||||
<option value="markdown">Markdown</option>
|
||||
<option value="html">HTML</option>
|
||||
<option value="raw">Raw/Binary</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" class="include-subtree-checkbox">
|
||||
Include subtree
|
||||
</label>
|
||||
<div class="help-block">Map entire note subtree to directory structure.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group subtree-options" style="display: none;">
|
||||
<label>
|
||||
<input type="checkbox" class="preserve-hierarchy-checkbox" checked>
|
||||
Preserve directory hierarchy
|
||||
</label>
|
||||
<div class="help-block">Create subdirectories matching note hierarchy.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="exclude-patterns">Exclude Patterns (one per line):</label>
|
||||
<textarea id="exclude-patterns" class="form-control exclude-patterns-textarea"
|
||||
rows="3" placeholder="*.tmp node_modules .git"></textarea>
|
||||
<div class="help-block">Files/directories matching these patterns will be ignored.</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary cancel-mapping-button">Cancel</button>
|
||||
<button type="button" class="btn btn-primary save-mapping-button">Save Mapping</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const MAPPING_ITEM_TPL = /*html*/`
|
||||
<div class="mapping-item card mb-2" data-mapping-id="">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="mapping-info">
|
||||
<div class="mapping-path">
|
||||
<strong class="file-path"></strong>
|
||||
</div>
|
||||
<div class="mapping-details text-muted">
|
||||
<span class="note-title"></span> •
|
||||
<span class="sync-direction-text"></span> •
|
||||
<span class="content-format-text"></span>
|
||||
</div>
|
||||
<div class="mapping-status">
|
||||
<span class="status-badge"></span>
|
||||
<span class="last-sync"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mapping-actions">
|
||||
<button class="btn btn-sm btn-secondary sync-mapping-button" title="Sync now">
|
||||
<i class="bx bx-refresh"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary edit-mapping-button" title="Edit">
|
||||
<i class="bx bx-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger delete-mapping-button" title="Delete">
|
||||
<i class="bx bx-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sync-errors" style="display: none;">
|
||||
<div class="alert alert-warning mt-2">
|
||||
<strong>Sync Errors:</strong>
|
||||
<ul class="error-list mb-0"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class FileSystemSyncOptions extends OptionsWidget {
|
||||
private $fileSyncEnabledCheckbox!: JQuery<HTMLElement>;
|
||||
private $fileSyncControls!: JQuery<HTMLElement>;
|
||||
private $syncStatusText!: JQuery<HTMLElement>;
|
||||
private $mappingsCount!: JQuery<HTMLElement>;
|
||||
private $mappingsList!: JQuery<HTMLElement>;
|
||||
private $createMappingButton!: JQuery<HTMLElement>;
|
||||
private $refreshStatusButton!: JQuery<HTMLElement>;
|
||||
|
||||
// Modal elements
|
||||
private $mappingModal!: JQuery<HTMLElement>;
|
||||
private $modalTitle!: JQuery<HTMLElement>;
|
||||
private $noteSelector!: JQuery<HTMLElement>;
|
||||
private $filePathInput!: JQuery<HTMLElement>;
|
||||
private $validatePathButton!: JQuery<HTMLElement>;
|
||||
private $pathValidationResult!: JQuery<HTMLElement>;
|
||||
private $syncDirectionSelect!: JQuery<HTMLElement>;
|
||||
private $contentFormatSelect!: JQuery<HTMLElement>;
|
||||
private $includeSubtreeCheckbox!: JQuery<HTMLElement>;
|
||||
private $preserveHierarchyCheckbox!: JQuery<HTMLElement>;
|
||||
private $subtreeOptions!: JQuery<HTMLElement>;
|
||||
private $excludePatternsTextarea!: JQuery<HTMLElement>;
|
||||
private $saveMappingButton!: JQuery<HTMLElement>;
|
||||
private $cancelMappingButton!: JQuery<HTMLElement>;
|
||||
private $modalClose!: JQuery<HTMLElement>;
|
||||
|
||||
private currentEditingMappingId: string | null = null;
|
||||
private mappings: FileSystemMapping[] = [];
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.initializeElements();
|
||||
// Ensure modal is hidden on initialization
|
||||
this.$mappingModal.addClass('modal-hidden').removeClass('modal-visible');
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
private initializeElements() {
|
||||
this.$fileSyncEnabledCheckbox = this.$widget.find(".file-sync-enabled-checkbox");
|
||||
this.$fileSyncControls = this.$widget.find(".file-sync-controls");
|
||||
this.$syncStatusText = this.$widget.find(".sync-status-text");
|
||||
this.$mappingsCount = this.$widget.find(".mappings-count");
|
||||
this.$mappingsList = this.$widget.find(".mappings-list");
|
||||
this.$createMappingButton = this.$widget.find(".create-mapping-button");
|
||||
this.$refreshStatusButton = this.$widget.find(".refresh-status-button");
|
||||
|
||||
// Modal elements
|
||||
this.$mappingModal = this.$widget.closest(".mapping-modal");
|
||||
this.$modalTitle = this.$mappingModal.find(".modal-title");
|
||||
this.$noteSelector = this.$mappingModal.find(".note-selector");
|
||||
this.$filePathInput = this.$mappingModal.find(".file-path-input");
|
||||
this.$validatePathButton = this.$mappingModal.find(".validate-path-button");
|
||||
this.$pathValidationResult = this.$mappingModal.find(".path-validation-result");
|
||||
this.$syncDirectionSelect = this.$mappingModal.find(".sync-direction-select");
|
||||
this.$contentFormatSelect = this.$mappingModal.find(".content-format-select");
|
||||
this.$includeSubtreeCheckbox = this.$mappingModal.find(".include-subtree-checkbox");
|
||||
this.$preserveHierarchyCheckbox = this.$mappingModal.find(".preserve-hierarchy-checkbox");
|
||||
this.$subtreeOptions = this.$mappingModal.find(".subtree-options");
|
||||
this.$excludePatternsTextarea = this.$mappingModal.find(".exclude-patterns-textarea");
|
||||
this.$saveMappingButton = this.$mappingModal.find(".save-mapping-button");
|
||||
this.$cancelMappingButton = this.$mappingModal.find(".cancel-mapping-button");
|
||||
this.$modalClose = this.$mappingModal.find(".modal-close");
|
||||
}
|
||||
|
||||
private setupEventHandlers() {
|
||||
this.$fileSyncEnabledCheckbox.on("change", async () => {
|
||||
const isEnabled = this.$fileSyncEnabledCheckbox.prop("checked");
|
||||
|
||||
try {
|
||||
if (isEnabled) {
|
||||
await server.post<ApiResponse>("file-system-sync/enable");
|
||||
} else {
|
||||
await server.post<ApiResponse>("file-system-sync/disable");
|
||||
}
|
||||
|
||||
this.toggleControls(isEnabled);
|
||||
if (isEnabled) {
|
||||
await this.refreshStatus();
|
||||
}
|
||||
|
||||
toastService.showMessage(`File system sync ${isEnabled ? 'enabled' : 'disabled'}`);
|
||||
} catch (error) {
|
||||
toastService.showError(`Failed to ${isEnabled ? 'enable' : 'disable'} file system sync`);
|
||||
// Revert checkbox state
|
||||
this.$fileSyncEnabledCheckbox.prop("checked", !isEnabled);
|
||||
}
|
||||
});
|
||||
|
||||
this.$createMappingButton.on("click", () => {
|
||||
this.showMappingModal();
|
||||
});
|
||||
|
||||
this.$refreshStatusButton.on("click", () => {
|
||||
this.refreshStatus();
|
||||
});
|
||||
|
||||
this.$validatePathButton.on("click", () => {
|
||||
this.validatePath();
|
||||
});
|
||||
|
||||
this.$includeSubtreeCheckbox.on("change", () => {
|
||||
const isChecked = this.$includeSubtreeCheckbox.prop("checked");
|
||||
this.$subtreeOptions.toggle(isChecked);
|
||||
});
|
||||
|
||||
// Modal handlers
|
||||
this.$saveMappingButton.on("click", () => {
|
||||
this.saveMapping();
|
||||
});
|
||||
|
||||
this.$cancelMappingButton.on("click", () => {
|
||||
this.hideMappingModal();
|
||||
});
|
||||
|
||||
this.$modalClose.on("click", () => {
|
||||
this.hideMappingModal();
|
||||
});
|
||||
|
||||
this.$mappingModal.find(".modal-backdrop").on("click", () => {
|
||||
this.hideMappingModal();
|
||||
});
|
||||
|
||||
// Note selector autocomplete will be initialized in showMappingModal
|
||||
}
|
||||
|
||||
private toggleControls(enabled: boolean) {
|
||||
this.$fileSyncControls.toggle(enabled);
|
||||
}
|
||||
|
||||
private async refreshStatus() {
|
||||
try {
|
||||
const status = await server.get<SyncStatus>("file-system-sync/status");
|
||||
|
||||
this.$syncStatusText.text(status.initialized ? "Active" : "Inactive");
|
||||
|
||||
if (status.initialized) {
|
||||
await this.loadMappings();
|
||||
}
|
||||
} catch (error) {
|
||||
this.$syncStatusText.text("Error");
|
||||
toastService.showError("Failed to get sync status");
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMappings() {
|
||||
try {
|
||||
this.mappings = await server.get<FileSystemMapping[]>("file-system-sync/mappings");
|
||||
this.renderMappings();
|
||||
this.$mappingsCount.text(this.mappings.length.toString());
|
||||
} catch (error) {
|
||||
toastService.showError("Failed to load mappings");
|
||||
}
|
||||
}
|
||||
|
||||
private renderMappings() {
|
||||
this.$mappingsList.empty();
|
||||
|
||||
for (const mapping of this.mappings) {
|
||||
const $item = $(MAPPING_ITEM_TPL);
|
||||
$item.attr("data-mapping-id", mapping.mappingId);
|
||||
|
||||
$item.find(".file-path").text(mapping.filePath);
|
||||
$item.find(".note-title").text(`Note: ${mapping.noteId}`); // TODO: Get actual note title
|
||||
$item.find(".sync-direction-text").text(this.formatSyncDirection(mapping.syncDirection));
|
||||
$item.find(".content-format-text").text(mapping.contentFormat);
|
||||
|
||||
// Status badge
|
||||
const $statusBadge = $item.find(".status-badge");
|
||||
if (mapping.syncErrors && mapping.syncErrors.length > 0) {
|
||||
$statusBadge.addClass("badge badge-danger").text("Error");
|
||||
const $errorsDiv = $item.find(".sync-errors");
|
||||
const $errorList = $errorsDiv.find(".error-list");
|
||||
mapping.syncErrors.forEach(error => {
|
||||
$errorList.append(`<li>${error}</li>`);
|
||||
});
|
||||
$errorsDiv.show();
|
||||
} else if (mapping.isActive) {
|
||||
$statusBadge.addClass("badge badge-success").text("Active");
|
||||
} else {
|
||||
$statusBadge.addClass("badge badge-secondary").text("Inactive");
|
||||
}
|
||||
|
||||
// Last sync time
|
||||
if (mapping.lastSyncTime) {
|
||||
const lastSync = new Date(mapping.lastSyncTime).toLocaleString();
|
||||
$item.find(".last-sync").text(`Last sync: ${lastSync}`);
|
||||
} else {
|
||||
$item.find(".last-sync").text("Never synced");
|
||||
}
|
||||
|
||||
// Action handlers
|
||||
$item.find(".sync-mapping-button").on("click", () => {
|
||||
this.syncMapping(mapping.mappingId);
|
||||
});
|
||||
|
||||
$item.find(".edit-mapping-button").on("click", () => {
|
||||
this.editMapping(mapping);
|
||||
});
|
||||
|
||||
$item.find(".delete-mapping-button").on("click", () => {
|
||||
this.deleteMapping(mapping.mappingId);
|
||||
});
|
||||
|
||||
this.$mappingsList.append($item);
|
||||
}
|
||||
}
|
||||
|
||||
private formatSyncDirection(direction: string): string {
|
||||
switch (direction) {
|
||||
case 'bidirectional': return 'Bidirectional';
|
||||
case 'trilium_to_disk': return 'Trilium → Disk';
|
||||
case 'disk_to_trilium': return 'Disk → Trilium';
|
||||
default: return direction;
|
||||
}
|
||||
}
|
||||
|
||||
private showMappingModal(mapping?: FileSystemMapping) {
|
||||
this.currentEditingMappingId = mapping?.mappingId || null;
|
||||
console.log("Showing mapping modal", this.currentEditingMappingId, this.$mappingModal);
|
||||
|
||||
if (mapping) {
|
||||
this.$modalTitle.text("Edit File System Mapping");
|
||||
this.populateMappingForm(mapping);
|
||||
} else {
|
||||
this.$modalTitle.text("Create File System Mapping");
|
||||
this.clearMappingForm();
|
||||
}
|
||||
|
||||
// Initialize note autocomplete
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$noteSelector, {
|
||||
allowCreatingNotes: true,
|
||||
});
|
||||
|
||||
// Handle note selection
|
||||
this.$noteSelector.off("autocomplete:noteselected").on("autocomplete:noteselected", (event: JQuery.Event, suggestion: Suggestion) => {
|
||||
// The note autocomplete service will automatically set the selected note path
|
||||
// which we can retrieve using getSelectedNoteId()
|
||||
});
|
||||
|
||||
this.$mappingModal.removeClass('modal-hidden').addClass('modal-visible');
|
||||
}
|
||||
|
||||
private hideMappingModal() {
|
||||
this.$mappingModal.removeClass('modal-visible').addClass('modal-hidden');
|
||||
this.clearMappingForm();
|
||||
this.currentEditingMappingId = null;
|
||||
}
|
||||
|
||||
private async populateMappingForm(mapping: FileSystemMapping) {
|
||||
// Set the note using the autocomplete service's setNote method
|
||||
await this.$noteSelector.setNote(mapping.noteId);
|
||||
|
||||
this.$filePathInput.val(mapping.filePath);
|
||||
this.$syncDirectionSelect.val(mapping.syncDirection);
|
||||
this.$contentFormatSelect.val(mapping.contentFormat);
|
||||
this.$includeSubtreeCheckbox.prop("checked", mapping.includeSubtree);
|
||||
this.$preserveHierarchyCheckbox.prop("checked", mapping.preserveHierarchy);
|
||||
this.$subtreeOptions.toggle(mapping.includeSubtree);
|
||||
|
||||
if (mapping.excludePatterns) {
|
||||
this.$excludePatternsTextarea.val(mapping.excludePatterns.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
private clearMappingForm() {
|
||||
// Clear the note selector using autocomplete service
|
||||
this.$noteSelector.val('').setSelectedNotePath('');
|
||||
this.$filePathInput.val('');
|
||||
this.$syncDirectionSelect.val('bidirectional');
|
||||
this.$contentFormatSelect.val('auto');
|
||||
this.$includeSubtreeCheckbox.prop("checked", false);
|
||||
this.$preserveHierarchyCheckbox.prop("checked", true);
|
||||
this.$subtreeOptions.hide();
|
||||
this.$excludePatternsTextarea.val('');
|
||||
this.$pathValidationResult.empty();
|
||||
}
|
||||
|
||||
private async validatePath() {
|
||||
const filePath = this.$filePathInput.val() as string;
|
||||
if (!filePath) {
|
||||
this.$pathValidationResult.html('<div class="text-danger">Please enter a file path</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await server.post<PathValidationResponse>("file-system-sync/validate-path", { filePath } as PathValidationRequest);
|
||||
|
||||
if (result.exists && result.stats) {
|
||||
const type = result.stats.isDirectory ? 'directory' : 'file';
|
||||
this.$pathValidationResult.html(
|
||||
`<div class="text-success">✓ Valid ${type} (${result.stats.size} bytes, modified ${new Date(result.stats.modified).toLocaleString()})</div>`
|
||||
);
|
||||
} else {
|
||||
this.$pathValidationResult.html('<div class="text-warning">⚠ Path does not exist</div>');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$pathValidationResult.html('<div class="text-danger">✗ Invalid path</div>');
|
||||
}
|
||||
}
|
||||
|
||||
private async saveMapping() {
|
||||
const noteId = this.$noteSelector.getSelectedNoteId();
|
||||
const filePath = this.$filePathInput.val() as string;
|
||||
const syncDirection = this.$syncDirectionSelect.val() as string;
|
||||
const contentFormat = this.$contentFormatSelect.val() as string;
|
||||
const includeSubtree = this.$includeSubtreeCheckbox.prop("checked");
|
||||
const preserveHierarchy = this.$preserveHierarchyCheckbox.prop("checked");
|
||||
const excludePatternsText = this.$excludePatternsTextarea.val() as string;
|
||||
|
||||
// Validation
|
||||
if (!noteId) {
|
||||
toastService.showError("Please select a note");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
toastService.showError("Please enter a file path");
|
||||
return;
|
||||
}
|
||||
|
||||
const excludePatterns = excludePatternsText.trim()
|
||||
? excludePatternsText.split('\n').map(p => p.trim()).filter(p => p)
|
||||
: null;
|
||||
|
||||
const mappingData: CreateMappingRequest = {
|
||||
noteId,
|
||||
filePath,
|
||||
syncDirection: syncDirection as 'bidirectional' | 'trilium_to_disk' | 'disk_to_trilium',
|
||||
contentFormat: contentFormat as 'auto' | 'markdown' | 'html' | 'raw',
|
||||
includeSubtree,
|
||||
preserveHierarchy,
|
||||
excludePatterns
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.currentEditingMappingId) {
|
||||
await server.put<ApiResponse>(`file-system-sync/mappings/${this.currentEditingMappingId}`, mappingData as UpdateMappingRequest);
|
||||
toastService.showMessage("Mapping updated successfully");
|
||||
} else {
|
||||
await server.post<ApiResponse>("file-system-sync/mappings", mappingData);
|
||||
toastService.showMessage("Mapping created successfully");
|
||||
}
|
||||
|
||||
this.hideMappingModal();
|
||||
await this.loadMappings();
|
||||
} catch (error) {
|
||||
toastService.showError("Failed to save mapping");
|
||||
}
|
||||
}
|
||||
|
||||
private async syncMapping(mappingId: string) {
|
||||
try {
|
||||
const result = await server.post<SyncMappingResponse>(`file-system-sync/mappings/${mappingId}/sync`);
|
||||
if (result.success) {
|
||||
toastService.showMessage("Sync completed successfully");
|
||||
} else {
|
||||
toastService.showError(`Sync failed: ${result.message}`);
|
||||
}
|
||||
await this.loadMappings();
|
||||
} catch (error) {
|
||||
toastService.showError("Failed to trigger sync");
|
||||
}
|
||||
}
|
||||
|
||||
private editMapping(mapping: FileSystemMapping) {
|
||||
this.showMappingModal(mapping);
|
||||
}
|
||||
|
||||
private async deleteMapping(mappingId: string) {
|
||||
if (!confirm("Are you sure you want to delete this mapping?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await server.delete<ApiResponse>(`file-system-sync/mappings/${mappingId}`);
|
||||
toastService.showMessage("Mapping deleted successfully");
|
||||
await this.loadMappings();
|
||||
} catch (error) {
|
||||
toastService.showError("Failed to delete mapping");
|
||||
}
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
const isEnabled = options.fileSystemSyncEnabled === "true";
|
||||
this.$fileSyncEnabledCheckbox.prop("checked", isEnabled);
|
||||
this.toggleControls(isEnabled);
|
||||
|
||||
if (isEnabled) {
|
||||
await this.refreshStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,17 @@ export default abstract class TypeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
activeNoteChangedEvent() {
|
||||
if (!this.isActiveNoteContext()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore focus to the editor when switching tabs, but only if the note tree is not already focused.
|
||||
if (!document.activeElement?.classList.contains("fancytree-title")) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
|
||||
@@ -20,7 +20,7 @@ const TPL = /*html*/`
|
||||
|
||||
function buildElement() {
|
||||
if (!utils.isElectron()) {
|
||||
return `<iframe class="note-detail-web-view-content" sandbox="allow-same-origin allow-scripts"></iframe>`;
|
||||
return `<iframe class="note-detail-web-view-content" sandbox="allow-same-origin allow-scripts allow-popups"></iframe>`;
|
||||
} else {
|
||||
return `<webview class="note-detail-web-view-content"></webview>`;
|
||||
}
|
||||
|
||||
180
apps/client/src/widgets/view_widgets/board_view/api.ts
Normal file
180
apps/client/src/widgets/view_widgets/board_view/api.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import attributes from "../../../services/attributes";
|
||||
import { executeBulkActions } from "../../../services/bulk_action";
|
||||
import note_create from "../../../services/note_create";
|
||||
import ViewModeStorage from "../view_mode_storage";
|
||||
import { BoardData } from "./config";
|
||||
import { ColumnMap, getBoardData } from "./data";
|
||||
|
||||
export default class BoardApi {
|
||||
|
||||
private constructor(
|
||||
private _columns: string[],
|
||||
private _parentNoteId: string,
|
||||
private viewStorage: ViewModeStorage<BoardData>,
|
||||
private byColumn: ColumnMap,
|
||||
private persistedData: BoardData,
|
||||
private _statusAttribute: string) {}
|
||||
|
||||
get columns() {
|
||||
return this._columns;
|
||||
}
|
||||
|
||||
get statusAttribute() {
|
||||
return this._statusAttribute;
|
||||
}
|
||||
|
||||
getColumn(column: string) {
|
||||
return this.byColumn.get(column);
|
||||
}
|
||||
|
||||
async changeColumn(noteId: string, newColumn: string) {
|
||||
await attributes.setLabel(noteId, this._statusAttribute, newColumn);
|
||||
}
|
||||
|
||||
openNote(noteId: string) {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: noteId });
|
||||
}
|
||||
|
||||
async insertRowAtPosition(
|
||||
column: string,
|
||||
relativeToBranchId: string,
|
||||
direction: "before" | "after",
|
||||
open: boolean = true) {
|
||||
const { note } = await note_create.createNote(this._parentNoteId, {
|
||||
activate: false,
|
||||
targetBranchId: relativeToBranchId,
|
||||
target: direction,
|
||||
title: "New item"
|
||||
});
|
||||
|
||||
if (!note) {
|
||||
throw new Error("Failed to create note");
|
||||
}
|
||||
|
||||
const { noteId } = note;
|
||||
await this.changeColumn(noteId, column);
|
||||
if (open) {
|
||||
this.openNote(noteId);
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
async renameColumn(oldValue: string, newValue: string, noteIds: string[]) {
|
||||
// Change the value in the notes.
|
||||
await executeBulkActions(noteIds, [
|
||||
{
|
||||
name: "updateLabelValue",
|
||||
labelName: this._statusAttribute,
|
||||
labelValue: newValue
|
||||
}
|
||||
]);
|
||||
|
||||
// Rename the column in the persisted data.
|
||||
for (const column of this.persistedData.columns || []) {
|
||||
if (column.value === oldValue) {
|
||||
column.value = newValue;
|
||||
}
|
||||
}
|
||||
await this.viewStorage.store(this.persistedData);
|
||||
}
|
||||
|
||||
async removeColumn(column: string) {
|
||||
// Remove the value from the notes.
|
||||
const noteIds = this.byColumn.get(column)?.map(item => item.note.noteId) || [];
|
||||
await executeBulkActions(noteIds, [
|
||||
{
|
||||
name: "deleteLabel",
|
||||
labelName: this._statusAttribute
|
||||
}
|
||||
]);
|
||||
|
||||
this.persistedData.columns = (this.persistedData.columns ?? []).filter(col => col.value !== column);
|
||||
this.viewStorage.store(this.persistedData);
|
||||
}
|
||||
|
||||
async createColumn(columnValue: string) {
|
||||
// Add the new column to persisted data if it doesn't exist
|
||||
if (!this.persistedData.columns) {
|
||||
this.persistedData.columns = [];
|
||||
}
|
||||
|
||||
const existingColumn = this.persistedData.columns.find(col => col.value === columnValue);
|
||||
if (!existingColumn) {
|
||||
this.persistedData.columns.push({ value: columnValue });
|
||||
await this.viewStorage.store(this.persistedData);
|
||||
}
|
||||
|
||||
return columnValue;
|
||||
}
|
||||
|
||||
async reorderColumns(newColumnOrder: string[]) {
|
||||
// Update the column order in persisted data
|
||||
if (!this.persistedData.columns) {
|
||||
this.persistedData.columns = [];
|
||||
}
|
||||
|
||||
// Create a map of existing column data
|
||||
const columnDataMap = new Map();
|
||||
this.persistedData.columns.forEach(col => {
|
||||
columnDataMap.set(col.value, col);
|
||||
});
|
||||
|
||||
// Reorder columns based on new order
|
||||
this.persistedData.columns = newColumnOrder.map(columnValue => {
|
||||
return columnDataMap.get(columnValue) || { value: columnValue };
|
||||
});
|
||||
|
||||
// Update internal columns array
|
||||
this._columns = newColumnOrder;
|
||||
|
||||
await this.viewStorage.store(this.persistedData);
|
||||
}
|
||||
|
||||
async refresh(parentNote: FNote) {
|
||||
// Refresh the API data by re-fetching from the parent note
|
||||
const statusAttribute = parentNote.getLabelValue("board:groupBy") ?? "status";
|
||||
this._statusAttribute = statusAttribute;
|
||||
|
||||
// Use the current in-memory persisted data instead of restoring from storage
|
||||
// This ensures we don't lose recent updates like column renames
|
||||
const { byColumn, newPersistedData } = await getBoardData(parentNote, statusAttribute, this.persistedData);
|
||||
|
||||
// Update internal state
|
||||
this.byColumn = byColumn;
|
||||
|
||||
if (newPersistedData) {
|
||||
this.persistedData = newPersistedData;
|
||||
this.viewStorage.store(this.persistedData);
|
||||
}
|
||||
|
||||
// Use the order from persistedData.columns, then add any new columns found
|
||||
const orderedColumns = this.persistedData.columns?.map(col => col.value) || [];
|
||||
const allColumns = Array.from(byColumn.keys());
|
||||
const newColumns = allColumns.filter(col => !orderedColumns.includes(col));
|
||||
this._columns = [...orderedColumns, ...newColumns];
|
||||
}
|
||||
|
||||
static async build(parentNote: FNote, viewStorage: ViewModeStorage<BoardData>) {
|
||||
const statusAttribute = parentNote.getLabelValue("board:groupBy") ?? "status";
|
||||
|
||||
let persistedData = await viewStorage.restore() ?? {};
|
||||
const { byColumn, newPersistedData } = await getBoardData(parentNote, statusAttribute, persistedData);
|
||||
|
||||
// Use the order from persistedData.columns, then add any new columns found
|
||||
const orderedColumns = persistedData.columns?.map(col => col.value) || [];
|
||||
const allColumns = Array.from(byColumn.keys());
|
||||
const newColumns = allColumns.filter(col => !orderedColumns.includes(col));
|
||||
const columns = [...orderedColumns, ...newColumns];
|
||||
|
||||
if (newPersistedData) {
|
||||
persistedData = newPersistedData;
|
||||
viewStorage.store(persistedData);
|
||||
}
|
||||
|
||||
return new BoardApi(columns, parentNote.noteId, viewStorage, byColumn, persistedData, statusAttribute);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import BoardApi from "./api";
|
||||
import { DragContext, BaseDragHandler } from "./drag_types";
|
||||
|
||||
export class ColumnDragHandler implements BaseDragHandler {
|
||||
private $container: JQuery<HTMLElement>;
|
||||
private api: BoardApi;
|
||||
private context: DragContext;
|
||||
|
||||
constructor(
|
||||
$container: JQuery<HTMLElement>,
|
||||
api: BoardApi,
|
||||
context: DragContext,
|
||||
) {
|
||||
this.$container = $container;
|
||||
this.api = api;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
setupColumnDrag($columnEl: JQuery<HTMLElement>, columnValue: string) {
|
||||
const $titleEl = $columnEl.find('h3[data-column-value]');
|
||||
|
||||
$titleEl.attr("draggable", "true");
|
||||
|
||||
// Delay drag start to allow click detection
|
||||
let dragStartTimer: number | null = null;
|
||||
|
||||
$titleEl.on("mousedown", (e) => {
|
||||
// Don't interfere with editing mode or input field interactions
|
||||
if ($titleEl.hasClass('editing') || $(e.target).is('input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing timer
|
||||
if (dragStartTimer) {
|
||||
clearTimeout(dragStartTimer);
|
||||
dragStartTimer = null;
|
||||
}
|
||||
|
||||
// Set a short delay before enabling dragging
|
||||
dragStartTimer = window.setTimeout(() => {
|
||||
$titleEl.attr("draggable", "true");
|
||||
dragStartTimer = null;
|
||||
}, 150);
|
||||
});
|
||||
|
||||
$titleEl.on("mouseup mouseleave", (e) => {
|
||||
// Don't interfere with editing mode
|
||||
if ($titleEl.hasClass('editing') || $(e.target).is('input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel drag start timer on mouse up or leave
|
||||
if (dragStartTimer) {
|
||||
clearTimeout(dragStartTimer);
|
||||
dragStartTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
$titleEl.on("dragstart", (e) => {
|
||||
// Only start dragging if the target is not an input (for inline editing)
|
||||
if ($(e.target).is('input') || $titleEl.hasClass('editing')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.context.draggedColumn = columnValue;
|
||||
this.context.draggedColumnElement = $columnEl;
|
||||
$columnEl.addClass("column-dragging");
|
||||
|
||||
const originalEvent = e.originalEvent as DragEvent;
|
||||
if (originalEvent.dataTransfer) {
|
||||
originalEvent.dataTransfer.effectAllowed = "move";
|
||||
originalEvent.dataTransfer.setData("text/plain", columnValue);
|
||||
}
|
||||
|
||||
// Prevent note dragging when column is being dragged
|
||||
e.stopPropagation();
|
||||
|
||||
// Setup global drag tracking for better drop indicator positioning
|
||||
this.setupGlobalColumnDragTracking();
|
||||
});
|
||||
|
||||
$titleEl.on("dragend", () => {
|
||||
$columnEl.removeClass("column-dragging");
|
||||
this.context.draggedColumn = null;
|
||||
this.context.draggedColumnElement = null;
|
||||
this.cleanupColumnDropIndicators();
|
||||
this.cleanupGlobalColumnDragTracking();
|
||||
|
||||
// Re-enable draggable
|
||||
$titleEl.attr("draggable", "true");
|
||||
});
|
||||
}
|
||||
|
||||
setupColumnDropZone($columnEl: JQuery<HTMLElement>) {
|
||||
$columnEl.on("dragover", (e) => {
|
||||
// Only handle column drops when a column is being dragged
|
||||
if (this.context.draggedColumn && !this.context.draggedNote) {
|
||||
e.preventDefault();
|
||||
const originalEvent = e.originalEvent as DragEvent;
|
||||
if (originalEvent.dataTransfer) {
|
||||
originalEvent.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
|
||||
// Don't highlight columns - we only care about the drop indicator position
|
||||
}
|
||||
});
|
||||
|
||||
$columnEl.on("drop", async (e) => {
|
||||
if (this.context.draggedColumn && !this.context.draggedNote) {
|
||||
e.preventDefault();
|
||||
console.log("Column drop event triggered for column:", this.context.draggedColumn);
|
||||
|
||||
// Use the drop indicator position to determine where to place the column
|
||||
await this.handleColumnDrop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.cleanupColumnDropIndicators();
|
||||
this.context.draggedColumn = null;
|
||||
this.context.draggedColumnElement = null;
|
||||
this.cleanupGlobalColumnDragTracking();
|
||||
}
|
||||
|
||||
private setupGlobalColumnDragTracking() {
|
||||
// Add container-level drag tracking for better indicator positioning
|
||||
this.$container.on("dragover.columnDrag", (e) => {
|
||||
if (this.context.draggedColumn) {
|
||||
e.preventDefault();
|
||||
const originalEvent = e.originalEvent as DragEvent;
|
||||
this.showColumnDropIndicator(originalEvent.clientX);
|
||||
}
|
||||
});
|
||||
|
||||
// Add container-level drop handler for column reordering
|
||||
this.$container.on("drop.columnDrag", async (e) => {
|
||||
if (this.context.draggedColumn) {
|
||||
e.preventDefault();
|
||||
console.log("Container drop event triggered for column:", this.context.draggedColumn);
|
||||
await this.handleColumnDrop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private cleanupGlobalColumnDragTracking() {
|
||||
this.$container.off("dragover.columnDrag");
|
||||
this.$container.off("drop.columnDrag");
|
||||
}
|
||||
|
||||
private cleanupColumnDropIndicators() {
|
||||
// Remove column drop indicators
|
||||
this.$container.find(".column-drop-indicator").remove();
|
||||
}
|
||||
|
||||
private showColumnDropIndicator(mouseX: number) {
|
||||
// Clean up existing indicators
|
||||
this.cleanupColumnDropIndicators();
|
||||
|
||||
// Get all columns (excluding the dragged one if it exists)
|
||||
let $allColumns = this.$container.find('.board-column');
|
||||
if (this.context.draggedColumnElement) {
|
||||
$allColumns = $allColumns.not(this.context.draggedColumnElement);
|
||||
}
|
||||
|
||||
let $targetColumn: JQuery<HTMLElement> = $();
|
||||
let insertBefore = false;
|
||||
|
||||
// Find which column the mouse is closest to
|
||||
$allColumns.each((_, columnEl) => {
|
||||
const $column = $(columnEl);
|
||||
const rect = columnEl.getBoundingClientRect();
|
||||
const columnMiddle = rect.left + rect.width / 2;
|
||||
|
||||
if (mouseX >= rect.left && mouseX <= rect.right) {
|
||||
// Mouse is over this column
|
||||
$targetColumn = $column;
|
||||
insertBefore = mouseX < columnMiddle;
|
||||
return false; // Break the loop
|
||||
}
|
||||
});
|
||||
|
||||
// If no column found under mouse, find the closest one
|
||||
if ($targetColumn.length === 0) {
|
||||
let closestDistance = Infinity;
|
||||
$allColumns.each((_, columnEl) => {
|
||||
const $column = $(columnEl);
|
||||
const rect = columnEl.getBoundingClientRect();
|
||||
const columnCenter = rect.left + rect.width / 2;
|
||||
const distance = Math.abs(mouseX - columnCenter);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
$targetColumn = $column;
|
||||
insertBefore = mouseX < columnCenter;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($targetColumn.length > 0) {
|
||||
const $dropIndicator = $("<div>").addClass("column-drop-indicator");
|
||||
|
||||
if (insertBefore) {
|
||||
$targetColumn.before($dropIndicator);
|
||||
} else {
|
||||
$targetColumn.after($dropIndicator);
|
||||
}
|
||||
|
||||
$dropIndicator.addClass("show");
|
||||
}
|
||||
}
|
||||
|
||||
private async handleColumnDrop() {
|
||||
console.log("handleColumnDrop called for:", this.context.draggedColumn);
|
||||
|
||||
if (!this.context.draggedColumn || !this.context.draggedColumnElement) {
|
||||
console.log("No dragged column or element found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Find the drop indicator to determine insert position
|
||||
const $dropIndicator = this.$container.find(".column-drop-indicator.show");
|
||||
console.log("Drop indicator found:", $dropIndicator.length > 0);
|
||||
|
||||
if ($dropIndicator.length > 0) {
|
||||
// Get current column order from the API (source of truth)
|
||||
const currentOrder = [...this.api.columns];
|
||||
|
||||
let newOrder = [...currentOrder];
|
||||
|
||||
// Remove dragged column from current position
|
||||
newOrder = newOrder.filter(col => col !== this.context.draggedColumn);
|
||||
|
||||
// Determine insertion position based on drop indicator position
|
||||
const $nextColumn = $dropIndicator.next('.board-column');
|
||||
const $prevColumn = $dropIndicator.prev('.board-column');
|
||||
|
||||
let insertIndex = -1;
|
||||
|
||||
if ($nextColumn.length > 0) {
|
||||
// Insert before the next column
|
||||
const nextColumnValue = $nextColumn.attr('data-column');
|
||||
if (nextColumnValue) {
|
||||
insertIndex = newOrder.indexOf(nextColumnValue);
|
||||
}
|
||||
} else if ($prevColumn.length > 0) {
|
||||
// Insert after the previous column
|
||||
const prevColumnValue = $prevColumn.attr('data-column');
|
||||
if (prevColumnValue) {
|
||||
insertIndex = newOrder.indexOf(prevColumnValue) + 1;
|
||||
}
|
||||
} else {
|
||||
// Insert at the beginning
|
||||
insertIndex = 0;
|
||||
}
|
||||
|
||||
// Insert the dragged column at the determined position
|
||||
if (insertIndex >= 0 && insertIndex <= newOrder.length) {
|
||||
newOrder.splice(insertIndex, 0, this.context.draggedColumn);
|
||||
} else {
|
||||
// Fallback: insert at the end
|
||||
newOrder.push(this.context.draggedColumn);
|
||||
}
|
||||
|
||||
// Update column order in API
|
||||
await this.api.reorderColumns(newOrder);
|
||||
} else {
|
||||
console.warn("No drop indicator found for column drop");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to reorder columns:", error);
|
||||
} finally {
|
||||
this.cleanupColumnDropIndicators();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface BoardColumnData {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface BoardData {
|
||||
columns?: BoardColumnData[];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user