mirror of
https://github.com/zadam/trilium.git
synced 2025-11-02 03:16:11 +01:00
Compare commits
749 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd25c735c1 | ||
|
|
7de33907c5 | ||
|
|
a3014434cf | ||
|
|
3ebab2c126 | ||
|
|
954619bd36 | ||
|
|
6995fbfd06 | ||
|
|
83b72eafa6 | ||
|
|
757a6777be | ||
|
|
d003e91b89 | ||
|
|
4a35df745a | ||
|
|
713a0f5b09 | ||
|
|
2cf9c98b43 | ||
|
|
d7af196a0c | ||
|
|
c363be57b7 | ||
|
|
10645790de | ||
|
|
8b18cf382c | ||
|
|
7a131e0bcc | ||
|
|
3d264379cc | ||
|
|
f405682ec1 | ||
|
|
3debf3ce1c | ||
|
|
5a76883969 | ||
|
|
6f51c5e0cc | ||
|
|
2c730d1f0b | ||
|
|
d487da0b2f | ||
|
|
cb8a5cbb62 | ||
|
|
ceb08593d8 | ||
|
|
9dd0eb7b9b | ||
|
|
ebff644d24 | ||
|
|
beb1c15fa5 | ||
|
|
40a5eee211 | ||
|
|
8f393d0bae | ||
|
|
94dad49e2f | ||
|
|
409638151c | ||
|
|
0d3de92890 | ||
|
|
5d619131ec | ||
|
|
e2c8443778 | ||
|
|
daa4743967 | ||
|
|
56553078ef | ||
|
|
5584a06cb3 | ||
|
|
cfeb69ace6 | ||
|
|
b0c8f110de | ||
|
|
aba1266c45 | ||
|
|
c331e0103d | ||
|
|
13978574e0 | ||
|
|
be85963558 | ||
|
|
8c19261ced | ||
|
|
7ca17fa609 | ||
|
|
3d107572df | ||
|
|
f7488655a7 | ||
|
|
876e0a29d4 | ||
|
|
af74375695 | ||
|
|
896965fec5 | ||
|
|
ba5ef93c1a | ||
|
|
ef1153d336 | ||
|
|
0d347f8823 | ||
|
|
897cdc26ae | ||
|
|
aba621c099 | ||
|
|
839813ebde | ||
|
|
545e2ddbfc | ||
|
|
1d63a5903a | ||
|
|
2b34c00a0c | ||
|
|
123068062a | ||
|
|
9a668e8709 | ||
|
|
f6f8937d64 | ||
|
|
c9f53a2880 | ||
|
|
2887e712c3 | ||
|
|
5d3a0ed1b4 | ||
|
|
334b6319de | ||
|
|
4c118c0fd4 | ||
|
|
db00d60684 | ||
|
|
25b74af363 | ||
|
|
eb57cf97ad | ||
|
|
c92e24363f | ||
|
|
8d5d00ac0f | ||
|
|
8b457384ba | ||
|
|
fab2d53ece | ||
|
|
774f27d8d2 | ||
|
|
d7f02ef1b3 | ||
|
|
97eaa6294c | ||
|
|
dc02bb0850 | ||
|
|
2c8c041e1c | ||
|
|
874b1c6654 | ||
|
|
fb982c7097 | ||
|
|
b7f5ce600e | ||
|
|
91604c9e26 | ||
|
|
c874333a37 | ||
|
|
1298b968f2 | ||
|
|
6fe5a854a7 | ||
|
|
aba3b5cb19 | ||
|
|
282aed22b5 | ||
|
|
669a3d9dcf | ||
|
|
9d7455d28a | ||
|
|
4f0c8b081c | ||
|
|
a5db5298a0 | ||
|
|
876c6e9252 | ||
|
|
aef824d262 | ||
|
|
a25ce42490 | ||
|
|
8b0fdaccf4 | ||
|
|
bd840a2421 | ||
|
|
27d515f289 | ||
|
|
df3b9faf8d | ||
|
|
0f129734ae | ||
|
|
275aacfba9 | ||
|
|
e7f47a0663 | ||
|
|
66486541fe | ||
|
|
34f1a84769 | ||
|
|
2244f0368f | ||
|
|
9d85005255 | ||
|
|
ad8629dca6 | ||
|
|
cccfe0e05a | ||
|
|
a8874257e8 | ||
|
|
f689c55f56 | ||
|
|
853c7be8b8 | ||
|
|
823df1e12d | ||
|
|
7570f818e9 | ||
|
|
03aa5aea2c | ||
|
|
a4e86ac353 | ||
|
|
cf6efc050a | ||
|
|
3e0802176b | ||
|
|
697954d4d9 | ||
|
|
741f6c1114 | ||
|
|
b2237ffa51 | ||
|
|
7b6d11bffa | ||
|
|
97565e8f36 | ||
|
|
c0dfee8439 | ||
|
|
fc98240614 | ||
|
|
169d1203c2 | ||
|
|
f3350bc8f5 | ||
|
|
504a19275c | ||
|
|
14cdc52670 | ||
|
|
cf8063f311 | ||
|
|
aa8902f5b9 | ||
|
|
7cd0e664ac | ||
|
|
a04804d3fa | ||
|
|
86f90e6685 | ||
|
|
8131a4b3d2 | ||
|
|
b91a3e13b0 | ||
|
|
5a7a0d32d1 | ||
|
|
3f5df18d6c | ||
|
|
df2cede075 | ||
|
|
4321c161ac | ||
|
|
b1f0c64ef2 | ||
|
|
c9b37dcc77 | ||
|
|
ab093ed9a0 | ||
|
|
cf31367acd | ||
|
|
e3d306cac3 | ||
|
|
960d321019 | ||
|
|
2d4ac93221 | ||
|
|
d4a4f15416 | ||
|
|
504a842d37 | ||
|
|
ded5b1f5d2 | ||
|
|
fcbbc21a80 | ||
|
|
38fce25b86 | ||
|
|
4cc2fa5300 | ||
|
|
4a82c3f65a | ||
|
|
b255d70e18 | ||
|
|
caa842cd55 | ||
|
|
cd338085fb | ||
|
|
e703ce92a8 | ||
|
|
84479a2c2a | ||
|
|
c13969217c | ||
|
|
402540f483 | ||
|
|
8c56315313 | ||
|
|
b29c3eff6e | ||
|
|
ec7dacfc9b | ||
|
|
5f9a6a9f76 | ||
|
|
28f4aea3d5 | ||
|
|
8d29c5fe1b | ||
|
|
ccd935b562 | ||
|
|
d77a49857b | ||
|
|
e30478e5d4 | ||
|
|
71863752cd | ||
|
|
e4a2a8e56d | ||
|
|
0f1c505823 | ||
|
|
1ecce11113 | ||
|
|
2287d67fb5 | ||
|
|
5b4f17ef3d | ||
|
|
3720ab6df6 | ||
|
|
3c893d69e5 | ||
|
|
b93a4a3e42 | ||
|
|
23cef0ab94 | ||
|
|
c8ffb8d694 | ||
|
|
08e08d8920 | ||
|
|
7acd300163 | ||
|
|
d8d95db4ec | ||
|
|
af97d3ef1d | ||
|
|
c65ec14943 | ||
|
|
adfdc7edb4 | ||
|
|
8cced607eb | ||
|
|
5dd5af90c2 | ||
|
|
7a48333b4f | ||
|
|
7044533398 | ||
|
|
560aad8df6 | ||
|
|
36c2099b2e | ||
|
|
6c157675d7 | ||
|
|
458d66cb21 | ||
|
|
201e8911c5 | ||
|
|
1b1ed2408f | ||
|
|
62487d21d8 | ||
|
|
bc752bdb0b | ||
|
|
9e00d421fb | ||
|
|
e7f02fe22b | ||
|
|
6d694f8e53 | ||
|
|
977befd0a7 | ||
|
|
1566ae4fbd | ||
|
|
4e97490cc6 | ||
|
|
446d5a0fcc | ||
|
|
1fd6465012 | ||
|
|
6cea8e3b87 | ||
|
|
28a63e0326 | ||
|
|
b73da46111 | ||
|
|
abafa8c2d2 | ||
|
|
4ae3272cdf | ||
|
|
6aa3b8dbd7 | ||
|
|
395e9b2228 | ||
|
|
be33f68c52 | ||
|
|
29d96381fa | ||
|
|
da8eecf774 | ||
|
|
de91326c12 | ||
|
|
ee1c3c35d7 | ||
|
|
70eece1429 | ||
|
|
b4f2be332b | ||
|
|
23fe76989b | ||
|
|
275d07659d | ||
|
|
a901e92573 | ||
|
|
6ead31b45f | ||
|
|
d4ce12dca9 | ||
|
|
bb6e22cdb7 | ||
|
|
2c9fc4812e | ||
|
|
60f4554afa | ||
|
|
3c486bfd1b | ||
|
|
26b9a95bb2 | ||
|
|
f7c9217cea | ||
|
|
e92022b73c | ||
|
|
61ff2353c8 | ||
|
|
c8cca26ca4 | ||
|
|
aa556ed4d5 | ||
|
|
5d694a7bdf | ||
|
|
c4787dae23 | ||
|
|
9f5f329c53 | ||
|
|
f82b96fcc4 | ||
|
|
d4b24fa427 | ||
|
|
c852f67c59 | ||
|
|
92c228a3c9 | ||
|
|
42f948e2b3 | ||
|
|
13e8932117 | ||
|
|
910d34bd42 | ||
|
|
b204ba29e7 | ||
|
|
d49244cbc8 | ||
|
|
ef2f2f17b4 | ||
|
|
b9f21dcf4c | ||
|
|
808fe690cc | ||
|
|
901eec04e5 | ||
|
|
9272394ada | ||
|
|
4457982fae | ||
|
|
7f67b2b461 | ||
|
|
7f3934f4c3 | ||
|
|
a3b80a2cc4 | ||
|
|
6d967e5e51 | ||
|
|
b674ca90d1 | ||
|
|
95edb60a84 | ||
|
|
40add78ccb | ||
|
|
1029c24c06 | ||
|
|
94d94fe8fb | ||
|
|
49489c0f45 | ||
|
|
215833a2c9 | ||
|
|
a7471a3d47 | ||
|
|
909aaefbd7 | ||
|
|
15c2f56bf2 | ||
|
|
84cdfec415 | ||
|
|
91572ab8b9 | ||
|
|
ed758f4c92 | ||
|
|
f1fc15e115 | ||
|
|
22300e8151 | ||
|
|
292646e14a | ||
|
|
b4921a20d8 | ||
|
|
54be79a725 | ||
|
|
4fc47370fe | ||
|
|
9e30bcf233 | ||
|
|
e5712c54e6 | ||
|
|
2a4fe21a39 | ||
|
|
b259558f0f | ||
|
|
e2f6d9e0d6 | ||
|
|
4fc2b0fa5e | ||
|
|
8dca79ecf2 | ||
|
|
c7f49f0e21 | ||
|
|
bce2094fb2 | ||
|
|
65c33e1aa0 | ||
|
|
8e108bc5e2 | ||
|
|
4e75ce7fdb | ||
|
|
1e42574d28 | ||
|
|
85ebaf6afa | ||
|
|
661c7e4056 | ||
|
|
1e8ea54dbc | ||
|
|
ddbe7e9936 | ||
|
|
cab86175ef | ||
|
|
ec7414b174 | ||
|
|
8343a5d1dd | ||
|
|
18c55784c7 | ||
|
|
39eac83d38 | ||
|
|
55bd6fb57d | ||
|
|
6fdec52332 | ||
|
|
824a3c5fcc | ||
|
|
87da644027 | ||
|
|
4f42f543d8 | ||
|
|
97ea3ac3fc | ||
|
|
f04b75fd36 | ||
|
|
f5bffc38f1 | ||
|
|
27738acefc | ||
|
|
59ce2072c5 | ||
|
|
ed68dda70b | ||
|
|
892ab02f06 | ||
|
|
7d9196d5e1 | ||
|
|
dccdb5ceb7 | ||
|
|
f961698e44 | ||
|
|
278fe3262e | ||
|
|
1fc860b052 | ||
|
|
88a8311173 | ||
|
|
63dc5697dd | ||
|
|
b595d1fade | ||
|
|
d91c59b7d0 | ||
|
|
aa2ab0da31 | ||
|
|
91f94106fb | ||
|
|
308f319138 | ||
|
|
fa0c01591a | ||
|
|
cb5a771490 | ||
|
|
0c17a13462 | ||
|
|
04593cb2d7 | ||
|
|
b6f50b6af0 | ||
|
|
fc454cba03 | ||
|
|
6f165df29e | ||
|
|
d16468071d | ||
|
|
20a492523f | ||
|
|
1216f51c78 | ||
|
|
ea3ac1041b | ||
|
|
d838e8baf0 | ||
|
|
60a7347d7d | ||
|
|
4e05e79426 | ||
|
|
aa872f47f2 | ||
|
|
fbd833ad86 | ||
|
|
bee65ed32c | ||
|
|
5adca76a9a | ||
|
|
e7467f6446 | ||
|
|
e49473fbd3 | ||
|
|
bfec44aa5a | ||
|
|
55b3bf6036 | ||
|
|
c9c07f0cb0 | ||
|
|
e25727441d | ||
|
|
51b7955ccd | ||
|
|
196bba9cda | ||
|
|
430ed78d85 | ||
|
|
2d11ed805d | ||
|
|
f55426bdb0 | ||
|
|
87b5068fec | ||
|
|
9ddd1a4ae2 | ||
|
|
736bc9c9bd | ||
|
|
5a2da62992 | ||
|
|
1a72eb91ee | ||
|
|
0d3c5b06e2 | ||
|
|
035b72a08d | ||
|
|
fc4a595725 | ||
|
|
444969bcf4 | ||
|
|
2cb6b14eca | ||
|
|
468b5022a4 | ||
|
|
c1897563ca | ||
|
|
5e533896b9 | ||
|
|
d3ceb7cfc1 | ||
|
|
731f74f421 | ||
|
|
46d82651a3 | ||
|
|
b3108c7e2b | ||
|
|
0cb988470e | ||
|
|
5a030014b0 | ||
|
|
2a43ef4dae | ||
|
|
6b5f9fc6ff | ||
|
|
b3a156c20d | ||
|
|
24340d3a8e | ||
|
|
2fac2a8c5e | ||
|
|
decb0c702d | ||
|
|
d45ff6cca5 | ||
|
|
83833e668c | ||
|
|
2cc181d1ac | ||
|
|
a946ce3534 | ||
|
|
3e9f476b37 | ||
|
|
de65c748a4 | ||
|
|
8a2bfb9d7b | ||
|
|
a1ced31fea | ||
|
|
989a9f506e | ||
|
|
59d55e2489 | ||
|
|
2b312a9234 | ||
|
|
16d9b982c2 | ||
|
|
a5600e75f5 | ||
|
|
f91dea62b6 | ||
|
|
4915ffcf2a | ||
|
|
9dbea2aa18 | ||
|
|
45f6a70fb8 | ||
|
|
96b4c611cc | ||
|
|
4e559d6594 | ||
|
|
db1a599f95 | ||
|
|
040964bbb7 | ||
|
|
dc6a303154 | ||
|
|
f88f14c983 | ||
|
|
f870649256 | ||
|
|
ed4dc30a6e | ||
|
|
ce9010ff13 | ||
|
|
994e9fa852 | ||
|
|
9df7d6227e | ||
|
|
242a576548 | ||
|
|
c1a5808f37 | ||
|
|
5c6bb99d78 | ||
|
|
63c408c45b | ||
|
|
2a665dffbc | ||
|
|
6509acd6ee | ||
|
|
4853d45609 | ||
|
|
fe78c1fee3 | ||
|
|
8102172557 | ||
|
|
a1341e6036 | ||
|
|
d31af2ddc2 | ||
|
|
a563330136 | ||
|
|
a58e5789bc | ||
|
|
68e258f23b | ||
|
|
dd18866156 | ||
|
|
1b1f1957c3 | ||
|
|
ff6b4effbd | ||
|
|
06fa59239c | ||
|
|
557bfbd1d6 | ||
|
|
f5a6dfa629 | ||
|
|
ce33dfb003 | ||
|
|
7b1c058d29 | ||
|
|
04c8f8a123 | ||
|
|
d15fccb1d8 | ||
|
|
229dd9cd18 | ||
|
|
a4faaa406b | ||
|
|
b6d2de54b2 | ||
|
|
d5e81d77a2 | ||
|
|
939e99637f | ||
|
|
579a261612 | ||
|
|
6d03304cbb | ||
|
|
b8d41b3421 | ||
|
|
6a5bb1f5c8 | ||
|
|
cd742a4617 | ||
|
|
54063b97ad | ||
|
|
7abb67e737 | ||
|
|
00fd1ba137 | ||
|
|
7ea37b9eb9 | ||
|
|
b749de8fe1 | ||
|
|
8efef6842d | ||
|
|
dc206f38d5 | ||
|
|
29a00a6c0e | ||
|
|
fe678230a8 | ||
|
|
9cdbeb061f | ||
|
|
6c308f35c1 | ||
|
|
34b89cf2e8 | ||
|
|
b566a188dc | ||
|
|
998432e236 | ||
|
|
1af8edfe4d | ||
|
|
5bf01106c5 | ||
|
|
a45289e385 | ||
|
|
4ffd005b09 | ||
|
|
e6ca89fea8 | ||
|
|
2225aea756 | ||
|
|
bfc4a84020 | ||
|
|
5390bfdcab | ||
|
|
301211ff41 | ||
|
|
64139e4e08 | ||
|
|
e6485cde92 | ||
|
|
891f6ba66f | ||
|
|
5d3c1e3fec | ||
|
|
087e755390 | ||
|
|
025dc1ce75 | ||
|
|
703200338d | ||
|
|
377c93ca0b | ||
|
|
69394ffe29 | ||
|
|
f85231d74a | ||
|
|
b93d8b0159 | ||
|
|
67b9329903 | ||
|
|
c0edd4ea4f | ||
|
|
8eaf2786e8 | ||
|
|
25622df464 | ||
|
|
a48900e178 | ||
|
|
ac8b0535d2 | ||
|
|
6ce25a825b | ||
|
|
b3f56851b8 | ||
|
|
4b86fedce1 | ||
|
|
1ebb70c4d2 | ||
|
|
3de7b81be8 | ||
|
|
d08225339c | ||
|
|
ba22d0706f | ||
|
|
ef80f104c0 | ||
|
|
af296a1e4e | ||
|
|
28a755306a | ||
|
|
461e085eff | ||
|
|
fbda049c32 | ||
|
|
4ded5e2b98 | ||
|
|
63537aff20 | ||
|
|
0f7a2adf15 | ||
|
|
60963abe2c | ||
|
|
08cf95aa38 | ||
|
|
e5b10ab16a | ||
|
|
7f5a1ee45a | ||
|
|
15c593f68e | ||
|
|
5f8ef0395b | ||
|
|
513636e1e0 | ||
|
|
ae9b2c08a9 | ||
|
|
d5327b3b4a | ||
|
|
323e3d3cac | ||
|
|
01b2257063 | ||
|
|
c69ef611a0 | ||
|
|
dcad23316d | ||
|
|
e411f9932f | ||
|
|
854969e1b8 | ||
|
|
4ac7b6e9e8 | ||
|
|
ac70908c5a | ||
|
|
45ac70b78f | ||
|
|
a4664576fe | ||
|
|
b293643398 | ||
|
|
a2e197facd | ||
|
|
8614d39ef4 | ||
|
|
6456bb34ae | ||
|
|
f5dc4de1c1 | ||
|
|
d869056910 | ||
|
|
821e4b17cb | ||
|
|
d8cb5efd2d | ||
|
|
f90e2fb484 | ||
|
|
2c9a7144da | ||
|
|
88d1af7210 | ||
|
|
300e5a5528 | ||
|
|
4418fefe4b | ||
|
|
fe5d1cac9a | ||
|
|
49d17fff9b | ||
|
|
557c6d2d8b | ||
|
|
45fc62357d | ||
|
|
840e3cc22f | ||
|
|
c158c7fc88 | ||
|
|
bc6f8fc2dd | ||
|
|
117730acb2 | ||
|
|
595a7dac83 | ||
|
|
64a4d70df4 | ||
|
|
be36199fe1 | ||
|
|
e46ad25677 | ||
|
|
d5ee663922 | ||
|
|
a7ab4be055 | ||
|
|
6bbf29e75a | ||
|
|
0a06c60cb7 | ||
|
|
03658575eb | ||
|
|
38114bddb9 | ||
|
|
0711a197db | ||
|
|
f8f818b211 | ||
|
|
988932209c | ||
|
|
2aa56cec30 | ||
|
|
93d493650c | ||
|
|
c6162ddcb4 | ||
|
|
038517eda4 | ||
|
|
30a9db73ab | ||
|
|
a50aa41bdb | ||
|
|
cbb322fdb8 | ||
|
|
026e2a020d | ||
|
|
07aab1d005 | ||
|
|
26f0f7b188 | ||
|
|
1efde3b86b | ||
|
|
8c1318f379 | ||
|
|
40e67e8e17 | ||
|
|
04466f52fd | ||
|
|
06baa5fb57 | ||
|
|
04e1657628 | ||
|
|
7816c8cab0 | ||
|
|
6636e658a4 | ||
|
|
2a06f0daef | ||
|
|
883cfa588c | ||
|
|
68011a0b5a | ||
|
|
5247d1a371 | ||
|
|
cabdd528d4 | ||
|
|
2bacbb796b | ||
|
|
aa0ed6434a | ||
|
|
5b2215d646 | ||
|
|
0e760e25f2 | ||
|
|
acbb85b409 | ||
|
|
ea1d4b97ad | ||
|
|
a81839c13f | ||
|
|
b9d4668d4d | ||
|
|
42b27f5965 | ||
|
|
9cc8222b1c | ||
|
|
e8479338df | ||
|
|
fa9e6c9fc0 | ||
|
|
5366173b52 | ||
|
|
63520c55b3 | ||
|
|
86f6d9b14a | ||
|
|
5270cf6284 | ||
|
|
4f46d81e1b | ||
|
|
294a2e6fdb | ||
|
|
b20a8bc90b | ||
|
|
68bdd1336f | ||
|
|
e62ccd932d | ||
|
|
d6c188df6e | ||
|
|
004000b5d2 | ||
|
|
633c8a3444 | ||
|
|
2f59a20b6b | ||
|
|
593c435f75 | ||
|
|
20ec45be57 | ||
|
|
d2a0e12409 | ||
|
|
33eebe117b | ||
|
|
ef0cfc2e7c | ||
|
|
b6e17ae543 | ||
|
|
8a33e2be89 | ||
|
|
5f91097987 | ||
|
|
0fd4f02951 | ||
|
|
106e78ed62 | ||
|
|
8855868b27 | ||
|
|
bfc3e8a907 | ||
|
|
154371e052 | ||
|
|
ab4a4d3d72 | ||
|
|
5a4de02db7 | ||
|
|
43cbc8c6e8 | ||
|
|
5938aa7b50 | ||
|
|
a49252b2f5 | ||
|
|
0be885d9bf | ||
|
|
ae1e8353f2 | ||
|
|
98fe88581f | ||
|
|
d66475576f | ||
|
|
65ff7be776 | ||
|
|
190b079494 | ||
|
|
b020a30bd4 | ||
|
|
81f8453c38 | ||
|
|
533e3cf42d | ||
|
|
69ee73492d | ||
|
|
4a902d04b2 | ||
|
|
2e48e316c2 | ||
|
|
bbe5dddb83 | ||
|
|
7c943fe4ac | ||
|
|
2cbb49681a | ||
|
|
84db4ed57c | ||
|
|
e155642ce4 | ||
|
|
87c4df60d3 | ||
|
|
ff412835e4 | ||
|
|
ad15828157 | ||
|
|
b2fc7f934e | ||
|
|
2fac4d91d6 | ||
|
|
125cd96354 | ||
|
|
af02e6b714 | ||
|
|
0c87b25244 | ||
|
|
e87ada6e79 | ||
|
|
282c8e58bd | ||
|
|
475b66b115 | ||
|
|
5bb971e61a | ||
|
|
ebad9ba723 | ||
|
|
6ece2a839e | ||
|
|
8d6527fb75 | ||
|
|
6bfff38182 | ||
|
|
9e446717fa | ||
|
|
408b48f606 | ||
|
|
8d077ad46d | ||
|
|
db72465e0b | ||
|
|
ba9f5e1688 | ||
|
|
caf40cd272 | ||
|
|
3edccd224a | ||
|
|
f48931a969 | ||
|
|
84f23aa997 | ||
|
|
1965da6a85 | ||
|
|
441ae3e25b | ||
|
|
7f612711a0 | ||
|
|
92eb4aa822 | ||
|
|
08ec522ae7 | ||
|
|
c5cc1fcc1e | ||
|
|
cedf91ea1a | ||
|
|
51b462f043 | ||
|
|
727eeb6c74 | ||
|
|
a114fba062 | ||
|
|
cf322b5c2a | ||
|
|
92116f1671 | ||
|
|
bc479248d7 | ||
|
|
8ee12f2950 | ||
|
|
dcea4c30ef | ||
|
|
e7ca56e061 | ||
|
|
09b800b9ad | ||
|
|
9a6a8580de | ||
|
|
a31ac17792 | ||
|
|
0e27cd0801 | ||
|
|
bc36676fa1 | ||
|
|
3d2db23f33 | ||
|
|
56d366a286 | ||
|
|
4a26f30d65 | ||
|
|
8e51469de5 | ||
|
|
50ebcd552c | ||
|
|
ada39cd3c7 | ||
|
|
b2d20af51a | ||
|
|
f528fa25d1 | ||
|
|
e09a7fb6e0 | ||
|
|
30f7939616 | ||
|
|
16b9375b9d | ||
|
|
4ef93569a1 | ||
|
|
1ce2aaeaf1 | ||
|
|
6bfe8dfcf0 | ||
|
|
8d8f4795e2 | ||
|
|
6f6d06377b | ||
|
|
f22823fcf6 | ||
|
|
93ce57ee1a | ||
|
|
97dd747252 | ||
|
|
bc8c136458 | ||
|
|
0774252dc1 | ||
|
|
ae30ae4be6 | ||
|
|
a2b8935763 | ||
|
|
703efb74d3 | ||
|
|
b2c6062e9a | ||
|
|
c9e7e461b1 | ||
|
|
6aaddfc5a4 | ||
|
|
7f2c41940d | ||
|
|
d31ba39a91 | ||
|
|
c058673e33 | ||
|
|
44ce6a5169 | ||
|
|
0fb0be4ffc | ||
|
|
e70ba00929 | ||
|
|
fe1dbb4cbf | ||
|
|
31df2341c3 | ||
|
|
9d99da14e1 | ||
|
|
f8e10f36db | ||
|
|
bb0f384a39 | ||
|
|
6a0b24f032 | ||
|
|
80d5536503 | ||
|
|
9dcd79bd94 | ||
|
|
c5020b8884 | ||
|
|
0b74de275c | ||
|
|
e66aef17df | ||
|
|
19eff5e6d6 | ||
|
|
88b4fc73de | ||
|
|
70694542eb | ||
|
|
360e5e3102 | ||
|
|
6e89a232e6 | ||
|
|
ecd3b7039f | ||
|
|
4a22e3d2d4 | ||
|
|
dcb4ebe5d9 | ||
|
|
dd379bf18d | ||
|
|
c9b556160f | ||
|
|
168e224d3e | ||
|
|
9e57c14130 | ||
|
|
9c137a1c48 | ||
|
|
ccb9b7e5fb | ||
|
|
c7b16cd043 | ||
|
|
7e20e41521 | ||
|
|
66761a69d3 | ||
|
|
fb32d26479 | ||
|
|
b6398fdb5d | ||
|
|
d9443527ee | ||
|
|
7c175da9f1 | ||
|
|
05aa087851 | ||
|
|
592e968f9f | ||
|
|
894a26cc67 | ||
|
|
1b5dd4638d | ||
|
|
a19186c508 | ||
|
|
5450bdeae9 | ||
|
|
fcd71957ff |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -2,3 +2,5 @@
|
||||
|
||||
github: [eliandoran]
|
||||
custom: ["https://paypal.me/eliandoran"]
|
||||
liberapay: ElianDoran
|
||||
buy_me_a_coffee: eliandoran
|
||||
|
||||
17
.github/workflows/checks.yml
vendored
Normal file
17
.github/workflows/checks.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Checks
|
||||
on:
|
||||
push:
|
||||
pull_request_target:
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check if PRs have conflicts
|
||||
uses: eps1lon/actions-label-merge-conflict@v3
|
||||
with:
|
||||
dirtyLabel: "merge-conflicts"
|
||||
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"
|
||||
4
.mailmap
4
.mailmap
@@ -1,2 +1,2 @@
|
||||
Adam Zivner <adam.zivner@gmail.com>
|
||||
Adam Zivner <zadam.apps@gmail.com>
|
||||
zadam <adam.zivner@gmail.com>
|
||||
zadam <zadam.apps@gmail.com>
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -28,5 +28,12 @@
|
||||
"typescript.validate.enable": true,
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"docs/**/*.html": true,
|
||||
"docs/**/*.png": true,
|
||||
"apps/server/src/assets/doc_notes/**": true,
|
||||
"apps/edit-docs/demo/**": true
|
||||
}
|
||||
}
|
||||
15
README.md
15
README.md
@@ -1,6 +1,7 @@
|
||||
# Trilium Notes
|
||||
|
||||

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

