mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 16:26:31 +01:00
Compare commits
1352 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a1ec266ad | ||
|
|
42fedaa241 | ||
|
|
4387bd4c6f | ||
|
|
51e1367b82 | ||
|
|
8bea3f4422 | ||
|
|
0eb2e405ff | ||
|
|
5dbd4a765f | ||
|
|
f6961c7e06 | ||
|
|
8d3ba90072 | ||
|
|
3772412d82 | ||
|
|
84389f467e | ||
|
|
eb41e0f96f | ||
|
|
2d44dff997 | ||
|
|
1483bf3d46 | ||
|
|
064cf6a3ee | ||
|
|
0c0d5eaa0a | ||
|
|
afecb33b5c | ||
|
|
fbb1e3a302 | ||
|
|
8704350359 | ||
|
|
d09e725d98 | ||
|
|
8be5b149c4 | ||
|
|
faeea6af18 | ||
|
|
3fa5ea1010 | ||
|
|
6aa31ae125 | ||
|
|
27f2e9c286 | ||
|
|
67cc36fdd2 | ||
|
|
ef7297e03b | ||
|
|
97a5314cdb | ||
|
|
a1195a2856 | ||
|
|
81419c6fe3 | ||
|
|
b8da793353 | ||
|
|
8140fa79cc | ||
|
|
abff4fe67d | ||
|
|
ec8f737eba | ||
|
|
cc6688ea00 | ||
|
|
c448b29be7 | ||
|
|
61bde294b3 | ||
|
|
acab81c61e | ||
|
|
1dd965973b | ||
|
|
d61981033f | ||
|
|
30197ba7ce | ||
|
|
1b6c957334 | ||
|
|
fb7a397bf9 | ||
|
|
133c9c5a7b | ||
|
|
8a587d4d21 | ||
|
|
29b813fa3b | ||
|
|
1dfe27d3df | ||
|
|
cda8fc7146 | ||
|
|
acb16f751b | ||
|
|
a1ac276be5 | ||
|
|
54e3ab5139 | ||
|
|
baf341b312 | ||
|
|
5b074c2e22 | ||
|
|
11d086ef12 | ||
|
|
0e6b10e400 | ||
|
|
0240222998 | ||
|
|
7fc739487f | ||
|
|
f6e275709f | ||
|
|
7e01dfd220 | ||
|
|
d5866a99ec | ||
|
|
5289d41b12 | ||
|
|
030178cad2 | ||
|
|
5d00630452 | ||
|
|
eb805bfa2a | ||
|
|
ee3a8e105e | ||
|
|
97fb273e7f | ||
|
|
2ef9009384 | ||
|
|
27c7888628 | ||
|
|
b4de37a9f4 | ||
|
|
1c5ebb54f8 | ||
|
|
f3e69dd6bd | ||
|
|
66364f5ce0 | ||
|
|
f25a1fb865 | ||
|
|
62c5b8b1fc | ||
|
|
2b0de37fc0 | ||
|
|
23ef73fe2f | ||
|
|
92ac3ee4ef | ||
|
|
a3ba5ca109 | ||
|
|
5b4e81cf18 | ||
|
|
772e6f5ebc | ||
|
|
60a9428b8b | ||
|
|
a7752a8421 | ||
|
|
aefa2315b7 | ||
|
|
37a79aeeab | ||
|
|
5bc4bdaeef | ||
|
|
5e28df883d | ||
|
|
0a57748075 | ||
|
|
45e3eee642 | ||
|
|
d724a80c2a | ||
|
|
5ea8c94d18 | ||
|
|
769bc760b3 | ||
|
|
f04f45ea62 | ||
|
|
a5cab6a2a2 | ||
|
|
138611beaf | ||
|
|
e1b608057a | ||
|
|
fed6d8329f | ||
|
|
9d03d52f28 | ||
|
|
055e11174d | ||
|
|
8fda2dd7f1 | ||
|
|
ea03695c75 | ||
|
|
17b206fc72 | ||
|
|
4ec8c5963a | ||
|
|
ab2d8accf5 | ||
|
|
de8b7e9ebe | ||
|
|
18d11523a6 | ||
|
|
7a0ab3c025 | ||
|
|
3575a7dc93 | ||
|
|
bb9e7b1c6e | ||
|
|
115e9e0202 | ||
|
|
e341de70c0 | ||
|
|
1d1a0ac4fd | ||
|
|
d48470ffb1 | ||
|
|
6574ca42a3 | ||
|
|
303ff35a76 | ||
|
|
e0850958b0 | ||
|
|
13115b9ed1 | ||
|
|
933a11e9db | ||
|
|
6915993a35 | ||
|
|
237a4e9a74 | ||
|
|
1565a0fd80 | ||
|
|
e8b16287e0 | ||
|
|
c09e124805 | ||
|
|
b6f55b0e1a | ||
|
|
964bc74b83 | ||
|
|
fa9b142cb7 | ||
|
|
7e3f412c84 | ||
|
|
82e16a5624 | ||
|
|
757488a95b | ||
|
|
d7f154cfd1 | ||
|
|
3517715aab | ||
|
|
d10bbdd7a7 | ||
|
|
c4ec27bb1e | ||
|
|
0b24553ace | ||
|
|
793867269b | ||
|
|
9508e92676 | ||
|
|
89378eae7b | ||
|
|
ace166a925 | ||
|
|
d59d544c0f | ||
|
|
37461d0eb3 | ||
|
|
126152ff63 | ||
|
|
60e19de0d1 | ||
|
|
3247a9facc | ||
|
|
7b114bed26 | ||
|
|
30ffbc760e | ||
|
|
4420913049 | ||
|
|
3762690c5f | ||
|
|
d684ac40d8 | ||
|
|
d217379644 | ||
|
|
d5f7fa2fe5 | ||
|
|
3e0ef10b25 | ||
|
|
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 | ||
|
|
57649d47ec | ||
|
|
089ae04542 | ||
|
|
441ae3e25b | ||
|
|
1016f98867 | ||
|
|
1f022aea4e | ||
|
|
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 | ||
|
|
06b507fdc5 | ||
|
|
256ffe39f2 | ||
|
|
6e89a232e6 | ||
|
|
45a3fb15e6 | ||
|
|
4139a401e6 | ||
|
|
4d617ccdb3 | ||
|
|
57039ae8f2 | ||
|
|
9a4dcda985 | ||
|
|
c13ae40ea4 | ||
|
|
0b963db405 | ||
|
|
78b946b208 | ||
|
|
4e8ddef915 | ||
|
|
0aa08cd297 | ||
|
|
a89ce5d931 | ||
|
|
fe3350f39f | ||
|
|
f32f9d4326 | ||
|
|
2ed24e8f9c | ||
|
|
537c282156 | ||
|
|
61f253787a | ||
|
|
ddad800771 | ||
|
|
69c24ef806 | ||
|
|
df7fe2bf13 | ||
|
|
c47d3514be | ||
|
|
2b979f46ab | ||
|
|
edc47eba05 | ||
|
|
f3b46d6bc7 | ||
|
|
69d6d6a4fd | ||
|
|
5916b7d2a3 | ||
|
|
fbb2c67edb | ||
|
|
7e1712712e | ||
|
|
aa846e9703 | ||
|
|
6ed9ec0851 | ||
|
|
3a0a79119d | ||
|
|
ecd3b7039f | ||
|
|
4a22e3d2d4 | ||
|
|
dcb4ebe5d9 | ||
|
|
dd379bf18d | ||
|
|
c9b556160f | ||
|
|
168e224d3e | ||
|
|
9e57c14130 | ||
|
|
9c137a1c48 | ||
|
|
ccb9b7e5fb | ||
|
|
c7b16cd043 | ||
|
|
7e20e41521 | ||
|
|
66761a69d3 | ||
|
|
fb32d26479 | ||
|
|
b6398fdb5d | ||
|
|
d9443527ee | ||
|
|
7c175da9f1 | ||
|
|
05aa087851 | ||
|
|
592e968f9f | ||
|
|
894a26cc67 | ||
|
|
1b5dd4638d | ||
|
|
a19186c508 | ||
|
|
5450bdeae9 | ||
|
|
fcd71957ff | ||
|
|
1ff7228ca5 | ||
|
|
f0f79b65e2 | ||
|
|
63610fd579 | ||
|
|
eab84bd34e | ||
|
|
72f4c40a8a | ||
|
|
482356206b | ||
|
|
5aa09fd881 | ||
|
|
81f50c46ed | ||
|
|
938dfe7f38 | ||
|
|
ea71439f27 | ||
|
|
9dc157b970 | ||
|
|
b7f5c0e07a | ||
|
|
3988bb5321 | ||
|
|
ba94616b87 | ||
|
|
1ac086b88e | ||
|
|
3b4efef4f0 | ||
|
|
9dc0807f78 | ||
|
|
22783662c5 | ||
|
|
64086592ef | ||
|
|
2bebed1b0f | ||
|
|
2262d300f7 | ||
|
|
dba98d6fc1 | ||
|
|
d8b85aad7c | ||
|
|
5a3503c577 | ||
|
|
18c96cd2bc | ||
|
|
b1bb1ed9af | ||
|
|
bc1ea98827 | ||
|
|
b984d612fd | ||
|
|
f53eecbf88 | ||
|
|
1a2c2dbfb8 | ||
|
|
78a0637283 | ||
|
|
e01858c86b | ||
|
|
ee080638be | ||
|
|
6db7ef415d | ||
|
|
cf171ff0b1 | ||
|
|
fdddc29092 | ||
|
|
d6545e1280 | ||
|
|
759fe04e37 | ||
|
|
e0c5f15480 | ||
|
|
f529cfd928 | ||
|
|
2a1a099dc9 | ||
|
|
26d0ba04ab | ||
|
|
f85ef444f4 | ||
|
|
87aa4422c0 | ||
|
|
f497b71d37 | ||
|
|
24ba17b3f3 | ||
|
|
e8cbce855e | ||
|
|
f97bd77117 | ||
|
|
962066fd60 | ||
|
|
ff917e93c9 | ||
|
|
a7091779b7 | ||
|
|
520b862551 | ||
|
|
5fbd34c7c1 | ||
|
|
c827b20e26 | ||
|
|
a150047432 | ||
|
|
4f6729857b | ||
|
|
ce305f0f45 | ||
|
|
3a50ffede1 | ||
|
|
41e88614d7 | ||
|
|
002c567ae9 | ||
|
|
a9c07af402 | ||
|
|
3048c39877 | ||
|
|
a95dc14d24 | ||
|
|
24c82fa7b6 | ||
|
|
02a0624e8f | ||
|
|
e6e276a0cf | ||
|
|
8d90231f76 | ||
|
|
4a40b22c9a | ||
|
|
79b3b92ec9 | ||
|
|
ae1a4fbbf6 | ||
|
|
3190aa6fe6 | ||
|
|
1c3cd9e7ca | ||
|
|
c61713333d | ||
|
|
a861defbee | ||
|
|
ea0b570910 | ||
|
|
8d46ab3806 | ||
|
|
058b3f8241 | ||
|
|
57a688b6b6 | ||
|
|
ad18d853e2 | ||
|
|
8f5be936a7 | ||
|
|
f2c02f869e | ||
|
|
cf28777119 | ||
|
|
cca8504796 | ||
|
|
09391a92e5 | ||
|
|
50db8ef9c3 | ||
|
|
8ab21f3bab | ||
|
|
efdf79feaa | ||
|
|
9418055b69 | ||
|
|
5070633257 | ||
|
|
764917562e | ||
|
|
54290a1373 | ||
|
|
0325bee425 | ||
|
|
e280968271 | ||
|
|
b809137c93 | ||
|
|
248f6d6a7d | ||
|
|
0744a85421 | ||
|
|
e5a6f53f98 | ||
|
|
751de1d43c | ||
|
|
2704b1546b | ||
|
|
acd68817e9 | ||
|
|
8b841c5aa7 | ||
|
|
a37af29c6c | ||
|
|
ade0d4bb1a | ||
|
|
d79c491777 | ||
|
|
f9cf542e66 | ||
|
|
609ed6274e | ||
|
|
7faaefee20 | ||
|
|
578310a1c0 | ||
|
|
93f544a221 | ||
|
|
9c80ab22af | ||
|
|
7decbc34c7 | ||
|
|
e606276f19 | ||
|
|
a78e4d7a58 | ||
|
|
426c9a377a | ||
|
|
d2a3ae2a6f | ||
|
|
fbd6b7d22b | ||
|
|
a42375931f | ||
|
|
d894bff2a0 | ||
|
|
4515ed600c | ||
|
|
4df3cdd975 | ||
|
|
5fc0a0460d | ||
|
|
74851a859b | ||
|
|
9bb94513ca | ||
|
|
bb74aa7976 | ||
|
|
12318b38ee | ||
|
|
bb70c2a3fa | ||
|
|
b47180a219 | ||
|
|
4eb64357a1 | ||
|
|
0fe89115d1 | ||
|
|
330932adde | ||
|
|
b9daca5b9c | ||
|
|
c6c48d84ab | ||
|
|
2c87721953 | ||
|
|
45a446d0f6 | ||
|
|
40086434ec | ||
|
|
bde3f0a55c | ||
|
|
9adae105e2 | ||
|
|
60dbf9dd67 | ||
|
|
374309a40c | ||
|
|
dcccb5ad30 | ||
|
|
47eaee8b70 | ||
|
|
9687a9d8ff | ||
|
|
fa11295693 | ||
|
|
7e399cc10c | ||
|
|
59e0857bb5 | ||
|
|
4f9bd970af | ||
|
|
3e40a35c19 | ||
|
|
97799bfacc | ||
|
|
fb1a74a96d | ||
|
|
9f82e0a6d6 | ||
|
|
421e125882 | ||
|
|
17ede00fb2 | ||
|
|
502638bae7 | ||
|
|
af8a905150 | ||
|
|
6c2a228267 | ||
|
|
f9ecfd1ad0 | ||
|
|
2c3cbcb1f9 | ||
|
|
d1583ca091 | ||
|
|
3c21d97a8a | ||
|
|
8f7468cd60 | ||
|
|
6668e639d5 | ||
|
|
bcc689cae3 | ||
|
|
20173d544b | ||
|
|
e90d4cf86f | ||
|
|
1de02b85b3 | ||
|
|
dab43d9372 | ||
|
|
3b579a3b7b | ||
|
|
a4a5e0bdf0 | ||
|
|
9cb227c1ca | ||
|
|
c4c7321f60 | ||
|
|
cce27900b8 | ||
|
|
32f4d7be39 | ||
|
|
bbeb4e25f3 | ||
|
|
88f78f3e32 | ||
|
|
36581ba882 | ||
|
|
1a64b3ce8e | ||
|
|
32ee75ea43 | ||
|
|
06ebe0a9b3 | ||
|
|
6069518749 | ||
|
|
c09a9aa7d3 | ||
|
|
13afcb8a49 | ||
|
|
9bfff03cff | ||
|
|
8780debc90 | ||
|
|
6f386f50ff | ||
|
|
43eff08004 | ||
|
|
3a0b616800 | ||
|
|
cb65d02dc6 | ||
|
|
bef485c676 | ||
|
|
9e4d350848 | ||
|
|
ccc7a8010e | ||
|
|
7269c46de3 | ||
|
|
178df38377 | ||
|
|
1201f7138a | ||
|
|
9ff4be8871 | ||
|
|
0a17fb586c | ||
|
|
1f8713f57e | ||
|
|
507fc17701 | ||
|
|
26ad428b24 | ||
|
|
26af3a9d68 | ||
|
|
8ecf6ad78e | ||
|
|
45977efd5f | ||
|
|
c0b746e03f | ||
|
|
a13e4d5d79 | ||
|
|
e8f2f0f577 | ||
|
|
f56ba7295a | ||
|
|
8cdb1859e3 | ||
|
|
6bfc78e148 | ||
|
|
cc97ec332d | ||
|
|
f2e4dad318 | ||
|
|
9c45e9fa1a | ||
|
|
e1e2afc3cd | ||
|
|
8349204982 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -2,3 +2,5 @@
|
|||||||
|
|
||||||
github: [eliandoran]
|
github: [eliandoran]
|
||||||
custom: ["https://paypal.me/eliandoran"]
|
custom: ["https://paypal.me/eliandoran"]
|
||||||
|
liberapay: ElianDoran
|
||||||
|
buy_me_a_coffee: eliandoran
|
||||||
|
|||||||
2
.github/actions/build-electron/action.yml
vendored
2
.github/actions/build-electron/action.yml
vendored
@@ -85,7 +85,7 @@ runs:
|
|||||||
APPLE_ID: ${{ env.APPLE_ID }}
|
APPLE_ID: ${{ env.APPLE_ID }}
|
||||||
APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }}
|
APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }}
|
||||||
WINDOWS_SIGN_EXECUTABLE: ${{ env.WINDOWS_SIGN_EXECUTABLE }}
|
WINDOWS_SIGN_EXECUTABLE: ${{ env.WINDOWS_SIGN_EXECUTABLE }}
|
||||||
TRILIUM_ARTIFACT_NAME_HINT: TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}
|
TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}
|
||||||
run: pnpm nx --project=desktop electron-forge:make -- --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
|
run: pnpm nx --project=desktop electron-forge:make -- --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
|
||||||
|
|
||||||
# Add DMG signing step
|
# Add DMG signing step
|
||||||
|
|||||||
2
.github/actions/build-server/action.yml
vendored
2
.github/actions/build-server/action.yml
vendored
@@ -30,4 +30,4 @@ runs:
|
|||||||
mkdir -p upload
|
mkdir -p upload
|
||||||
file=$(find ./apps/server/out -name '*.tar.xz' -print -quit)
|
file=$(find ./apps/server/out -name '*.tar.xz' -print -quit)
|
||||||
name=${{ github.ref_name }}
|
name=${{ github.ref_name }}
|
||||||
cp "$file" "upload/TriliumNextNotes-Server-${name//\//-}-${{ inputs.os }}-${{ inputs.arch }}.tar.xz"
|
cp "$file" "upload/TriliumNotes-Server-${name//\//-}-${{ inputs.os }}-${{ inputs.arch }}.tar.xz"
|
||||||
|
|||||||
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.9 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
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -13,9 +13,9 @@ name: "CodeQL Advanced"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "develop" ]
|
branches: [ "main" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "develop" ]
|
branches: [ "main" ]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '20 7 * * 0'
|
- cron: '20 7 * * 0'
|
||||||
|
|
||||||
|
|||||||
9
.github/workflows/dev.yml
vendored
9
.github/workflows/dev.yml
vendored
@@ -1,9 +1,9 @@
|
|||||||
name: Dev
|
name: Dev
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ develop ]
|
branches: [ main ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ develop ]
|
branches: [ main ]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -12,8 +12,8 @@ concurrency:
|
|||||||
env:
|
env:
|
||||||
GHCR_REGISTRY: ghcr.io
|
GHCR_REGISTRY: ghcr.io
|
||||||
DOCKERHUB_REGISTRY: docker.io
|
DOCKERHUB_REGISTRY: docker.io
|
||||||
IMAGE_NAME: ${{ github.repository_owner }}/notes
|
IMAGE_NAME: ${{ github.repository}}
|
||||||
TEST_TAG: ${{ github.repository_owner }}/notes:test
|
TEST_TAG: ${{ github.repository}}:test
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write # for PR comments
|
pull-requests: write # for PR comments
|
||||||
@@ -77,6 +77,7 @@ jobs:
|
|||||||
- name: Trigger client build
|
- name: Trigger client build
|
||||||
run: pnpm nx run client:build
|
run: pnpm nx run client:build
|
||||||
- name: Send client bundle stats to RelativeCI
|
- name: Send client bundle stats to RelativeCI
|
||||||
|
if: false
|
||||||
uses: relative-ci/agent-action@v3
|
uses: relative-ci/agent-action@v3
|
||||||
with:
|
with:
|
||||||
webpackStatsFile: ./apps/client/dist/webpack-stats.json
|
webpackStatsFile: ./apps/client/dist/webpack-stats.json
|
||||||
|
|||||||
14
.github/workflows/main-docker.yml
vendored
14
.github/workflows/main-docker.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "develop"
|
- "main"
|
||||||
- "feature/update**"
|
- "feature/update**"
|
||||||
- "feature/server_esm**"
|
- "feature/server_esm**"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
@@ -14,8 +14,8 @@ on:
|
|||||||
env:
|
env:
|
||||||
GHCR_REGISTRY: ghcr.io
|
GHCR_REGISTRY: ghcr.io
|
||||||
DOCKERHUB_REGISTRY: docker.io
|
DOCKERHUB_REGISTRY: docker.io
|
||||||
IMAGE_NAME: ${{ github.repository_owner }}/notes
|
IMAGE_NAME: ${{ github.repository}}
|
||||||
TEST_TAG: ${{ github.repository_owner }}/notes:test
|
TEST_TAG: ${{ github.repository}}:test
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -83,6 +83,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e
|
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e
|
||||||
|
|
||||||
|
- name: Upload Playwright trace
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Playwright trace (${{ matrix.dockerfile }})
|
||||||
|
path: test-output/playwright/output
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -12,7 +12,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- .github/actions/build-electron/*
|
- .github/actions/build-electron/*
|
||||||
- .github/workflows/nightly.yml
|
- .github/workflows/nightly.yml
|
||||||
- forge.config.cjs
|
- forge.config.ts
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}
|
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
|
||||||
path: apps/desktop/upload
|
path: apps/desktop/upload
|
||||||
|
|
||||||
nightly-server:
|
nightly-server:
|
||||||
|
|||||||
4
.github/workflows/playwright.yml
vendored
4
.github/workflows/playwright.yml
vendored
@@ -3,7 +3,7 @@ name: playwright
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -40,4 +40,4 @@ jobs:
|
|||||||
# - run: npx nx-cloud record -- echo Hello World
|
# - run: npx nx-cloud record -- echo Hello World
|
||||||
# Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
|
# Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
|
||||||
# When you enable task distribution, run the e2e-ci task instead of e2e
|
# When you enable task distribution, run the e2e-ci task instead of e2e
|
||||||
- run: pnpm exec nx affected -t e2e
|
- run: pnpm exec nx affected -t e2e --exclude desktop-e2e
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
forge_platform: darwin
|
forge_platform: darwin
|
||||||
- name: linux
|
- name: linux
|
||||||
image: ubuntu-latest
|
image: ubuntu-22.04
|
||||||
shell: bash
|
shell: bash
|
||||||
forge_platform: linux
|
forge_platform: linux
|
||||||
- name: windows
|
- name: windows
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
files: upload/*.*
|
files: upload/*.*
|
||||||
discussion_category_name: Announcements
|
discussion_category_name: Releases
|
||||||
make_latest: ${{ !contains(github.ref, 'rc') }}
|
make_latest: ${{ !contains(github.ref, 'rc') }}
|
||||||
prerelease: ${{ contains(github.ref, 'rc') }}
|
prerelease: ${{ contains(github.ref, 'rc') }}
|
||||||
token: ${{ secrets.RELEASE_PAT }}
|
token: ${{ secrets.RELEASE_PAT }}
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,11 +1,5 @@
|
|||||||
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
# Workaround for Nx bug: parent .gitignore files with '*' can cause
|
|
||||||
# `nx show projects` to return nothing by ignoring subprojects.
|
|
||||||
# See: https://github.com/nrwl/nx/issues/27368
|
|
||||||
# Unignore everything to ensure Nx detects all projects
|
|
||||||
!*
|
|
||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
dist
|
dist
|
||||||
tmp
|
tmp
|
||||||
|
|||||||
4
.mailmap
4
.mailmap
@@ -1,2 +1,2 @@
|
|||||||
Adam Zivner <adam.zivner@gmail.com>
|
zadam <adam.zivner@gmail.com>
|
||||||
Adam Zivner <zadam.apps@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.validate.enable": true,
|
||||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"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
|
||||||
34
README.md
34
README.md
@@ -1,13 +1,13 @@
|
|||||||
# TriliumNext Notes
|
# Trilium Notes
|
||||||
|
|
||||||

|
 
|
||||||

|

|
||||||

|

|
||||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
|
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/)
|
||||||
|
|
||||||
[English](./README.md) | [Chinese](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
|
[English](./README.md) | [Chinese](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
|
||||||
|
|
||||||
TriliumNext Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
|
Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
|
||||||
|
|
||||||
See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview:
|
See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview:
|
||||||
|
|
||||||
@@ -115,12 +115,20 @@ To install TriliumNext on your own server (including via Docker from [Dockerhub]
|
|||||||
|
|
||||||
## 💻 Contribute
|
## 💻 Contribute
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
If you are a native speaker, help us translate Trilium by heading over to our [Weblate page](https://hosted.weblate.org/engage/trilium/).
|
||||||
|
|
||||||
|
Here's the language coverage we have so far:
|
||||||
|
|
||||||
|
[](https://hosted.weblate.org/engage/trilium/)
|
||||||
|
|
||||||
### Code
|
### Code
|
||||||
|
|
||||||
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
|
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/TriliumNext/Notes.git
|
git clone https://github.com/TriliumNext/Trilium.git
|
||||||
cd Notes
|
cd Trilium
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run server:start
|
pnpm run server:start
|
||||||
```
|
```
|
||||||
@@ -129,8 +137,8 @@ pnpm run server:start
|
|||||||
|
|
||||||
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
|
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/TriliumNext/Notes.git
|
git clone https://github.com/TriliumNext/Trilium.git
|
||||||
cd Notes
|
cd Trilium
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm nx run edit-docs:edit-docs
|
pnpm nx run edit-docs:edit-docs
|
||||||
```
|
```
|
||||||
@@ -138,8 +146,8 @@ pnpm nx run edit-docs:edit-docs
|
|||||||
### Building the Executable
|
### Building the Executable
|
||||||
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
|
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/TriliumNext/Notes.git
|
git clone https://github.com/TriliumNext/Trilium.git
|
||||||
cd Notes
|
cd Trilium
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
|
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
|
||||||
```
|
```
|
||||||
@@ -153,7 +161,7 @@ Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide
|
|||||||
## 👏 Shoutouts
|
## 👏 Shoutouts
|
||||||
|
|
||||||
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team
|
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team
|
||||||
* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. TriliumNext Notes would not be the same without it.
|
* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it.
|
||||||
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
|
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
|
||||||
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
|
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
|
||||||
|
|
||||||
|
|||||||
@@ -91,5 +91,5 @@ async function start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @TriliumNextTODO sqlInit.dbReady never seems to resolve so program hangs
|
// @TriliumNextTODO sqlInit.dbReady never seems to resolve so program hangs
|
||||||
// see https://github.com/TriliumNext/Notes/issues/1020
|
// see https://github.com/TriliumNext/Trilium/issues/1020
|
||||||
sqlInit.dbReady.then(cls.wrap(start)).catch((err) => console.error(err));
|
sqlInit.dbReady.then(cls.wrap(start)).catch((err) => console.error(err));
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ if ! git diff-index --quiet HEAD --; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
BASE_BRANCH=master
|
BASE_BRANCH=main
|
||||||
|
|
||||||
if [[ "$VERSION" == *"beta"* ]]; then
|
if [[ "$VERSION" == *"beta"* ]]; then
|
||||||
BASE_BRANCH=beta
|
BASE_BRANCH=beta
|
||||||
|
|||||||
@@ -47,11 +47,3 @@ echo "Tagging commit with $TAG"
|
|||||||
|
|
||||||
git tag $TAG
|
git tag $TAG
|
||||||
git push origin $TAG
|
git push origin $TAG
|
||||||
|
|
||||||
echo "Updating master"
|
|
||||||
|
|
||||||
git fetch
|
|
||||||
git checkout master
|
|
||||||
git reset --hard origin/master
|
|
||||||
git merge origin/develop
|
|
||||||
git push
|
|
||||||
@@ -25,15 +25,16 @@ stats() {
|
|||||||
# Print the number of existing strings on the JSON files for each locale
|
# Print the number of existing strings on the JSON files for each locale
|
||||||
s=$(number_of_keys "${paths[0]}/en/server.json")
|
s=$(number_of_keys "${paths[0]}/en/server.json")
|
||||||
c=$(number_of_keys "${paths[1]}/en/translation.json")
|
c=$(number_of_keys "${paths[1]}/en/translation.json")
|
||||||
echo "| locale |server strings |client strings |"
|
echo "| locale | server strings | client strings |"
|
||||||
echo "|--------|---------------|---------------|"
|
echo "|--------|----------------|----------------|"
|
||||||
echo "| en | ${s} | ${c} |"
|
echo "| en | ${s} | ${c} |"
|
||||||
|
echo "|--------|----------------|----------------|"
|
||||||
for locale in "${locales[@]}"; do
|
for locale in "${locales[@]}"; do
|
||||||
s=$(number_of_keys "${paths[0]}/${locale}/server.json")
|
s=$(number_of_keys "${paths[0]}/${locale}/server.json")
|
||||||
c=$(number_of_keys "${paths[1]}/${locale}/translation.json")
|
c=$(number_of_keys "${paths[1]}/${locale}/translation.json")
|
||||||
n1=$(((8 - ${#locale}) / 2))
|
n1=$(((8 - ${#locale}) / 2))
|
||||||
n2=$((n1 == 1 ? n1 + 1 : n1))
|
n2=$((n1 == 1 ? n1 + 1 : n1))
|
||||||
echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")| ${s} | ${c} |"
|
echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")| ${s} | ${c} |"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +79,10 @@ file_path="$(
|
|||||||
cd -- "$(dirname "${0}")" >/dev/null 2>&1 || exit
|
cd -- "$(dirname "${0}")" >/dev/null 2>&1 || exit
|
||||||
pwd -P
|
pwd -P
|
||||||
)"
|
)"
|
||||||
paths=("${file_path}/../translations/" "${file_path}/../src/public/translations/")
|
paths=(
|
||||||
|
"${file_path}/../../apps/server/src/assets/translations/"
|
||||||
|
"${file_path}/../../apps/client/src/translations/"
|
||||||
|
)
|
||||||
locales=(cn de es fr pt_br ro tw)
|
locales=(cn de es fr pt_br ro tw)
|
||||||
|
|
||||||
if [ $# -eq 1 ]; then
|
if [ $# -eq 1 ]; then
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ test("Displays update badge when there is a version available", async ({ page })
|
|||||||
await page.getByText(`Version ${expectedVersion} is available,`).click();
|
await page.getByText(`Version ${expectedVersion} is available,`).click();
|
||||||
|
|
||||||
const page1 = await page.waitForEvent("popup");
|
const page1 = await page.waitForEvent("popup");
|
||||||
expect(page1.url()).toBe(`https://github.com/TriliumNext/Notes/releases/tag/v${expectedVersion}`);
|
expect(page1.url()).toBe(`https://github.com/TriliumNext/Trilium/releases/tag/v${expectedVersion}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,13 +35,13 @@
|
|||||||
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.53.0",
|
"@playwright/test": "1.54.2",
|
||||||
"@stylistic/eslint-plugin": "4.4.1",
|
"@stylistic/eslint-plugin": "5.2.2",
|
||||||
"@types/express": "5.0.3",
|
"@types/express": "5.0.3",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.17.0",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"@vitest/coverage-v8": "3.2.3",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"eslint": "9.29.0",
|
"eslint": "9.32.0",
|
||||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
"jsdoc": "4.0.4",
|
"jsdoc": "4.0.4",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"rcedit": "4.0.1",
|
"rcedit": "4.0.1",
|
||||||
"rimraf": "6.0.1",
|
"rimraf": "6.0.1",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"typedoc": "0.28.5",
|
"typedoc": "0.28.9",
|
||||||
"typedoc-plugin-missing-exports": "4.0.0"
|
"typedoc-plugin-missing-exports": "4.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
5
apps/client/.env
Normal file
5
apps/client/.env
Normal file
@@ -0,0 +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_ENABLE_INSPECTOR=false
|
||||||
1
apps/client/.env.production
Normal file
1
apps/client/.env.production
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_CKEDITOR_ENABLE_INSPECTOR=false
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "@triliumnext/client",
|
"name": "@triliumnext/client",
|
||||||
"version": "0.95.0",
|
"version": "0.97.2",
|
||||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "TriliumNext Notes Team",
|
"name": "Trilium Notes Team",
|
||||||
"email": "contact@eliandoran.me",
|
"email": "contact@eliandoran.me",
|
||||||
"url": "https://github.com/TriliumNext/Notes"
|
"url": "https://github.com/TriliumNext/Notes"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/js": "9.29.0",
|
"@eslint/js": "9.32.0",
|
||||||
"@excalidraw/excalidraw": "0.18.0",
|
"@excalidraw/excalidraw": "0.18.0",
|
||||||
"@fullcalendar/core": "6.1.17",
|
"@fullcalendar/core": "6.1.18",
|
||||||
"@fullcalendar/daygrid": "6.1.17",
|
"@fullcalendar/daygrid": "6.1.18",
|
||||||
"@fullcalendar/interaction": "6.1.17",
|
"@fullcalendar/interaction": "6.1.18",
|
||||||
"@fullcalendar/list": "6.1.17",
|
"@fullcalendar/list": "6.1.18",
|
||||||
"@fullcalendar/multimonth": "6.1.17",
|
"@fullcalendar/multimonth": "6.1.18",
|
||||||
"@fullcalendar/timegrid": "6.1.17",
|
"@fullcalendar/timegrid": "6.1.18",
|
||||||
"@mermaid-js/layout-elk": "0.1.7",
|
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
||||||
"@mind-elixir/node-menu": "1.0.5",
|
"@mermaid-js/layout-elk": "0.1.8",
|
||||||
|
"@mind-elixir/node-menu": "5.0.0",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
"@triliumnext/ckeditor5": "workspace:*",
|
"@triliumnext/ckeditor5": "workspace:*",
|
||||||
"@triliumnext/codemirror": "workspace:*",
|
"@triliumnext/codemirror": "workspace:*",
|
||||||
@@ -27,18 +28,17 @@
|
|||||||
"@triliumnext/highlightjs": "workspace:*",
|
"@triliumnext/highlightjs": "workspace:*",
|
||||||
"@triliumnext/share-theme": "workspace:*",
|
"@triliumnext/share-theme": "workspace:*",
|
||||||
"autocomplete.js": "0.38.1",
|
"autocomplete.js": "0.38.1",
|
||||||
"bootstrap": "5.3.6",
|
"bootstrap": "5.3.7",
|
||||||
"boxicons": "2.1.4",
|
"boxicons": "2.1.4",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"dayjs-plugin-utc": "0.1.2",
|
"dayjs-plugin-utc": "0.1.2",
|
||||||
"debounce": "2.2.0",
|
"debounce": "2.2.0",
|
||||||
"draggabilly": "3.0.0",
|
"draggabilly": "3.0.0",
|
||||||
"force-graph": "1.49.6",
|
"force-graph": "1.50.1",
|
||||||
"globals": "16.2.0",
|
"globals": "16.3.0",
|
||||||
"i18next": "25.2.1",
|
"i18next": "25.3.2",
|
||||||
"i18next-http-backend": "3.0.2",
|
"i18next-http-backend": "3.0.2",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery-hotkeys": "0.2.2",
|
|
||||||
"jquery.fancytree": "2.38.5",
|
"jquery.fancytree": "2.38.5",
|
||||||
"jsplumb": "2.15.6",
|
"jsplumb": "2.15.6",
|
||||||
"katex": "0.16.22",
|
"katex": "0.16.22",
|
||||||
@@ -46,27 +46,29 @@
|
|||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-gpx": "2.2.0",
|
"leaflet-gpx": "2.2.0",
|
||||||
"mark.js": "8.11.1",
|
"mark.js": "8.11.1",
|
||||||
"marked": "15.0.12",
|
"marked": "16.1.1",
|
||||||
"mermaid": "11.6.0",
|
"mermaid": "11.9.0",
|
||||||
"mind-elixir": "4.6.0",
|
"mind-elixir": "5.0.4",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.26.9",
|
"preact": "10.27.0",
|
||||||
"split.js": "1.6.5",
|
"split.js": "1.6.5",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
|
"tabulator-tables": "6.3.1",
|
||||||
"vanilla-js-wheel-zoom": "9.0.4"
|
"vanilla-js-wheel-zoom": "9.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ckeditor/ckeditor5-inspector": "4.1.0",
|
"@ckeditor/ckeditor5-inspector": "5.0.0",
|
||||||
"@types/bootstrap": "5.2.10",
|
"@types/bootstrap": "5.2.10",
|
||||||
"@types/jquery": "3.5.32",
|
"@types/jquery": "3.5.32",
|
||||||
"@types/leaflet": "1.9.18",
|
"@types/leaflet": "1.9.20",
|
||||||
"@types/leaflet-gpx": "1.3.7",
|
"@types/leaflet-gpx": "1.3.7",
|
||||||
"@types/mark.js": "8.11.12",
|
"@types/mark.js": "8.11.12",
|
||||||
|
"@types/tabulator-tables": "6.2.9",
|
||||||
"copy-webpack-plugin": "13.0.0",
|
"copy-webpack-plugin": "13.0.0",
|
||||||
"happy-dom": "18.0.1",
|
"happy-dom": "18.0.1",
|
||||||
"script-loader": "0.7.2",
|
"script-loader": "0.7.2",
|
||||||
"vite-plugin-static-copy": "3.0.2"
|
"vite-plugin-static-copy": "3.1.1"
|
||||||
},
|
},
|
||||||
"nx": {
|
"nx": {
|
||||||
"name": "client",
|
"name": "client",
|
||||||
@@ -75,6 +77,9 @@
|
|||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
"^build"
|
"^build"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"circular-deps": {
|
||||||
|
"command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,424 +0,0 @@
|
|||||||
/*
|
|
||||||
* Remove template code below
|
|
||||||
*/
|
|
||||||
html {
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
||||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
|
||||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
|
||||||
'Noto Color Emoji';
|
|
||||||
line-height: 1.5;
|
|
||||||
tab-size: 4;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
font-family: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
p,
|
|
||||||
pre {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
*,
|
|
||||||
::before,
|
|
||||||
::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-width: 0;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: currentColor;
|
|
||||||
}
|
|
||||||
h1,
|
|
||||||
h2 {
|
|
||||||
font-size: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
|
||||||
'Liberation Mono', 'Courier New', monospace;
|
|
||||||
}
|
|
||||||
svg {
|
|
||||||
display: block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
shape-rendering: auto;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
background-color: rgba(55, 65, 81, 1);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
color: rgba(229, 231, 235, 1);
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
|
||||||
'Liberation Mono', 'Courier New', monospace;
|
|
||||||
overflow: scroll;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow {
|
|
||||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
||||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
.rounded {
|
|
||||||
border-radius: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
max-width: 768px;
|
|
||||||
padding-bottom: 3rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
color: rgba(55, 65, 81, 1);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#welcome {
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
}
|
|
||||||
#welcome h1 {
|
|
||||||
font-size: 3rem;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
#welcome span {
|
|
||||||
display: block;
|
|
||||||
font-size: 1.875rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 2.25rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
#hero {
|
|
||||||
align-items: center;
|
|
||||||
background-color: hsla(214, 62%, 21%, 1);
|
|
||||||
border: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: rgba(55, 65, 81, 1);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
margin-top: 3.5rem;
|
|
||||||
}
|
|
||||||
#hero .text-container {
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
padding: 3rem 2rem;
|
|
||||||
}
|
|
||||||
#hero .text-container h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 2rem;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
#hero .text-container h2 svg {
|
|
||||||
color: hsla(162, 47%, 50%, 1);
|
|
||||||
height: 2rem;
|
|
||||||
left: -0.25rem;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 2rem;
|
|
||||||
}
|
|
||||||
#hero .text-container h2 span {
|
|
||||||
margin-left: 2.5rem;
|
|
||||||
}
|
|
||||||
#hero .text-container a {
|
|
||||||
background-color: rgba(255, 255, 255, 1);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
color: rgba(55, 65, 81, 1);
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
#hero .logo-container {
|
|
||||||
display: none;
|
|
||||||
justify-content: center;
|
|
||||||
padding-left: 2rem;
|
|
||||||
padding-right: 2rem;
|
|
||||||
}
|
|
||||||
#hero .logo-container svg {
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
width: 66.666667%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#middle-content {
|
|
||||||
align-items: flex-start;
|
|
||||||
display: grid;
|
|
||||||
gap: 4rem;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
margin-top: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#learning-materials {
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
}
|
|
||||||
#learning-materials h2 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
.list-item-link {
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.list-item-link svg:first-child {
|
|
||||||
margin-right: 1rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
width: 1.5rem;
|
|
||||||
}
|
|
||||||
.list-item-link > span {
|
|
||||||
flex-grow: 1;
|
|
||||||
font-weight: 400;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
}
|
|
||||||
.list-item-link > span > span {
|
|
||||||
color: rgba(107, 114, 128, 1);
|
|
||||||
display: block;
|
|
||||||
flex-grow: 1;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 1rem;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
}
|
|
||||||
.list-item-link svg:last-child {
|
|
||||||
height: 1rem;
|
|
||||||
transition-property: all;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
width: 1rem;
|
|
||||||
}
|
|
||||||
.list-item-link:hover {
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
background-color: hsla(162, 47%, 50%, 1);
|
|
||||||
}
|
|
||||||
.list-item-link:hover > span {
|
|
||||||
}
|
|
||||||
.list-item-link:hover > span > span {
|
|
||||||
color: rgba(243, 244, 246, 1);
|
|
||||||
}
|
|
||||||
.list-item-link:hover svg:last-child {
|
|
||||||
transform: translateX(0.25rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
#other-links {
|
|
||||||
}
|
|
||||||
.button-pill {
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
transition-duration: 300ms;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.button-pill svg {
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 3rem;
|
|
||||||
}
|
|
||||||
.button-pill > span {
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
.button-pill span span {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
}
|
|
||||||
.button-pill:hover svg,
|
|
||||||
.button-pill:hover {
|
|
||||||
color: rgba(255, 255, 255, 1) !important;
|
|
||||||
}
|
|
||||||
#nx-console:hover {
|
|
||||||
background-color: rgba(0, 122, 204, 1);
|
|
||||||
}
|
|
||||||
#nx-console svg {
|
|
||||||
color: rgba(0, 122, 204, 1);
|
|
||||||
}
|
|
||||||
#nx-console-jetbrains {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
#nx-console-jetbrains:hover {
|
|
||||||
background-color: rgba(255, 49, 140, 1);
|
|
||||||
}
|
|
||||||
#nx-console-jetbrains svg {
|
|
||||||
color: rgba(255, 49, 140, 1);
|
|
||||||
}
|
|
||||||
#nx-repo:hover {
|
|
||||||
background-color: rgba(24, 23, 23, 1);
|
|
||||||
}
|
|
||||||
#nx-repo svg {
|
|
||||||
color: rgba(24, 23, 23, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nx-cloud {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
}
|
|
||||||
#nx-cloud > div {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
#nx-cloud > div svg {
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 3rem;
|
|
||||||
}
|
|
||||||
#nx-cloud > div h2 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
#nx-cloud > div h2 span {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
}
|
|
||||||
#nx-cloud p {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
#nx-cloud pre {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
#nx-cloud a {
|
|
||||||
color: rgba(107, 114, 128, 1);
|
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
#nx-cloud a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#commands {
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
|
|
||||||
margin-top: 3.5rem;
|
|
||||||
}
|
|
||||||
#commands h2 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 400;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
#commands p {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
details {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
details pre > span {
|
|
||||||
color: rgba(181, 181, 181, 1);
|
|
||||||
}
|
|
||||||
summary {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke,
|
|
||||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
|
||||||
-webkit-backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
}
|
|
||||||
summary:hover {
|
|
||||||
background-color: rgba(243, 244, 246, 1);
|
|
||||||
}
|
|
||||||
summary svg {
|
|
||||||
height: 1.5rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
width: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#love {
|
|
||||||
color: rgba(107, 114, 128, 1);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
margin-top: 3.5rem;
|
|
||||||
opacity: 0.6;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#love svg {
|
|
||||||
color: rgba(252, 165, 165, 1);
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
display: inline;
|
|
||||||
margin-top: -0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
#hero {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
#hero .logo-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
#middle-content {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { AppElement } from './app.element';
|
|
||||||
|
|
||||||
describe('AppElement', () => {
|
|
||||||
let app: AppElement;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
app = new AppElement();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create successfully', () => {
|
|
||||||
expect(app).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have a greeting', () => {
|
|
||||||
app.connectedCallback();
|
|
||||||
|
|
||||||
expect(app.querySelector('h1').innerHTML).toContain(
|
|
||||||
'Welcome @triliumnext/client'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
import './app.element.css';
|
|
||||||
|
|
||||||
export class AppElement extends HTMLElement {
|
|
||||||
public static observedAttributes = [
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
const title = '@triliumnext/client';
|
|
||||||
this.innerHTML = `
|
|
||||||
<div class="wrapper">
|
|
||||||
<div class="container">
|
|
||||||
<!-- WELCOME -->
|
|
||||||
<div id="welcome">
|
|
||||||
<h1>
|
|
||||||
<span> Hello there, </span>
|
|
||||||
Welcome ${title} 👋
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- HERO -->
|
|
||||||
<div id="hero" class="rounded">
|
|
||||||
<div class="text-container">
|
|
||||||
<h2>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>You're up and running</span>
|
|
||||||
</h2>
|
|
||||||
<a href="#commands"> What's next? </a>
|
|
||||||
</div>
|
|
||||||
<div class="logo-container">
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- MIDDLE CONTENT -->
|
|
||||||
<div id="middle-content">
|
|
||||||
<div id="learning-materials" class="rounded shadow">
|
|
||||||
<h2>Learning materials</h2>
|
|
||||||
<a href="https://nx.dev/getting-started/intro?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Documentation
|
|
||||||
<span> Everything is in there </span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="https://nx.dev/blog/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Blog
|
|
||||||
<span> Changelog, features & events </span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.youtube.com/@NxDevtools/videos?utm_source=nx-project&sub_confirmation=1" target="_blank" rel="noreferrer" class="list-item-link">
|
|
||||||
<svg
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>YouTube</title>
|
|
||||||
<path
|
|
||||||
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
YouTube channel
|
|
||||||
<span> Nx Show, talks & tutorials </span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="https://nx.dev/react-tutorial/1-code-generation?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Interactive tutorials
|
|
||||||
<span> Create an app, step-by-step </span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="https://nxplaybook.com/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path d="M12 14l9-5-9-5-9 5 9 5z" />
|
|
||||||
<path
|
|
||||||
d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Video courses
|
|
||||||
<span> Nx custom courses </span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div id="other-links">
|
|
||||||
<a id="nx-console" class="button-pill rounded shadow" href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project" target="_blank" rel="noreferrer">
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>Visual Studio Code</title>
|
|
||||||
<path
|
|
||||||
d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Install Nx Console for VSCode
|
|
||||||
<span>The official VSCode extension for Nx.</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
id="nx-console-jetbrains"
|
|
||||||
class="button-pill rounded shadow"
|
|
||||||
href="https://plugins.jetbrains.com/plugin/21060-nx-console"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
height="48"
|
|
||||||
width="48"
|
|
||||||
viewBox="20 20 60 60"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path d="m22.5 22.5h60v60h-60z" />
|
|
||||||
<g fill="#fff">
|
|
||||||
<path d="m29.03 71.25h22.5v3.75h-22.5z" />
|
|
||||||
<path d="m28.09 38 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.44 1.06-1.31v-5.98h2.58v6a3.48 3.48 0 0 1 -.87 2.6 3.56 3.56 0 0 1 -2.57.95 3.84 3.84 0 0 1 -3.34-1.55z" />
|
|
||||||
<path d="m36 30h7.53v2.19h-5v1.44h4.49v2h-4.42v1.49h5v2.21h-7.6z" />
|
|
||||||
<path d="m47.23 32.29h-2.8v-2.29h8.21v2.27h-2.81v7.1h-2.6z" />
|
|
||||||
<path d="m29.13 43.08h4.42a3.53 3.53 0 0 1 2.55.83 2.09 2.09 0 0 1 .6 1.53 2.16 2.16 0 0 1 -1.44 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.61-1.31 2.59-3.55 2.59h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.24c.79 0 1.25-.26 1.25-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86 0-.5-.4-.87-1.27-.87z" />
|
|
||||||
<path d="m38 43.08h4.1a4.19 4.19 0 0 1 3 1 2.93 2.93 0 0 1 .9 2.19 3 3 0 0 1 -1.93 2.89l2.24 3.27h-3l-1.88-2.84h-.87v2.84h-2.56zm4 4.5c.87 0 1.39-.43 1.39-1.11 0-.75-.54-1.12-1.4-1.12h-1.44v2.26z" />
|
|
||||||
<path d="m49.59 43h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.69h-2.71zm2.27 5.73-1-2.65-1.06 2.65z" />
|
|
||||||
<path d="m56.46 43.05h2.6v9.37h-2.6z" />
|
|
||||||
<path d="m60.06 43.05h2.42l3.37 5v-5h2.57v9.37h-2.26l-3.53-5.14v5.14h-2.57z" />
|
|
||||||
<path d="m68.86 51 1.45-1.73a4.84 4.84 0 0 0 3 1.13c.71 0 1.08-.24 1.08-.65 0-.4-.31-.6-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.37-3 3.62-3a5.89 5.89 0 0 1 3.86 1.25l-1.26 1.84a4.63 4.63 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .42.32.61 1.63.91 2.14.46 3.44 1.16 3.44 2.91 0 1.91-1.51 3-3.79 3a6.58 6.58 0 0 1 -4.35-1.5z" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Install Nx Console for JetBrains
|
|
||||||
<span>
|
|
||||||
Available for WebStorm, Intellij IDEA Ultimate and more!
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<div id="nx-cloud" class="rounded shadow">
|
|
||||||
<div>
|
|
||||||
<svg id="nx-cloud-logo" role="img" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" fill="transparent" viewBox="0 0 24 24">
|
|
||||||
<path stroke-width="2" d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z" />
|
|
||||||
<path stroke-width="2" d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z" />
|
|
||||||
</svg>
|
|
||||||
<h2>
|
|
||||||
Nx Cloud
|
|
||||||
<span>
|
|
||||||
Enable faster CI & better DX
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
You can activate distributed tasks executions and caching by
|
|
||||||
running:
|
|
||||||
</p>
|
|
||||||
<pre>nx connect</pre>
|
|
||||||
<a href="https://nx.app/?utm_source=nx-project" target="_blank" rel="noreferrer"> What is Nx Cloud? </a>
|
|
||||||
</div>
|
|
||||||
<a id="nx-repo" class="button-pill rounded shadow" href="https://github.com/nrwl/nx?utm_source=nx-project" target="_blank" rel="noreferrer">
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>
|
|
||||||
Nx is open source
|
|
||||||
<span> Love Nx? Give us a star! </span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- COMMANDS -->
|
|
||||||
<div id="commands" class="rounded shadow">
|
|
||||||
<h2>Next steps</h2>
|
|
||||||
<p>Here are some things you can do with Nx:</p>
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Add UI library
|
|
||||||
</summary>
|
|
||||||
<pre><span># Generate UI lib</span>
|
|
||||||
nx g @nx/angular:lib ui
|
|
||||||
|
|
||||||
<span># Add a component</span>
|
|
||||||
nx g @nx/angular:component ui/src/lib/button</pre>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
View interactive project graph
|
|
||||||
</summary>
|
|
||||||
<pre>nx graph</pre>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Run affected commands
|
|
||||||
</summary>
|
|
||||||
<pre><span># see what's been affected by changes</span>
|
|
||||||
nx affected:graph
|
|
||||||
|
|
||||||
<span># run tests for current changes</span>
|
|
||||||
nx affected:test
|
|
||||||
|
|
||||||
<span># run e2e tests for current changes</span>
|
|
||||||
nx affected:e2e</pre>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p id="love">
|
|
||||||
Carefully crafted with
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
stroke="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('triliumnext-root', AppElement);
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,14 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Client</title>
|
|
||||||
<base href="/" />
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<triliumnext-root></triliumnext-root>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import './app/app.element';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import bundleService from "../services/bundle.js";
|
|
||||||
import RootCommandExecutor from "./root_command_executor.js";
|
import RootCommandExecutor from "./root_command_executor.js";
|
||||||
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
|
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
|
||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
@@ -29,6 +28,8 @@ import TouchBarComponent from "./touch_bar.js";
|
|||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
import { StartupChecks } from "./startup_checks.js";
|
import { StartupChecks } from "./startup_checks.js";
|
||||||
|
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||||
|
import { ColumnComponent } from "tabulator-tables";
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
getRootWidget: (appContext: AppContext) => RootWidget;
|
||||||
@@ -123,6 +124,7 @@ export type CommandMappings = {
|
|||||||
showImportDialog: CommandData & { noteId: string };
|
showImportDialog: CommandData & { noteId: string };
|
||||||
openNewNoteSplit: NoteCommandData;
|
openNewNoteSplit: NoteCommandData;
|
||||||
openInWindow: NoteCommandData;
|
openInWindow: NoteCommandData;
|
||||||
|
openInPopup: CommandData & { noteIdOrPath: string; };
|
||||||
openNoteInNewTab: CommandData;
|
openNoteInNewTab: CommandData;
|
||||||
openNoteInNewSplit: CommandData;
|
openNoteInNewSplit: CommandData;
|
||||||
openNoteInNewWindow: CommandData;
|
openNoteInNewWindow: CommandData;
|
||||||
@@ -131,6 +133,8 @@ export type CommandMappings = {
|
|||||||
hideLeftPane: CommandData;
|
hideLeftPane: CommandData;
|
||||||
showCpuArchWarning: CommandData;
|
showCpuArchWarning: CommandData;
|
||||||
showLeftPane: CommandData;
|
showLeftPane: CommandData;
|
||||||
|
showAttachments: CommandData;
|
||||||
|
showSearchHistory: CommandData;
|
||||||
hoistNote: CommandData & { noteId: string };
|
hoistNote: CommandData & { noteId: string };
|
||||||
leaveProtectedSession: CommandData;
|
leaveProtectedSession: CommandData;
|
||||||
enterProtectedSession: CommandData;
|
enterProtectedSession: CommandData;
|
||||||
@@ -141,6 +145,7 @@ export type CommandMappings = {
|
|||||||
};
|
};
|
||||||
openInTab: ContextMenuCommandData;
|
openInTab: ContextMenuCommandData;
|
||||||
openNoteInSplit: ContextMenuCommandData;
|
openNoteInSplit: ContextMenuCommandData;
|
||||||
|
openNoteInPopup: ContextMenuCommandData;
|
||||||
toggleNoteHoisting: ContextMenuCommandData;
|
toggleNoteHoisting: ContextMenuCommandData;
|
||||||
insertNoteAfter: ContextMenuCommandData;
|
insertNoteAfter: ContextMenuCommandData;
|
||||||
insertChildNote: ContextMenuCommandData;
|
insertChildNote: ContextMenuCommandData;
|
||||||
@@ -170,7 +175,7 @@ export type CommandMappings = {
|
|||||||
deleteNotes: ContextMenuCommandData;
|
deleteNotes: ContextMenuCommandData;
|
||||||
importIntoNote: ContextMenuCommandData;
|
importIntoNote: ContextMenuCommandData;
|
||||||
exportNote: ContextMenuCommandData;
|
exportNote: ContextMenuCommandData;
|
||||||
searchInSubtree: ContextMenuCommandData;
|
searchInSubtree: CommandData & { notePath: string; };
|
||||||
moveNoteUp: ContextMenuCommandData;
|
moveNoteUp: ContextMenuCommandData;
|
||||||
moveNoteDown: ContextMenuCommandData;
|
moveNoteDown: ContextMenuCommandData;
|
||||||
moveNoteUpInHierarchy: ContextMenuCommandData;
|
moveNoteUpInHierarchy: ContextMenuCommandData;
|
||||||
@@ -259,10 +264,76 @@ export type CommandMappings = {
|
|||||||
closeThisNoteSplit: CommandData;
|
closeThisNoteSplit: CommandData;
|
||||||
moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
|
moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
|
||||||
jumpToNote: CommandData;
|
jumpToNote: CommandData;
|
||||||
|
commandPalette: CommandData;
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
backInNoteHistory: CommandData;
|
||||||
|
forwardInNoteHistory: CommandData;
|
||||||
|
forceSaveRevision: CommandData;
|
||||||
|
scrollToActiveNote: CommandData;
|
||||||
|
quickSearch: CommandData;
|
||||||
|
collapseTree: CommandData;
|
||||||
|
createNoteAfter: CommandData;
|
||||||
|
createNoteInto: CommandData;
|
||||||
|
addNoteAboveToSelection: CommandData;
|
||||||
|
addNoteBelowToSelection: CommandData;
|
||||||
|
openNewTab: CommandData;
|
||||||
|
activateNextTab: CommandData;
|
||||||
|
activatePreviousTab: CommandData;
|
||||||
|
openNewWindow: CommandData;
|
||||||
|
toggleTray: CommandData;
|
||||||
|
firstTab: CommandData;
|
||||||
|
secondTab: CommandData;
|
||||||
|
thirdTab: CommandData;
|
||||||
|
fourthTab: CommandData;
|
||||||
|
fifthTab: CommandData;
|
||||||
|
sixthTab: CommandData;
|
||||||
|
seventhTab: CommandData;
|
||||||
|
eigthTab: CommandData;
|
||||||
|
ninthTab: CommandData;
|
||||||
|
lastTab: CommandData;
|
||||||
|
showNoteSource: CommandData;
|
||||||
|
showSQLConsole: CommandData;
|
||||||
|
showBackendLog: CommandData;
|
||||||
|
showCheatsheet: CommandData;
|
||||||
|
showHelp: CommandData;
|
||||||
|
addLinkToText: CommandData;
|
||||||
|
followLinkUnderCursor: CommandData;
|
||||||
|
insertDateTimeToText: CommandData;
|
||||||
|
pasteMarkdownIntoText: CommandData;
|
||||||
|
cutIntoNote: CommandData;
|
||||||
|
addIncludeNoteToText: CommandData;
|
||||||
|
editReadOnlyNote: CommandData;
|
||||||
|
toggleRibbonTabClassicEditor: CommandData;
|
||||||
|
toggleRibbonTabBasicProperties: CommandData;
|
||||||
|
toggleRibbonTabBookProperties: CommandData;
|
||||||
|
toggleRibbonTabFileProperties: CommandData;
|
||||||
|
toggleRibbonTabImageProperties: CommandData;
|
||||||
|
toggleRibbonTabOwnedAttributes: CommandData;
|
||||||
|
toggleRibbonTabInheritedAttributes: CommandData;
|
||||||
|
toggleRibbonTabPromotedAttributes: CommandData;
|
||||||
|
toggleRibbonTabNoteMap: CommandData;
|
||||||
|
toggleRibbonTabNoteInfo: CommandData;
|
||||||
|
toggleRibbonTabNotePaths: CommandData;
|
||||||
|
toggleRibbonTabSimilarNotes: CommandData;
|
||||||
|
toggleRightPane: CommandData;
|
||||||
|
printActiveNote: CommandData;
|
||||||
|
exportAsPdf: CommandData;
|
||||||
|
openNoteExternally: CommandData;
|
||||||
|
renderActiveNote: CommandData;
|
||||||
|
unhoist: CommandData;
|
||||||
|
reloadFrontendApp: CommandData;
|
||||||
|
openDevTools: CommandData;
|
||||||
|
findInText: CommandData;
|
||||||
|
toggleLeftPane: CommandData;
|
||||||
|
toggleFullscreen: CommandData;
|
||||||
|
zoomOut: CommandData;
|
||||||
|
zoomIn: CommandData;
|
||||||
|
zoomReset: CommandData;
|
||||||
|
copyWithoutFormatting: CommandData;
|
||||||
|
|
||||||
// Geomap
|
// Geomap
|
||||||
deleteFromMap: { noteId: string };
|
deleteFromMap: { noteId: string };
|
||||||
openGeoLocation: { noteId: string; event: JQuery.MouseDownEvent };
|
|
||||||
|
|
||||||
toggleZenMode: CommandData;
|
toggleZenMode: CommandData;
|
||||||
|
|
||||||
@@ -276,11 +347,27 @@ export type CommandMappings = {
|
|||||||
|
|
||||||
geoMapCreateChildNote: CommandData;
|
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 & {
|
buildTouchBar: CommandData & {
|
||||||
TouchBar: typeof TouchBar;
|
TouchBar: typeof TouchBar;
|
||||||
buildIcon(name: string): NativeImage;
|
buildIcon(name: string): NativeImage;
|
||||||
};
|
};
|
||||||
refreshTouchBar: CommandData;
|
refreshTouchBar: CommandData;
|
||||||
|
reloadTextEditor: CommandData;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EventMappings = {
|
type EventMappings = {
|
||||||
@@ -469,6 +556,7 @@ export class AppContext extends Component {
|
|||||||
|
|
||||||
this.tabManager.loadTabs();
|
this.tabManager.loadTabs();
|
||||||
|
|
||||||
|
const bundleService = (await import("../services/bundle.js")).default;
|
||||||
setTimeout(() => bundleService.executeStartupBundles(), 2000);
|
setTimeout(() => bundleService.executeStartupBundles(), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,11 +93,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
|
|
||||||
if (fun) {
|
if (fun) {
|
||||||
return this.callMethod(fun, data);
|
return this.callMethod(fun, data);
|
||||||
} else {
|
} else if (this.parent) {
|
||||||
if (!this.parent) {
|
|
||||||
throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.parent.triggerCommand(name, data);
|
return this.parent.triggerCommand(name, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,6 @@ interface CreateChildrenResponse {
|
|||||||
export default class Entrypoints extends Component {
|
export default class Entrypoints extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
if (jQuery.hotkeys) {
|
|
||||||
// hot keys are active also inside inputs and content editables
|
|
||||||
jQuery.hotkeys.options.filterInputAcceptingElements = false;
|
|
||||||
jQuery.hotkeys.options.filterContentEditable = false;
|
|
||||||
jQuery.hotkeys.options.filterTextInputs = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openDevToolsCommand() {
|
openDevToolsCommand() {
|
||||||
@@ -113,7 +106,9 @@ export default class Entrypoints extends Component {
|
|||||||
if (win.isFullScreenable()) {
|
if (win.isFullScreenable()) {
|
||||||
win.setFullScreen(!win.isFullScreen());
|
win.setFullScreen(!win.isFullScreen());
|
||||||
}
|
}
|
||||||
} // outside of electron this is handled by the browser
|
} else {
|
||||||
|
document.documentElement.requestFullscreen();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadFrontendAppCommand() {
|
reloadFrontendAppCommand() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type FNote from "../entities/fnote.js";
|
|||||||
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
|
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
|
||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
|
import { closeActiveDialog } from "../services/dialog.js";
|
||||||
|
|
||||||
export interface SetNoteOpts {
|
export interface SetNoteOpts {
|
||||||
triggerSwitchEvent?: unknown;
|
triggerSwitchEvent?: unknown;
|
||||||
@@ -83,7 +84,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
|
|
||||||
await this.triggerEvent("beforeNoteSwitch", { noteContext: this });
|
await this.triggerEvent("beforeNoteSwitch", { noteContext: this });
|
||||||
|
|
||||||
utils.closeActiveDialog();
|
closeActiveDialog();
|
||||||
|
|
||||||
this.notePath = resolvedNotePath;
|
this.notePath = resolvedNotePath;
|
||||||
this.viewScope = opts.viewScope;
|
this.viewScope = opts.viewScope;
|
||||||
@@ -314,14 +315,39 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasNoteList() {
|
hasNoteList() {
|
||||||
return (
|
const note = this.note;
|
||||||
this.note &&
|
|
||||||
["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "") &&
|
if (!note) {
|
||||||
(this.note.hasChildren() || this.note.getLabelValue("viewType") === "calendar") &&
|
return false;
|
||||||
["book", "text", "code"].includes(this.note.type) &&
|
}
|
||||||
this.note.mime !== "text/x-sqlite;schema=trilium" &&
|
|
||||||
!this.note.isLabelTruthy("hideChildrenOverview")
|
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) {
|
async getTextEditor(callback?: GetTextEditorCallback) {
|
||||||
|
|||||||
@@ -688,7 +688,7 @@ export default class TabManager extends Component {
|
|||||||
const titleFragments = [
|
const titleFragments = [
|
||||||
// it helps to navigate in history if note title is included in the title
|
// it helps to navigate in history if note title is included in the title
|
||||||
await activeNoteContext.getNavigationTitle(),
|
await activeNoteContext.getNavigationTitle(),
|
||||||
"TriliumNext Notes"
|
"Trilium Notes"
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
document.title = titleFragments.join(" - ");
|
document.title = titleFragments.join(" - ");
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import type ElectronRemote from "@electron/remote";
|
|||||||
import type Electron from "electron";
|
import type Electron from "electron";
|
||||||
import "./stylesheets/bootstrap.scss";
|
import "./stylesheets/bootstrap.scss";
|
||||||
import "boxicons/css/boxicons.min.css";
|
import "boxicons/css/boxicons.min.css";
|
||||||
import "jquery-hotkeys";
|
|
||||||
import "autocomplete.js/index_jquery.js";
|
import "autocomplete.js/index_jquery.js";
|
||||||
|
|
||||||
await appContext.earlyInit();
|
await appContext.earlyInit();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||||
import ws from "../services/ws.js";
|
import ws from "../services/ws.js";
|
||||||
import froca from "../services/froca.js";
|
|
||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
import cssClassManager from "../services/css_class_manager.js";
|
import cssClassManager from "../services/css_class_manager.js";
|
||||||
import type { Froca } from "../services/froca-interface.js";
|
import type { Froca } from "../services/froca-interface.js";
|
||||||
@@ -28,7 +27,6 @@ const NOTE_TYPE_ICONS = {
|
|||||||
doc: "bx bxs-file-doc",
|
doc: "bx bxs-file-doc",
|
||||||
contentWidget: "bx bxs-widget",
|
contentWidget: "bx bxs-widget",
|
||||||
mindMap: "bx bx-sitemap",
|
mindMap: "bx bx-sitemap",
|
||||||
geoMap: "bx bx-map-alt",
|
|
||||||
aiChat: "bx bx-bot"
|
aiChat: "bx bx-bot"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,7 +35,7 @@ const NOTE_TYPE_ICONS = {
|
|||||||
* end user. Those types should be used only for checking against, they are
|
* end user. Those types should be used only for checking against, they are
|
||||||
* not for direct use.
|
* 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 {
|
export interface NotePathRecord {
|
||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
@@ -258,6 +256,20 @@ class FNote {
|
|||||||
return this.children;
|
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() {
|
async getChildNotes() {
|
||||||
return await this.froca.getNotes(this.children);
|
return await this.froca.getNotes(this.children);
|
||||||
}
|
}
|
||||||
@@ -410,8 +422,8 @@ class FNote {
|
|||||||
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
|
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
|
||||||
notePath: path,
|
notePath: path,
|
||||||
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
|
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
|
||||||
isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
|
isArchived: path.some((noteId) => this.froca.notes[noteId].isArchived),
|
||||||
isSearch: path.some((noteId) => froca.notes[noteId].type === "search"),
|
isSearch: path.some((noteId) => this.froca.notes[noteId].type === "search"),
|
||||||
isHidden: path.includes("_hidden")
|
isHidden: path.includes("_hidden")
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -982,7 +994,7 @@ class FNote {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNote = froca.notes[parentNoteId];
|
const parentNote = this.froca.notes[parentNoteId];
|
||||||
|
|
||||||
if (!parentNote || parentNote.type === "search") {
|
if (!parentNote || parentNote.type === "search") {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -46,28 +46,7 @@ import SharedInfoWidget from "../widgets/shared_info.js";
|
|||||||
import FindWidget from "../widgets/find.js";
|
import FindWidget from "../widgets/find.js";
|
||||||
import TocWidget from "../widgets/toc.js";
|
import TocWidget from "../widgets/toc.js";
|
||||||
import HighlightsListWidget from "../widgets/highlights_list.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 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 FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
||||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||||
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.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 ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
||||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||||
import options from "../services/options.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 GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
|
||||||
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
|
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
|
||||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
import CloseZenButton from "../widgets/close_zen_button.js";
|
||||||
@@ -229,7 +208,7 @@ export default class DesktopLayout {
|
|||||||
.child(new PromotedAttributesWidget())
|
.child(new PromotedAttributesWidget())
|
||||||
.child(new SqlTableSchemasWidget())
|
.child(new SqlTableSchemasWidget())
|
||||||
.child(new NoteDetailWidget())
|
.child(new NoteDetailWidget())
|
||||||
.child(new NoteListWidget())
|
.child(new NoteListWidget(false))
|
||||||
.child(new SearchResultWidget())
|
.child(new SearchResultWidget())
|
||||||
.child(new SqlResultWidget())
|
.child(new SqlResultWidget())
|
||||||
.child(new ScrollPaddingWidget())
|
.child(new ScrollPaddingWidget())
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
|||||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||||
import InfoDialog from "../widgets/dialogs/info.js";
|
import InfoDialog from "../widgets/dialogs/info.js";
|
||||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.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) {
|
export function applyModals(rootContainer: RootContainer) {
|
||||||
rootContainer
|
rootContainer
|
||||||
@@ -47,4 +55,15 @@ export function applyModals(rootContainer: RootContainer) {
|
|||||||
.child(new ConfirmDialog())
|
.child(new ConfirmDialog())
|
||||||
.child(new PromptDialog())
|
.child(new PromptDialog())
|
||||||
.child(new IncorrectCpuArchDialog())
|
.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()
|
.filling()
|
||||||
.contentSized()
|
.contentSized()
|
||||||
.child(new NoteDetailWidget())
|
.child(new NoteDetailWidget())
|
||||||
.child(new NoteListWidget())
|
.child(new NoteListWidget(false))
|
||||||
.child(new FilePropertiesWidget().css("font-size", "smaller"))
|
.child(new FilePropertiesWidget().css("font-size", "smaller"))
|
||||||
)
|
)
|
||||||
.child(new MobileEditorToolbar())
|
.child(new MobileEditorToolbar())
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import keyboardActionService from "../services/keyboard_actions.js";
|
|||||||
import note_tooltip from "../services/note_tooltip.js";
|
import note_tooltip from "../services/note_tooltip.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
|
|
||||||
interface ContextMenuOptions<T> {
|
export interface ContextMenuOptions<T> {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
orientation?: "left";
|
orientation?: "left";
|
||||||
@@ -17,17 +17,30 @@ interface MenuSeparatorItem {
|
|||||||
title: "----";
|
title: "----";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MenuItemBadge {
|
||||||
|
title: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MenuCommandItem<T> {
|
export interface MenuCommandItem<T> {
|
||||||
title: string;
|
title: string;
|
||||||
command?: T;
|
command?: T;
|
||||||
type?: string;
|
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;
|
uiIcon?: string;
|
||||||
|
badges?: MenuItemBadge[];
|
||||||
templateNoteId?: string;
|
templateNoteId?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
handler?: MenuHandler<T>;
|
handler?: MenuHandler<T>;
|
||||||
items?: MenuItem<T>[] | null;
|
items?: MenuItem<T>[] | null;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
spellingSuggestion?: string;
|
spellingSuggestion?: string;
|
||||||
|
checked?: boolean;
|
||||||
|
columns?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||||
@@ -146,10 +159,13 @@ class ContextMenu {
|
|||||||
} else {
|
} else {
|
||||||
const $icon = $("<span>");
|
const $icon = $("<span>");
|
||||||
|
|
||||||
if ("uiIcon" in item && item.uiIcon) {
|
if ("uiIcon" in item || "checked" in item) {
|
||||||
$icon.addClass(item.uiIcon);
|
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||||
} else {
|
if (icon) {
|
||||||
$icon.append(" ");
|
$icon.addClass(icon);
|
||||||
|
} else {
|
||||||
|
$icon.append(" ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const $link = $("<span>")
|
const $link = $("<span>")
|
||||||
@@ -157,6 +173,18 @@ class ContextMenu {
|
|||||||
.append(" ") // some space between icon and text
|
.append(" ") // some space between icon and text
|
||||||
.append(item.title);
|
.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) {
|
if ("shortcut" in item && item.shortcut) {
|
||||||
$link.append($("<kbd>").text(item.shortcut));
|
$link.append($("<kbd>").text(item.shortcut));
|
||||||
}
|
}
|
||||||
@@ -194,14 +222,15 @@ class ContextMenu {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.isMobile) {
|
$item.on("mouseup", (e) => {
|
||||||
$item.on("mouseup", (e) =>{
|
// Prevent submenu from failing to expand on mobile
|
||||||
|
if (!this.isMobile || !("items" in item && item.items)) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
|
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
|
||||||
this.hide();
|
this.hide();
|
||||||
return false;
|
return false;
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
||||||
$item.addClass("disabled");
|
$item.addClass("disabled");
|
||||||
@@ -212,6 +241,9 @@ class ContextMenu {
|
|||||||
$link.addClass("dropdown-toggle");
|
$link.addClass("dropdown-toggle");
|
||||||
|
|
||||||
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
||||||
|
if (!this.isMobile && item.columns) {
|
||||||
|
$subMenu.css("column-count", item.columns);
|
||||||
|
}
|
||||||
|
|
||||||
this.addItems($subMenu, item.items);
|
this.addItems($subMenu, item.items);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ function getItems(): MenuItem<CommandNames>[] {
|
|||||||
return [
|
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_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_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 });
|
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
||||||
} else if (command === "openNoteInNewWindow") {
|
} else if (command === "openNoteInNewWindow") {
|
||||||
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||||
|
} else if (command === "openNoteInPopup") {
|
||||||
|
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ let lastTargetNode: HTMLElement | null = null;
|
|||||||
|
|
||||||
// This will include all commands that implement ContextMenuCommandData, but it will not work if it additional options are added via the `|` operator,
|
// This will include all commands that implement ContextMenuCommandData, but it will not work if it additional options are added via the `|` operator,
|
||||||
// so they need to be added manually.
|
// so they need to be added manually.
|
||||||
export type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData> | "openBulkActionsDialog";
|
export type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData> | "openBulkActionsDialog" | "searchInSubtree";
|
||||||
|
|
||||||
export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> {
|
export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> {
|
||||||
private treeWidget: NoteTreeWidget;
|
private treeWidget: NoteTreeWidget;
|
||||||
@@ -70,8 +70,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
|
|
||||||
const items: (MenuItem<TreeCommandNames> | null)[] = [
|
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-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-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
|
isHoisted
|
||||||
? null
|
? null
|
||||||
@@ -92,7 +92,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
command: "insertNoteAfter",
|
command: "insertNoteAfter",
|
||||||
uiIcon: "bx bx-plus",
|
uiIcon: "bx bx-plus",
|
||||||
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
|
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",
|
command: "insertChildNote",
|
||||||
uiIcon: "bx bx-plus",
|
uiIcon: "bx bx-plus",
|
||||||
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
|
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
|
||||||
enabled: notSearch && noSelectedNotes && notOptionsOrHelp
|
enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
|
||||||
|
columns: 2
|
||||||
},
|
},
|
||||||
|
|
||||||
{ title: "----" },
|
{ title: "----" },
|
||||||
@@ -127,12 +129,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
|
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.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: "----" },
|
||||||
|
|
||||||
@@ -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.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>`,
|
title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`,
|
||||||
command: "deleteNotes",
|
command: "deleteNotes",
|
||||||
@@ -244,6 +247,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
||||||
|
|
||||||
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
|
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
|
||||||
|
} else if (command === "openNoteInPopup") {
|
||||||
|
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
|
||||||
} else if (command === "convertNoteToAttachment") {
|
} else if (command === "convertNoteToAttachment") {
|
||||||
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
|
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -79,7 +79,19 @@ async function renderAttributes(attributes: FAttribute[], renderIsInheritable: b
|
|||||||
return $container;
|
return $container;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HIDDEN_ATTRIBUTES = ["originalFileName", "fileSize", "template", "inherit", "cssClass", "iconClass", "pageSize", "viewType", "geolocation", "docName"];
|
const HIDDEN_ATTRIBUTES = [
|
||||||
|
"originalFileName",
|
||||||
|
"fileSize",
|
||||||
|
"template",
|
||||||
|
"inherit",
|
||||||
|
"cssClass",
|
||||||
|
"iconClass",
|
||||||
|
"pageSize",
|
||||||
|
"viewType",
|
||||||
|
"geolocation",
|
||||||
|
"docName",
|
||||||
|
"webViewSrc"
|
||||||
|
];
|
||||||
|
|
||||||
async function renderNormalAttributes(note: FNote) {
|
async function renderNormalAttributes(note: FNote) {
|
||||||
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();
|
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();
|
||||||
|
|||||||
@@ -3,19 +3,21 @@ import froca from "./froca.js";
|
|||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import type { AttributeRow } from "./load_results.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`, {
|
await server.put(`notes/${noteId}/attribute`, {
|
||||||
type: "label",
|
type: "label",
|
||||||
name: name,
|
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`, {
|
await server.put(`notes/${noteId}/set-attribute`, {
|
||||||
type: "label",
|
type: "label",
|
||||||
name: name,
|
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 name the name of the attribute to set.
|
||||||
* @param value the value 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) {
|
if (value) {
|
||||||
// Create or update the attribute.
|
// Create or update the attribute.
|
||||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
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);
|
branchIdsToDelete = filterRootNote(branchIdsToDelete);
|
||||||
|
|
||||||
if (branchIdsToDelete.length === 0) {
|
if (branchIdsToDelete.length === 0) {
|
||||||
@@ -110,10 +118,12 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (moveToParent) {
|
||||||
await activateParentNotePath();
|
try {
|
||||||
} catch (e) {
|
await activateParentNotePath();
|
||||||
console.error(e);
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskId = utils.randomString(10);
|
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 RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
|
||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
|
import toast from "./toast.js";
|
||||||
|
import { BulkAction } from "@triliumnext/commons";
|
||||||
|
|
||||||
const ACTION_GROUPS = [
|
const ACTION_GROUPS = [
|
||||||
{
|
{
|
||||||
@@ -89,6 +91,17 @@ function parseActions(note: FNote) {
|
|||||||
.filter((action) => !!action);
|
.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 {
|
export default {
|
||||||
addAction,
|
addAction,
|
||||||
parseActions,
|
parseActions,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ScriptContext from "./script_context.js";
|
import ScriptContext from "./script_context.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import toastService from "./toast.js";
|
import toastService, { showError } from "./toast.js";
|
||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
@@ -37,7 +37,9 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const note = await froca.getNote(bundle.noteId);
|
const note = await froca.getNote(bundle.noteId);
|
||||||
|
|
||||||
toastService.showAndLogError(`Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`);
|
const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`;
|
||||||
|
showError(message);
|
||||||
|
logError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import froca from "./froca.js";
|
|||||||
import linkService from "./link.js";
|
import linkService from "./link.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
import toast from "./toast.js";
|
import { throwError } from "./ws.js";
|
||||||
|
|
||||||
let clipboardBranchIds: string[] = [];
|
let clipboardBranchIds: string[] = [];
|
||||||
let clipboardMode: string | null = null;
|
let clipboardMode: string | null = null;
|
||||||
@@ -37,7 +37,7 @@ async function pasteAfter(afterBranchId: string) {
|
|||||||
|
|
||||||
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
|
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
|
||||||
} else {
|
} else {
|
||||||
toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`);
|
throwError(`Unrecognized clipboard mode=${clipboardMode}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ async function pasteInto(parentBranchId: string) {
|
|||||||
|
|
||||||
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
|
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
|
||||||
} else {
|
} else {
|
||||||
toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`);
|
throwError(`Unrecognized clipboard mode=${clipboardMode}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
295
apps/client/src/services/command_registry.ts
Normal file
295
apps/client/src/services/command_registry.ts
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
import { ActionKeyboardShortcut } from "@triliumnext/commons";
|
||||||
|
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||||
|
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
|
import { t, translationsInitializedPromise } from "./i18n.js";
|
||||||
|
import keyboardActions from "./keyboard_actions.js";
|
||||||
|
import utils from "./utils.js";
|
||||||
|
|
||||||
|
export interface CommandDefinition {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
shortcut?: string;
|
||||||
|
commandName?: CommandNames;
|
||||||
|
handler?: () => Promise<unknown> | null | undefined | void;
|
||||||
|
aliases?: string[];
|
||||||
|
source?: "manual" | "keyboard-action";
|
||||||
|
/** Reference to the original keyboard action for scope checking. */
|
||||||
|
keyboardAction?: ActionKeyboardShortcut;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommandRegistry {
|
||||||
|
private commands: Map<string, CommandDefinition> = new Map();
|
||||||
|
private aliases: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadCommands() {
|
||||||
|
await translationsInitializedPromise;
|
||||||
|
this.registerDefaultCommands();
|
||||||
|
await this.loadKeyboardActionsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerDefaultCommands() {
|
||||||
|
this.register({
|
||||||
|
id: "export-note",
|
||||||
|
name: t("command_palette.export_note_title"),
|
||||||
|
description: t("command_palette.export_note_description"),
|
||||||
|
icon: "bx bx-export",
|
||||||
|
handler: () => {
|
||||||
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
|
if (notePath) {
|
||||||
|
appContext.triggerCommand("showExportDialog", {
|
||||||
|
notePath,
|
||||||
|
defaultType: "single"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.register({
|
||||||
|
id: "show-attachments",
|
||||||
|
name: t("command_palette.show_attachments_title"),
|
||||||
|
description: t("command_palette.show_attachments_description"),
|
||||||
|
icon: "bx bx-paperclip",
|
||||||
|
handler: () => appContext.triggerCommand("showAttachments")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Special search commands with custom logic
|
||||||
|
this.register({
|
||||||
|
id: "search-notes",
|
||||||
|
name: t("command_palette.search_notes_title"),
|
||||||
|
description: t("command_palette.search_notes_description"),
|
||||||
|
icon: "bx bx-search",
|
||||||
|
handler: () => appContext.triggerCommand("searchNotes", {})
|
||||||
|
});
|
||||||
|
|
||||||
|
this.register({
|
||||||
|
id: "search-in-subtree",
|
||||||
|
name: t("command_palette.search_subtree_title"),
|
||||||
|
description: t("command_palette.search_subtree_description"),
|
||||||
|
icon: "bx bx-search-alt",
|
||||||
|
handler: () => {
|
||||||
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
|
if (notePath) {
|
||||||
|
appContext.triggerCommand("searchInSubtree", { notePath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.register({
|
||||||
|
id: "show-search-history",
|
||||||
|
name: t("command_palette.search_history_title"),
|
||||||
|
description: t("command_palette.search_history_description"),
|
||||||
|
icon: "bx bx-history",
|
||||||
|
handler: () => appContext.triggerCommand("showSearchHistory")
|
||||||
|
});
|
||||||
|
|
||||||
|
this.register({
|
||||||
|
id: "show-launch-bar",
|
||||||
|
name: t("command_palette.configure_launch_bar_title"),
|
||||||
|
description: t("command_palette.configure_launch_bar_description"),
|
||||||
|
icon: "bx bx-sidebar",
|
||||||
|
handler: () => appContext.triggerCommand("showLaunchBarSubtree")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadKeyboardActionsAsync() {
|
||||||
|
try {
|
||||||
|
const actions = await keyboardActions.getActions();
|
||||||
|
this.registerKeyboardActions(actions);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load keyboard actions:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerKeyboardActions(actions: ActionKeyboardShortcut[]) {
|
||||||
|
for (const action of actions) {
|
||||||
|
// Skip actions that we've already manually registered
|
||||||
|
if (this.commands.has(action.actionName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip actions that don't have a description (likely separators)
|
||||||
|
if (!action.description) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip Electron-only actions if not in Electron environment
|
||||||
|
if (action.isElectronOnly && !utils.isElectron()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip actions that should not appear in the command palette
|
||||||
|
if (action.ignoreFromCommandPalette) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the primary shortcut (first one in the list)
|
||||||
|
const primaryShortcut = action.effectiveShortcuts?.[0];
|
||||||
|
|
||||||
|
let name = action.friendlyName;
|
||||||
|
if (action.scope === "note-tree") {
|
||||||
|
name = t("command_palette.tree-action-name", { name: action.friendlyName });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a command definition from the keyboard action
|
||||||
|
const commandDef: CommandDefinition = {
|
||||||
|
id: action.actionName,
|
||||||
|
name,
|
||||||
|
description: action.description,
|
||||||
|
icon: action.iconClass,
|
||||||
|
shortcut: primaryShortcut ? this.formatShortcut(primaryShortcut) : undefined,
|
||||||
|
commandName: action.actionName as CommandNames,
|
||||||
|
source: "keyboard-action",
|
||||||
|
keyboardAction: action
|
||||||
|
};
|
||||||
|
|
||||||
|
this.register(commandDef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatShortcut(shortcut: string): string {
|
||||||
|
// Convert electron accelerator format to display format
|
||||||
|
return shortcut
|
||||||
|
.replace(/CommandOrControl/g, 'Ctrl')
|
||||||
|
.replace(/\+/g, ' + ');
|
||||||
|
}
|
||||||
|
|
||||||
|
register(command: CommandDefinition) {
|
||||||
|
this.commands.set(command.id, command);
|
||||||
|
|
||||||
|
// Register aliases
|
||||||
|
if (command.aliases) {
|
||||||
|
for (const alias of command.aliases) {
|
||||||
|
this.aliases.set(alias.toLowerCase(), command.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommand(id: string): CommandDefinition | undefined {
|
||||||
|
return this.commands.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllCommands(): CommandDefinition[] {
|
||||||
|
const commands = Array.from(this.commands.values());
|
||||||
|
|
||||||
|
// Sort commands by name
|
||||||
|
commands.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchCommands(query: string): CommandDefinition[] {
|
||||||
|
const normalizedQuery = query.toLowerCase();
|
||||||
|
const results: { command: CommandDefinition; score: number }[] = [];
|
||||||
|
|
||||||
|
for (const command of this.commands.values()) {
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
// Exact match on name
|
||||||
|
if (command.name.toLowerCase() === normalizedQuery) {
|
||||||
|
score = 100;
|
||||||
|
}
|
||||||
|
// Name starts with query
|
||||||
|
else if (command.name.toLowerCase().startsWith(normalizedQuery)) {
|
||||||
|
score = 80;
|
||||||
|
}
|
||||||
|
// Name contains query
|
||||||
|
else if (command.name.toLowerCase().includes(normalizedQuery)) {
|
||||||
|
score = 60;
|
||||||
|
}
|
||||||
|
// Description contains query
|
||||||
|
else if (command.description?.toLowerCase().includes(normalizedQuery)) {
|
||||||
|
score = 40;
|
||||||
|
}
|
||||||
|
// Check aliases
|
||||||
|
else if (command.aliases?.some(alias => alias.toLowerCase().includes(normalizedQuery))) {
|
||||||
|
score = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (score > 0) {
|
||||||
|
results.push({ command, score });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by score (highest first) and then by name
|
||||||
|
results.sort((a, b) => {
|
||||||
|
if (a.score !== b.score) {
|
||||||
|
return b.score - a.score;
|
||||||
|
}
|
||||||
|
return a.command.name.localeCompare(b.command.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return results.map(r => r.command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeCommand(commandId: string) {
|
||||||
|
const command = this.getCommand(commandId);
|
||||||
|
if (!command) {
|
||||||
|
console.error(`Command not found: ${commandId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute custom handler if provided
|
||||||
|
if (command.handler) {
|
||||||
|
await command.handler();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle keyboard action with scope-aware execution
|
||||||
|
if (command.keyboardAction && command.commandName) {
|
||||||
|
if (command.keyboardAction.scope === "note-tree") {
|
||||||
|
this.executeWithNoteTreeFocus(command.commandName);
|
||||||
|
} else if (command.keyboardAction.scope === "text-detail") {
|
||||||
|
this.executeWithTextDetail(command.commandName);
|
||||||
|
} else {
|
||||||
|
appContext.triggerCommand(command.commandName, {
|
||||||
|
ntxId: appContext.tabManager.activeNtxId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for commands without keyboard action reference
|
||||||
|
if (command.commandName) {
|
||||||
|
appContext.triggerCommand(command.commandName, {
|
||||||
|
ntxId: appContext.tabManager.activeNtxId
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Command ${commandId} has no handler or commandName`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private executeWithNoteTreeFocus(actionName: CommandNames) {
|
||||||
|
const tree = document.querySelector(".tree-wrapper") as HTMLElement;
|
||||||
|
if (!tree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeComponent = appContext.getComponentByEl(tree) as NoteTreeWidget;
|
||||||
|
const activeNode = treeComponent.getActiveNode();
|
||||||
|
treeComponent.triggerCommand(actionName, {
|
||||||
|
ntxId: appContext.tabManager.activeNtxId,
|
||||||
|
node: activeNode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeWithTextDetail(actionName: CommandNames) {
|
||||||
|
const typeWidget = await appContext.tabManager.getActiveContext()?.getTypeWidget();
|
||||||
|
if (!typeWidget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
typeWidget.triggerCommand(actionName, {
|
||||||
|
ntxId: appContext.tabManager.activeNtxId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandRegistry = new CommandRegistry();
|
||||||
|
export default commandRegistry;
|
||||||
@@ -65,6 +65,9 @@ async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FA
|
|||||||
|
|
||||||
$renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button));
|
$renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button));
|
||||||
} else if (entity instanceof FNote) {
|
} else if (entity instanceof FNote) {
|
||||||
|
$renderedContent
|
||||||
|
.css("display", "flex")
|
||||||
|
.css("flex-direction", "column");
|
||||||
$renderedContent.append(
|
$renderedContent.append(
|
||||||
$("<div>")
|
$("<div>")
|
||||||
.css("display", "flex")
|
.css("display", "flex")
|
||||||
@@ -72,8 +75,33 @@ async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FA
|
|||||||
.css("align-items", "center")
|
.css("align-items", "center")
|
||||||
.css("height", "100%")
|
.css("height", "100%")
|
||||||
.css("font-size", "500%")
|
.css("font-size", "500%")
|
||||||
|
.css("flex-grow", "1")
|
||||||
.append($("<span>").addClass(entity.getIcon()))
|
.append($("<span>").addClass(entity.getIcon()))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (entity.type === "webView" && entity.hasLabel("webViewSrc")) {
|
||||||
|
const $footer = $("<footer>")
|
||||||
|
.addClass("webview-footer");
|
||||||
|
const $openButton = $(`
|
||||||
|
<button class="file-open btn btn-primary" type="button">
|
||||||
|
<span class="bx bx-link-external"></span>
|
||||||
|
${t("content_renderer.open_externally")}
|
||||||
|
</button>
|
||||||
|
`)
|
||||||
|
.appendTo($footer)
|
||||||
|
.on("click", () => {
|
||||||
|
const webViewSrc = entity.getLabelValue("webViewSrc");
|
||||||
|
if (webViewSrc) {
|
||||||
|
if (utils.isElectron()) {
|
||||||
|
const electron = utils.dynamicRequire("electron");
|
||||||
|
electron.shell.openExternal(webViewSrc);
|
||||||
|
} else {
|
||||||
|
window.open(webViewSrc, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$footer.appendTo($renderedContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity instanceof FNote) {
|
if (entity instanceof FNote) {
|
||||||
@@ -118,8 +146,17 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
|
|||||||
async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
|
async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
|
||||||
const blob = await note.getBlob();
|
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>");
|
const $codeBlock = $("<code>");
|
||||||
$codeBlock.text(blob?.content || "");
|
$codeBlock.text(content);
|
||||||
$renderedContent.append($("<pre>").append($codeBlock));
|
$renderedContent.append($("<pre>").append($codeBlock));
|
||||||
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
||||||
}
|
}
|
||||||
@@ -301,7 +338,7 @@ function getRenderingType(entity: FNote | FAttachment) {
|
|||||||
|
|
||||||
if (type === "file" && mime === "application/pdf") {
|
if (type === "file" && mime === "application/pdf") {
|
||||||
type = "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";
|
type = "code";
|
||||||
} else if (type === "file" && mime && mime.startsWith("audio/")) {
|
} else if (type === "file" && mime && mime.startsWith("audio/")) {
|
||||||
type = "audio";
|
type = "audio";
|
||||||
|
|||||||
@@ -1,13 +1,54 @@
|
|||||||
|
import { Modal } from "bootstrap";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
||||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||||
|
import { focusSavedElement, saveFocusedElement } from "./focus.js";
|
||||||
|
|
||||||
|
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
|
||||||
|
if (closeActDialog) {
|
||||||
|
closeActiveDialog();
|
||||||
|
glob.activeDialog = $dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFocusedElement();
|
||||||
|
Modal.getOrCreateInstance($dialog[0], config).show();
|
||||||
|
|
||||||
|
$dialog.on("hidden.bs.modal", () => {
|
||||||
|
const $autocompleteEl = $(".aa-input");
|
||||||
|
if ("autocomplete" in $autocompleteEl) {
|
||||||
|
$autocompleteEl.autocomplete("close");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!glob.activeDialog || glob.activeDialog === $dialog) {
|
||||||
|
focusSavedElement();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
|
||||||
|
keyboardActionsService.updateDisplayedShortcuts($dialog);
|
||||||
|
|
||||||
|
return $dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeActiveDialog() {
|
||||||
|
if (glob.activeDialog) {
|
||||||
|
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
|
||||||
|
glob.activeDialog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function info(message: string) {
|
async function info(message: string) {
|
||||||
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
|
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) {
|
async function confirm(message: string) {
|
||||||
return new Promise((res) =>
|
return new Promise<boolean>((res) =>
|
||||||
appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{
|
appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{
|
||||||
message,
|
message,
|
||||||
callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed)
|
callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed)
|
||||||
|
|||||||
29
apps/client/src/services/focus.ts
Normal file
29
apps/client/src/services/focus.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
let $lastFocusedElement: JQuery<HTMLElement> | null;
|
||||||
|
|
||||||
|
// perhaps there should be saved focused element per tab?
|
||||||
|
export function saveFocusedElement() {
|
||||||
|
$lastFocusedElement = $(":focus");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function focusSavedElement() {
|
||||||
|
if (!$lastFocusedElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lastFocusedElement.hasClass("ck")) {
|
||||||
|
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
|
||||||
|
// the bug manifests itself in resetting the cursor position to the first character - jumping above
|
||||||
|
|
||||||
|
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
editor.editing.view.focus();
|
||||||
|
} else {
|
||||||
|
console.log("Could not find CKEditor instance to focus last element");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$lastFocusedElement.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastFocusedElement = null;
|
||||||
|
}
|
||||||
@@ -245,6 +245,10 @@ class FrocaImpl implements Froca {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getNotes(noteIds: string[] | JQuery<string>, silentNotFoundError = false): Promise<FNote[]> {
|
async getNotes(noteIds: string[] | JQuery<string>, silentNotFoundError = false): Promise<FNote[]> {
|
||||||
|
if (noteIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
noteIds = Array.from(new Set(noteIds)); // make unique
|
noteIds = Array.from(new Set(noteIds)); // make unique
|
||||||
const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]);
|
const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]);
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ function setupGlobs() {
|
|||||||
const string = e?.reason?.message?.toLowerCase();
|
const string = e?.reason?.message?.toLowerCase();
|
||||||
|
|
||||||
let message = "Uncaught error: ";
|
let message = "Uncaught error: ";
|
||||||
|
let errorObjectString;
|
||||||
|
|
||||||
|
try {
|
||||||
|
errorObjectString = JSON.stringify(e.reason)
|
||||||
|
} catch (error: any) {
|
||||||
|
errorObjectString = error.toString();
|
||||||
|
}
|
||||||
|
|
||||||
if (string?.includes("script error")) {
|
if (string?.includes("script error")) {
|
||||||
message += "No details available";
|
message += "No details available";
|
||||||
@@ -57,7 +64,7 @@ function setupGlobs() {
|
|||||||
`Message: ${e.reason.message}`,
|
`Message: ${e.reason.message}`,
|
||||||
`Line: ${e.reason.lineNumber}`,
|
`Line: ${e.reason.lineNumber}`,
|
||||||
`Column: ${e.reason.columnNumber}`,
|
`Column: ${e.reason.columnNumber}`,
|
||||||
`Error object: ${JSON.stringify(e.reason)}`,
|
`Error object: ${errorObjectString}`,
|
||||||
`Stack: ${e.reason && e.reason.stack}`
|
`Stack: ${e.reason && e.reason.stack}`
|
||||||
].join(", ");
|
].join(", ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { LOCALES } from "@triliumnext/commons";
|
import { LOCALES } from "@triliumnext/commons";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("i18n", () => {
|
describe("i18n", () => {
|
||||||
it("translations are valid JSON", () => {
|
it("translations are valid JSON", () => {
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import type { Locale } from "@triliumnext/commons";
|
|||||||
|
|
||||||
let locales: Locale[] | null;
|
let locales: Locale[] | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A deferred promise that resolves when translations are initialized.
|
||||||
|
*/
|
||||||
|
export let translationsInitializedPromise = $.Deferred();
|
||||||
|
|
||||||
export async function initLocale() {
|
export async function initLocale() {
|
||||||
const locale = (options.get("locale") as string) || "en";
|
const locale = (options.get("locale") as string) || "en";
|
||||||
|
|
||||||
@@ -19,6 +24,8 @@ export async function initLocale() {
|
|||||||
},
|
},
|
||||||
returnEmptyString: false
|
returnEmptyString: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
translationsInitializedPromise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvailableLocales() {
|
export function getAvailableLocales() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
import toastService from "./toast.js";
|
import toastService, { showError } from "./toast.js";
|
||||||
|
|
||||||
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
|
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
|
||||||
try {
|
try {
|
||||||
@@ -11,7 +11,9 @@ function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
|
|||||||
if (success) {
|
if (success) {
|
||||||
toastService.showMessage(t("image.copied-to-clipboard"));
|
toastService.showMessage(t("image.copied-to-clipboard"));
|
||||||
} else {
|
} else {
|
||||||
toastService.showAndLogError(t("image.cannot-copy"));
|
const message = t("image.cannot-copy");
|
||||||
|
showError(message);
|
||||||
|
logError(message);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
window.getSelection()?.removeAllRanges();
|
window.getSelection()?.removeAllRanges();
|
||||||
|
|||||||
@@ -2,21 +2,15 @@ import server from "./server.js";
|
|||||||
import appContext, { type CommandNames } from "../components/app_context.js";
|
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||||
import shortcutService from "./shortcuts.js";
|
import shortcutService from "./shortcuts.js";
|
||||||
import type Component from "../components/component.js";
|
import type Component from "../components/component.js";
|
||||||
|
import type { ActionKeyboardShortcut } from "@triliumnext/commons";
|
||||||
|
|
||||||
const keyboardActionRepo: Record<string, Action> = {};
|
const keyboardActionRepo: Record<string, ActionKeyboardShortcut> = {};
|
||||||
|
|
||||||
// TODO: Deduplicate with server.
|
const keyboardActionsLoaded = server.get<ActionKeyboardShortcut[]>("keyboard-actions").then((actions) => {
|
||||||
export interface Action {
|
|
||||||
actionName: CommandNames;
|
|
||||||
effectiveShortcuts: string[];
|
|
||||||
scope: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyboardActionsLoaded = server.get<Action[]>("keyboard-actions").then((actions) => {
|
|
||||||
actions = actions.filter((a) => !!a.actionName); // filter out separators
|
actions = actions.filter((a) => !!a.actionName); // filter out separators
|
||||||
|
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
action.effectiveShortcuts = action.effectiveShortcuts.filter((shortcut) => !shortcut.startsWith("global:"));
|
action.effectiveShortcuts = (action.effectiveShortcuts ?? []).filter((shortcut) => !shortcut.startsWith("global:"));
|
||||||
|
|
||||||
keyboardActionRepo[action.actionName] = action;
|
keyboardActionRepo[action.actionName] = action;
|
||||||
}
|
}
|
||||||
@@ -38,7 +32,7 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c
|
|||||||
const actions = await getActionsForScope(scope);
|
const actions = await getActionsForScope(scope);
|
||||||
|
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
for (const shortcut of action.effectiveShortcuts) {
|
for (const shortcut of action.effectiveShortcuts ?? []) {
|
||||||
shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
|
shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +40,7 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c
|
|||||||
|
|
||||||
getActionsForScope("window").then((actions) => {
|
getActionsForScope("window").then((actions) => {
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
for (const shortcut of action.effectiveShortcuts) {
|
for (const shortcut of action.effectiveShortcuts ?? []) {
|
||||||
shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
|
shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +74,7 @@ function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
|
|||||||
const action = await getAction(actionName, true);
|
const action = await getAction(actionName, true);
|
||||||
|
|
||||||
if (action) {
|
if (action) {
|
||||||
const keyboardActions = action.effectiveShortcuts.join(", ");
|
const keyboardActions = (action.effectiveShortcuts ?? []).join(", ");
|
||||||
|
|
||||||
if (keyboardActions || $(el).text() !== "not set") {
|
if (keyboardActions || $(el).text() !== "not set") {
|
||||||
$(el).text(keyboardActions);
|
$(el).text(keyboardActions);
|
||||||
@@ -99,7 +93,7 @@ function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
|
|||||||
|
|
||||||
if (action) {
|
if (action) {
|
||||||
const title = $(el).attr("title");
|
const title = $(el).attr("title");
|
||||||
const shortcuts = action.effectiveShortcuts.join(", ");
|
const shortcuts = (action.effectiveShortcuts ?? []).join(", ");
|
||||||
|
|
||||||
if (title?.includes(shortcuts)) {
|
if (title?.includes(shortcuts)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
|||||||
let ntxId: string | null = null;
|
let ntxId: string | null = null;
|
||||||
let hoistedNoteId: string | null = null;
|
let hoistedNoteId: string | null = null;
|
||||||
let searchString: string | null = null;
|
let searchString: string | null = null;
|
||||||
|
let openInPopup = false;
|
||||||
|
|
||||||
if (paramString) {
|
if (paramString) {
|
||||||
for (const pair of paramString.split("&")) {
|
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
|
searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
|
||||||
} else if (["viewMode", "attachmentId"].includes(name)) {
|
} else if (["viewMode", "attachmentId"].includes(name)) {
|
||||||
(viewScope as any)[name] = value;
|
(viewScope as any)[name] = value;
|
||||||
|
} else if (name === "popup") {
|
||||||
|
openInPopup = true;
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Unrecognized hash parameter '${name}'.`);
|
console.warn(`Unrecognized hash parameter '${name}'.`);
|
||||||
}
|
}
|
||||||
@@ -266,7 +269,8 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
|||||||
ntxId,
|
ntxId,
|
||||||
hoistedNoteId,
|
hoistedNoteId,
|
||||||
viewScope,
|
viewScope,
|
||||||
searchString
|
searchString,
|
||||||
|
openInPopup
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,13 +281,21 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
|
|||||||
return goToLinkExt(evt, hrefLink, $link);
|
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:")) {
|
if (hrefLink?.startsWith("data:")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
evt.preventDefault();
|
evt?.preventDefault();
|
||||||
evt.stopPropagation();
|
evt?.stopPropagation();
|
||||||
|
|
||||||
if (hrefLink && hrefLink.startsWith("#") && !hrefLink.startsWith("#root/") && $link) {
|
if (hrefLink && hrefLink.startsWith("#") && !hrefLink.startsWith("#root/") && $link) {
|
||||||
if (handleAnchor(hrefLink, $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 ctrlKey = evt && utils.isCtrlKey(evt);
|
||||||
const shiftKey = evt.shiftKey;
|
const shiftKey = evt?.shiftKey;
|
||||||
const isLeftClick = "which" in evt && evt.which === 1;
|
const isLeftClick = !evt || ("which" in evt && evt.which === 1);
|
||||||
const isMiddleClick = "which" in evt && evt.which === 2;
|
// Right click is handled separately.
|
||||||
|
const isMiddleClick = evt && "which" in evt && evt.which === 2;
|
||||||
const targetIsBlank = ($link?.attr("target") === "_blank");
|
const targetIsBlank = ($link?.attr("target") === "_blank");
|
||||||
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank;
|
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank;
|
||||||
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
|
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
|
||||||
const openInNewWindow = isLeftClick && evt.shiftKey && !ctrlKey;
|
const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
|
||||||
|
|
||||||
if (notePath) {
|
if (notePath) {
|
||||||
if (openInNewWindow) {
|
if (isLeftClick && openInPopup) {
|
||||||
|
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
||||||
|
} else if (openInNewWindow) {
|
||||||
appContext.triggerCommand("openInWindow", { notePath, viewScope });
|
appContext.triggerCommand("openInWindow", { notePath, viewScope });
|
||||||
} else if (openInNewTab) {
|
} else if (openInNewTab) {
|
||||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
||||||
@@ -311,7 +326,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
|
|||||||
viewScope
|
viewScope
|
||||||
});
|
});
|
||||||
} else if (isLeftClick) {
|
} else if (isLeftClick) {
|
||||||
const ntxId = $(evt.target as any)
|
const ntxId = $(evt?.target as any)
|
||||||
.closest("[data-ntx-id]")
|
.closest("[data-ntx-id]")
|
||||||
.attr("data-ntx-id");
|
.attr("data-ntx-id");
|
||||||
|
|
||||||
@@ -379,6 +394,12 @@ function linkContextMenu(e: PointerEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (utils.isCtrlKey(e) && e.button === 2) {
|
||||||
|
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
linkContextMenuService.openContextMenu(notePath, e, viewScope, null);
|
linkContextMenuService.openContextMenu(notePath, e, viewScope, null);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import appContext from "../components/app_context.js";
|
|||||||
import noteCreateService from "./note_create.js";
|
import noteCreateService from "./note_create.js";
|
||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
|
import commandRegistry from "./command_registry.js";
|
||||||
import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5";
|
import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5";
|
||||||
|
|
||||||
// this key needs to have this value, so it's hit by the tooltip
|
// this key needs to have this value, so it's hit by the tooltip
|
||||||
@@ -29,9 +30,12 @@ export interface Suggestion {
|
|||||||
notePathTitle?: string;
|
notePathTitle?: string;
|
||||||
notePath?: string;
|
notePath?: string;
|
||||||
highlightedNotePathTitle?: string;
|
highlightedNotePathTitle?: string;
|
||||||
action?: string | "create-note" | "search-notes" | "external-link";
|
action?: string | "create-note" | "search-notes" | "external-link" | "command";
|
||||||
parentNoteId?: string;
|
parentNoteId?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
commandId?: string;
|
||||||
|
commandDescription?: string;
|
||||||
|
commandShortcut?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
@@ -40,7 +44,12 @@ interface Options {
|
|||||||
allowCreatingNotes?: boolean;
|
allowCreatingNotes?: boolean;
|
||||||
allowJumpToSearchNotes?: boolean;
|
allowJumpToSearchNotes?: boolean;
|
||||||
allowExternalLinks?: boolean;
|
allowExternalLinks?: boolean;
|
||||||
|
/** If set, hides the right-side button corresponding to go to selected note. */
|
||||||
hideGoToSelectedNoteButton?: boolean;
|
hideGoToSelectedNoteButton?: boolean;
|
||||||
|
/** If set, hides all right-side buttons in the autocomplete dropdown */
|
||||||
|
hideAllButtons?: boolean;
|
||||||
|
/** If set, enables command palette mode */
|
||||||
|
isCommandPalette?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function autocompleteSourceForCKEditor(queryText: string) {
|
async function autocompleteSourceForCKEditor(queryText: string) {
|
||||||
@@ -70,6 +79,31 @@ async function autocompleteSourceForCKEditor(queryText: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) {
|
async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) {
|
||||||
|
// Check if we're in command mode
|
||||||
|
if (options.isCommandPalette && term.startsWith(">")) {
|
||||||
|
const commandQuery = term.substring(1).trim();
|
||||||
|
|
||||||
|
// Get commands (all if no query, filtered if query provided)
|
||||||
|
const commands = commandQuery.length === 0
|
||||||
|
? commandRegistry.getAllCommands()
|
||||||
|
: commandRegistry.searchCommands(commandQuery);
|
||||||
|
|
||||||
|
// Convert commands to suggestions
|
||||||
|
const commandSuggestions: Suggestion[] = commands.map(cmd => ({
|
||||||
|
action: "command",
|
||||||
|
commandId: cmd.id,
|
||||||
|
noteTitle: cmd.name,
|
||||||
|
notePathTitle: `>${cmd.name}`,
|
||||||
|
highlightedNotePathTitle: cmd.name,
|
||||||
|
commandDescription: cmd.description,
|
||||||
|
commandShortcut: cmd.shortcut,
|
||||||
|
icon: cmd.icon
|
||||||
|
}));
|
||||||
|
|
||||||
|
cb(commandSuggestions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fastSearch = options.fastSearch === false ? false : true;
|
const fastSearch = options.fastSearch === false ? false : true;
|
||||||
if (fastSearch === false) {
|
if (fastSearch === false) {
|
||||||
if (term.trim().length === 0) {
|
if (term.trim().length === 0) {
|
||||||
@@ -143,6 +177,12 @@ function showRecentNotes($el: JQuery<HTMLElement>) {
|
|||||||
$el.trigger("focus");
|
$el.trigger("focus");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showAllCommands($el: JQuery<HTMLElement>) {
|
||||||
|
searchDelay = 0;
|
||||||
|
$el.setSelectedNotePath("");
|
||||||
|
$el.autocomplete("val", ">").autocomplete("open");
|
||||||
|
}
|
||||||
|
|
||||||
function fullTextSearch($el: JQuery<HTMLElement>, options: Options) {
|
function fullTextSearch($el: JQuery<HTMLElement>, options: Options) {
|
||||||
const searchString = $el.autocomplete("val") as unknown as string;
|
const searchString = $el.autocomplete("val") as unknown as string;
|
||||||
if (options.fastSearch === false || searchString?.trim().length === 0) {
|
if (options.fastSearch === false || searchString?.trim().length === 0) {
|
||||||
@@ -190,9 +230,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");
|
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);
|
$el.after($goToSelectedNoteButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +307,24 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
|||||||
},
|
},
|
||||||
displayKey: "notePathTitle",
|
displayKey: "notePathTitle",
|
||||||
templates: {
|
templates: {
|
||||||
suggestion: (suggestion) => `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`
|
suggestion: (suggestion) => {
|
||||||
|
if (suggestion.action === "command") {
|
||||||
|
let html = `<div class="command-suggestion">`;
|
||||||
|
html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`;
|
||||||
|
html += `<div class="command-content">`;
|
||||||
|
html += `<div class="command-name">${suggestion.highlightedNotePathTitle}</div>`;
|
||||||
|
if (suggestion.commandDescription) {
|
||||||
|
html += `<div class="command-description">${suggestion.commandDescription}</div>`;
|
||||||
|
}
|
||||||
|
html += `</div>`;
|
||||||
|
if (suggestion.commandShortcut) {
|
||||||
|
html += `<kbd class="command-shortcut">${suggestion.commandShortcut}</kbd>`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
return `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
|
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
|
||||||
cache: false
|
cache: false
|
||||||
@@ -275,6 +334,12 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
|||||||
|
|
||||||
// TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
|
// TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
|
||||||
($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => {
|
($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => {
|
||||||
|
if (suggestion.action === "command") {
|
||||||
|
$el.autocomplete("close");
|
||||||
|
$el.trigger("autocomplete:commandselected", [suggestion]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (suggestion.action === "external-link") {
|
if (suggestion.action === "external-link") {
|
||||||
$el.setSelectedNotePath(null);
|
$el.setSelectedNotePath(null);
|
||||||
$el.setSelectedExternalLink(suggestion.externalLink);
|
$el.setSelectedExternalLink(suggestion.externalLink);
|
||||||
@@ -289,13 +354,11 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (suggestion.action === "create-note") {
|
if (suggestion.action === "create-note") {
|
||||||
const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType();
|
const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, {
|
||||||
const { note } = await noteCreateService.createNote(suggestion.parentNoteId, {
|
|
||||||
title: suggestion.noteTitle,
|
title: suggestion.noteTitle,
|
||||||
activate: false,
|
activate: false,
|
||||||
type: noteType,
|
type: noteType,
|
||||||
@@ -393,6 +456,7 @@ export default {
|
|||||||
autocompleteSourceForCKEditor,
|
autocompleteSourceForCKEditor,
|
||||||
initNoteAutocomplete,
|
initNoteAutocomplete,
|
||||||
showRecentNotes,
|
showRecentNotes,
|
||||||
|
showAllCommands,
|
||||||
setText,
|
setText,
|
||||||
init
|
init
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type FBranch from "../entities/fbranch.js";
|
|||||||
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
|
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
|
||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
|
|
||||||
interface CreateNoteOpts {
|
export interface CreateNoteOpts {
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
saveSelection?: boolean;
|
saveSelection?: boolean;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
@@ -116,7 +116,7 @@ async function chooseNoteType() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
|
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
|
||||||
const { success, noteType, templateNoteId } = await chooseNoteType();
|
const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return;
|
return;
|
||||||
@@ -125,7 +125,7 @@ async function createNoteWithTypePrompt(parentNotePath: string, options: CreateN
|
|||||||
options.type = noteType;
|
options.type = noteType;
|
||||||
options.templateNoteId = templateNoteId;
|
options.templateNoteId = templateNoteId;
|
||||||
|
|
||||||
return await createNote(parentNotePath, options);
|
return await createNote(notePath || parentNotePath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If the first element is heading, parse it out and use it as a new heading. */
|
/* If the first element is heading, parse it out and use it as a new heading. */
|
||||||
|
|||||||
@@ -1,38 +1,31 @@
|
|||||||
import type FNote from "../entities/fnote.js";
|
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 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 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 { ViewModeArgs } from "../widgets/view_widgets/view_mode.js";
|
||||||
import type ViewMode 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 {
|
export default class NoteListRenderer {
|
||||||
|
|
||||||
private viewType: ViewTypeOptions;
|
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) {
|
constructor(args: ArgsWithoutNoteId) {
|
||||||
this.viewType = this.#getViewType(parentNote);
|
this.args = args;
|
||||||
const args: ViewModeArgs = {
|
this.viewType = this.#getViewType(args.parentNote);
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#getViewType(parentNote: FNote): ViewTypeOptions {
|
#getViewType(parentNote: FNote): ViewTypeOptions {
|
||||||
const viewType = parentNote.getLabelValue("viewType");
|
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
|
// when not explicitly set, decide based on the note type
|
||||||
return parentNote.type === "search" ? "list" : "grid";
|
return parentNote.type === "search" ? "list" : "grid";
|
||||||
} else {
|
} else {
|
||||||
@@ -41,15 +34,38 @@ export default class NoteListRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isFullHeight() {
|
get isFullHeight() {
|
||||||
return this.viewMode?.isFullHeight;
|
switch (this.viewType) {
|
||||||
|
case "list":
|
||||||
|
case "grid":
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderList() {
|
async renderList() {
|
||||||
if (!this.viewMode) {
|
const args = this.args;
|
||||||
return null;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ let openTooltipElements: JQuery<HTMLElement>[] = [];
|
|||||||
let dismissTimer: ReturnType<typeof setTimeout>;
|
let dismissTimer: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
function setupGlobalTooltip() {
|
function setupGlobalTooltip() {
|
||||||
$(document).on("mouseenter", "a", mouseEnterHandler);
|
$(document).on("mouseenter", "a:not(.no-tooltip-preview)", mouseEnterHandler);
|
||||||
|
$(document).on("mouseenter", "[data-href]:not(.no-tooltip-preview)", mouseEnterHandler);
|
||||||
|
|
||||||
// close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
|
// close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
|
||||||
$(document).on("click", (e) => {
|
$(document).on("click", (e) => {
|
||||||
@@ -167,7 +168,10 @@ async function renderTooltip(note: FNote | null) {
|
|||||||
if (isContentEmpty) {
|
if (isContentEmpty) {
|
||||||
classes.push("note-no-content");
|
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>`;
|
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 += $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;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +1,236 @@
|
|||||||
import server from "./server.js";
|
|
||||||
import froca from "./froca.js";
|
|
||||||
import { t } from "./i18n.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";
|
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) {
|
async function getNoteTypeItems(command?: TreeCommandNames) {
|
||||||
const items: MenuItem<TreeCommandNames>[] = [
|
const items: MenuItem<TreeCommandNames>[] = [
|
||||||
{ title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" },
|
...getBlankNoteTypes(command),
|
||||||
{ title: t("note_types.code"), command, type: "code", uiIcon: "bx bx-code" },
|
...await getBuiltInTemplates(t("note_types.collections"), command, true),
|
||||||
{ title: t("note_types.saved-search"), command, type: "search", uiIcon: "bx bx-file-find" },
|
...await getBuiltInTemplates(null, command, false),
|
||||||
{ title: t("note_types.relation-map"), command, type: "relationMap", uiIcon: "bx bxs-network-chart" },
|
...await getUserTemplates(command)
|
||||||
{ 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" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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 templateNoteIds = await server.get<string[]>("search-templates");
|
||||||
const templateNotes = await froca.getNotes(templateNoteIds);
|
const templateNotes = await froca.getNotes(templateNoteIds);
|
||||||
|
if (templateNotes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (templateNotes.length > 0) {
|
const items: MenuItem<TreeCommandNames>[] = [
|
||||||
items.push({ title: "----" });
|
SEPARATOR
|
||||||
|
];
|
||||||
|
|
||||||
for (const templateNote of templateNotes) {
|
for (const templateNote of templateNotes) {
|
||||||
items.push({
|
const item: MenuItem<TreeCommandNames> = {
|
||||||
title: templateNote.title,
|
title: templateNote.title,
|
||||||
uiIcon: templateNote.getIcon(),
|
uiIcon: templateNote.getIcon(),
|
||||||
command: command,
|
command: command,
|
||||||
type: templateNote.type,
|
type: templateNote.type,
|
||||||
templateNoteId: templateNote.noteId
|
templateNoteId: templateNote.noteId
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (await isNewTemplate(templateNote.noteId)) {
|
||||||
|
item.badges = [NEW_BADGE];
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const childNotes = await templatesRoot.getChildNotes();
|
||||||
|
if (childNotes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
// 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 {
|
export default {
|
||||||
|
|||||||
@@ -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";
|
type Multiplicity = "single" | "multi";
|
||||||
|
|
||||||
export interface DefinitionObject {
|
export interface DefinitionObject {
|
||||||
@@ -17,7 +17,7 @@ function parse(value: string) {
|
|||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
if (token === "promoted") {
|
if (token === "promoted") {
|
||||||
defObj.isPromoted = true;
|
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;
|
defObj.labelType = token as LabelType;
|
||||||
} else if (["single", "multi"].includes(token)) {
|
} else if (["single", "multi"].includes(token)) {
|
||||||
defObj.multiplicity = token as Multiplicity;
|
defObj.multiplicity = token as Multiplicity;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import FrontendScriptApi, { type Entity } from "./frontend_script_api.js";
|
import type { Entity } from "./frontend_script_api.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import froca from "./froca.js";
|
import froca from "./froca.js";
|
||||||
|
|
||||||
@@ -14,6 +14,8 @@ async function ScriptContext(startNoteId: string, allNoteIds: string[], originEn
|
|||||||
throw new Error(`Could not find start note ${startNoteId}.`);
|
throw new Error(`Could not find start note ${startNoteId}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FrontendScriptApi = (await import("./frontend_script_api.js")).default;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
modules: modules,
|
modules: modules,
|
||||||
notes: utils.toObject(allNotes, (note) => [note.noteId, note]),
|
notes: utils.toObject(allNotes, (note) => [note.noteId, note]),
|
||||||
|
|||||||
@@ -276,7 +276,8 @@ async function reportError(method: string, url: string, statusCode: number, resp
|
|||||||
} else {
|
} else {
|
||||||
const title = `${statusCode} ${method} ${url}`;
|
const title = `${statusCode} ${method} ${url}`;
|
||||||
toastService.showErrorTitleAndMessage(title, messageStr);
|
toastService.showErrorTitleAndMessage(title, messageStr);
|
||||||
toastService.throwError(`${title} - ${message}`);
|
const { throwError } = await import("./ws.js");
|
||||||
|
throwError(`${title} - ${message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
323
apps/client/src/services/shortcuts.spec.ts
Normal file
323
apps/client/src/services/shortcuts.spec.ts
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import shortcuts, { keyMatches, matchesShortcut } from "./shortcuts.js";
|
||||||
|
|
||||||
|
// Mock utils module
|
||||||
|
vi.mock("./utils.js", () => ({
|
||||||
|
default: {
|
||||||
|
isDesktop: () => true
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock jQuery globally since it's used in the shortcuts module
|
||||||
|
const mockElement = {
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockJQuery = vi.fn(() => [mockElement]);
|
||||||
|
(mockJQuery as any).length = 1;
|
||||||
|
mockJQuery[0] = mockElement;
|
||||||
|
|
||||||
|
(global as any).$ = mockJQuery as any;
|
||||||
|
global.document = mockElement as any;
|
||||||
|
|
||||||
|
describe("shortcuts", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up any active bindings after each test
|
||||||
|
shortcuts.removeGlobalShortcut("test-namespace");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("normalizeShortcut", () => {
|
||||||
|
it("should normalize shortcut to lowercase and remove whitespace", () => {
|
||||||
|
expect(shortcuts.normalizeShortcut("Ctrl + A")).toBe("ctrl+a");
|
||||||
|
expect(shortcuts.normalizeShortcut(" SHIFT + F1 ")).toBe("shift+f1");
|
||||||
|
expect(shortcuts.normalizeShortcut("Alt+Space")).toBe("alt+space");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty or null shortcuts", () => {
|
||||||
|
expect(shortcuts.normalizeShortcut("")).toBe("");
|
||||||
|
expect(shortcuts.normalizeShortcut(null as any)).toBe(null);
|
||||||
|
expect(shortcuts.normalizeShortcut(undefined as any)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle shortcuts with multiple spaces", () => {
|
||||||
|
expect(shortcuts.normalizeShortcut("Ctrl + Shift + A")).toBe("ctrl+shift+a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn about malformed shortcuts", () => {
|
||||||
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
shortcuts.normalizeShortcut("ctrl+");
|
||||||
|
shortcuts.normalizeShortcut("+a");
|
||||||
|
shortcuts.normalizeShortcut("ctrl++a");
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalledTimes(3);
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("keyMatches", () => {
|
||||||
|
const createKeyboardEvent = (key: string, code?: string) => ({
|
||||||
|
key,
|
||||||
|
code: code || `Key${key.toUpperCase()}`
|
||||||
|
} as KeyboardEvent);
|
||||||
|
|
||||||
|
it("should match regular letter keys using key code", () => {
|
||||||
|
const event = createKeyboardEvent("a", "KeyA");
|
||||||
|
expect(keyMatches(event, "a")).toBe(true);
|
||||||
|
expect(keyMatches(event, "A")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match number keys using digit codes", () => {
|
||||||
|
const event = createKeyboardEvent("1", "Digit1");
|
||||||
|
expect(keyMatches(event, "1")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match special keys using key mapping", () => {
|
||||||
|
expect(keyMatches({ key: "Enter" } as KeyboardEvent, "return")).toBe(true);
|
||||||
|
expect(keyMatches({ key: "Enter" } as KeyboardEvent, "enter")).toBe(true);
|
||||||
|
expect(keyMatches({ key: "Delete" } as KeyboardEvent, "del")).toBe(true);
|
||||||
|
expect(keyMatches({ key: "Escape" } as KeyboardEvent, "esc")).toBe(true);
|
||||||
|
expect(keyMatches({ key: " " } as KeyboardEvent, "space")).toBe(true);
|
||||||
|
expect(keyMatches({ key: "ArrowUp" } as KeyboardEvent, "up")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match function keys", () => {
|
||||||
|
expect(keyMatches({ key: "F1" } as KeyboardEvent, "f1")).toBe(true);
|
||||||
|
expect(keyMatches({ key: "F12" } as KeyboardEvent, "f12")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle undefined or null keys", () => {
|
||||||
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
expect(keyMatches({} as KeyboardEvent, null as any)).toBe(false);
|
||||||
|
expect(keyMatches({} as KeyboardEvent, undefined as any)).toBe(false);
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalled();
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("matchesShortcut", () => {
|
||||||
|
const createKeyboardEvent = (options: {
|
||||||
|
key: string;
|
||||||
|
code?: string;
|
||||||
|
ctrlKey?: boolean;
|
||||||
|
altKey?: boolean;
|
||||||
|
shiftKey?: boolean;
|
||||||
|
metaKey?: boolean;
|
||||||
|
}) => ({
|
||||||
|
key: options.key,
|
||||||
|
code: options.code || `Key${options.key.toUpperCase()}`,
|
||||||
|
ctrlKey: options.ctrlKey || false,
|
||||||
|
altKey: options.altKey || false,
|
||||||
|
shiftKey: options.shiftKey || false,
|
||||||
|
metaKey: options.metaKey || false
|
||||||
|
} as KeyboardEvent);
|
||||||
|
|
||||||
|
it("should match simple key shortcuts", () => {
|
||||||
|
const event = createKeyboardEvent({ key: "a", code: "KeyA" });
|
||||||
|
expect(matchesShortcut(event, "a")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match shortcuts with modifiers", () => {
|
||||||
|
const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
|
||||||
|
expect(matchesShortcut(event, "ctrl+a")).toBe(true);
|
||||||
|
|
||||||
|
const shiftEvent = createKeyboardEvent({ key: "a", code: "KeyA", shiftKey: true });
|
||||||
|
expect(matchesShortcut(shiftEvent, "shift+a")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match complex modifier combinations", () => {
|
||||||
|
const event = createKeyboardEvent({
|
||||||
|
key: "a",
|
||||||
|
code: "KeyA",
|
||||||
|
ctrlKey: true,
|
||||||
|
shiftKey: true
|
||||||
|
});
|
||||||
|
expect(matchesShortcut(event, "ctrl+shift+a")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not match when modifiers don't match", () => {
|
||||||
|
const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
|
||||||
|
expect(matchesShortcut(event, "alt+a")).toBe(false);
|
||||||
|
expect(matchesShortcut(event, "a")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle alternative modifier names", () => {
|
||||||
|
const ctrlEvent = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true });
|
||||||
|
expect(matchesShortcut(ctrlEvent, "control+a")).toBe(true);
|
||||||
|
|
||||||
|
const metaEvent = createKeyboardEvent({ key: "a", code: "KeyA", metaKey: true });
|
||||||
|
expect(matchesShortcut(metaEvent, "cmd+a")).toBe(true);
|
||||||
|
expect(matchesShortcut(metaEvent, "command+a")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty or invalid shortcuts", () => {
|
||||||
|
const event = createKeyboardEvent({ key: "a", code: "KeyA" });
|
||||||
|
expect(matchesShortcut(event, "")).toBe(false);
|
||||||
|
expect(matchesShortcut(event, null as any)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle invalid events", () => {
|
||||||
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
expect(matchesShortcut(null as any, "a")).toBe(false);
|
||||||
|
expect(matchesShortcut({} as KeyboardEvent, "a")).toBe(false);
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalled();
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn about invalid shortcut formats", () => {
|
||||||
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
const event = createKeyboardEvent({ key: "a", code: "KeyA" });
|
||||||
|
|
||||||
|
matchesShortcut(event, "ctrl+");
|
||||||
|
matchesShortcut(event, "+");
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalled();
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bindGlobalShortcut", () => {
|
||||||
|
it("should bind a global shortcut", () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
||||||
|
|
||||||
|
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not bind shortcuts when handler is null", () => {
|
||||||
|
shortcuts.bindGlobalShortcut("ctrl+a", null, "test-namespace");
|
||||||
|
|
||||||
|
expect(mockElement.addEventListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove previous bindings when namespace is reused", () => {
|
||||||
|
const handler1 = vi.fn();
|
||||||
|
const handler2 = vi.fn();
|
||||||
|
|
||||||
|
shortcuts.bindGlobalShortcut("ctrl+a", handler1, "test-namespace");
|
||||||
|
expect(mockElement.addEventListener).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
shortcuts.bindGlobalShortcut("ctrl+b", handler2, "test-namespace");
|
||||||
|
expect(mockElement.removeEventListener).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockElement.addEventListener).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bindElShortcut", () => {
|
||||||
|
it("should bind shortcut to specific element", () => {
|
||||||
|
const mockEl = { addEventListener: vi.fn(), removeEventListener: vi.fn() };
|
||||||
|
const mockJQueryEl = [mockEl] as any;
|
||||||
|
mockJQueryEl.length = 1;
|
||||||
|
|
||||||
|
const handler = vi.fn();
|
||||||
|
shortcuts.bindElShortcut(mockJQueryEl, "ctrl+a", handler, "test-namespace");
|
||||||
|
|
||||||
|
expect(mockEl.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to document when element is empty", () => {
|
||||||
|
const emptyJQuery = [] as any;
|
||||||
|
emptyJQuery.length = 0;
|
||||||
|
|
||||||
|
const handler = vi.fn();
|
||||||
|
shortcuts.bindElShortcut(emptyJQuery, "ctrl+a", handler, "test-namespace");
|
||||||
|
|
||||||
|
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("removeGlobalShortcut", () => {
|
||||||
|
it("should remove shortcuts for a specific namespace", () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
||||||
|
|
||||||
|
shortcuts.removeGlobalShortcut("test-namespace");
|
||||||
|
|
||||||
|
expect(mockElement.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("event handling", () => {
|
||||||
|
it.skip("should call handler when shortcut matches", () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
||||||
|
|
||||||
|
// Get the listener that was registered
|
||||||
|
expect(mockElement.addEventListener.mock.calls).toHaveLength(1);
|
||||||
|
const [, listener] = mockElement.addEventListener.mock.calls[0];
|
||||||
|
|
||||||
|
// First verify that matchesShortcut works directly
|
||||||
|
const testEvent = {
|
||||||
|
type: "keydown",
|
||||||
|
key: "a",
|
||||||
|
code: "KeyA",
|
||||||
|
ctrlKey: true,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
preventDefault: vi.fn(),
|
||||||
|
stopPropagation: vi.fn()
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Test matchesShortcut directly first
|
||||||
|
expect(matchesShortcut(testEvent, "ctrl+a")).toBe(true);
|
||||||
|
|
||||||
|
// Now test the actual listener
|
||||||
|
listener(testEvent);
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalled();
|
||||||
|
expect(testEvent.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(testEvent.stopPropagation).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not call handler for non-keyboard events", () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
||||||
|
|
||||||
|
const [, listener] = mockElement.addEventListener.mock.calls[0];
|
||||||
|
|
||||||
|
// Simulate a non-keyboard event
|
||||||
|
const event = {
|
||||||
|
type: "click"
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
listener(event);
|
||||||
|
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not call handler when shortcut doesn't match", () => {
|
||||||
|
const handler = vi.fn();
|
||||||
|
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
||||||
|
|
||||||
|
const [, listener] = mockElement.addEventListener.mock.calls[0];
|
||||||
|
|
||||||
|
// Simulate a non-matching keydown event
|
||||||
|
const event = {
|
||||||
|
type: "keydown",
|
||||||
|
key: "b",
|
||||||
|
code: "KeyB",
|
||||||
|
ctrlKey: true,
|
||||||
|
altKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
preventDefault: vi.fn(),
|
||||||
|
stopPropagation: vi.fn()
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
listener(event);
|
||||||
|
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,18 @@
|
|||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
|
|
||||||
type ElementType = HTMLElement | Document;
|
type ElementType = HTMLElement | Document;
|
||||||
type Handler = (e: JQuery.TriggeredEvent<ElementType | Element, string, ElementType | Element, ElementType | Element>) => void;
|
type Handler = (e: KeyboardEvent) => void;
|
||||||
|
|
||||||
|
interface ShortcutBinding {
|
||||||
|
element: HTMLElement | Document;
|
||||||
|
shortcut: string;
|
||||||
|
handler: Handler;
|
||||||
|
namespace: string | null;
|
||||||
|
listener: (evt: Event) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store all active shortcut bindings for management
|
||||||
|
const activeBindings: Map<string, ShortcutBinding[]> = new Map();
|
||||||
|
|
||||||
function removeGlobalShortcut(namespace: string) {
|
function removeGlobalShortcut(namespace: string) {
|
||||||
bindGlobalShortcut("", null, namespace);
|
bindGlobalShortcut("", null, namespace);
|
||||||
@@ -15,38 +26,167 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st
|
|||||||
if (utils.isDesktop()) {
|
if (utils.isDesktop()) {
|
||||||
keyboardShortcut = normalizeShortcut(keyboardShortcut);
|
keyboardShortcut = normalizeShortcut(keyboardShortcut);
|
||||||
|
|
||||||
let eventName = "keydown";
|
// If namespace is provided, remove all previous bindings for this namespace
|
||||||
|
|
||||||
if (namespace) {
|
if (namespace) {
|
||||||
eventName += `.${namespace}`;
|
removeNamespaceBindings(namespace);
|
||||||
|
|
||||||
// if there's a namespace, then we replace the existing event handler with the new one
|
|
||||||
$el.off(eventName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted)
|
// Method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted)
|
||||||
if (keyboardShortcut) {
|
if (keyboardShortcut && handler) {
|
||||||
$el.bind(eventName, keyboardShortcut, (e) => {
|
const element = $el.length > 0 ? $el[0] as (HTMLElement | Document) : document;
|
||||||
if (handler) {
|
|
||||||
handler(e);
|
const listener = (evt: Event) => {
|
||||||
|
// Only handle keyboard events
|
||||||
|
if (evt.type !== 'keydown' || !(evt instanceof KeyboardEvent)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
const e = evt as KeyboardEvent;
|
||||||
e.stopPropagation();
|
if (matchesShortcut(e, keyboardShortcut)) {
|
||||||
});
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handler(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the event listener
|
||||||
|
element.addEventListener('keydown', listener);
|
||||||
|
|
||||||
|
// Store the binding for later cleanup
|
||||||
|
const binding: ShortcutBinding = {
|
||||||
|
element,
|
||||||
|
shortcut: keyboardShortcut,
|
||||||
|
handler,
|
||||||
|
namespace,
|
||||||
|
listener
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = namespace || 'global';
|
||||||
|
if (!activeBindings.has(key)) {
|
||||||
|
activeBindings.set(key, []);
|
||||||
|
}
|
||||||
|
activeBindings.get(key)!.push(binding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeNamespaceBindings(namespace: string) {
|
||||||
|
const bindings = activeBindings.get(namespace);
|
||||||
|
if (bindings) {
|
||||||
|
// Remove all event listeners for this namespace
|
||||||
|
bindings.forEach(binding => {
|
||||||
|
binding.element.removeEventListener('keydown', binding.listener);
|
||||||
|
});
|
||||||
|
activeBindings.delete(namespace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesShortcut(e: KeyboardEvent, shortcut: string): boolean {
|
||||||
|
if (!shortcut) return false;
|
||||||
|
|
||||||
|
// Ensure we have a proper KeyboardEvent with key property
|
||||||
|
if (!e || typeof e.key !== 'string') {
|
||||||
|
console.warn('matchesShortcut called with invalid event:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = shortcut.toLowerCase().split('+');
|
||||||
|
const key = parts[parts.length - 1]; // Last part is the actual key
|
||||||
|
const modifiers = parts.slice(0, -1); // Everything before is modifiers
|
||||||
|
|
||||||
|
// Defensive check - ensure we have a valid key
|
||||||
|
if (!key || key.trim() === '') {
|
||||||
|
console.warn('Invalid shortcut format:', shortcut);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the main key matches
|
||||||
|
if (!keyMatches(e, key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check modifiers
|
||||||
|
const expectedCtrl = modifiers.includes('ctrl') || modifiers.includes('control');
|
||||||
|
const expectedAlt = modifiers.includes('alt');
|
||||||
|
const expectedShift = modifiers.includes('shift');
|
||||||
|
const expectedMeta = modifiers.includes('meta') || modifiers.includes('cmd') || modifiers.includes('command');
|
||||||
|
|
||||||
|
return e.ctrlKey === expectedCtrl &&
|
||||||
|
e.altKey === expectedAlt &&
|
||||||
|
e.shiftKey === expectedShift &&
|
||||||
|
e.metaKey === expectedMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keyMatches(e: KeyboardEvent, key: string): boolean {
|
||||||
|
// Defensive check for undefined/null key
|
||||||
|
if (!key) {
|
||||||
|
console.warn('keyMatches called with undefined/null key');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle special key mappings and aliases
|
||||||
|
const keyMap: { [key: string]: string[] } = {
|
||||||
|
'return': ['Enter'],
|
||||||
|
'enter': ['Enter'], // alias for return
|
||||||
|
'del': ['Delete'],
|
||||||
|
'delete': ['Delete'], // alias for del
|
||||||
|
'esc': ['Escape'],
|
||||||
|
'escape': ['Escape'], // alias for esc
|
||||||
|
'space': [' ', 'Space'],
|
||||||
|
'tab': ['Tab'],
|
||||||
|
'backspace': ['Backspace'],
|
||||||
|
'home': ['Home'],
|
||||||
|
'end': ['End'],
|
||||||
|
'pageup': ['PageUp'],
|
||||||
|
'pagedown': ['PageDown'],
|
||||||
|
'up': ['ArrowUp'],
|
||||||
|
'down': ['ArrowDown'],
|
||||||
|
'left': ['ArrowLeft'],
|
||||||
|
'right': ['ArrowRight']
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function keys
|
||||||
|
for (let i = 1; i <= 19; i++) {
|
||||||
|
keyMap[`f${i}`] = [`F${i}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedKeys = keyMap[key.toLowerCase()];
|
||||||
|
if (mappedKeys) {
|
||||||
|
return mappedKeys.includes(e.key) || mappedKeys.includes(e.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For number keys, use the physical key code regardless of modifiers
|
||||||
|
// This works across all keyboard layouts
|
||||||
|
if (key >= '0' && key <= '9') {
|
||||||
|
return e.code === `Digit${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For letter keys, use the physical key code for consistency
|
||||||
|
if (key.length === 1 && key >= 'a' && key <= 'z') {
|
||||||
|
return e.code === `Key${key.toUpperCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For regular keys, check both key and code as fallback
|
||||||
|
return e.key.toLowerCase() === key.toLowerCase() ||
|
||||||
|
e.code.toLowerCase() === key.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize to the form expected by the jquery.hotkeys.js
|
* Simple normalization - just lowercase and trim whitespace
|
||||||
*/
|
*/
|
||||||
function normalizeShortcut(shortcut: string): string {
|
function normalizeShortcut(shortcut: string): string {
|
||||||
if (!shortcut) {
|
if (!shortcut) {
|
||||||
return shortcut;
|
return shortcut;
|
||||||
}
|
}
|
||||||
|
|
||||||
return shortcut.toLowerCase().replace("enter", "return").replace("delete", "del").replace("ctrl+alt", "alt+ctrl").replace("meta+alt", "alt+meta"); // alt needs to be first;
|
const normalized = shortcut.toLowerCase().trim().replace(/\s+/g, '');
|
||||||
|
|
||||||
|
// Warn about potentially problematic shortcuts
|
||||||
|
if (normalized.endsWith('+') || normalized.startsWith('+') || normalized.includes('++')) {
|
||||||
|
console.warn('Potentially malformed shortcut:', shortcut, '-> normalized to:', normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ export default class SpacedUpdate {
|
|||||||
this.lastUpdated = Date.now();
|
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() {
|
triggerUpdate() {
|
||||||
if (!this.changed) {
|
if (!this.changed) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
|
|||||||
const $copyButton = $("<button>")
|
const $copyButton = $("<button>")
|
||||||
.addClass("bx component icon-action tn-tool-button bx-copy copy-button")
|
.addClass("bx component icon-action tn-tool-button bx-copy copy-button")
|
||||||
.attr("title", t("code_block.copy_title"))
|
.attr("title", t("code_block.copy_title"))
|
||||||
.on("click", () => {
|
.on("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
if (!isShare) {
|
if (!isShare) {
|
||||||
copyTextWithToast($codeBlock.text());
|
copyTextWithToast($codeBlock.text());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -78,13 +78,7 @@ function showMessage(message: string, delay = 2000) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAndLogError(message: string, delay = 10000) {
|
export function showError(message: string, delay = 10000) {
|
||||||
showError(message, delay);
|
|
||||||
|
|
||||||
ws.logError(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message: string, delay = 10000) {
|
|
||||||
console.log(utils.now(), "error: ", message);
|
console.log(utils.now(), "error: ", message);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -108,18 +102,10 @@ function showErrorTitleAndMessage(title: string, message: string, delay = 10000)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function throwError(message: string) {
|
|
||||||
ws.logError(message);
|
|
||||||
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
showMessage,
|
showMessage,
|
||||||
showError,
|
showError,
|
||||||
showErrorTitleAndMessage,
|
showErrorTitleAndMessage,
|
||||||
showAndLogError,
|
|
||||||
throwError,
|
|
||||||
showPersistent,
|
showPersistent,
|
||||||
closePersistent
|
closePersistent
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Modal } from "bootstrap";
|
|
||||||
import type { ViewScope } from "./link.js";
|
import type { ViewScope } from "./link.js";
|
||||||
|
|
||||||
const SVG_MIME = "image/svg+xml";
|
const SVG_MIME = "image/svg+xml";
|
||||||
@@ -275,71 +274,6 @@ function getMimeTypeClass(mime: string) {
|
|||||||
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
|
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeActiveDialog() {
|
|
||||||
if (glob.activeDialog) {
|
|
||||||
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
|
|
||||||
glob.activeDialog = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let $lastFocusedElement: JQuery<HTMLElement> | null;
|
|
||||||
|
|
||||||
// perhaps there should be saved focused element per tab?
|
|
||||||
function saveFocusedElement() {
|
|
||||||
$lastFocusedElement = $(":focus");
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusSavedElement() {
|
|
||||||
if (!$lastFocusedElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($lastFocusedElement.hasClass("ck")) {
|
|
||||||
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
|
|
||||||
// the bug manifests itself in resetting the cursor position to the first character - jumping above
|
|
||||||
|
|
||||||
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
|
|
||||||
|
|
||||||
if (editor) {
|
|
||||||
editor.editing.view.focus();
|
|
||||||
} else {
|
|
||||||
console.log("Could not find CKEditor instance to focus last element");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$lastFocusedElement.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
$lastFocusedElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
|
|
||||||
if (closeActDialog) {
|
|
||||||
closeActiveDialog();
|
|
||||||
glob.activeDialog = $dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveFocusedElement();
|
|
||||||
Modal.getOrCreateInstance($dialog[0]).show();
|
|
||||||
|
|
||||||
$dialog.on("hidden.bs.modal", () => {
|
|
||||||
const $autocompleteEl = $(".aa-input");
|
|
||||||
if ("autocomplete" in $autocompleteEl) {
|
|
||||||
$autocompleteEl.autocomplete("close");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!glob.activeDialog || glob.activeDialog === $dialog) {
|
|
||||||
focusSavedElement();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Fix once keyboard_actions is ported.
|
|
||||||
// @ts-ignore
|
|
||||||
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
|
|
||||||
keyboardActionsService.updateDisplayedShortcuts($dialog);
|
|
||||||
|
|
||||||
return $dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHtmlEmpty(html: string) {
|
function isHtmlEmpty(html: string) {
|
||||||
if (!html) {
|
if (!html) {
|
||||||
return true;
|
return true;
|
||||||
@@ -825,10 +759,6 @@ export default {
|
|||||||
setCookie,
|
setCookie,
|
||||||
getNoteTypeClass,
|
getNoteTypeClass,
|
||||||
getMimeTypeClass,
|
getMimeTypeClass,
|
||||||
closeActiveDialog,
|
|
||||||
openDialog,
|
|
||||||
saveFocusedElement,
|
|
||||||
focusSavedElement,
|
|
||||||
isHtmlEmpty,
|
isHtmlEmpty,
|
||||||
clearBrowserCache,
|
clearBrowserCache,
|
||||||
copySelectionToClipboard,
|
copySelectionToClipboard,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
|
|||||||
let lastPingTs: number;
|
let lastPingTs: number;
|
||||||
let frontendUpdateDataQueue: EntityChange[] = [];
|
let frontendUpdateDataQueue: EntityChange[] = [];
|
||||||
|
|
||||||
function logError(message: string) {
|
export function logError(message: string) {
|
||||||
console.error(utils.now(), message); // needs to be separate from .trace()
|
console.error(utils.now(), message); // needs to be separate from .trace()
|
||||||
|
|
||||||
if (ws && ws.readyState === 1) {
|
if (ws && ws.readyState === 1) {
|
||||||
@@ -301,6 +301,12 @@ setTimeout(() => {
|
|||||||
setInterval(sendPing, 1000);
|
setInterval(sendPing, 1000);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
export function throwError(message: string) {
|
||||||
|
logError(message);
|
||||||
|
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
logError,
|
logError,
|
||||||
subscribeToMessages,
|
subscribeToMessages,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import "jquery";
|
import "jquery";
|
||||||
import "jquery-hotkeys";
|
|
||||||
import utils from "./services/utils.js";
|
import utils from "./services/utils.js";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import "./stylesheets/bootstrap.scss";
|
import "./stylesheets/bootstrap.scss";
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ async function formatCodeBlocks() {
|
|||||||
await formatCodeBlocks($("#content"));
|
await formatCodeBlocks($("#content"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setupTextNote() {
|
||||||
|
formatCodeBlocks();
|
||||||
|
applyMath();
|
||||||
|
|
||||||
|
const setupMermaid = (await import("./share/mermaid.js")).default;
|
||||||
|
setupMermaid();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch note with given ID from backend
|
* Fetch note with given ID from backend
|
||||||
*
|
*
|
||||||
@@ -47,8 +55,11 @@ async function fetchNote(noteId: string | null = null) {
|
|||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"DOMContentLoaded",
|
"DOMContentLoaded",
|
||||||
() => {
|
() => {
|
||||||
formatCodeBlocks();
|
const noteType = determineNoteType();
|
||||||
applyMath();
|
|
||||||
|
if (noteType === "text") {
|
||||||
|
setupTextNote();
|
||||||
|
}
|
||||||
|
|
||||||
const toggleMenuButton = document.getElementById("toggleMenuButton");
|
const toggleMenuButton = document.getElementById("toggleMenuButton");
|
||||||
const layout = document.getElementById("layout");
|
const layout = document.getElementById("layout");
|
||||||
@@ -60,6 +71,12 @@ document.addEventListener(
|
|||||||
false
|
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:
|
// workaround to prevent webpack from removing "fetchNote" as dead code:
|
||||||
// add fetchNote as property to the window object
|
// add fetchNote as property to the window object
|
||||||
Object.defineProperty(window, "fetchNote", {
|
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. --------------------- */
|
/* -- Overrides the default colors used by the ckeditor5-image package. --------------------- */
|
||||||
|
|
||||||
--ck-color-image-caption-background: var(--main-background-color);
|
--ck-content-color-image-caption-background: var(--main-background-color);
|
||||||
--ck-color-image-caption-text: var(--main-text-color);
|
--ck-content-color-image-caption-text: var(--main-text-color);
|
||||||
|
|
||||||
/* -- Overrides the default colors used by the ckeditor5-widget package. -------------------- */
|
/* -- Overrides the default colors used by the ckeditor5-widget package. -------------------- */
|
||||||
|
|
||||||
|
|||||||
@@ -320,3 +320,8 @@ h6 {
|
|||||||
page-break-after: avoid;
|
page-break-after: avoid;
|
||||||
break-after: avoid;
|
break-after: avoid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
figure.table {
|
||||||
|
/* Workaround for https://github.com/ckeditor/ckeditor5/issues/18903. Remove once official fix is released */
|
||||||
|
display: table !important;
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
--bs-body-font-weight: var(--main-font-weight) !important;
|
--bs-body-font-weight: var(--main-font-weight) !important;
|
||||||
--bs-body-color: var(--main-text-color) !important;
|
--bs-body-color: var(--main-text-color) !important;
|
||||||
--bs-body-bg: var(--main-background-color) !important;
|
--bs-body-bg: var(--main-background-color) !important;
|
||||||
|
--ck-mention-list-max-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
@@ -138,12 +139,6 @@ textarea,
|
|||||||
color: var(--muted-text-color);
|
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 */
|
/* Add a gap between consecutive radios / check boxes */
|
||||||
label.tn-radio + label.tn-radio,
|
label.tn-radio + label.tn-radio,
|
||||||
label.tn-checkbox + label.tn-checkbox {
|
label.tn-checkbox + label.tn-checkbox {
|
||||||
@@ -191,6 +186,13 @@ samp {
|
|||||||
font-family: var(--monospace-font-family) !important;
|
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 {
|
.input-group-text {
|
||||||
background-color: var(--accented-background-color) !important;
|
background-color: var(--accented-background-color) !important;
|
||||||
color: var(--muted-text-color) !important;
|
color: var(--muted-text-color) !important;
|
||||||
@@ -319,7 +321,8 @@ button kbd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu,
|
||||||
|
.tabulator-popup-container {
|
||||||
color: var(--menu-text-color) !important;
|
color: var(--menu-text-color) !important;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
background-color: var(--menu-background-color) !important;
|
background-color: var(--menu-background-color) !important;
|
||||||
@@ -329,7 +332,13 @@ button kbd {
|
|||||||
--bs-dropdown-link-active-bg: var(--active-item-background-color) !important;
|
--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);
|
border: 1px solid var(--dropdown-border-color);
|
||||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
||||||
animation: dropdown-menu-opening 100ms ease-in;
|
animation: dropdown-menu-opening 100ms ease-in;
|
||||||
@@ -372,7 +381,8 @@ body.desktop .dropdown-menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu a:hover:not(.disabled),
|
.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;
|
color: var(--hover-item-text-color) !important;
|
||||||
background-color: var(--hover-item-background-color) !important;
|
background-color: var(--hover-item-background-color) !important;
|
||||||
border-color: var(--hover-item-border-color) !important;
|
border-color: var(--hover-item-border-color) !important;
|
||||||
@@ -391,7 +401,7 @@ body.desktop .dropdown-menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
|
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
|
||||||
body.desktop #context-menu-container .dropdown-item > span {
|
body #context-menu-container .dropdown-item > span {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -439,10 +449,11 @@ body.desktop #context-menu-container .dropdown-item > span {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
|
font-size: var(--monospace-font-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
body .cm-editor {
|
.cm-scroller {
|
||||||
font-size: var(--monospace-font-size);
|
font-family: var(--monospace-font-family) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body .cm-editor .cm-gutters {
|
body .cm-editor .cm-gutters {
|
||||||
@@ -526,6 +537,7 @@ button.btn-sm {
|
|||||||
/* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */
|
/* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre:not(.hljs) {
|
pre:not(.hljs) {
|
||||||
@@ -757,6 +769,14 @@ table.promoted-attributes-in-tooltip th {
|
|||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-tooltip-content .open-popup-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
bottom: 8px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.note-tooltip-attributes {
|
.note-tooltip-attributes {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
@@ -898,6 +918,13 @@ div[data-notify="container"] {
|
|||||||
font-family: var(--monospace-font-family);
|
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 {
|
.ck-content .table table th {
|
||||||
background-color: var(--accented-background-color);
|
background-color: var(--accented-background-color);
|
||||||
}
|
}
|
||||||
@@ -1184,12 +1211,14 @@ body.mobile .dropdown-submenu > .dropdown-menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#context-menu-container,
|
#context-menu-container,
|
||||||
#context-menu-container .dropdown-menu {
|
#context-menu-container .dropdown-menu,
|
||||||
padding: 3px 0 0;
|
.tabulator-popup-container {
|
||||||
|
padding: 3px 0;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
#context-menu-container .dropdown-item {
|
#context-menu-container .dropdown-item,
|
||||||
|
.tabulator-menu .tabulator-menu-item {
|
||||||
padding: 0 7px 0 10px;
|
padding: 0 7px 0 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -1273,6 +1302,29 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Slash commands */
|
||||||
|
|
||||||
|
.ck.ck-slash-command-button {
|
||||||
|
padding: 0.5em 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-slash-command-button__text-part,
|
||||||
|
.ck.ck-template-form__text-part {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
line-height: 1.2em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-slash-command-button__text-part > span,
|
||||||
|
.ck.ck-template-form__text-part > span {
|
||||||
|
line-height: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ck.ck-slash-command-button__text-part .ck.ck-slash-command-button__description,
|
||||||
|
.ck.ck-template-form__text-part .ck-template-form__description {
|
||||||
|
display: block;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
.area-expander {
|
.area-expander {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -1728,6 +1780,54 @@ textarea {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Command palette styling */
|
||||||
|
.jump-to-note-dialog .command-suggestion {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-to-note-dialog .aa-suggestion .command-suggestion,
|
||||||
|
.jump-to-note-dialog .aa-suggestion .command-suggestion div {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-to-note-dialog .aa-cursor .command-suggestion,
|
||||||
|
.jump-to-note-dialog .aa-suggestion:hover .command-suggestion {
|
||||||
|
border-left-color: var(--link-color);
|
||||||
|
background-color: var(--hover-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-to-note-dialog .command-icon {
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-to-note-dialog .command-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-to-note-dialog .command-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-to-note-dialog .command-description {
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.3;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-to-note-dialog kbd.command-shortcut {
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
opacity: 0.75;
|
||||||
|
font-family: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-table-placeholder {
|
.empty-table-placeholder {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
@@ -1837,12 +1937,14 @@ body.zen .note-title-widget input {
|
|||||||
|
|
||||||
/* Content renderer */
|
/* Content renderer */
|
||||||
|
|
||||||
footer.file-footer {
|
footer.file-footer,
|
||||||
|
footer.webview-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer.file-footer button {
|
footer.file-footer button,
|
||||||
|
footer.webview-footer button {
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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;
|
--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-background-color: var(--card-background-color);
|
||||||
--promoted-attribute-card-shadow-color: #000000b3;
|
--promoted-attribute-card-shadow-color: #000000b3;
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,9 @@
|
|||||||
|
|
||||||
--alert-bar-background: #32637b29;
|
--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-background-color: var(--card-background-color);
|
||||||
--promoted-attribute-card-shadow-color: #00000033;
|
--promoted-attribute-card-shadow-color: #00000033;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@import url(./pages.css);
|
@import url(./pages.css);
|
||||||
@import url(./ribbon.css);
|
@import url(./ribbon.css);
|
||||||
@import url(./notes/text.css);
|
@import url(./notes/text.css);
|
||||||
|
@import url(./notes/collections/table.css);
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Inter";
|
font-family: "Inter";
|
||||||
@@ -171,9 +172,19 @@ html body .dropdown-item[disabled] {
|
|||||||
opacity: var(--menu-item-disabled-opacity);
|
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 */
|
/* Menu item icon */
|
||||||
.dropdown-item .bx {
|
.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;
|
color: var(--menu-item-icon-color) !important;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
@@ -447,6 +458,11 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-list-wrapper .note-book-card .note-book-content.type-image .rendered-content,
|
||||||
|
.note-list-wrapper .note-book-card .note-book-content.type-pdf .rendered-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.note-list-wrapper .note-book-card .note-book-content .rendered-content.text-with-ellipsis {
|
.note-list-wrapper .note-book-card .note-book-content .rendered-content.text-with-ellipsis {
|
||||||
padding: 1rem !important;
|
padding: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,10 +128,15 @@ div.tn-tool-dialog {
|
|||||||
|
|
||||||
.jump-to-note-dialog .modal-header {
|
.jump-to-note-dialog .modal-header {
|
||||||
padding: unset !important;
|
padding: unset !important;
|
||||||
|
padding-bottom: 26px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jump-to-note-dialog .modal-body {
|
.jump-to-note-dialog .modal-body {
|
||||||
padding: 26px 0 !important;
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-to-note-dialog .modal-footer {
|
||||||
|
padding-top: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search box wrapper */
|
/* Search box wrapper */
|
||||||
@@ -382,6 +387,10 @@ div.tn-tool-dialog {
|
|||||||
|
|
||||||
/* DELETE NOTE PREVIEW DIALOG */
|
/* DELETE NOTE PREVIEW DIALOG */
|
||||||
|
|
||||||
|
.delete-notes-dialog .modal-dialog {
|
||||||
|
--bs-modal-width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
.delete-notes-list .note-path {
|
.delete-notes-list .note-path {
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
@@ -396,3 +405,19 @@ div.tn-tool-dialog {
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE TYPE CHOOSER DIALOG
|
||||||
|
*/
|
||||||
|
|
||||||
|
.note-type-chooser-dialog div.note-type-dropdown {
|
||||||
|
/* Disable the active item highlighting since there is no use for it here */
|
||||||
|
--active-item-text-color: initial;
|
||||||
|
--active-item-background-color: initial;
|
||||||
|
|
||||||
|
font-size: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-type-chooser-dialog div.note-type-dropdown .dropdown-item span.bx {
|
||||||
|
margin-right: .25em;
|
||||||
|
}
|
||||||
@@ -267,7 +267,7 @@ input::selection,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-group button:focus-visible,
|
.input-group button:focus-visible,
|
||||||
.input-group a:focus-visible {
|
.input-group a:focus-visible:not(.dropdown-item) {
|
||||||
box-shadow: unset;
|
box-shadow: unset;
|
||||||
outline: transparent;
|
outline: transparent;
|
||||||
border: transparent;
|
border: transparent;
|
||||||
@@ -349,7 +349,7 @@ select:hover,
|
|||||||
select.form-select:hover,
|
select.form-select:hover,
|
||||||
select.form-control:hover,
|
select.form-control:hover,
|
||||||
.select-button.dropdown-toggle.btn:hover {
|
.select-button.dropdown-toggle.btn:hover {
|
||||||
background: var(--input-hover-background) var(--dropdown-arrow);
|
background: var(--input-hover-background) var(--dropdown-arrow,);
|
||||||
color: var(--input-hover-color);
|
color: var(--input-hover-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -201,6 +201,11 @@
|
|||||||
color: var(--menu-item-icon-color);
|
color: var(--menu-item-icon-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Slash commands */
|
||||||
|
.ck.ck-slash-command-button__text-part .ck.ck-button__label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
/* Separator */
|
/* Separator */
|
||||||
:root .ck .ck-list__separator {
|
:root .ck .ck-list__separator {
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ div.note-detail-empty {
|
|||||||
border: unset;
|
border: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* NOTE ATTACHMENTS */
|
||||||
|
|
||||||
|
.attachment-list div.links-wrapper {
|
||||||
|
font-size: unset;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* OPTIONS PAGES
|
* OPTIONS PAGES
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ div.promoted-attributes-container {
|
|||||||
.image-properties > div:first-child > span > strong {
|
.image-properties > div:first-child > span > strong {
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-info-widget-table td,
|
||||||
|
.file-properties-widget .file-table td {
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-properties-widget {
|
.file-properties-widget {
|
||||||
|
|||||||
@@ -71,12 +71,13 @@ body.background-effects.platform-win32.layout-vertical #vertical-main-container
|
|||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* Matches when the left pane is collapsed */
|
/* 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;
|
--center-pane-border-radius: 0;
|
||||||
--tab-first-item-horiz-offset: 5px;
|
--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);
|
border-right: 2px solid var(--left-pane-collapsed-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +355,7 @@ body.layout-horizontal > .horizontal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button {
|
.calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button {
|
||||||
--select-arrow-svg: ""; /* Disable the dropdown arrow */
|
--select-arrow-svg: initial; /* Disable the dropdown arrow */
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
@@ -1145,12 +1146,18 @@ body.mobile .note-title {
|
|||||||
|
|
||||||
/* The "Change note icon" button */
|
/* The "Change note icon" button */
|
||||||
|
|
||||||
.note-icon-widget .note-icon {
|
:root .note-icon-widget button.note-icon,
|
||||||
|
:root .note-icon-widget button.note-icon:hover {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-icon-widget .note-icon:hover {
|
/* Dropdown open */
|
||||||
|
:root .note-icon-widget button.note-icon.show {
|
||||||
|
background: var(--ck-editor-toolbar-dropdown-button-open-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root .note-icon-widget button.note-icon:not(:disabled):hover {
|
||||||
background: var(--icon-button-hover-background);
|
background: var(--icon-button-hover-background);
|
||||||
color: var(--icon-button-hover-color);
|
color: var(--icon-button-hover-color);
|
||||||
}
|
}
|
||||||
@@ -1294,9 +1301,9 @@ div.promoted-attribute-cell .tn-checkbox {
|
|||||||
height: 1cap;
|
height: 1cap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The <div> containing the checkbox for a promoted boolean attribute */
|
/* Relocate the checkbox before the label */
|
||||||
div.promoted-attribute-cell div:has(input[type="checkbox"]) {
|
div.promoted-attribute-cell.promoted-attribute-label-boolean > div:first-of-type {
|
||||||
order: -1; /* Relocate the checkbox before the label */
|
order: -1;
|
||||||
margin-right: 1.5em;
|
margin-right: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1672,3 +1679,41 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
transition: none;
|
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;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user