|
||||

|
||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
|
||||
@@ -119,8 +120,8 @@ To install TriliumNext on your own server (including via Docker from [Dockerhub]
|
||||
|
||||
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
|
||||
```shell
|
||||
git clone https://github.com/TriliumNext/Notes.git
|
||||
cd Notes
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm run server:start
|
||||
```
|
||||
@@ -129,8 +130,8 @@ pnpm run server:start
|
||||
|
||||
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
|
||||
```shell
|
||||
git clone https://github.com/TriliumNext/Notes.git
|
||||
cd Notes
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm nx run edit-docs:edit-docs
|
||||
```
|
||||
@@ -138,8 +139,8 @@ pnpm nx run edit-docs:edit-docs
|
||||
### Building the Executable
|
||||
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
|
||||
```shell
|
||||
git clone https://github.com/TriliumNext/Notes.git
|
||||
cd Notes
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
|
||||
```
|
||||
|
||||
@@ -35,13 +35,13 @@
|
||||
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.53.1",
|
||||
"@stylistic/eslint-plugin": "5.0.0",
|
||||
"@playwright/test": "1.54.1",
|
||||
"@stylistic/eslint-plugin": "5.2.0",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/node": "22.15.33",
|
||||
"@types/node": "22.16.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"eslint": "9.29.0",
|
||||
"eslint": "9.31.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"esm": "3.2.25",
|
||||
"jsdoc": "4.0.4",
|
||||
@@ -49,7 +49,7 @@
|
||||
"rcedit": "4.0.1",
|
||||
"rimraf": "6.0.1",
|
||||
"tslib": "2.8.1",
|
||||
"typedoc": "0.28.5",
|
||||
"typedoc": "0.28.7",
|
||||
"typedoc-plugin-missing-exports": "4.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# The development license key for premium CKEditor features.
|
||||
# Note: This key must only be used for the Trilium Notes project.
|
||||
# Expires on: 2025-09-13
|
||||
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w
|
||||
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w
|
||||
VITE_CKEDITOR_ENABLE_INSPECTOR=false
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.96.0",
|
||||
"version": "0.97.1",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -10,16 +10,16 @@
|
||||
"url": "https://github.com/TriliumNext/Notes"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "9.29.0",
|
||||
"@eslint/js": "9.31.0",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@fullcalendar/core": "6.1.17",
|
||||
"@fullcalendar/daygrid": "6.1.17",
|
||||
"@fullcalendar/interaction": "6.1.17",
|
||||
"@fullcalendar/list": "6.1.17",
|
||||
"@fullcalendar/multimonth": "6.1.17",
|
||||
"@fullcalendar/timegrid": "6.1.17",
|
||||
"@fullcalendar/core": "6.1.18",
|
||||
"@fullcalendar/daygrid": "6.1.18",
|
||||
"@fullcalendar/interaction": "6.1.18",
|
||||
"@fullcalendar/list": "6.1.18",
|
||||
"@fullcalendar/multimonth": "6.1.18",
|
||||
"@fullcalendar/timegrid": "6.1.18",
|
||||
"@mermaid-js/layout-elk": "0.1.8",
|
||||
"@mind-elixir/node-menu": "1.0.5",
|
||||
"@mind-elixir/node-menu": "5.0.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@triliumnext/codemirror": "workspace:*",
|
||||
@@ -33,9 +33,9 @@
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
"debounce": "2.2.0",
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.49.6",
|
||||
"globals": "16.2.0",
|
||||
"i18next": "25.2.1",
|
||||
"force-graph": "1.50.1",
|
||||
"globals": "16.3.0",
|
||||
"i18next": "25.3.2",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery-hotkeys": "0.2.2",
|
||||
@@ -46,27 +46,29 @@
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "15.0.12",
|
||||
"mermaid": "11.7.0",
|
||||
"mind-elixir": "4.6.1",
|
||||
"marked": "16.1.1",
|
||||
"mermaid": "11.9.0",
|
||||
"mind-elixir": "5.0.2",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.26.9",
|
||||
"split.js": "1.6.5",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
"vanilla-js-wheel-zoom": "9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-inspector": "4.1.0",
|
||||
"@ckeditor/ckeditor5-inspector": "5.0.0",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/jquery": "3.5.32",
|
||||
"@types/leaflet": "1.9.19",
|
||||
"@types/leaflet": "1.9.20",
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/tabulator-tables": "6.2.7",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"happy-dom": "18.0.1",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.0"
|
||||
"vite-plugin-static-copy": "3.1.1"
|
||||
},
|
||||
"nx": {
|
||||
"name": "client",
|
||||
|
||||
@@ -28,6 +28,8 @@ import TouchBarComponent from "./touch_bar.js";
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import type CodeMirror from "@triliumnext/codemirror";
|
||||
import { StartupChecks } from "./startup_checks.js";
|
||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
||||
@@ -122,6 +124,7 @@ export type CommandMappings = {
|
||||
showImportDialog: CommandData & { noteId: string };
|
||||
openNewNoteSplit: NoteCommandData;
|
||||
openInWindow: NoteCommandData;
|
||||
openInPopup: CommandData & { noteIdOrPath: string; };
|
||||
openNoteInNewTab: CommandData;
|
||||
openNoteInNewSplit: CommandData;
|
||||
openNoteInNewWindow: CommandData;
|
||||
@@ -140,6 +143,7 @@ export type CommandMappings = {
|
||||
};
|
||||
openInTab: ContextMenuCommandData;
|
||||
openNoteInSplit: ContextMenuCommandData;
|
||||
openNoteInPopup: ContextMenuCommandData;
|
||||
toggleNoteHoisting: ContextMenuCommandData;
|
||||
insertNoteAfter: ContextMenuCommandData;
|
||||
insertChildNote: ContextMenuCommandData;
|
||||
@@ -261,7 +265,6 @@ export type CommandMappings = {
|
||||
|
||||
// Geomap
|
||||
deleteFromMap: { noteId: string };
|
||||
openGeoLocation: { noteId: string; event: JQuery.MouseDownEvent };
|
||||
|
||||
toggleZenMode: CommandData;
|
||||
|
||||
@@ -275,6 +278,21 @@ export type CommandMappings = {
|
||||
|
||||
geoMapCreateChildNote: CommandData;
|
||||
|
||||
// Table view
|
||||
addNewRow: CommandData & {
|
||||
customOpts: CreateNoteOpts;
|
||||
parentNotePath?: string;
|
||||
};
|
||||
addNewTableColumn: CommandData & {
|
||||
columnToEdit?: ColumnComponent;
|
||||
referenceColumn?: ColumnComponent;
|
||||
direction?: "before" | "after";
|
||||
type?: "label" | "relation";
|
||||
};
|
||||
deleteTableColumn: CommandData & {
|
||||
columnToDelete?: ColumnComponent;
|
||||
};
|
||||
|
||||
buildTouchBar: CommandData & {
|
||||
TouchBar: typeof TouchBar;
|
||||
buildIcon(name: string): NativeImage;
|
||||
|
||||
@@ -93,11 +93,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
|
||||
if (fun) {
|
||||
return this.callMethod(fun, data);
|
||||
} else {
|
||||
if (!this.parent) {
|
||||
throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`);
|
||||
}
|
||||
|
||||
} else if (this.parent) {
|
||||
return this.parent.triggerCommand(name, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,14 +315,38 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
}
|
||||
|
||||
hasNoteList() {
|
||||
return (
|
||||
this.note &&
|
||||
["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "") &&
|
||||
(this.note.hasChildren() || this.note.getLabelValue("viewType") === "calendar") &&
|
||||
["book", "text", "code"].includes(this.note.type) &&
|
||||
this.note.mime !== "text/x-sqlite;schema=trilium" &&
|
||||
!this.note.isLabelTruthy("hideChildrenOverview")
|
||||
);
|
||||
const note = this.note;
|
||||
|
||||
if (!note) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Some book types must always display a note list, even if no children.
|
||||
if (["calendar", "table", "geoMap"].includes(note.getLabelValue("viewType") ?? "")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!note.hasChildren()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!["book", "text", "code"].includes(note.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (note.mime === "text/x-sqlite;schema=trilium") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (note.isLabelTruthy("hideChildrenOverview")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async getTextEditor(callback?: GetTextEditorCallback) {
|
||||
|
||||
@@ -27,7 +27,6 @@ const NOTE_TYPE_ICONS = {
|
||||
doc: "bx bxs-file-doc",
|
||||
contentWidget: "bx bxs-widget",
|
||||
mindMap: "bx bx-sitemap",
|
||||
geoMap: "bx bx-map-alt",
|
||||
aiChat: "bx bx-bot"
|
||||
};
|
||||
|
||||
@@ -36,7 +35,7 @@ const NOTE_TYPE_ICONS = {
|
||||
* end user. Those types should be used only for checking against, they are
|
||||
* not for direct use.
|
||||
*/
|
||||
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap" | "aiChat";
|
||||
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "aiChat";
|
||||
|
||||
export interface NotePathRecord {
|
||||
isArchived: boolean;
|
||||
@@ -257,6 +256,20 @@ class FNote {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
async getSubtreeNoteIds() {
|
||||
let noteIds: (string | string[])[] = [];
|
||||
for (const child of await this.getChildNotes()) {
|
||||
noteIds.push(child.noteId);
|
||||
noteIds.push(await child.getSubtreeNoteIds());
|
||||
}
|
||||
return noteIds.flat();
|
||||
}
|
||||
|
||||
async getSubtreeNotes() {
|
||||
const noteIds = await this.getSubtreeNoteIds();
|
||||
return this.froca.getNotes(noteIds);
|
||||
}
|
||||
|
||||
async getChildNotes() {
|
||||
return await this.froca.getNotes(this.children);
|
||||
}
|
||||
|
||||
@@ -46,28 +46,7 @@ import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import TocWidget from "../widgets/toc.js";
|
||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||
import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js";
|
||||
import AboutDialog from "../widgets/dialogs/about.js";
|
||||
import HelpDialog from "../widgets/dialogs/help.js";
|
||||
import RecentChangesDialog from "../widgets/dialogs/recent_changes.js";
|
||||
import BranchPrefixDialog from "../widgets/dialogs/branch_prefix.js";
|
||||
import SortChildNotesDialog from "../widgets/dialogs/sort_child_notes.js";
|
||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||
import IncludeNoteDialog from "../widgets/dialogs/include_note.js";
|
||||
import NoteTypeChooserDialog from "../widgets/dialogs/note_type_chooser.js";
|
||||
import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js";
|
||||
import AddLinkDialog from "../widgets/dialogs/add_link.js";
|
||||
import CloneToDialog from "../widgets/dialogs/clone_to.js";
|
||||
import MoveToDialog from "../widgets/dialogs/move_to.js";
|
||||
import ImportDialog from "../widgets/dialogs/import.js";
|
||||
import ExportDialog from "../widgets/dialogs/export.js";
|
||||
import MarkdownImportDialog from "../widgets/dialogs/markdown_import.js";
|
||||
import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
|
||||
import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||
import InfoDialog from "../widgets/dialogs/info.js";
|
||||
import ConfirmDialog from "../widgets/dialogs/confirm.js";
|
||||
import PromptDialog from "../widgets/dialogs/prompt.js";
|
||||
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
|
||||
@@ -83,7 +62,7 @@ import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_ref
|
||||
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||
import options from "../services/options.js";
|
||||
import utils, { hasTouchBar } from "../services/utils.js";
|
||||
import utils from "../services/utils.js";
|
||||
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
|
||||
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
|
||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
||||
@@ -229,7 +208,7 @@ export default class DesktopLayout {
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new SqlTableSchemasWidget())
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget())
|
||||
.child(new NoteListWidget(false))
|
||||
.child(new SearchResultWidget())
|
||||
.child(new SqlResultWidget())
|
||||
.child(new ScrollPaddingWidget())
|
||||
|
||||
@@ -22,6 +22,14 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||
import InfoDialog from "../widgets/dialogs/info.js";
|
||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
|
||||
export function applyModals(rootContainer: RootContainer) {
|
||||
rootContainer
|
||||
@@ -47,4 +55,15 @@ export function applyModals(rootContainer: RootContainer) {
|
||||
.child(new ConfirmDialog())
|
||||
.child(new PromptDialog())
|
||||
.child(new IncorrectCpuArchDialog())
|
||||
.child(new PopupEditorDialog()
|
||||
.child(new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.css("align-items", "center")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(new NoteIconWidget())
|
||||
.child(new NoteTitleWidget()))
|
||||
.child(new ClassicEditorToolbar())
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget(true)))
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ export default class MobileLayout {
|
||||
.filling()
|
||||
.contentSized()
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget())
|
||||
.child(new NoteListWidget(false))
|
||||
.child(new FilePropertiesWidget().css("font-size", "smaller"))
|
||||
)
|
||||
.child(new MobileEditorToolbar())
|
||||
|
||||
@@ -2,7 +2,7 @@ import keyboardActionService from "../services/keyboard_actions.js";
|
||||
import note_tooltip from "../services/note_tooltip.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
interface ContextMenuOptions<T> {
|
||||
export interface ContextMenuOptions<T> {
|
||||
x: number;
|
||||
y: number;
|
||||
orientation?: "left";
|
||||
@@ -17,17 +17,30 @@ interface MenuSeparatorItem {
|
||||
title: "----";
|
||||
}
|
||||
|
||||
export interface MenuItemBadge {
|
||||
title: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface MenuCommandItem<T> {
|
||||
title: string;
|
||||
command?: T;
|
||||
type?: string;
|
||||
/**
|
||||
* The icon to display in the menu item.
|
||||
*
|
||||
* If not set, no icon is displayed and the item will appear shifted slightly to the left if there are other items with icons. To avoid this, use `bx bx-empty`.
|
||||
*/
|
||||
uiIcon?: string;
|
||||
badges?: MenuItemBadge[];
|
||||
templateNoteId?: string;
|
||||
enabled?: boolean;
|
||||
handler?: MenuHandler<T>;
|
||||
items?: MenuItem<T>[] | null;
|
||||
shortcut?: string;
|
||||
spellingSuggestion?: string;
|
||||
checked?: boolean;
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||
@@ -146,10 +159,13 @@ class ContextMenu {
|
||||
} else {
|
||||
const $icon = $("<span>");
|
||||
|
||||
if ("uiIcon" in item && item.uiIcon) {
|
||||
$icon.addClass(item.uiIcon);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
if ("uiIcon" in item || "checked" in item) {
|
||||
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||
if (icon) {
|
||||
$icon.addClass(icon);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
const $link = $("<span>")
|
||||
@@ -157,6 +173,18 @@ class ContextMenu {
|
||||
.append(" ") // some space between icon and text
|
||||
.append(item.title);
|
||||
|
||||
if ("badges" in item && item.badges) {
|
||||
for (let badge of item.badges) {
|
||||
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
||||
|
||||
if (badge.className) {
|
||||
badgeElement.addClass(badge.className);
|
||||
}
|
||||
|
||||
$link.append(badgeElement);
|
||||
}
|
||||
}
|
||||
|
||||
if ("shortcut" in item && item.shortcut) {
|
||||
$link.append($("<kbd>").text(item.shortcut));
|
||||
}
|
||||
@@ -213,6 +241,9 @@ class ContextMenu {
|
||||
$link.addClass("dropdown-toggle");
|
||||
|
||||
const $subMenu = $("<ul>").addClass("dropdown-menu");
|
||||
if (!this.isMobile && item.columns) {
|
||||
$subMenu.css("column-count", item.columns);
|
||||
}
|
||||
|
||||
this.addItems($subMenu, item.items);
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ function getItems(): MenuItem<CommandNames>[] {
|
||||
return [
|
||||
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
||||
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
||||
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }
|
||||
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
|
||||
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
|
||||
];
|
||||
}
|
||||
|
||||
@@ -40,6 +41,8 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string
|
||||
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
||||
} else if (command === "openNoteInNewWindow") {
|
||||
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||
} else if (command === "openNoteInPopup") {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
const items: (MenuItem<TreeCommandNames> | null)[] = [
|
||||
{ title: `${t("tree-context-menu.open-in-a-new-tab")}`, command: "openInTab", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
|
||||
|
||||
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes },
|
||||
|
||||
isHoisted
|
||||
? null
|
||||
@@ -92,7 +92,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
command: "insertNoteAfter",
|
||||
uiIcon: "bx bx-plus",
|
||||
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
|
||||
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptionsOrHelp
|
||||
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptionsOrHelp,
|
||||
columns: 2
|
||||
},
|
||||
|
||||
{
|
||||
@@ -100,7 +101,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
command: "insertChildNote",
|
||||
uiIcon: "bx bx-plus",
|
||||
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
|
||||
enabled: notSearch && noSelectedNotes && notOptionsOrHelp
|
||||
enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
|
||||
columns: 2
|
||||
},
|
||||
|
||||
{ title: "----" },
|
||||
@@ -127,13 +129,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
|
||||
},
|
||||
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
||||
{
|
||||
title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`,
|
||||
command: "duplicateSubtree",
|
||||
uiIcon: "bx bx-outline",
|
||||
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
|
||||
},
|
||||
|
||||
|
||||
{ title: "----" },
|
||||
|
||||
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||
@@ -186,6 +182,13 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
{ title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted },
|
||||
|
||||
{
|
||||
title: `${t("tree-context-menu.duplicate")} <kbd data-command="duplicateSubtree">`,
|
||||
command: "duplicateSubtree",
|
||||
uiIcon: "bx bx-outline",
|
||||
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
|
||||
},
|
||||
|
||||
{
|
||||
title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`,
|
||||
command: "deleteNotes",
|
||||
@@ -244,6 +247,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
||||
|
||||
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
|
||||
} else if (command === "openNoteInPopup") {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
|
||||
} else if (command === "convertNoteToAttachment") {
|
||||
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
|
||||
return;
|
||||
|
||||
@@ -3,19 +3,21 @@ import froca from "./froca.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { AttributeRow } from "./load_results.js";
|
||||
|
||||
async function addLabel(noteId: string, name: string, value: string = "") {
|
||||
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/attribute`, {
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value
|
||||
value: value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
|
||||
async function setLabel(noteId: string, name: string, value: string = "") {
|
||||
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value
|
||||
value: value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,7 +51,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
|
||||
* @param name the name of the attribute to set.
|
||||
* @param value the value of the attribute to set.
|
||||
*/
|
||||
async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||
if (value) {
|
||||
// Create or update the attribute.
|
||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
||||
|
||||
@@ -95,7 +95,15 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false) {
|
||||
/**
|
||||
* Shows the delete confirmation screen
|
||||
*
|
||||
* @param branchIdsToDelete the list of branch IDs to delete.
|
||||
* @param forceDeleteAllClones whether to check by default the "Delete also all clones" checkbox.
|
||||
* @param moveToParent whether to automatically go to the parent note path after a succesful delete. Usually makes sense if deleting the active note(s).
|
||||
* @returns promise that returns false if the operation was cancelled or there was nothing to delete, true if the operation succeeded.
|
||||
*/
|
||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true) {
|
||||
branchIdsToDelete = filterRootNote(branchIdsToDelete);
|
||||
|
||||
if (branchIdsToDelete.length === 0) {
|
||||
@@ -110,10 +118,12 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await activateParentNotePath();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (moveToParent) {
|
||||
try {
|
||||
await activateParentNotePath();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const taskId = utils.randomString(10);
|
||||
|
||||
@@ -15,6 +15,8 @@ import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation
|
||||
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
|
||||
import { t } from "./i18n.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import toast from "./toast.js";
|
||||
import { BulkAction } from "@triliumnext/commons";
|
||||
|
||||
const ACTION_GROUPS = [
|
||||
{
|
||||
@@ -89,6 +91,17 @@ function parseActions(note: FNote) {
|
||||
.filter((action) => !!action);
|
||||
}
|
||||
|
||||
export async function executeBulkActions(parentNoteId: string, actions: BulkAction[]) {
|
||||
await server.post("bulk-action/execute", {
|
||||
noteIds: [ parentNoteId ],
|
||||
includeDescendants: true,
|
||||
actions
|
||||
});
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
|
||||
}
|
||||
|
||||
export default {
|
||||
addAction,
|
||||
parseActions,
|
||||
|
||||
@@ -118,8 +118,17 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
|
||||
async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
|
||||
const blob = await note.getBlob();
|
||||
|
||||
let content = blob?.content || "";
|
||||
if (note.mime === "application/json") {
|
||||
try {
|
||||
content = JSON.stringify(JSON.parse(content), null, 4);
|
||||
} catch (e) {
|
||||
// Ignore JSON parsing errors.
|
||||
}
|
||||
}
|
||||
|
||||
const $codeBlock = $("<code>");
|
||||
$codeBlock.text(blob?.content || "");
|
||||
$codeBlock.text(content);
|
||||
$renderedContent.append($("<pre>").append($codeBlock));
|
||||
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
|
||||
}
|
||||
@@ -301,7 +310,7 @@ function getRenderingType(entity: FNote | FAttachment) {
|
||||
|
||||
if (type === "file" && mime === "application/pdf") {
|
||||
type = "pdf";
|
||||
} else if (type === "file" && mime && CODE_MIME_TYPES.has(mime)) {
|
||||
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime)) {
|
||||
type = "code";
|
||||
} else if (type === "file" && mime && mime.startsWith("audio/")) {
|
||||
type = "audio";
|
||||
|
||||
@@ -4,14 +4,14 @@ import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptio
|
||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
import { focusSavedElement, saveFocusedElement } from "./focus.js";
|
||||
|
||||
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
|
||||
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
|
||||
if (closeActDialog) {
|
||||
closeActiveDialog();
|
||||
glob.activeDialog = $dialog;
|
||||
}
|
||||
|
||||
saveFocusedElement();
|
||||
Modal.getOrCreateInstance($dialog[0]).show();
|
||||
Modal.getOrCreateInstance($dialog[0], config).show();
|
||||
|
||||
$dialog.on("hidden.bs.modal", () => {
|
||||
const $autocompleteEl = $(".aa-input");
|
||||
@@ -41,8 +41,14 @@ async function info(message: string) {
|
||||
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a confirmation dialog with the given message.
|
||||
*
|
||||
* @param message the message to display in the dialog.
|
||||
* @returns A promise that resolves to true if the user confirmed, false otherwise.
|
||||
*/
|
||||
async function confirm(message: string) {
|
||||
return new Promise((res) =>
|
||||
return new Promise<boolean>((res) =>
|
||||
appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{
|
||||
message,
|
||||
callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed)
|
||||
|
||||
@@ -49,6 +49,13 @@ function setupGlobs() {
|
||||
const string = e?.reason?.message?.toLowerCase();
|
||||
|
||||
let message = "Uncaught error: ";
|
||||
let errorObjectString;
|
||||
|
||||
try {
|
||||
errorObjectString = JSON.stringify(e.reason)
|
||||
} catch (error: any) {
|
||||
errorObjectString = error.toString();
|
||||
}
|
||||
|
||||
if (string?.includes("script error")) {
|
||||
message += "No details available";
|
||||
@@ -57,7 +64,7 @@ function setupGlobs() {
|
||||
`Message: ${e.reason.message}`,
|
||||
`Line: ${e.reason.lineNumber}`,
|
||||
`Column: ${e.reason.columnNumber}`,
|
||||
`Error object: ${JSON.stringify(e.reason)}`,
|
||||
`Error object: ${errorObjectString}`,
|
||||
`Stack: ${e.reason && e.reason.stack}`
|
||||
].join(", ");
|
||||
}
|
||||
|
||||
@@ -231,6 +231,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
let ntxId: string | null = null;
|
||||
let hoistedNoteId: string | null = null;
|
||||
let searchString: string | null = null;
|
||||
let openInPopup = false;
|
||||
|
||||
if (paramString) {
|
||||
for (const pair of paramString.split("&")) {
|
||||
@@ -246,6 +247,8 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
|
||||
} else if (["viewMode", "attachmentId"].includes(name)) {
|
||||
(viewScope as any)[name] = value;
|
||||
} else if (name === "popup") {
|
||||
openInPopup = true;
|
||||
} else {
|
||||
console.warn(`Unrecognized hash parameter '${name}'.`);
|
||||
}
|
||||
@@ -266,7 +269,8 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
ntxId,
|
||||
hoistedNoteId,
|
||||
viewScope,
|
||||
searchString
|
||||
searchString,
|
||||
openInPopup
|
||||
};
|
||||
}
|
||||
|
||||
@@ -277,13 +281,21 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
|
||||
return goToLinkExt(evt, hrefLink, $link);
|
||||
}
|
||||
|
||||
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
|
||||
/**
|
||||
* Handles navigation to a link, which can be an internal note path (e.g., `#root/1234`) or an external URL (e.g., `https://example.com`).
|
||||
*
|
||||
* @param evt the event that triggered the link navigation, or `null` if the link was clicked programmatically. Used to determine if the link should be opened in a new tab/window, based on the button presses.
|
||||
* @param hrefLink the link to navigate to, which can be a note path (e.g., `#root/1234`) or an external URL with any supported protocol (e.g., `https://example.com`).
|
||||
* @param $link the jQuery element of the link that was clicked, used to determine if the link is an anchor link (e.g., `#fn1` or `#fnref1`) and to handle it accordingly.
|
||||
* @returns `true` if the link was handled (i.e., the element was found and scrolled to), or a falsy value otherwise.
|
||||
*/
|
||||
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
|
||||
if (hrefLink?.startsWith("data:")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
evt?.preventDefault();
|
||||
evt?.stopPropagation();
|
||||
|
||||
if (hrefLink && hrefLink.startsWith("#") && !hrefLink.startsWith("#root/") && $link) {
|
||||
if (handleAnchor(hrefLink, $link)) {
|
||||
@@ -291,19 +303,22 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
|
||||
}
|
||||
}
|
||||
|
||||
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
|
||||
const { notePath, viewScope, openInPopup } = parseNavigationStateFromUrl(hrefLink);
|
||||
|
||||
const ctrlKey = utils.isCtrlKey(evt);
|
||||
const shiftKey = evt.shiftKey;
|
||||
const isLeftClick = "which" in evt && evt.which === 1;
|
||||
const isMiddleClick = "which" in evt && evt.which === 2;
|
||||
const ctrlKey = evt && utils.isCtrlKey(evt);
|
||||
const shiftKey = evt?.shiftKey;
|
||||
const isLeftClick = !evt || ("which" in evt && evt.which === 1);
|
||||
// Right click is handled separately.
|
||||
const isMiddleClick = evt && "which" in evt && evt.which === 2;
|
||||
const targetIsBlank = ($link?.attr("target") === "_blank");
|
||||
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank;
|
||||
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
|
||||
const openInNewWindow = isLeftClick && evt.shiftKey && !ctrlKey;
|
||||
const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
|
||||
|
||||
if (notePath) {
|
||||
if (openInNewWindow) {
|
||||
if (isLeftClick && openInPopup) {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
||||
} else if (openInNewWindow) {
|
||||
appContext.triggerCommand("openInWindow", { notePath, viewScope });
|
||||
} else if (openInNewTab) {
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
||||
@@ -311,7 +326,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
|
||||
viewScope
|
||||
});
|
||||
} else if (isLeftClick) {
|
||||
const ntxId = $(evt.target as any)
|
||||
const ntxId = $(evt?.target as any)
|
||||
.closest("[data-ntx-id]")
|
||||
.attr("data-ntx-id");
|
||||
|
||||
@@ -379,6 +394,12 @@ function linkContextMenu(e: PointerEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.isCtrlKey(e) && e.button === 2) {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
linkContextMenuService.openContextMenu(notePath, e, viewScope, null);
|
||||
|
||||
@@ -40,7 +40,10 @@ interface Options {
|
||||
allowCreatingNotes?: boolean;
|
||||
allowJumpToSearchNotes?: boolean;
|
||||
allowExternalLinks?: boolean;
|
||||
/** If set, hides the right-side button corresponding to go to selected note. */
|
||||
hideGoToSelectedNoteButton?: boolean;
|
||||
/** If set, hides all right-side buttons in the autocomplete dropdown */
|
||||
hideAllButtons?: boolean;
|
||||
}
|
||||
|
||||
async function autocompleteSourceForCKEditor(queryText: string) {
|
||||
@@ -190,9 +193,11 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
||||
|
||||
const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right");
|
||||
|
||||
$el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton);
|
||||
if (!options.hideAllButtons) {
|
||||
$el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton);
|
||||
}
|
||||
|
||||
if (!options.hideGoToSelectedNoteButton) {
|
||||
if (!options.hideGoToSelectedNoteButton && !options.hideAllButtons) {
|
||||
$el.after($goToSelectedNoteButton);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import type FBranch from "../entities/fbranch.js";
|
||||
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
|
||||
interface CreateNoteOpts {
|
||||
export interface CreateNoteOpts {
|
||||
isProtected?: boolean;
|
||||
saveSelection?: boolean;
|
||||
title?: string | null;
|
||||
|
||||
@@ -1,38 +1,29 @@
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import CalendarView from "../widgets/view_widgets/calendar_view.js";
|
||||
import GeoView from "../widgets/view_widgets/geo_view/index.js";
|
||||
import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js";
|
||||
import TableView from "../widgets/view_widgets/table_view/index.js";
|
||||
import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js";
|
||||
import type ViewMode from "../widgets/view_widgets/view_mode.js";
|
||||
|
||||
export type ViewTypeOptions = "list" | "grid" | "calendar";
|
||||
export type ArgsWithoutNoteId = Omit<ViewModeArgs, "noteIds">;
|
||||
export type ViewTypeOptions = "list" | "grid" | "calendar" | "table" | "geoMap";
|
||||
|
||||
export default class NoteListRenderer {
|
||||
|
||||
private viewType: ViewTypeOptions;
|
||||
public viewMode: ViewMode | null;
|
||||
private args: ArgsWithoutNoteId;
|
||||
public viewMode?: ViewMode<any>;
|
||||
|
||||
constructor($parent: JQuery<HTMLElement>, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) {
|
||||
this.viewType = this.#getViewType(parentNote);
|
||||
const args: ViewModeArgs = {
|
||||
$parent,
|
||||
parentNote,
|
||||
noteIds,
|
||||
showNotePath
|
||||
};
|
||||
|
||||
if (this.viewType === "list" || this.viewType === "grid") {
|
||||
this.viewMode = new ListOrGridView(this.viewType, args);
|
||||
} else if (this.viewType === "calendar") {
|
||||
this.viewMode = new CalendarView(args);
|
||||
} else {
|
||||
this.viewMode = null;
|
||||
}
|
||||
constructor(args: ArgsWithoutNoteId) {
|
||||
this.args = args;
|
||||
this.viewType = this.#getViewType(args.parentNote);
|
||||
}
|
||||
|
||||
#getViewType(parentNote: FNote): ViewTypeOptions {
|
||||
const viewType = parentNote.getLabelValue("viewType");
|
||||
|
||||
if (!["list", "grid", "calendar"].includes(viewType || "")) {
|
||||
if (!["list", "grid", "calendar", "table", "geoMap"].includes(viewType || "")) {
|
||||
// when not explicitly set, decide based on the note type
|
||||
return parentNote.type === "search" ? "list" : "grid";
|
||||
} else {
|
||||
@@ -41,15 +32,36 @@ export default class NoteListRenderer {
|
||||
}
|
||||
|
||||
get isFullHeight() {
|
||||
return this.viewMode?.isFullHeight;
|
||||
switch (this.viewType) {
|
||||
case "list":
|
||||
case "grid":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
if (!this.viewMode) {
|
||||
return null;
|
||||
}
|
||||
const args = this.args;
|
||||
const viewMode = this.#buildViewMode(args);
|
||||
this.viewMode = viewMode;
|
||||
await viewMode.beforeRender();
|
||||
return await viewMode.renderList();
|
||||
}
|
||||
|
||||
return await this.viewMode.renderList();
|
||||
#buildViewMode(args: ViewModeArgs) {
|
||||
switch (this.viewType) {
|
||||
case "calendar":
|
||||
return new CalendarView(args);
|
||||
case "table":
|
||||
return new TableView(args);
|
||||
case "geoMap":
|
||||
return new GeoView(args);
|
||||
case "list":
|
||||
case "grid":
|
||||
default:
|
||||
return new ListOrGridView(this.viewType, args);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ let dismissTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
function setupGlobalTooltip() {
|
||||
$(document).on("mouseenter", "a", mouseEnterHandler);
|
||||
$(document).on("mouseenter", "[data-href]", mouseEnterHandler);
|
||||
|
||||
// close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
|
||||
$(document).on("click", (e) => {
|
||||
@@ -167,7 +168,10 @@ async function renderTooltip(note: FNote | null) {
|
||||
if (isContentEmpty) {
|
||||
classes.push("note-no-content");
|
||||
}
|
||||
content = `<h5 class="${classes.join(" ")}"><a href="#${note.noteId}" data-no-context-menu="true">${noteTitleWithPathAsSuffix.prop("outerHTML")}</a></h5>`;
|
||||
content = `\
|
||||
<h5 class="${classes.join(" ")}">
|
||||
<a href="#${note.noteId}" data-no-context-menu="true">${noteTitleWithPathAsSuffix.prop("outerHTML")}</a>
|
||||
</h5>`;
|
||||
}
|
||||
|
||||
content = `${content}<div class="note-tooltip-attributes">${$renderedAttributes[0].outerHTML}</div>`;
|
||||
@@ -175,6 +179,7 @@ async function renderTooltip(note: FNote | null) {
|
||||
content += $renderedContent[0].outerHTML;
|
||||
}
|
||||
|
||||
content += `<a class="open-popup-button" title="${t("note_tooltip.quick-edit")}" href="#${note.noteId}?popup"><span class="bx bx-edit" /></a>`;
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,118 @@
|
||||
import server from "./server.js";
|
||||
import froca from "./froca.js";
|
||||
import { t } from "./i18n.js";
|
||||
import type { MenuItem } from "../menus/context_menu.js";
|
||||
import froca from "./froca.js";
|
||||
import server from "./server.js";
|
||||
import type { MenuCommandItem, MenuItem, MenuItemBadge } from "../menus/context_menu.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
|
||||
|
||||
export interface NoteTypeMapping {
|
||||
type: NoteType;
|
||||
mime?: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
/** Indicates whether this type should be marked as a newly introduced feature. */
|
||||
isNew?: boolean;
|
||||
/** Indicates that this note type is part of a beta feature. */
|
||||
isBeta?: boolean;
|
||||
/** Indicates that this note type cannot be created by the user. */
|
||||
reserved?: boolean;
|
||||
/** Indicates that once a note of this type is created, its type can no longer be changed. */
|
||||
static?: boolean;
|
||||
}
|
||||
|
||||
export const NOTE_TYPES: NoteTypeMapping[] = [
|
||||
// The suggested note type ordering method: insert the item into the corresponding group,
|
||||
// then ensure the items within the group are ordered alphabetically.
|
||||
|
||||
// The default note type (always the first item)
|
||||
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
|
||||
|
||||
// Text notes group
|
||||
{ type: "book", mime: "", title: t("note_types.book"), icon: "bx-book" },
|
||||
|
||||
// Graphic notes
|
||||
{ type: "canvas", mime: "application/json", title: t("note_types.canvas"), icon: "bx-pen" },
|
||||
{ type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), icon: "bx-selection" },
|
||||
|
||||
// Map notes
|
||||
{ type: "mindMap", mime: "application/json", title: t("note_types.mind-map"), icon: "bx-sitemap" },
|
||||
{ type: "noteMap", mime: "", title: t("note_types.note-map"), icon: "bxs-network-chart", static: true },
|
||||
{ type: "relationMap", mime: "application/json", title: t("note_types.relation-map"), icon: "bxs-network-chart" },
|
||||
|
||||
// Misc note types
|
||||
{ type: "render", mime: "", title: t("note_types.render-note"), icon: "bx-extension" },
|
||||
{ type: "search", title: t("note_types.saved-search"), icon: "bx-file-find", static: true },
|
||||
{ type: "webView", mime: "", title: t("note_types.web-view"), icon: "bx-globe-alt" },
|
||||
|
||||
// Code notes
|
||||
{ type: "code", mime: "text/plain", title: t("note_types.code"), icon: "bx-code" },
|
||||
|
||||
// Reserved types (cannot be created by the user)
|
||||
{ type: "contentWidget", mime: "", title: t("note_types.widget"), reserved: true },
|
||||
{ type: "doc", mime: "", title: t("note_types.doc"), reserved: true },
|
||||
{ type: "file", title: t("note_types.file"), reserved: true },
|
||||
{ type: "image", title: t("note_types.image"), reserved: true },
|
||||
{ type: "launcher", mime: "", title: t("note_types.launcher"), reserved: true },
|
||||
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), reserved: true }
|
||||
];
|
||||
|
||||
/** The maximum age in days for a template to be marked with the "New" badge */
|
||||
const NEW_TEMPLATE_MAX_AGE = 3;
|
||||
|
||||
/** The length of a day in milliseconds. */
|
||||
const DAY_LENGTH = 1000 * 60 * 60 * 24;
|
||||
|
||||
/** The menu item badge used to mark new note types and templates */
|
||||
const NEW_BADGE: MenuItemBadge = {
|
||||
title: t("note_types.new-feature"),
|
||||
className: "new-note-type-badge"
|
||||
};
|
||||
|
||||
/** The menu item badge used to mark note types that are part of a beta feature */
|
||||
const BETA_BADGE = {
|
||||
title: t("note_types.beta-feature")
|
||||
};
|
||||
|
||||
const SEPARATOR = { title: "----" };
|
||||
|
||||
const creationDateCache = new Map<string, Date>();
|
||||
let rootCreationDate: Date | undefined;
|
||||
|
||||
async function getNoteTypeItems(command?: TreeCommandNames) {
|
||||
const items: MenuItem<TreeCommandNames>[] = [
|
||||
{ title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" },
|
||||
{ title: t("note_types.code"), command, type: "code", uiIcon: "bx bx-code" },
|
||||
{ title: t("note_types.saved-search"), command, type: "search", uiIcon: "bx bx-file-find" },
|
||||
{ title: t("note_types.relation-map"), command, type: "relationMap", uiIcon: "bx bxs-network-chart" },
|
||||
{ title: t("note_types.note-map"), command, type: "noteMap", uiIcon: "bx bxs-network-chart" },
|
||||
{ title: t("note_types.render-note"), command, type: "render", uiIcon: "bx bx-extension" },
|
||||
{ title: t("note_types.book"), command, type: "book", uiIcon: "bx bx-book" },
|
||||
{ title: t("note_types.mermaid-diagram"), command, type: "mermaid", uiIcon: "bx bx-selection" },
|
||||
{ title: t("note_types.canvas"), command, type: "canvas", uiIcon: "bx bx-pen" },
|
||||
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
|
||||
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
|
||||
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
|
||||
...await getBuiltInTemplates(command),
|
||||
...getBlankNoteTypes(command),
|
||||
...await getBuiltInTemplates(t("note_types.collections"), command, true),
|
||||
...await getBuiltInTemplates(null, command, false),
|
||||
...await getUserTemplates(command)
|
||||
];
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandNames>[] {
|
||||
return NOTE_TYPES
|
||||
.filter((nt) => !nt.reserved && nt.type !== "book")
|
||||
.map((nt) => {
|
||||
const menuItem: MenuCommandItem<TreeCommandNames> = {
|
||||
title: nt.title,
|
||||
command,
|
||||
type: nt.type,
|
||||
uiIcon: "bx " + nt.icon,
|
||||
badges: []
|
||||
}
|
||||
|
||||
if (nt.isNew) {
|
||||
menuItem.badges?.push(NEW_BADGE);
|
||||
}
|
||||
|
||||
if (nt.isBeta) {
|
||||
menuItem.badges?.push(BETA_BADGE);
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
});
|
||||
}
|
||||
|
||||
async function getUserTemplates(command?: TreeCommandNames) {
|
||||
const templateNoteIds = await server.get<string[]>("search-templates");
|
||||
const templateNotes = await froca.getNotes(templateNoteIds);
|
||||
@@ -37,19 +123,26 @@ async function getUserTemplates(command?: TreeCommandNames) {
|
||||
const items: MenuItem<TreeCommandNames>[] = [
|
||||
SEPARATOR
|
||||
];
|
||||
|
||||
for (const templateNote of templateNotes) {
|
||||
items.push({
|
||||
const item: MenuItem<TreeCommandNames> = {
|
||||
title: templateNote.title,
|
||||
uiIcon: templateNote.getIcon(),
|
||||
command: command,
|
||||
type: templateNote.type,
|
||||
templateNoteId: templateNote.noteId
|
||||
});
|
||||
};
|
||||
|
||||
if (await isNewTemplate(templateNote.noteId)) {
|
||||
item.badges = [NEW_BADGE];
|
||||
}
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async function getBuiltInTemplates(command?: TreeCommandNames) {
|
||||
async function getBuiltInTemplates(title: string | null, command: TreeCommandNames | undefined, filterCollections: boolean) {
|
||||
const templatesRoot = await froca.getNote("_templates");
|
||||
if (!templatesRoot) {
|
||||
console.warn("Unable to find template root.");
|
||||
@@ -61,21 +154,85 @@ async function getBuiltInTemplates(command?: TreeCommandNames) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: MenuItem<TreeCommandNames>[] = [
|
||||
SEPARATOR
|
||||
];
|
||||
for (const templateNote of childNotes) {
|
||||
const items: MenuItem<TreeCommandNames>[] = [];
|
||||
if (title) {
|
||||
items.push({
|
||||
title: title,
|
||||
enabled: false,
|
||||
uiIcon: "bx bx-empty"
|
||||
});
|
||||
} else {
|
||||
items.push(SEPARATOR);
|
||||
}
|
||||
|
||||
for (const templateNote of childNotes) {
|
||||
if (templateNote.hasLabel("collection") !== filterCollections) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const item: MenuItem<TreeCommandNames> = {
|
||||
title: templateNote.title,
|
||||
uiIcon: templateNote.getIcon(),
|
||||
command: command,
|
||||
type: templateNote.type,
|
||||
templateNoteId: templateNote.noteId
|
||||
});
|
||||
};
|
||||
|
||||
if (await isNewTemplate(templateNote.noteId)) {
|
||||
item.badges = [NEW_BADGE];
|
||||
}
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async function isNewTemplate(templateNoteId) {
|
||||
if (rootCreationDate === undefined) {
|
||||
// Retrieve the root note creation date
|
||||
try {
|
||||
let rootNoteInfo: any = await server.get("notes/root");
|
||||
if ("dateCreated" in rootNoteInfo) {
|
||||
rootCreationDate = new Date(rootNoteInfo.dateCreated);
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to retrieve the template's creation date from the cache
|
||||
let creationDate: Date | undefined = creationDateCache.get(templateNoteId);
|
||||
|
||||
if (creationDate === undefined) {
|
||||
// The creation date isn't available in the cache, try to retrieve it from the server
|
||||
try {
|
||||
const noteInfo: any = await server.get("notes/" + templateNoteId);
|
||||
if ("dateCreated" in noteInfo) {
|
||||
creationDate = new Date(noteInfo.dateCreated);
|
||||
creationDateCache.set(templateNoteId, creationDate);
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (creationDate) {
|
||||
if (rootCreationDate && creationDate.getTime() - rootCreationDate.getTime() < 30000) {
|
||||
// Ignore templates created within 30 seconds after the root note is created.
|
||||
// This is useful to prevent predefined templates from being marked
|
||||
// as 'New' after setting up a new database.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine the difference in days between now and the template's creation date
|
||||
const age = (new Date().getTime() - creationDate.getTime()) / DAY_LENGTH;
|
||||
// Return true if the template is at most NEW_TEMPLATE_MAX_AGE days old
|
||||
return (age <= NEW_TEMPLATE_MAX_AGE);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getNoteTypeItems
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url";
|
||||
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url";
|
||||
type Multiplicity = "single" | "multi";
|
||||
|
||||
export interface DefinitionObject {
|
||||
|
||||
@@ -81,8 +81,8 @@ body {
|
||||
|
||||
/* -- Overrides the default colors used by the ckeditor5-image package. --------------------- */
|
||||
|
||||
--ck-color-image-caption-background: var(--main-background-color);
|
||||
--ck-color-image-caption-text: var(--main-text-color);
|
||||
--ck-content-color-image-caption-background: var(--main-background-color);
|
||||
--ck-content-color-image-caption-text: var(--main-text-color);
|
||||
|
||||
/* -- Overrides the default colors used by the ckeditor5-widget package. -------------------- */
|
||||
|
||||
|
||||
@@ -192,6 +192,13 @@ samp {
|
||||
font-family: var(--monospace-font-family) !important;
|
||||
}
|
||||
|
||||
.badge {
|
||||
--bs-badge-color: var(--muted-text-color);
|
||||
|
||||
margin-left: 8px;
|
||||
background: var(--accented-background-color);
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: var(--accented-background-color) !important;
|
||||
color: var(--muted-text-color) !important;
|
||||
@@ -320,7 +327,8 @@ button kbd {
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
.dropdown-menu,
|
||||
.tabulator-popup-container {
|
||||
color: var(--menu-text-color) !important;
|
||||
font-size: inherit;
|
||||
background-color: var(--menu-background-color) !important;
|
||||
@@ -330,7 +338,13 @@ button kbd {
|
||||
--bs-dropdown-link-active-bg: var(--active-item-background-color) !important;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu {
|
||||
.dropdown-menu .dropdown-divider {
|
||||
break-before: avoid;
|
||||
break-after: avoid;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu,
|
||||
body.desktop .tabulator-popup-container {
|
||||
border: 1px solid var(--dropdown-border-color);
|
||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
||||
animation: dropdown-menu-opening 100ms ease-in;
|
||||
@@ -373,7 +387,8 @@ body.desktop .dropdown-menu {
|
||||
}
|
||||
|
||||
.dropdown-menu a:hover:not(.disabled),
|
||||
.dropdown-item:hover:not(.disabled, .dropdown-item-container) {
|
||||
.dropdown-item:hover:not(.disabled, .dropdown-item-container),
|
||||
.tabulator-menu-item:hover {
|
||||
color: var(--hover-item-text-color) !important;
|
||||
background-color: var(--hover-item-background-color) !important;
|
||||
border-color: var(--hover-item-border-color) !important;
|
||||
@@ -528,6 +543,7 @@ button.btn-sm {
|
||||
/* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
pre:not(.hljs) {
|
||||
@@ -759,6 +775,14 @@ table.promoted-attributes-in-tooltip th {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.note-tooltip-content .open-popup-button {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
bottom: 8px;
|
||||
font-size: 1.2em;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.note-tooltip-attributes {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
@@ -900,6 +924,13 @@ div[data-notify="container"] {
|
||||
font-family: var(--monospace-font-family);
|
||||
}
|
||||
|
||||
.ck-content {
|
||||
--ck-content-font-family: var(--detail-font-family);
|
||||
--ck-content-font-size: 1.1em;
|
||||
--ck-content-font-color: var(--main-text-color);
|
||||
--ck-content-line-height: var(--bs-body-line-height);
|
||||
}
|
||||
|
||||
.ck-content .table table th {
|
||||
background-color: var(--accented-background-color);
|
||||
}
|
||||
@@ -1186,12 +1217,14 @@ body.mobile .dropdown-submenu > .dropdown-menu {
|
||||
}
|
||||
|
||||
#context-menu-container,
|
||||
#context-menu-container .dropdown-menu {
|
||||
padding: 3px 0 0;
|
||||
#context-menu-container .dropdown-menu,
|
||||
.tabulator-popup-container {
|
||||
padding: 3px 0;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
#context-menu-container .dropdown-item {
|
||||
#context-menu-container .dropdown-item,
|
||||
.tabulator-menu .tabulator-menu-item {
|
||||
padding: 0 7px 0 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
199
apps/client/src/stylesheets/table.css
Normal file
199
apps/client/src/stylesheets/table.css
Normal file
@@ -0,0 +1,199 @@
|
||||
.tabulator {
|
||||
--table-background-color: var(--main-background-color);
|
||||
|
||||
--col-header-background-color: var(--main-background-color);
|
||||
--col-header-hover-background-color: var(--accented-background-color);
|
||||
--col-header-text-color: var(--main-text-color);
|
||||
--col-header-arrow-active-color: var(--main-text-color);
|
||||
--col-header-arrow-inactive-color: var(--more-accented-background-color);
|
||||
--col-header-separator-border: none;
|
||||
--col-header-bottom-border: 2px solid var(--main-border-color);
|
||||
|
||||
--row-background-color: var(--main-background-color);
|
||||
--row-alternate-background-color: var(--main-background-color);
|
||||
--row-moving-background-color: var(--accented-background-color);
|
||||
--row-text-color: var(--main-text-color);
|
||||
--row-delimiter-color: var(--more-accented-background-color);
|
||||
|
||||
--cell-horiz-padding-size: 8px;
|
||||
--cell-vert-padding-size: 8px;
|
||||
|
||||
--cell-editable-hover-outline-color: var(--main-border-color);
|
||||
--cell-read-only-text-color: var(--muted-text-color);
|
||||
|
||||
--cell-editing-border-color: var(--main-border-color);
|
||||
--cell-editing-border-width: 2px;
|
||||
--cell-editing-background-color: var(--ck-color-selector-focused-cell-background);
|
||||
--cell-editing-text-color: initial;
|
||||
|
||||
background: unset;
|
||||
border: unset;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-tableholder .tabulator-table {
|
||||
background: var(--table-background-color);
|
||||
}
|
||||
|
||||
/* Column headers */
|
||||
|
||||
.tabulator div.tabulator-header {
|
||||
border-bottom: var(--col-header-bottom-border);
|
||||
background: var(--col-header-background-color);
|
||||
color: var(--col-header-text-color);
|
||||
}
|
||||
|
||||
.tabulator .tabulator-col-content {
|
||||
padding: 8px 4px !important;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover {
|
||||
background-color: var(--col-header-hover-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.tabulator div.tabulator-header .tabulator-col.tabulator-moving {
|
||||
border: none;
|
||||
background: var(--col-header-hover-background-color);
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
|
||||
border-bottom-color: var(--col-header-arrow-active-color);
|
||||
border-top-color: var(--col-header-arrow-active-color);
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="none"] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
|
||||
border-bottom-color: var(--col-header-arrow-inactive-color);
|
||||
}
|
||||
|
||||
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
|
||||
margin-left: var(--cell-editing-border-width);
|
||||
}
|
||||
|
||||
.tabulator div.tabulator-header .tabulator-col,
|
||||
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
|
||||
background: var(--col-header-background-color);
|
||||
border-right: var(--col-header-separator-border);
|
||||
}
|
||||
|
||||
/* Table body */
|
||||
|
||||
.tabulator-tableholder {
|
||||
padding-top: 10px;
|
||||
height: unset !important; /* Don't extend on the full height */
|
||||
}
|
||||
|
||||
/* Rows */
|
||||
|
||||
.tabulator-row .tabulator-cell {
|
||||
padding: var(--cell-vert-padding-size) var(--cell-horiz-padding-size);
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell input {
|
||||
padding-left: var(--cell-horiz-padding-size) !important;
|
||||
padding-right: var(--cell-horiz-padding-size) !important;
|
||||
}
|
||||
|
||||
.tabulator-row {
|
||||
background: transparent;
|
||||
border-top: none;
|
||||
border-bottom: 1px solid var(--row-delimiter-color);
|
||||
color: var(--row-text-color);
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-row-odd {
|
||||
background: var(--row-background-color);
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-row-even {
|
||||
background: var(--row-alternate-background-color);
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-moving {
|
||||
border-color: transparent;
|
||||
background-color: var(--row-moving-background-color);
|
||||
}
|
||||
|
||||
/* Cell */
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
|
||||
margin-right: var(--cell-editing-border-width);
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,
|
||||
.tabulator-row .tabulator-cell {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell:not(.tabulator-editable) {
|
||||
color: var(--cell-read-only-text-color);
|
||||
}
|
||||
|
||||
.tabulator:not(.tabulator-editing) .tabulator-row .tabulator-cell.tabulator-editable:hover {
|
||||
outline: 2px solid var(--cell-editable-hover-outline-color);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-editing {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.tabulator-row:not(.tabulator-moving) .tabulator-cell.tabulator-editing {
|
||||
outline: calc(var(--cell-editing-border-width) - 1px) solid var(--cell-editing-border-color);
|
||||
border-color: var(--cell-editing-border-color);
|
||||
background: var(--cell-editing-background-color);
|
||||
}
|
||||
|
||||
.tabulator-row:not(.tabulator-moving) .tabulator-cell.tabulator-editing > * {
|
||||
color: var(--cell-editing-text-color);
|
||||
}
|
||||
|
||||
.tabulator .tree-collapse,
|
||||
.tabulator .tree-expand {
|
||||
color: var(--row-text-color);
|
||||
}
|
||||
|
||||
/* Align items without children/expander to the ones with. */
|
||||
.tabulator-cell[tabulator-field="title"] > span:first-child, /* 1st level */
|
||||
.tabulator-cell[tabulator-field="title"] > div:first-child + span { /* sub-level */
|
||||
padding-left: 21px;
|
||||
}
|
||||
|
||||
/* Checkbox cells */
|
||||
|
||||
.tabulator .tabulator-cell:has(svg),
|
||||
.tabulator .tabulator-cell:has(input[type="checkbox"]) {
|
||||
padding-left: 8px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-cell input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
/* Context menus */
|
||||
|
||||
.tabulator-popup-container {
|
||||
min-width: 10em;
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.tabulator-menu .tabulator-menu-item {
|
||||
border: 1px solid transparent;
|
||||
color: var(--menu-text-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
:root .tabulator .tabulator-footer {
|
||||
border-top: unset;
|
||||
padding: 10px 0;
|
||||
}
|
||||
@@ -178,6 +178,9 @@
|
||||
|
||||
--alert-bar-background: #6b6b6b3b;
|
||||
|
||||
--badge-background-color: #ffffff1a;
|
||||
--badge-text-color: var(--muted-text-color);
|
||||
|
||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
||||
--promoted-attribute-card-shadow-color: #000000b3;
|
||||
|
||||
|
||||
@@ -171,6 +171,9 @@
|
||||
|
||||
--alert-bar-background: #32637b29;
|
||||
|
||||
--badge-background-color: #00000011;
|
||||
--badge-text-color: var(--muted-text-color);
|
||||
|
||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
||||
--promoted-attribute-card-shadow-color: #00000033;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@import url(./pages.css);
|
||||
@import url(./ribbon.css);
|
||||
@import url(./notes/text.css);
|
||||
@import url(./notes/collections/table.css);
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
@@ -171,9 +172,19 @@ html body .dropdown-item[disabled] {
|
||||
opacity: var(--menu-item-disabled-opacity);
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
:root .badge {
|
||||
--bs-badge-color: var(--badge-text-color);
|
||||
--bs-badge-font-weight: 500;
|
||||
|
||||
background: var(--badge-background-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .2pt;
|
||||
}
|
||||
|
||||
/* Menu item icon */
|
||||
.dropdown-item .bx {
|
||||
transform: translateY(var(--menu-item-icon-vert-offset));
|
||||
translate: 0 var(--menu-item-icon-vert-offset);
|
||||
color: var(--menu-item-icon-color) !important;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
@@ -382,6 +382,10 @@ div.tn-tool-dialog {
|
||||
|
||||
/* DELETE NOTE PREVIEW DIALOG */
|
||||
|
||||
.delete-notes-dialog .modal-dialog {
|
||||
--bs-modal-width: fit-content;
|
||||
}
|
||||
|
||||
.delete-notes-list .note-path {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
:root .tabulator {
|
||||
--col-header-hover-background-color: var(--hover-item-background-color);
|
||||
--col-header-arrow-active-color: var(--active-item-text-color);
|
||||
--col-header-arrow-inactive-color: var(--main-border-color);
|
||||
|
||||
--row-moving-background-color: var(--more-accented-background-color);
|
||||
|
||||
--cell-editable-hover-outline-color: var(--input-focus-outline-color);
|
||||
|
||||
--cell-editing-border-color: var(--input-focus-outline-color);
|
||||
--cell-editing-background-color: var(--input-background-color);
|
||||
--cell-editing-text-color: var(--input-text-color);
|
||||
}
|
||||
@@ -46,6 +46,12 @@ div.promoted-attributes-container {
|
||||
.image-properties > div:first-child > span > strong {
|
||||
opacity: 0.65;
|
||||
font-weight: 500;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.note-info-widget-table td,
|
||||
.file-properties-widget .file-table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.file-properties-widget {
|
||||
|
||||
@@ -71,12 +71,13 @@ body.background-effects.platform-win32.layout-vertical #vertical-main-container
|
||||
/* #endregion */
|
||||
|
||||
/* Matches when the left pane is collapsed */
|
||||
:has(.layout-vertical #left-pane.hidden-int) {
|
||||
#horizontal-main-container.left-pane-hidden {
|
||||
--center-pane-border-radius: 0;
|
||||
--tab-first-item-horiz-offset: 5px;
|
||||
}
|
||||
|
||||
:has(#left-pane.hidden-int) #launcher-pane.vertical {
|
||||
/* Add a border to the vertical launch bar if collapsed. */
|
||||
body.layout-vertical #horizontal-main-container.left-pane-hidden #launcher-pane.vertical {
|
||||
border-right: 2px solid var(--left-pane-collapsed-border-color);
|
||||
}
|
||||
|
||||
@@ -1300,9 +1301,9 @@ div.promoted-attribute-cell .tn-checkbox {
|
||||
height: 1cap;
|
||||
}
|
||||
|
||||
/* The <div> containing the checkbox for a promoted boolean attribute */
|
||||
div.promoted-attribute-cell div:has(input[type="checkbox"]) {
|
||||
order: -1; /* Relocate the checkbox before the label */
|
||||
/* Relocate the checkbox before the label */
|
||||
div.promoted-attribute-cell.promoted-attribute-label-boolean > div:first-of-type {
|
||||
order: -1;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
|
||||
@@ -754,7 +754,7 @@
|
||||
"expand_all_children": "展开所有子项",
|
||||
"collapse": "折叠",
|
||||
"expand": "展开",
|
||||
"book_properties": "书籍属性",
|
||||
"book_properties": "",
|
||||
"invalid_view_type": "无效的查看类型 '{{type}}'",
|
||||
"calendar": "日历"
|
||||
},
|
||||
@@ -1431,7 +1431,6 @@
|
||||
"move-to": "移动到...",
|
||||
"paste-into": "粘贴到里面",
|
||||
"paste-after": "粘贴到后面",
|
||||
"duplicate-subtree": "复制子树",
|
||||
"export": "导出",
|
||||
"import-into-note": "导入到笔记",
|
||||
"apply-bulk-actions": "应用批量操作",
|
||||
|
||||
@@ -750,7 +750,7 @@
|
||||
"expand_all_children": "Unternotizen ausklappen",
|
||||
"collapse": "Einklappen",
|
||||
"expand": "Ausklappen",
|
||||
"book_properties": "Bucheigenschaften",
|
||||
"book_properties": "",
|
||||
"invalid_view_type": "Ungültiger Ansichtstyp „{{type}}“",
|
||||
"calendar": "Kalender"
|
||||
},
|
||||
@@ -1384,7 +1384,7 @@
|
||||
"move-to": "Verschieben nach...",
|
||||
"paste-into": "Als Unternotiz einfügen",
|
||||
"paste-after": "Danach einfügen",
|
||||
"duplicate-subtree": "Notizbaum duplizieren",
|
||||
"duplicate": "Duplizieren",
|
||||
"export": "Exportieren",
|
||||
"import-into-note": "In Notiz importieren",
|
||||
"apply-bulk-actions": "Massenaktionen ausführen",
|
||||
|
||||
@@ -758,9 +758,11 @@
|
||||
"expand_all_children": "Expand all children",
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"book_properties": "Book Properties",
|
||||
"book_properties": "Collection Properties",
|
||||
"invalid_view_type": "Invalid view type '{{type}}'",
|
||||
"calendar": "Calendar"
|
||||
"calendar": "Calendar",
|
||||
"table": "Table",
|
||||
"geo-map": "Geo Map"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "No edited notes on this day yet...",
|
||||
@@ -960,7 +962,7 @@
|
||||
"no_attachments": "This note has no attachments."
|
||||
},
|
||||
"book": {
|
||||
"no_children_help": "This note of type Book doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details."
|
||||
"no_children_help": "This collection doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details."
|
||||
},
|
||||
"editable_code": {
|
||||
"placeholder": "Type the content of your code note here..."
|
||||
@@ -1023,7 +1025,7 @@
|
||||
"title": "Consistency Checks",
|
||||
"find_and_fix_button": "Find and fix consistency issues",
|
||||
"finding_and_fixing_message": "Finding and fixing consistency issues...",
|
||||
"issues_fixed_message": "Consistency issues should be fixed."
|
||||
"issues_fixed_message": "Any consistency issue which may have been found is now fixed."
|
||||
},
|
||||
"database_anonymization": {
|
||||
"title": "Database Anonymization",
|
||||
@@ -1593,12 +1595,13 @@
|
||||
"move-to": "Move to...",
|
||||
"paste-into": "Paste into",
|
||||
"paste-after": "Paste after",
|
||||
"duplicate-subtree": "Duplicate subtree",
|
||||
"duplicate": "Duplicate",
|
||||
"export": "Export",
|
||||
"import-into-note": "Import into note",
|
||||
"apply-bulk-actions": "Apply bulk actions",
|
||||
"converted-to-attachments": "{{count}} notes have been converted to attachments.",
|
||||
"convert-to-attachment-confirm": "Are you sure you want to convert note selected notes into attachments of their parent notes?"
|
||||
"convert-to-attachment-confirm": "Are you sure you want to convert note selected notes into attachments of their parent notes?",
|
||||
"open-in-popup": "Quick edit"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "This note is shared publicly on",
|
||||
@@ -1626,7 +1629,9 @@
|
||||
"geo-map": "Geo Map",
|
||||
"beta-feature": "Beta",
|
||||
"ai-chat": "AI Chat",
|
||||
"task-list": "Task List"
|
||||
"task-list": "Task List",
|
||||
"new-feature": "New",
|
||||
"collections": "Collections"
|
||||
},
|
||||
"protect_note": {
|
||||
"toggle-on": "Protect the note",
|
||||
@@ -1828,7 +1833,8 @@
|
||||
"link_context_menu": {
|
||||
"open_note_in_new_tab": "Open note in a new tab",
|
||||
"open_note_in_new_split": "Open note in a new split",
|
||||
"open_note_in_new_window": "Open note in a new window"
|
||||
"open_note_in_new_window": "Open note in a new window",
|
||||
"open_note_in_popup": "Quick edit"
|
||||
},
|
||||
"electron_integration": {
|
||||
"desktop-application": "Desktop Application",
|
||||
@@ -1848,7 +1854,8 @@
|
||||
"full-text-search": "Full text search"
|
||||
},
|
||||
"note_tooltip": {
|
||||
"note-has-been-deleted": "Note has been deleted."
|
||||
"note-has-been-deleted": "Note has been deleted.",
|
||||
"quick-edit": "Quick edit"
|
||||
},
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Create a new child note and add it to the map",
|
||||
@@ -1857,7 +1864,8 @@
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Open location",
|
||||
"remove-from-map": "Remove from map"
|
||||
"remove-from-map": "Remove from map",
|
||||
"add-note": "Add a marker at this location"
|
||||
},
|
||||
"help-button": {
|
||||
"title": "Open the relevant help page"
|
||||
@@ -1933,5 +1941,32 @@
|
||||
"title": "Features",
|
||||
"emoji_completion_enabled": "Enable Emoji auto-completion",
|
||||
"note_completion_enabled": "Enable note auto-completion"
|
||||
},
|
||||
"table_view": {
|
||||
"new-row": "New row",
|
||||
"new-column": "New column",
|
||||
"sort-column-by": "Sort by \"{{title}}\"",
|
||||
"sort-column-ascending": "Ascending",
|
||||
"sort-column-descending": "Descending",
|
||||
"sort-column-clear": "Clear sorting",
|
||||
"hide-column": "Hide column \"{{title}}\"",
|
||||
"show-hide-columns": "Show/hide columns",
|
||||
"row-insert-above": "Insert row above",
|
||||
"row-insert-below": "Insert row below",
|
||||
"row-insert-child": "Insert child note",
|
||||
"add-column-to-the-left": "Add column to the left",
|
||||
"add-column-to-the-right": "Add column to the right",
|
||||
"edit-column": "Edit column",
|
||||
"delete_column_confirmation": "Are you sure you want to delete this column? The corresponding attribute will be removed from all notes.",
|
||||
"delete-column": "Delete column",
|
||||
"new-column-label": "Label",
|
||||
"new-column-relation": "Relation"
|
||||
},
|
||||
"book_properties_config": {
|
||||
"hide-weekends": "Hide weekends",
|
||||
"display-week-numbers": "Display week numbers"
|
||||
},
|
||||
"table_context_menu": {
|
||||
"delete_row": "Delete row"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,7 +758,7 @@
|
||||
"expand_all_children": "Ampliar todas las subnotas",
|
||||
"collapse": "Colapsar",
|
||||
"expand": "Expandir",
|
||||
"book_properties": "Propiedades del libro",
|
||||
"book_properties": "",
|
||||
"invalid_view_type": "Tipo de vista inválida '{{type}}'",
|
||||
"calendar": "Calendario"
|
||||
},
|
||||
@@ -1593,7 +1593,7 @@
|
||||
"move-to": "Mover a...",
|
||||
"paste-into": "Pegar en",
|
||||
"paste-after": "Pegar después de",
|
||||
"duplicate-subtree": "Duplicar subárbol",
|
||||
"duplicate": "Duplicar",
|
||||
"export": "Exportar",
|
||||
"import-into-note": "Importar a nota",
|
||||
"apply-bulk-actions": "Aplicar acciones en lote",
|
||||
|
||||
@@ -753,7 +753,7 @@
|
||||
"expand_all_children": "Développer tous les enfants",
|
||||
"collapse": "Réduire",
|
||||
"expand": "Développer",
|
||||
"book_properties": "Propriétés du livre",
|
||||
"book_properties": "",
|
||||
"invalid_view_type": "Type de vue non valide '{{type}}'",
|
||||
"calendar": "Calendrier"
|
||||
},
|
||||
@@ -1389,7 +1389,7 @@
|
||||
"move-to": "Déplacer vers...",
|
||||
"paste-into": "Coller dans",
|
||||
"paste-after": "Coller après",
|
||||
"duplicate-subtree": "Dupliquer le sous-arbre",
|
||||
"duplicate": "Dupliquer",
|
||||
"export": "Exporter",
|
||||
"import-into-note": "Importer dans la note",
|
||||
"apply-bulk-actions": "Appliquer des Actions groupées",
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
"no_children_help": "Această notiță de tip Carte nu are nicio subnotiță așadar nu este nimic de afișat. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> pentru detalii."
|
||||
},
|
||||
"book_properties": {
|
||||
"book_properties": "Proprietăți carte",
|
||||
"book_properties": "",
|
||||
"collapse": "Minimizează",
|
||||
"collapse_all_notes": "Minimizează toate notițele",
|
||||
"expand": "Expandează",
|
||||
@@ -1349,7 +1349,7 @@
|
||||
"copy-note-path-to-clipboard": "Copiază calea notiței în clipboard",
|
||||
"cut": "Decupează",
|
||||
"delete": "Șterge",
|
||||
"duplicate-subtree": "Dublifică ierarhia",
|
||||
"duplicate": "Dublifică",
|
||||
"edit-branch-prefix": "Editează prefixul ramurii",
|
||||
"expand-subtree": "Expandează subnotițele",
|
||||
"export": "Exportă",
|
||||
|
||||
@@ -718,7 +718,7 @@
|
||||
"expand_all_children": "展開所有子項",
|
||||
"collapse": "折疊",
|
||||
"expand": "展開",
|
||||
"book_properties": "書籍屬性",
|
||||
"book_properties": "",
|
||||
"invalid_view_type": "無效的查看類型 '{{type}}'"
|
||||
},
|
||||
"edited_notes": {
|
||||
@@ -1336,7 +1336,6 @@
|
||||
"move-to": "移動到...",
|
||||
"paste-into": "貼上到裡面",
|
||||
"paste-after": "貼上到後面",
|
||||
"duplicate-subtree": "複製子樹",
|
||||
"export": "匯出",
|
||||
"import-into-note": "匯入到筆記",
|
||||
"apply-bulk-actions": "應用批量操作",
|
||||
|
||||
@@ -78,7 +78,7 @@ const TPL = /*html*/`
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
|
||||
<h5 class="attr-detail-title">${t("attribute_detail.attr_detail_title")}</h5>
|
||||
|
||||
<span class="bx bx-x close-attr-detail-button tn-tool-button" title="${t("attribute_detail.close_button_title")}"></span>
|
||||
@@ -295,6 +295,8 @@ interface AttributeDetailOpts {
|
||||
x: number;
|
||||
y: number;
|
||||
focus?: "name";
|
||||
parent?: HTMLElement;
|
||||
hideMultiplicity?: boolean;
|
||||
}
|
||||
|
||||
interface SearchRelatedResponse {
|
||||
@@ -477,7 +479,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
});
|
||||
}
|
||||
|
||||
async showAttributeDetail({ allAttributes, attribute, isOwned, x, y, focus }: AttributeDetailOpts) {
|
||||
async showAttributeDetail({ allAttributes, attribute, isOwned, x, y, focus, hideMultiplicity }: AttributeDetailOpts) {
|
||||
if (!attribute) {
|
||||
this.hide();
|
||||
|
||||
@@ -528,7 +530,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$rowPromotedAlias.toggle(!!definition.isPromoted);
|
||||
this.$inputPromotedAlias.val(definition.promotedAlias || "").attr("disabled", disabledFn);
|
||||
|
||||
this.$rowMultiplicity.toggle(["label-definition", "relation-definition"].includes(this.attrType || ""));
|
||||
this.$rowMultiplicity.toggle(["label-definition", "relation-definition"].includes(this.attrType || "") && !hideMultiplicity);
|
||||
this.$inputMultiplicity.val(definition.multiplicity || "").attr("disabled", disabledFn);
|
||||
|
||||
this.$rowLabelType.toggle(this.attrType === "label-definition");
|
||||
@@ -560,19 +562,22 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.toggleInt(true);
|
||||
|
||||
const offset = this.parent?.$widget.offset() || { top: 0, left: 0 };
|
||||
const offset = this.parent?.$widget?.offset() || { top: 0, left: 0 };
|
||||
const detPosition = this.getDetailPosition(x, offset);
|
||||
const outerHeight = this.$widget.outerHeight();
|
||||
const height = $(window).height();
|
||||
|
||||
if (detPosition && outerHeight && height) {
|
||||
this.$widget
|
||||
.css("left", detPosition.left)
|
||||
.css("right", detPosition.right)
|
||||
.css("top", y - offset.top + 70)
|
||||
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
|
||||
if (!detPosition || !outerHeight || !height) {
|
||||
console.warn("Can't position popup, is it attached?");
|
||||
return;
|
||||
}
|
||||
|
||||
this.$widget
|
||||
.css("left", detPosition.left)
|
||||
.css("right", detPosition.right)
|
||||
.css("top", y - offset.top + 70)
|
||||
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
|
||||
|
||||
if (focus === "name") {
|
||||
this.$inputName.trigger("focus").trigger("select");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import noteAutocompleteService, { type Suggestion } from "../../services/note_au
|
||||
import server from "../../services/server.js";
|
||||
import contextMenuService from "../../menus/context_menu.js";
|
||||
import attributeParser, { type Attribute } from "../../services/attribute_parser.js";
|
||||
import { AttributeEditor, type EditorConfig, type Element, type MentionFeed, type Node, type Position } from "@triliumnext/ckeditor5";
|
||||
import { AttributeEditor, type EditorConfig, type ModelElement, type MentionFeed, type ModelNode, type ModelPosition } from "@triliumnext/ckeditor5";
|
||||
import froca from "../../services/froca.js";
|
||||
import attributeRenderer from "../../services/attribute_renderer.js";
|
||||
import noteCreateService from "../../services/note_create.js";
|
||||
@@ -417,16 +417,16 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
|
||||
this.$editor.tooltip("show");
|
||||
}
|
||||
|
||||
getClickIndex(pos: Position) {
|
||||
getClickIndex(pos: ModelPosition) {
|
||||
let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0);
|
||||
|
||||
let curNode: Node | Text | Element | null = pos.textNode;
|
||||
let curNode: ModelNode | Text | ModelElement | null = pos.textNode;
|
||||
|
||||
while (curNode?.previousSibling) {
|
||||
curNode = curNode.previousSibling;
|
||||
|
||||
if ((curNode as Element).name === "reference") {
|
||||
clickIndex += (curNode.getAttribute("notePath") as string).length + 1;
|
||||
if ((curNode as ModelElement).name === "reference") {
|
||||
clickIndex += (curNode.getAttribute("href") as string).length + 1;
|
||||
} else if ("data" in curNode) {
|
||||
clickIndex += (curNode.data as string).length;
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
|
||||
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book", "mindMap"].includes(note.type));
|
||||
|
||||
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
|
||||
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type));
|
||||
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type));
|
||||
|
||||
const canPrint = ["text", "code"].includes(note.type);
|
||||
this.toggleDisabled(this.$printActiveNoteButton, canPrint);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { default as Component, TypedComponent } from "../../components/component.js";
|
||||
import BasicWidget, { TypedBasicWidget } from "../basic_widget.js";
|
||||
import type { TypedComponent } from "../../components/component.js";
|
||||
import { TypedBasicWidget } from "../basic_widget.js";
|
||||
|
||||
export default class Container<T extends TypedComponent<any>> extends TypedBasicWidget<T> {
|
||||
doRender() {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type Component from "../../components/component.js";
|
||||
|
||||
export default class LeftPaneContainer extends FlexContainer<Component> {
|
||||
private currentLeftPaneVisible: boolean;
|
||||
|
||||
|
||||
constructor() {
|
||||
super("column");
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class LeftPaneContainer extends FlexContainer<Component> {
|
||||
this.currentLeftPaneVisible = leftPaneVisible ?? !this.currentLeftPaneVisible;
|
||||
const visible = this.isEnabled();
|
||||
this.toggleInt(visible);
|
||||
this.parent?.$widget.toggleClass("left-pane-hidden", !visible);
|
||||
|
||||
if (visible) {
|
||||
this.triggerEvent("focusTree", {});
|
||||
|
||||
@@ -154,13 +154,21 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
||||
this.$noteTypeDropdown.append($('<h6 class="dropdown-header">').append(t("note_type_chooser.templates")));
|
||||
} else {
|
||||
const commandItem = noteType as MenuCommandItem<CommandNames>;
|
||||
this.$noteTypeDropdown.append(
|
||||
$('<a class="dropdown-item" tabindex="0">')
|
||||
.attr("data-note-type", commandItem.type || "")
|
||||
.attr("data-template-note-id", commandItem.templateNoteId || "")
|
||||
.append($("<span>").addClass(commandItem.uiIcon || ""))
|
||||
.append(` ${noteType.title}`)
|
||||
);
|
||||
const listItem = $('<a class="dropdown-item" tabindex="0">')
|
||||
.attr("data-note-type", commandItem.type || "")
|
||||
.attr("data-template-note-id", commandItem.templateNoteId || "")
|
||||
.append($("<span>").addClass(commandItem.uiIcon || ""))
|
||||
.append(` ${noteType.title}`);
|
||||
|
||||
if (commandItem.badges) {
|
||||
for (let badge of commandItem.badges) {
|
||||
listItem.append($(`<span class="badge">`)
|
||||
.addClass(badge.className || "")
|
||||
.text(badge.title));
|
||||
}
|
||||
}
|
||||
|
||||
this.$noteTypeDropdown.append(listItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
157
apps/client/src/widgets/dialogs/popup_editor.ts
Normal file
157
apps/client/src/widgets/dialogs/popup_editor.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type { EventNames, EventData } from "../../components/app_context.js";
|
||||
import NoteContext from "../../components/note_context.js";
|
||||
import { openDialog } from "../../services/dialog.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import Container from "../containers/container.js";
|
||||
import TypeWidget from "../type_widgets/type_widget.js";
|
||||
|
||||
const TPL = /*html*/`\
|
||||
<div class="popup-editor-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<style>
|
||||
body.desktop .modal.popup-editor-dialog .modal-dialog {
|
||||
max-width: 75vw;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-header .modal-title {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-body {
|
||||
padding: 0;
|
||||
height: 75vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail-editable-text {
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .title-row,
|
||||
.modal.popup-editor-dialog .modal-title,
|
||||
.modal.popup-editor-dialog .note-icon-widget {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-icon-widget {
|
||||
width: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-icon-widget button.note-icon,
|
||||
.modal.popup-editor-dialog .note-title-widget input.note-title {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .classic-toolbar-widget {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--modal-background-color);
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail-file {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">
|
||||
<!-- This is where the first child will be injected -->
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- This is where all but the first child will be injected. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class PopupEditorDialog extends Container<BasicWidget> {
|
||||
|
||||
private noteContext: NoteContext;
|
||||
private $modalHeader!: JQuery<HTMLElement>;
|
||||
private $modalBody!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.noteContext = new NoteContext("_popup-editor");
|
||||
}
|
||||
|
||||
doRender() {
|
||||
// This will populate this.$widget with the content of the children.
|
||||
super.doRender();
|
||||
|
||||
// Now we wrap it in the modal.
|
||||
const $newWidget = $(TPL);
|
||||
this.$modalHeader = $newWidget.find(".modal-title");
|
||||
this.$modalBody = $newWidget.find(".modal-body");
|
||||
|
||||
const children = this.$widget.children();
|
||||
this.$modalHeader.append(children[0]);
|
||||
this.$modalBody.append(children.slice(1));
|
||||
this.$widget = $newWidget;
|
||||
this.setVisibility(false);
|
||||
}
|
||||
|
||||
async openInPopupEvent({ noteIdOrPath }: EventData<"openInPopup">) {
|
||||
const $dialog = await openDialog(this.$widget, false, {
|
||||
focus: false
|
||||
});
|
||||
|
||||
await this.noteContext.setNote(noteIdOrPath);
|
||||
|
||||
const activeEl = document.activeElement;
|
||||
if (activeEl && "blur" in activeEl) {
|
||||
(activeEl as HTMLElement).blur();
|
||||
}
|
||||
|
||||
$dialog.on("shown.bs.modal", async () => {
|
||||
// Reduce the z-index of modals so that ckeditor popups are properly shown on top of it.
|
||||
// The backdrop instance is not shared so it's OK to make a one-off modification.
|
||||
$("body > .modal-backdrop").css("z-index", "998");
|
||||
$dialog.css("z-index", "999");
|
||||
|
||||
await this.handleEventInChildren("activeContextChanged", { noteContext: this.noteContext });
|
||||
this.setVisibility(true);
|
||||
await this.handleEventInChildren("focusOnDetail", { ntxId: this.noteContext.ntxId });
|
||||
});
|
||||
$dialog.on("hidden.bs.modal", () => {
|
||||
const $typeWidgetEl = $dialog.find(".note-detail-printable");
|
||||
if ($typeWidgetEl.length) {
|
||||
const typeWidget = glob.getComponentByEl($typeWidgetEl[0]) as TypeWidget;
|
||||
typeWidget.cleanup();
|
||||
}
|
||||
|
||||
this.setVisibility(false);
|
||||
});
|
||||
}
|
||||
|
||||
setVisibility(visible: boolean) {
|
||||
const $bodyItems = this.$modalBody.find("> div");
|
||||
if (visible) {
|
||||
$bodyItems.fadeIn();
|
||||
this.$modalHeader.children().show();
|
||||
} else {
|
||||
$bodyItems.hide();
|
||||
this.$modalHeader.children().hide();
|
||||
}
|
||||
}
|
||||
|
||||
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
|
||||
// Avoid events related to the current tab interfere with our popup.
|
||||
if (["noteSwitched", "noteSwitchedAndActivated"].includes(name)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return super.handleEventInChildren(name, data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,7 +23,9 @@ const TPL = /*html*/`\
|
||||
export default class GeoMapButtons extends NoteContextAwareWidget {
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.note?.type === "geoMap";
|
||||
return super.isEnabled()
|
||||
&& this.note?.getLabelValue("viewType") === "geoMap"
|
||||
&& !this.note.hasLabel("readOnly");
|
||||
}
|
||||
|
||||
doRender() {
|
||||
|
||||
@@ -17,7 +17,6 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
contentWidget: null,
|
||||
doc: null,
|
||||
file: null,
|
||||
geoMap: "81SGnPGMk7Xc",
|
||||
image: null,
|
||||
launcher: null,
|
||||
mermaid: null,
|
||||
@@ -32,9 +31,11 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
};
|
||||
|
||||
export const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
list: null,
|
||||
grid: null,
|
||||
calendar: "xWbu3jpNWapp"
|
||||
list: "mULW0Q3VojwY",
|
||||
grid: "8QqnMzx393bx",
|
||||
calendar: "xWbu3jpNWapp",
|
||||
table: "2FvYrpmOXm29",
|
||||
geoMap: "81SGnPGMk7Xc"
|
||||
};
|
||||
|
||||
export default class ContextualHelpButton extends NoteContextAwareWidget {
|
||||
|
||||
@@ -39,10 +39,20 @@ export default class ToggleReadOnlyButton extends OnClickButtonWidget {
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled()
|
||||
&& this.note?.type === "mermaid"
|
||||
&& this.note?.isContentAvailable()
|
||||
&& this.noteContext?.viewScope?.viewMode === "default";
|
||||
if (!super.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this?.note?.isContentAvailable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.noteContext?.viewScope?.viewMode !== "default") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.note.type === "mermaid" ||
|
||||
(this.note.getLabelValue("viewType") === "geoMap");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { Map } from "leaflet";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
|
||||
const TPL = /*html*/`\
|
||||
<div class="geo-map-widget">
|
||||
<style>
|
||||
.note-detail-geo-map,
|
||||
.geo-map-widget,
|
||||
.geo-map-container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
z-index: 900;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="geo-map-container"></div>
|
||||
</div>`;
|
||||
|
||||
export type Leaflet = typeof L;
|
||||
export type InitCallback = (L: Leaflet) => void;
|
||||
|
||||
export default class GeoMapWidget extends NoteContextAwareWidget {
|
||||
|
||||
map?: Map;
|
||||
$container!: JQuery<HTMLElement>;
|
||||
private initCallback?: InitCallback;
|
||||
|
||||
constructor(widgetMode: "type", initCallback?: InitCallback) {
|
||||
super();
|
||||
this.initCallback = initCallback;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$container = this.$widget.find(".geo-map-container");
|
||||
|
||||
const map = L.map(this.$container[0], {
|
||||
worldCopyJump: true
|
||||
});
|
||||
|
||||
this.map = map;
|
||||
if (this.initCallback) {
|
||||
this.initCallback(L);
|
||||
}
|
||||
|
||||
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
detectRetina: true
|
||||
}).addTo(map);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
|
||||
import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
|
||||
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
|
||||
import MindMapWidget from "./type_widgets/mind_map.js";
|
||||
import GeoMapTypeWidget from "./type_widgets/geo_map.js";
|
||||
import utils from "../services/utils.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import type TypeWidget from "./type_widgets/type_widget.js";
|
||||
@@ -71,7 +70,6 @@ const typeWidgetClasses = {
|
||||
attachmentDetail: AttachmentDetailTypeWidget,
|
||||
attachmentList: AttachmentListTypeWidget,
|
||||
mindMap: MindMapWidget,
|
||||
geoMap: GeoMapTypeWidget,
|
||||
aiChat: AiChatTypeWidget,
|
||||
|
||||
// Split type editors
|
||||
@@ -197,7 +195,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
const isBackendNote = this.noteContext?.noteId === "_backendLog";
|
||||
const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
|
||||
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap", "mermaid"].includes(this.type ?? "");
|
||||
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "mermaid", "file"].includes(this.type ?? "");
|
||||
const isFullHeight = (!this.noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|
||||
|| this.noteContext?.viewScope?.viewMode === "attachments"
|
||||
|| isBackendNote;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import NoteListRenderer from "../services/note_list_renderer.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { CommandListener, CommandListenerData, EventData } from "../components/app_context.js";
|
||||
import type { CommandListener, CommandListenerData, CommandMappings, CommandNames, EventData, EventNames } from "../components/app_context.js";
|
||||
import type ViewMode from "./view_widgets/view_mode.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
@@ -36,10 +36,31 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
private isIntersecting?: boolean;
|
||||
private noteIdRefreshed?: string;
|
||||
private shownNoteId?: string | null;
|
||||
private viewMode?: ViewMode | null;
|
||||
private viewMode?: ViewMode<any> | null;
|
||||
private displayOnlyCollections: boolean;
|
||||
|
||||
/**
|
||||
* @param displayOnlyCollections if set to `true` then only collection-type views are displayed such as geo-map and the calendar. The original book types grid and list will be ignored.
|
||||
*/
|
||||
constructor(displayOnlyCollections: boolean) {
|
||||
super();
|
||||
|
||||
this.displayOnlyCollections = displayOnlyCollections;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.noteContext?.hasNoteList();
|
||||
if (!super.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.displayOnlyCollections && this.note?.type !== "book") {
|
||||
const viewType = this.note?.getLabelValue("viewType");
|
||||
if (!viewType || ["grid", "list"].includes(viewType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return this.noteContext?.hasNoteList();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
@@ -76,7 +97,11 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async renderNoteList(note: FNote) {
|
||||
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
|
||||
const noteListRenderer = new NoteListRenderer({
|
||||
$parent: this.$content,
|
||||
parentNote: note,
|
||||
parentNotePath: this.notePath
|
||||
});
|
||||
this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight);
|
||||
await noteListRenderer.renderList();
|
||||
this.viewMode = noteListRenderer.viewMode;
|
||||
@@ -120,12 +145,6 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
this.refresh();
|
||||
this.checkRenderStatus();
|
||||
}
|
||||
|
||||
// Inform the view mode of changes and refresh if needed.
|
||||
if (this.viewMode && this.viewMode.onEntitiesReloaded(e)) {
|
||||
this.refresh();
|
||||
this.checkRenderStatus();
|
||||
}
|
||||
}
|
||||
|
||||
buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) {
|
||||
@@ -134,4 +153,26 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
triggerCommand<K extends CommandNames>(name: K, data?: CommandMappings[K]): Promise<unknown> | undefined | null {
|
||||
// Pass the commands to the view mode, which is not actually attached to the hierarchy.
|
||||
if (this.viewMode?.triggerCommand(name, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return super.triggerCommand(name, data);
|
||||
}
|
||||
|
||||
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
|
||||
super.handleEventInChildren(name, data);
|
||||
|
||||
if (this.viewMode) {
|
||||
const ret = this.viewMode.handleEvent(name, data);
|
||||
if (ret) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -324,7 +324,13 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
const mapRootNoteId = this.getMapRootNoteId();
|
||||
const data = await this.loadNotesAndRelations(mapRootNoteId);
|
||||
|
||||
const labelValues = (name: string) => this.note?.getLabels(name).map(l => l.value) ?? [];
|
||||
|
||||
const excludeRelations = labelValues("mapExcludeRelation");
|
||||
const includeRelations = labelValues("mapIncludeRelation");
|
||||
|
||||
const data = await this.loadNotesAndRelations(mapRootNoteId, excludeRelations, includeRelations);
|
||||
|
||||
const nodeLinkRatio = data.nodes.length / data.links.length;
|
||||
const magnifiedRatio = Math.pow(nodeLinkRatio, 1.5);
|
||||
@@ -473,8 +479,10 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
async loadNotesAndRelations(mapRootNoteId: string): Promise<NotesAndRelationsData> {
|
||||
const resp = await server.post<PostNotesMapResponse>(`note-map/${mapRootNoteId}/${this.mapType}`);
|
||||
async loadNotesAndRelations(mapRootNoteId: string, excludeRelations: string[], includeRelations: string[]): Promise<NotesAndRelationsData> {
|
||||
const resp = await server.post<PostNotesMapResponse>(`note-map/${mapRootNoteId}/${this.mapType}`, {
|
||||
excludeRelations, includeRelations
|
||||
});
|
||||
|
||||
this.calculateNodeSizes(resp);
|
||||
|
||||
|
||||
@@ -186,6 +186,15 @@ interface RefreshContext {
|
||||
noteIdsToReload: Set<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The information contained within a drag event.
|
||||
*/
|
||||
export interface DragData {
|
||||
noteId: string;
|
||||
branchId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
private $tree!: JQuery<HTMLElement>;
|
||||
private $treeActions!: JQuery<HTMLElement>;
|
||||
@@ -231,15 +240,21 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
this.$tree.on("mousedown", ".fancytree-title", (e) => {
|
||||
if (e.which === 2) {
|
||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||
|
||||
const notePath = treeService.getNotePath(node);
|
||||
|
||||
if (notePath) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
||||
activate: e.shiftKey ? true : false
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
this.$tree.on("mouseup", ".fancytree-title", (e) => {
|
||||
// Prevent middle click from pasting in the editor.
|
||||
if (e.which === 2) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -698,7 +713,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
});
|
||||
} else {
|
||||
this.$tree.on("contextmenu", ".fancytree-node", (e) => {
|
||||
this.showContextMenu(e);
|
||||
if (!utils.isCtrlKey(e)) {
|
||||
this.showContextMenu(e);
|
||||
} else {
|
||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||
const notePath = treeService.getNotePath(node);
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
||||
}
|
||||
return false; // blocks default browser right click menu
|
||||
});
|
||||
|
||||
|
||||
@@ -1,59 +1,15 @@
|
||||
import server from "../services/server.js";
|
||||
import { Dropdown } from "bootstrap";
|
||||
import { NOTE_TYPES } from "../services/note_types.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import mimeTypesService from "../services/mime_types.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import server from "../services/server.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
import { Dropdown } from "bootstrap";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
|
||||
interface NoteTypeMapping {
|
||||
type: NoteType;
|
||||
mime?: string;
|
||||
title: string;
|
||||
isBeta?: boolean;
|
||||
selectable: boolean;
|
||||
}
|
||||
|
||||
const NOTE_TYPES: NoteTypeMapping[] = [
|
||||
// The suggested note type ordering method: insert the item into the corresponding group,
|
||||
// then ensure the items within the group are ordered alphabetically.
|
||||
|
||||
// The default note type (always the first item)
|
||||
{ type: "text", mime: "text/html", title: t("note_types.text"), selectable: true },
|
||||
|
||||
// Text notes group
|
||||
{ type: "book", mime: "", title: t("note_types.book"), selectable: true },
|
||||
|
||||
// Graphic notes
|
||||
{ type: "canvas", mime: "application/json", title: t("note_types.canvas"), selectable: true },
|
||||
{ type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), selectable: true },
|
||||
|
||||
// Map notes
|
||||
{ type: "geoMap", mime: "application/json", title: t("note_types.geo-map"), isBeta: true, selectable: true },
|
||||
{ type: "mindMap", mime: "application/json", title: t("note_types.mind-map"), selectable: true },
|
||||
{ type: "relationMap", mime: "application/json", title: t("note_types.relation-map"), selectable: true },
|
||||
|
||||
// Misc note types
|
||||
{ type: "render", mime: "", title: t("note_types.render-note"), selectable: true },
|
||||
{ type: "webView", mime: "", title: t("note_types.web-view"), selectable: true },
|
||||
|
||||
// Code notes
|
||||
{ type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true },
|
||||
|
||||
// Reserved types (cannot be created by the user)
|
||||
{ type: "contentWidget", mime: "", title: t("note_types.widget"), selectable: false },
|
||||
{ type: "doc", mime: "", title: t("note_types.doc"), selectable: false },
|
||||
{ type: "file", title: t("note_types.file"), selectable: false },
|
||||
{ type: "image", title: t("note_types.image"), selectable: false },
|
||||
{ type: "launcher", mime: "", title: t("note_types.launcher"), selectable: false },
|
||||
{ type: "noteMap", mime: "", title: t("note_types.note-map"), selectable: false },
|
||||
{ type: "search", title: t("note_types.saved-search"), selectable: false },
|
||||
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), selectable: false }
|
||||
];
|
||||
|
||||
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => !nt.selectable).map((nt) => nt.type);
|
||||
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type);
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="dropdown note-type-widget">
|
||||
@@ -63,13 +19,6 @@ const TPL = /*html*/`
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.note-type-dropdown .badge {
|
||||
margin-left: 8px;
|
||||
background: var(--accented-background-color);
|
||||
font-weight: normal;
|
||||
color: var(--menu-text-color);
|
||||
}
|
||||
</style>
|
||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button note-type-button">
|
||||
<span class="note-type-desc"></span>
|
||||
@@ -116,10 +65,15 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const noteType of NOTE_TYPES.filter((nt) => nt.selectable)) {
|
||||
for (const noteType of NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static)) {
|
||||
let $typeLink: JQuery<HTMLElement>;
|
||||
|
||||
const $title = $("<span>").text(noteType.title);
|
||||
|
||||
if (noteType.isNew) {
|
||||
$title.append($(`<span class="badge new-note-type-badge">`).text(t("note_types.new-feature")));
|
||||
}
|
||||
|
||||
if (noteType.isBeta) {
|
||||
$title.append($(`<span class="badge">`).text(t("note_types.beta-feature")));
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
}
|
||||
|
||||
#isFullWidthNote(note: FNote) {
|
||||
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap", "geoMap"].includes(note.type)) {
|
||||
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import attributeService from "../../services/attributes.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import { bookPropertiesConfig, BookProperty } from "./book_properties_config.js";
|
||||
import attributes from "../../services/attributes.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="book-properties-widget">
|
||||
@@ -15,6 +17,24 @@ const TPL = /*html*/`
|
||||
.book-properties-widget > * {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.book-properties-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.book-properties-container > div {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.book-properties-container > .type-number > label {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.book-properties-container input[type="checkbox"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="display: flex; align-items: baseline">
|
||||
@@ -24,33 +44,21 @@ const TPL = /*html*/`
|
||||
<option value="grid">${t("book_properties.grid")}</option>
|
||||
<option value="list">${t("book_properties.list")}</option>
|
||||
<option value="calendar">${t("book_properties.calendar")}</option>
|
||||
<option value="table">${t("book_properties.table")}</option>
|
||||
<option value="geoMap">${t("book_properties.geo-map")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
class="collapse-all-button btn btn-sm"
|
||||
title="${t("book_properties.collapse_all_notes")}">
|
||||
|
||||
<span class="bx bx-layer-minus"></span>
|
||||
|
||||
${t("book_properties.collapse")}
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="expand-children-button btn btn-sm"
|
||||
title="${t("book_properties.expand_all_children")}">
|
||||
<span class="bx bx-move-vertical"></span>
|
||||
|
||||
${t("book_properties.expand")}
|
||||
</button>
|
||||
<div class="book-properties-container">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $viewTypeSelect!: JQuery<HTMLElement>;
|
||||
private $expandChildrenButton!: JQuery<HTMLElement>;
|
||||
private $collapseAllButton!: JQuery<HTMLElement>;
|
||||
private $propertiesContainer!: JQuery<HTMLElement>;
|
||||
private labelsToWatch: string[] = [];
|
||||
|
||||
get name() {
|
||||
return "bookProperties";
|
||||
@@ -67,7 +75,6 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
getTitle() {
|
||||
return {
|
||||
show: this.isEnabled(),
|
||||
activate: true,
|
||||
title: t("book_properties.book_properties"),
|
||||
icon: "bx bx-book"
|
||||
};
|
||||
@@ -80,32 +87,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
this.$viewTypeSelect = this.$widget.find(".view-type-select");
|
||||
this.$viewTypeSelect.on("change", () => this.toggleViewType(String(this.$viewTypeSelect.val())));
|
||||
|
||||
this.$expandChildrenButton = this.$widget.find(".expand-children-button");
|
||||
this.$expandChildrenButton.on("click", async () => {
|
||||
if (!this.noteId || !this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.note?.isLabelTruthy("expanded")) {
|
||||
await attributeService.addLabel(this.noteId, "expanded");
|
||||
}
|
||||
|
||||
this.triggerCommand("refreshNoteList", { noteId: this.noteId });
|
||||
});
|
||||
|
||||
this.$collapseAllButton = this.$widget.find(".collapse-all-button");
|
||||
this.$collapseAllButton.on("click", async () => {
|
||||
if (!this.noteId || !this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
// owned is important - we shouldn't remove inherited expanded labels
|
||||
for (const expandedAttr of this.note.getOwnedLabels("expanded")) {
|
||||
await attributeService.removeAttributeById(this.noteId, expandedAttr.attributeId);
|
||||
}
|
||||
|
||||
this.triggerCommand("refreshNoteList", { noteId: this.noteId });
|
||||
});
|
||||
this.$propertiesContainer = this.$widget.find(".book-properties-container");
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
@@ -117,8 +99,15 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$viewTypeSelect.val(viewType);
|
||||
|
||||
this.$expandChildrenButton.toggle(viewType === "list");
|
||||
this.$collapseAllButton.toggle(viewType === "list");
|
||||
this.$propertiesContainer.empty();
|
||||
|
||||
const bookPropertiesData = bookPropertiesConfig[viewType];
|
||||
if (bookPropertiesData) {
|
||||
for (const property of bookPropertiesData.properties) {
|
||||
this.$propertiesContainer.append(this.renderBookProperty(property));
|
||||
this.labelsToWatch.push(property.bindToLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async toggleViewType(type: string) {
|
||||
@@ -126,7 +115,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!["list", "grid", "calendar"].includes(type)) {
|
||||
if (!["list", "grid", "calendar", "table", "geoMap"].includes(type)) {
|
||||
throw new Error(t("book_properties.invalid_view_type", { type }));
|
||||
}
|
||||
|
||||
@@ -134,8 +123,82 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name === "viewType")) {
|
||||
if (loadResults.getAttributeRows().find((attr) =>
|
||||
attr.noteId === this.noteId
|
||||
&& (attr.name === "viewType" || this.labelsToWatch.includes(attr.name ?? "")))) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
renderBookProperty(property: BookProperty) {
|
||||
const $container = $("<div>");
|
||||
$container.addClass(`type-${property.type}`);
|
||||
const note = this.note;
|
||||
if (!note) {
|
||||
return $container;
|
||||
}
|
||||
switch (property.type) {
|
||||
case "checkbox":
|
||||
const $label = $("<label>").text(property.label);
|
||||
const $checkbox = $("<input>", {
|
||||
type: "checkbox",
|
||||
class: "form-check-input",
|
||||
});
|
||||
$checkbox.on("change", () => {
|
||||
if ($checkbox.prop("checked")) {
|
||||
attributes.setLabel(note.noteId, property.bindToLabel);
|
||||
} else {
|
||||
attributes.removeOwnedLabelByName(note, property.bindToLabel);
|
||||
}
|
||||
});
|
||||
$checkbox.prop("checked", note.hasOwnedLabel(property.bindToLabel));
|
||||
$label.prepend($checkbox);
|
||||
$container.append($label);
|
||||
break;
|
||||
case "button":
|
||||
const $button = $("<button>", {
|
||||
type: "button",
|
||||
class: "btn btn-sm"
|
||||
}).text(property.label);
|
||||
if (property.title) {
|
||||
$button.attr("title", property.title);
|
||||
}
|
||||
if (property.icon) {
|
||||
$button.prepend($("<span>", { class: property.icon }));
|
||||
}
|
||||
$button.on("click", () => {
|
||||
property.onClick({
|
||||
note,
|
||||
triggerCommand: this.triggerCommand.bind(this)
|
||||
});
|
||||
});
|
||||
$container.append($button);
|
||||
break;
|
||||
case "number":
|
||||
const $numberInput = $("<input>", {
|
||||
type: "number",
|
||||
class: "form-control form-control-sm",
|
||||
value: note.getLabelValue(property.bindToLabel) || "",
|
||||
width: property.width ?? 100,
|
||||
min: property.min ?? 0
|
||||
});
|
||||
$numberInput.on("change", () => {
|
||||
const value = $numberInput.val();
|
||||
if (value === "") {
|
||||
attributes.removeOwnedLabelByName(note, property.bindToLabel);
|
||||
} else {
|
||||
attributes.setLabel(note.noteId, property.bindToLabel, String(value));
|
||||
}
|
||||
});
|
||||
$container.append($("<label>")
|
||||
.text(property.label)
|
||||
.append(" ".repeat(2))
|
||||
.append($numberInput));
|
||||
break;
|
||||
}
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
105
apps/client/src/widgets/ribbon_widgets/book_properties_config.ts
Normal file
105
apps/client/src/widgets/ribbon_widgets/book_properties_config.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { t } from "i18next";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { ViewTypeOptions } from "../../services/note_list_renderer"
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||
|
||||
interface BookConfig {
|
||||
properties: BookProperty[];
|
||||
}
|
||||
|
||||
interface CheckBoxProperty {
|
||||
type: "checkbox",
|
||||
label: string;
|
||||
bindToLabel: string
|
||||
}
|
||||
|
||||
interface ButtonProperty {
|
||||
type: "button",
|
||||
label: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
onClick: (context: BookContext) => void;
|
||||
}
|
||||
|
||||
interface NumberProperty {
|
||||
type: "number",
|
||||
label: string;
|
||||
bindToLabel: string;
|
||||
width?: number;
|
||||
min?: number;
|
||||
}
|
||||
|
||||
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty;
|
||||
|
||||
interface BookContext {
|
||||
note: FNote;
|
||||
triggerCommand: NoteContextAwareWidget["triggerCommand"];
|
||||
}
|
||||
|
||||
export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
||||
grid: {
|
||||
properties: []
|
||||
},
|
||||
list: {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties.collapse"),
|
||||
title: t("book_properties.collapse_all_notes"),
|
||||
type: "button",
|
||||
icon: "bx bx-layer-minus",
|
||||
async onClick({ note, triggerCommand }) {
|
||||
const { noteId } = note;
|
||||
|
||||
// owned is important - we shouldn't remove inherited expanded labels
|
||||
for (const expandedAttr of note.getOwnedLabels("expanded")) {
|
||||
await attributes.removeAttributeById(noteId, expandedAttr.attributeId);
|
||||
}
|
||||
|
||||
triggerCommand("refreshNoteList", { noteId: noteId });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("book_properties.expand"),
|
||||
title: t("book_properties.expand_all_children"),
|
||||
type: "button",
|
||||
icon: "bx bx-move-vertical",
|
||||
async onClick({ note, triggerCommand }) {
|
||||
const { noteId } = note;
|
||||
if (!note.isLabelTruthy("expanded")) {
|
||||
await attributes.addLabel(noteId, "expanded");
|
||||
}
|
||||
|
||||
triggerCommand("refreshNoteList", { noteId });
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
calendar: {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties_config.hide-weekends"),
|
||||
type: "checkbox",
|
||||
bindToLabel: "calendar:hideWeekends"
|
||||
},
|
||||
{
|
||||
label: t("book_properties_config.display-week-numbers"),
|
||||
type: "checkbox",
|
||||
bindToLabel: "calendar:weekNumbers"
|
||||
}
|
||||
]
|
||||
},
|
||||
geoMap: {
|
||||
properties: []
|
||||
},
|
||||
table: {
|
||||
properties: [
|
||||
{
|
||||
label: "Max nesting depth:",
|
||||
type: "number",
|
||||
bindToLabel: "maxNestingDepth",
|
||||
width: 65
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -48,6 +48,18 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget {
|
||||
this.contentSized();
|
||||
}
|
||||
|
||||
isEnabled(): boolean | null | undefined {
|
||||
if (options.get("textNoteEditorType") !== "ckeditor-classic") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.note || this.note.type !== "text") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async getTitle() {
|
||||
return {
|
||||
show: await this.#shouldDisplay(),
|
||||
@@ -58,11 +70,7 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async #shouldDisplay() {
|
||||
if (options.get("textNoteEditorType") !== "ckeditor-classic") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.note || this.note.type !== "text") {
|
||||
if (!this.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,11 +69,6 @@ interface AttributeResult {
|
||||
attributeId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This widget is quite special because it's used in the desktop ribbon, but in mobile outside of ribbon.
|
||||
* This works without many issues (apart from autocomplete), but it should be kept in mind when changing things
|
||||
* and testing.
|
||||
*/
|
||||
export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $container!: JQuery<HTMLElement>;
|
||||
@@ -117,7 +112,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
// the order of attributes is important as well
|
||||
ownedAttributes.sort((a, b) => a.position - b.position);
|
||||
|
||||
if (promotedDefAttrs.length === 0) {
|
||||
if (promotedDefAttrs.length === 0 || note.getLabelValue("viewType") === "table") {
|
||||
this.toggleInt(false);
|
||||
return;
|
||||
}
|
||||
@@ -188,6 +183,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
.append($multiplicityCell);
|
||||
|
||||
if (valueAttr.type === "label") {
|
||||
$wrapper.addClass(`promoted-attribute-label-${definition.labelType}`);
|
||||
if (definition.labelType === "text") {
|
||||
$input.prop("type", "text");
|
||||
|
||||
|
||||
@@ -65,7 +65,11 @@ export default class SearchResultWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds(), true);
|
||||
const noteListRenderer = new NoteListRenderer({
|
||||
$parent: this.$content,
|
||||
parentNote: note,
|
||||
showNotePath: true
|
||||
});
|
||||
await noteListRenderer.renderList();
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,22 @@ export default class BookTypeWidget extends TypeWidget {
|
||||
}
|
||||
|
||||
async doRefresh(note: FNote) {
|
||||
this.$helpNoChildren.toggle(!this.note?.hasChildren() && this.note?.getAttributeValue("label", "viewType") !== "calendar");
|
||||
this.$helpNoChildren.toggle(this.shouldDisplayNoChildrenWarning());
|
||||
}
|
||||
|
||||
shouldDisplayNoChildrenWarning() {
|
||||
if (this.note?.hasChildren()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.note?.getAttributeValue("label", "viewType")) {
|
||||
case "calendar":
|
||||
case "table":
|
||||
case "geoMap":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
|
||||
@@ -59,7 +59,7 @@ async function handleContentUpdate(affectedNoteIds: string[]) {
|
||||
const templateNoteIds = new Set(templateCache.keys());
|
||||
const affectedTemplateNoteIds = templateNoteIds.intersection(updatedNoteIds);
|
||||
|
||||
await froca.getNotes(affectedNoteIds);
|
||||
await froca.getNotes(affectedNoteIds, true);
|
||||
|
||||
let fullReloadNeeded = false;
|
||||
for (const affectedTemplateNoteId of affectedTemplateNoteIds) {
|
||||
|
||||
@@ -16,6 +16,10 @@ const TPL = /*html*/`<div class="note-detail-doc note-detail-printable">
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.note-detail-doc-content code {
|
||||
font-variant: none;
|
||||
}
|
||||
|
||||
.note-detail-doc-content pre:not(.hljs) {
|
||||
background-color: var(--accented-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
|
||||
@@ -178,13 +178,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
});
|
||||
|
||||
if (isClassicEditor) {
|
||||
let $classicToolbarWidget;
|
||||
if (!utils.isMobile()) {
|
||||
const $parentSplit = this.$widget.parents(".note-split.type-text");
|
||||
$classicToolbarWidget = $parentSplit.find("> .ribbon-container .classic-toolbar-widget");
|
||||
} else {
|
||||
$classicToolbarWidget = $("body").find(".classic-toolbar-widget");
|
||||
}
|
||||
const $classicToolbarWidget = this.findClassicToolbar();
|
||||
|
||||
$classicToolbarWidget.empty();
|
||||
if ($classicToolbarWidget.length) {
|
||||
@@ -271,7 +265,12 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$editor.trigger("focus");
|
||||
const editor = this.watchdog.editor;
|
||||
if (editor) {
|
||||
editor.editing.view.focus();
|
||||
} else {
|
||||
this.$editor.trigger("focus");
|
||||
}
|
||||
}
|
||||
|
||||
scrollToEnd() {
|
||||
@@ -515,6 +514,22 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
}
|
||||
}
|
||||
|
||||
findClassicToolbar(): JQuery<HTMLElement> {
|
||||
if (!utils.isMobile()) {
|
||||
const $parentSplit = this.$widget.parents(".note-split.type-text");
|
||||
|
||||
if ($parentSplit.length) {
|
||||
// The editor is in a normal tab.
|
||||
return $parentSplit.find("> .ribbon-container .classic-toolbar-widget");
|
||||
} else {
|
||||
// The editor is in a popup.
|
||||
return this.$widget.closest(".modal-body").find(".classic-toolbar-widget");
|
||||
}
|
||||
} else {
|
||||
return $("body").find(".classic-toolbar-widget");
|
||||
}
|
||||
}
|
||||
|
||||
buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) {
|
||||
const { TouchBar, buildIcon } = data;
|
||||
const { TouchBarSegmentedControl, TouchBarGroup, TouchBarButton } = TouchBar;
|
||||
|
||||
@@ -3,7 +3,6 @@ import TypeWidget from "./type_widget.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import searchService from "../../services/search.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="note-detail-empty note-detail-printable">
|
||||
|
||||
@@ -22,7 +22,8 @@ const TPL = /*html*/`
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.note-split.full-content-width .note-detail-file[data-preview-type="video"] {
|
||||
.note-detail.full-height .note-detail-file[data-preview-type="pdf"],
|
||||
.note-detail.full-height .note-detail-file[data-preview-type="video"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
import { GPX, Marker, type LatLng, type LeafletMouseEvent } from "leaflet";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import GeoMapWidget, { type InitCallback, type Leaflet } from "../geo_map.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import server from "../../services/server.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import dialogService from "../../services/dialog.js";
|
||||
import type { CommandListenerData, EventData } from "../../components/app_context.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import attributes from "../../services/attributes.js";
|
||||
import openContextMenu from "./geo_map_context_menu.js";
|
||||
import link from "../../services/link.js";
|
||||
import note_tooltip from "../../services/note_tooltip.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
|
||||
import markerIcon from "leaflet/dist/images/marker-icon.png";
|
||||
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
|
||||
import { hasTouchBar } from "../../services/utils.js";
|
||||
|
||||
const TPL = /*html*/`\
|
||||
<div class="note-detail-geo-map note-detail-printable">
|
||||
<style>
|
||||
.leaflet-pane {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.geo-map-container.placing-note {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.geo-map-container .marker-pin {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .icon-shadow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .bx {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 2px;
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .title-label {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.75rem;
|
||||
height: 1rem;
|
||||
color: black;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
|
||||
white-space: no-wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</div>`;
|
||||
|
||||
const LOCATION_ATTRIBUTE = "geolocation";
|
||||
const CHILD_NOTE_ICON = "bx bx-pin";
|
||||
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
|
||||
const DEFAULT_ZOOM = 2;
|
||||
|
||||
interface MapData {
|
||||
view?: {
|
||||
center?: LatLng | [number, number];
|
||||
zoom?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Deduplicate
|
||||
interface CreateChildResponse {
|
||||
note: {
|
||||
noteId: string;
|
||||
};
|
||||
}
|
||||
|
||||
enum State {
|
||||
Normal,
|
||||
NewNote
|
||||
}
|
||||
|
||||
export default class GeoMapTypeWidget extends TypeWidget {
|
||||
|
||||
private geoMapWidget: GeoMapWidget;
|
||||
private _state: State;
|
||||
private L!: Leaflet;
|
||||
private currentMarkerData: Record<string, Marker>;
|
||||
private currentTrackData: Record<string, GPX>;
|
||||
private gpxLoaded?: boolean;
|
||||
private ignoreNextZoomEvent?: boolean;
|
||||
|
||||
static getType() {
|
||||
return "geoMap";
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.geoMapWidget = new GeoMapWidget("type", (L: Leaflet) => this.#onMapInitialized(L));
|
||||
this.currentMarkerData = {};
|
||||
this.currentTrackData = {};
|
||||
this._state = State.Normal;
|
||||
|
||||
this.child(this.geoMapWidget);
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
this.$widget = $(TPL);
|
||||
this.$widget.append(this.geoMapWidget.render());
|
||||
}
|
||||
|
||||
async #onMapInitialized(L: Leaflet) {
|
||||
this.L = L;
|
||||
const map = this.geoMapWidget.map;
|
||||
if (!map) {
|
||||
throw new Error(t("geo-map.unable-to-load-map"));
|
||||
}
|
||||
|
||||
this.#restoreViewportAndZoom();
|
||||
|
||||
// Restore markers.
|
||||
await this.#reloadMarkers();
|
||||
|
||||
// This fixes an issue with the map appearing cut off at the beginning, due to the container not being properly attached
|
||||
setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
}, 100);
|
||||
|
||||
const updateFn = () => this.spacedUpdate.scheduleUpdate();
|
||||
map.on("moveend", updateFn);
|
||||
map.on("zoomend", updateFn);
|
||||
map.on("click", (e) => this.#onMapClicked(e));
|
||||
|
||||
if (hasTouchBar) {
|
||||
map.on("zoom", () => {
|
||||
if (!this.ignoreNextZoomEvent) {
|
||||
this.triggerCommand("refreshTouchBar");
|
||||
}
|
||||
|
||||
this.ignoreNextZoomEvent = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async #restoreViewportAndZoom() {
|
||||
const map = this.geoMapWidget.map;
|
||||
if (!map || !this.note) {
|
||||
return;
|
||||
}
|
||||
const blob = await this.note.getBlob();
|
||||
|
||||
let parsedContent: MapData = {};
|
||||
if (blob && blob.content) {
|
||||
parsedContent = JSON.parse(blob.content);
|
||||
}
|
||||
|
||||
// Restore viewport position & zoom
|
||||
const center = parsedContent.view?.center ?? DEFAULT_COORDINATES;
|
||||
const zoom = parsedContent.view?.zoom ?? DEFAULT_ZOOM;
|
||||
map.setView(center, zoom);
|
||||
}
|
||||
|
||||
async #reloadMarkers() {
|
||||
if (!this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete all existing markers
|
||||
for (const marker of Object.values(this.currentMarkerData)) {
|
||||
marker.remove();
|
||||
}
|
||||
|
||||
// Delete all existing tracks
|
||||
for (const track of Object.values(this.currentTrackData)) {
|
||||
track.remove();
|
||||
}
|
||||
|
||||
// Add the new markers.
|
||||
this.currentMarkerData = {};
|
||||
const childNotes = await this.note.getChildNotes();
|
||||
for (const childNote of childNotes) {
|
||||
if (childNote.mime === "application/gpx+xml") {
|
||||
this.#processNoteWithGpxTrack(childNote);
|
||||
continue;
|
||||
}
|
||||
|
||||
const latLng = childNote.getAttributeValue("label", LOCATION_ATTRIBUTE);
|
||||
if (latLng) {
|
||||
this.#processNoteWithMarker(childNote, latLng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #processNoteWithGpxTrack(note: FNote) {
|
||||
if (!this.L || !this.geoMapWidget.map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.gpxLoaded) {
|
||||
await import("leaflet-gpx");
|
||||
this.gpxLoaded = true;
|
||||
}
|
||||
|
||||
const xmlResponse = await server.get<string | Uint8Array>(`notes/${note.noteId}/open`, undefined, true);
|
||||
let stringResponse: string;
|
||||
if (xmlResponse instanceof Uint8Array) {
|
||||
stringResponse = new TextDecoder().decode(xmlResponse);
|
||||
} else {
|
||||
stringResponse = xmlResponse;
|
||||
}
|
||||
|
||||
const track = new this.L.GPX(stringResponse, {
|
||||
markers: {
|
||||
startIcon: this.#buildIcon(note.getIcon(), note.getColorClass(), note.title),
|
||||
endIcon: this.#buildIcon("bxs-flag-checkered"),
|
||||
wptIcons: {
|
||||
"": this.#buildIcon("bx bx-pin")
|
||||
}
|
||||
},
|
||||
polyline_options: {
|
||||
color: note.getLabelValue("color") ?? "blue"
|
||||
}
|
||||
});
|
||||
track.addTo(this.geoMapWidget.map);
|
||||
this.currentTrackData[note.noteId] = track;
|
||||
}
|
||||
|
||||
#processNoteWithMarker(note: FNote, latLng: string) {
|
||||
const map = this.geoMapWidget.map;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lat, lng] = latLng.split(",", 2).map((el) => parseFloat(el));
|
||||
const L = this.L;
|
||||
const icon = this.#buildIcon(note.getIcon(), note.getColorClass(), note.title);
|
||||
|
||||
const marker = L.marker(L.latLng(lat, lng), {
|
||||
icon,
|
||||
draggable: true,
|
||||
autoPan: true,
|
||||
autoPanSpeed: 5
|
||||
})
|
||||
.addTo(map)
|
||||
.on("moveend", (e) => {
|
||||
this.moveMarker(note.noteId, (e.target as Marker).getLatLng());
|
||||
});
|
||||
marker.on("mousedown", ({ originalEvent }) => {
|
||||
// Middle click to open in new tab
|
||||
if (originalEvent.button === 1) {
|
||||
const hoistedNoteId = this.hoistedNoteId;
|
||||
//@ts-ignore, fix once tab manager is ported.
|
||||
appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
marker.on("contextmenu", (e) => {
|
||||
openContextMenu(note.noteId, e.originalEvent);
|
||||
});
|
||||
|
||||
const el = marker.getElement();
|
||||
if (el) {
|
||||
const $el = $(el);
|
||||
$el.attr("data-href", `#${note.noteId}`);
|
||||
note_tooltip.setupElementTooltip($($el));
|
||||
}
|
||||
|
||||
this.currentMarkerData[note.noteId] = marker;
|
||||
}
|
||||
|
||||
#buildIcon(bxIconClass: string, colorClass?: string, title?: string) {
|
||||
return this.L.divIcon({
|
||||
html: /*html*/`\
|
||||
<img class="icon" src="${markerIcon}" />
|
||||
<img class="icon-shadow" src="${markerIconShadow}" />
|
||||
<span class="bx ${bxIconClass} ${colorClass ?? ""}"></span>
|
||||
<span class="title-label">${title ?? ""}</span>`,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41]
|
||||
});
|
||||
}
|
||||
|
||||
#changeState(newState: State) {
|
||||
this._state = newState;
|
||||
this.geoMapWidget.$container.toggleClass("placing-note", newState === State.NewNote);
|
||||
if (hasTouchBar) {
|
||||
this.triggerCommand("refreshTouchBar");
|
||||
}
|
||||
}
|
||||
|
||||
async #onMapClicked(e: LeafletMouseEvent) {
|
||||
if (this._state !== State.NewNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.closePersistent("geo-new-note");
|
||||
const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
|
||||
|
||||
if (title?.trim()) {
|
||||
const { note } = await server.post<CreateChildResponse>(`notes/${this.noteId}/children?target=into`, {
|
||||
title,
|
||||
content: "",
|
||||
type: "text"
|
||||
});
|
||||
attributes.setLabel(note.noteId, "iconClass", CHILD_NOTE_ICON);
|
||||
this.moveMarker(note.noteId, e.latlng);
|
||||
}
|
||||
|
||||
this.#changeState(State.Normal);
|
||||
}
|
||||
|
||||
async moveMarker(noteId: string, latLng: LatLng | null) {
|
||||
const value = latLng ? [latLng.lat, latLng.lng].join(",") : "";
|
||||
await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value);
|
||||
}
|
||||
|
||||
getData(): any {
|
||||
const map = this.geoMapWidget.map;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: MapData = {
|
||||
view: {
|
||||
center: map.getBounds().getCenter(),
|
||||
zoom: map.getZoom()
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
content: JSON.stringify(data)
|
||||
};
|
||||
}
|
||||
|
||||
async geoMapCreateChildNoteEvent({ ntxId }: EventData<"geoMapCreateChildNote">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.showPersistent({
|
||||
icon: "plus",
|
||||
id: "geo-new-note",
|
||||
title: "New note",
|
||||
message: t("geo-map.create-child-note-instruction")
|
||||
});
|
||||
|
||||
this.#changeState(State.NewNote);
|
||||
|
||||
const globalKeyListener: (this: Window, ev: KeyboardEvent) => any = (e) => {
|
||||
if (e.key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#changeState(State.Normal);
|
||||
|
||||
window.removeEventListener("keydown", globalKeyListener);
|
||||
toastService.closePersistent("geo-new-note");
|
||||
};
|
||||
window.addEventListener("keydown", globalKeyListener);
|
||||
}
|
||||
|
||||
async doRefresh(note: FNote) {
|
||||
await this.geoMapWidget.refresh();
|
||||
this.#restoreViewportAndZoom();
|
||||
await this.#reloadMarkers();
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// If any of the children branches are altered.
|
||||
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.noteId)) {
|
||||
this.#reloadMarkers();
|
||||
return;
|
||||
}
|
||||
|
||||
// If any of note has its location attribute changed.
|
||||
// TODO: Should probably filter by parent here as well.
|
||||
const attributeRows = loadResults.getAttributeRows();
|
||||
if (attributeRows.find((at) => [LOCATION_ATTRIBUTE, "color"].includes(at.name ?? ""))) {
|
||||
this.#reloadMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
openGeoLocationEvent({ noteId, event }: EventData<"openGeoLocation">) {
|
||||
const marker = this.currentMarkerData[noteId];
|
||||
if (!marker) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latLng = this.currentMarkerData[noteId].getLatLng();
|
||||
const url = `geo:${latLng.lat},${latLng.lng}`;
|
||||
link.goToLinkExt(event, url);
|
||||
}
|
||||
|
||||
deleteFromMapEvent({ noteId }: EventData<"deleteFromMap">) {
|
||||
this.moveMarker(noteId, null);
|
||||
}
|
||||
|
||||
buildTouchBarCommand({ TouchBar }: CommandListenerData<"buildTouchBar">) {
|
||||
const map = this.geoMapWidget.map;
|
||||
const that = this;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [
|
||||
new TouchBar.TouchBarSlider({
|
||||
label: "Zoom",
|
||||
value: map.getZoom(),
|
||||
minValue: map.getMinZoom(),
|
||||
maxValue: map.getMaxZoom(),
|
||||
change(newValue) {
|
||||
that.ignoreNextZoomEvent = true;
|
||||
map.setZoom(newValue);
|
||||
},
|
||||
}),
|
||||
new TouchBar.TouchBarButton({
|
||||
label: "New geo note",
|
||||
click: () => this.triggerCommand("geoMapCreateChildNote", { ntxId: this.ntxId }),
|
||||
enabled: (this._state === State.Normal)
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import appContext from "../../components/app_context.js";
|
||||
import type { ContextMenuEvent } from "../../menus/context_menu.js";
|
||||
import contextMenu from "../../menus/context_menu.js";
|
||||
import linkContextMenu from "../../menus/link_context_menu.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
|
||||
export default function openContextMenu(noteId: string, e: ContextMenuEvent) {
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
...linkContextMenu.getItems(),
|
||||
{ title: t("geo-map-context.open-location"), command: "openGeoLocation", uiIcon: "bx bx-map-alt" },
|
||||
{ title: "----" },
|
||||
{ title: t("geo-map-context.remove-from-map"), command: "deleteFromMap", uiIcon: "bx bx-trash" }
|
||||
],
|
||||
selectMenuItemHandler: ({ command }, e) => {
|
||||
if (command === "deleteFromMap") {
|
||||
appContext.triggerCommand(command, { noteId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "openGeoLocation") {
|
||||
appContext.triggerCommand(command, { noteId, event: e });
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the events to the link context menu
|
||||
linkContextMenu.handleLinkContextMenuItem(command, noteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import type { MindElixirCtor, MindElixirInstance } from "mind-elixir";
|
||||
import type { MindElixirInstance } from "mind-elixir";
|
||||
import nodeMenu from "@mind-elixir/node-menu";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
// allow node-menu plugin css to be bundled by webpack
|
||||
import "mind-elixir/style";
|
||||
import "@mind-elixir/node-menu/dist/style.css";
|
||||
|
||||
const NEW_TOPIC_NAME = "";
|
||||
|
||||
@@ -71,6 +71,17 @@ export default abstract class TypeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
activeNoteChangedEvent() {
|
||||
if (!this.isActiveNoteContext()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore focus to the editor when switching tabs, but only if the note tree is not already focused.
|
||||
if (!document.activeElement?.classList.contains("fancytree-title")) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
|
||||
@@ -20,7 +20,7 @@ const TPL = /*html*/`
|
||||
|
||||
function buildElement() {
|
||||
if (!utils.isElectron()) {
|
||||
return `<iframe class="note-detail-web-view-content" sandbox="allow-same-origin allow-scripts"></iframe>`;
|
||||
return `<iframe class="note-detail-web-view-content" sandbox="allow-same-origin allow-scripts allow-popups"></iframe>`;
|
||||
} else {
|
||||
return `<webview class="note-detail-web-view-content"></webview>`;
|
||||
}
|
||||
|
||||
@@ -109,32 +109,24 @@ const CALENDAR_VIEWS = [
|
||||
"listMonth"
|
||||
]
|
||||
|
||||
export default class CalendarView extends ViewMode {
|
||||
export default class CalendarView extends ViewMode<{}> {
|
||||
|
||||
private $root: JQuery<HTMLElement>;
|
||||
private $calendarContainer: JQuery<HTMLElement>;
|
||||
private noteIds: string[];
|
||||
private parentNote: FNote;
|
||||
private calendar?: Calendar;
|
||||
private isCalendarRoot: boolean;
|
||||
private lastView?: string;
|
||||
private debouncedSaveView?: DebouncedFunction<() => void>;
|
||||
|
||||
constructor(args: ViewModeArgs) {
|
||||
super(args);
|
||||
super(args, "calendar");
|
||||
|
||||
this.$root = $(TPL);
|
||||
this.$calendarContainer = this.$root.find(".calendar-container");
|
||||
this.noteIds = args.noteIds;
|
||||
this.parentNote = args.parentNote;
|
||||
this.isCalendarRoot = false;
|
||||
args.$parent.append(this.$root);
|
||||
}
|
||||
|
||||
get isFullHeight(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async renderList(): Promise<JQuery<HTMLElement> | undefined> {
|
||||
this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot");
|
||||
const isEditable = !this.isCalendarRoot;
|
||||
@@ -227,6 +219,7 @@ export default class CalendarView extends ViewMode {
|
||||
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
||||
}
|
||||
},
|
||||
// Called upon when clicking the day number in the calendar, opens or creates the day note but only if in a calendar root.
|
||||
dateClick: async (e) => {
|
||||
if (!this.isCalendarRoot) {
|
||||
return;
|
||||
@@ -234,7 +227,8 @@ export default class CalendarView extends ViewMode {
|
||||
|
||||
const note = await date_notes.getDayNote(e.dateStr);
|
||||
if (note) {
|
||||
appContext.tabManager.getActiveContext()?.setNote(note.noteId);
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId });
|
||||
appContext.triggerCommand("refreshNoteList", { noteId: this.parentNote.noteId });
|
||||
}
|
||||
},
|
||||
datesSet: (e) => this.#onDatesSet(e),
|
||||
@@ -396,7 +390,7 @@ export default class CalendarView extends ViewMode {
|
||||
}
|
||||
}
|
||||
|
||||
onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// Refresh note IDs if they got changed.
|
||||
if (loadResults.getBranchRows().some((branch) => branch.parentNoteId === this.parentNote.noteId)) {
|
||||
this.noteIds = this.parentNote.getChildNoteIds();
|
||||
@@ -407,9 +401,14 @@ export default class CalendarView extends ViewMode {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Refresh on note title change.
|
||||
if (loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId))) {
|
||||
this.calendar?.refetchEvents();
|
||||
}
|
||||
|
||||
// Refresh dataset on subnote change.
|
||||
if (this.calendar && loadResults.getAttributeRows().some((a) => this.noteIds.includes(a.noteId ?? ""))) {
|
||||
this.calendar.refetchEvents();
|
||||
if (loadResults.getAttributeRows().some((a) => this.noteIds.includes(a.noteId ?? ""))) {
|
||||
this.calendar?.refetchEvents();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +437,7 @@ export default class CalendarView extends ViewMode {
|
||||
events.push(await CalendarView.buildEvent(dateNote, { startDate }));
|
||||
|
||||
if (dateNote.hasChildren()) {
|
||||
const childNoteIds = dateNote.getChildNoteIds();
|
||||
const childNoteIds = await dateNote.getSubtreeNoteIds();
|
||||
for (const childNoteId of childNoteIds) {
|
||||
childNoteToDateMapping[childNoteId] = startDate;
|
||||
}
|
||||
@@ -464,13 +463,6 @@ export default class CalendarView extends ViewMode {
|
||||
for (const note of notes) {
|
||||
const startDate = CalendarView.#getCustomisableLabel(note, "startDate", "calendar:startDate");
|
||||
|
||||
if (note.hasChildren()) {
|
||||
const childrenEventData = await this.buildEvents(note.getChildNoteIds());
|
||||
if (childrenEventData.length > 0) {
|
||||
events.push(childrenEventData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!startDate) {
|
||||
continue;
|
||||
}
|
||||
@@ -535,7 +527,7 @@ export default class CalendarView extends ViewMode {
|
||||
const eventData: EventInput = {
|
||||
title: title,
|
||||
start: startDate,
|
||||
url: `#${note.noteId}`,
|
||||
url: `#${note.noteId}?popup`,
|
||||
noteId: note.noteId,
|
||||
color: color ?? undefined,
|
||||
iconClass: note.getLabelValue("iconClass"),
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { LatLng, LeafletMouseEvent } from "leaflet";
|
||||
import appContext, { type CommandMappings } from "../../../components/app_context.js";
|
||||
import contextMenu, { type MenuItem } from "../../../menus/context_menu.js";
|
||||
import linkContextMenu from "../../../menus/link_context_menu.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import { createNewNote } from "./editing.js";
|
||||
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||
import link from "../../../services/link.js";
|
||||
|
||||
export default function openContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) {
|
||||
let items: MenuItem<keyof CommandMappings>[] = [
|
||||
...buildGeoLocationItem(e),
|
||||
{ title: "----" },
|
||||
...linkContextMenu.getItems(),
|
||||
];
|
||||
|
||||
if (isEditable) {
|
||||
items = [
|
||||
...items,
|
||||
{ title: "----" },
|
||||
{ title: t("geo-map-context.remove-from-map"), command: "deleteFromMap", uiIcon: "bx bx-trash" }
|
||||
];
|
||||
}
|
||||
|
||||
contextMenu.show({
|
||||
x: e.originalEvent.pageX,
|
||||
y: e.originalEvent.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: ({ command }, e) => {
|
||||
if (command === "deleteFromMap") {
|
||||
appContext.triggerCommand(command, { noteId });
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the events to the link context menu
|
||||
linkContextMenu.handleLinkContextMenuItem(command, noteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) {
|
||||
let items: MenuItem<keyof CommandMappings>[] = [
|
||||
...buildGeoLocationItem(e)
|
||||
];
|
||||
|
||||
if (isEditable) {
|
||||
items = [
|
||||
...items,
|
||||
{ title: "----" },
|
||||
{
|
||||
title: t("geo-map-context.add-note"),
|
||||
handler: () => createNewNote(noteId, e),
|
||||
uiIcon: "bx bx-plus"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
contextMenu.show({
|
||||
x: e.originalEvent.pageX,
|
||||
y: e.originalEvent.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: () => {
|
||||
// Nothing to do, as the commands handle themselves.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildGeoLocationItem(e: LeafletMouseEvent) {
|
||||
function formatGeoLocation(latlng: LatLng, precision: number = 6) {
|
||||
return `${latlng.lat.toFixed(precision)}, ${latlng.lng.toFixed(precision)}`;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: formatGeoLocation(e.latlng),
|
||||
uiIcon: "bx bx-current-location",
|
||||
handler: () => copyTextWithToast(formatGeoLocation(e.latlng, 15))
|
||||
},
|
||||
{
|
||||
title: t("geo-map-context.open-location"),
|
||||
uiIcon: "bx bx-map-alt",
|
||||
handler: () => link.goToLinkExt(null, `geo:${e.latlng.lat},${e.latlng.lng}`)
|
||||
}
|
||||
];
|
||||
}
|
||||
80
apps/client/src/widgets/view_widgets/geo_view/editing.ts
Normal file
80
apps/client/src/widgets/view_widgets/geo_view/editing.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { LatLng, LeafletMouseEvent } from "leaflet";
|
||||
import attributes from "../../../services/attributes";
|
||||
import { LOCATION_ATTRIBUTE } from "./index.js";
|
||||
import dialog from "../../../services/dialog";
|
||||
import server from "../../../services/server";
|
||||
import { t } from "../../../services/i18n";
|
||||
import type { Map } from "leaflet";
|
||||
import type { DragData } from "../../note_tree.js";
|
||||
import froca from "../../../services/froca.js";
|
||||
import branches from "../../../services/branches.js";
|
||||
|
||||
const CHILD_NOTE_ICON = "bx bx-pin";
|
||||
|
||||
// TODO: Deduplicate
|
||||
interface CreateChildResponse {
|
||||
note: {
|
||||
noteId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function moveMarker(noteId: string, latLng: LatLng | null) {
|
||||
const value = latLng ? [latLng.lat, latLng.lng].join(",") : "";
|
||||
await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value);
|
||||
}
|
||||
|
||||
export async function createNewNote(noteId: string, e: LeafletMouseEvent) {
|
||||
const title = await dialog.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
|
||||
|
||||
if (title?.trim()) {
|
||||
const { note } = await server.post<CreateChildResponse>(`notes/${noteId}/children?target=into`, {
|
||||
title,
|
||||
content: "",
|
||||
type: "text"
|
||||
});
|
||||
attributes.setLabel(note.noteId, "iconClass", CHILD_NOTE_ICON);
|
||||
moveMarker(note.noteId, e.latlng);
|
||||
}
|
||||
}
|
||||
|
||||
export function setupDragging($container: JQuery<HTMLElement>, map: Map, mapNoteId: string) {
|
||||
$container.on("dragover", (e) => {
|
||||
// Allow drag.
|
||||
e.preventDefault();
|
||||
});
|
||||
$container.on("drop", async (e) => {
|
||||
if (!e.originalEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = e.originalEvent.dataTransfer?.getData('text');
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedData = JSON.parse(data) as DragData[];
|
||||
if (!parsedData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { noteId } = parsedData[0];
|
||||
|
||||
const offset = $container.offset();
|
||||
const x = e.originalEvent.clientX - (offset?.left ?? 0);
|
||||
const y = e.originalEvent.clientY - (offset?.top ?? 0);
|
||||
const latlng = map.containerPointToLatLng([ x, y ]);
|
||||
|
||||
const note = await froca.getNote(noteId, true);
|
||||
const parents = note?.getParentNoteIds();
|
||||
if (parents?.includes(mapNoteId)) {
|
||||
await moveMarker(noteId, latlng);
|
||||
} else {
|
||||
await branches.cloneNoteToParentNote(noteId, mapNoteId);
|
||||
await moveMarker(noteId, latlng);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
332
apps/client/src/widgets/view_widgets/geo_view/index.ts
Normal file
332
apps/client/src/widgets/view_widgets/geo_view/index.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import ViewMode, { ViewModeArgs } from "../view_mode.js";
|
||||
import L from "leaflet";
|
||||
import type { GPX, LatLng, LeafletMouseEvent, Map, Marker } from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import processNoteWithMarker, { processNoteWithGpxTrack } from "./markers.js";
|
||||
import { hasTouchBar } from "../../../services/utils.js";
|
||||
import toast from "../../../services/toast.js";
|
||||
import { CommandListenerData, EventData } from "../../../components/app_context.js";
|
||||
import { createNewNote, moveMarker, setupDragging } from "./editing.js";
|
||||
import { openMapContextMenu } from "./context_menu.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="geo-view">
|
||||
<style>
|
||||
.geo-view {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.geo-map-container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leaflet-pane {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
z-index: 997;
|
||||
}
|
||||
|
||||
.geo-map-container.placing-note {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.geo-map-container .marker-pin {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .icon-shadow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .bx {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 2px;
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.geo-map-container .leaflet-div-icon .title-label {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.75rem;
|
||||
height: 1rem;
|
||||
color: black;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
|
||||
white-space: no-wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="geo-map-container"></div>
|
||||
</div>`;
|
||||
|
||||
interface MapData {
|
||||
view?: {
|
||||
center?: LatLng | [number, number];
|
||||
zoom?: number;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
|
||||
const DEFAULT_ZOOM = 2;
|
||||
export const LOCATION_ATTRIBUTE = "geolocation";
|
||||
|
||||
enum State {
|
||||
Normal,
|
||||
NewNote
|
||||
}
|
||||
|
||||
export default class GeoView extends ViewMode<MapData> {
|
||||
|
||||
private $root: JQuery<HTMLElement>;
|
||||
private $container!: JQuery<HTMLElement>;
|
||||
private map?: Map;
|
||||
private spacedUpdate: SpacedUpdate;
|
||||
private _state: State;
|
||||
private ignoreNextZoomEvent?: boolean;
|
||||
|
||||
private currentMarkerData: Record<string, Marker>;
|
||||
private currentTrackData: Record<string, GPX>;
|
||||
|
||||
constructor(args: ViewModeArgs) {
|
||||
super(args, "geoMap");
|
||||
this.$root = $(TPL);
|
||||
this.$container = this.$root.find(".geo-map-container");
|
||||
this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000);
|
||||
|
||||
this.currentMarkerData = {};
|
||||
this.currentTrackData = {};
|
||||
this._state = State.Normal;
|
||||
|
||||
args.$parent.append(this.$root);
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
this.renderMap();
|
||||
return this.$root;
|
||||
}
|
||||
|
||||
async renderMap() {
|
||||
const map = L.map(this.$container[0], {
|
||||
worldCopyJump: true
|
||||
});
|
||||
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
detectRetina: true
|
||||
}).addTo(map);
|
||||
|
||||
this.map = map;
|
||||
|
||||
this.#onMapInitialized();
|
||||
}
|
||||
|
||||
async #onMapInitialized() {
|
||||
const map = this.map;
|
||||
if (!map) {
|
||||
throw new Error(t("geo-map.unable-to-load-map"));
|
||||
}
|
||||
|
||||
this.#restoreViewportAndZoom();
|
||||
|
||||
const isEditable = !this.isReadOnly;
|
||||
const updateFn = () => this.spacedUpdate.scheduleUpdate();
|
||||
map.on("moveend", updateFn);
|
||||
map.on("zoomend", updateFn);
|
||||
map.on("click", (e) => this.#onMapClicked(e))
|
||||
map.on("contextmenu", (e) => openMapContextMenu(this.parentNote.noteId, e, isEditable));
|
||||
|
||||
if (isEditable) {
|
||||
setupDragging(this.$container, map, this.parentNote.noteId);
|
||||
}
|
||||
|
||||
this.#reloadMarkers();
|
||||
|
||||
if (hasTouchBar) {
|
||||
map.on("zoom", () => {
|
||||
if (!this.ignoreNextZoomEvent) {
|
||||
this.triggerCommand("refreshTouchBar");
|
||||
}
|
||||
|
||||
this.ignoreNextZoomEvent = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async #restoreViewportAndZoom() {
|
||||
const map = this.map;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedContent = await this.viewStorage.restore();
|
||||
|
||||
// Restore viewport position & zoom
|
||||
const center = parsedContent?.view?.center ?? DEFAULT_COORDINATES;
|
||||
const zoom = parsedContent?.view?.zoom ?? DEFAULT_ZOOM;
|
||||
map.setView(center, zoom);
|
||||
}
|
||||
|
||||
private onSave() {
|
||||
const map = this.map;
|
||||
let data: MapData = {};
|
||||
if (map) {
|
||||
data = {
|
||||
view: {
|
||||
center: map.getBounds().getCenter(),
|
||||
zoom: map.getZoom()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.viewStorage.store(data);
|
||||
}
|
||||
|
||||
async #reloadMarkers() {
|
||||
if (!this.map) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete all existing markers
|
||||
for (const marker of Object.values(this.currentMarkerData)) {
|
||||
marker.remove();
|
||||
}
|
||||
|
||||
// Delete all existing tracks
|
||||
for (const track of Object.values(this.currentTrackData)) {
|
||||
track.remove();
|
||||
}
|
||||
|
||||
// Add the new markers.
|
||||
this.currentMarkerData = {};
|
||||
const notes = await this.parentNote.getSubtreeNotes();
|
||||
const draggable = !this.isReadOnly;
|
||||
for (const childNote of notes) {
|
||||
if (childNote.mime === "application/gpx+xml") {
|
||||
const track = await processNoteWithGpxTrack(this.map, childNote);
|
||||
this.currentTrackData[childNote.noteId] = track;
|
||||
continue;
|
||||
}
|
||||
|
||||
const latLng = childNote.getAttributeValue("label", LOCATION_ATTRIBUTE);
|
||||
if (latLng) {
|
||||
const marker = processNoteWithMarker(this.map, childNote, latLng, draggable);
|
||||
this.currentMarkerData[childNote.noteId] = marker;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#changeState(newState: State) {
|
||||
this._state = newState;
|
||||
this.$container.toggleClass("placing-note", newState === State.NewNote);
|
||||
if (hasTouchBar) {
|
||||
this.triggerCommand("refreshTouchBar");
|
||||
}
|
||||
}
|
||||
|
||||
async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// If any of the children branches are altered.
|
||||
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.parentNote.noteId)) {
|
||||
this.#reloadMarkers();
|
||||
return;
|
||||
}
|
||||
|
||||
// If any of note has its location attribute changed.
|
||||
// TODO: Should probably filter by parent here as well.
|
||||
const attributeRows = loadResults.getAttributeRows();
|
||||
if (attributeRows.find((at) => [LOCATION_ATTRIBUTE, "color"].includes(at.name ?? ""))) {
|
||||
this.#reloadMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
async geoMapCreateChildNoteEvent({ ntxId }: EventData<"geoMapCreateChildNote">) {
|
||||
toast.showPersistent({
|
||||
icon: "plus",
|
||||
id: "geo-new-note",
|
||||
title: "New note",
|
||||
message: t("geo-map.create-child-note-instruction")
|
||||
});
|
||||
|
||||
this.#changeState(State.NewNote);
|
||||
|
||||
const globalKeyListener: (this: Window, ev: KeyboardEvent) => any = (e) => {
|
||||
if (e.key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#changeState(State.Normal);
|
||||
|
||||
window.removeEventListener("keydown", globalKeyListener);
|
||||
toast.closePersistent("geo-new-note");
|
||||
};
|
||||
window.addEventListener("keydown", globalKeyListener);
|
||||
}
|
||||
|
||||
async #onMapClicked(e: LeafletMouseEvent) {
|
||||
if (this._state !== State.NewNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.closePersistent("geo-new-note");
|
||||
await createNewNote(this.parentNote.noteId, e);
|
||||
this.#changeState(State.Normal);
|
||||
}
|
||||
|
||||
deleteFromMapEvent({ noteId }: EventData<"deleteFromMap">) {
|
||||
moveMarker(noteId, null);
|
||||
}
|
||||
|
||||
buildTouchBarCommand({ TouchBar }: CommandListenerData<"buildTouchBar">) {
|
||||
const map = this.map;
|
||||
const that = this;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [
|
||||
new TouchBar.TouchBarSlider({
|
||||
label: "Zoom",
|
||||
value: map.getZoom(),
|
||||
minValue: map.getMinZoom(),
|
||||
maxValue: map.getMaxZoom(),
|
||||
change(newValue) {
|
||||
that.ignoreNextZoomEvent = true;
|
||||
map.setZoom(newValue);
|
||||
},
|
||||
}),
|
||||
new TouchBar.TouchBarButton({
|
||||
label: "New geo note",
|
||||
click: () => this.triggerCommand("geoMapCreateChildNote"),
|
||||
enabled: (this._state === State.Normal)
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
99
apps/client/src/widgets/view_widgets/geo_view/markers.ts
Normal file
99
apps/client/src/widgets/view_widgets/geo_view/markers.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import markerIcon from "leaflet/dist/images/marker-icon.png";
|
||||
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
|
||||
import { marker, latLng, divIcon, Map, type Marker } from "leaflet";
|
||||
import type FNote from "../../../entities/fnote.js";
|
||||
import openContextMenu from "./context_menu.js";
|
||||
import server from "../../../services/server.js";
|
||||
import { moveMarker } from "./editing.js";
|
||||
import appContext from "../../../components/app_context.js";
|
||||
import L from "leaflet";
|
||||
|
||||
let gpxLoaded = false;
|
||||
|
||||
export default function processNoteWithMarker(map: Map, note: FNote, location: string, isEditable: boolean) {
|
||||
const [lat, lng] = location.split(",", 2).map((el) => parseFloat(el));
|
||||
const icon = buildIcon(note.getIcon(), note.getColorClass(), note.title, note.noteId);
|
||||
|
||||
const newMarker = marker(latLng(lat, lng), {
|
||||
icon,
|
||||
draggable: isEditable,
|
||||
autoPan: true,
|
||||
autoPanSpeed: 5
|
||||
}).addTo(map);
|
||||
|
||||
if (isEditable) {
|
||||
newMarker.on("moveend", (e) => {
|
||||
moveMarker(note.noteId, (e.target as Marker).getLatLng());
|
||||
});
|
||||
}
|
||||
|
||||
newMarker.on("mousedown", ({ originalEvent }) => {
|
||||
// Middle click to open in new tab
|
||||
if (originalEvent.button === 1) {
|
||||
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
//@ts-ignore, fix once tab manager is ported.
|
||||
appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
newMarker.on("contextmenu", (e) => {
|
||||
openContextMenu(note.noteId, e, isEditable);
|
||||
});
|
||||
|
||||
if (!isEditable) {
|
||||
newMarker.on("click", (e) => {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId });
|
||||
});
|
||||
}
|
||||
|
||||
return newMarker;
|
||||
}
|
||||
|
||||
export async function processNoteWithGpxTrack(map: Map, note: FNote) {
|
||||
if (!gpxLoaded) {
|
||||
const GPX = await import("leaflet-gpx");
|
||||
gpxLoaded = true;
|
||||
}
|
||||
|
||||
const xmlResponse = await server.get<string | Uint8Array>(`notes/${note.noteId}/open`, undefined, true);
|
||||
let stringResponse: string;
|
||||
if (xmlResponse instanceof Uint8Array) {
|
||||
stringResponse = new TextDecoder().decode(xmlResponse);
|
||||
} else {
|
||||
stringResponse = xmlResponse;
|
||||
}
|
||||
|
||||
const track = new L.GPX(stringResponse, {
|
||||
markers: {
|
||||
startIcon: buildIcon(note.getIcon(), note.getColorClass(), note.title),
|
||||
endIcon: buildIcon("bxs-flag-checkered"),
|
||||
wptIcons: {
|
||||
"": buildIcon("bx bx-pin")
|
||||
}
|
||||
},
|
||||
polyline_options: {
|
||||
color: note.getLabelValue("color") ?? "blue"
|
||||
}
|
||||
});
|
||||
track.addTo(map);
|
||||
return track;
|
||||
}
|
||||
|
||||
function buildIcon(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string) {
|
||||
let html = /*html*/`\
|
||||
<img class="icon" src="${markerIcon}" />
|
||||
<img class="icon-shadow" src="${markerIconShadow}" />
|
||||
<span class="bx ${bxIconClass} ${colorClass ?? ""}"></span>
|
||||
<span class="title-label">${title ?? ""}</span>`;
|
||||
|
||||
if (noteIdLink) {
|
||||
html = `<div data-href="#root/${noteIdLink}">${html}</div>`;
|
||||
}
|
||||
|
||||
return divIcon({
|
||||
html,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41]
|
||||
});
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import treeService from "../../services/tree.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import ViewMode, { type ViewModeArgs } from "./view_mode.js";
|
||||
import type { ViewTypeOptions } from "../../services/note_list_renderer.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="note-list">
|
||||
@@ -157,33 +158,22 @@ const TPL = /*html*/`
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
class ListOrGridView extends ViewMode {
|
||||
class ListOrGridView extends ViewMode<{}> {
|
||||
private $noteList: JQuery<HTMLElement>;
|
||||
|
||||
private parentNote: FNote;
|
||||
private noteIds: string[];
|
||||
private filteredNoteIds!: string[];
|
||||
private page?: number;
|
||||
private pageSize?: number;
|
||||
private viewType?: string | null;
|
||||
private showNotePath?: boolean;
|
||||
private highlightRegex?: RegExp | null;
|
||||
|
||||
/*
|
||||
* We're using noteIds so that it's not necessary to load all notes at once when paging
|
||||
*/
|
||||
constructor(viewType: string, args: ViewModeArgs) {
|
||||
super(args);
|
||||
constructor(viewType: ViewTypeOptions, args: ViewModeArgs) {
|
||||
super(args, viewType);
|
||||
this.$noteList = $(TPL);
|
||||
this.viewType = viewType;
|
||||
|
||||
this.parentNote = args.parentNote;
|
||||
const includedNoteIds = this.getIncludedNoteIds();
|
||||
|
||||
this.noteIds = args.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
|
||||
|
||||
if (this.noteIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
args.$parent.append(this.$noteList);
|
||||
|
||||
@@ -207,8 +197,14 @@ class ListOrGridView extends ViewMode {
|
||||
return new Set(includedLinks.map((rel) => rel.value));
|
||||
}
|
||||
|
||||
async beforeRender() {
|
||||
super.beforeRender();
|
||||
const includedNoteIds = this.getIncludedNoteIds();
|
||||
this.filteredNoteIds = this.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
if (this.noteIds.length === 0 || !this.page || !this.pageSize) {
|
||||
if (this.filteredNoteIds.length === 0 || !this.page || !this.pageSize) {
|
||||
this.$noteList.hide();
|
||||
return;
|
||||
}
|
||||
@@ -229,7 +225,7 @@ class ListOrGridView extends ViewMode {
|
||||
const startIdx = (this.page - 1) * this.pageSize;
|
||||
const endIdx = startIdx + this.pageSize;
|
||||
|
||||
const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length));
|
||||
const pageNoteIds = this.filteredNoteIds.slice(startIdx, Math.min(endIdx, this.filteredNoteIds.length));
|
||||
const pageNotes = await froca.getNotes(pageNoteIds);
|
||||
|
||||
for (const note of pageNotes) {
|
||||
@@ -249,7 +245,7 @@ class ListOrGridView extends ViewMode {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageCount = Math.ceil(this.noteIds.length / this.pageSize);
|
||||
const pageCount = Math.ceil(this.filteredNoteIds.length / this.pageSize);
|
||||
|
||||
$pager.toggle(pageCount > 1);
|
||||
|
||||
@@ -260,7 +256,7 @@ class ListOrGridView extends ViewMode {
|
||||
lastPrinted = true;
|
||||
|
||||
const startIndex = (i - 1) * this.pageSize + 1;
|
||||
const endIndex = Math.min(this.noteIds.length, i * this.pageSize);
|
||||
const endIndex = Math.min(this.filteredNoteIds.length, i * this.pageSize);
|
||||
|
||||
$pager.append(
|
||||
i === this.page
|
||||
@@ -282,7 +278,7 @@ class ListOrGridView extends ViewMode {
|
||||
}
|
||||
|
||||
// no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all
|
||||
$pager.append(`<span class="note-list-pager-total-count">(${this.noteIds.length} notes)</span>`);
|
||||
$pager.append(`<span class="note-list-pager-total-count">(${this.filteredNoteIds.length} notes)</span>`);
|
||||
}
|
||||
|
||||
async renderNote(note: FNote, expand: boolean = false) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { executeBulkActions } from "../../../services/bulk_action.js";
|
||||
|
||||
export async function renameColumn(parentNoteId: string, type: "label" | "relation", originalName: string, newName: string) {
|
||||
if (type === "label") {
|
||||
return executeBulkActions(parentNoteId, [{
|
||||
name: "renameLabel",
|
||||
oldLabelName: originalName,
|
||||
newLabelName: newName
|
||||
}]);
|
||||
} else {
|
||||
return executeBulkActions(parentNoteId, [{
|
||||
name: "renameRelation",
|
||||
oldRelationName: originalName,
|
||||
newRelationName: newName
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteColumn(parentNoteId: string, type: "label" | "relation", columnName: string) {
|
||||
if (type === "label") {
|
||||
return executeBulkActions(parentNoteId, [{
|
||||
name: "deleteLabel",
|
||||
labelName: columnName
|
||||
}]);
|
||||
} else {
|
||||
return executeBulkActions(parentNoteId, [{
|
||||
name: "deleteRelation",
|
||||
relationName: columnName
|
||||
}]);
|
||||
}
|
||||
}
|
||||
152
apps/client/src/widgets/view_widgets/table_view/col_editing.ts
Normal file
152
apps/client/src/widgets/view_widgets/table_view/col_editing.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Tabulator } from "tabulator-tables";
|
||||
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
|
||||
import { Attribute } from "../../../services/attribute_parser";
|
||||
import Component from "../../../components/component";
|
||||
import { CommandListenerData, EventData } from "../../../components/app_context";
|
||||
import attributes from "../../../services/attributes";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { deleteColumn, renameColumn } from "./bulk_actions";
|
||||
import dialog from "../../../services/dialog";
|
||||
import { t } from "../../../services/i18n";
|
||||
|
||||
export default class TableColumnEditing extends Component {
|
||||
|
||||
private attributeDetailWidget: AttributeDetailWidget;
|
||||
private api: Tabulator;
|
||||
private parentNote: FNote;
|
||||
|
||||
private newAttribute?: Attribute;
|
||||
private newAttributePosition?: number;
|
||||
private existingAttributeToEdit?: Attribute;
|
||||
|
||||
constructor($parent: JQuery<HTMLElement>, parentNote: FNote, api: Tabulator) {
|
||||
super();
|
||||
const parentComponent = glob.getComponentByEl($parent[0]);
|
||||
this.attributeDetailWidget = new AttributeDetailWidget()
|
||||
.contentSized()
|
||||
.setParent(parentComponent);
|
||||
$parent.append(this.attributeDetailWidget.render());
|
||||
this.api = api;
|
||||
this.parentNote = parentNote;
|
||||
}
|
||||
|
||||
addNewTableColumnCommand({ referenceColumn, columnToEdit, direction, type }: EventData<"addNewTableColumn">) {
|
||||
let attr: Attribute | undefined;
|
||||
|
||||
this.existingAttributeToEdit = undefined;
|
||||
if (columnToEdit) {
|
||||
attr = this.getAttributeFromField(columnToEdit.getField());
|
||||
if (attr) {
|
||||
this.existingAttributeToEdit = { ...attr };
|
||||
}
|
||||
}
|
||||
|
||||
if (!attr) {
|
||||
attr = {
|
||||
type: "label",
|
||||
name: `${type ?? "label"}:myLabel`,
|
||||
value: "promoted,single,text",
|
||||
isInheritable: true
|
||||
};
|
||||
}
|
||||
|
||||
if (referenceColumn && this.api) {
|
||||
this.newAttributePosition = this.api.getColumns().indexOf(referenceColumn);
|
||||
|
||||
if (direction === "after") {
|
||||
this.newAttributePosition++;
|
||||
}
|
||||
} else {
|
||||
this.newAttributePosition = undefined;
|
||||
}
|
||||
|
||||
this.attributeDetailWidget!.showAttributeDetail({
|
||||
attribute: attr,
|
||||
allAttributes: [ attr ],
|
||||
isOwned: true,
|
||||
x: 0,
|
||||
y: 150,
|
||||
focus: "name",
|
||||
hideMultiplicity: true
|
||||
});
|
||||
}
|
||||
|
||||
async updateAttributeListCommand({ attributes }: CommandListenerData<"updateAttributeList">) {
|
||||
this.newAttribute = attributes[0];
|
||||
}
|
||||
|
||||
async saveAttributesCommand() {
|
||||
if (!this.newAttribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, value, isInheritable } = this.newAttribute;
|
||||
|
||||
this.api.blockRedraw();
|
||||
const isRename = (this.existingAttributeToEdit && this.existingAttributeToEdit.name !== name);
|
||||
try {
|
||||
if (isRename) {
|
||||
const oldName = this.existingAttributeToEdit!.name.split(":")[1];
|
||||
const [ type, newName ] = name.split(":");
|
||||
await renameColumn(this.parentNote.noteId, type as "label" | "relation", oldName, newName);
|
||||
}
|
||||
|
||||
if (this.existingAttributeToEdit && (isRename || this.existingAttributeToEdit.isInheritable !== isInheritable)) {
|
||||
attributes.removeOwnedLabelByName(this.parentNote, this.existingAttributeToEdit.name);
|
||||
}
|
||||
attributes.setLabel(this.parentNote.noteId, name, value, isInheritable);
|
||||
} finally {
|
||||
this.api.restoreRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteTableColumnCommand({ columnToDelete }: CommandListenerData<"deleteTableColumn">) {
|
||||
if (!columnToDelete || !await dialog.confirm(t("table_view.delete_column_confirmation"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
let [ type, name ] = columnToDelete.getField()?.split(".", 2);
|
||||
if (!type || !name) {
|
||||
return;
|
||||
}
|
||||
type = type.replace("s", "");
|
||||
|
||||
this.api.blockRedraw();
|
||||
try {
|
||||
await deleteColumn(this.parentNote.noteId, type as "label" | "relation", name);
|
||||
attributes.removeOwnedLabelByName(this.parentNote, `${type}:${name}`);
|
||||
} finally {
|
||||
this.api.restoreRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
getNewAttributePosition() {
|
||||
return this.newAttributePosition;
|
||||
}
|
||||
|
||||
resetNewAttributePosition() {
|
||||
this.newAttribute = undefined;
|
||||
this.newAttributePosition = undefined;
|
||||
this.existingAttributeToEdit = undefined;
|
||||
}
|
||||
|
||||
getFAttributeFromField(field: string) {
|
||||
const [ type, name ] = field.split(".", 2);
|
||||
const attrName = `${type.replace("s", "")}:${name}`;
|
||||
return this.parentNote.getLabel(attrName);
|
||||
}
|
||||
|
||||
getAttributeFromField(field: string): Attribute | undefined {
|
||||
const fAttribute = this.getFAttributeFromField(field);
|
||||
if (fAttribute) {
|
||||
return {
|
||||
name: fAttribute.name,
|
||||
value: fAttribute.value,
|
||||
type: fAttribute.type,
|
||||
isInheritable: fAttribute.isInheritable
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
133
apps/client/src/widgets/view_widgets/table_view/columns.spec.ts
Normal file
133
apps/client/src/widgets/view_widgets/table_view/columns.spec.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { restoreExistingData } from "./columns";
|
||||
import type { ColumnDefinition } from "tabulator-tables";
|
||||
|
||||
describe("restoreExistingData", () => {
|
||||
it("maintains important columns properties", () => {
|
||||
const newDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", editor: "input" },
|
||||
{ field: "noteId", title: "Note ID", formatter: "color", visible: false }
|
||||
];
|
||||
const oldDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", width: 300, visible: true },
|
||||
{ field: "noteId", title: "Note ID", width: 200, visible: true }
|
||||
];
|
||||
const restored = restoreExistingData(newDefs, oldDefs);
|
||||
expect(restored[0].editor).toBe("input");
|
||||
expect(restored[1].formatter).toBe("color");
|
||||
});
|
||||
|
||||
it("should restore existing column data", () => {
|
||||
const newDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", editor: "input" },
|
||||
{ field: "noteId", title: "Note ID", visible: false }
|
||||
];
|
||||
const oldDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", width: 300, visible: true },
|
||||
{ field: "noteId", title: "Note ID", width: 200, visible: true }
|
||||
];
|
||||
const restored = restoreExistingData(newDefs, oldDefs);
|
||||
expect(restored[0].width).toBe(300);
|
||||
expect(restored[1].width).toBe(200);
|
||||
});
|
||||
|
||||
it("restores order of columns", () => {
|
||||
const newDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", editor: "input" },
|
||||
{ field: "noteId", title: "Note ID", visible: false }
|
||||
];
|
||||
const oldDefs: ColumnDefinition[] = [
|
||||
{ field: "noteId", title: "Note ID", width: 200, visible: true },
|
||||
{ field: "title", title: "Title", width: 300, visible: true }
|
||||
];
|
||||
const restored = restoreExistingData(newDefs, oldDefs);
|
||||
expect(restored[0].field).toBe("noteId");
|
||||
expect(restored[1].field).toBe("title");
|
||||
});
|
||||
|
||||
it("inserts new columns at given position", () => {
|
||||
const newDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", editor: "input" },
|
||||
{ field: "noteId", title: "Note ID", visible: false },
|
||||
{ field: "newColumn", title: "New Column", editor: "input" }
|
||||
];
|
||||
const oldDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", width: 300, visible: true },
|
||||
{ field: "noteId", title: "Note ID", width: 200, visible: true }
|
||||
];
|
||||
const restored = restoreExistingData(newDefs, oldDefs, 0);
|
||||
expect(restored.length).toBe(3);
|
||||
expect(restored[0].field).toBe("newColumn");
|
||||
expect(restored[1].field).toBe("title");
|
||||
expect(restored[2].field).toBe("noteId");
|
||||
});
|
||||
|
||||
it("inserts new columns at the end if no position is specified", () => {
|
||||
const newDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", editor: "input" },
|
||||
{ field: "noteId", title: "Note ID", visible: false },
|
||||
{ field: "newColumn", title: "New Column", editor: "input" }
|
||||
];
|
||||
const oldDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", width: 300, visible: true },
|
||||
{ field: "noteId", title: "Note ID", width: 200, visible: true }
|
||||
];
|
||||
const restored = restoreExistingData(newDefs, oldDefs);
|
||||
expect(restored.length).toBe(3);
|
||||
expect(restored[0].field).toBe("title");
|
||||
expect(restored[1].field).toBe("noteId");
|
||||
expect(restored[2].field).toBe("newColumn");
|
||||
});
|
||||
|
||||
it("supports a rename", () => {
|
||||
const newDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", editor: "input" },
|
||||
{ field: "noteId", title: "Note ID", visible: false },
|
||||
{ field: "newColumn", title: "New Column", editor: "input" }
|
||||
];
|
||||
const oldDefs: ColumnDefinition[] = [
|
||||
{ field: "title", title: "Title", width: 300, visible: true },
|
||||
{ field: "noteId", title: "Note ID", width: 200, visible: true },
|
||||
{ field: "oldColumn", title: "New Column", editor: "input" }
|
||||
];
|
||||
const restored = restoreExistingData(newDefs, oldDefs);
|
||||
expect(restored.length).toBe(3);
|
||||
});
|
||||
|
||||
it("doesn't alter the existing order", () => {
|
||||
const newDefs: ColumnDefinition[] = [
|
||||
{ title: "#", headerSort: false, hozAlign: "center", resizable: false, frozen: true, rowHandle: false },
|
||||
{ field: "noteId", title: "Note ID", visible: false },
|
||||
{ field: "title", title: "Title", editor: "input", width: 400 }
|
||||
]
|
||||
const oldDefs: ColumnDefinition[] = [
|
||||
{ title: "#", headerSort: false, hozAlign: "center", resizable: false, rowHandle: false },
|
||||
{ field: "noteId", title: "Note ID", visible: false },
|
||||
{ field: "title", title: "Title", editor: "input", width: 400 }
|
||||
];
|
||||
const restored = restoreExistingData(newDefs, oldDefs);
|
||||
expect(restored).toStrictEqual(newDefs);
|
||||
});
|
||||
|
||||
it("allows hiding the row number column", () => {
|
||||
const newDefs: ColumnDefinition[] = [
|
||||
{ title: "#", headerSort: false, hozAlign: "center", resizable: false, frozen: true, rowHandle: false },
|
||||
]
|
||||
const oldDefs: ColumnDefinition[] = [
|
||||
{ title: "#", headerSort: false, hozAlign: "center", resizable: false, rowHandle: false, visible: false },
|
||||
];
|
||||
const restored = restoreExistingData(newDefs, oldDefs);
|
||||
expect(restored[0].visible).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it("enforces size for non-resizable columns", () => {
|
||||
const newDefs: ColumnDefinition[] = [
|
||||
{ title: "#", resizable: false, width: "100px" },
|
||||
]
|
||||
const oldDefs: ColumnDefinition[] = [
|
||||
{ title: "#", resizable: false, width: "120px" },
|
||||
];
|
||||
const restored = restoreExistingData(newDefs, oldDefs);
|
||||
expect(restored[0].width).toStrictEqual("100px");
|
||||
});
|
||||
});
|
||||
147
apps/client/src/widgets/view_widgets/table_view/columns.ts
Normal file
147
apps/client/src/widgets/view_widgets/table_view/columns.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { RelationEditor } from "./relation_editor.js";
|
||||
import { MonospaceFormatter, NoteFormatter, NoteTitleFormatter, RowNumberFormatter } from "./formatters.js";
|
||||
import type { ColumnDefinition } from "tabulator-tables";
|
||||
import { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
|
||||
|
||||
type ColumnType = LabelType | "relation";
|
||||
|
||||
export interface AttributeDefinitionInformation {
|
||||
name: string;
|
||||
title?: string;
|
||||
type?: ColumnType;
|
||||
}
|
||||
|
||||
const labelTypeMappings: Record<ColumnType, Partial<ColumnDefinition>> = {
|
||||
text: {
|
||||
editor: "input"
|
||||
},
|
||||
boolean: {
|
||||
formatter: "tickCross",
|
||||
editor: "tickCross"
|
||||
},
|
||||
date: {
|
||||
editor: "date",
|
||||
},
|
||||
datetime: {
|
||||
editor: "datetime"
|
||||
},
|
||||
number: {
|
||||
editor: "number"
|
||||
},
|
||||
time: {
|
||||
editor: "input"
|
||||
},
|
||||
url: {
|
||||
formatter: "link",
|
||||
editor: "input"
|
||||
},
|
||||
relation: {
|
||||
editor: RelationEditor,
|
||||
formatter: NoteFormatter
|
||||
}
|
||||
};
|
||||
|
||||
interface BuildColumnArgs {
|
||||
info: AttributeDefinitionInformation[];
|
||||
movableRows: boolean;
|
||||
existingColumnData: ColumnDefinition[] | undefined;
|
||||
rowNumberHint: number;
|
||||
position?: number;
|
||||
}
|
||||
|
||||
export function buildColumnDefinitions({ info, movableRows, existingColumnData, rowNumberHint, position }: BuildColumnArgs) {
|
||||
let columnDefs: ColumnDefinition[] = [
|
||||
{
|
||||
title: "#",
|
||||
headerSort: false,
|
||||
hozAlign: "center",
|
||||
resizable: false,
|
||||
frozen: true,
|
||||
rowHandle: movableRows,
|
||||
width: calculateIndexColumnWidth(rowNumberHint, movableRows),
|
||||
formatter: RowNumberFormatter(movableRows)
|
||||
},
|
||||
{
|
||||
field: "noteId",
|
||||
title: "Note ID",
|
||||
formatter: MonospaceFormatter,
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
field: "title",
|
||||
title: "Title",
|
||||
editor: "input",
|
||||
formatter: NoteTitleFormatter,
|
||||
width: 400
|
||||
}
|
||||
];
|
||||
|
||||
const seenFields = new Set<string>();
|
||||
for (const { name, title, type } of info) {
|
||||
const prefix = (type === "relation" ? "relations" : "labels");
|
||||
const field = `${prefix}.${name}`;
|
||||
|
||||
if (seenFields.has(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
columnDefs.push({
|
||||
field,
|
||||
title: title ?? name,
|
||||
editor: "input",
|
||||
rowHandle: false,
|
||||
...labelTypeMappings[type ?? "text"],
|
||||
});
|
||||
seenFields.add(field);
|
||||
}
|
||||
|
||||
if (existingColumnData) {
|
||||
columnDefs = restoreExistingData(columnDefs, existingColumnData, position);
|
||||
}
|
||||
|
||||
return columnDefs;
|
||||
}
|
||||
|
||||
export function restoreExistingData(newDefs: ColumnDefinition[], oldDefs: ColumnDefinition[], position?: number) {
|
||||
// 1. Keep existing columns, but restore their properties like width, visibility and order.
|
||||
const newItemsByField = new Map<string, ColumnDefinition>(
|
||||
newDefs.map(def => [def.field!, def])
|
||||
);
|
||||
const existingColumns = oldDefs
|
||||
.filter(item => (item.field && newItemsByField.has(item.field!)) || item.title === "#")
|
||||
.map(oldItem => {
|
||||
const data = newItemsByField.get(oldItem.field!)!;
|
||||
if (oldItem.resizable !== false && oldItem.width !== undefined) {
|
||||
data.width = oldItem.width;
|
||||
}
|
||||
if (oldItem.visible !== undefined) {
|
||||
data.visible = oldItem.visible;
|
||||
}
|
||||
return data;
|
||||
}) as ColumnDefinition[];
|
||||
|
||||
// 2. Determine new columns.
|
||||
const existingFields = new Set(existingColumns.map(item => item.field));
|
||||
const newColumns = newDefs
|
||||
.filter(item => !existingFields.has(item.field!));
|
||||
|
||||
// Clamp position to a valid range
|
||||
const insertPos = position !== undefined
|
||||
? Math.min(Math.max(position, 0), existingColumns.length)
|
||||
: existingColumns.length;
|
||||
|
||||
// 3. Insert new columns at the specified position
|
||||
return [
|
||||
...existingColumns.slice(0, insertPos),
|
||||
...newColumns,
|
||||
...existingColumns.slice(insertPos)
|
||||
];
|
||||
}
|
||||
|
||||
function calculateIndexColumnWidth(rowNumberHint: number, movableRows: boolean): number {
|
||||
let columnWidth = 16 * (rowNumberHint.toString().length || 1);
|
||||
if (movableRows) {
|
||||
columnWidth += 32;
|
||||
}
|
||||
return columnWidth;
|
||||
}
|
||||
277
apps/client/src/widgets/view_widgets/table_view/context_menu.ts
Normal file
277
apps/client/src/widgets/view_widgets/table_view/context_menu.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import { ColumnComponent, RowComponent, Tabulator } from "tabulator-tables";
|
||||
import contextMenu, { MenuItem } from "../../../menus/context_menu.js";
|
||||
import { TableData } from "./rows.js";
|
||||
import branches from "../../../services/branches.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import link_context_menu from "../../../menus/link_context_menu.js";
|
||||
import type FNote from "../../../entities/fnote.js";
|
||||
import froca from "../../../services/froca.js";
|
||||
import type Component from "../../../components/component.js";
|
||||
|
||||
export function setupContextMenu(tabulator: Tabulator, parentNote: FNote) {
|
||||
tabulator.on("rowContext", (e, row) => showRowContextMenu(e, row, parentNote, tabulator));
|
||||
tabulator.on("headerContext", (e, col) => showColumnContextMenu(e, col, parentNote, tabulator));
|
||||
tabulator.on("renderComplete", () => {
|
||||
const headerRow = tabulator.element.querySelector(".tabulator-header-contents");
|
||||
headerRow?.addEventListener("contextmenu", (e) => showHeaderContextMenu(e, tabulator));
|
||||
});
|
||||
|
||||
// Pressing the expand button prevents bubbling and the context menu remains menu when it shouldn't.
|
||||
if (tabulator.options.dataTree) {
|
||||
const dismissContextMenu = () => contextMenu.hide();
|
||||
tabulator.on("dataTreeRowExpanded", dismissContextMenu);
|
||||
tabulator.on("dataTreeRowCollapsed", dismissContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
function showColumnContextMenu(_e: UIEvent, column: ColumnComponent, parentNote: FNote, tabulator: Tabulator) {
|
||||
const e = _e as MouseEvent;
|
||||
const { title, field } = column.getDefinition();
|
||||
|
||||
const sorters = tabulator.getSorters();
|
||||
const sorter = sorters.find(sorter => sorter.field === field);
|
||||
const isUserDefinedColumn = (!!field && (field?.startsWith("labels.") || field?.startsWith("relations.")));
|
||||
|
||||
contextMenu.show({
|
||||
items: [
|
||||
{
|
||||
title: t("table_view.sort-column-by", { title }),
|
||||
enabled: !!field,
|
||||
uiIcon: "bx bx-sort-alt-2",
|
||||
items: [
|
||||
{
|
||||
title: t("table_view.sort-column-ascending"),
|
||||
checked: (sorter?.dir === "asc"),
|
||||
uiIcon: "bx bx-empty",
|
||||
handler: () => tabulator.setSort([
|
||||
{
|
||||
column: field!,
|
||||
dir: "asc",
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
title: t("table_view.sort-column-descending"),
|
||||
checked: (sorter?.dir === "desc"),
|
||||
uiIcon: "bx bx-empty",
|
||||
handler: () => tabulator.setSort([
|
||||
{
|
||||
column: field!,
|
||||
dir: "desc"
|
||||
}
|
||||
])
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("table_view.sort-column-clear"),
|
||||
enabled: sorters.length > 0,
|
||||
uiIcon: "bx bx-x-circle",
|
||||
handler: () => tabulator.clearSort()
|
||||
},
|
||||
{
|
||||
title: "----"
|
||||
},
|
||||
{
|
||||
title: t("table_view.hide-column", { title }),
|
||||
uiIcon: "bx bx-hide",
|
||||
handler: () => column.hide()
|
||||
},
|
||||
{
|
||||
title: t("table_view.show-hide-columns"),
|
||||
uiIcon: "bx bx-columns",
|
||||
items: buildColumnItems(tabulator)
|
||||
},
|
||||
{ title: "----" },
|
||||
{
|
||||
title: t("table_view.add-column-to-the-left"),
|
||||
uiIcon: "bx bx-horizontal-left",
|
||||
enabled: !column.getDefinition().frozen,
|
||||
items: buildInsertSubmenu(e, column, "before"),
|
||||
handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", {
|
||||
referenceColumn: column
|
||||
})
|
||||
},
|
||||
{
|
||||
title: t("table_view.add-column-to-the-right"),
|
||||
uiIcon: "bx bx-horizontal-right",
|
||||
items: buildInsertSubmenu(e, column, "after"),
|
||||
handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", {
|
||||
referenceColumn: column,
|
||||
direction: "after"
|
||||
})
|
||||
},
|
||||
{ title: "----" },
|
||||
{
|
||||
title: t("table_view.edit-column"),
|
||||
uiIcon: "bx bxs-edit-alt",
|
||||
enabled: isUserDefinedColumn,
|
||||
handler: () => getParentComponent(e)?.triggerCommand("addNewTableColumn", {
|
||||
referenceColumn: column,
|
||||
columnToEdit: column
|
||||
})
|
||||
},
|
||||
{
|
||||
title: t("table_view.delete-column"),
|
||||
uiIcon: "bx bx-trash",
|
||||
enabled: isUserDefinedColumn,
|
||||
handler: () => getParentComponent(e)?.triggerCommand("deleteTableColumn", {
|
||||
columnToDelete: column
|
||||
})
|
||||
}
|
||||
],
|
||||
selectMenuItemHandler() {},
|
||||
x: e.pageX,
|
||||
y: e.pageY
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a context menu which has options dedicated to the header area (the part where the columns are, but in the empty space).
|
||||
* Provides generic options such as toggling columns.
|
||||
*/
|
||||
function showHeaderContextMenu(_e: Event, tabulator: Tabulator) {
|
||||
const e = _e as MouseEvent;
|
||||
contextMenu.show({
|
||||
items: [
|
||||
{
|
||||
title: t("table_view.show-hide-columns"),
|
||||
uiIcon: "bx bx-columns",
|
||||
items: buildColumnItems(tabulator)
|
||||
},
|
||||
{ title: "----" },
|
||||
{
|
||||
title: t("table_view.new-column"),
|
||||
uiIcon: "bx bx-empty",
|
||||
enabled: false
|
||||
},
|
||||
...buildInsertSubmenu(e)
|
||||
],
|
||||
selectMenuItemHandler() {},
|
||||
x: e.pageX,
|
||||
y: e.pageY
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
export function showRowContextMenu(_e: UIEvent, row: RowComponent, parentNote: FNote, tabulator: Tabulator) {
|
||||
const e = _e as MouseEvent;
|
||||
const rowData = row.getData() as TableData;
|
||||
|
||||
let parentNoteId: string = parentNote.noteId;
|
||||
|
||||
if (tabulator.options.dataTree) {
|
||||
const parentRow = row.getTreeParent();
|
||||
if (parentRow) {
|
||||
parentNoteId = parentRow.getData().noteId as string;
|
||||
}
|
||||
}
|
||||
|
||||
contextMenu.show({
|
||||
items: [
|
||||
...link_context_menu.getItems(),
|
||||
{ title: "----" },
|
||||
{
|
||||
title: t("table_view.row-insert-above"),
|
||||
uiIcon: "bx bx-horizontal-left bx-rotate-90",
|
||||
handler: () => getParentComponent(e)?.triggerCommand("addNewRow", {
|
||||
parentNotePath: parentNoteId,
|
||||
customOpts: {
|
||||
target: "before",
|
||||
targetBranchId: rowData.branchId,
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
title: t("table_view.row-insert-child"),
|
||||
uiIcon: "bx bx-subdirectory-right",
|
||||
handler: async () => {
|
||||
const branchId = row.getData().branchId;
|
||||
const note = await froca.getBranch(branchId)?.getNote();
|
||||
getParentComponent(e)?.triggerCommand("addNewRow", {
|
||||
parentNotePath: note?.noteId,
|
||||
customOpts: {
|
||||
target: "after",
|
||||
targetBranchId: branchId,
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("table_view.row-insert-below"),
|
||||
uiIcon: "bx bx-horizontal-left bx-rotate-270",
|
||||
handler: () => getParentComponent(e)?.triggerCommand("addNewRow", {
|
||||
parentNotePath: parentNoteId,
|
||||
customOpts: {
|
||||
target: "after",
|
||||
targetBranchId: rowData.branchId,
|
||||
}
|
||||
})
|
||||
},
|
||||
{ title: "----" },
|
||||
{
|
||||
title: t("table_context_menu.delete_row"),
|
||||
uiIcon: "bx bx-trash",
|
||||
handler: () => branches.deleteNotes([ rowData.branchId ], false, false)
|
||||
}
|
||||
],
|
||||
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, rowData.noteId),
|
||||
x: e.pageX,
|
||||
y: e.pageY
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function getParentComponent(e: MouseEvent) {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $(e.target)
|
||||
.closest(".component")
|
||||
.prop("component") as Component;
|
||||
}
|
||||
|
||||
function buildColumnItems(tabulator: Tabulator) {
|
||||
const items: MenuItem<unknown>[] = [];
|
||||
for (const column of tabulator.getColumns()) {
|
||||
const { title } = column.getDefinition();
|
||||
|
||||
items.push({
|
||||
title,
|
||||
checked: column.isVisible(),
|
||||
uiIcon: "bx bx-empty",
|
||||
handler: () => column.toggle()
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function buildInsertSubmenu(e: MouseEvent, referenceColumn?: ColumnComponent, direction?: "before" | "after"): MenuItem<unknown>[] {
|
||||
return [
|
||||
{
|
||||
title: t("table_view.new-column-label"),
|
||||
uiIcon: "bx bx-hash",
|
||||
handler: () => {
|
||||
getParentComponent(e)?.triggerCommand("addNewTableColumn", {
|
||||
referenceColumn,
|
||||
type: "label",
|
||||
direction
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("table_view.new-column-relation"),
|
||||
uiIcon: "bx bx-transfer",
|
||||
handler: () => {
|
||||
getParentComponent(e)?.triggerCommand("addNewTableColumn", {
|
||||
referenceColumn,
|
||||
type: "relation",
|
||||
direction
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
25
apps/client/src/widgets/view_widgets/table_view/dragging.ts
Normal file
25
apps/client/src/widgets/view_widgets/table_view/dragging.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Tabulator } from "tabulator-tables";
|
||||
import type FNote from "../../../entities/fnote.js";
|
||||
import branches from "../../../services/branches.js";
|
||||
|
||||
export function canReorderRows(parentNote: FNote) {
|
||||
return !parentNote.hasLabel("sorted")
|
||||
&& parentNote.type !== "search";
|
||||
}
|
||||
|
||||
export function configureReorderingRows(tabulator: Tabulator) {
|
||||
tabulator.on("rowMoved", (row) => {
|
||||
const branchIdsToMove = [ row.getData().branchId ];
|
||||
|
||||
const prevRow = row.getPrevRow();
|
||||
if (prevRow) {
|
||||
branches.moveAfterBranch(branchIdsToMove, prevRow.getData().branchId);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRow = row.getNextRow();
|
||||
if (nextRow) {
|
||||
branches.moveBeforeBranch(branchIdsToMove, nextRow.getData().branchId);
|
||||
}
|
||||
});
|
||||
}
|
||||
22
apps/client/src/widgets/view_widgets/table_view/footer.ts
Normal file
22
apps/client/src/widgets/view_widgets/table_view/footer.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import FNote from "../../../entities/fnote.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
|
||||
function shouldDisplayFooter(parentNote: FNote) {
|
||||
return (parentNote.type !== "search");
|
||||
}
|
||||
|
||||
export default function buildFooter(parentNote: FNote) {
|
||||
if (!shouldDisplayFooter(parentNote)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return /*html*/`\
|
||||
<button class="btn btn-sm" data-trigger-command="addNewRow">
|
||||
<span class="bx bx-plus"></span> ${t("table_view.new-row")}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm" data-trigger-command="addNewTableColumn">
|
||||
<span class="bx bx-carousel"></span> ${t("table_view.new-column")}
|
||||
</button>
|
||||
`.trimStart();
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { CellComponent } from "tabulator-tables";
|
||||
import froca from "../../../services/froca.js";
|
||||
import FNote from "../../../entities/fnote.js";
|
||||
|
||||
/**
|
||||
* Custom formatter to represent a note, with the icon and note title being rendered.
|
||||
*
|
||||
* The value of the cell must be the note ID.
|
||||
*/
|
||||
export function NoteFormatter(cell: CellComponent, _formatterParams, onRendered): string {
|
||||
let noteId = cell.getValue();
|
||||
if (!noteId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function buildLink(note: FNote | undefined) {
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iconClass = note.getIcon();
|
||||
const title = note.title;
|
||||
const { $noteRef } = buildNoteLink(noteId, title, iconClass, note.getColorClass());
|
||||
return $noteRef[0];
|
||||
}
|
||||
|
||||
const cachedNote = froca.getNoteFromCache(noteId);
|
||||
if (cachedNote) {
|
||||
// Cache hit, build the link immediately
|
||||
const el = buildLink(cachedNote);
|
||||
return el?.outerHTML ?? "";
|
||||
} else {
|
||||
// Cache miss, load the note asynchronously
|
||||
onRendered(async () => {
|
||||
const note = await froca.getNote(noteId);
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = buildLink(note);
|
||||
if (el) {
|
||||
cell.getElement().appendChild(el);
|
||||
}
|
||||
});
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom formatter for the note title that is quite similar to {@link NoteFormatter}, but where the title and icons are read from separate fields.
|
||||
*/
|
||||
export function NoteTitleFormatter(cell: CellComponent) {
|
||||
const { noteId, iconClass, colorClass } = cell.getRow().getData();
|
||||
if (!noteId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const { $noteRef } = buildNoteLink(noteId, cell.getValue(), iconClass, colorClass);
|
||||
return $noteRef[0].outerHTML;
|
||||
}
|
||||
|
||||
export function RowNumberFormatter(draggableRows: boolean) {
|
||||
return (cell: CellComponent) => {
|
||||
let html = "";
|
||||
if (draggableRows) {
|
||||
html += `<span class="bx bx-dots-vertical-rounded"></span> `;
|
||||
}
|
||||
html += cell.getRow().getPosition(true);
|
||||
return html;
|
||||
};
|
||||
}
|
||||
|
||||
export function MonospaceFormatter(cell: CellComponent) {
|
||||
return `<code>${cell.getValue()}</code>`;
|
||||
}
|
||||
|
||||
function buildNoteLink(noteId: string, title: string, iconClass: string, colorClass?: string) {
|
||||
const $noteRef = $("<span>");
|
||||
const href = `#root/${noteId}`;
|
||||
$noteRef.addClass("reference-link");
|
||||
$noteRef.attr("data-href", href);
|
||||
$noteRef.text(title);
|
||||
$noteRef.prepend($("<span>").addClass(iconClass));
|
||||
if (colorClass) {
|
||||
$noteRef.addClass(colorClass);
|
||||
}
|
||||
return { $noteRef, href };
|
||||
}
|
||||
272
apps/client/src/widgets/view_widgets/table_view/index.ts
Normal file
272
apps/client/src/widgets/view_widgets/table_view/index.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import ViewMode, { type ViewModeArgs } from "../view_mode.js";
|
||||
import attributes from "../../../services/attributes.js";
|
||||
import SpacedUpdate from "../../../services/spaced_update.js";
|
||||
import type { EventData } from "../../../components/app_context.js";
|
||||
import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent, ColumnComponent} from 'tabulator-tables';
|
||||
import "tabulator-tables/dist/css/tabulator.css";
|
||||
import "../../../../src/stylesheets/table.css";
|
||||
import { canReorderRows, configureReorderingRows } from "./dragging.js";
|
||||
import buildFooter from "./footer.js";
|
||||
import getAttributeDefinitionInformation, { buildRowDefinitions } from "./rows.js";
|
||||
import { AttributeDefinitionInformation, buildColumnDefinitions } from "./columns.js";
|
||||
import { setupContextMenu } from "./context_menu.js";
|
||||
import TableColumnEditing from "./col_editing.js";
|
||||
import TableRowEditing from "./row_editing.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="table-view">
|
||||
<style>
|
||||
.table-view {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 0 5px 0 10px;
|
||||
}
|
||||
|
||||
.table-view-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-result-widget-content .table-view {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.tabulator-cell .autocomplete {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: transparent;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header {
|
||||
border-top: unset;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left,
|
||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer {
|
||||
background-color: unset;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer .tabulator-footer-contents {
|
||||
justify-content: left;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.tabulator button.tree-expand,
|
||||
.tabulator button.tree-collapse {
|
||||
display: inline-block;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
width: 1.5em;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tabulator button.tree-expand span,
|
||||
.tabulator button.tree-collapse span {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
font-size: 1.5em;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="table-view-container"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export interface StateInfo {
|
||||
tableData?: {
|
||||
columns?: ColumnDefinition[];
|
||||
};
|
||||
}
|
||||
|
||||
export default class TableView extends ViewMode<StateInfo> {
|
||||
|
||||
private $root: JQuery<HTMLElement>;
|
||||
private $container: JQuery<HTMLElement>;
|
||||
private spacedUpdate: SpacedUpdate;
|
||||
private api?: Tabulator;
|
||||
private persistentData: StateInfo["tableData"];
|
||||
private colEditing?: TableColumnEditing;
|
||||
private rowEditing?: TableRowEditing;
|
||||
private maxDepth: number = -1;
|
||||
private rowNumberHint: number = 1;
|
||||
|
||||
constructor(args: ViewModeArgs) {
|
||||
super(args, "table");
|
||||
|
||||
this.$root = $(TPL);
|
||||
this.$container = this.$root.find(".table-view-container");
|
||||
this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000);
|
||||
this.persistentData = {};
|
||||
args.$parent.append(this.$root);
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
this.$container.empty();
|
||||
this.renderTable(this.$container[0]);
|
||||
return this.$root;
|
||||
}
|
||||
|
||||
private async renderTable(el: HTMLElement) {
|
||||
const info = getAttributeDefinitionInformation(this.parentNote);
|
||||
const modules = [ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule ];
|
||||
for (const module of modules) {
|
||||
Tabulator.registerModule(module);
|
||||
}
|
||||
|
||||
this.initialize(el, info);
|
||||
}
|
||||
|
||||
private async initialize(el: HTMLElement, info: AttributeDefinitionInformation[]) {
|
||||
const viewStorage = await this.viewStorage.restore();
|
||||
this.persistentData = viewStorage?.tableData || {};
|
||||
|
||||
this.maxDepth = parseInt(this.parentNote.getLabelValue("maxNestingDepth") ?? "-1", 10);
|
||||
const { definitions: rowData, hasSubtree: hasChildren, rowNumber } = await buildRowDefinitions(this.parentNote, info, this.maxDepth);
|
||||
this.rowNumberHint = rowNumber;
|
||||
const movableRows = canReorderRows(this.parentNote) && !hasChildren;
|
||||
const columnDefs = buildColumnDefinitions({
|
||||
info,
|
||||
movableRows,
|
||||
existingColumnData: this.persistentData.columns,
|
||||
rowNumberHint: this.rowNumberHint
|
||||
});
|
||||
let opts: Options = {
|
||||
layout: "fitDataFill",
|
||||
index: "branchId",
|
||||
columns: columnDefs,
|
||||
data: rowData,
|
||||
persistence: true,
|
||||
movableColumns: true,
|
||||
movableRows,
|
||||
footerElement: buildFooter(this.parentNote),
|
||||
persistenceWriterFunc: (_id, type: string, data: object) => {
|
||||
(this.persistentData as Record<string, {}>)[type] = data;
|
||||
this.spacedUpdate.scheduleUpdate();
|
||||
},
|
||||
persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type],
|
||||
};
|
||||
|
||||
if (hasChildren) {
|
||||
opts = {
|
||||
...opts,
|
||||
dataTree: hasChildren,
|
||||
dataTreeStartExpanded: true,
|
||||
dataTreeBranchElement: false,
|
||||
dataTreeElementColumn: "title",
|
||||
dataTreeChildIndent: 20,
|
||||
dataTreeExpandElement: `<button class="tree-expand"><span class="bx bx-chevron-right"></span></button>`,
|
||||
dataTreeCollapseElement: `<button class="tree-collapse"><span class="bx bx-chevron-down"></span></button>`
|
||||
}
|
||||
}
|
||||
|
||||
this.api = new Tabulator(el, opts);
|
||||
|
||||
this.colEditing = new TableColumnEditing(this.args.$parent, this.args.parentNote, this.api);
|
||||
this.rowEditing = new TableRowEditing(this.api, this.args.parentNotePath!);
|
||||
|
||||
if (movableRows) {
|
||||
configureReorderingRows(this.api);
|
||||
}
|
||||
setupContextMenu(this.api, this.parentNote);
|
||||
}
|
||||
|
||||
private onSave() {
|
||||
this.viewStorage.store({
|
||||
tableData: this.persistentData,
|
||||
});
|
||||
}
|
||||
|
||||
async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (!this.api) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force a refresh if sorted is changed since we need to disable reordering.
|
||||
if (loadResults.getAttributeRows().find(a => a.name === "sorted" && attributes.isAffecting(a, this.parentNote))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Refresh if promoted attributes get changed.
|
||||
if (loadResults.getAttributeRows().find(attr =>
|
||||
attr.type === "label" &&
|
||||
(attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) &&
|
||||
attributes.isAffecting(attr, this.parentNote))) {
|
||||
this.#manageColumnUpdate();
|
||||
return await this.#manageRowsUpdate();
|
||||
}
|
||||
|
||||
// Refresh max depth
|
||||
if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "maxNestingDepth" && attributes.isAffecting(attr, this.parentNote))) {
|
||||
this.maxDepth = parseInt(this.parentNote.getLabelValue("maxNestingDepth") ?? "-1", 10);
|
||||
return await this.#manageRowsUpdate();
|
||||
}
|
||||
|
||||
if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? ""))
|
||||
|| loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId))
|
||||
|| loadResults.getAttributeRows().some(attr => this.noteIds.includes(attr.noteId!))) {
|
||||
return await this.#manageRowsUpdate();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#manageColumnUpdate() {
|
||||
if (!this.api) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = getAttributeDefinitionInformation(this.parentNote);
|
||||
const columnDefs = buildColumnDefinitions({
|
||||
info,
|
||||
movableRows: !!this.api.options.movableRows,
|
||||
existingColumnData: this.persistentData?.columns,
|
||||
rowNumberHint: this.rowNumberHint,
|
||||
position: this.colEditing?.getNewAttributePosition()
|
||||
});
|
||||
this.api.setColumns(columnDefs);
|
||||
this.colEditing?.resetNewAttributePosition();
|
||||
}
|
||||
|
||||
addNewRowCommand(e) { this.rowEditing?.addNewRowCommand(e); }
|
||||
addNewTableColumnCommand(e) { this.colEditing?.addNewTableColumnCommand(e); }
|
||||
deleteTableColumnCommand(e) { this.colEditing?.deleteTableColumnCommand(e); }
|
||||
updateAttributeListCommand(e) { this.colEditing?.updateAttributeListCommand(e); }
|
||||
saveAttributesCommand() { this.colEditing?.saveAttributesCommand(); }
|
||||
|
||||
async #manageRowsUpdate() {
|
||||
if (!this.api) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = getAttributeDefinitionInformation(this.parentNote);
|
||||
const { definitions, hasSubtree, rowNumber } = await buildRowDefinitions(this.parentNote, info, this.maxDepth);
|
||||
this.rowNumberHint = rowNumber;
|
||||
|
||||
// Force a refresh if the data tree needs enabling/disabling.
|
||||
if (this.api.options.dataTree !== hasSubtree) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await this.api.replaceData(definitions);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CellComponent } from "tabulator-tables";
|
||||
import note_autocomplete from "../../../services/note_autocomplete";
|
||||
import froca from "../../../services/froca";
|
||||
|
||||
export function RelationEditor(cell: CellComponent, onRendered, success, cancel, editorParams){
|
||||
//cell - the cell component for the editable cell
|
||||
//onRendered - function to call when the editor has been rendered
|
||||
//success - function to call to pass thesuccessfully updated value to Tabulator
|
||||
//cancel - function to call to abort the edit and return to a normal cell
|
||||
//editorParams - params object passed into the editorParams column definition property
|
||||
|
||||
//create and style editor
|
||||
const editor = document.createElement("input");
|
||||
|
||||
const $editor = $(editor);
|
||||
editor.classList.add("form-control");
|
||||
|
||||
//create and style input
|
||||
editor.style.padding = "3px";
|
||||
editor.style.width = "100%";
|
||||
editor.style.boxSizing = "border-box";
|
||||
|
||||
//Set value of editor to the current value of the cell
|
||||
const originalNoteId = cell.getValue();
|
||||
if (originalNoteId) {
|
||||
const note = froca.getNoteFromCache(originalNoteId);
|
||||
editor.value = note.title;
|
||||
} else {
|
||||
editor.value = "";
|
||||
}
|
||||
|
||||
//set focus on the select box when the editor is selected
|
||||
onRendered(function(){
|
||||
let newNoteId = originalNoteId;
|
||||
|
||||
note_autocomplete.initNoteAutocomplete($editor, {
|
||||
allowCreatingNotes: true,
|
||||
hideAllButtons: true
|
||||
}).on("autocomplete:noteselected", (event, suggestion, dataset) => {
|
||||
const notePath = suggestion.notePath;
|
||||
newNoteId = (notePath ?? "").split("/").at(-1);
|
||||
}).on("blur", () => {
|
||||
if (!editor.value) {
|
||||
newNoteId = "";
|
||||
}
|
||||
success(newNoteId);
|
||||
});
|
||||
editor.focus();
|
||||
});
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("input-group");
|
||||
container.classList.add("autocomplete");
|
||||
container.appendChild(editor);
|
||||
return container;
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import { RowComponent, Tabulator } from "tabulator-tables";
|
||||
import Component from "../../../components/component.js";
|
||||
import { setAttribute, setLabel } from "../../../services/attributes.js";
|
||||
import server from "../../../services/server.js";
|
||||
import froca from "../../../services/froca.js";
|
||||
import note_create, { CreateNoteOpts } from "../../../services/note_create.js";
|
||||
import { CommandListenerData } from "../../../components/app_context.js";
|
||||
|
||||
export default class TableRowEditing extends Component {
|
||||
|
||||
private parentNotePath: string;
|
||||
private api: Tabulator;
|
||||
|
||||
constructor(api: Tabulator, parentNotePath: string) {
|
||||
super();
|
||||
this.api = api;
|
||||
this.parentNotePath = parentNotePath;
|
||||
api.on("cellEdited", async (cell) => {
|
||||
const noteId = cell.getRow().getData().noteId;
|
||||
const field = cell.getField();
|
||||
let newValue = cell.getValue();
|
||||
|
||||
if (field === "title") {
|
||||
server.put(`notes/${noteId}/title`, { title: newValue });
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.includes(".")) {
|
||||
const [ type, name ] = field.split(".", 2);
|
||||
if (type === "labels") {
|
||||
if (typeof newValue === "boolean") {
|
||||
newValue = newValue ? "true" : "false";
|
||||
}
|
||||
setLabel(noteId, name, newValue);
|
||||
} else if (type === "relations") {
|
||||
const note = await froca.getNote(noteId);
|
||||
if (note) {
|
||||
setAttribute(note, "relation", name, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) {
|
||||
const parentNotePath = customNotePath ?? this.parentNotePath;
|
||||
if (parentNotePath) {
|
||||
const opts: CreateNoteOpts = {
|
||||
activate: false,
|
||||
...customOpts
|
||||
}
|
||||
note_create.createNote(parentNotePath, opts).then(({ branch }) => {
|
||||
if (branch) {
|
||||
setTimeout(() => {
|
||||
this.focusOnBranch(branch?.branchId);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
focusOnBranch(branchId: string) {
|
||||
if (!this.api) {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = findRowDataById(this.api.getRows(), branchId);
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Expand the parent tree if any.
|
||||
if (this.api.options.dataTree) {
|
||||
const parent = row.getTreeParent();
|
||||
if (parent) {
|
||||
parent.treeExpand();
|
||||
}
|
||||
}
|
||||
|
||||
row.getCell("title").edit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function findRowDataById(rows: RowComponent[], branchId: string): RowComponent | null {
|
||||
for (let row of rows) {
|
||||
const item = row.getIndex() as string;
|
||||
|
||||
if (item === branchId) {
|
||||
return row;
|
||||
}
|
||||
|
||||
let found = findRowDataById(row.getTreeChildren(), branchId);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
94
apps/client/src/widgets/view_widgets/table_view/rows.ts
Normal file
94
apps/client/src/widgets/view_widgets/table_view/rows.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import FNote from "../../../entities/fnote.js";
|
||||
import type { LabelType } from "../../../services/promoted_attribute_definition_parser.js";
|
||||
import type { AttributeDefinitionInformation } from "./columns.js";
|
||||
|
||||
export type TableData = {
|
||||
iconClass: string;
|
||||
noteId: string;
|
||||
title: string;
|
||||
labels: Record<string, boolean | string | null>;
|
||||
relations: Record<string, boolean | string | null>;
|
||||
branchId: string;
|
||||
colorClass: string | undefined;
|
||||
_children?: TableData[];
|
||||
};
|
||||
|
||||
export async function buildRowDefinitions(parentNote: FNote, infos: AttributeDefinitionInformation[], maxDepth = -1, currentDepth = 0) {
|
||||
const definitions: TableData[] = [];
|
||||
const childBranches = parentNote.getChildBranches();
|
||||
let hasSubtree = false;
|
||||
let rowNumber = childBranches.length;
|
||||
|
||||
for (const branch of childBranches) {
|
||||
const note = await branch.getNote();
|
||||
if (!note) {
|
||||
continue; // Skip if the note is not found
|
||||
}
|
||||
|
||||
const labels: typeof definitions[0]["labels"] = {};
|
||||
const relations: typeof definitions[0]["relations"] = {};
|
||||
for (const { name, type } of infos) {
|
||||
if (type === "relation") {
|
||||
relations[name] = note.getRelationValue(name);
|
||||
} else {
|
||||
labels[name] = note.getLabelValue(name);
|
||||
}
|
||||
}
|
||||
|
||||
const def: TableData = {
|
||||
iconClass: note.getIcon(),
|
||||
noteId: note.noteId,
|
||||
title: note.title,
|
||||
labels,
|
||||
relations,
|
||||
branchId: branch.branchId,
|
||||
colorClass: note.getColorClass()
|
||||
}
|
||||
|
||||
if (note.hasChildren() && (maxDepth < 0 || currentDepth < maxDepth)) {
|
||||
const { definitions, rowNumber: subRowNumber } = (await buildRowDefinitions(note, infos, maxDepth, currentDepth + 1));
|
||||
def._children = definitions;
|
||||
hasSubtree = true;
|
||||
rowNumber += subRowNumber;
|
||||
}
|
||||
|
||||
definitions.push(def);
|
||||
}
|
||||
|
||||
return {
|
||||
definitions,
|
||||
hasSubtree,
|
||||
rowNumber
|
||||
};
|
||||
}
|
||||
|
||||
export default function getAttributeDefinitionInformation(parentNote: FNote) {
|
||||
const info: AttributeDefinitionInformation[] = [];
|
||||
const attrDefs = parentNote.getAttributes()
|
||||
.filter(attr => attr.isDefinition());
|
||||
for (const attrDef of attrDefs) {
|
||||
const def = attrDef.getDefinition();
|
||||
if (def.multiplicity !== "single") {
|
||||
console.warn("Multiple values are not supported for now");
|
||||
continue;
|
||||
}
|
||||
|
||||
const [ labelType, name ] = attrDef.name.split(":", 2);
|
||||
if (attrDef.type !== "label") {
|
||||
console.warn("Relations are not supported for now");
|
||||
continue;
|
||||
}
|
||||
|
||||
let type: LabelType | "relation" = def.labelType || "text";
|
||||
if (labelType === "relation") {
|
||||
type = "relation";
|
||||
}
|
||||
|
||||
info.push({
|
||||
name,
|
||||
title: def.promotedAlias,
|
||||
type
|
||||
});
|
||||
}
|
||||
return info;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user