mirror of
https://github.com/mathuo/dockview
synced 2025-05-01 09:08:24 +00:00
Compare commits
1486 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
093a034738 | ||
|
f9ca5b99d5 | ||
|
420466398d | ||
|
20b5c844f3 | ||
|
6d2f2f1012 | ||
|
3ddea55834 | ||
|
2f334c63c1 | ||
|
8334f9795c | ||
|
7ea3154946 | ||
|
1ef3dc06ce | ||
|
1ff74f2e65 | ||
|
a82aac09bc | ||
|
d1239a5ee5 | ||
|
22deb851dc | ||
|
87f257df1e | ||
|
a92a056981 | ||
|
c3e203f420 | ||
|
8bdd15161c | ||
|
8a3a7e5063 | ||
|
65d95f1271 | ||
|
3ec6627013 | ||
|
c0227d620c | ||
|
aad168da84 | ||
|
dfee4111a2 | ||
|
712f3f860d | ||
|
f545a0a5c7 | ||
|
e3d39fd2ed | ||
|
dca0eb0df7 | ||
|
8188fd9429 | ||
|
54a51ed10b | ||
|
1a11a4ba2c | ||
|
b6d83df1aa | ||
|
6444e4cb7d | ||
|
ca7bd0a86d | ||
|
dc828b7658 | ||
|
6e02e59fec | ||
|
f79bbe0a6a | ||
|
12448e006f | ||
|
5e380affe7 | ||
|
a44fc01f34 | ||
|
8e03e584a7 | ||
|
1ddcaefe80 | ||
|
d086dfd081 | ||
|
b231367e44 | ||
|
bba22fedd4 | ||
|
3ebcb8eaef | ||
|
39dd5f0759 | ||
|
e4f07dfbda | ||
|
d30263af67 | ||
|
54ae457ccd | ||
|
416039bc99 | ||
|
e8fdabba08 | ||
|
9373802115 | ||
|
759dcc7b05 | ||
|
f8faca731d | ||
|
d168356344 | ||
|
64e11a880c | ||
|
a306742508 | ||
|
bffd2dea89 | ||
|
ac92d59d0a | ||
|
21d94b654b | ||
|
1030bdb778 | ||
|
082b811d20 | ||
|
4eff83e9a0 | ||
|
090f2d26f8 | ||
|
e34fb43913 | ||
|
c0119d65c0 | ||
|
d3d57b62b2 | ||
|
96d6947aa6 | ||
|
5fb13dd8a2 | ||
|
cfe37766a9 | ||
|
7a6b2cb26d | ||
|
6511b8d936 | ||
|
5754ddc3f4 | ||
|
eec57c1cde | ||
|
3530c9896e | ||
|
d811ca6554 | ||
|
ab965ca726 | ||
|
b13c841416 | ||
|
773bf5a53c | ||
|
837fabac86 | ||
|
21caabce60 | ||
|
14b918b7ae | ||
|
f98be640ce | ||
|
cd1aa81d93 | ||
|
490fc05baf | ||
|
16373429a8 | ||
|
59fba25ade | ||
|
cdd0183fa9 | ||
|
98cbe377c2 | ||
|
d8da2f29c3 | ||
|
8ed0fb5586 | ||
|
a56f01215b | ||
|
e71d8b1d09 | ||
|
71b99541b3 | ||
|
0e3afcf83f | ||
|
1a85444b81 | ||
|
19a22c49c3 | ||
|
06f02ba411 | ||
|
0e9c648fa9 | ||
|
bb93c9e4c8 | ||
|
c6dcef537f | ||
|
3e409c4265 | ||
|
ca09ae537d | ||
|
e9985df262 | ||
|
dc6bfc67e3 | ||
|
5ca5ffac8d | ||
|
723272d6de | ||
|
6421a72653 | ||
|
0c1b8ce4d7 | ||
|
f40532f272 | ||
|
6b75b5093c | ||
|
c0cbe89835 | ||
|
b46a473cdf | ||
|
239c99ffe5 | ||
|
6678ae24e0 | ||
|
680eaca96c | ||
|
3d23a2d352 | ||
|
c7c1ae9238 | ||
|
c122bfd310 | ||
|
1a3c6ea7db | ||
|
c6865c691c | ||
|
872ec7cba9 | ||
|
2a3647db28 | ||
|
2f9af48146 | ||
|
04e4ea1a70 | ||
|
2d8d38b466 | ||
|
10fd5778dc | ||
|
cd91f4a4d3 | ||
|
148a05a201 | ||
|
8359596be0 | ||
|
cb45889aca | ||
|
5e116c80b4 | ||
|
20eb30ed1c | ||
|
5173922c69 | ||
|
0ca56eaa8a | ||
|
c14b66e5e5 | ||
|
cc5037d208 | ||
|
a53665adc5 | ||
|
d799b5bc89 | ||
|
fade057412 | ||
|
f6e7e4e390 | ||
|
d33babd522 | ||
|
7df9c510bd | ||
|
3479828237 | ||
|
b3ce3e0366 | ||
|
32fa1511d8 | ||
|
e1304ce694 | ||
|
bdf81fd5b5 | ||
|
cd7984cb2b | ||
|
491872ea75 | ||
|
296b49180e | ||
|
487d02bd90 | ||
|
4d01d4e0af | ||
|
8650de90aa | ||
|
e45d9e48e8 | ||
|
da750e89d7 | ||
|
81a904ccbc | ||
|
6016362c5c | ||
|
7fe2e43d43 | ||
|
56d4dc7c74 | ||
|
6fbe2b4233 | ||
|
f749372eb2 | ||
|
c216d70354 | ||
|
0d32d285d2 | ||
|
5d868e63ce | ||
|
7515b9b05d | ||
|
703418e27f | ||
|
53cdf42d84 | ||
|
140a61f401 | ||
|
ff0bf75632 | ||
|
dd918294d1 | ||
|
684b0593e7 | ||
|
13c4a65e18 | ||
|
755135c5ad | ||
|
15bf4bbd1f | ||
|
429177436e | ||
|
cf6891368e | ||
|
e2302372b1 | ||
|
f13f009155 | ||
|
c5025424c7 | ||
|
a66fdae8a3 | ||
|
c777ff463d | ||
|
04327b4613 | ||
|
a02241d1a7 | ||
|
615886e666 | ||
|
1b921864fc | ||
|
1ce96c8ab8 | ||
|
d651c2213e | ||
|
1d61a47e7c | ||
|
aeb0e38249 | ||
|
df3269d56e | ||
|
0233e8fa42 | ||
|
2bcd62412d | ||
|
32e759ef75 | ||
|
ede5a58ec9 | ||
|
56bdec846c | ||
|
25489bf48e | ||
|
9564319e43 | ||
|
461c30dc5b | ||
|
be628a5683 | ||
|
19f027a0db | ||
|
e46a586044 | ||
|
2f4150013b | ||
|
24cc974a68 | ||
|
a00875aae0 | ||
|
02d0ad8634 | ||
|
93b29e77c4 | ||
|
df0d12128b | ||
|
986132c602 | ||
|
fe39c475a2 | ||
|
c9e90168a4 | ||
|
1a9ee8c34e | ||
|
8a05aede4c | ||
|
83b01a9a66 | ||
|
7ebbd437a1 | ||
|
d8ae04e274 | ||
|
3f5c9743ad | ||
|
3d1fa25677 | ||
|
108bc49401 | ||
|
291e76afdb | ||
|
2dc4f81b20 | ||
|
ca6f46ac8e | ||
|
dad9eba474 | ||
|
adaeb16b98 | ||
|
ee7cf637bb | ||
|
e70755e4fc | ||
|
112dc2e2a9 | ||
|
5cf3cfb616 | ||
|
f6f248c63a | ||
|
4bf3fc4820 | ||
|
40c1a26ead | ||
|
3f3ed52007 | ||
|
b812cf9117 | ||
|
210ff38cfa | ||
|
3ffb5034c6 | ||
|
2657c79620 | ||
|
cd4cd0f0ce | ||
|
408db93917 | ||
|
f557e9080a | ||
|
e0744531ca | ||
|
3c680b251f | ||
|
c086cabd39 | ||
|
a202a49182 | ||
|
efd77413da | ||
|
0097da7fde | ||
|
8c7369ccf0 | ||
|
8015860bde | ||
|
14382aa0fd | ||
|
7ddb63383f | ||
|
ac56a1f250 | ||
|
12bb6f6d20 | ||
|
bc455265cd | ||
|
6881daa593 | ||
|
3c90e9d196 | ||
|
c55257ac56 | ||
|
1b7f29e800 | ||
|
693a3cd6ef | ||
|
d1d8c8083e | ||
|
fd29ea570f | ||
|
1acdd969a0 | ||
|
aeda5f4ca1 | ||
|
5df4c5cce1 | ||
|
62fee4e7ec | ||
|
b24470fa42 | ||
|
0c602afab5 | ||
|
ab5a11f13b | ||
|
fc7b9fb2e2 | ||
|
d519c239e3 | ||
|
2b36844110 | ||
|
e2e91834ff | ||
|
72f457ab9d | ||
|
1033b80783 | ||
|
7adabec088 | ||
|
4841d95573 | ||
|
2dc0dafa5a | ||
|
9d4f4cb534 | ||
|
05084030db | ||
|
4d71775804 | ||
|
77bccb3f69 | ||
|
affb8590dc | ||
|
6c39c448bb | ||
|
a6a2d048c7 | ||
|
464e255d56 | ||
|
0e8139217b | ||
|
1876511c70 | ||
|
56182aa60f | ||
|
bf35b265db | ||
|
520aa39724 | ||
|
59acd94bc6 | ||
|
619b0b3276 | ||
|
d686b2c2c7 | ||
|
479d4bb322 | ||
|
49b1c5a174 | ||
|
35b3ee2b0f | ||
|
a443bb5e41 | ||
|
a05d2af417 | ||
|
1efa5c0ef6 | ||
|
6bf0263797 | ||
|
ed3fd57d6a | ||
|
5a80b3a58d | ||
|
f022f3cb26 | ||
|
af5ce5bbfd | ||
|
ea5b94ad90 | ||
|
e5e9603221 | ||
|
cc18f5055c | ||
|
fafc18b419 | ||
|
bb312d871c | ||
|
49561d14cc | ||
|
1cd4251083 | ||
|
ee74785d7e | ||
|
eda3ea1210 | ||
|
f689e38e59 | ||
|
f765dd52fc | ||
|
04d54e1dc5 | ||
|
e93a008fe4 | ||
|
5b9dbdf57e | ||
|
0e77bdf4d7 | ||
|
0211148b66 | ||
|
973ecff0be | ||
|
bdf286103f | ||
|
e86155adf4 | ||
|
374bd2adff | ||
|
8618ed3aad | ||
|
aa39b55563 | ||
|
24e8c99dea | ||
|
eeab0251d2 | ||
|
2111e2293c | ||
|
bd28468bf5 | ||
|
05b1c27320 | ||
|
b326291f94 | ||
|
8a4380ec14 | ||
|
eb3cac8b1b | ||
|
0db4f59990 | ||
|
aed7b97bac | ||
|
9e4b3bbdd6 | ||
|
cd59248f34 | ||
|
876b107284 | ||
|
ea9dc13992 | ||
|
f20b5285da | ||
|
9b46c1e7d9 | ||
|
d7e1fd4bb2 | ||
|
ae88703a8b | ||
|
cf8f18dbbd | ||
|
b96ccf00ce | ||
|
5285baebdb | ||
|
3652505a08 | ||
|
c5f94906d4 | ||
|
e3fb689d27 | ||
|
93e5ecec4c | ||
|
82dbdb29c3 | ||
|
476b0a1d42 | ||
|
e7622f6c2b | ||
|
cd6604d28c | ||
|
cffd742b7b | ||
|
bbdb99dbb5 | ||
|
ce381f8ce9 | ||
|
1bd2bff8f8 | ||
|
943c2dc425 | ||
|
bc278f2beb | ||
|
b81afd45e8 | ||
|
77b8023b56 | ||
|
963607defe | ||
|
4a0b328622 | ||
|
5762faac9d | ||
|
7f750330b1 | ||
|
55d9ca31d4 | ||
|
fde6764377 | ||
|
9c6dae30ca | ||
|
acadeedaf6 | ||
|
fc84942b9b | ||
|
e3eb851ab7 | ||
|
84d02b9925 | ||
|
3fcbf6515e | ||
|
9306d9fcdc | ||
|
d29052e606 | ||
|
9ee2b821ff | ||
|
c4ab6ac562 | ||
|
35630e4f1c | ||
|
07b548c6b6 | ||
|
7eff8a8642 | ||
|
abaad37edd | ||
|
b68d8df9ac | ||
|
07d67b2246 | ||
|
0ae16cf444 | ||
|
26cd1cc1cc | ||
|
56457fe269 | ||
|
688abef81d | ||
|
4803109203 | ||
|
4d878a375c | ||
|
0de2e57a1d | ||
|
87d42c96e2 | ||
|
61abf780d1 | ||
|
ffe7d59b74 | ||
|
3dc590351c | ||
|
5d6055c4d2 | ||
|
d0eda81a9e | ||
|
db9703a150 | ||
|
f6e816fa10 | ||
|
9c091f5126 | ||
|
c6403ff6e8 | ||
|
24817976f6 | ||
|
459963c13b | ||
|
eaabf7968c | ||
|
521d2a0e6d | ||
|
2a3f623c30 | ||
|
33d8a7a026 | ||
|
dc684d05a7 | ||
|
36527910f3 | ||
|
4b1b4bab32 | ||
|
2f0e34c99f | ||
|
67fee68ebe | ||
|
7eff1da988 | ||
|
ffbd7bcf04 | ||
|
dbe97b086c | ||
|
b0ed0a99ff | ||
|
3c45c962b2 | ||
|
f124414594 | ||
|
715cd1e03d | ||
|
d00c2207d9 | ||
|
6ca39ba58b | ||
|
4a05fd0aed | ||
|
bc8fa59557 | ||
|
936499aa1e | ||
|
9642dd844d | ||
|
40e5c8053d | ||
|
b0d98102fc | ||
|
ec5b681196 | ||
|
5a1e41775f | ||
|
95685efbbd | ||
|
ce3706d1b3 | ||
|
bb09f2ccf0 | ||
|
da0270066b | ||
|
baef193d78 | ||
|
61f3c252d4 | ||
|
b0e4c6d926 | ||
|
eb7d6b87d6 | ||
|
78a5534bab | ||
|
f79b7296e6 | ||
|
deb59eb94a | ||
|
331be77b08 | ||
|
b09ab48cd8 | ||
|
61eaae30c8 | ||
|
362cce88eb | ||
|
09edbddf72 | ||
|
4e54915a93 | ||
|
4a9a791047 | ||
|
db747f1ead | ||
|
e147255fcd | ||
|
0b459800cc | ||
|
c5770d4381 | ||
|
ddcb42fe53 | ||
|
5bd1f1e1c6 | ||
|
ba0b5f3985 | ||
|
451a8579d9 | ||
|
da6704b517 | ||
|
68f0319e1b | ||
|
c55c3e4a2f | ||
|
f93e5b613e | ||
|
1c33ee46a2 | ||
|
258be8594d | ||
|
b25264e190 | ||
|
45919ff397 | ||
|
c584f24a02 | ||
|
9a9dc3500e | ||
|
0634f4fe52 | ||
|
f00b3d88e1 | ||
|
d3b22d83cf | ||
|
2ee9b46eb9 | ||
|
2f1f35bd31 | ||
|
465141c097 | ||
|
0d34d7285e | ||
|
9ee00ab423 | ||
|
12c873c7cf | ||
|
5a635336b3 | ||
|
60f49ce449 | ||
|
50608f4520 | ||
|
566fc0a7c8 | ||
|
d2baa1371c | ||
|
814a8e4dae | ||
|
e85863b65f | ||
|
cc50199cff | ||
|
8533a18fea | ||
|
fd2460416a | ||
|
889e4d200c | ||
|
14b7e8de80 | ||
|
5fddff72e7 | ||
|
775145f176 | ||
|
5418538874 | ||
|
05ff795188 | ||
|
5c945e97d6 | ||
|
68d36612dc | ||
|
70014a8c10 | ||
|
bb40c45a29 | ||
|
a84bdb9182 | ||
|
d379b111ee | ||
|
157ea8a3d1 | ||
|
9523e6fb48 | ||
|
7b0cc492e7 | ||
|
ed40e224e7 | ||
|
53de69a61a | ||
|
6b2ed70a7c | ||
|
de93234632 | ||
|
024c2d5349 | ||
|
e7ab35a30e | ||
|
874da1c7d9 | ||
|
30c7fa2c6a | ||
|
d2b9493fb4 | ||
|
8d56a2ec3a | ||
|
284fb1440b | ||
|
807ccf80de | ||
|
89fe866ac5 | ||
|
cb69d47926 | ||
|
9e57a8691a | ||
|
f7b173f7df | ||
|
c4f46a190a | ||
|
d488e8c3d9 | ||
|
c70327e268 | ||
|
d8cb5ac3f8 | ||
|
9b641f64be | ||
|
aeccce639c | ||
|
1d7ab22027 | ||
|
0b1a09d910 | ||
|
5fc8a6a4d3 | ||
|
66d96cf6ae | ||
|
926b688267 | ||
|
0bca63b550 | ||
|
c2791c6124 | ||
|
e1849f72a5 | ||
|
fb6aba1d7f | ||
|
e28c76626f | ||
|
aa8e7e09e0 | ||
|
47b691b6b4 | ||
|
20c1a66d20 | ||
|
8f9d225c61 | ||
|
0127857544 | ||
|
6274708acb | ||
|
a8472e8b71 | ||
|
586400019f | ||
|
0fd3a669c7 | ||
|
a24dd21ca2 | ||
|
b901f4de0c | ||
|
c7eb2dc105 | ||
|
c11d65bef7 | ||
|
f4866c9f89 | ||
|
e9cf1bfd9f | ||
|
e47ccdea6a | ||
|
a855c55132 | ||
|
8f24d6b366 | ||
|
75066f2420 | ||
|
1cc340f3f1 | ||
|
23b1edb003 | ||
|
87e52f88d0 | ||
|
6499e78544 | ||
|
02af66a10f | ||
|
a6a6ffc058 | ||
|
03a39507dc | ||
|
502a984d2b | ||
|
1762389406 | ||
|
87b999fe46 | ||
|
b0a5e66cff | ||
|
16e0971275 | ||
|
cae8f3664e | ||
|
9ccc7add41 | ||
|
22ba48e81e | ||
|
6a1f47d4da | ||
|
8da26f1484 | ||
|
512a8d2c72 | ||
|
6c670c1fbb | ||
|
cc88096413 | ||
|
e5334f0fd7 | ||
|
a6bafcc063 | ||
|
c61cb1a6e8 | ||
|
c49fcd4d9d | ||
|
e3e87f4133 | ||
|
0df4062de4 | ||
|
8a67f60747 | ||
|
90690fa8ea | ||
|
9e245e6f5e | ||
|
ffddc95602 | ||
|
5ca1cc5e62 | ||
|
9c44a08a31 | ||
|
1d9b285bf4 | ||
|
31596772a7 | ||
|
0380b4fda0 | ||
|
c2ae86de3a | ||
|
718e3344ca | ||
|
ccef43cb82 | ||
|
d0d6508ae3 | ||
|
b084ac6e00 | ||
|
dbe0fe88ad | ||
|
3ec3c8358a | ||
|
312aeded25 | ||
|
16a0bc5b3e | ||
|
c7d7749308 | ||
|
8ca80b542d | ||
|
e686ecb9d5 | ||
|
0b67d8e50b | ||
|
da269aa71b | ||
|
4c9caadcf5 | ||
|
3df1642d91 | ||
|
2c77b7e208 | ||
|
64076b2bdd | ||
|
5e45b9a469 | ||
|
b78a7a6207 | ||
|
105be3661f | ||
|
aa6eb27d7d | ||
|
22d5067395 | ||
|
32d668f69d | ||
|
4b616c5578 | ||
|
10256672b4 | ||
|
43548618ba | ||
|
9b1c366ce5 | ||
|
5a2d7394be | ||
|
b222de86f7 | ||
|
3e9f96bdbf | ||
|
4045689bd0 | ||
|
ecf12b175e | ||
|
740ded45f4 | ||
|
bb1a7934c0 | ||
|
ea51b5c883 | ||
|
5ec847ec77 | ||
|
a18b95f9ad | ||
|
cb203bdf1e | ||
|
cf6ee14f3d | ||
|
6a620d4088 | ||
|
bd2d8d7bf6 | ||
|
c195fa19bf | ||
|
eb0172f06c | ||
|
fa25c55655 | ||
|
406af8a87f | ||
|
ff6d40a545 | ||
|
ca12d7d920 | ||
|
73cd0dba4e | ||
|
584795b099 | ||
|
343d42cb86 | ||
|
36dd190ddb | ||
|
f7fc6235e5 | ||
|
5799549901 | ||
|
450e37f973 | ||
|
a2a4e68166 | ||
|
ef70ea236d | ||
|
de88179b26 | ||
|
b17aa24637 | ||
|
a1b3a94c42 | ||
|
8df1de888a | ||
|
53c0a39d7a | ||
|
3c747aa4a7 | ||
|
8aa0d0192e | ||
|
331e190bf8 | ||
|
ed466f758f | ||
|
7ca2997970 | ||
|
26805d023a | ||
|
126d01ede0 | ||
|
d953cf9e4e | ||
|
9fbb1ee479 | ||
|
8e6f7bf808 | ||
|
882c1353c7 | ||
|
318cbd6854 | ||
|
329ed77115 | ||
|
c104503d62 | ||
|
c6753f176a | ||
|
dc50d4e2ac | ||
|
0f555075ae | ||
|
b4ff626b74 | ||
|
eeddad542b | ||
|
b07961df3a | ||
|
d4d79b220f | ||
|
e3ed4251ed | ||
|
58e9f9b355 | ||
|
47f78ad649 | ||
|
a3b20deee9 | ||
|
3803bfa13a | ||
|
03235172a8 | ||
|
859723a890 | ||
|
b5c3bcc86a | ||
|
e726b4ab02 | ||
|
1661ff77d2 | ||
|
4300b1f89c | ||
|
42a4d50c47 | ||
|
76c3fb6fa0 | ||
|
0ee10912a2 | ||
|
82b83e7ed3 | ||
|
46bceea217 | ||
|
5a3f0b1d4a | ||
|
faeb8241a5 | ||
|
e71fd7b153 | ||
|
5af4aa2da9 | ||
|
4ee0617014 | ||
|
717247bbec | ||
|
2337373a6f | ||
|
a96400d970 | ||
|
9006fb9917 | ||
|
fdb148bc85 | ||
|
bd1a8f1d44 | ||
|
fc1c747bed | ||
|
4c142b9632 | ||
|
08ec3fd3a5 | ||
|
206255b411 | ||
|
ffbcaa6259 | ||
|
a15e895337 | ||
|
19595f3b12 | ||
|
1ccd75b2b3 | ||
|
4d1952387c | ||
|
4ad5b0ffe4 | ||
|
739976ea28 | ||
|
3ea9f21e90 | ||
|
bac862a5fc | ||
|
78e0b89a78 | ||
|
16a49c7b27 | ||
|
292e25935f | ||
|
0b39e84f86 | ||
|
a1cd01380b | ||
|
f6cfb4418c | ||
|
65a7da6a36 | ||
|
4aa707132f | ||
|
63ca9053a5 | ||
|
bfa4f60288 | ||
|
493f843987 | ||
|
e64d96cef7 | ||
|
3832222fef | ||
|
007b2a8fea | ||
|
2ce175dbf3 | ||
|
6c2d511827 | ||
|
715281b804 | ||
|
a1b2b9b1da | ||
|
5ac2a9ffdb | ||
|
821c2f5c44 | ||
|
be0fea7104 | ||
|
ffc19f891d | ||
|
3ef9f4ed2d | ||
|
3fb2800db4 | ||
|
d577f0238c | ||
|
b551d93dac | ||
|
55802a7a16 | ||
|
17c0f6e575 | ||
|
7701625b6f | ||
|
141b2beaf3 | ||
|
bae45867fa | ||
|
8ae5a9fc94 | ||
|
76d1f9ab70 | ||
|
3ab3467547 | ||
|
175e8caa67 | ||
|
b36fd3f312 | ||
|
dfdda78ab4 | ||
|
a8e243b3fd | ||
|
2aa700bca0 | ||
|
8833a973b9 | ||
|
a7dafbf301 | ||
|
4b49c66283 | ||
|
2202a89c15 | ||
|
430f97fd7e | ||
|
866869710b | ||
|
59f9016e8d | ||
|
0eec369120 | ||
|
1dd8392726 | ||
|
c89dd5009a | ||
|
133ecbfccc | ||
|
83d0c350c1 | ||
|
e089b29442 | ||
|
3b27621623 | ||
|
5cd51f9f67 | ||
|
ce15208694 | ||
|
8f94f0a3c2 | ||
|
37a688c874 | ||
|
7f5f34d6f2 | ||
|
e234312e03 | ||
|
59aa2723fc | ||
|
11269c4625 | ||
|
2323c24a6a | ||
|
6acc444c58 | ||
|
c4e2d1c645 | ||
|
a084c8b5b0 | ||
|
ed265d398b | ||
|
559bf48372 | ||
|
f1357edbb5 | ||
|
58840287b2 | ||
|
f9a6233481 | ||
|
38b02a3775 | ||
|
07df1b48ba | ||
|
30e035b2f3 | ||
|
f32cb06056 | ||
|
2dbfda25e9 | ||
|
307780d15a | ||
|
42b95e5f0a | ||
|
319cf65ac2 | ||
|
12b4a0d27b | ||
|
f364bb70a6 | ||
|
c1bf5deaf9 | ||
|
47fb99a06f | ||
|
1462b6a37a | ||
|
b2b58a4e57 | ||
|
86be252e99 | ||
|
50b70a298e | ||
|
c53d2690c3 | ||
|
e0f167050c | ||
|
359b0e81d0 | ||
|
b77df7c168 | ||
|
5d1b6c336f | ||
|
49fed85612 | ||
|
13516a5f37 | ||
|
33cf223334 | ||
|
2c8422b85e | ||
|
f87341b99c | ||
|
5f3f44bd21 | ||
|
f483e08497 | ||
|
5058a9c978 | ||
|
4fb8a3a098 | ||
|
59a947c4c7 | ||
|
6665c61a95 | ||
|
bfc4faeed2 | ||
|
853a5be569 | ||
|
782a468b7b | ||
|
01544a9953 | ||
|
9e10814840 | ||
|
fbe6c65186 | ||
|
e3dd45a185 | ||
|
0eff522f1f | ||
|
1e756209a0 | ||
|
35a97b7426 | ||
|
477394cd1e | ||
|
54bc30c1aa | ||
|
c42d8648ea | ||
|
d8e8103f80 | ||
|
1923ad66ca | ||
|
4f5b6adb5f | ||
|
6f9b540fdf | ||
|
ba3fe82c02 | ||
|
7905af2945 | ||
|
a39c2938f0 | ||
|
1539321320 | ||
|
2ca4c6fd65 | ||
|
7dde18c636 | ||
|
d9906eb802 | ||
|
88565f022c | ||
|
78eac85c68 | ||
|
105017245b | ||
|
77925dc4ca | ||
|
bd5999b0ea | ||
|
b112b2d5e4 | ||
|
936a5c6917 | ||
|
97483623d4 | ||
|
0cb621e860 | ||
|
d7baa93a9b | ||
|
3d47c8e2c5 | ||
|
1964d6b306 | ||
|
5b493b95e0 | ||
|
44a7019c60 | ||
|
efbe019583 | ||
|
b49e2e5d10 | ||
|
13d3db605b | ||
|
6434e68b5b | ||
|
7fdede6952 | ||
|
1cf23f04ba | ||
|
32746e248d | ||
|
107c20a81d | ||
|
3c24579e90 | ||
|
d083ddd129 | ||
|
36299d08f9 | ||
|
e6cae5f90d | ||
|
22018e0d28 | ||
|
fda40a9fcb | ||
|
50789414ea | ||
|
5de5bae766 | ||
|
a147a28222 | ||
|
bdecc00c81 | ||
|
be11692114 | ||
|
ec341f0706 | ||
|
575a1d7031 | ||
|
7f54cff960 | ||
|
302c269849 | ||
|
99d4c1e180 | ||
|
c1edea8744 | ||
|
d20689170d | ||
|
75da885f91 | ||
|
98c63628af | ||
|
6b7f18a3ee | ||
|
7b3adb919e | ||
|
081e665e56 | ||
|
c4f778f1cc | ||
|
acb500a9d8 | ||
|
dd70450a8e | ||
|
21bbaa7349 | ||
|
24e7b05321 | ||
|
82df784065 | ||
|
9c5fd414c1 | ||
|
e07cedf01f | ||
|
36299f8c93 | ||
|
7343009e9b | ||
|
ffd5db273e | ||
|
1f384c3c65 | ||
|
74f451ea16 | ||
|
d5da2a443b | ||
|
d68a1c88d0 | ||
|
857b475be7 | ||
|
3cbd238c8a | ||
|
e95777a6a2 | ||
|
1853d45191 | ||
|
c3db8e303d | ||
|
551731ffd1 | ||
|
d847f18cd0 | ||
|
360d6ef429 | ||
|
2b7ba6489c | ||
|
a51740c123 | ||
|
0f828a909a | ||
|
42a608b93d | ||
|
4a7c6b3fee | ||
|
5e15af4ad5 | ||
|
17cf5a6057 | ||
|
84088505a8 | ||
|
464c4fd938 | ||
|
20f200963f | ||
|
1e3462a193 | ||
|
997208822b | ||
|
84e88f458f | ||
|
6aca089685 | ||
|
ad05a3cc25 | ||
|
a74daf410e | ||
|
d1f67828e3 | ||
|
c44ccfa52e | ||
|
5b1aa4221f | ||
|
6e5e8429ec | ||
|
454542b729 | ||
|
015cef7fb0 | ||
|
325a203877 | ||
|
3a458461a1 | ||
|
09a6f265d5 | ||
|
1134f972d3 | ||
|
1b6c2ba92c | ||
|
6d5f3e5975 | ||
|
5ee487f110 | ||
|
1f3bed9740 | ||
|
d60b4c6ba2 | ||
|
3e0911aa91 | ||
|
c5e6ebbe7f | ||
|
3fdb3fd3d6 | ||
|
dd8a648a3e | ||
|
7ac07bdf0d | ||
|
e0bf694512 | ||
|
db1cb49487 | ||
|
d00da9234d | ||
|
8918e254ed | ||
|
7d50eb5453 | ||
|
c3e2ed1a87 | ||
|
8528f8d70f | ||
|
a43ee59bc2 | ||
|
52e458772e | ||
|
b883e2fd31 | ||
|
f26a1cd404 | ||
|
37b0a062a4 | ||
|
a9b44d9641 | ||
|
e2049edab7 | ||
|
5f72f5a36d | ||
|
d09b2e8ff4 | ||
|
722152c80c | ||
|
4f09deeae5 | ||
|
660d99754a | ||
|
0f714daaa8 | ||
|
c7565f7c24 | ||
|
2e09a7028f | ||
|
4222d32b8d | ||
|
acdad8f439 | ||
|
417cdf875f | ||
|
14f565da4e | ||
|
fdf7db15e3 | ||
|
34ea71a09a | ||
|
2e757e7aed | ||
|
7cfabd53d4 | ||
|
e7caeb1906 | ||
|
eb5006fc03 | ||
|
97c75eb7f7 | ||
|
b02087d222 | ||
|
1eac7e83ea | ||
|
171031d940 | ||
|
199e4ff77d | ||
|
4ff77d4696 | ||
|
fb9c056ff6 | ||
|
200e1c4ccb | ||
|
9f504f2597 | ||
|
d4ef82835a | ||
|
fe5a882631 | ||
|
debdcb8d8b | ||
|
9e8b0fddd4 | ||
|
4bd6eba53a | ||
|
4e830afa3c | ||
|
213d3ed235 | ||
|
6600cb88d8 | ||
|
3052857dfc | ||
|
321f78ec0e | ||
|
bf350e404b | ||
|
747049b8f3 | ||
|
026e6b9b91 | ||
|
01816a1d8b | ||
|
716424d972 | ||
|
fa5cf06431 | ||
|
c99a385a1d | ||
|
b8a5d30f81 | ||
|
78336d6d90 | ||
|
82490cba46 | ||
|
a35bb26f3d | ||
|
ff3f0b23a8 | ||
|
bd381640af | ||
|
b21b222ee4 | ||
|
7aa19b2c89 | ||
|
c41158cea6 | ||
|
c34e03f158 | ||
|
edaf3cb067 | ||
|
563d57be59 | ||
|
f5709549c6 | ||
|
5e4d2cb506 | ||
|
080db84537 | ||
|
4fdd836380 | ||
|
5e4a9d5855 | ||
|
7168f55fab | ||
|
834d763445 | ||
|
33a52523fe | ||
|
407d9b3aad | ||
|
19ecd531e1 | ||
|
d5920bd4f8 | ||
|
8810ad1a66 | ||
|
6b146cf1bf | ||
|
b1216dc96d | ||
|
5171a4a592 | ||
|
562055a72f | ||
|
f8fca83166 | ||
|
9e6f0877f1 | ||
|
bb7d8dfb52 | ||
|
f8137b7eb7 | ||
|
3533671406 | ||
|
9ca535efa7 | ||
|
e2f981274c | ||
|
e9bf894acb | ||
|
06252f1d0c | ||
|
9951563ce1 | ||
|
a05f101ad4 | ||
|
653fead20b | ||
|
9dee87dce4 | ||
|
34d8317ec5 | ||
|
42881ab3c0 | ||
|
4e124e6eb6 | ||
|
fbc8fce942 | ||
|
a7f69fc617 | ||
|
a92fb3f554 | ||
|
ea69f14f73 | ||
|
a2929a84ff | ||
|
60333848af | ||
|
0c28f2dabe | ||
|
cb354445ac | ||
|
68e573e5f0 | ||
|
878452e9d3 | ||
|
3faf0ae6dc | ||
|
f82f2c8636 | ||
|
1921e170e0 | ||
|
238c11f969 | ||
|
025ea3279a | ||
|
9ade38b0c4 | ||
|
96ef9ee464 | ||
|
1865fe7ca0 | ||
|
61875fff08 | ||
|
6c2c59f858 | ||
|
7882e581e5 | ||
|
28f77ac3ae | ||
|
10e12340f9 | ||
|
d1cf3818a9 | ||
|
f563ae4fe4 | ||
|
c75964632b | ||
|
b271565bf3 | ||
|
101dbbdfde | ||
|
8b736513a4 | ||
|
6218281af0 | ||
|
f697fc5da8 | ||
|
95c7b1a624 | ||
|
b2c2869574 | ||
|
64e71f8931 | ||
|
04f42ba801 | ||
|
eed74f5310 | ||
|
982bbc1ed9 | ||
|
faf4103f6e | ||
|
bfee4c6a51 | ||
|
b486e3ae58 | ||
|
9dd307b5e3 | ||
|
88c7761b4e | ||
|
84463e4240 | ||
|
2fe3f53ac1 | ||
|
014125d823 | ||
|
659a59f16f | ||
|
0ad2fc4b48 | ||
|
1f022635cf | ||
|
93ff4045a0 | ||
|
7544926d08 | ||
|
598175ea60 | ||
|
06f467184b | ||
|
8a51593ac3 | ||
|
d306387a0a | ||
|
2774a27eda | ||
|
92465af989 | ||
|
87d474d11b | ||
|
d9453329cf | ||
|
79df8020b8 | ||
|
03642d50a2 | ||
|
81bd9953c5 | ||
|
c0f5b1d86b | ||
|
0dc3098d69 | ||
|
3cfc01bec4 | ||
|
5fba8814a8 | ||
|
575dbac8ca | ||
|
b7085493a7 | ||
|
7d69014561 | ||
|
10317206e0 | ||
|
0693c49098 | ||
|
ba2976dd74 | ||
|
f24e33f224 | ||
|
f5b90725f4 | ||
|
c259b44e39 | ||
|
6d58e48470 | ||
|
568731b723 | ||
|
b4a1f46a49 | ||
|
c249d44017 | ||
|
a7b05cf1a8 | ||
|
48a1cc9693 | ||
|
bb247e1303 | ||
|
88860cd4cd | ||
|
df52c808ab | ||
|
8a9106ab76 | ||
|
0932a83f1a | ||
|
8bd560688a | ||
|
63a9ba81cc | ||
|
d84f48e151 | ||
|
2d3b35c008 | ||
|
4d72d84971 | ||
|
73da6be06a | ||
|
0ba72eeb5e | ||
|
1f0bc97b10 | ||
|
3241cec703 | ||
|
cec14b9d50 | ||
|
530226dcbd | ||
|
d94460955d | ||
|
d7d6dc2635 | ||
|
79f4379b8c | ||
|
ce60215af5 | ||
|
2883276888 | ||
|
50896ff879 | ||
|
6ebd185f0a | ||
|
49594e5bfe | ||
|
0943ff8f4b | ||
|
c044af68e9 | ||
|
901a4d0c7f | ||
|
b0124d6180 | ||
|
2381481ec5 | ||
|
a5ce4f5873 | ||
|
a6b0250b86 | ||
|
05f8ab8f8b | ||
|
1627dacd50 | ||
|
4806421ba5 | ||
|
926272067a | ||
|
77db3adbe9 | ||
|
20b6db2202 | ||
|
ee3a1b4d4e | ||
|
9dd77af81a | ||
|
7f00754571 | ||
|
d90aadb67a | ||
|
bb13a10d5c | ||
|
b77e7922bb | ||
|
432ab10e7f | ||
|
094d19cb34 | ||
|
4936f64c0b | ||
|
30a42a6fcd | ||
|
9a38f039f4 | ||
|
e8c746208c | ||
|
bc7731a39d | ||
|
03b6c62239 | ||
|
9d253d2c70 | ||
|
8f8083e835 | ||
|
7f0786dd7a | ||
|
15ccdfa3c0 | ||
|
fe3d0b23bd | ||
|
083ca6a8fb | ||
|
0a32c2ed89 | ||
|
0987e92676 | ||
|
bbf59bd311 | ||
|
aa579c4e32 | ||
|
666df6b7a9 | ||
|
4df08e0661 | ||
|
5c61854ba4 | ||
|
f852f2f97c | ||
|
306a54a5b4 | ||
|
979cb9aa5c | ||
|
3ddbd1bc85 | ||
|
8cec47ef81 | ||
|
944725d5e8 | ||
|
1aa215676d | ||
|
077bacbc27 | ||
|
8b8c0fd79b | ||
|
a40ce0d428 | ||
|
ec7116fbf1 | ||
|
2f8abd0e11 | ||
|
9c75701182 | ||
|
2dc0c4ab21 | ||
|
a2c269dbd4 | ||
|
3c7781d945 | ||
|
7071cf7b72 | ||
|
f24a3239a3 | ||
|
557621aa83 | ||
|
14b222a138 | ||
|
2e051f6690 | ||
|
203c532f87 | ||
|
5c0d4124bc | ||
|
cc406fce17 | ||
|
dc4a90a5ce | ||
|
994ffcafd0 | ||
|
b1e89e92e3 | ||
|
c499a6b11f | ||
|
98e0ac340f | ||
|
f6bc266e1d | ||
|
5b11d316d0 | ||
|
d2404f679b | ||
|
dd03d44d2a | ||
|
19005f948d | ||
|
6a1d2757f0 | ||
|
168b090502 | ||
|
4b1e94fcdf | ||
|
be55cd7b0e | ||
|
58bfcef222 | ||
|
0c41630b29 | ||
|
4fbc0fd5e7 | ||
|
074fefc59e | ||
|
1816476fbb | ||
|
28a149b924 | ||
|
cca7b57dae | ||
|
87eb58d49f | ||
|
317957c26f | ||
|
b14443c009 | ||
|
3acfdc8ccc | ||
|
6b60712943 | ||
|
6a1b4bee8c | ||
|
85093617d1 | ||
|
8484db6968 | ||
|
8a9f00169e | ||
|
16ea522ee9 | ||
|
39eb6a3e25 | ||
|
ae15e4e897 | ||
|
cb64522757 | ||
|
99fff495f6 | ||
|
e21ca56092 | ||
|
6642bcb430 | ||
|
2a58771a17 | ||
|
3d970f51ea | ||
|
e487c80315 | ||
|
4f4d21d9dc | ||
|
7b01e59622 | ||
|
88b871af25 | ||
|
c3f1bda881 | ||
|
2e7711486b | ||
|
486c1546fb | ||
|
ce46422347 | ||
|
7a189091bb | ||
|
9608ec957d | ||
|
0d9abc4937 | ||
|
e8a7157d45 | ||
|
b43a4b9d06 | ||
|
f55b64a215 | ||
|
e520a25ef0 | ||
|
cd33a753d6 | ||
|
b42d8f9852 | ||
|
181c5f0a2b | ||
|
cc60d3b129 | ||
|
9ae54bd779 | ||
|
a637ca714b | ||
|
26f2908b39 | ||
|
ff88de0395 | ||
|
943ea3b93e | ||
|
fe8845631d | ||
|
aa3f8430a8 | ||
|
e1d42c22c1 | ||
|
58083bb6a9 | ||
|
87110574b6 | ||
|
4a34374fd8 | ||
|
494def7d3a | ||
|
e4b6c2e7cd | ||
|
d180320b19 | ||
|
38fa8f2f6e | ||
|
c9eb954de8 | ||
|
0b05115733 | ||
|
67b4b2502f | ||
|
6ba8015502 | ||
|
e0bcecfac0 | ||
|
5dbbdce43c | ||
|
4ef8049413 | ||
|
3dbe00d2a1 | ||
|
929690e730 | ||
|
8f49d29bdb | ||
|
5264ecf506 | ||
|
8b236c9c0d | ||
|
60d49fde61 | ||
|
647945debc | ||
|
017e6856f9 | ||
|
5c2a0cf93d | ||
|
aca9ae670f | ||
|
699a1fb8bb | ||
|
6aff4d1709 | ||
|
af8f6e2f25 | ||
|
d4079cdf4c | ||
|
2faf43ef80 | ||
|
760c28ff8c | ||
|
28ffd9f01b | ||
|
c989b80e00 | ||
|
3b2214d6b6 | ||
|
367db9fa40 | ||
|
bcb16f0fc4 | ||
|
f1ec5dfe4f | ||
|
17f64bcc9e | ||
|
a15a646e11 | ||
|
f713f6385d | ||
|
8c6f7db665 | ||
|
fb593231d9 | ||
|
cfb1b5c38b | ||
|
ecbf0f7fc7 | ||
|
0f85c093f9 | ||
|
c087dc1418 | ||
|
b37b6775d1 | ||
|
0da044671f | ||
|
dd81a1bbad | ||
|
3b35997b2f | ||
|
ee22a08b2e | ||
|
0ef3df76f9 | ||
|
005d802a99 | ||
|
7c3343a431 | ||
|
333c71ad09 | ||
|
edbeeffd60 | ||
|
725f364186 | ||
|
af88a17950 | ||
|
80856c4364 | ||
|
72470acc3d | ||
|
50aa22f3ff | ||
|
937e851d11 | ||
|
e41d0ea64a | ||
|
569809f072 | ||
|
8cbeb2c531 | ||
|
b17019af08 | ||
|
3919fcf6c5 | ||
|
e41efe54fe | ||
|
5fccfb1db6 | ||
|
596a0d8ee1 | ||
|
03c0e12181 | ||
|
8daaf2a248 | ||
|
1dc4e65073 | ||
|
35b210cbd1 | ||
|
5676bc4f8c | ||
|
52de9080e4 | ||
|
26177327a5 | ||
|
2b86f16b54 | ||
|
b234599172 | ||
|
e339a48c38 | ||
|
6b52a9f24e | ||
|
29771cace1 | ||
|
28685f679a | ||
|
b0cf543d44 | ||
|
db390583d7 | ||
|
c140b560fe | ||
|
c51597fde3 | ||
|
7ec824ea01 | ||
|
bce106217a | ||
|
adf09a1a25 | ||
|
529bbed42f | ||
|
c94987d88c | ||
|
29f6775af6 | ||
|
c7090b5301 | ||
|
c407929a03 | ||
|
cd21c54f0b | ||
|
88d6026335 | ||
|
374d59f2cb | ||
|
850ce4f0aa | ||
|
ccd8e7fdf5 | ||
|
3f93b34f73 | ||
|
53b9b9368b | ||
|
ecf592fee0 | ||
|
ef8c2efaa9 | ||
|
5555005de0 | ||
|
d728ee8590 | ||
|
907d7d5a12 | ||
|
682faea308 | ||
|
68572e4f24 | ||
|
23f1c58742 | ||
|
c01290811e | ||
|
f40f96f840 | ||
|
f70e26c8ad | ||
|
9060ff1a55 | ||
|
2c02bed5d0 | ||
|
59e2f25802 | ||
|
184dc90544 | ||
|
46fa98fd8c | ||
|
92ae705dfb | ||
|
4442ca0caa | ||
|
d04bbe5d9a | ||
|
ce28ad271b | ||
|
93a09b1b4f | ||
|
c98121edd6 | ||
|
24c98c5830 | ||
|
ca70217b62 | ||
|
36f34f305c | ||
|
8646ea43d8 | ||
|
cabc4e901b | ||
|
e98eb96a2d | ||
|
cbeb197347 | ||
|
0533073e1d | ||
|
cea4dcffc0 | ||
|
f368b3ea8e | ||
|
c6ddeb3c58 | ||
|
b5de6c7e43 | ||
|
b70b914009 | ||
|
2faa8512e6 | ||
|
b24e6b225f | ||
|
581e5be904 | ||
|
85e8a6923d | ||
|
6e1e457e60 | ||
|
bc8d6fa30d | ||
|
3b140c140c | ||
|
b3b3d6836d | ||
|
bd9fe555b6 | ||
|
02568345b6 | ||
|
0c6318c655 | ||
|
ad9fd519ec | ||
|
650b1e253c | ||
|
8a953f28c5 | ||
|
b678afe668 | ||
|
233dee5bae | ||
|
f5530d316b | ||
|
8378c368d5 | ||
|
4ac7a56379 | ||
|
21e531c82c | ||
|
62e568265e | ||
|
9c547f90b6 | ||
|
7aaec28ebd | ||
|
03f3617c50 | ||
|
606241c499 | ||
|
de5faec9ec | ||
|
ee3f782d91 | ||
|
2631765972 | ||
|
09c639c28f | ||
|
822180e533 | ||
|
f0526ff278 | ||
|
43de915410 | ||
|
5da6a5e200 | ||
|
abbc5479bc | ||
|
50e252e840 | ||
|
fa903f1793 | ||
|
c9c4ec28c3 | ||
|
c91ab8617c | ||
|
65a8095e27 | ||
|
b7d1dad0da | ||
|
f877de7e98 | ||
|
7dafd633c2 | ||
|
13bcef8672 | ||
|
f341b01fea | ||
|
85f53ffae9 | ||
|
05334f1b5e | ||
|
72c111795f | ||
|
8205806be4 | ||
|
951050e000 | ||
|
9b002ed595 | ||
|
076ff7e1b5 | ||
|
bf5d2962b5 | ||
|
b508da1643 | ||
|
6d3ef658e1 | ||
|
eb78bf26e7 | ||
|
8538266928 | ||
|
87fb3497b4 | ||
|
e2a8053e94 | ||
|
9ace22003c | ||
|
748f284bcc | ||
|
03134ed774 | ||
|
549e64422e | ||
|
8d56f06b72 | ||
|
f82039aeda | ||
|
2bf5dd60d8 | ||
|
9cb46add24 | ||
|
8564f7a9f5 | ||
|
723299aac9 | ||
|
a3776e9d6c | ||
|
21c623e8e8 | ||
|
fb2c4cbea9 | ||
|
f6c4791f0c | ||
|
fc7ef9226b | ||
|
64c24dca55 | ||
|
7d21f9f19e | ||
|
27b072ec5b |
46
.codesandbox/ci.json
Normal file
46
.codesandbox/ci.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"packages": [
|
||||
"packages/dockview-core",
|
||||
"packages/dockview-vue",
|
||||
"packages/dockview-react",
|
||||
"packages/dockview"
|
||||
],
|
||||
"sandboxes": [
|
||||
"/packages/docs/sandboxes/constraints-dockview",
|
||||
"/packages/docs/sandboxes/customheader-dockview",
|
||||
"/packages/docs/sandboxes/demo-dockview",
|
||||
"/packages/docs/sandboxes/dnd-dockview",
|
||||
"/packages/docs/sandboxes/dockview-app",
|
||||
"/packages/docs/sandboxes/editor-gridview",
|
||||
"/packages/docs/sandboxes/events-dockview",
|
||||
"/packages/docs/sandboxes/externaldnd-dockview",
|
||||
"/packages/docs/sandboxes/floatinggroup-dockview",
|
||||
"/packages/docs/sandboxes/fullwidthtab-dockview",
|
||||
"/packages/docs/sandboxes/headeractions-dockview",
|
||||
"/packages/docs/sandboxes/groupcontol-dockview",
|
||||
"/packages/docs/sandboxes/iframe-dockview",
|
||||
"/packages/docs/sandboxes/keyboard-dockview",
|
||||
"/packages/docs/sandboxes/layout-dockview",
|
||||
"/packages/docs/sandboxes/lockedgroup-dockview",
|
||||
"/packages/docs/sandboxes/maximizegroup-dockview",
|
||||
"/packages/docs/sandboxes/nativeapp-dockview",
|
||||
"/packages/docs/sandboxes/nested-dockview",
|
||||
"/packages/docs/sandboxes/popoutgroup-dockview",
|
||||
"/packages/docs/sandboxes/rendering-dockview",
|
||||
"/packages/docs/sandboxes/rendermode-dockview",
|
||||
"/packages/docs/sandboxes/resize-dockview",
|
||||
"/packages/docs/sandboxes/resizecontainer-dockview",
|
||||
"/packages/docs/sandboxes/scrollbars-dockview",
|
||||
"/packages/docs/sandboxes/simple-dockview",
|
||||
"/packages/docs/sandboxes/simple-gridview",
|
||||
"/packages/docs/sandboxes/simple-paneview",
|
||||
"/packages/docs/sandboxes/tabheight-dockview",
|
||||
"/packages/docs/sandboxes/updatetitle-dockview",
|
||||
"/packages/docs/sandboxes/watermark-dockview",
|
||||
"/packages/docs/sandboxes/javascript/fullwidthtab-dockview",
|
||||
"/packages/docs/sandboxes/javascript/simple-dockview",
|
||||
"/packages/docs/sandboxes/javascript/tabheight-dockview",
|
||||
"/packages/docs/sandboxes/javascript/vanilla-dockview"
|
||||
],
|
||||
"node": "18"
|
||||
}
|
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
quote_type = single
|
21
.eslintrc
21
.eslintrc
@ -1,21 +0,0 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-case-declarations":"off",
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-non-null-assertion":"off"
|
||||
}
|
||||
}
|
19
.eslintrc.js
Normal file
19
.eslintrc.js
Normal file
@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
|
||||
rules: {
|
||||
'no-case-declarations': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
};
|
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
[dockview.dev](https://dockview.dev) provides a number of examples with Code Sandbox templates. Are you able to produce the bug by forking one of those templates? Sharing a link to the forked sandbox with the bug would be extremely helpful.
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
90
.github/workflows/codeql-analysis.yml
vendored
90
.github/workflows/codeql-analysis.yml
vendored
@ -3,60 +3,60 @@
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
name: "CodeQL"
|
||||
name: 'CodeQL'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 4 * * 1'
|
||||
# push:
|
||||
# branches: [master]
|
||||
# pull_request:
|
||||
# # The branches below must be a subset of the branches above
|
||||
# branches: [master]
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['javascript']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['javascript']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
46
.github/workflows/deploy-docs.yml
vendored
Normal file
46
.github/workflows/deploy-docs.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy-nightly-demo-app:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: yarn install
|
||||
- run: npm run build
|
||||
working-directory: packages/dockview-core
|
||||
- run: npm run build
|
||||
working-directory: packages/dockview
|
||||
- run: npm run build
|
||||
working-directory: packages/dockview-vue
|
||||
- run: npm run build
|
||||
working-directory: packages/dockview-react
|
||||
- run: npm run build
|
||||
working-directory: packages/docs
|
||||
- run: npm run docs
|
||||
working-directory: .
|
||||
- run: npm run package-docs
|
||||
working-directory: .
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: gh-pages # The branch the action should deploy to.
|
||||
FOLDER: build # The folder the action should deploy.
|
||||
TARGET_FOLDER: .
|
||||
CLEAN: true # Automatically remove deleted files from the deploy branch
|
40
.github/workflows/deploy_nightly.yml
vendored
40
.github/workflows/deploy_nightly.yml
vendored
@ -1,40 +0,0 @@
|
||||
name: Deploy Nightly Demo App
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *' # every day at 3 am UTC
|
||||
|
||||
jobs:
|
||||
deploy-nightly-demo-app:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npm ci
|
||||
- run: npm run bootstrap-no-hoist
|
||||
- run: npm run build
|
||||
- run: npm run test
|
||||
- run: npm run package-all
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: gh-pages # The branch the action should deploy to.
|
||||
FOLDER: output # The folder the action should deploy.
|
||||
TARGET_FOLDER: output
|
||||
CLEAN: true # Automatically remove deleted files from the deploy branch
|
34
.github/workflows/main.yml
vendored
34
.github/workflows/main.yml
vendored
@ -7,25 +7,27 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
# might be required for sonar to work correctly
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '12.x'
|
||||
node-version: '20.x'
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npm ci
|
||||
- run: npm run bootstrap
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: yarn
|
||||
- run: npm run build
|
||||
- run: npm run test-cov
|
||||
- run: npm run codecov-publish
|
||||
- run: npm run test:cov
|
||||
- name: SonarCloud Scan
|
||||
uses: sonarsource/sonarqube-scan-action@v5
|
||||
env:
|
||||
CI: true
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
78
.github/workflows/publish.yml
vendored
Normal file
78
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
name: Publish to npm
|
||||
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
publish:
|
||||
if: github.event_name == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: yarn
|
||||
- name: Publish dockview-core
|
||||
run: npm publish --provenance
|
||||
working-directory: packages/dockview-core
|
||||
- name: Publish dockview
|
||||
run: npm publish --provenance
|
||||
working-directory: packages/dockview
|
||||
- name: Publish dockview-vue
|
||||
run: npm publish --provenance
|
||||
working-directory: packages/dockview-vue
|
||||
- name: Publish dockview-react
|
||||
run: npm publish --provenance
|
||||
working-directory: packages/dockview-react
|
||||
publish-experimental:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: yarn
|
||||
- run: npm run set-experimental-versions
|
||||
- name: Publish dockview-core
|
||||
run: npm publish --provenance --tag experimental
|
||||
working-directory: packages/dockview-core
|
||||
- name: Publish dockview
|
||||
run: npm publish --provenance --tag experimental
|
||||
working-directory: packages/dockview
|
||||
- name: Publish dockview-vue
|
||||
run: npm publish --provenance --tag experimental
|
||||
working-directory: packages/dockview-vue
|
||||
- name: Publish dockview-react
|
||||
run: npm publish --provenance --tag experimental
|
||||
working-directory: packages/dockview-react
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -7,4 +7,11 @@ typedocs/
|
||||
.DS_Store
|
||||
*-debug.log
|
||||
.build
|
||||
storybook-static/
|
||||
storybook-static/
|
||||
.rollup.cache/
|
||||
test-report.xml
|
||||
*.code-workspace
|
||||
yarn-error.log
|
||||
/build
|
||||
/docs/
|
||||
/generated/
|
||||
|
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@ -3,7 +3,13 @@
|
||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
||||
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": ["esbenp.prettier-vscode", "redhat.vscode-yaml"],
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"redhat.vscode-yaml",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"vue.volar"
|
||||
],
|
||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 mathuo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
103
README.md
103
README.md
@ -1,97 +1,38 @@
|
||||
<div align="center">
|
||||
<h1>dockview</h1>
|
||||
|
||||
<p>Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support</p>
|
||||
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews. Supports React, Vue and Vanilla TypeScript</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||

|
||||
[](https://www.npmjs.com/package/dockview)
|
||||
[](https://www.npmjs.com/package/dockview-core)
|
||||
[](https://www.npmjs.com/package/dockview-core)
|
||||
[](https://github.com/mathuo/dockview/actions?query=workflow%3ACI)
|
||||
[](https://codecov.io/gh/mathuo/dockview/branch/master)
|
||||
[](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
|
||||
[](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
|
||||
[](https://bundlephobia.com/result?p=dockview-core)
|
||||
|
||||
##
|
||||
|
||||
A zero dependency layout manager based on the layering of split-view components with ReactJS support.
|
||||
- View the live demo [here](https://mathuo.github.io/dockview/).
|
||||
- Storybook demo [here](https://mathuo.github.io/dockview/output/storybook-static).
|
||||
- Automatically generated TypeDocs can be found [here](https://mathuo.github.io/dockview/output/docs/index.html).
|
||||

|
||||
|
||||
## Installation
|
||||
You can install the project from [npm](https://www.npmjs.com/package/dockview). The project comes with TypeScript typings.
|
||||
Please see the website: https://dockview.dev
|
||||
|
||||
```bash
|
||||
npm install --save dockview
|
||||
```
|
||||
## Features
|
||||
|
||||
## Configuration
|
||||
- Serialization / deserialization with full layout management
|
||||
- Support for split-views, grid-views and 'dockable' views
|
||||
- Themeable and customizable
|
||||
- Tab and Group docking / Drag n' Drop
|
||||
- Popout Windows
|
||||
- Floating Groups
|
||||
- Extensive API
|
||||
- Supports Shadow DOMs
|
||||
- High test coverage
|
||||
- Documentation website with live examples
|
||||
- Transparent builds and Code Analysis
|
||||
- Security at mind - verifed publishing and builds through GitHub Actions
|
||||
|
||||
You must import the core css stylesheet but you are free to supply your own theming in addition to the core stylesheet. The location to reference for the stylesheet is
|
||||
|
||||
```
|
||||
dockview/dist/styles.css
|
||||
```
|
||||
|
||||
By default the seperator between panels is `transparent` but this can be set through the CSS varibable `--separator-border`. Alternatively, or if you require the `DockviewReact` you should attach the classname of an included theme; either `dockview-theme-dark` or `dockview-theme-light`.
|
||||
|
||||
|
||||
### Sandbox examples
|
||||
- [Dockview](https://codesandbox.io/s/simple-dockview-t6491)
|
||||
- [Gridview](https://codesandbox.io/s/simple-gridview-jrp0n)
|
||||
- [Splitview](https://codesandbox.io/s/simple-splitview-l53nn)
|
||||
- [Paneview](https://codesandbox.io/s/simple-paneview-v8qvb)
|
||||
|
||||
## React
|
||||
|
||||
### Splitview
|
||||
|
||||
```javascript
|
||||
import {
|
||||
ISplitviewPanelProps,
|
||||
Orientation,
|
||||
SplitviewReact,
|
||||
SplitviewReadyEvent
|
||||
} from "dockview";
|
||||
|
||||
const components = {
|
||||
"my-component": (props: ISplitviewPanelProps) => {
|
||||
return (
|
||||
<div>
|
||||
<span>This is a panel</span>
|
||||
<span>{props.arbitraryProp}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Example = () => {
|
||||
const onReady = (event: SplitviewReadyEvent) => {
|
||||
event.addPanel({
|
||||
id: "panel-1",
|
||||
component: "my-component",
|
||||
params: {
|
||||
arbitraryProp: "Hello World"
|
||||
}
|
||||
});
|
||||
event.addPanel({
|
||||
id: "panel-2",
|
||||
component: "my-component",
|
||||
params: {
|
||||
arbitraryProp: "World Hello"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SplitviewReact
|
||||
components={components}
|
||||
onReady={onReady}
|
||||
orientation={Orientation.VERTICAL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Run the demo locally
|
||||
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#user-content-provenance).
|
||||
|
8
SECURITY.md
Normal file
8
SECURITY.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
- Dockview is an entirely open source project.
|
||||
- All build and publication scripts use public Github Action files found [here](https://github.com/mathuo/dockview/tree/master/.github/workflows).
|
||||
- All npm publications are verified through the use of [provenance statements](https://docs.npmjs.com/generating-provenance-statements/).
|
||||
- All builds are scanned with SonarCube and outputs can be found [here](https://sonarcloud.io/summary/overall?id=mathuo_dockview).
|
||||
|
||||
If you believe you have found a security or vulnerability issue please send a complete example to github.mathuo@gmail.com where it will be investigated.
|
1
jest-setup.ts
Normal file
1
jest-setup.ts
Normal file
@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
@ -1,20 +0,0 @@
|
||||
const {join, normalize} = require("path");
|
||||
|
||||
const tsconfig = normalize(join(__dirname, "tsconfig.test.json"))
|
||||
|
||||
module.exports = {
|
||||
displayName: { name: "root" },
|
||||
preset: "ts-jest",
|
||||
projects: ["<rootDir>/packages/*/jest.config.js"],
|
||||
transform: {
|
||||
"^.+\\.tsx?$":"ts-jest"
|
||||
},
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsconfig,
|
||||
experimental: true,
|
||||
compilerHost: true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
const baseConfig = require("./jest.config.base");
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
displayName: { name: "root", color: "blue" },
|
||||
projects: ["<rootDir>/packages/*/jest.config.js"],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom:[
|
||||
"<rootDir>/packages/*/src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
coveragePathIgnorePatterns: [
|
||||
"/node_modules/",
|
||||
"<rootDir>packages/*/src/__tests__/",
|
||||
],
|
||||
coverageDirectory: "coverage"
|
||||
};
|
17
jest.config.ts
Normal file
17
jest.config.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { JestConfigWithTsJest } from 'ts-jest';
|
||||
|
||||
const config: JestConfigWithTsJest = {
|
||||
preset: 'ts-jest',
|
||||
displayName: { name: 'root', color: 'blue' },
|
||||
projects: ['<rootDir>/packages/*/jest.config.ts'],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['<rootDir>/packages/*/src/**/*.{js,jsx,ts,tsx}'],
|
||||
coveragePathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'<rootDir>/packages/*/src/__tests__/',
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
testResultsProcessor: 'jest-sonar-reporter',
|
||||
};
|
||||
|
||||
export default config;
|
16
lerna.json
16
lerna.json
@ -1,6 +1,12 @@
|
||||
{
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.0.5"
|
||||
}
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "4.2.4",
|
||||
"npmClient": "yarn",
|
||||
"command": {
|
||||
"publish": {
|
||||
"message": "chore(release): publish %s"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "es6",
|
||||
"declaration": true,
|
||||
"target": "es5",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"downlevelIteration": true,
|
||||
"incremental": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitAny": true,
|
||||
"allowUnreachableCode": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
// "strict": true,
|
||||
"strictBindCallApply": true, // pass
|
||||
"alwaysStrict": true, // pass
|
||||
"noImplicitThis": true, // pass
|
||||
"strictFunctionTypes": true, // pass
|
||||
"strictNullChecks": false,
|
||||
"strictPropertyInitialization": false,
|
||||
"lib": [
|
||||
"ES2015",
|
||||
"ES2016.Array.Include",
|
||||
"ES2017.String",
|
||||
"ES2018.Promise",
|
||||
"DOM",
|
||||
]
|
||||
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"target": "es5",
|
||||
"esModuleInterop": true,
|
||||
"downlevelIteration": true,
|
||||
"incremental": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitAny": true,
|
||||
"allowUnreachableCode": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
// "strict": true,
|
||||
"strictBindCallApply": true, // pass
|
||||
"alwaysStrict": true, // pass
|
||||
"noImplicitThis": true, // pass
|
||||
"strictFunctionTypes": true, // pass
|
||||
"strictNullChecks": false,
|
||||
"strictPropertyInitialization": false,
|
||||
"lib": [
|
||||
"ES2015",
|
||||
"ES2016.Array.Include",
|
||||
"ES2017.String",
|
||||
"ES2018.Promise",
|
||||
"DOM",
|
||||
]
|
||||
}
|
||||
}
|
18027
package-lock.json
generated
18027
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
138
package.json
138
package.json
@ -1,61 +1,81 @@
|
||||
{
|
||||
"name": "splitview-root",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"lint": "eslint packages/**/src/** --ext .ts,.tsx,.js,.jsx",
|
||||
"package": "node scripts/package.js",
|
||||
"package-all": "npm run build-demo && npm run docs && node scripts/package.js",
|
||||
"build": "lerna run build --ignore splitview-demo",
|
||||
"build-demo": "lerna run build --scope splitview-demo",
|
||||
"docs": "lerna run docs --scope dockview",
|
||||
"clean": "lerna run clean",
|
||||
"bootstrap": "lerna bootstrap --hoist",
|
||||
"bootstrap-no-hoist": "lerna bootstrap",
|
||||
"test-cov": "jest --coverage",
|
||||
"codecov-publish": "codecov",
|
||||
"version-beta-build": "lerna version prerelease --preid beta",
|
||||
"publish-app": "lerna publish"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mathuo/dockview.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mathuo/dockview/issues"
|
||||
},
|
||||
"homepage": "https://github.com/mathuo/dockview#readme",
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^7.28.1",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@typescript-eslint/eslint-plugin": "^4.8.1",
|
||||
"@typescript-eslint/parser": "^4.8.1",
|
||||
"codecov": "^3.8.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^3.6.0",
|
||||
"eslint": "^7.14.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-sass": "^4.1.0",
|
||||
"jest": "^26.6.3",
|
||||
"jsdom": "^16.4.0",
|
||||
"lerna": "^3.22.1",
|
||||
"merge2": "^1.4.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass-loader": "^8.0.2",
|
||||
"style-loader": "^1.3.0",
|
||||
"ts-jest": "^26.4.4",
|
||||
"ts-loader": "^7.0.5",
|
||||
"typescript": "^4.1.2",
|
||||
"webpack": "^5.6.0",
|
||||
"webpack-cli": "^4.2.0",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
"name": "dockview-monorepo-root",
|
||||
"private": true,
|
||||
"description": "Monorepo for https://github.com/mathuo/dockview",
|
||||
"homepage": "https://github.com/mathuo/dockview#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mathuo/dockview/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mathuo/dockview.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "https://github.com/mathuo",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "lerna run build --scope '{dockview-core,dockview,dockview-vue,dockview-react}'",
|
||||
"clean": "lerna run clean",
|
||||
"docs": "typedoc",
|
||||
"generate-docs": "node scripts/docs.mjs",
|
||||
"lint": "eslint packages/**/src/** --ext .ts,.tsx,.js,.jsx",
|
||||
"package": "node scripts/package.js",
|
||||
"package-docs": "node scripts/package-docs.js",
|
||||
"set-experimental-versions": "node scripts/set-experimental-versions",
|
||||
"test": "jest",
|
||||
"test:cov": "jest --coverage",
|
||||
"version": "lerna version"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^11.1.5",
|
||||
"@testing-library/dom": "^9.3.3",
|
||||
"@testing-library/jest-dom": "^6.1.6",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@total-typescript/shoehorn": "^0.1.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-dart-sass": "^1.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdom": "^23.0.1",
|
||||
"lerna": "^8.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"rollup": "^4.9.2",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.6.2",
|
||||
"typedoc": "^0.25.6",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.5",
|
||||
"vue": "^3.4.21",
|
||||
"vue-sfc-loader": "^0.1.0",
|
||||
"vue-tsc": "^2.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
}
|
||||
|
18
packages/README.md
Normal file
18
packages/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Project Structure
|
||||
|
||||
This mono-repository has a number of packages containing the code for the [dockview](https://www.npmjs.com/package/dockview) library and the documentation website [dockview.dev](dockview.dev).
|
||||
|
||||
## dockview-core
|
||||
|
||||
- Contains the core logic for the dockview library.
|
||||
- Written entirely in vanilla JavaScript/TypeScript.
|
||||
|
||||
## dockview
|
||||
|
||||
- Depends on `dockview-core`.
|
||||
- Exports a `React` wrapper.
|
||||
- Published as [dockview](https://www.npmjs.com/package/dockview) on npm.
|
||||
|
||||
## docs
|
||||
|
||||
- Code for [dockview.dev](dockview.dev).
|
56
packages/dockview-angular/README.md
Normal file
56
packages/dockview-angular/README.md
Normal file
@ -0,0 +1,56 @@
|
||||
<div align="center">
|
||||
<h1>dockview</h1>
|
||||
|
||||
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews with ReactJS support written in TypeScript</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
[](https://www.npmjs.com/package/dockview)
|
||||
[](https://www.npmjs.com/package/dockview)
|
||||
[](https://github.com/mathuo/dockview/actions?query=workflow%3ACI)
|
||||
[](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
|
||||
[](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
|
||||
[](https://bundlephobia.com/result?p=dockview)
|
||||
|
||||
##
|
||||
|
||||
Please see the website: https://dockview.dev
|
||||
|
||||
## Features
|
||||
|
||||
- Serialization / deserialization with full layout management
|
||||
- Support for split-views, grid-views and 'dockable' views
|
||||
- Themeable and customizable
|
||||
- Tab and Group docking / Drag n' Drop
|
||||
- Popout Windows
|
||||
- Floating Groups
|
||||
- Extensive API
|
||||
- Supports Shadow DOMs
|
||||
- High test coverage
|
||||
- Documentation website with live examples
|
||||
- Transparent builds and Code Analysis
|
||||
- Security at mind - verifed publishing and builds through GitHub Actions
|
||||
|
||||
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#Provenance).
|
||||
|
||||
## Quick start
|
||||
|
||||
Dockview has a peer dependency on `react >= 16.8.0` and `react-dom >= 16.8.0`. You can install dockview from [npm](https://www.npmjs.com/package/dockview).
|
||||
|
||||
```
|
||||
npm install --save dockview
|
||||
```
|
||||
|
||||
Within your project you must import or reference the stylesheet at `dockview/dist/styles/dockview.css` and attach a theme.
|
||||
|
||||
```css
|
||||
@import '~dockview/dist/styles/dockview.css';
|
||||
```
|
||||
|
||||
You should also attach a dockview theme to an element containing your components. For example:
|
||||
|
||||
```html
|
||||
<body classname="dockview-theme-dark"></body>
|
||||
```
|
34
packages/dockview-angular/jest.config.ts
Normal file
34
packages/dockview-angular/jest.config.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { JestConfigWithTsJest } from 'ts-jest';
|
||||
|
||||
const config: JestConfigWithTsJest = {
|
||||
preset: 'ts-jest',
|
||||
roots: ['<rootDir>/packages/dockview-angular'],
|
||||
modulePaths: ['<rootDir>/packages/dockview-angular/src'],
|
||||
displayName: { name: 'dockview-angular', color: 'blue' },
|
||||
rootDir: '../../',
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/packages/dockview-angular/src/**/*.{js,jsx,ts,tsx}',
|
||||
],
|
||||
setupFiles: [
|
||||
// '<rootDir>/packages/dockview-angular/src/__tests__/__mocks__/resizeObserver.js',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
|
||||
coveragePathIgnorePatterns: ['/node_modules/'],
|
||||
modulePathIgnorePatterns: [
|
||||
// '<rootDir>/packages/dockview-angular/src/__tests__/__mocks__',
|
||||
// '<rootDir>/packages/dockview-angular/src/__tests__/__test_utils__',
|
||||
],
|
||||
coverageDirectory: '<rootDir>/packages/dockview-angular/coverage/',
|
||||
testResultsProcessor: 'jest-sonar-reporter',
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.test.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
59
packages/dockview-angular/package.json
Normal file
59
packages/dockview-angular/package.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "dockview-angular",
|
||||
"version": "4.2.4",
|
||||
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
|
||||
"keywords": [
|
||||
"splitview",
|
||||
"split-view",
|
||||
"gridview",
|
||||
"grid-view",
|
||||
"dockview",
|
||||
"dock-view",
|
||||
"grid",
|
||||
"tabs",
|
||||
"layout",
|
||||
"layout manager",
|
||||
"dock layout",
|
||||
"dock",
|
||||
"docking",
|
||||
"splitter",
|
||||
"drag-and-drop",
|
||||
"drag",
|
||||
"drop",
|
||||
"react",
|
||||
"react-component"
|
||||
],
|
||||
"homepage": "https://github.com/mathuo/dockview",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mathuo/dockview/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mathuo/dockview.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "https://github.com/mathuo",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/cjs/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build:package && npm run build:bundles",
|
||||
"build:bundles": "rollup -c",
|
||||
"build:cjs": "cross-env ../../node_modules/.bin/tsc --build ./tsconfig.json --verbose --extendedDiagnostics",
|
||||
"build:css": "gulp sass",
|
||||
"build:esm": "cross-env ../../node_modules/.bin/tsc --build ./tsconfig.esm.json --verbose --extendedDiagnostics",
|
||||
"build:package": "npm run build:cjs && npm run build:esm && npm run build:css",
|
||||
"clean": "rimraf dist/ .build/ .rollup.cache/",
|
||||
"prepublishOnly": "npm run rebuild && npm run test",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"test": "cross-env ../../node_modules/.bin/jest --selectProjects dockview",
|
||||
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"dockview-core": "^4.2.4"
|
||||
}
|
||||
}
|
113
packages/dockview-angular/rollup.config.js
Normal file
113
packages/dockview-angular/rollup.config.js
Normal file
@ -0,0 +1,113 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { join } = require('path');
|
||||
const typescript = require('@rollup/plugin-typescript');
|
||||
const terser = require('@rollup/plugin-terser');
|
||||
const postcss = require('rollup-plugin-postcss');
|
||||
const nodeResolve = require('@rollup/plugin-node-resolve');
|
||||
|
||||
const { name, version, homepage, license } = require('./package.json');
|
||||
const main = join(__dirname, './scripts/rollupEntryTarget.ts');
|
||||
const mainNoStyles = join(__dirname, './src/index.ts');
|
||||
const outputDir = join(__dirname, 'dist');
|
||||
|
||||
function outputFile(format, isMinified, withStyles) {
|
||||
let filename = join(outputDir, name);
|
||||
|
||||
if (format !== 'umd') {
|
||||
filename += `.${format}`;
|
||||
}
|
||||
if (isMinified) {
|
||||
filename += '.min';
|
||||
}
|
||||
if (!withStyles) {
|
||||
filename += '.noStyle';
|
||||
}
|
||||
|
||||
return `${filename}.js`;
|
||||
}
|
||||
|
||||
function getInput(options) {
|
||||
const { withStyles } = options;
|
||||
|
||||
if (withStyles) {
|
||||
return main;
|
||||
}
|
||||
|
||||
return mainNoStyles;
|
||||
}
|
||||
|
||||
function createBundle(format, options) {
|
||||
const { withStyles, isMinified } = options;
|
||||
const input = getInput(options);
|
||||
const file = outputFile(format, isMinified, withStyles);
|
||||
|
||||
const external = [];
|
||||
|
||||
const output = {
|
||||
file,
|
||||
format,
|
||||
sourcemap: true,
|
||||
globals: {},
|
||||
banner: [
|
||||
`/**`,
|
||||
` * ${name}`,
|
||||
` * @version ${version}`,
|
||||
` * @link ${homepage}`,
|
||||
` * @license ${license}`,
|
||||
` */`,
|
||||
].join('\n'),
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
nodeResolve({
|
||||
include: ['node_modules/dockview-core/**'],
|
||||
}),
|
||||
typescript({
|
||||
tsconfig: 'tsconfig.esm.json',
|
||||
}),
|
||||
];
|
||||
|
||||
if (isMinified) {
|
||||
plugins.push(terser());
|
||||
}
|
||||
if (withStyles) {
|
||||
plugins.push(postcss());
|
||||
}
|
||||
|
||||
if (format === 'umd') {
|
||||
output['name'] = name;
|
||||
}
|
||||
|
||||
external.push('react', 'react-dom');
|
||||
|
||||
if (format === 'umd') {
|
||||
output.globals['react'] = 'React';
|
||||
output.globals['react-dom'] = 'ReactDOM';
|
||||
}
|
||||
|
||||
return {
|
||||
input,
|
||||
output,
|
||||
plugins,
|
||||
external,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = [
|
||||
// amd
|
||||
createBundle('amd', { withStyles: false, isMinified: false }),
|
||||
createBundle('amd', { withStyles: true, isMinified: false }),
|
||||
createBundle('amd', { withStyles: false, isMinified: true }),
|
||||
createBundle('amd', { withStyles: true, isMinified: true }),
|
||||
// umd
|
||||
createBundle('umd', { withStyles: false, isMinified: false }),
|
||||
createBundle('umd', { withStyles: true, isMinified: false }),
|
||||
createBundle('umd', { withStyles: false, isMinified: true }),
|
||||
createBundle('umd', { withStyles: true, isMinified: true }),
|
||||
// cjs
|
||||
createBundle('cjs', { withStyles: true, isMinified: false }),
|
||||
// esm
|
||||
createBundle('esm', { withStyles: true, isMinified: false }),
|
||||
createBundle('esm', { withStyles: true, isMinified: true }),
|
||||
];
|
2
packages/dockview-angular/scripts/rollupEntryTarget.ts
Normal file
2
packages/dockview-angular/scripts/rollupEntryTarget.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import '../dist/styles/dockview.css';
|
||||
export * from '../src/index';
|
5
packages/dockview-angular/src/__tests__/empty.spec.ts
Normal file
5
packages/dockview-angular/src/__tests__/empty.spec.ts
Normal file
@ -0,0 +1,5 @@
|
||||
describe('empty', () => {
|
||||
test('that passes', () => {
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
});
|
1
packages/dockview-angular/src/index.ts
Normal file
1
packages/dockview-angular/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from 'dockview-core';
|
14
packages/dockview-angular/tsconfig.esm.json
Normal file
14
packages/dockview-angular/tsconfig.esm.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"outDir": "dist/esm",
|
||||
"tsBuildInfoFile": ".build/tsconfig.tsbuildinfo.esm",
|
||||
"jsx": "react",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["**/node_modules", "src/__tests__"]
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../module-build/tsconfig.json",
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/cjs",
|
||||
"tsBuildInfoFile": ".build/tsconfig.tsbuildinfo.cjs",
|
||||
@ -8,4 +8,4 @@
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["**/node_modules", "src/__tests__"]
|
||||
}
|
||||
}
|
5
packages/dockview-angular/typedoc.json
Normal file
5
packages/dockview-angular/typedoc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": ["../../typedoc.base.json"],
|
||||
"entryPoints": ["src/index.ts"],
|
||||
"exclude": ["**/dist/**"]
|
||||
}
|
38
packages/dockview-core/README.md
Normal file
38
packages/dockview-core/README.md
Normal file
@ -0,0 +1,38 @@
|
||||
<div align="center">
|
||||
<h1>dockview</h1>
|
||||
|
||||
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews. Supports React, Vue and Vanilla TypeScript</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
[](https://www.npmjs.com/package/dockview)
|
||||
[](https://www.npmjs.com/package/dockview)
|
||||
[](https://github.com/mathuo/dockview/actions?query=workflow%3ACI)
|
||||
[](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
|
||||
[](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
|
||||
[](https://bundlephobia.com/result?p=dockview)
|
||||
|
||||
##
|
||||
|
||||

|
||||
|
||||
Please see the website: https://dockview.dev
|
||||
|
||||
## Features
|
||||
|
||||
- Serialization / deserialization with full layout management
|
||||
- Support for split-views, grid-views and 'dockable' views
|
||||
- Themeable and customizable
|
||||
- Tab and Group docking / Drag n' Drop
|
||||
- Popout Windows
|
||||
- Floating Groups
|
||||
- Extensive API
|
||||
- Supports Shadow DOMs
|
||||
- High test coverage
|
||||
- Documentation website with live examples
|
||||
- Transparent builds and Code Analysis
|
||||
- Security at mind - verifed publishing and builds through GitHub Actions
|
||||
|
||||
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#Provenance).
|
13
packages/dockview-core/gulpfile.js
Normal file
13
packages/dockview-core/gulpfile.js
Normal file
@ -0,0 +1,13 @@
|
||||
const gulp = require('gulp');
|
||||
const gulpSass = require('gulp-dart-sass');
|
||||
const concat = require('gulp-concat');
|
||||
|
||||
gulp.task('sass', () => {
|
||||
return gulp
|
||||
.src('./src/**/*.scss')
|
||||
.pipe(gulpSass().on('error', gulpSass.logError))
|
||||
.pipe(concat('dockview.css'))
|
||||
.pipe(gulp.dest('./dist/styles/'));
|
||||
});
|
||||
|
||||
gulp.task('run', gulp.series(['sass']));
|
34
packages/dockview-core/jest.config.ts
Normal file
34
packages/dockview-core/jest.config.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { JestConfigWithTsJest } from 'ts-jest';
|
||||
|
||||
const config: JestConfigWithTsJest = {
|
||||
preset: 'ts-jest',
|
||||
roots: ['<rootDir>/packages/dockview-core'],
|
||||
modulePaths: ['<rootDir>/packages/dockview-core/src'],
|
||||
displayName: { name: 'dockview-core', color: 'blue' },
|
||||
rootDir: '../../',
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/packages/dockview-core/src/**/*.{js,jsx,ts,tsx}',
|
||||
],
|
||||
setupFiles: [
|
||||
'<rootDir>/packages/dockview-core/src/__tests__/__mocks__/resizeObserver.js',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
|
||||
coveragePathIgnorePatterns: ['/node_modules/'],
|
||||
modulePathIgnorePatterns: [
|
||||
'<rootDir>/packages/dockview-core/src/__tests__/__mocks__',
|
||||
'<rootDir>/packages/dockview-core/src/__tests__/__test_utils__',
|
||||
],
|
||||
coverageDirectory: '<rootDir>/packages/dockview-core/coverage/',
|
||||
testResultsProcessor: 'jest-sonar-reporter',
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.test.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
56
packages/dockview-core/package.json
Normal file
56
packages/dockview-core/package.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "dockview-core",
|
||||
"version": "4.2.4",
|
||||
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
|
||||
"keywords": [
|
||||
"splitview",
|
||||
"split-view",
|
||||
"gridview",
|
||||
"grid-view",
|
||||
"dockview",
|
||||
"dock-view",
|
||||
"grid",
|
||||
"tabs",
|
||||
"layout",
|
||||
"layout manager",
|
||||
"dock layout",
|
||||
"dock",
|
||||
"docking",
|
||||
"splitter",
|
||||
"drag-and-drop",
|
||||
"drag",
|
||||
"drop",
|
||||
"react",
|
||||
"react-component"
|
||||
],
|
||||
"homepage": "https://github.com/mathuo/dockview",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mathuo/dockview/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mathuo/dockview.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "https://github.com/mathuo",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/cjs/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build:package && npm run build:bundles",
|
||||
"build:bundles": "rollup -c",
|
||||
"build:cjs": "cross-env ../../node_modules/.bin/tsc --build ./tsconfig.json --verbose --extendedDiagnostics",
|
||||
"build:css": "gulp sass",
|
||||
"build:esm": "cross-env ../../node_modules/.bin/tsc --build ./tsconfig.esm.json --verbose --extendedDiagnostics",
|
||||
"build:package": "npm run build:cjs && npm run build:esm && npm run build:css",
|
||||
"clean": "rimraf dist/ .build/ .rollup.cache/",
|
||||
"prepublishOnly": "npm run rebuild && npm run test",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"test": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-core",
|
||||
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-core --coverage"
|
||||
}
|
||||
}
|
102
packages/dockview-core/rollup.config.js
Normal file
102
packages/dockview-core/rollup.config.js
Normal file
@ -0,0 +1,102 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { join } = require('path');
|
||||
const typescript = require('@rollup/plugin-typescript');
|
||||
const terser = require('@rollup/plugin-terser');
|
||||
const postcss = require('rollup-plugin-postcss');
|
||||
|
||||
const { name, version, homepage, license } = require('./package.json');
|
||||
const main = join(__dirname, './scripts/rollupEntryTarget.ts');
|
||||
const mainNoStyles = join(__dirname, './src/index.ts');
|
||||
const outputDir = join(__dirname, 'dist');
|
||||
|
||||
function outputFile(format, isMinified, withStyles) {
|
||||
let filename = join(outputDir, name);
|
||||
|
||||
if (format !== 'umd') {
|
||||
filename += `.${format}`;
|
||||
}
|
||||
if (isMinified) {
|
||||
filename += '.min';
|
||||
}
|
||||
if (!withStyles) {
|
||||
filename += '.noStyle';
|
||||
}
|
||||
|
||||
return `${filename}.js`;
|
||||
}
|
||||
|
||||
function getInput(options) {
|
||||
const { withStyles } = options;
|
||||
|
||||
if (withStyles) {
|
||||
return main;
|
||||
}
|
||||
|
||||
return mainNoStyles;
|
||||
}
|
||||
|
||||
function createBundle(format, options) {
|
||||
const { withStyles, isMinified, isReact } = options;
|
||||
const input = getInput(options);
|
||||
const file = outputFile(format, isMinified, withStyles, isReact);
|
||||
|
||||
const external = [];
|
||||
|
||||
const output = {
|
||||
file,
|
||||
format,
|
||||
sourcemap: true,
|
||||
globals: {},
|
||||
banner: [
|
||||
`/**`,
|
||||
` * ${name}`,
|
||||
` * @version ${version}`,
|
||||
` * @link ${homepage}`,
|
||||
` * @license ${license}`,
|
||||
` */`,
|
||||
].join('\n'),
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
typescript({
|
||||
tsconfig: 'tsconfig.esm.json',
|
||||
}),
|
||||
];
|
||||
|
||||
if (isMinified) {
|
||||
plugins.push(terser());
|
||||
}
|
||||
if (withStyles) {
|
||||
plugins.push(postcss());
|
||||
}
|
||||
|
||||
if (format === 'umd') {
|
||||
output['name'] = name;
|
||||
}
|
||||
|
||||
return {
|
||||
input,
|
||||
output,
|
||||
plugins,
|
||||
external,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = [
|
||||
// amd
|
||||
createBundle('amd', { withStyles: false, isMinified: false }),
|
||||
createBundle('amd', { withStyles: true, isMinified: false }),
|
||||
createBundle('amd', { withStyles: false, isMinified: true }),
|
||||
createBundle('amd', { withStyles: true, isMinified: true }),
|
||||
// umd
|
||||
createBundle('umd', { withStyles: false, isMinified: false }),
|
||||
createBundle('umd', { withStyles: true, isMinified: false }),
|
||||
createBundle('umd', { withStyles: false, isMinified: true }),
|
||||
createBundle('umd', { withStyles: true, isMinified: true }),
|
||||
// cjs
|
||||
createBundle('cjs', { withStyles: true, isMinified: false }),
|
||||
// esm
|
||||
createBundle('esm', { withStyles: true, isMinified: false }),
|
||||
createBundle('esm', { withStyles: true, isMinified: true }),
|
||||
];
|
2
packages/dockview-core/scripts/rollupEntryTarget.ts
Normal file
2
packages/dockview-core/scripts/rollupEntryTarget.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import '../dist/styles/dockview.css';
|
||||
export * from '../src/index';
|
@ -0,0 +1,47 @@
|
||||
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
|
||||
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
||||
import {
|
||||
TabPartInitParameters,
|
||||
IContentRenderer,
|
||||
ITabRenderer,
|
||||
} from '../../dockview/types';
|
||||
import { PanelUpdateEvent } from '../../panel/types';
|
||||
import { TabLocation } from '../../dockview/framework';
|
||||
|
||||
export class DockviewPanelModelMock implements IDockviewPanelModel {
|
||||
constructor(
|
||||
readonly contentComponent: string,
|
||||
readonly content: IContentRenderer,
|
||||
readonly tabComponent: string,
|
||||
readonly tab: ITabRenderer
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
|
||||
return this.tab;
|
||||
}
|
||||
|
||||
init(params: TabPartInitParameters): void {
|
||||
//
|
||||
}
|
||||
|
||||
updateParentGroup(
|
||||
group: DockviewGroupPanel,
|
||||
isPanelVisible: boolean
|
||||
): void {
|
||||
//
|
||||
}
|
||||
|
||||
update(event: PanelUpdateEvent): void {
|
||||
//
|
||||
}
|
||||
|
||||
layout(width: number, height: number): void {
|
||||
//
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
//
|
||||
}
|
||||
}
|
45
packages/dockview-core/src/__tests__/__mocks__/mockWindow.ts
Normal file
45
packages/dockview-core/src/__tests__/__mocks__/mockWindow.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
|
||||
export function setupMockWindow() {
|
||||
const listeners: Record<string, (() => void)[]> = {};
|
||||
|
||||
let width = 1000;
|
||||
let height = 2000;
|
||||
|
||||
return fromPartial<Window>({
|
||||
addEventListener: (type: string, listener: () => void) => {
|
||||
if (!listeners[type]) {
|
||||
listeners[type] = [];
|
||||
}
|
||||
listeners[type].push(listener);
|
||||
if (type === 'load') {
|
||||
listener();
|
||||
}
|
||||
},
|
||||
removeEventListener: (type: string, listener: () => void) => {
|
||||
if (listeners[type]) {
|
||||
const index = listeners[type].indexOf(listener);
|
||||
if (index > -1) {
|
||||
listeners[type].splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
dispatchEvent: (event: Event) => {
|
||||
const items = listeners[event.type];
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
items.forEach((item) => item());
|
||||
},
|
||||
document: document,
|
||||
close: () => {
|
||||
listeners['beforeunload']?.forEach((f) => f());
|
||||
},
|
||||
get innerWidth() {
|
||||
return width++;
|
||||
},
|
||||
get innerHeight() {
|
||||
return height++;
|
||||
},
|
||||
});
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
class ResizeObserver {
|
||||
observe() {
|
||||
// do nothing
|
||||
}
|
||||
unobserve() {
|
||||
// do nothing
|
||||
}
|
||||
disconnect() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
window.ResizeObserver = ResizeObserver;
|
73
packages/dockview-core/src/__tests__/__test_utils__/utils.ts
Normal file
73
packages/dockview-core/src/__tests__/__test_utils__/utils.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* useful utility type to erase readonly signatures for testing purposes
|
||||
*
|
||||
* @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#readonly-mapped-type-modifiers-and-readonly-arrays
|
||||
*/
|
||||
export type Writable<T> = T extends object
|
||||
? { -readonly [K in keyof T]: Writable<T[K]> }
|
||||
: T;
|
||||
|
||||
export function setMockRefElement(node: Partial<HTMLElement>): void {
|
||||
const mockRef = {
|
||||
get current() {
|
||||
return node;
|
||||
},
|
||||
set current(_value) {
|
||||
//noop
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(React, 'useRef').mockReturnValueOnce(mockRef);
|
||||
}
|
||||
|
||||
export function createOffsetDragOverEvent(params: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
}): Event {
|
||||
const event = new Event('dragover', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
Object.defineProperty(event, 'clientX', { get: () => params.clientX });
|
||||
Object.defineProperty(event, 'clientY', { get: () => params.clientY });
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* `jest.runAllTicks` doesn't seem to exhaust all events in the micro-task queue so
|
||||
* as a **hacky** alternative we'll wait for an empty Promise to complete which runs
|
||||
* on the micro-task queue so will force a run-to-completion emptying the queue
|
||||
* of any pending micro-task
|
||||
*/
|
||||
export function exhaustMicrotaskQueue(): Promise<void> {
|
||||
return new Promise<void>((resolve) => resolve());
|
||||
}
|
||||
|
||||
export const mockGetBoundingClientRect = ({
|
||||
left,
|
||||
top,
|
||||
height,
|
||||
width,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
height: number;
|
||||
width: number;
|
||||
}) => {
|
||||
const result = {
|
||||
left,
|
||||
top,
|
||||
height,
|
||||
width,
|
||||
right: left + width,
|
||||
bottom: top + height,
|
||||
x: left,
|
||||
y: top,
|
||||
};
|
||||
return {
|
||||
...result,
|
||||
toJSON: () => result,
|
||||
};
|
||||
};
|
69
packages/dockview-core/src/__tests__/api/api.spec.ts
Normal file
69
packages/dockview-core/src/__tests__/api/api.spec.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { PanelApiImpl } from '../../api/panelApi';
|
||||
import { IPanel } from '../../panel/types';
|
||||
|
||||
describe('api', () => {
|
||||
let api: PanelApiImpl;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new PanelApiImpl('dummy_id', 'fake-component');
|
||||
});
|
||||
|
||||
test('updateParameters', () => {
|
||||
const panel = {
|
||||
update: jest.fn(),
|
||||
} as Partial<IPanel>;
|
||||
|
||||
api.initialize(panel as IPanel);
|
||||
|
||||
expect(panel.update).toHaveBeenCalledTimes(0);
|
||||
|
||||
api.updateParameters({ keyA: 'valueA' });
|
||||
expect(panel.update).toHaveBeenCalledTimes(1);
|
||||
expect(panel.update).toHaveBeenCalledWith({
|
||||
params: { keyA: 'valueA' },
|
||||
});
|
||||
});
|
||||
|
||||
test('should update isFcoused getter', () => {
|
||||
expect(api.isFocused).toBeFalsy();
|
||||
|
||||
api._onDidChangeFocus.fire({ isFocused: true });
|
||||
expect(api.isFocused).toBeTruthy();
|
||||
|
||||
api._onDidChangeFocus.fire({ isFocused: false });
|
||||
expect(api.isFocused).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should update isActive getter', () => {
|
||||
expect(api.isFocused).toBeFalsy();
|
||||
|
||||
api._onDidActiveChange.fire({ isActive: true });
|
||||
expect(api.isActive).toBeTruthy();
|
||||
|
||||
api._onDidActiveChange.fire({ isActive: false });
|
||||
expect(api.isActive).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should update isActive getter', () => {
|
||||
expect(api.isVisible).toBeTruthy();
|
||||
|
||||
api._onDidVisibilityChange.fire({ isVisible: false });
|
||||
expect(api.isVisible).toBeFalsy();
|
||||
|
||||
api._onDidVisibilityChange.fire({ isVisible: true });
|
||||
expect(api.isVisible).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should update width and height getter', () => {
|
||||
expect(api.height).toBe(0);
|
||||
expect(api.width).toBe(0);
|
||||
|
||||
api._onDidDimensionChange.fire({ height: 10, width: 20 });
|
||||
expect(api.height).toBe(10);
|
||||
expect(api.width).toBe(20);
|
||||
|
||||
api._onDidDimensionChange.fire({ height: 20, width: 10 });
|
||||
expect(api.height).toBe(20);
|
||||
expect(api.width).toBe(10);
|
||||
});
|
||||
});
|
155
packages/dockview-core/src/__tests__/api/component.api.spec.ts
Normal file
155
packages/dockview-core/src/__tests__/api/component.api.spec.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import {
|
||||
SplitviewApi,
|
||||
PaneviewApi,
|
||||
GridviewApi,
|
||||
DockviewApi,
|
||||
} from '../../api/component.api';
|
||||
import { GridviewComponent } from '../../gridview/gridviewComponent';
|
||||
import { PaneviewComponent } from '../../paneview/paneviewComponent';
|
||||
import { SplitviewComponent } from '../../splitview/splitviewComponent';
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
|
||||
describe('component.api', () => {
|
||||
describe('splitview', () => {
|
||||
test('splitviewapi', () => {
|
||||
const list: (keyof SplitviewComponent)[] = [
|
||||
'minimumSize',
|
||||
'maximumSize',
|
||||
'height',
|
||||
'width',
|
||||
'length',
|
||||
'orientation',
|
||||
'onDidLayoutChange',
|
||||
'onDidAddView',
|
||||
'onDidRemoveView',
|
||||
'panels',
|
||||
'focus',
|
||||
'toJSON',
|
||||
];
|
||||
|
||||
for (const _ of list) {
|
||||
const f = jest.fn();
|
||||
|
||||
const component: Partial<SplitviewComponent> = {
|
||||
[_]: f(),
|
||||
};
|
||||
|
||||
const cut = new SplitviewApi(<SplitviewComponent>component);
|
||||
|
||||
(cut as any)[_];
|
||||
|
||||
expect(f).toBeCalledTimes(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('paneview', () => {
|
||||
test('panviewapi', () => {
|
||||
const list: (keyof PaneviewComponent)[] = [
|
||||
'minimumSize',
|
||||
'maximumSize',
|
||||
'height',
|
||||
'width',
|
||||
'onDidLayoutChange',
|
||||
'onDidAddView',
|
||||
'onDidRemoveView',
|
||||
'panels',
|
||||
'focus',
|
||||
'toJSON',
|
||||
];
|
||||
|
||||
for (const _ of list) {
|
||||
const f = jest.fn();
|
||||
|
||||
const component: Partial<PaneviewComponent> = {
|
||||
[_]: f(),
|
||||
};
|
||||
|
||||
const cut = new PaneviewApi(<PaneviewComponent>component);
|
||||
|
||||
(cut as any)[_];
|
||||
|
||||
expect(f).toBeCalledTimes(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('gridview', () => {
|
||||
test('gridviewapi', () => {
|
||||
const list: (keyof GridviewComponent)[] = [
|
||||
'minimumHeight',
|
||||
'maximumHeight',
|
||||
'minimumWidth',
|
||||
'maximumWidth',
|
||||
'width',
|
||||
'height',
|
||||
'onDidLayoutChange',
|
||||
'orientation',
|
||||
'focus',
|
||||
'toJSON',
|
||||
'onDidActiveGroupChange',
|
||||
'onDidAddGroup',
|
||||
'onDidRemoveGroup',
|
||||
'onDidLayoutFromJSON',
|
||||
];
|
||||
|
||||
for (const _ of list) {
|
||||
const f = jest.fn();
|
||||
|
||||
const component: Partial<GridviewComponent> = {
|
||||
[_]: f(),
|
||||
};
|
||||
|
||||
const cut = new GridviewApi(<GridviewComponent>component);
|
||||
|
||||
(cut as any)[_];
|
||||
|
||||
expect(f).toBeCalledTimes(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('dockview', () => {
|
||||
test('dockviewapi', () => {
|
||||
const list: (keyof DockviewComponent)[] = [
|
||||
'minimumHeight',
|
||||
'maximumHeight',
|
||||
'minimumWidth',
|
||||
'maximumWidth',
|
||||
'width',
|
||||
'height',
|
||||
'size',
|
||||
'totalPanels',
|
||||
'onDidLayoutChange',
|
||||
'panels',
|
||||
'groups',
|
||||
'activeGroup',
|
||||
'activePanel',
|
||||
'focus',
|
||||
'closeAllGroups',
|
||||
'toJSON',
|
||||
'onDidActiveGroupChange',
|
||||
'onDidAddGroup',
|
||||
'onDidRemoveGroup',
|
||||
'onDidActivePanelChange',
|
||||
'onDidAddPanel',
|
||||
'onDidRemovePanel',
|
||||
'onDidLayoutFromJSON',
|
||||
];
|
||||
|
||||
for (const _ of list) {
|
||||
const f = jest.fn();
|
||||
|
||||
const component: Partial<DockviewComponent> = {
|
||||
[_]: f(),
|
||||
};
|
||||
|
||||
const cut = new DockviewApi(<DockviewComponent>component);
|
||||
|
||||
(cut as any)[_];
|
||||
|
||||
expect(f).toBeCalledTimes(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,123 @@
|
||||
import { DockviewPanelApiImpl } from '../../api/dockviewPanelApi';
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
import { DockviewPanel } from '../../dockview/dockviewPanel';
|
||||
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
|
||||
describe('groupPanelApi', () => {
|
||||
test('title', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const panelMock = jest.fn<DockviewPanel, []>(() => {
|
||||
return {
|
||||
update: jest.fn(),
|
||||
setTitle: jest.fn(),
|
||||
} as any;
|
||||
});
|
||||
|
||||
const panel = new panelMock();
|
||||
const group = fromPartial<DockviewGroupPanel>({
|
||||
api: {
|
||||
onDidVisibilityChange: jest.fn(),
|
||||
onDidLocationChange: jest.fn(),
|
||||
onDidActiveChange: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
const cut = new DockviewPanelApiImpl(
|
||||
panel,
|
||||
group,
|
||||
<DockviewComponent>accessor,
|
||||
'fake-component'
|
||||
);
|
||||
|
||||
cut.setTitle('test_title');
|
||||
expect(panel.setTitle).toBeCalledTimes(1);
|
||||
expect(panel.setTitle).toBeCalledWith('test_title');
|
||||
});
|
||||
|
||||
test('updateParameters', () => {
|
||||
const groupPanel: Partial<DockviewPanel> = {
|
||||
id: 'test_id',
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupViewPanel = new DockviewGroupPanel(
|
||||
<DockviewComponent>accessor,
|
||||
'',
|
||||
{}
|
||||
);
|
||||
|
||||
const cut = new DockviewPanelApiImpl(
|
||||
<DockviewPanel>groupPanel,
|
||||
<DockviewGroupPanel>groupViewPanel,
|
||||
<DockviewComponent>accessor,
|
||||
'fake-component'
|
||||
);
|
||||
|
||||
cut.updateParameters({ keyA: 'valueA' });
|
||||
|
||||
expect(groupPanel.update).toHaveBeenCalledWith({
|
||||
params: { keyA: 'valueA' },
|
||||
});
|
||||
expect(groupPanel.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('onDidGroupChange', () => {
|
||||
const groupPanel: Partial<DockviewPanel> = {
|
||||
id: 'test_id',
|
||||
};
|
||||
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupViewPanel = new DockviewGroupPanel(
|
||||
<DockviewComponent>accessor,
|
||||
'',
|
||||
{}
|
||||
);
|
||||
|
||||
const cut = new DockviewPanelApiImpl(
|
||||
<DockviewPanel>groupPanel,
|
||||
<DockviewGroupPanel>groupViewPanel,
|
||||
<DockviewComponent>accessor,
|
||||
'fake-component'
|
||||
);
|
||||
|
||||
let events = 0;
|
||||
|
||||
const disposable = cut.onDidGroupChange(() => {
|
||||
events++;
|
||||
});
|
||||
|
||||
expect(events).toBe(0);
|
||||
expect(cut.group).toBe(groupViewPanel);
|
||||
|
||||
const groupViewPanel2 = new DockviewGroupPanel(
|
||||
<DockviewComponent>accessor,
|
||||
'',
|
||||
{}
|
||||
);
|
||||
cut.group = groupViewPanel2;
|
||||
expect(events).toBe(1);
|
||||
expect(cut.group).toBe(groupViewPanel2);
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
});
|
@ -3,24 +3,25 @@ import {
|
||||
last,
|
||||
pushToEnd,
|
||||
pushToStart,
|
||||
range,
|
||||
remove,
|
||||
sequenceEquals,
|
||||
tail,
|
||||
} from '../array';
|
||||
|
||||
describe('array', () => {
|
||||
it('tail', () => {
|
||||
test('tail', () => {
|
||||
expect(tail([1, 2, 3, 4, 5])).toEqual([[1, 2, 3, 4], 5]);
|
||||
expect(tail([1, 2])).toEqual([[1], 2]);
|
||||
expect(tail([1])).toEqual([[], 1]);
|
||||
expect(() => tail([])).toThrow('Invalid tail call');
|
||||
});
|
||||
|
||||
it('last', () => {
|
||||
test('last', () => {
|
||||
expect(last([1, 2, 3, 4])).toBe(4);
|
||||
expect(last([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('pushToEnd', () => {
|
||||
test('pushToEnd', () => {
|
||||
const arr1 = [1, 2, 3, 4];
|
||||
pushToEnd(arr1, 3);
|
||||
expect(arr1).toEqual([1, 2, 4, 3]);
|
||||
@ -28,7 +29,7 @@ describe('array', () => {
|
||||
expect(arr1).toEqual([1, 2, 4, 3]);
|
||||
});
|
||||
|
||||
it('pushToStart', () => {
|
||||
test('pushToStart', () => {
|
||||
const arr1 = [1, 2, 3, 4];
|
||||
pushToStart(arr1, 3);
|
||||
expect(arr1).toEqual([3, 1, 2, 4]);
|
||||
@ -36,21 +37,33 @@ describe('array', () => {
|
||||
expect(arr1).toEqual([3, 1, 2, 4]);
|
||||
});
|
||||
|
||||
it('range', () => {
|
||||
expect(range(0, 5)).toEqual([0, 1, 2, 3, 4]);
|
||||
expect(range(5, 0)).toEqual([5, 4, 3, 2, 1]);
|
||||
expect(range(5)).toEqual([0, 1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('firstIndex', () => {
|
||||
test('firstIndex', () => {
|
||||
expect(firstIndex([1, 2, 3, 4, 3], (item) => item === 3)).toBe(2);
|
||||
expect(firstIndex([1, 2, 3, 4, 3], (item) => item === 5)).toBe(-1);
|
||||
});
|
||||
|
||||
it('firstIndex', () => {
|
||||
test('firstIndex', () => {
|
||||
expect(sequenceEquals([1, 2, 3, 4], [1, 2, 3, 4])).toBeTruthy();
|
||||
expect(sequenceEquals([1, 2, 3, 4], [4, 3, 2, 1])).toBeFalsy();
|
||||
expect(sequenceEquals([1, 2, 3, 4], [1, 2, 3])).toBeFalsy();
|
||||
expect(sequenceEquals([1, 2, 3, 4], [1, 2, 3, 4, 5])).toBeFalsy();
|
||||
});
|
||||
|
||||
test('remove', () => {
|
||||
const arr1 = [1, 2, 3, 4];
|
||||
remove(arr1, 2);
|
||||
expect(arr1).toEqual([1, 3, 4]);
|
||||
|
||||
const arr2 = [1, 2, 2, 3, 4];
|
||||
remove(arr2, 2);
|
||||
expect(arr2).toEqual([1, 2, 3, 4]);
|
||||
|
||||
const arr3 = [1];
|
||||
remove(arr3, 2);
|
||||
expect(arr3).toEqual([1]);
|
||||
remove(arr3, 1);
|
||||
expect(arr3).toEqual([]);
|
||||
remove(arr3, 1);
|
||||
expect(arr3).toEqual([]);
|
||||
});
|
||||
});
|
@ -0,0 +1,179 @@
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import { DragHandler } from '../../dnd/abstractDragHandler';
|
||||
import { IDisposable } from '../../lifecycle';
|
||||
|
||||
describe('abstractDragHandler', () => {
|
||||
test('that className dv-dragged is added to element after dragstart event', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const element = document.createElement('div');
|
||||
|
||||
const handler = new (class TestClass extends DragHandler {
|
||||
constructor(el: HTMLElement) {
|
||||
super(el);
|
||||
}
|
||||
|
||||
getData(): IDisposable {
|
||||
return {
|
||||
dispose: () => {
|
||||
// /
|
||||
},
|
||||
};
|
||||
}
|
||||
})(element);
|
||||
|
||||
expect(element.classList.contains('dv-dragged')).toBeFalsy();
|
||||
|
||||
fireEvent.dragStart(element);
|
||||
expect(element.classList.contains('dv-dragged')).toBeTruthy();
|
||||
|
||||
jest.runAllTimers();
|
||||
expect(element.classList.contains('dv-dragged')).toBeFalsy();
|
||||
|
||||
handler.dispose();
|
||||
});
|
||||
|
||||
test('that iframes and webviews have pointerEvents=none set whilst drag action is in process', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const element = document.createElement('div');
|
||||
const iframe = document.createElement('iframe');
|
||||
const webview = document.createElement('webview');
|
||||
const span = document.createElement('span');
|
||||
|
||||
document.body.appendChild(element);
|
||||
document.body.appendChild(iframe);
|
||||
document.body.appendChild(webview);
|
||||
document.body.appendChild(span);
|
||||
|
||||
const handler = new (class TestClass extends DragHandler {
|
||||
constructor(el: HTMLElement) {
|
||||
super(el);
|
||||
}
|
||||
|
||||
getData(): IDisposable {
|
||||
return {
|
||||
dispose: () => {
|
||||
// /
|
||||
},
|
||||
};
|
||||
}
|
||||
})(element);
|
||||
|
||||
expect(iframe.style.pointerEvents).toBeFalsy();
|
||||
expect(webview.style.pointerEvents).toBeFalsy();
|
||||
expect(span.style.pointerEvents).toBeFalsy();
|
||||
|
||||
fireEvent.dragStart(element);
|
||||
expect(iframe.style.pointerEvents).toBe('none');
|
||||
expect(webview.style.pointerEvents).toBe('none');
|
||||
expect(span.style.pointerEvents).toBeFalsy();
|
||||
|
||||
fireEvent.dragEnd(element);
|
||||
expect(iframe.style.pointerEvents).toBe('');
|
||||
expect(webview.style.pointerEvents).toBe('');
|
||||
expect(span.style.pointerEvents).toBeFalsy();
|
||||
|
||||
handler.dispose();
|
||||
});
|
||||
|
||||
test('that the disabling of pointerEvents is restored on a premature disposal of the handler', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const element = document.createElement('div');
|
||||
const iframe = document.createElement('iframe');
|
||||
const webview = document.createElement('webview');
|
||||
const span = document.createElement('span');
|
||||
|
||||
document.body.appendChild(element);
|
||||
document.body.appendChild(iframe);
|
||||
document.body.appendChild(webview);
|
||||
document.body.appendChild(span);
|
||||
|
||||
const handler = new (class TestClass extends DragHandler {
|
||||
constructor(el: HTMLElement) {
|
||||
super(el);
|
||||
}
|
||||
|
||||
getData(): IDisposable {
|
||||
return {
|
||||
dispose: () => {
|
||||
// /
|
||||
},
|
||||
};
|
||||
}
|
||||
})(element);
|
||||
|
||||
expect(iframe.style.pointerEvents).toBeFalsy();
|
||||
expect(webview.style.pointerEvents).toBeFalsy();
|
||||
expect(span.style.pointerEvents).toBeFalsy();
|
||||
|
||||
fireEvent.dragStart(element);
|
||||
expect(iframe.style.pointerEvents).toBe('none');
|
||||
expect(webview.style.pointerEvents).toBe('none');
|
||||
expect(span.style.pointerEvents).toBeFalsy();
|
||||
|
||||
handler.dispose();
|
||||
expect(iframe.style.pointerEvents).toBe('');
|
||||
expect(webview.style.pointerEvents).toBe('');
|
||||
expect(span.style.pointerEvents).toBeFalsy();
|
||||
});
|
||||
|
||||
test('that .preventDefault() is called for cancelled events', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const handler = new (class TestClass extends DragHandler {
|
||||
constructor(el: HTMLElement) {
|
||||
super(el);
|
||||
}
|
||||
|
||||
protected isCancelled(_event: DragEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
getData(): IDisposable {
|
||||
return {
|
||||
dispose: () => {
|
||||
// /
|
||||
},
|
||||
};
|
||||
}
|
||||
})(element);
|
||||
|
||||
const event = new Event('dragstart');
|
||||
const spy = jest.spyOn(event, 'preventDefault');
|
||||
fireEvent(element, event);
|
||||
expect(spy).toBeCalledTimes(1);
|
||||
|
||||
handler.dispose();
|
||||
});
|
||||
|
||||
test('that .preventDefault() is not called for non-cancelled events', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const handler = new (class TestClass extends DragHandler {
|
||||
constructor(el: HTMLElement) {
|
||||
super(el);
|
||||
}
|
||||
|
||||
protected isCancelled(_event: DragEvent): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getData(): IDisposable {
|
||||
return {
|
||||
dispose: () => {
|
||||
// /
|
||||
},
|
||||
};
|
||||
}
|
||||
})(element);
|
||||
|
||||
const event = new Event('dragstart');
|
||||
const spy = jest.spyOn(event, 'preventDefault');
|
||||
fireEvent(element, event);
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
handler.dispose();
|
||||
});
|
||||
});
|
101
packages/dockview-core/src/__tests__/dnd/dataTransfer.spec.ts
Normal file
101
packages/dockview-core/src/__tests__/dnd/dataTransfer.spec.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import {
|
||||
getPaneData,
|
||||
getPanelData,
|
||||
LocalSelectionTransfer,
|
||||
PanelTransfer,
|
||||
PaneTransfer,
|
||||
} from '../../dnd/dataTransfer';
|
||||
|
||||
describe('dataTransfer', () => {
|
||||
describe('getPanelData', () => {
|
||||
test('should be undefined when there is no local transfer object', () => {
|
||||
expect(getPanelData()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should be undefined when there is a local transfer object that is not a PanelTransfer', () => {
|
||||
LocalSelectionTransfer.getInstance<PaneTransfer>().setData(
|
||||
[new PaneTransfer('viewId', 'groupId')],
|
||||
PaneTransfer.prototype
|
||||
);
|
||||
|
||||
expect(getPanelData()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should retrieve the PanelTransfer object when transfer is active', () => {
|
||||
const transferObject = new PanelTransfer(
|
||||
'viewId',
|
||||
'groupId',
|
||||
'panelId'
|
||||
);
|
||||
LocalSelectionTransfer.getInstance<PanelTransfer>().setData(
|
||||
[transferObject],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
expect(getPanelData()).toBe(transferObject);
|
||||
});
|
||||
|
||||
test('should retrieve the PanelTransfer when a new transfer overrides an existing one', () => {
|
||||
LocalSelectionTransfer.getInstance<PaneTransfer>().setData(
|
||||
[new PaneTransfer('viewId', 'groupId')],
|
||||
PaneTransfer.prototype
|
||||
);
|
||||
|
||||
expect(getPanelData()).toBeUndefined();
|
||||
|
||||
const transferObject = new PanelTransfer(
|
||||
'viewId',
|
||||
'groupId',
|
||||
'panelId'
|
||||
);
|
||||
LocalSelectionTransfer.getInstance<PanelTransfer>().setData(
|
||||
[transferObject],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
expect(getPanelData()).toBe(transferObject);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPaneData', () => {
|
||||
test('should be undefined when there is no local transfer object', () => {
|
||||
expect(getPaneData()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should be undefined when there is a local transfer object that is not a PaneTransfer', () => {
|
||||
LocalSelectionTransfer.getInstance<PanelTransfer>().setData(
|
||||
[new PanelTransfer('viewId', 'groupId', 'panelId')],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
expect(getPaneData()).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should retrieve the PaneTransfer object when transfer is active', () => {
|
||||
const transferObject = new PaneTransfer('viewId', 'groupId');
|
||||
LocalSelectionTransfer.getInstance<PaneTransfer>().setData(
|
||||
[transferObject],
|
||||
PaneTransfer.prototype
|
||||
);
|
||||
|
||||
expect(getPaneData()).toBe(transferObject);
|
||||
});
|
||||
|
||||
test('should retrieve the PanelTransfer when a new transfer overrides an existing one', () => {
|
||||
LocalSelectionTransfer.getInstance<PanelTransfer>().setData(
|
||||
[new PanelTransfer('viewId', 'groupId', 'panelId')],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
expect(getPaneData()).toBeUndefined();
|
||||
|
||||
const transferObject = new PaneTransfer('viewId', 'groupId');
|
||||
LocalSelectionTransfer.getInstance<PaneTransfer>().setData(
|
||||
[transferObject],
|
||||
PaneTransfer.prototype
|
||||
);
|
||||
|
||||
expect(getPaneData()).toBe(transferObject);
|
||||
});
|
||||
});
|
||||
});
|
401
packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts
Normal file
401
packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts
Normal file
@ -0,0 +1,401 @@
|
||||
import {
|
||||
calculateQuadrantAsPercentage,
|
||||
calculateQuadrantAsPixels,
|
||||
directionToPosition,
|
||||
Droptarget,
|
||||
Position,
|
||||
positionToDirection,
|
||||
} from '../../dnd/droptarget';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import { createOffsetDragOverEvent } from '../__test_utils__/utils';
|
||||
|
||||
describe('droptarget', () => {
|
||||
let element: HTMLElement;
|
||||
let droptarget: Droptarget;
|
||||
|
||||
beforeEach(() => {
|
||||
element = document.createElement('div');
|
||||
|
||||
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 200);
|
||||
});
|
||||
|
||||
test('that dragover events are marked', () => {
|
||||
droptarget = new Droptarget(element, {
|
||||
canDisplayOverlay: () => true,
|
||||
acceptedTargetZones: ['center'],
|
||||
});
|
||||
|
||||
fireEvent.dragEnter(element);
|
||||
const event = new Event('dragover');
|
||||
fireEvent(element, event);
|
||||
|
||||
expect(
|
||||
(event as any)['__dockview_droptarget_event_is_used__']
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('that the drop target is removed when receiving a marked dragover event', () => {
|
||||
let position: Position | undefined = undefined;
|
||||
|
||||
droptarget = new Droptarget(element, {
|
||||
canDisplayOverlay: () => true,
|
||||
acceptedTargetZones: ['center'],
|
||||
});
|
||||
|
||||
droptarget.onDrop((event) => {
|
||||
position = event.position;
|
||||
});
|
||||
|
||||
fireEvent.dragEnter(element);
|
||||
fireEvent.dragOver(element);
|
||||
|
||||
const target = element.querySelector(
|
||||
'.dv-drop-target-dropzone'
|
||||
) as HTMLElement;
|
||||
fireEvent.drop(target);
|
||||
expect(position).toBe('center');
|
||||
|
||||
const event = new Event('dragover');
|
||||
(event as any)['__dockview_droptarget_event_is_used__'] = true;
|
||||
fireEvent(element, event);
|
||||
expect(element.querySelector('.dv-drop-target-dropzone')).toBeNull();
|
||||
});
|
||||
|
||||
test('directionToPosition', () => {
|
||||
expect(directionToPosition('above')).toBe('top');
|
||||
expect(directionToPosition('below')).toBe('bottom');
|
||||
expect(directionToPosition('left')).toBe('left');
|
||||
expect(directionToPosition('right')).toBe('right');
|
||||
expect(directionToPosition('within')).toBe('center');
|
||||
expect(() => directionToPosition('bad_input' as any)).toThrow(
|
||||
"invalid direction 'bad_input'"
|
||||
);
|
||||
});
|
||||
|
||||
test('positionToDirection', () => {
|
||||
expect(positionToDirection('top')).toBe('above');
|
||||
expect(positionToDirection('bottom')).toBe('below');
|
||||
expect(positionToDirection('left')).toBe('left');
|
||||
expect(positionToDirection('right')).toBe('right');
|
||||
expect(positionToDirection('center')).toBe('within');
|
||||
expect(() => positionToDirection('bad_input' as any)).toThrow(
|
||||
"invalid position 'bad_input'"
|
||||
);
|
||||
});
|
||||
|
||||
test('non-directional', () => {
|
||||
let position: Position | undefined = undefined;
|
||||
|
||||
droptarget = new Droptarget(element, {
|
||||
canDisplayOverlay: () => true,
|
||||
acceptedTargetZones: ['center'],
|
||||
});
|
||||
|
||||
droptarget.onDrop((event) => {
|
||||
position = event.position;
|
||||
});
|
||||
|
||||
fireEvent.dragEnter(element);
|
||||
fireEvent.dragOver(element);
|
||||
|
||||
const target = element.querySelector(
|
||||
'.dv-drop-target-dropzone'
|
||||
) as HTMLElement;
|
||||
fireEvent.drop(target);
|
||||
expect(position).toBe('center');
|
||||
});
|
||||
|
||||
test('drop', () => {
|
||||
let position: Position | undefined = undefined;
|
||||
|
||||
droptarget = new Droptarget(element, {
|
||||
canDisplayOverlay: () => true,
|
||||
acceptedTargetZones: ['top', 'left', 'right', 'bottom', 'center'],
|
||||
});
|
||||
|
||||
droptarget.onDrop((event) => {
|
||||
position = event.position;
|
||||
});
|
||||
|
||||
fireEvent.dragEnter(element);
|
||||
fireEvent.dragOver(element);
|
||||
|
||||
const target = element.querySelector(
|
||||
'.dv-drop-target-dropzone'
|
||||
) as HTMLElement;
|
||||
|
||||
jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100);
|
||||
jest.spyOn(target, 'clientWidth', 'get').mockImplementation(() => 200);
|
||||
|
||||
fireEvent(
|
||||
target,
|
||||
createOffsetDragOverEvent({
|
||||
clientX: 19,
|
||||
clientY: 0,
|
||||
})
|
||||
);
|
||||
|
||||
expect(position).toBeUndefined();
|
||||
fireEvent.drop(target);
|
||||
expect(position).toBe('left');
|
||||
});
|
||||
|
||||
test('default', () => {
|
||||
droptarget = new Droptarget(element, {
|
||||
canDisplayOverlay: () => true,
|
||||
acceptedTargetZones: ['top', 'left', 'right', 'bottom', 'center'],
|
||||
});
|
||||
|
||||
expect(droptarget.state).toBeUndefined();
|
||||
|
||||
fireEvent.dragEnter(element);
|
||||
fireEvent.dragOver(element);
|
||||
|
||||
let viewQuery = element.querySelectorAll(
|
||||
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
|
||||
);
|
||||
expect(viewQuery.length).toBe(1);
|
||||
|
||||
const target = element.querySelector(
|
||||
'.dv-drop-target-dropzone'
|
||||
) as HTMLElement;
|
||||
|
||||
jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100);
|
||||
jest.spyOn(target, 'clientWidth', 'get').mockImplementation(() => 200);
|
||||
|
||||
fireEvent(
|
||||
target,
|
||||
createOffsetDragOverEvent({ clientX: 19, clientY: 0 })
|
||||
);
|
||||
|
||||
function check(
|
||||
element: HTMLElement,
|
||||
box: {
|
||||
left: string;
|
||||
top: string;
|
||||
width: string;
|
||||
height: string;
|
||||
}
|
||||
) {
|
||||
expect(element.style.top).toBe(box.top);
|
||||
expect(element.style.left).toBe(box.left);
|
||||
expect(element.style.width).toBe(box.width);
|
||||
expect(element.style.height).toBe(box.height);
|
||||
}
|
||||
|
||||
viewQuery = element.querySelectorAll(
|
||||
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
|
||||
);
|
||||
expect(viewQuery.length).toBe(1);
|
||||
expect(droptarget.state).toBe('left');
|
||||
check(
|
||||
element
|
||||
.getElementsByClassName('dv-drop-target-selection')
|
||||
.item(0) as HTMLDivElement,
|
||||
{
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
width: '50%',
|
||||
height: '100%',
|
||||
}
|
||||
);
|
||||
|
||||
fireEvent(
|
||||
target,
|
||||
createOffsetDragOverEvent({ clientX: 40, clientY: 19 })
|
||||
);
|
||||
|
||||
viewQuery = element.querySelectorAll(
|
||||
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
|
||||
);
|
||||
expect(viewQuery.length).toBe(1);
|
||||
expect(droptarget.state).toBe('top');
|
||||
check(
|
||||
element
|
||||
.getElementsByClassName('dv-drop-target-selection')
|
||||
.item(0) as HTMLDivElement,
|
||||
{
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
width: '100%',
|
||||
height: '50%',
|
||||
}
|
||||
);
|
||||
|
||||
fireEvent(
|
||||
target,
|
||||
createOffsetDragOverEvent({ clientX: 160, clientY: 81 })
|
||||
);
|
||||
|
||||
viewQuery = element.querySelectorAll(
|
||||
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
|
||||
);
|
||||
expect(viewQuery.length).toBe(1);
|
||||
expect(droptarget.state).toBe('bottom');
|
||||
check(
|
||||
element
|
||||
.getElementsByClassName('dv-drop-target-selection')
|
||||
.item(0) as HTMLDivElement,
|
||||
{
|
||||
top: '50%',
|
||||
left: '0px',
|
||||
width: '100%',
|
||||
height: '50%',
|
||||
}
|
||||
);
|
||||
|
||||
fireEvent(
|
||||
target,
|
||||
createOffsetDragOverEvent({ clientX: 161, clientY: 0 })
|
||||
);
|
||||
|
||||
viewQuery = element.querySelectorAll(
|
||||
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection'
|
||||
);
|
||||
expect(viewQuery.length).toBe(1);
|
||||
expect(droptarget.state).toBe('right');
|
||||
check(
|
||||
element
|
||||
.getElementsByClassName('dv-drop-target-selection')
|
||||
.item(0) as HTMLDivElement,
|
||||
{
|
||||
top: '0px',
|
||||
left: '50%',
|
||||
width: '50%',
|
||||
height: '100%',
|
||||
}
|
||||
);
|
||||
fireEvent(
|
||||
target,
|
||||
createOffsetDragOverEvent({ clientX: 100, clientY: 50 })
|
||||
);
|
||||
expect(droptarget.state).toBe('center');
|
||||
expect(
|
||||
(
|
||||
element
|
||||
.getElementsByClassName('dv-drop-target-selection')
|
||||
.item(0) as HTMLDivElement
|
||||
).style.transform
|
||||
).toBe('');
|
||||
|
||||
fireEvent.dragLeave(target);
|
||||
expect(droptarget.state).toBe('center');
|
||||
viewQuery = element.querySelectorAll('.dv-drop-target');
|
||||
expect(viewQuery.length).toBe(0);
|
||||
});
|
||||
|
||||
describe('calculateQuadrantAsPercentage', () => {
|
||||
test('variety of cases', () => {
|
||||
const inputs: Array<{
|
||||
directions: Position[];
|
||||
x: number;
|
||||
y: number;
|
||||
result: Position | null;
|
||||
}> = [
|
||||
{ directions: ['left', 'right'], x: 19, y: 50, result: 'left' },
|
||||
{
|
||||
directions: ['left', 'right'],
|
||||
x: 81,
|
||||
y: 50,
|
||||
result: 'right',
|
||||
},
|
||||
{
|
||||
directions: ['top', 'bottom'],
|
||||
x: 50,
|
||||
y: 19,
|
||||
result: 'top',
|
||||
},
|
||||
{
|
||||
directions: ['top', 'bottom'],
|
||||
x: 50,
|
||||
y: 81,
|
||||
result: 'bottom',
|
||||
},
|
||||
{
|
||||
directions: ['left', 'right', 'top', 'bottom', 'center'],
|
||||
x: 50,
|
||||
y: 50,
|
||||
result: 'center',
|
||||
},
|
||||
{
|
||||
directions: ['left', 'right', 'top', 'bottom'],
|
||||
x: 50,
|
||||
y: 50,
|
||||
result: null,
|
||||
},
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
expect(
|
||||
calculateQuadrantAsPercentage(
|
||||
new Set(input.directions),
|
||||
input.x,
|
||||
input.y,
|
||||
100,
|
||||
100,
|
||||
20
|
||||
)
|
||||
).toBe(input.result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateQuadrantAsPixels', () => {
|
||||
test('variety of cases', () => {
|
||||
const inputs: Array<{
|
||||
directions: Position[];
|
||||
x: number;
|
||||
y: number;
|
||||
result: Position | null;
|
||||
}> = [
|
||||
{ directions: ['left', 'right'], x: 19, y: 50, result: 'left' },
|
||||
{
|
||||
directions: ['left', 'right'],
|
||||
x: 81,
|
||||
y: 50,
|
||||
result: 'right',
|
||||
},
|
||||
{
|
||||
directions: ['top', 'bottom'],
|
||||
x: 50,
|
||||
y: 19,
|
||||
result: 'top',
|
||||
},
|
||||
{
|
||||
directions: ['top', 'bottom'],
|
||||
x: 50,
|
||||
y: 81,
|
||||
result: 'bottom',
|
||||
},
|
||||
{
|
||||
directions: ['left', 'right', 'top', 'bottom', 'center'],
|
||||
x: 50,
|
||||
y: 50,
|
||||
result: 'center',
|
||||
},
|
||||
{
|
||||
directions: ['left', 'right', 'top', 'bottom'],
|
||||
x: 50,
|
||||
y: 50,
|
||||
result: null,
|
||||
},
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
expect(
|
||||
calculateQuadrantAsPixels(
|
||||
new Set(input.directions),
|
||||
input.x,
|
||||
input.y,
|
||||
100,
|
||||
100,
|
||||
20
|
||||
)
|
||||
).toBe(input.result);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
31
packages/dockview-core/src/__tests__/dnd/ghost.spec.ts
Normal file
31
packages/dockview-core/src/__tests__/dnd/ghost.spec.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { addGhostImage } from '../../dnd/ghost';
|
||||
|
||||
describe('ghost', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.clearAllTimers();
|
||||
});
|
||||
|
||||
test('that a custom class is added, the element is added to the document and all is removed afterwards', () => {
|
||||
const dataTransferMock = jest.fn<Partial<DataTransfer>, []>(() => {
|
||||
return {
|
||||
setDragImage: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const element = document.createElement('div');
|
||||
const dataTransfer = <DataTransfer>new dataTransferMock();
|
||||
|
||||
addGhostImage(dataTransfer, element);
|
||||
|
||||
expect(element.className).toBe('dv-dragged');
|
||||
expect(element.parentElement).toBe(document.body);
|
||||
expect(dataTransfer.setDragImage).toBeCalledTimes(1);
|
||||
expect(dataTransfer.setDragImage).toBeCalledWith(element, 0, 0);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(element.className).toBe('');
|
||||
expect(element.parentElement).toBe(null);
|
||||
});
|
||||
});
|
@ -0,0 +1,114 @@
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import { GroupDragHandler } from '../../dnd/groupDragHandler';
|
||||
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
||||
import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer';
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
|
||||
describe('groupDragHandler', () => {
|
||||
test('that the dnd transfer object is setup and torndown', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
const partial: Partial<DockviewGroupPanel> = {
|
||||
id: 'test_group_id',
|
||||
api: { location: { type: 'grid' } } as any,
|
||||
};
|
||||
return partial as DockviewGroupPanel;
|
||||
});
|
||||
const group = new groupMock();
|
||||
|
||||
const cut = new GroupDragHandler(
|
||||
element,
|
||||
{ id: 'test_accessor_id' } as DockviewComponent,
|
||||
group
|
||||
);
|
||||
|
||||
fireEvent.dragStart(element, new Event('dragstart'));
|
||||
|
||||
expect(
|
||||
LocalSelectionTransfer.getInstance<PanelTransfer>().hasData(
|
||||
PanelTransfer.prototype
|
||||
)
|
||||
).toBeTruthy();
|
||||
const transferObject =
|
||||
LocalSelectionTransfer.getInstance<PanelTransfer>().getData(
|
||||
PanelTransfer.prototype
|
||||
)![0];
|
||||
expect(transferObject).toBeTruthy();
|
||||
expect(transferObject.viewId).toBe('test_accessor_id');
|
||||
expect(transferObject.groupId).toBe('test_group_id');
|
||||
expect(transferObject.panelId).toBeNull();
|
||||
|
||||
fireEvent.dragStart(element, new Event('dragend'));
|
||||
expect(
|
||||
LocalSelectionTransfer.getInstance<PanelTransfer>().hasData(
|
||||
PanelTransfer.prototype
|
||||
)
|
||||
).toBeFalsy();
|
||||
|
||||
cut.dispose();
|
||||
});
|
||||
test('that the event is cancelled when floating and shiftKey=true', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
const partial: Partial<DockviewGroupPanel> = {
|
||||
api: { location: { type: 'floating' } } as any,
|
||||
};
|
||||
return partial as DockviewGroupPanel;
|
||||
});
|
||||
const group = new groupMock();
|
||||
|
||||
const cut = new GroupDragHandler(
|
||||
element,
|
||||
{ id: 'accessor_id' } as DockviewComponent,
|
||||
group
|
||||
);
|
||||
|
||||
const event = new KeyboardEvent('dragstart', { shiftKey: false });
|
||||
|
||||
const spy = jest.spyOn(event, 'preventDefault');
|
||||
fireEvent(element, event);
|
||||
expect(spy).toBeCalledTimes(1);
|
||||
|
||||
const event2 = new KeyboardEvent('dragstart', { shiftKey: true });
|
||||
|
||||
const spy2 = jest.spyOn(event2, 'preventDefault');
|
||||
fireEvent(element, event);
|
||||
expect(spy2).toBeCalledTimes(0);
|
||||
|
||||
cut.dispose();
|
||||
});
|
||||
|
||||
test('that the event is never cancelled when the group is not floating', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
const partial: Partial<DockviewGroupPanel> = {
|
||||
api: { location: { type: 'grid' } } as any,
|
||||
};
|
||||
return partial as DockviewGroupPanel;
|
||||
});
|
||||
const group = new groupMock();
|
||||
|
||||
const cut = new GroupDragHandler(
|
||||
element,
|
||||
{ id: 'accessor_id' } as DockviewComponent,
|
||||
group
|
||||
);
|
||||
|
||||
const event = new KeyboardEvent('dragstart', { shiftKey: false });
|
||||
|
||||
const spy = jest.spyOn(event, 'preventDefault');
|
||||
fireEvent(element, event);
|
||||
expect(spy).toBeCalledTimes(0);
|
||||
|
||||
const event2 = new KeyboardEvent('dragstart', { shiftKey: true });
|
||||
|
||||
const spy2 = jest.spyOn(event2, 'preventDefault');
|
||||
fireEvent(element, event);
|
||||
expect(spy2).toBeCalledTimes(0);
|
||||
|
||||
cut.dispose();
|
||||
});
|
||||
});
|
@ -0,0 +1,174 @@
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import { ContentContainer } from '../../../../dockview/components/panel/content';
|
||||
import {
|
||||
GroupPanelPartInitParameters,
|
||||
IContentRenderer,
|
||||
} from '../../../../dockview/types';
|
||||
import { CompositeDisposable } from '../../../../lifecycle';
|
||||
import { PanelUpdateEvent } from '../../../../panel/types';
|
||||
import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
|
||||
import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel';
|
||||
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel';
|
||||
import { OverlayRenderContainer } from '../../../../overlay/overlayRenderContainer';
|
||||
|
||||
class TestContentRenderer
|
||||
extends CompositeDisposable
|
||||
implements IContentRenderer
|
||||
{
|
||||
readonly element: HTMLElement;
|
||||
|
||||
constructor(public id: string) {
|
||||
super();
|
||||
this.element = document.createElement('div');
|
||||
this.element.id = id;
|
||||
}
|
||||
|
||||
init(parameters: GroupPanelPartInitParameters): void {
|
||||
//
|
||||
}
|
||||
|
||||
layout(width: number, height: number): void {
|
||||
//
|
||||
}
|
||||
update(event: PanelUpdateEvent): void {
|
||||
//
|
||||
}
|
||||
|
||||
toJSON(): object {
|
||||
return {};
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
describe('contentContainer', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
test('basic focus test', () => {
|
||||
let focus = 0;
|
||||
let blur = 0;
|
||||
|
||||
const disposable = new CompositeDisposable();
|
||||
|
||||
const overlayRenderContainer = new OverlayRenderContainer(
|
||||
document.createElement('div'),
|
||||
fromPartial<DockviewComponent>({})
|
||||
);
|
||||
|
||||
const cut = new ContentContainer(
|
||||
fromPartial<DockviewComponent>({
|
||||
renderer: 'onlyWhenVisible',
|
||||
overlayRenderContainer,
|
||||
}),
|
||||
fromPartial<DockviewGroupPanelModel>({
|
||||
renderContainer: overlayRenderContainer,
|
||||
})
|
||||
);
|
||||
|
||||
disposable.addDisposables(
|
||||
cut.onDidFocus(() => {
|
||||
focus++;
|
||||
}),
|
||||
cut.onDidBlur(() => {
|
||||
blur++;
|
||||
})
|
||||
);
|
||||
|
||||
const contentRenderer = new TestContentRenderer('id-1');
|
||||
|
||||
const panel = fromPartial<IDockviewPanel>({
|
||||
view: {
|
||||
content: contentRenderer,
|
||||
},
|
||||
api: { renderer: 'onlyWhenVisible' },
|
||||
});
|
||||
|
||||
cut.openPanel(panel as IDockviewPanel);
|
||||
|
||||
expect(focus).toBe(0);
|
||||
expect(blur).toBe(0);
|
||||
|
||||
// container has focus within
|
||||
fireEvent.focus(contentRenderer.element);
|
||||
expect(focus).toBe(1);
|
||||
expect(blur).toBe(0);
|
||||
|
||||
// container looses focus
|
||||
fireEvent.blur(contentRenderer.element);
|
||||
jest.runAllTimers();
|
||||
expect(focus).toBe(1);
|
||||
expect(blur).toBe(1);
|
||||
|
||||
const contentRenderer2 = new TestContentRenderer('id-2');
|
||||
|
||||
const panel2 = {
|
||||
view: {
|
||||
content: contentRenderer2,
|
||||
} as Partial<IDockviewPanelModel>,
|
||||
api: { renderer: 'onlyWhenVisible' },
|
||||
} as Partial<IDockviewPanel>;
|
||||
|
||||
cut.openPanel(panel2 as IDockviewPanel);
|
||||
// expect(focus).toBe(2);
|
||||
// expect(blur).toBe(1);
|
||||
|
||||
// new panel recieves focus
|
||||
fireEvent.focus(contentRenderer2.element);
|
||||
expect(focus).toBe(2);
|
||||
expect(blur).toBe(1);
|
||||
|
||||
// new panel looses focus
|
||||
fireEvent.blur(contentRenderer2.element);
|
||||
jest.runAllTimers();
|
||||
expect(focus).toBe(2);
|
||||
expect(blur).toBe(2);
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test("that panels renderered as 'onlyWhenVisible' are removed when closed", () => {
|
||||
const overlayRenderContainer = fromPartial<OverlayRenderContainer>({
|
||||
detatch: jest.fn(),
|
||||
});
|
||||
|
||||
const cut = new ContentContainer(
|
||||
fromPartial<DockviewComponent>({
|
||||
overlayRenderContainer,
|
||||
}),
|
||||
fromPartial<DockviewGroupPanelModel>({
|
||||
renderContainer: overlayRenderContainer,
|
||||
})
|
||||
);
|
||||
|
||||
const panel1 = fromPartial<IDockviewPanel>({
|
||||
api: {
|
||||
renderer: 'onlyWhenVisible',
|
||||
},
|
||||
view: { content: new TestContentRenderer('panel_1') },
|
||||
});
|
||||
|
||||
const panel2 = fromPartial<IDockviewPanel>({
|
||||
api: {
|
||||
renderer: 'onlyWhenVisible',
|
||||
},
|
||||
view: { content: new TestContentRenderer('panel_2') },
|
||||
});
|
||||
|
||||
cut.openPanel(panel1);
|
||||
|
||||
expect(panel1.view.content.element.parentElement).toBe(cut.element);
|
||||
expect(cut.element.childNodes.length).toBe(1);
|
||||
|
||||
cut.openPanel(panel2);
|
||||
|
||||
expect(panel1.view.content.element.parentElement).toBeNull();
|
||||
expect(panel2.view.content.element.parentElement).toBe(cut.element);
|
||||
expect(cut.element.childNodes.length).toBe(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,310 @@
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import {
|
||||
LocalSelectionTransfer,
|
||||
PanelTransfer,
|
||||
} from '../../../dnd/dataTransfer';
|
||||
import { DockviewComponent } from '../../../dockview/dockviewComponent';
|
||||
import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel';
|
||||
import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel';
|
||||
import { Tab } from '../../../dockview/components/tab/tab';
|
||||
import { IDockviewPanel } from '../../../dockview/dockviewPanel';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
|
||||
describe('tab', () => {
|
||||
test('that empty tab has inactive-tab class', () => {
|
||||
const accessorMock = jest.fn();
|
||||
const groupMock = jest.fn();
|
||||
|
||||
const cut = new Tab(
|
||||
{ id: 'panelId' } as IDockviewPanel,
|
||||
new accessorMock(),
|
||||
new groupMock()
|
||||
);
|
||||
|
||||
expect(cut.element.className).toBe('dv-tab dv-inactive-tab');
|
||||
});
|
||||
|
||||
test('that active tab has active-tab class', () => {
|
||||
const accessorMock = jest.fn();
|
||||
const groupMock = jest.fn();
|
||||
|
||||
const cut = new Tab(
|
||||
{ id: 'panelId' } as IDockviewPanel,
|
||||
new accessorMock(),
|
||||
new groupMock()
|
||||
);
|
||||
|
||||
cut.setActive(true);
|
||||
expect(cut.element.className).toBe('dv-tab dv-active-tab');
|
||||
|
||||
cut.setActive(false);
|
||||
expect(cut.element.className).toBe('dv-tab dv-inactive-tab');
|
||||
});
|
||||
|
||||
test('that an external event does not render a drop target and calls through to the group model', () => {
|
||||
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
|
||||
return {
|
||||
id: 'testcomponentid',
|
||||
};
|
||||
});
|
||||
|
||||
const groupView = fromPartial<DockviewGroupPanelModel>({
|
||||
canDisplayOverlay: jest.fn(),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
};
|
||||
});
|
||||
|
||||
const accessor = new accessorMock() as DockviewComponent;
|
||||
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
|
||||
|
||||
const cut = new Tab(
|
||||
{ id: 'panelId' } as IDockviewPanel,
|
||||
accessor,
|
||||
groupPanel
|
||||
);
|
||||
|
||||
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
|
||||
fireEvent.dragEnter(cut.element);
|
||||
fireEvent.dragOver(cut.element);
|
||||
|
||||
expect(groupView.canDisplayOverlay).toHaveBeenCalled();
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('that if you drag over yourself a drop target is shown', () => {
|
||||
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
|
||||
return {
|
||||
id: 'testcomponentid',
|
||||
};
|
||||
});
|
||||
|
||||
const groupView = fromPartial<DockviewGroupPanelModel>({
|
||||
canDisplayOverlay: jest.fn(),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
};
|
||||
});
|
||||
|
||||
const accessor = new accessorMock() as DockviewComponent;
|
||||
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
|
||||
|
||||
const cut = new Tab(
|
||||
{ id: 'panel1' } as IDockviewPanel,
|
||||
accessor,
|
||||
groupPanel
|
||||
);
|
||||
|
||||
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
|
||||
LocalSelectionTransfer.getInstance().setData(
|
||||
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel1')],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
fireEvent.dragEnter(cut.element);
|
||||
fireEvent.dragOver(cut.element);
|
||||
|
||||
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('that if you drag over another tab a drop target is shown', () => {
|
||||
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
|
||||
return {
|
||||
id: 'testcomponentid',
|
||||
};
|
||||
});
|
||||
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
||||
() => {
|
||||
return {
|
||||
canDisplayOverlay: jest.fn(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const groupView = new groupviewMock() as DockviewGroupPanelModel;
|
||||
|
||||
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
};
|
||||
});
|
||||
|
||||
const accessor = new accessorMock() as DockviewComponent;
|
||||
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
|
||||
|
||||
const cut = new Tab(
|
||||
{ id: 'panel1' } as IDockviewPanel,
|
||||
accessor,
|
||||
groupPanel
|
||||
);
|
||||
|
||||
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
|
||||
LocalSelectionTransfer.getInstance().setData(
|
||||
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel2')],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
fireEvent.dragEnter(cut.element);
|
||||
fireEvent.dragOver(cut.element);
|
||||
|
||||
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('that dropping on a tab with the same id but from a different component should not render a drop over and call through to the group model', () => {
|
||||
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
|
||||
return {
|
||||
id: 'testcomponentid',
|
||||
};
|
||||
});
|
||||
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
||||
() => {
|
||||
return {
|
||||
canDisplayOverlay: jest.fn(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const groupView = new groupviewMock() as DockviewGroupPanelModel;
|
||||
|
||||
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
};
|
||||
});
|
||||
|
||||
const accessor = new accessorMock() as DockviewComponent;
|
||||
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
|
||||
|
||||
const cut = new Tab(
|
||||
{ id: 'panel1' } as IDockviewPanel,
|
||||
accessor,
|
||||
groupPanel
|
||||
);
|
||||
|
||||
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
|
||||
LocalSelectionTransfer.getInstance().setData(
|
||||
[
|
||||
new PanelTransfer(
|
||||
'anothercomponentid',
|
||||
'anothergroupid',
|
||||
'panel1'
|
||||
),
|
||||
],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
fireEvent.dragEnter(cut.element);
|
||||
fireEvent.dragOver(cut.element);
|
||||
|
||||
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('that dropping on a tab from a different component should not render a drop over and call through to the group model', () => {
|
||||
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
|
||||
return {
|
||||
id: 'testcomponentid',
|
||||
};
|
||||
});
|
||||
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
||||
() => {
|
||||
return {
|
||||
canDisplayOverlay: jest.fn(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const groupView = new groupviewMock() as DockviewGroupPanelModel;
|
||||
|
||||
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
};
|
||||
});
|
||||
|
||||
const accessor = new accessorMock() as DockviewComponent;
|
||||
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
|
||||
|
||||
const cut = new Tab(
|
||||
{ id: 'panel1' } as IDockviewPanel,
|
||||
accessor,
|
||||
groupPanel
|
||||
);
|
||||
|
||||
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
|
||||
LocalSelectionTransfer.getInstance().setData(
|
||||
[
|
||||
new PanelTransfer(
|
||||
'anothercomponentid',
|
||||
'anothergroupid',
|
||||
'panel2'
|
||||
),
|
||||
],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
fireEvent.dragEnter(cut.element);
|
||||
fireEvent.dragOver(cut.element);
|
||||
|
||||
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
@ -0,0 +1,63 @@
|
||||
import { DockviewApi } from '../../../../api/component.api';
|
||||
import { DockviewPanelApi, TitleEvent } from '../../../../api/dockviewPanelApi';
|
||||
import { DefaultTab } from '../../../../dockview/components/tab/defaultTab';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { Emitter } from '../../../../events';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
|
||||
describe('defaultTab', () => {
|
||||
test('that title updates', () => {
|
||||
const cut = new DefaultTab();
|
||||
|
||||
let el = cut.element.querySelector('.dv-default-tab-content');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el!.textContent).toBe('');
|
||||
|
||||
const onDidTitleChange = new Emitter<TitleEvent>();
|
||||
|
||||
const api = fromPartial<DockviewPanelApi>({
|
||||
onDidTitleChange: onDidTitleChange.event,
|
||||
});
|
||||
const containerApi = fromPartial<DockviewApi>({});
|
||||
|
||||
cut.init({
|
||||
api,
|
||||
containerApi,
|
||||
params: {},
|
||||
title: 'title_abc',
|
||||
});
|
||||
|
||||
el = cut.element.querySelector('.dv-default-tab-content');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el!.textContent).toBe('title_abc');
|
||||
|
||||
onDidTitleChange.fire({ title: 'title_def' });
|
||||
|
||||
expect(el!.textContent).toBe('title_def');
|
||||
});
|
||||
|
||||
test('that click closes tab', () => {
|
||||
const cut = new DefaultTab();
|
||||
|
||||
const api = fromPartial<DockviewPanelApi>({
|
||||
onDidTitleChange: jest.fn(),
|
||||
close: jest.fn(),
|
||||
});
|
||||
const containerApi = fromPartial<DockviewApi>({});
|
||||
|
||||
cut.init({
|
||||
api,
|
||||
containerApi,
|
||||
params: {},
|
||||
title: 'title_abc',
|
||||
});
|
||||
|
||||
let el = cut.element.querySelector('.dv-default-tab-action');
|
||||
|
||||
fireEvent.pointerDown(el!);
|
||||
expect(api.close).toHaveBeenCalledTimes(0);
|
||||
|
||||
fireEvent.click(el!);
|
||||
expect(api.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,66 @@
|
||||
import { Tabs } from '../../../../dockview/components/titlebar/tabs';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
|
||||
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
|
||||
|
||||
describe('tabs', () => {
|
||||
describe('disableCustomScrollbars', () => {
|
||||
test('enabled by default', () => {
|
||||
const cut = new Tabs(
|
||||
fromPartial<DockviewGroupPanel>({}),
|
||||
fromPartial<DockviewComponent>({
|
||||
options: {},
|
||||
}),
|
||||
{
|
||||
showTabsOverflowControl: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
cut.element.querySelectorAll(
|
||||
'.dv-scrollable > .dv-tabs-container'
|
||||
).length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('enabled when disabled flag is false', () => {
|
||||
const cut = new Tabs(
|
||||
fromPartial<DockviewGroupPanel>({}),
|
||||
fromPartial<DockviewComponent>({
|
||||
options: {
|
||||
scrollbars: 'custom',
|
||||
},
|
||||
}),
|
||||
{
|
||||
showTabsOverflowControl: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
cut.element.querySelectorAll(
|
||||
'.dv-scrollable > .dv-tabs-container'
|
||||
).length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('disabled when disabled flag is true', () => {
|
||||
const cut = new Tabs(
|
||||
fromPartial<DockviewGroupPanel>({}),
|
||||
fromPartial<DockviewComponent>({
|
||||
options: {
|
||||
scrollbars: 'native',
|
||||
},
|
||||
}),
|
||||
{
|
||||
showTabsOverflowControl: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
cut.element.querySelectorAll(
|
||||
'.dv-scrollable > .dv-tabs-container'
|
||||
).length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,867 @@
|
||||
import {
|
||||
LocalSelectionTransfer,
|
||||
PanelTransfer,
|
||||
} from '../../../../dnd/dataTransfer';
|
||||
import { TabsContainer } from '../../../../dockview/components/titlebar/tabsContainer';
|
||||
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
|
||||
import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
|
||||
import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import { TestPanel } from '../../dockviewGroupPanelModel.spec';
|
||||
import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { DockviewPanelApi } from '../../../../api/dockviewPanelApi';
|
||||
|
||||
describe('tabsContainer', () => {
|
||||
test('that an external event does not render a drop target and calls through to the group mode', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
||||
() => {
|
||||
return {
|
||||
canDisplayOverlay: jest.fn(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const groupView = new groupviewMock() as DockviewGroupPanelModel;
|
||||
|
||||
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
|
||||
return {
|
||||
model: groupView,
|
||||
};
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
const emptySpace = cut.element
|
||||
.getElementsByClassName('dv-void-container')
|
||||
.item(0) as HTMLElement;
|
||||
|
||||
if (!emptySpace!) {
|
||||
fail('element not found');
|
||||
}
|
||||
|
||||
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
|
||||
fireEvent.dragEnter(emptySpace!);
|
||||
fireEvent.dragOver(emptySpace!);
|
||||
|
||||
expect(groupView.canDisplayOverlay).toHaveBeenCalled();
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('that a drag over event from another tab should render a drop target', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
id: 'testcomponentid',
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const dropTargetContainer = document.createElement('div');
|
||||
|
||||
const groupView = fromPartial<DockviewGroupPanelModel>({
|
||||
canDisplayOverlay: jest.fn(),
|
||||
// dropTargetContainer: new DropTargetAnchorContainer(
|
||||
// dropTargetContainer
|
||||
// ),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
panels: [],
|
||||
};
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
const emptySpace = cut.element
|
||||
.getElementsByClassName('dv-void-container')
|
||||
.item(0) as HTMLElement;
|
||||
|
||||
if (!emptySpace!) {
|
||||
fail('element not found');
|
||||
}
|
||||
|
||||
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
|
||||
LocalSelectionTransfer.getInstance().setData(
|
||||
[
|
||||
new PanelTransfer(
|
||||
'testcomponentid',
|
||||
'anothergroupid',
|
||||
'anotherpanelid'
|
||||
),
|
||||
],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
fireEvent.dragEnter(emptySpace!);
|
||||
fireEvent.dragOver(emptySpace!);
|
||||
|
||||
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
|
||||
).toBe(1);
|
||||
// expect(
|
||||
// dropTargetContainer.getElementsByClassName('dv-drop-target-anchor')
|
||||
// .length
|
||||
// ).toBe(1);
|
||||
});
|
||||
|
||||
test('that dropping over the empty space should render a drop target', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
id: 'testcomponentid',
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
||||
() => {
|
||||
return {
|
||||
canDisplayOverlay: jest.fn(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const groupView = new groupviewMock() as DockviewGroupPanelModel;
|
||||
|
||||
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
panels: [],
|
||||
};
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
|
||||
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
|
||||
|
||||
const emptySpace = cut.element
|
||||
.getElementsByClassName('dv-void-container')
|
||||
.item(0) as HTMLElement;
|
||||
|
||||
if (!emptySpace!) {
|
||||
fail('element not found');
|
||||
}
|
||||
|
||||
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
|
||||
LocalSelectionTransfer.getInstance().setData(
|
||||
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel2')],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
fireEvent.dragEnter(emptySpace!);
|
||||
fireEvent.dragOver(emptySpace!);
|
||||
|
||||
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('that dropping the first tab should render a drop target', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
id: 'testcomponentid',
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
||||
() => {
|
||||
return {
|
||||
canDisplayOverlay: jest.fn(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const groupView = new groupviewMock() as DockviewGroupPanelModel;
|
||||
|
||||
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
panels: [],
|
||||
};
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
|
||||
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
|
||||
|
||||
const emptySpace = cut.element
|
||||
.getElementsByClassName('dv-void-container')
|
||||
.item(0) as HTMLElement;
|
||||
|
||||
if (!emptySpace!) {
|
||||
fail('element not found');
|
||||
}
|
||||
|
||||
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
|
||||
LocalSelectionTransfer.getInstance().setData(
|
||||
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel1')],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
fireEvent.dragEnter(emptySpace!);
|
||||
fireEvent.dragOver(emptySpace!);
|
||||
|
||||
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('that dropping a tab from another component should not render a drop target', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
id: 'testcomponentid',
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
|
||||
() => {
|
||||
return {
|
||||
canDisplayOverlay: jest.fn(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const groupView = new groupviewMock() as DockviewGroupPanelModel;
|
||||
|
||||
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
};
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
|
||||
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
|
||||
|
||||
const emptySpace = cut.element
|
||||
.getElementsByClassName('dv-void-container')
|
||||
.item(0) as HTMLElement;
|
||||
|
||||
if (!emptySpace!) {
|
||||
fail('element not found');
|
||||
}
|
||||
|
||||
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
|
||||
LocalSelectionTransfer.getInstance().setData(
|
||||
[
|
||||
new PanelTransfer(
|
||||
'anothercomponentid',
|
||||
'anothergroupid',
|
||||
'panel1'
|
||||
),
|
||||
],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
fireEvent.dragEnter(emptySpace!);
|
||||
fireEvent.dragOver(emptySpace!);
|
||||
|
||||
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('left actions', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
id: 'testcomponentid',
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
return (<Partial<DockviewGroupPanel>>{}) as DockviewGroupPanel;
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock();
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
let query = cut.element.querySelectorAll(
|
||||
'.dv-tabs-and-actions-container > .dv-left-actions-container'
|
||||
);
|
||||
|
||||
expect(query.length).toBe(1);
|
||||
expect(query[0].children.length).toBe(0);
|
||||
|
||||
// add left action
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.className = 'test-left-actions-element';
|
||||
cut.setLeftActionsElement(left);
|
||||
|
||||
query = cut.element.querySelectorAll(
|
||||
'.dv-tabs-and-actions-container > .dv-left-actions-container'
|
||||
);
|
||||
expect(query.length).toBe(1);
|
||||
expect(query[0].children.item(0)?.className).toBe(
|
||||
'test-left-actions-element'
|
||||
);
|
||||
expect(query[0].children.length).toBe(1);
|
||||
|
||||
// add left action
|
||||
|
||||
const left2 = document.createElement('div');
|
||||
left2.className = 'test-left-actions-element-2';
|
||||
cut.setLeftActionsElement(left2);
|
||||
|
||||
query = cut.element.querySelectorAll(
|
||||
'.dv-tabs-and-actions-container > .dv-left-actions-container'
|
||||
);
|
||||
expect(query.length).toBe(1);
|
||||
expect(query[0].children.item(0)?.className).toBe(
|
||||
'test-left-actions-element-2'
|
||||
);
|
||||
expect(query[0].children.length).toBe(1);
|
||||
|
||||
// remove left action
|
||||
|
||||
cut.setLeftActionsElement(undefined);
|
||||
query = cut.element.querySelectorAll(
|
||||
'.dv-tabs-and-actions-container > .dv-left-actions-container'
|
||||
);
|
||||
|
||||
expect(query.length).toBe(1);
|
||||
expect(query[0].children.length).toBe(0);
|
||||
});
|
||||
|
||||
test('right actions', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
id: 'testcomponentid',
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
return (<Partial<DockviewGroupPanel>>{}) as DockviewGroupPanel;
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock();
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
let query = cut.element.querySelectorAll(
|
||||
'.dv-tabs-and-actions-container > .dv-right-actions-container'
|
||||
);
|
||||
|
||||
expect(query.length).toBe(1);
|
||||
expect(query[0].children.length).toBe(0);
|
||||
|
||||
// add right action
|
||||
|
||||
const right = document.createElement('div');
|
||||
right.className = 'test-right-actions-element';
|
||||
cut.setRightActionsElement(right);
|
||||
|
||||
query = cut.element.querySelectorAll(
|
||||
'.dv-tabs-and-actions-container > .dv-right-actions-container'
|
||||
);
|
||||
expect(query.length).toBe(1);
|
||||
expect(query[0].children.item(0)?.className).toBe(
|
||||
'test-right-actions-element'
|
||||
);
|
||||
expect(query[0].children.length).toBe(1);
|
||||
|
||||
// add right action
|
||||
|
||||
const right2 = document.createElement('div');
|
||||
right2.className = 'test-right-actions-element-2';
|
||||
cut.setRightActionsElement(right2);
|
||||
|
||||
query = cut.element.querySelectorAll(
|
||||
'.dv-tabs-and-actions-container > .dv-right-actions-container'
|
||||
);
|
||||
expect(query.length).toBe(1);
|
||||
expect(query[0].children.item(0)?.className).toBe(
|
||||
'test-right-actions-element-2'
|
||||
);
|
||||
expect(query[0].children.length).toBe(1);
|
||||
|
||||
// remove right action
|
||||
|
||||
cut.setRightActionsElement(undefined);
|
||||
query = cut.element.querySelectorAll(
|
||||
'.dv-tabs-and-actions-container > .dv-right-actions-container'
|
||||
);
|
||||
|
||||
expect(query.length).toBe(1);
|
||||
expect(query[0].children.length).toBe(0);
|
||||
});
|
||||
|
||||
test('that a tab will become floating when clicked if not floating and shift is selected', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
options: {},
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
element: document.createElement('div'),
|
||||
addFloatingGroup: jest.fn(),
|
||||
doSetGroupActive: jest.fn(),
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
return (<Partial<DockviewGroupPanel>>{
|
||||
api: { location: { type: 'grid' } } as any,
|
||||
}) as DockviewGroupPanel;
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock();
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
const container = cut.element.querySelector('.dv-void-container')!;
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
|
||||
() => {
|
||||
return { top: 50, left: 100, width: 0, height: 0 } as any;
|
||||
}
|
||||
);
|
||||
jest.spyOn(
|
||||
accessor.element,
|
||||
'getBoundingClientRect'
|
||||
).mockImplementation(() => {
|
||||
return { top: 10, left: 20, width: 0, height: 0 } as any;
|
||||
});
|
||||
|
||||
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
|
||||
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
|
||||
fireEvent(container, event);
|
||||
|
||||
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(groupPanel);
|
||||
expect(accessor.addFloatingGroup).toHaveBeenCalledWith(groupPanel, {
|
||||
x: 100,
|
||||
y: 60,
|
||||
inDragMode: true,
|
||||
});
|
||||
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
|
||||
expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const event2 = new KeyboardEvent('pointerdown', { shiftKey: false });
|
||||
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
|
||||
fireEvent(container, event2);
|
||||
|
||||
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
|
||||
expect(eventPreventDefaultSpy2).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('that a tab that is already floating cannot be floated again', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
options: {},
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
element: document.createElement('div'),
|
||||
addFloatingGroup: jest.fn(),
|
||||
doSetGroupActive: jest.fn(),
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
return (<Partial<DockviewGroupPanel>>{
|
||||
api: { location: { type: 'floating' } } as any,
|
||||
}) as DockviewGroupPanel;
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock();
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
const container = cut.element.querySelector('.dv-void-container')!;
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
|
||||
() => {
|
||||
return { top: 50, left: 100, width: 0, height: 0 } as any;
|
||||
}
|
||||
);
|
||||
jest.spyOn(
|
||||
accessor.element,
|
||||
'getBoundingClientRect'
|
||||
).mockImplementation(() => {
|
||||
return { top: 10, left: 20, width: 0, height: 0 } as any;
|
||||
});
|
||||
|
||||
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
|
||||
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
|
||||
fireEvent(container, event);
|
||||
|
||||
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(groupPanel);
|
||||
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0);
|
||||
expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(0);
|
||||
|
||||
const event2 = new KeyboardEvent('pointerdown', { shiftKey: false });
|
||||
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
|
||||
fireEvent(container, event2);
|
||||
|
||||
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0);
|
||||
expect(eventPreventDefaultSpy2).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('that selecting a tab with shift down will move that tab into a new floating group', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
options: {},
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
element: document.createElement('div'),
|
||||
addFloatingGroup: jest.fn(),
|
||||
getGroupPanel: jest.fn(),
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
return (<Partial<DockviewGroupPanel>>{
|
||||
api: { location: { type: 'floating' } } as any,
|
||||
model: {} as any,
|
||||
}) as DockviewGroupPanel;
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock();
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
const createPanel = (id: string) =>
|
||||
fromPartial<IDockviewPanel>({
|
||||
id,
|
||||
view: {
|
||||
tab: {
|
||||
element: document.createElement('div'),
|
||||
},
|
||||
content: {
|
||||
element: document.createElement('div'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const panel = createPanel('test_id');
|
||||
cut.openPanel(panel);
|
||||
|
||||
const el = cut.element.querySelector('.dv-tab')!;
|
||||
expect(el).toBeTruthy();
|
||||
|
||||
const event = new KeyboardEvent('pointerdown', { shiftKey: true });
|
||||
const preventDefaultSpy = jest.spyOn(event, 'preventDefault');
|
||||
fireEvent(el, event);
|
||||
|
||||
// a floating group with a single tab shouldn't be eligible
|
||||
expect(preventDefaultSpy).toHaveBeenCalledTimes(0);
|
||||
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0);
|
||||
|
||||
const panel2 = createPanel('test_id_2');
|
||||
cut.openPanel(panel2);
|
||||
fireEvent(el, event);
|
||||
|
||||
expect(preventDefaultSpy).toHaveBeenCalledTimes(1);
|
||||
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('pre header actions', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
options: {},
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
element: document.createElement('div'),
|
||||
addFloatingGroup: jest.fn(),
|
||||
getGroupPanel: jest.fn(),
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
return (<Partial<DockviewGroupPanel>>{
|
||||
api: { location: { type: 'grid' } } as any,
|
||||
model: {} as any,
|
||||
}) as DockviewGroupPanel;
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock();
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => {
|
||||
const partial: Partial<IDockviewPanel> = {
|
||||
id,
|
||||
|
||||
view: {
|
||||
tab: {
|
||||
element: document.createElement('div'),
|
||||
} as any,
|
||||
content: {
|
||||
element: document.createElement('div'),
|
||||
} as any,
|
||||
} as any,
|
||||
};
|
||||
return partial as IDockviewPanel;
|
||||
});
|
||||
|
||||
const panel = new panelMock('test_id');
|
||||
cut.openPanel(panel);
|
||||
|
||||
let result = cut.element.querySelector('.dv-pre-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(0);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
cut.setPrefixActionsElement(actions);
|
||||
|
||||
result = cut.element.querySelector('.dv-pre-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(1);
|
||||
expect(result!.childNodes.item(0)).toBe(actions);
|
||||
|
||||
const updatedActions = document.createElement('div');
|
||||
cut.setPrefixActionsElement(updatedActions);
|
||||
|
||||
result = cut.element.querySelector('.dv-pre-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(1);
|
||||
expect(result!.childNodes.item(0)).toBe(updatedActions);
|
||||
|
||||
cut.setPrefixActionsElement(undefined);
|
||||
|
||||
result = cut.element.querySelector('.dv-pre-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
test('left header actions', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
options: {},
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
element: document.createElement('div'),
|
||||
addFloatingGroup: jest.fn(),
|
||||
getGroupPanel: jest.fn(),
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
return (<Partial<DockviewGroupPanel>>{
|
||||
api: { location: { type: 'grid' } } as any,
|
||||
model: {} as any,
|
||||
}) as DockviewGroupPanel;
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock();
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => {
|
||||
const partial: Partial<IDockviewPanel> = {
|
||||
id,
|
||||
|
||||
view: {
|
||||
tab: {
|
||||
element: document.createElement('div'),
|
||||
} as any,
|
||||
content: {
|
||||
element: document.createElement('div'),
|
||||
} as any,
|
||||
} as any,
|
||||
};
|
||||
return partial as IDockviewPanel;
|
||||
});
|
||||
|
||||
const panel = new panelMock('test_id');
|
||||
cut.openPanel(panel);
|
||||
|
||||
let result = cut.element.querySelector('.dv-left-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(0);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
cut.setLeftActionsElement(actions);
|
||||
|
||||
result = cut.element.querySelector('.dv-left-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(1);
|
||||
expect(result!.childNodes.item(0)).toBe(actions);
|
||||
|
||||
const updatedActions = document.createElement('div');
|
||||
cut.setLeftActionsElement(updatedActions);
|
||||
|
||||
result = cut.element.querySelector('.dv-left-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(1);
|
||||
expect(result!.childNodes.item(0)).toBe(updatedActions);
|
||||
|
||||
cut.setLeftActionsElement(undefined);
|
||||
|
||||
result = cut.element.querySelector('.dv-left-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
test('right header actions', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
options: {},
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
element: document.createElement('div'),
|
||||
addFloatingGroup: jest.fn(),
|
||||
getGroupPanel: jest.fn(),
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
|
||||
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
|
||||
return (<Partial<DockviewGroupPanel>>{
|
||||
api: { location: { type: 'grid' } } as any,
|
||||
model: {} as any,
|
||||
}) as DockviewGroupPanel;
|
||||
});
|
||||
|
||||
const groupPanel = new groupPanelMock();
|
||||
|
||||
const cut = new TabsContainer(accessor, groupPanel);
|
||||
|
||||
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => {
|
||||
const partial: Partial<IDockviewPanel> = {
|
||||
id,
|
||||
|
||||
view: {
|
||||
tab: {
|
||||
element: document.createElement('div'),
|
||||
} as any,
|
||||
content: {
|
||||
element: document.createElement('div'),
|
||||
} as any,
|
||||
} as any,
|
||||
};
|
||||
return partial as IDockviewPanel;
|
||||
});
|
||||
|
||||
const panel = new panelMock('test_id');
|
||||
cut.openPanel(panel);
|
||||
|
||||
let result = cut.element.querySelector('.dv-right-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(0);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
cut.setRightActionsElement(actions);
|
||||
|
||||
result = cut.element.querySelector('.dv-right-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(1);
|
||||
expect(result!.childNodes.item(0)).toBe(actions);
|
||||
|
||||
const updatedActions = document.createElement('div');
|
||||
cut.setRightActionsElement(updatedActions);
|
||||
|
||||
result = cut.element.querySelector('.dv-right-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(1);
|
||||
expect(result!.childNodes.item(0)).toBe(updatedActions);
|
||||
|
||||
cut.setRightActionsElement(undefined);
|
||||
|
||||
result = cut.element.querySelector('.dv-right-actions-container');
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.childNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
test('class dv-single-tab is present when only one tab exists`', () => {
|
||||
const cut = new TabsContainer(
|
||||
fromPartial<DockviewComponent>({
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
}),
|
||||
fromPartial<DockviewGroupPanel>({})
|
||||
);
|
||||
|
||||
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
|
||||
|
||||
const panel1 = new TestPanel(
|
||||
'panel_1',
|
||||
fromPartial<DockviewPanelApi>({})
|
||||
);
|
||||
cut.openPanel(panel1);
|
||||
expect(cut.element.classList.contains('dv-single-tab')).toBeTruthy();
|
||||
|
||||
const panel2 = new TestPanel(
|
||||
'panel_2',
|
||||
fromPartial<DockviewPanelApi>({})
|
||||
);
|
||||
cut.openPanel(panel2);
|
||||
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
|
||||
|
||||
cut.closePanel(panel1);
|
||||
expect(cut.element.classList.contains('dv-single-tab')).toBeTruthy();
|
||||
|
||||
cut.closePanel(panel2);
|
||||
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
import { VoidContainer } from '../../../../dockview/components/titlebar/voidContainer';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
|
||||
import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
|
||||
describe('voidContainer', () => {
|
||||
test('that `pointerDown` triggers activation', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
doSetGroupActive: jest.fn(),
|
||||
});
|
||||
const group = fromPartial<DockviewGroupPanel>({});
|
||||
const cut = new VoidContainer(accessor, group);
|
||||
|
||||
expect(accessor.doSetGroupActive).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.pointerDown(cut.element);
|
||||
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(group);
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,190 @@
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { GroupOptions } from '../../dockview/dockviewGroupPanelModel';
|
||||
import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
|
||||
import { DockviewPanelModelMock } from '../__mocks__/mockDockviewPanelModel';
|
||||
import { IContentRenderer, ITabRenderer } from '../../dockview/types';
|
||||
import { OverlayRenderContainer } from '../../overlay/overlayRenderContainer';
|
||||
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
|
||||
import { ContentContainer } from '../../dockview/components/panel/content';
|
||||
|
||||
describe('dockviewGroupPanel', () => {
|
||||
test('default minimum/maximium width/height', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
onDidActivePanelChange: jest.fn(),
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
const options = fromPartial<GroupOptions>({});
|
||||
const cut = new DockviewGroupPanel(accessor, 'test_id', options);
|
||||
|
||||
expect(cut.minimumWidth).toBe(100);
|
||||
expect(cut.minimumHeight).toBe(100);
|
||||
expect(cut.maximumHeight).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(cut.maximumWidth).toBe(Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
|
||||
test('that onDidActivePanelChange is configured at inline', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
onDidActivePanelChange: jest.fn(),
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
api: {},
|
||||
renderer: 'always',
|
||||
overlayRenderContainer: {
|
||||
attach: jest.fn(),
|
||||
detatch: jest.fn(),
|
||||
},
|
||||
doSetGroupActive: jest.fn(),
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
const options = fromPartial<GroupOptions>({});
|
||||
|
||||
const cut = new DockviewGroupPanel(accessor, 'test_id', options);
|
||||
|
||||
let counter = 0;
|
||||
|
||||
cut.api.onDidActivePanelChange((event) => {
|
||||
counter++;
|
||||
});
|
||||
|
||||
cut.model.openPanel(
|
||||
fromPartial<IDockviewPanel>({
|
||||
updateParentGroup: jest.fn(),
|
||||
view: {
|
||||
tab: { element: document.createElement('div') },
|
||||
content: new ContentContainer(accessor, cut.model),
|
||||
},
|
||||
api: {
|
||||
renderer: 'onlyWhenVisible',
|
||||
onDidTitleChange: jest.fn(),
|
||||
onDidParametersChange: jest.fn(),
|
||||
},
|
||||
layout: jest.fn(),
|
||||
runEvents: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
test('group constraints', () => {
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
onDidActivePanelChange: jest.fn(),
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
doSetGroupActive: jest.fn(),
|
||||
overlayRenderContainer: fromPartial<OverlayRenderContainer>({
|
||||
attach: jest.fn(),
|
||||
detatch: jest.fn(),
|
||||
}),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
});
|
||||
const options = fromPartial<GroupOptions>({});
|
||||
const cut = new DockviewGroupPanel(accessor, 'test_id', options);
|
||||
|
||||
cut.api.setConstraints({
|
||||
minimumHeight: 10,
|
||||
maximumHeight: 100,
|
||||
minimumWidth: 20,
|
||||
maximumWidth: 200,
|
||||
});
|
||||
|
||||
// initial constraints
|
||||
|
||||
expect(cut.minimumWidth).toBe(20);
|
||||
expect(cut.minimumHeight).toBe(10);
|
||||
expect(cut.maximumHeight).toBe(100);
|
||||
expect(cut.maximumWidth).toBe(200);
|
||||
|
||||
const panelModel = new DockviewPanelModelMock(
|
||||
'content_component',
|
||||
fromPartial<IContentRenderer>({
|
||||
element: document.createElement('div'),
|
||||
}),
|
||||
'tab_component',
|
||||
fromPartial<ITabRenderer>({
|
||||
element: document.createElement('div'),
|
||||
})
|
||||
);
|
||||
|
||||
const panel = new DockviewPanel(
|
||||
'panel_id',
|
||||
'component_id',
|
||||
undefined,
|
||||
accessor,
|
||||
accessor.api,
|
||||
cut,
|
||||
panelModel,
|
||||
{
|
||||
renderer: 'onlyWhenVisible',
|
||||
minimumWidth: 21,
|
||||
minimumHeight: 11,
|
||||
maximumHeight: 101,
|
||||
maximumWidth: 201,
|
||||
}
|
||||
);
|
||||
|
||||
cut.model.openPanel(panel);
|
||||
|
||||
// active panel constraints
|
||||
|
||||
expect(cut.minimumWidth).toBe(21);
|
||||
expect(cut.minimumHeight).toBe(11);
|
||||
expect(cut.maximumHeight).toBe(101);
|
||||
expect(cut.maximumWidth).toBe(201);
|
||||
|
||||
const panel2 = new DockviewPanel(
|
||||
'panel_id',
|
||||
'component_id',
|
||||
undefined,
|
||||
accessor,
|
||||
accessor.api,
|
||||
cut,
|
||||
panelModel,
|
||||
{
|
||||
renderer: 'onlyWhenVisible',
|
||||
minimumWidth: 22,
|
||||
minimumHeight: 12,
|
||||
maximumHeight: 102,
|
||||
maximumWidth: 202,
|
||||
}
|
||||
);
|
||||
|
||||
cut.model.openPanel(panel2);
|
||||
|
||||
// active panel constraints
|
||||
|
||||
expect(cut.minimumWidth).toBe(22);
|
||||
expect(cut.minimumHeight).toBe(12);
|
||||
expect(cut.maximumHeight).toBe(102);
|
||||
expect(cut.maximumWidth).toBe(202);
|
||||
|
||||
const panel3 = new DockviewPanel(
|
||||
'panel_id',
|
||||
'component_id',
|
||||
undefined,
|
||||
accessor,
|
||||
accessor.api,
|
||||
cut,
|
||||
panelModel,
|
||||
{
|
||||
renderer: 'onlyWhenVisible',
|
||||
}
|
||||
);
|
||||
|
||||
cut.model.openPanel(panel3);
|
||||
|
||||
// active panel without specified constraints so falls back to group constraints
|
||||
|
||||
expect(cut.minimumWidth).toBe(20);
|
||||
expect(cut.minimumHeight).toBe(10);
|
||||
expect(cut.maximumHeight).toBe(100);
|
||||
expect(cut.maximumWidth).toBe(200);
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,287 @@
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
import { DockviewApi } from '../../api/component.api';
|
||||
import { DockviewPanel } from '../../dockview/dockviewPanel';
|
||||
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
|
||||
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
|
||||
describe('dockviewPanel', () => {
|
||||
test('update title', () => {
|
||||
const api = fromPartial<DockviewApi>({});
|
||||
const accessor = fromPartial<DockviewComponent>({});
|
||||
const group = fromPartial<DockviewGroupPanel>({
|
||||
api: {
|
||||
onDidVisibilityChange: jest.fn(),
|
||||
onDidLocationChange: jest.fn(),
|
||||
onDidActiveChange: jest.fn(),
|
||||
},
|
||||
});
|
||||
const model = fromPartial<IDockviewPanelModel>({
|
||||
update: jest.fn(),
|
||||
init: jest.fn(),
|
||||
dispose: jest.fn(),
|
||||
});
|
||||
|
||||
const cut = new DockviewPanel(
|
||||
'fake-id',
|
||||
'fake-component',
|
||||
undefined,
|
||||
accessor,
|
||||
api,
|
||||
group,
|
||||
model,
|
||||
{
|
||||
renderer: 'onlyWhenVisible',
|
||||
}
|
||||
);
|
||||
|
||||
let latestTitle: string | undefined = undefined;
|
||||
|
||||
const disposable = cut.api.onDidTitleChange((event) => {
|
||||
latestTitle = event.title;
|
||||
});
|
||||
|
||||
expect(cut.title).toBeUndefined();
|
||||
|
||||
cut.init({ title: 'new title', params: {} });
|
||||
expect(latestTitle).toBe('new title');
|
||||
expect(cut.title).toBe('new title');
|
||||
|
||||
cut.setTitle('another title');
|
||||
expect(latestTitle).toBe('another title');
|
||||
expect(cut.title).toBe('another title');
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('that .setTitle updates the title', () => {
|
||||
const api = fromPartial<DockviewApi>({});
|
||||
const accessor = fromPartial<DockviewComponent>({});
|
||||
const group = fromPartial<DockviewGroupPanel>({
|
||||
api: {
|
||||
onDidVisibilityChange: jest.fn(),
|
||||
onDidLocationChange: jest.fn(),
|
||||
onDidActiveChange: jest.fn(),
|
||||
},
|
||||
});
|
||||
const model = fromPartial<IDockviewPanelModel>({
|
||||
update: jest.fn(),
|
||||
init: jest.fn(),
|
||||
});
|
||||
|
||||
const cut = new DockviewPanel(
|
||||
'fake-id',
|
||||
'fake-component',
|
||||
undefined,
|
||||
accessor,
|
||||
api,
|
||||
group,
|
||||
model,
|
||||
{
|
||||
renderer: 'onlyWhenVisible',
|
||||
}
|
||||
);
|
||||
|
||||
cut.init({ title: 'myTitle', params: {} });
|
||||
expect(cut.title).toBe('myTitle');
|
||||
|
||||
cut.setTitle('newTitle');
|
||||
expect(cut.title).toBe('newTitle');
|
||||
|
||||
cut.api.setTitle('new title 2');
|
||||
expect(cut.title).toBe('new title 2');
|
||||
});
|
||||
|
||||
test('dispose cleanup', () => {
|
||||
const api = fromPartial<DockviewApi>({});
|
||||
const accessor = fromPartial<DockviewComponent>({});
|
||||
const group = fromPartial<DockviewGroupPanel>({
|
||||
api: {
|
||||
onDidVisibilityChange: jest
|
||||
.fn()
|
||||
.mockReturnValue({ dispose: jest.fn() }),
|
||||
onDidLocationChange: jest
|
||||
.fn()
|
||||
.mockReturnValue({ dispose: jest.fn() }),
|
||||
onDidActiveChange: jest
|
||||
.fn()
|
||||
.mockReturnValue({ dispose: jest.fn() }),
|
||||
},
|
||||
});
|
||||
const model = fromPartial<IDockviewPanelModel>({
|
||||
update: jest.fn(),
|
||||
init: jest.fn(),
|
||||
dispose: jest.fn(),
|
||||
});
|
||||
|
||||
const cut = new DockviewPanel(
|
||||
'fake-id',
|
||||
'fake-component',
|
||||
undefined,
|
||||
accessor,
|
||||
api,
|
||||
group,
|
||||
model,
|
||||
{
|
||||
renderer: 'onlyWhenVisible',
|
||||
}
|
||||
);
|
||||
|
||||
cut.init({ params: {}, title: 'title' });
|
||||
|
||||
cut.dispose();
|
||||
|
||||
expect(model.dispose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('get params', () => {
|
||||
const api = fromPartial<DockviewApi>({});
|
||||
const accessor = fromPartial<DockviewComponent>({});
|
||||
const group = fromPartial<DockviewGroupPanel>({
|
||||
api: {
|
||||
onDidVisibilityChange: jest.fn(),
|
||||
onDidLocationChange: jest.fn(),
|
||||
onDidActiveChange: jest.fn(),
|
||||
},
|
||||
});
|
||||
const model = fromPartial<IDockviewPanelModel>({
|
||||
update: jest.fn(),
|
||||
init: jest.fn(),
|
||||
dispose: jest.fn(),
|
||||
});
|
||||
|
||||
const cut = new DockviewPanel(
|
||||
'fake-id',
|
||||
'fake-component',
|
||||
undefined,
|
||||
accessor,
|
||||
api,
|
||||
group,
|
||||
model,
|
||||
{
|
||||
renderer: 'onlyWhenVisible',
|
||||
}
|
||||
);
|
||||
|
||||
expect(cut.params).toEqual(undefined);
|
||||
|
||||
cut.update({ params: { variableA: 'A', variableB: 'B' } });
|
||||
|
||||
expect(cut.params).toEqual({ variableA: 'A', variableB: 'B' });
|
||||
});
|
||||
|
||||
test('setSize propagates to underlying group', () => {
|
||||
const api = fromPartial<DockviewApi>({});
|
||||
const accessor = fromPartial<DockviewComponent>({});
|
||||
const group = fromPartial<DockviewGroupPanel>({
|
||||
api: {
|
||||
onDidVisibilityChange: jest.fn(),
|
||||
onDidLocationChange: jest.fn(),
|
||||
onDidActiveChange: jest.fn(),
|
||||
setSize: jest.fn(),
|
||||
},
|
||||
});
|
||||
const model = fromPartial<IDockviewPanelModel>({
|
||||
update: jest.fn(),
|
||||
init: jest.fn(),
|
||||
dispose: jest.fn(),
|
||||
});
|
||||
|
||||
const cut = new DockviewPanel(
|
||||
'fake-id',
|
||||
'fake-component',
|
||||
undefined,
|
||||
accessor,
|
||||
api,
|
||||
group,
|
||||
model,
|
||||
{
|
||||
renderer: 'onlyWhenVisible',
|
||||
}
|
||||
);
|
||||
|
||||
cut.api.setSize({ height: 123, width: 456 });
|
||||
|
||||
expect(group.api.setSize).toHaveBeenCalledWith({
|
||||
height: 123,
|
||||
width: 456,
|
||||
});
|
||||
expect(group.api.setSize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('updateParameter', () => {
|
||||
const api = fromPartial<DockviewApi>({});
|
||||
const accessor = fromPartial<DockviewComponent>({});
|
||||
const group = fromPartial<DockviewGroupPanel>({
|
||||
api: {
|
||||
onDidVisibilityChange: jest.fn(),
|
||||
onDidLocationChange: jest.fn(),
|
||||
onDidActiveChange: jest.fn(),
|
||||
},
|
||||
});
|
||||
const model = fromPartial<IDockviewPanelModel>({
|
||||
update: jest.fn(),
|
||||
init: jest.fn(),
|
||||
dispose: jest.fn(),
|
||||
});
|
||||
|
||||
const cut = new DockviewPanel(
|
||||
'fake-id',
|
||||
'fake-component',
|
||||
undefined,
|
||||
accessor,
|
||||
api,
|
||||
group,
|
||||
model,
|
||||
{
|
||||
renderer: 'onlyWhenVisible',
|
||||
}
|
||||
);
|
||||
|
||||
cut.init({ params: { a: '1', b: '2' }, title: 'A title' });
|
||||
expect(cut.params).toEqual({ a: '1', b: '2' });
|
||||
|
||||
// update 'a' and add 'c'
|
||||
cut.update({ params: { a: '-1', c: '3' } });
|
||||
expect(cut.params).toEqual({ a: '-1', b: '2', c: '3' });
|
||||
expect(model.update).toHaveBeenCalledWith({
|
||||
params: { a: '-1', b: '2', c: '3' },
|
||||
});
|
||||
|
||||
cut.update({ params: { d: '4', e: '5', f: '6' } });
|
||||
expect(cut.params).toEqual({
|
||||
a: '-1',
|
||||
b: '2',
|
||||
c: '3',
|
||||
d: '4',
|
||||
e: '5',
|
||||
f: '6',
|
||||
});
|
||||
expect(model.update).toHaveBeenCalledWith({
|
||||
params: { a: '-1', b: '2', c: '3', d: '4', e: '5', f: '6' },
|
||||
});
|
||||
|
||||
cut.update({
|
||||
params: {
|
||||
d: '',
|
||||
e: null,
|
||||
f: undefined,
|
||||
g: '',
|
||||
h: null,
|
||||
i: undefined,
|
||||
},
|
||||
});
|
||||
expect(cut.params).toEqual({
|
||||
a: '-1',
|
||||
b: '2',
|
||||
c: '3',
|
||||
d: '',
|
||||
e: null,
|
||||
g: '',
|
||||
h: null,
|
||||
});
|
||||
expect(model.update).toHaveBeenCalledWith({
|
||||
params: { a: '-1', b: '2', c: '3', d: '', e: null, g: '', h: null },
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,200 @@
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
import { DockviewPanelModel } from '../../dockview/dockviewPanelModel';
|
||||
import { IContentRenderer, ITabRenderer } from '../../dockview/types';
|
||||
import { DefaultTab } from '../../dockview/components/tab/defaultTab';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
|
||||
describe('dockviewGroupPanel', () => {
|
||||
let contentMock: jest.Mock<IContentRenderer>;
|
||||
let tabMock: jest.Mock<ITabRenderer>;
|
||||
let accessorMock: DockviewComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
contentMock = jest.fn<IContentRenderer, []>(() => {
|
||||
const partial: Partial<IContentRenderer> = {
|
||||
element: document.createElement('div'),
|
||||
dispose: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
return partial as IContentRenderer;
|
||||
});
|
||||
|
||||
tabMock = jest.fn<ITabRenderer, []>(() => {
|
||||
const partial: Partial<IContentRenderer> = {
|
||||
element: document.createElement('div'),
|
||||
dispose: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
return partial as IContentRenderer;
|
||||
});
|
||||
|
||||
accessorMock = fromPartial<DockviewComponent>({
|
||||
options: {
|
||||
createComponent(options) {
|
||||
switch (options.name) {
|
||||
case 'contentComponent':
|
||||
return new contentMock(options.id, options.name);
|
||||
default:
|
||||
throw new Error(`unsupported`);
|
||||
}
|
||||
},
|
||||
createTabComponent(options) {
|
||||
switch (options.name) {
|
||||
case 'tabComponent':
|
||||
return new tabMock(options.id, options.name);
|
||||
default:
|
||||
throw new Error(`unsupported`);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('that dispose is called on content and tab renderers when present', () => {
|
||||
const cut = new DockviewPanelModel(
|
||||
accessorMock,
|
||||
'id',
|
||||
'contentComponent',
|
||||
'tabComponent'
|
||||
);
|
||||
|
||||
cut.dispose();
|
||||
|
||||
expect(cut.content.dispose).toHaveBeenCalled();
|
||||
expect(cut.tab.dispose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('that update is called on content and tab renderers when present', () => {
|
||||
const cut = new DockviewPanelModel(
|
||||
accessorMock,
|
||||
'id',
|
||||
'contentComponent',
|
||||
'tabComponent'
|
||||
);
|
||||
|
||||
cut.update({
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(cut.content.update).toHaveBeenCalled();
|
||||
expect(cut.tab.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('that the default tab is created', () => {
|
||||
accessorMock = fromPartial<DockviewComponent>({
|
||||
options: {
|
||||
createComponent(options) {
|
||||
switch (options.name) {
|
||||
case 'contentComponent':
|
||||
return new contentMock(options.id, options.name);
|
||||
default:
|
||||
throw new Error(`unsupported`);
|
||||
}
|
||||
},
|
||||
createTabComponent(options) {
|
||||
switch (options.name) {
|
||||
case 'tabComponent':
|
||||
return tabMock;
|
||||
default:
|
||||
throw new Error(`unsupported`);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cut = new DockviewPanelModel(
|
||||
accessorMock,
|
||||
'id',
|
||||
'contentComponent',
|
||||
'tabComponent'
|
||||
);
|
||||
|
||||
expect(cut.tab).toEqual(tabMock);
|
||||
});
|
||||
|
||||
test('that the provided default tab is chosen when no implementation is provided', () => {
|
||||
accessorMock = fromPartial<DockviewComponent>({
|
||||
options: {
|
||||
defaultTabComponent: 'tabComponent',
|
||||
createComponent(options) {
|
||||
switch (options.name) {
|
||||
case 'contentComponent':
|
||||
return new contentMock(options.id, options.name);
|
||||
default:
|
||||
throw new Error(`unsupported`);
|
||||
}
|
||||
},
|
||||
createTabComponent(options) {
|
||||
switch (options.name) {
|
||||
case 'tabComponent':
|
||||
return tabMock;
|
||||
default:
|
||||
throw new Error(`unsupported`);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cut = new DockviewPanelModel(
|
||||
accessorMock,
|
||||
'id',
|
||||
'contentComponent'
|
||||
);
|
||||
|
||||
expect(cut.tab).toEqual(tabMock);
|
||||
});
|
||||
|
||||
test('that is library default tab instance is created when no alternative exists', () => {
|
||||
accessorMock = fromPartial<DockviewComponent>({
|
||||
options: {
|
||||
createComponent(options) {
|
||||
switch (options.name) {
|
||||
case 'contentComponent':
|
||||
return new contentMock(options.id, options.name);
|
||||
default:
|
||||
throw new Error(`unsupported`);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cut = new DockviewPanelModel(
|
||||
accessorMock,
|
||||
'id',
|
||||
'contentComponent'
|
||||
);
|
||||
|
||||
expect(cut.tab instanceof DefaultTab).toBeTruthy();
|
||||
});
|
||||
|
||||
test('that the default content is created', () => {
|
||||
accessorMock = fromPartial<DockviewComponent>({
|
||||
options: {
|
||||
createComponent(options) {
|
||||
switch (options.name) {
|
||||
case 'contentComponent':
|
||||
return contentMock;
|
||||
default:
|
||||
throw new Error(`unsupported`);
|
||||
}
|
||||
},
|
||||
createTabComponent(options) {
|
||||
switch (options.name) {
|
||||
case 'tabComponent':
|
||||
return tabMock;
|
||||
default:
|
||||
throw new Error(`unsupported`);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cut = new DockviewPanelModel(
|
||||
accessorMock,
|
||||
'id',
|
||||
'contentComponent'
|
||||
);
|
||||
|
||||
expect(cut.content).toEqual(contentMock);
|
||||
});
|
||||
});
|
83
packages/dockview-core/src/__tests__/dom.spec.ts
Normal file
83
packages/dockview-core/src/__tests__/dom.spec.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import {
|
||||
disableIframePointEvents,
|
||||
isInDocument,
|
||||
quasiDefaultPrevented,
|
||||
quasiPreventDefault,
|
||||
} from '../dom';
|
||||
|
||||
describe('dom', () => {
|
||||
test('quasiPreventDefault', () => {
|
||||
const event = new Event('myevent');
|
||||
expect((event as any)['dv-quasiPreventDefault']).toBeUndefined();
|
||||
quasiPreventDefault(event);
|
||||
expect((event as any)['dv-quasiPreventDefault']).toBe(true);
|
||||
});
|
||||
|
||||
test('quasiDefaultPrevented', () => {
|
||||
const event = new Event('myevent');
|
||||
expect(quasiDefaultPrevented(event)).toBeFalsy();
|
||||
|
||||
(event as any)['dv-quasiPreventDefault'] = false;
|
||||
expect(quasiDefaultPrevented(event)).toBeFalsy();
|
||||
|
||||
(event as any)['dv-quasiPreventDefault'] = true;
|
||||
expect(quasiDefaultPrevented(event)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('isInDocument: DOM element', () => {
|
||||
const el = document.createElement('div');
|
||||
|
||||
expect(isInDocument(el)).toBeFalsy();
|
||||
|
||||
document.body.appendChild(el);
|
||||
expect(isInDocument(el)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('isInDocument: Shadow DOM element', () => {
|
||||
const el = document.createElement('div');
|
||||
document.body.appendChild(el);
|
||||
|
||||
const shadow = el.attachShadow({ mode: 'open' });
|
||||
|
||||
const el2 = document.createElement('div');
|
||||
expect(isInDocument(el2)).toBeFalsy();
|
||||
|
||||
shadow.appendChild(el2);
|
||||
|
||||
expect(isInDocument(el2)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('disableIframePointEvents', () => {
|
||||
const el1 = document.createElement('iframe');
|
||||
const el2 = document.createElement('iframe');
|
||||
const el3 = document.createElement('webview');
|
||||
const el4 = document.createElement('webview');
|
||||
|
||||
document.body.appendChild(el1);
|
||||
document.body.appendChild(el2);
|
||||
document.body.appendChild(el3);
|
||||
document.body.appendChild(el4);
|
||||
|
||||
el1.style.pointerEvents = 'inherit';
|
||||
el3.style.pointerEvents = 'inherit';
|
||||
|
||||
expect(el1.style.pointerEvents).toBe('inherit');
|
||||
expect(el2.style.pointerEvents).toBe('');
|
||||
expect(el3.style.pointerEvents).toBe('inherit');
|
||||
expect(el4.style.pointerEvents).toBe('');
|
||||
|
||||
const f = disableIframePointEvents();
|
||||
|
||||
expect(el1.style.pointerEvents).toBe('none');
|
||||
expect(el2.style.pointerEvents).toBe('none');
|
||||
expect(el3.style.pointerEvents).toBe('none');
|
||||
expect(el4.style.pointerEvents).toBe('none');
|
||||
|
||||
f.release();
|
||||
|
||||
expect(el1.style.pointerEvents).toBe('inherit');
|
||||
expect(el2.style.pointerEvents).toBe('');
|
||||
expect(el3.style.pointerEvents).toBe('inherit');
|
||||
expect(el4.style.pointerEvents).toBe('');
|
||||
});
|
||||
});
|
278
packages/dockview-core/src/__tests__/events.spec.ts
Normal file
278
packages/dockview-core/src/__tests__/events.spec.ts
Normal file
@ -0,0 +1,278 @@
|
||||
import {
|
||||
AsapEvent,
|
||||
Emitter,
|
||||
Event,
|
||||
addDisposableListener,
|
||||
} from '../events';
|
||||
|
||||
describe('events', () => {
|
||||
describe('emitter', () => {
|
||||
it('debug mode is off', () => {
|
||||
expect(Emitter.ENABLE_TRACKING).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should emit values', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
let value: number | undefined = undefined;
|
||||
|
||||
emitter.fire(-1);
|
||||
expect(value).toBeUndefined();
|
||||
|
||||
const stream = emitter.event((x) => {
|
||||
value = x;
|
||||
});
|
||||
|
||||
emitter.fire(0);
|
||||
expect(value).toBe(0);
|
||||
|
||||
emitter.fire(1);
|
||||
expect(value).toBe(1);
|
||||
|
||||
stream.dispose();
|
||||
});
|
||||
|
||||
it('should stop emitting after dispose', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
let value: number | undefined = undefined;
|
||||
|
||||
const stream = emitter.event((x) => {
|
||||
value = x;
|
||||
});
|
||||
|
||||
emitter.fire(0);
|
||||
expect(value).toBe(0);
|
||||
|
||||
stream.dispose();
|
||||
|
||||
value = undefined;
|
||||
emitter.fire(1);
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should stop emitting after dispose', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
let value: number | undefined = undefined;
|
||||
|
||||
const stream = emitter.event((x) => {
|
||||
value = x;
|
||||
});
|
||||
|
||||
emitter.fire(0);
|
||||
expect(value).toBe(0);
|
||||
|
||||
stream.dispose();
|
||||
|
||||
value = undefined;
|
||||
emitter.fire(1);
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should relay last value in replay mode', () => {
|
||||
const emitter = new Emitter<number>({ replay: true });
|
||||
let value: number | undefined = undefined;
|
||||
|
||||
emitter.fire(1);
|
||||
|
||||
const stream = emitter.event((x) => {
|
||||
value = x;
|
||||
});
|
||||
expect(value).toBe(1);
|
||||
|
||||
stream.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('asapEvent', () => {
|
||||
test('that asapEvents fire once per event-loop-cycle', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const event = new AsapEvent();
|
||||
|
||||
let preFireCount = 0;
|
||||
let postFireCount = 0;
|
||||
|
||||
event.onEvent(() => {
|
||||
preFireCount++;
|
||||
});
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
event.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* check that subscribing after the events have fired but before the event-loop cycle completes
|
||||
* results in no event fires.
|
||||
*/
|
||||
event.onEvent((e) => {
|
||||
postFireCount++;
|
||||
});
|
||||
|
||||
expect(preFireCount).toBe(0);
|
||||
expect(postFireCount).toBe(0);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(preFireCount).toBe(1);
|
||||
expect(postFireCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit a value when any event fires', () => {
|
||||
const emitter1 = new Emitter<number>();
|
||||
const emitter2 = new Emitter<number>();
|
||||
const emitter3 = new Emitter<number>();
|
||||
|
||||
let value: number | undefined = 0;
|
||||
|
||||
const stream = Event.any(
|
||||
emitter1.event,
|
||||
emitter2.event,
|
||||
emitter3.event
|
||||
)((x) => {
|
||||
value = x;
|
||||
});
|
||||
|
||||
emitter2.fire(2);
|
||||
expect(value).toBe(2);
|
||||
|
||||
emitter1.fire(1);
|
||||
expect(value).toBe(1);
|
||||
|
||||
emitter3.fire(3);
|
||||
expect(value).toBe(3);
|
||||
});
|
||||
|
||||
it('addDisposableListener with capture options', () => {
|
||||
const element = {
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
};
|
||||
|
||||
const handler = jest.fn();
|
||||
|
||||
const disposable = addDisposableListener(
|
||||
element as any,
|
||||
'pointerdown',
|
||||
handler,
|
||||
true
|
||||
);
|
||||
|
||||
expect(element.addEventListener).toBeCalledTimes(1);
|
||||
expect(element.addEventListener).toHaveBeenCalledWith(
|
||||
'pointerdown',
|
||||
handler,
|
||||
true
|
||||
);
|
||||
expect(element.removeEventListener).toBeCalledTimes(0);
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(element.addEventListener).toBeCalledTimes(1);
|
||||
expect(element.removeEventListener).toBeCalledTimes(1);
|
||||
expect(element.removeEventListener).toBeCalledWith(
|
||||
'pointerdown',
|
||||
handler,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('addDisposableListener without capture options', () => {
|
||||
const element = {
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
};
|
||||
|
||||
const handler = jest.fn();
|
||||
|
||||
const disposable = addDisposableListener(
|
||||
element as any,
|
||||
'pointerdown',
|
||||
handler
|
||||
);
|
||||
|
||||
expect(element.addEventListener).toBeCalledTimes(1);
|
||||
expect(element.addEventListener).toHaveBeenCalledWith(
|
||||
'pointerdown',
|
||||
handler,
|
||||
undefined
|
||||
);
|
||||
expect(element.removeEventListener).toBeCalledTimes(0);
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(element.addEventListener).toBeCalledTimes(1);
|
||||
expect(element.removeEventListener).toBeCalledTimes(1);
|
||||
expect(element.removeEventListener).toBeCalledWith(
|
||||
'pointerdown',
|
||||
handler,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('addDisposableListener with capture options', () => {
|
||||
const element = {
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
};
|
||||
|
||||
const handler = jest.fn();
|
||||
|
||||
const disposable = addDisposableListener(
|
||||
element as any,
|
||||
'pointerdown',
|
||||
handler,
|
||||
true
|
||||
);
|
||||
|
||||
expect(element.addEventListener).toBeCalledTimes(1);
|
||||
expect(element.addEventListener).toHaveBeenCalledWith(
|
||||
'pointerdown',
|
||||
handler,
|
||||
true
|
||||
);
|
||||
expect(element.removeEventListener).toBeCalledTimes(0);
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(element.addEventListener).toBeCalledTimes(1);
|
||||
expect(element.removeEventListener).toBeCalledTimes(1);
|
||||
expect(element.removeEventListener).toBeCalledWith(
|
||||
'pointerdown',
|
||||
handler,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('addDisposableListener without capture options', () => {
|
||||
const element = {
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
};
|
||||
|
||||
const handler = jest.fn();
|
||||
|
||||
const disposable = addDisposableListener(
|
||||
element as any,
|
||||
'pointerdown',
|
||||
handler
|
||||
);
|
||||
|
||||
expect(element.addEventListener).toBeCalledTimes(1);
|
||||
expect(element.addEventListener).toHaveBeenCalledWith(
|
||||
'pointerdown',
|
||||
handler,
|
||||
undefined
|
||||
);
|
||||
expect(element.removeEventListener).toBeCalledTimes(0);
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(element.addEventListener).toBeCalledTimes(1);
|
||||
expect(element.removeEventListener).toBeCalledTimes(1);
|
||||
expect(element.removeEventListener).toBeCalledWith(
|
||||
'pointerdown',
|
||||
handler,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,206 @@
|
||||
import { Emitter } from '../../events';
|
||||
import {
|
||||
BaseGrid,
|
||||
IGridPanelView,
|
||||
BaseGridOptions,
|
||||
} from '../../gridview/baseComponentGridview';
|
||||
import { IViewSize } from '../../gridview/gridview';
|
||||
import { CompositeDisposable } from '../../lifecycle';
|
||||
import {
|
||||
PanelInitParameters,
|
||||
PanelUpdateEvent,
|
||||
Parameters,
|
||||
} from '../../panel/types';
|
||||
import { LayoutPriority, Orientation } from '../../splitview/splitview';
|
||||
|
||||
class TestPanel implements IGridPanelView {
|
||||
_onDidChange = new Emitter<IViewSize | undefined>();
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
isVisible: boolean = true;
|
||||
isActive: boolean = true;
|
||||
params: Parameters = {};
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly element: HTMLElement,
|
||||
public readonly minimumWidth: number,
|
||||
public readonly maximumWidth: number,
|
||||
public readonly minimumHeight: number,
|
||||
public readonly maximumHeight: number,
|
||||
public priority: LayoutPriority,
|
||||
public snap: boolean
|
||||
) {}
|
||||
|
||||
init(params: PanelInitParameters): void {
|
||||
//
|
||||
}
|
||||
|
||||
setActive(isActive: boolean): void {
|
||||
//
|
||||
}
|
||||
|
||||
toJSON(): object {
|
||||
return {};
|
||||
}
|
||||
|
||||
layout(width: number, height: number): void {
|
||||
//
|
||||
}
|
||||
|
||||
update(event: PanelUpdateEvent<Parameters>): void {
|
||||
//
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
//
|
||||
}
|
||||
|
||||
fromJSON(json: object): void {
|
||||
//
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
class ClassUnderTest extends BaseGrid<TestPanel> {
|
||||
readonly gridview = this.gridview;
|
||||
|
||||
constructor(parentElement: HTMLElement, options: BaseGridOptions) {
|
||||
super(parentElement, options);
|
||||
}
|
||||
|
||||
doRemoveGroup(
|
||||
group: TestPanel,
|
||||
options?: { skipActive?: boolean; skipDispose?: boolean }
|
||||
): TestPanel {
|
||||
return super.doRemoveGroup(group, options);
|
||||
}
|
||||
|
||||
doAddGroup(group: TestPanel, location?: number[], size?: number): void {
|
||||
this._groups.set(group.id, {
|
||||
value: group,
|
||||
disposable: {
|
||||
dispose: () => {
|
||||
//
|
||||
},
|
||||
},
|
||||
});
|
||||
super.doAddGroup(group, location, size);
|
||||
}
|
||||
|
||||
public fromJSON(data: any): void {
|
||||
//
|
||||
}
|
||||
|
||||
public toJSON(): object {
|
||||
return {};
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
describe('baseComponentGridview', () => {
|
||||
test('that the container is not removed when grid is disposed', () => {
|
||||
const root = document.createElement('div');
|
||||
const container = document.createElement('div');
|
||||
root.appendChild(container);
|
||||
|
||||
const cut = new ClassUnderTest(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: true,
|
||||
});
|
||||
|
||||
cut.dispose();
|
||||
|
||||
expect(container.parentElement).toBe(root);
|
||||
});
|
||||
|
||||
test('that .layout(...) force flag works', () => {
|
||||
const cut = new ClassUnderTest(document.createElement('div'), {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: true,
|
||||
});
|
||||
|
||||
const spy = jest.spyOn(cut.gridview, 'layout');
|
||||
|
||||
cut.layout(100, 100);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
cut.layout(100, 100, false);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
cut.layout(100, 100, true);
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
|
||||
cut.layout(150, 150, false);
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
|
||||
cut.layout(150, 150, true);
|
||||
expect(spy).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
test('can add group', () => {
|
||||
const cut = new ClassUnderTest(document.createElement('div'), {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: true,
|
||||
});
|
||||
|
||||
const events: { type: string; panel: TestPanel | undefined }[] = [];
|
||||
|
||||
const disposable = new CompositeDisposable(
|
||||
cut.onDidAdd((event) => {
|
||||
events.push({ type: 'add', panel: event });
|
||||
}),
|
||||
cut.onDidRemove((event) => {
|
||||
events.push({ type: 'remove', panel: event });
|
||||
}),
|
||||
cut.onDidActiveChange((event) => {
|
||||
events.push({ type: 'active', panel: event });
|
||||
})
|
||||
);
|
||||
|
||||
const panel1 = new TestPanel(
|
||||
'id',
|
||||
document.createElement('div'),
|
||||
0,
|
||||
100,
|
||||
0,
|
||||
100,
|
||||
LayoutPriority.Normal,
|
||||
false
|
||||
);
|
||||
|
||||
cut.doAddGroup(panel1);
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0]).toEqual({ type: 'add', panel: panel1 });
|
||||
|
||||
const panel2 = new TestPanel(
|
||||
'id',
|
||||
document.createElement('div'),
|
||||
0,
|
||||
100,
|
||||
0,
|
||||
100,
|
||||
LayoutPriority.Normal,
|
||||
false
|
||||
);
|
||||
|
||||
cut.doAddGroup(panel2);
|
||||
|
||||
expect(events.length).toBe(2);
|
||||
expect(events[1]).toEqual({ type: 'add', panel: panel2 });
|
||||
|
||||
cut.doRemoveGroup(panel1);
|
||||
expect(events.length).toBe(3);
|
||||
expect(events[2]).toEqual({ type: 'remove', panel: panel1 });
|
||||
|
||||
disposable.dispose();
|
||||
cut.dispose();
|
||||
});
|
||||
});
|
1207
packages/dockview-core/src/__tests__/gridview/gridview.spec.ts
Normal file
1207
packages/dockview-core/src/__tests__/gridview/gridview.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
||||
|
||||
describe('gridviewPanel', () => {
|
||||
test('get panel', () => {
|
||||
const accessorMock = jest.fn<DockviewComponent, []>(() => {
|
||||
return {
|
||||
onDidAddPanel: jest.fn(),
|
||||
onDidRemovePanel: jest.fn(),
|
||||
options: {},
|
||||
onDidOptionsChange: jest.fn(),
|
||||
} as any;
|
||||
});
|
||||
|
||||
const accessor = new accessorMock();
|
||||
|
||||
const cut = new DockviewGroupPanel(accessor, 'id', {});
|
||||
|
||||
expect(cut.params).toEqual(undefined);
|
||||
|
||||
cut.update({ params: { variableA: 'A', variableB: 'B' } });
|
||||
|
||||
expect(cut.params).toEqual({ variableA: 'A', variableB: 'B' });
|
||||
});
|
||||
});
|
83
packages/dockview-core/src/__tests__/lifecycle.spec.ts
Normal file
83
packages/dockview-core/src/__tests__/lifecycle.spec.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import {
|
||||
CompositeDisposable,
|
||||
Disposable,
|
||||
MutableDisposable,
|
||||
} from '../lifecycle';
|
||||
|
||||
describe('lifecycle', () => {
|
||||
test('mutable disposable', () => {
|
||||
const mutableDisposable = new MutableDisposable();
|
||||
|
||||
let disposed = 0;
|
||||
|
||||
const disposable = () => ({
|
||||
dispose: () => {
|
||||
disposed++;
|
||||
},
|
||||
});
|
||||
|
||||
mutableDisposable.value = disposable();
|
||||
expect(disposed).toBe(0);
|
||||
|
||||
mutableDisposable.value = disposable();
|
||||
expect(disposed).toBe(1);
|
||||
|
||||
mutableDisposable.dispose();
|
||||
expect(disposed).toBe(2);
|
||||
|
||||
mutableDisposable.dispose();
|
||||
});
|
||||
|
||||
test('composite disposable', () => {
|
||||
const d1 = {
|
||||
dispose: jest.fn(),
|
||||
};
|
||||
const d2 = {
|
||||
dispose: jest.fn(),
|
||||
};
|
||||
const d3 = {
|
||||
dispose: jest.fn(),
|
||||
};
|
||||
const d4 = {
|
||||
dispose: jest.fn(),
|
||||
};
|
||||
|
||||
const cut = new CompositeDisposable(d1, d2);
|
||||
cut.addDisposables(d3, d4);
|
||||
|
||||
cut.dispose();
|
||||
|
||||
expect(d1.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(d2.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(d3.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(d4.dispose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('that isDisposed=true once CompositeDisposable is disposed', () => {
|
||||
class Test extends CompositeDisposable {
|
||||
checkIsDisposed(): boolean {
|
||||
return this.isDisposed;
|
||||
}
|
||||
}
|
||||
|
||||
const cut = new Test();
|
||||
|
||||
expect(cut.checkIsDisposed()).toBeFalsy();
|
||||
|
||||
cut.dispose();
|
||||
|
||||
expect(cut.checkIsDisposed()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Disposable.from(...)', () => {
|
||||
const func = jest.fn();
|
||||
|
||||
const disposable = Disposable.from(func);
|
||||
|
||||
expect(func).not.toHaveBeenCalled();
|
||||
|
||||
disposable.dispose();
|
||||
|
||||
expect(func).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
21
packages/dockview-core/src/__tests__/math.spec.ts
Normal file
21
packages/dockview-core/src/__tests__/math.spec.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { clamp, range } from '../math';
|
||||
|
||||
describe('math', () => {
|
||||
describe('clamp', () => {
|
||||
it('should clamp between a minimum and maximum value', () => {
|
||||
expect(clamp(45, 40, 50)).toBe(45);
|
||||
expect(clamp(35, 40, 50)).toBe(40);
|
||||
expect(clamp(55, 40, 50)).toBe(50);
|
||||
});
|
||||
|
||||
it('if min > max return min', () => {
|
||||
expect(clamp(55, 50, 40)).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
test('range', () => {
|
||||
expect(range(0, 5)).toEqual([0, 1, 2, 3, 4]);
|
||||
expect(range(5, 0)).toEqual([5, 4, 3, 2, 1]);
|
||||
expect(range(5)).toEqual([0, 1, 2, 3, 4]);
|
||||
});
|
||||
});
|
418
packages/dockview-core/src/__tests__/overlay/overlay.spec.ts
Normal file
418
packages/dockview-core/src/__tests__/overlay/overlay.spec.ts
Normal file
@ -0,0 +1,418 @@
|
||||
import { Overlay } from '../../overlay/overlay';
|
||||
import { mockGetBoundingClientRect } from '../__test_utils__/utils';
|
||||
|
||||
describe('overlay', () => {
|
||||
test('toJSON, top left', () => {
|
||||
const container = document.createElement('div');
|
||||
const content = document.createElement('div');
|
||||
|
||||
document.body.appendChild(container);
|
||||
container.appendChild(content);
|
||||
|
||||
const cut = new Overlay({
|
||||
height: 200,
|
||||
width: 100,
|
||||
left: 10,
|
||||
top: 20,
|
||||
minimumInViewportWidth: 0,
|
||||
minimumInViewportHeight: 0,
|
||||
container,
|
||||
content,
|
||||
});
|
||||
|
||||
jest.spyOn(
|
||||
container.childNodes.item(0) as HTMLElement,
|
||||
'getBoundingClientRect'
|
||||
).mockImplementation(() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 80,
|
||||
top: 100,
|
||||
width: 40,
|
||||
height: 50,
|
||||
});
|
||||
});
|
||||
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
|
||||
() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 20,
|
||||
top: 30,
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
cut.setBounds();
|
||||
|
||||
expect(cut.toJSON()).toEqual({
|
||||
top: 70,
|
||||
left: 60,
|
||||
width: 40,
|
||||
height: 50,
|
||||
});
|
||||
|
||||
cut.dispose();
|
||||
});
|
||||
|
||||
test('toJSON, bottom right', () => {
|
||||
const container = document.createElement('div');
|
||||
const content = document.createElement('div');
|
||||
|
||||
document.body.appendChild(container);
|
||||
container.appendChild(content);
|
||||
|
||||
const cut = new Overlay({
|
||||
height: 200,
|
||||
width: 100,
|
||||
right: 10,
|
||||
bottom: 20,
|
||||
minimumInViewportWidth: 0,
|
||||
minimumInViewportHeight: 0,
|
||||
container,
|
||||
content,
|
||||
});
|
||||
|
||||
jest.spyOn(
|
||||
container.childNodes.item(0) as HTMLElement,
|
||||
'getBoundingClientRect'
|
||||
).mockImplementation(() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 80,
|
||||
top: 100,
|
||||
width: 40,
|
||||
height: 50,
|
||||
});
|
||||
});
|
||||
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
|
||||
() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 20,
|
||||
top: 30,
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
cut.setBounds();
|
||||
|
||||
expect(cut.toJSON()).toEqual({
|
||||
bottom: -20,
|
||||
right: 0,
|
||||
width: 40,
|
||||
height: 50,
|
||||
});
|
||||
|
||||
cut.dispose();
|
||||
});
|
||||
|
||||
test('that out-of-bounds dimensions are fixed, top left', () => {
|
||||
const container = document.createElement('div');
|
||||
const content = document.createElement('div');
|
||||
|
||||
document.body.appendChild(container);
|
||||
container.appendChild(content);
|
||||
|
||||
const cut = new Overlay({
|
||||
height: 200,
|
||||
width: 100,
|
||||
left: -1000,
|
||||
top: -1000,
|
||||
minimumInViewportWidth: 0,
|
||||
minimumInViewportHeight: 0,
|
||||
container,
|
||||
content,
|
||||
});
|
||||
|
||||
jest.spyOn(
|
||||
container.childNodes.item(0) as HTMLElement,
|
||||
'getBoundingClientRect'
|
||||
).mockImplementation(() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 80,
|
||||
top: 100,
|
||||
width: 40,
|
||||
height: 50,
|
||||
});
|
||||
});
|
||||
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
|
||||
() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 20,
|
||||
top: 30,
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
cut.setBounds();
|
||||
|
||||
expect(cut.toJSON()).toEqual({
|
||||
top: 70,
|
||||
left: 60,
|
||||
width: 40,
|
||||
height: 50,
|
||||
});
|
||||
|
||||
cut.dispose();
|
||||
});
|
||||
|
||||
test('that out-of-bounds dimensions are fixed, bottom right', () => {
|
||||
const container = document.createElement('div');
|
||||
const content = document.createElement('div');
|
||||
|
||||
document.body.appendChild(container);
|
||||
container.appendChild(content);
|
||||
|
||||
const cut = new Overlay({
|
||||
height: 200,
|
||||
width: 100,
|
||||
bottom: -1000,
|
||||
right: -1000,
|
||||
minimumInViewportWidth: 0,
|
||||
minimumInViewportHeight: 0,
|
||||
container,
|
||||
content,
|
||||
});
|
||||
|
||||
jest.spyOn(
|
||||
container.childNodes.item(0) as HTMLElement,
|
||||
'getBoundingClientRect'
|
||||
).mockImplementation(() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 80,
|
||||
top: 100,
|
||||
width: 40,
|
||||
height: 50,
|
||||
});
|
||||
});
|
||||
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
|
||||
() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 20,
|
||||
top: 30,
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
cut.setBounds();
|
||||
|
||||
expect(cut.toJSON()).toEqual({
|
||||
bottom: -20,
|
||||
right: 0,
|
||||
width: 40,
|
||||
height: 50,
|
||||
});
|
||||
|
||||
cut.dispose();
|
||||
});
|
||||
|
||||
test('setBounds, top left', () => {
|
||||
const container = document.createElement('div');
|
||||
const content = document.createElement('div');
|
||||
|
||||
document.body.appendChild(container);
|
||||
container.appendChild(content);
|
||||
|
||||
const cut = new Overlay({
|
||||
height: 1000,
|
||||
width: 1000,
|
||||
left: 0,
|
||||
top: 0,
|
||||
minimumInViewportWidth: 0,
|
||||
minimumInViewportHeight: 0,
|
||||
container,
|
||||
content,
|
||||
});
|
||||
|
||||
const element: HTMLElement = container.querySelector(
|
||||
'.dv-resize-container'
|
||||
)!;
|
||||
expect(element).toBeTruthy();
|
||||
|
||||
jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 300,
|
||||
top: 400,
|
||||
width: 200,
|
||||
height: 100,
|
||||
});
|
||||
});
|
||||
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
|
||||
() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
cut.setBounds({ height: 100, width: 200, left: 300, top: 400 });
|
||||
|
||||
expect(element.style.height).toBe('100px');
|
||||
expect(element.style.width).toBe('200px');
|
||||
expect(element.style.left).toBe('300px');
|
||||
expect(element.style.top).toBe('400px');
|
||||
|
||||
cut.dispose();
|
||||
});
|
||||
|
||||
test('setBounds, bottom right', () => {
|
||||
const container = document.createElement('div');
|
||||
const content = document.createElement('div');
|
||||
|
||||
document.body.appendChild(container);
|
||||
container.appendChild(content);
|
||||
|
||||
const cut = new Overlay({
|
||||
height: 1000,
|
||||
width: 1000,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
minimumInViewportWidth: 0,
|
||||
minimumInViewportHeight: 0,
|
||||
container,
|
||||
content,
|
||||
});
|
||||
|
||||
const element: HTMLElement = container.querySelector(
|
||||
'.dv-resize-container'
|
||||
)!;
|
||||
expect(element).toBeTruthy();
|
||||
|
||||
jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 500,
|
||||
top: 500,
|
||||
width: 200,
|
||||
height: 100,
|
||||
});
|
||||
});
|
||||
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
|
||||
() => {
|
||||
return mockGetBoundingClientRect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
cut.setBounds({ height: 100, width: 200, right: 300, bottom: 400 });
|
||||
|
||||
expect(element.style.height).toBe('100px');
|
||||
expect(element.style.width).toBe('200px');
|
||||
expect(element.style.right).toBe('300px');
|
||||
expect(element.style.bottom).toBe('400px');
|
||||
|
||||
cut.dispose();
|
||||
});
|
||||
|
||||
test('that the resize handles are added', () => {
|
||||
const container = document.createElement('div');
|
||||
const content = document.createElement('div');
|
||||
|
||||
const cut = new Overlay({
|
||||
height: 500,
|
||||
width: 500,
|
||||
left: 100,
|
||||
top: 200,
|
||||
minimumInViewportWidth: 0,
|
||||
minimumInViewportHeight: 0,
|
||||
container,
|
||||
content,
|
||||
});
|
||||
|
||||
expect(container.querySelector('.dv-resize-handle-top')).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('.dv-resize-handle-bottom')
|
||||
).toBeTruthy();
|
||||
expect(container.querySelector('.dv-resize-handle-left')).toBeTruthy();
|
||||
expect(container.querySelector('.dv-resize-handle-right')).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('.dv-resize-handle-topleft')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('.dv-resize-handle-topright')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('.dv-resize-handle-bottomleft')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('.dv-resize-handle-bottomright')
|
||||
).toBeTruthy();
|
||||
|
||||
cut.dispose();
|
||||
});
|
||||
|
||||
test('aria-level attributes and corresponding z-index', () => {
|
||||
const container = document.createElement('div');
|
||||
const content = document.createElement('div');
|
||||
|
||||
const createOverlay = () =>
|
||||
new Overlay({
|
||||
height: 500,
|
||||
width: 500,
|
||||
left: 100,
|
||||
top: 200,
|
||||
minimumInViewportWidth: 0,
|
||||
minimumInViewportHeight: 0,
|
||||
container,
|
||||
content,
|
||||
});
|
||||
|
||||
const overlay1 = createOverlay();
|
||||
|
||||
const zIndexValue = (delta: number) =>
|
||||
`calc(var(--dv-overlay-z-index, 999) + ${delta})`;
|
||||
|
||||
expect(overlay1.element.getAttribute('aria-level')).toBe('0');
|
||||
expect(overlay1.element.style.zIndex).toBe(zIndexValue(0));
|
||||
|
||||
const overlay2 = createOverlay();
|
||||
const overlay3 = createOverlay();
|
||||
|
||||
expect(overlay1.element.getAttribute('aria-level')).toBe('0');
|
||||
expect(overlay2.element.getAttribute('aria-level')).toBe('1');
|
||||
expect(overlay3.element.getAttribute('aria-level')).toBe('2');
|
||||
expect(overlay1.element.style.zIndex).toBe(zIndexValue(0));
|
||||
expect(overlay2.element.style.zIndex).toBe(zIndexValue(2));
|
||||
expect(overlay3.element.style.zIndex).toBe(zIndexValue(4));
|
||||
|
||||
overlay2.bringToFront();
|
||||
|
||||
expect(overlay1.element.getAttribute('aria-level')).toBe('0');
|
||||
expect(overlay2.element.getAttribute('aria-level')).toBe('2');
|
||||
expect(overlay3.element.getAttribute('aria-level')).toBe('1');
|
||||
expect(overlay1.element.style.zIndex).toBe(zIndexValue(0));
|
||||
expect(overlay2.element.style.zIndex).toBe(zIndexValue(4));
|
||||
expect(overlay3.element.style.zIndex).toBe(zIndexValue(2));
|
||||
|
||||
overlay1.bringToFront();
|
||||
|
||||
expect(overlay1.element.getAttribute('aria-level')).toBe('2');
|
||||
expect(overlay2.element.getAttribute('aria-level')).toBe('1');
|
||||
expect(overlay3.element.getAttribute('aria-level')).toBe('0');
|
||||
expect(overlay1.element.style.zIndex).toBe(zIndexValue(4));
|
||||
expect(overlay2.element.style.zIndex).toBe(zIndexValue(2));
|
||||
expect(overlay3.element.style.zIndex).toBe(zIndexValue(0));
|
||||
|
||||
overlay2.dispose();
|
||||
|
||||
expect(overlay1.element.getAttribute('aria-level')).toBe('1');
|
||||
expect(overlay3.element.getAttribute('aria-level')).toBe('0');
|
||||
expect(overlay1.element.style.zIndex).toBe(zIndexValue(2));
|
||||
expect(overlay3.element.style.zIndex).toBe(zIndexValue(0));
|
||||
|
||||
overlay1.dispose();
|
||||
|
||||
expect(overlay3.element.getAttribute('aria-level')).toBe('0');
|
||||
expect(overlay3.element.style.zIndex).toBe(zIndexValue(0));
|
||||
});
|
||||
});
|
@ -0,0 +1,265 @@
|
||||
import { Droptarget } from '../../dnd/droptarget';
|
||||
import { IDockviewPanel } from '../../dockview/dockviewPanel';
|
||||
import { Emitter } from '../../events';
|
||||
import {
|
||||
IRenderable,
|
||||
OverlayRenderContainer,
|
||||
} from '../../overlay/overlayRenderContainer';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { Writable, exhaustMicrotaskQueue } from '../__test_utils__/utils';
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
||||
|
||||
describe('overlayRenderContainer', () => {
|
||||
let referenceContainer: IRenderable;
|
||||
let parentContainer: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
parentContainer = document.createElement('div');
|
||||
|
||||
referenceContainer = {
|
||||
element: document.createElement('div'),
|
||||
dropTarget: fromPartial<Droptarget>({}),
|
||||
};
|
||||
});
|
||||
|
||||
test('that attach(...) and detach(...) mutate the DOM as expected', () => {
|
||||
const cut = new OverlayRenderContainer(
|
||||
parentContainer,
|
||||
fromPartial<DockviewComponent>({})
|
||||
);
|
||||
|
||||
const panelContentEl = document.createElement('div');
|
||||
|
||||
const onDidVisibilityChange = new Emitter<any>();
|
||||
const onDidDimensionsChange = new Emitter<any>();
|
||||
const onDidLocationChange = new Emitter<any>();
|
||||
|
||||
const panel = fromPartial<IDockviewPanel>({
|
||||
api: {
|
||||
id: 'test_panel_id',
|
||||
onDidVisibilityChange: onDidVisibilityChange.event,
|
||||
onDidDimensionsChange: onDidDimensionsChange.event,
|
||||
onDidLocationChange: onDidLocationChange.event,
|
||||
isVisible: true,
|
||||
location: { type: 'grid' },
|
||||
},
|
||||
view: {
|
||||
content: {
|
||||
element: panelContentEl,
|
||||
},
|
||||
},
|
||||
group: {
|
||||
api: {
|
||||
location: { type: 'grid' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cut.attach({ panel, referenceContainer });
|
||||
|
||||
expect(panelContentEl.parentElement?.parentElement).toBe(
|
||||
parentContainer
|
||||
);
|
||||
|
||||
cut.detatch(panel);
|
||||
|
||||
expect(panelContentEl.parentElement?.parentElement).toBeUndefined();
|
||||
});
|
||||
|
||||
test('add a view that is not currently in the DOM', async () => {
|
||||
const cut = new OverlayRenderContainer(
|
||||
parentContainer,
|
||||
fromPartial<DockviewComponent>({})
|
||||
);
|
||||
|
||||
const panelContentEl = document.createElement('div');
|
||||
|
||||
const onDidVisibilityChange = new Emitter<any>();
|
||||
const onDidDimensionsChange = new Emitter<any>();
|
||||
const onDidLocationChange = new Emitter<any>();
|
||||
|
||||
const panel = fromPartial<IDockviewPanel>({
|
||||
api: {
|
||||
id: 'test_panel_id',
|
||||
onDidVisibilityChange: onDidVisibilityChange.event,
|
||||
onDidDimensionsChange: onDidDimensionsChange.event,
|
||||
onDidLocationChange: onDidLocationChange.event,
|
||||
isVisible: true,
|
||||
location: { type: 'grid' },
|
||||
},
|
||||
view: {
|
||||
content: {
|
||||
element: panelContentEl,
|
||||
},
|
||||
},
|
||||
group: {
|
||||
api: {
|
||||
location: { type: 'grid' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
(parentContainer as jest.Mocked<HTMLDivElement>).getBoundingClientRect =
|
||||
jest
|
||||
.fn<DOMRect, []>()
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 100,
|
||||
top: 200,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 101,
|
||||
top: 201,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 100,
|
||||
top: 200,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
})
|
||||
);
|
||||
|
||||
(
|
||||
referenceContainer.element as jest.Mocked<HTMLDivElement>
|
||||
).getBoundingClientRect = jest
|
||||
.fn<DOMRect, []>()
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 150,
|
||||
top: 300,
|
||||
width: 100,
|
||||
height: 200,
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 150,
|
||||
top: 300,
|
||||
width: 101,
|
||||
height: 201,
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 150,
|
||||
top: 300,
|
||||
width: 100,
|
||||
height: 200,
|
||||
})
|
||||
);
|
||||
|
||||
const container = cut.attach({ panel, referenceContainer });
|
||||
|
||||
await exhaustMicrotaskQueue();
|
||||
|
||||
expect(panelContentEl.parentElement).toBe(container);
|
||||
expect(container.parentElement).toBe(parentContainer);
|
||||
|
||||
expect(container.style.display).toBe('');
|
||||
|
||||
expect(container.style.left).toBe('50px');
|
||||
expect(container.style.top).toBe('100px');
|
||||
expect(container.style.width).toBe('100px');
|
||||
expect(container.style.height).toBe('200px');
|
||||
expect(
|
||||
referenceContainer.element.getBoundingClientRect
|
||||
).toHaveBeenCalledTimes(1);
|
||||
|
||||
onDidDimensionsChange.fire({});
|
||||
expect(container.style.display).toBe('');
|
||||
|
||||
expect(container.style.left).toBe('49px');
|
||||
expect(container.style.top).toBe('99px');
|
||||
expect(container.style.width).toBe('101px');
|
||||
expect(container.style.height).toBe('201px');
|
||||
expect(
|
||||
referenceContainer.element.getBoundingClientRect
|
||||
).toHaveBeenCalledTimes(2);
|
||||
|
||||
(panel as Writable<IDockviewPanel>).api.isVisible = false;
|
||||
onDidVisibilityChange.fire({});
|
||||
expect(container.style.display).toBe('none');
|
||||
expect(
|
||||
referenceContainer.element.getBoundingClientRect
|
||||
).toHaveBeenCalledTimes(2);
|
||||
|
||||
(panel as Writable<IDockviewPanel>).api.isVisible = true;
|
||||
onDidVisibilityChange.fire({});
|
||||
expect(container.style.display).toBe('');
|
||||
|
||||
expect(container.style.left).toBe('50px');
|
||||
expect(container.style.top).toBe('100px');
|
||||
expect(container.style.width).toBe('100px');
|
||||
expect(container.style.height).toBe('200px');
|
||||
expect(
|
||||
referenceContainer.element.getBoundingClientRect
|
||||
).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('related z-index from `aria-level` set on floating panels', async () => {
|
||||
const group = fromPartial<DockviewGroupPanel>({});
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute('aria-level', '2');
|
||||
const spy = jest.spyOn(element, 'getAttribute');
|
||||
|
||||
const accessor = fromPartial<DockviewComponent>({
|
||||
floatingGroups: [
|
||||
{
|
||||
group,
|
||||
overlay: {
|
||||
element,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const cut = new OverlayRenderContainer(parentContainer, accessor);
|
||||
|
||||
const panelContentEl = document.createElement('div');
|
||||
|
||||
const onDidVisibilityChange = new Emitter<any>();
|
||||
const onDidDimensionsChange = new Emitter<any>();
|
||||
const onDidLocationChange = new Emitter<any>();
|
||||
|
||||
const panel = fromPartial<IDockviewPanel>({
|
||||
api: {
|
||||
id: 'test_panel_id',
|
||||
onDidVisibilityChange: onDidVisibilityChange.event,
|
||||
onDidDimensionsChange: onDidDimensionsChange.event,
|
||||
onDidLocationChange: onDidLocationChange.event,
|
||||
isVisible: true,
|
||||
group,
|
||||
location: { type: 'floating' },
|
||||
},
|
||||
view: {
|
||||
content: {
|
||||
element: panelContentEl,
|
||||
},
|
||||
},
|
||||
group: {
|
||||
api: {
|
||||
location: { type: 'floating' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cut.attach({ panel, referenceContainer });
|
||||
|
||||
await exhaustMicrotaskQueue();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('aria-level');
|
||||
expect(panelContentEl.parentElement!.style.zIndex).toBe(
|
||||
'calc(var(--dv-overlay-z-index, 999) + 5)'
|
||||
);
|
||||
});
|
||||
});
|
147
packages/dockview-core/src/__tests__/paneview/paneview.spec.ts
Normal file
147
packages/dockview-core/src/__tests__/paneview/paneview.spec.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import { CompositeDisposable } from '../../lifecycle';
|
||||
import { Paneview } from '../../paneview/paneview';
|
||||
import { IPanePart, PaneviewPanel } from '../../paneview/paneviewPanel';
|
||||
import { Orientation } from '../../splitview/splitview';
|
||||
|
||||
class TestPanel extends PaneviewPanel {
|
||||
protected getBodyComponent(): IPanePart {
|
||||
return {
|
||||
element: document.createElement('div'),
|
||||
update: () => {
|
||||
//
|
||||
},
|
||||
dispose: () => {
|
||||
//
|
||||
},
|
||||
init: () => {
|
||||
// /
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected getHeaderComponent(): IPanePart {
|
||||
return {
|
||||
element: document.createElement('div'),
|
||||
update: () => {
|
||||
//
|
||||
},
|
||||
dispose: () => {
|
||||
//
|
||||
},
|
||||
init: () => {
|
||||
// /
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('paneview', () => {
|
||||
let container: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
container.className = 'container';
|
||||
});
|
||||
|
||||
test('onDidAddView and onDidRemoveView events', () => {
|
||||
const paneview = new Paneview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
const added: PaneviewPanel[] = [];
|
||||
const removed: PaneviewPanel[] = [];
|
||||
|
||||
const disposable = new CompositeDisposable(
|
||||
paneview.onDidAddView((view) => added.push(view)),
|
||||
paneview.onDidRemoveView((view) => removed.push(view))
|
||||
);
|
||||
|
||||
const view1 = new TestPanel({
|
||||
id: 'id',
|
||||
component: 'component',
|
||||
headerComponent: 'headerComponent',
|
||||
orientation: Orientation.VERTICAL,
|
||||
isExpanded: true,
|
||||
isHeaderVisible: true,
|
||||
headerSize: 22,
|
||||
minimumBodySize: 0,
|
||||
maximumBodySize: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
const view2 = new TestPanel({
|
||||
id: 'id2',
|
||||
component: 'component',
|
||||
headerComponent: 'headerComponent',
|
||||
orientation: Orientation.VERTICAL,
|
||||
isExpanded: true,
|
||||
isHeaderVisible: true,
|
||||
headerSize: 22,
|
||||
minimumBodySize: 0,
|
||||
maximumBodySize: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
|
||||
expect(added.length).toBe(0);
|
||||
expect(removed.length).toBe(0);
|
||||
|
||||
paneview.addPane(view1);
|
||||
expect(added.length).toBe(1);
|
||||
expect(removed.length).toBe(0);
|
||||
expect(added[0]).toBe(view1);
|
||||
|
||||
paneview.addPane(view2);
|
||||
expect(added.length).toBe(2);
|
||||
expect(removed.length).toBe(0);
|
||||
expect(added[1]).toBe(view2);
|
||||
|
||||
paneview.removePane(0);
|
||||
expect(added.length).toBe(2);
|
||||
expect(removed.length).toBe(1);
|
||||
expect(removed[0]).toBe(view1);
|
||||
|
||||
paneview.removePane(0);
|
||||
expect(added.length).toBe(2);
|
||||
expect(removed.length).toBe(2);
|
||||
expect(removed[1]).toBe(view2);
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('dispose of paneview', () => {
|
||||
expect(container.childNodes.length).toBe(0);
|
||||
|
||||
const paneview = new Paneview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
const view1 = new TestPanel({
|
||||
id: 'id',
|
||||
component: 'component',
|
||||
headerComponent: 'headerComponent',
|
||||
orientation: Orientation.VERTICAL,
|
||||
isExpanded: true,
|
||||
isHeaderVisible: true,
|
||||
headerSize: 22,
|
||||
minimumBodySize: 0,
|
||||
maximumBodySize: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
const view2 = new TestPanel({
|
||||
id: 'id2',
|
||||
component: 'component',
|
||||
headerComponent: 'headerComponent',
|
||||
orientation: Orientation.VERTICAL,
|
||||
isExpanded: true,
|
||||
isHeaderVisible: true,
|
||||
headerSize: 22,
|
||||
minimumBodySize: 0,
|
||||
maximumBodySize: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
|
||||
paneview.addPane(view1);
|
||||
paneview.addPane(view2);
|
||||
|
||||
expect(container.childNodes.length).toBeGreaterThan(0);
|
||||
|
||||
paneview.dispose();
|
||||
|
||||
expect(container.childNodes.length).toBe(0);
|
||||
});
|
||||
});
|
@ -0,0 +1,619 @@
|
||||
import { PanelDimensionChangeEvent } from '../../api/panelApi';
|
||||
import { CompositeDisposable } from '../../lifecycle';
|
||||
import { PanelUpdateEvent } from '../../panel/types';
|
||||
import { PaneviewComponent } from '../../paneview/paneviewComponent';
|
||||
import {
|
||||
PaneviewPanel,
|
||||
IPanePart,
|
||||
PanePanelComponentInitParameter,
|
||||
} from '../../paneview/paneviewPanel';
|
||||
import { Orientation } from '../../splitview/splitview';
|
||||
|
||||
class TestPanel extends PaneviewPanel {
|
||||
constructor(id: string, component: string) {
|
||||
super({
|
||||
id,
|
||||
component,
|
||||
headerComponent: 'header',
|
||||
orientation: Orientation.VERTICAL,
|
||||
isExpanded: false,
|
||||
isHeaderVisible: true,
|
||||
headerSize: 22,
|
||||
minimumBodySize: 0,
|
||||
maximumBodySize: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
}
|
||||
|
||||
getHeaderComponent() {
|
||||
return new (class Header implements IPanePart {
|
||||
private _element: HTMLElement = document.createElement('div');
|
||||
|
||||
get element() {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
init(params: PanePanelComponentInitParameter) {
|
||||
//
|
||||
}
|
||||
|
||||
update(params: PanelUpdateEvent) {
|
||||
//
|
||||
}
|
||||
|
||||
dispose() {
|
||||
//
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
getBodyComponent() {
|
||||
return new (class Header implements IPanePart {
|
||||
private _element: HTMLElement = document.createElement('div');
|
||||
|
||||
get element() {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
init(params: PanePanelComponentInitParameter) {
|
||||
//
|
||||
}
|
||||
|
||||
update(params: PanelUpdateEvent) {
|
||||
//
|
||||
}
|
||||
|
||||
dispose() {
|
||||
//
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
describe('paneviewComponent', () => {
|
||||
let container: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
container.className = 'container';
|
||||
});
|
||||
|
||||
test('that the container is not removed when grid is disposed', () => {
|
||||
const root = document.createElement('div');
|
||||
const container = document.createElement('div');
|
||||
root.appendChild(container);
|
||||
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
paneview.dispose();
|
||||
|
||||
expect(container.parentElement).toBe(root);
|
||||
expect(container.children.length).toBe(0);
|
||||
});
|
||||
|
||||
test('vertical panels', () => {
|
||||
const disposables = new CompositeDisposable();
|
||||
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
paneview.layout(300, 200);
|
||||
|
||||
paneview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
title: 'Panel 1',
|
||||
});
|
||||
paneview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
title: 'Panel2',
|
||||
});
|
||||
|
||||
const panel1 = paneview.getPanel('panel1') as PaneviewPanel;
|
||||
const panel2 = paneview.getPanel('panel2') as PaneviewPanel;
|
||||
|
||||
let panel1Dimensions: PanelDimensionChangeEvent | undefined = undefined;
|
||||
disposables.addDisposables(
|
||||
panel1.api.onDidDimensionsChange((event) => {
|
||||
panel1Dimensions = event;
|
||||
})
|
||||
);
|
||||
|
||||
let panel2Dimensions: PanelDimensionChangeEvent | undefined = undefined;
|
||||
disposables.addDisposables(
|
||||
panel2.api.onDidDimensionsChange((event) => {
|
||||
panel2Dimensions = event;
|
||||
})
|
||||
);
|
||||
|
||||
paneview.layout(600, 400);
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 600, height: 22 });
|
||||
expect(panel2Dimensions).toEqual({ width: 600, height: 22 });
|
||||
|
||||
panel1.api.setSize({ size: 300 });
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 600, height: 22 });
|
||||
expect(panel2Dimensions).toEqual({ width: 600, height: 22 });
|
||||
|
||||
paneview.layout(200, 600);
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 200, height: 22 });
|
||||
expect(panel2Dimensions).toEqual({ width: 200, height: 22 });
|
||||
|
||||
panel1.api.setExpanded(true);
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 200, height: 578 });
|
||||
expect(panel2Dimensions).toEqual({ width: 200, height: 22 });
|
||||
|
||||
panel2.api.setExpanded(true);
|
||||
panel1.api.setSize({ size: 300 });
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 200, height: 300 });
|
||||
expect(panel2Dimensions).toEqual({ width: 200, height: 300 });
|
||||
|
||||
panel1.api.setSize({ size: 200 });
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 200, height: 200 });
|
||||
expect(panel2Dimensions).toEqual({ width: 200, height: 400 });
|
||||
|
||||
disposables.dispose();
|
||||
paneview.dispose();
|
||||
});
|
||||
|
||||
test('serialization', () => {
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
|
||||
|
||||
paneview.fromJSON({
|
||||
size: 6,
|
||||
views: [
|
||||
{
|
||||
size: 1,
|
||||
data: {
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
title: 'Panel 1',
|
||||
},
|
||||
expanded: true,
|
||||
},
|
||||
{
|
||||
size: 2,
|
||||
data: {
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
title: 'Panel 2',
|
||||
},
|
||||
expanded: false,
|
||||
},
|
||||
{
|
||||
size: 3,
|
||||
data: {
|
||||
id: 'panel3',
|
||||
component: 'default',
|
||||
title: 'Panel 3',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
|
||||
|
||||
paneview.layout(400, 800);
|
||||
|
||||
const panel1 = paneview.getPanel('panel1');
|
||||
|
||||
expect(panel1!.api.height).toBe(756);
|
||||
expect(panel1!.api.width).toBe(400);
|
||||
expect(panel1!.api.id).toBe('panel1');
|
||||
// expect(panel1!.api.isActive).toBeTruthy();
|
||||
// expect(panel1?.api.isFocused).toBeFalsy();
|
||||
expect(panel1!.api.isVisible).toBeTruthy();
|
||||
expect(panel1!.api.isExpanded).toBeTruthy();
|
||||
|
||||
const panel2 = paneview.getPanel('panel2');
|
||||
|
||||
expect(panel2!.api.height).toBe(22);
|
||||
expect(panel2!.api.width).toBe(400);
|
||||
expect(panel2!.api.id).toBe('panel2');
|
||||
// expect(panel2!.api.isActive).toBeTruthy();
|
||||
// expect(panel2?.api.isFocused).toBeFalsy();
|
||||
expect(panel2!.api.isVisible).toBeTruthy();
|
||||
expect(panel2!.api.isExpanded).toBeFalsy();
|
||||
|
||||
const panel3 = paneview.getPanel('panel3');
|
||||
|
||||
expect(panel3!.api.height).toBe(22);
|
||||
expect(panel3!.api.width).toBe(400);
|
||||
expect(panel3!.api.id).toBe('panel3');
|
||||
// expect(panel3!.api.isActive).toBeTruthy();
|
||||
// expect(panel3?.api.isFocused).toBeFalsy();
|
||||
expect(panel3!.api.isVisible).toBeTruthy();
|
||||
expect(panel3!.api.isExpanded).toBeFalsy();
|
||||
|
||||
expect(JSON.parse(JSON.stringify(paneview.toJSON()))).toEqual({
|
||||
size: 800,
|
||||
views: [
|
||||
{
|
||||
size: 756,
|
||||
data: {
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
title: 'Panel 1',
|
||||
},
|
||||
expanded: true,
|
||||
headerSize: 22,
|
||||
},
|
||||
{
|
||||
size: 22,
|
||||
data: {
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
title: 'Panel 2',
|
||||
},
|
||||
expanded: false,
|
||||
headerSize: 22,
|
||||
},
|
||||
{
|
||||
size: 22,
|
||||
data: {
|
||||
id: 'panel3',
|
||||
component: 'default',
|
||||
title: 'Panel 3',
|
||||
},
|
||||
expanded: false,
|
||||
headerSize: 22,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('toJSON shouldnt fire any layout events', () => {
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
paneview.layout(1000, 1000);
|
||||
|
||||
paneview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
title: 'Panel 1',
|
||||
});
|
||||
paneview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
title: 'Panel 2',
|
||||
});
|
||||
|
||||
const disposable = paneview.onDidLayoutChange(() => {
|
||||
fail('onDidLayoutChange shouldnt have been called');
|
||||
});
|
||||
|
||||
const result = paneview.toJSON();
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('panel is disposed of when component is disposed', () => {
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
paneview.layout(1000, 1000);
|
||||
|
||||
paneview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
title: 'Panel 1',
|
||||
});
|
||||
paneview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
title: 'Panel 2',
|
||||
});
|
||||
|
||||
const panel1 = paneview.getPanel('panel1')!;
|
||||
const panel2 = paneview.getPanel('panel2')!;
|
||||
|
||||
const panel1Spy = jest.spyOn(panel1, 'dispose');
|
||||
const panel2Spy = jest.spyOn(panel2, 'dispose');
|
||||
|
||||
paneview.dispose();
|
||||
|
||||
expect(panel1Spy).toHaveBeenCalledTimes(1);
|
||||
expect(panel2Spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('panel is disposed of when removed', () => {
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
paneview.layout(1000, 1000);
|
||||
|
||||
paneview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
title: 'Panel 1',
|
||||
});
|
||||
paneview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
title: 'Panel 2',
|
||||
});
|
||||
|
||||
const panel1 = paneview.getPanel('panel1')!;
|
||||
const panel2 = paneview.getPanel('panel2')!;
|
||||
|
||||
const panel1Spy = jest.spyOn(panel1, 'dispose');
|
||||
const panel2Spy = jest.spyOn(panel2, 'dispose');
|
||||
|
||||
paneview.removePanel(panel2);
|
||||
|
||||
expect(panel1Spy).not.toHaveBeenCalled();
|
||||
expect(panel2Spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('panel is disposed of when fromJSON is called', () => {
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
paneview.layout(1000, 1000);
|
||||
|
||||
paneview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
title: 'Panel 1',
|
||||
});
|
||||
paneview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
title: 'Panel 2',
|
||||
});
|
||||
|
||||
const panel1 = paneview.getPanel('panel1')!;
|
||||
const panel2 = paneview.getPanel('panel2')!;
|
||||
|
||||
const panel1Spy = jest.spyOn(panel1, 'dispose');
|
||||
const panel2Spy = jest.spyOn(panel2, 'dispose');
|
||||
|
||||
paneview.fromJSON({ views: [], size: 0 });
|
||||
|
||||
expect(panel1Spy).toHaveBeenCalledTimes(1);
|
||||
expect(panel2Spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('that fromJSON layouts are resized to the current dimensions', async () => {
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
paneview.layout(400, 600);
|
||||
|
||||
paneview.fromJSON({
|
||||
size: 6,
|
||||
views: [
|
||||
{
|
||||
size: 1,
|
||||
data: {
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
title: 'Panel 1',
|
||||
},
|
||||
minimumSize: 100,
|
||||
expanded: true,
|
||||
},
|
||||
{
|
||||
size: 2,
|
||||
data: {
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
title: 'Panel 2',
|
||||
},
|
||||
expanded: true,
|
||||
},
|
||||
{
|
||||
size: 3,
|
||||
data: {
|
||||
id: 'panel3',
|
||||
component: 'default',
|
||||
title: 'Panel 3',
|
||||
},
|
||||
expanded: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// heights slightly differ because header height isn't accounted for
|
||||
expect(JSON.parse(JSON.stringify(paneview.toJSON()))).toEqual({
|
||||
size: 600,
|
||||
views: [
|
||||
{
|
||||
size: 122,
|
||||
data: {
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
title: 'Panel 1',
|
||||
},
|
||||
expanded: true,
|
||||
minimumSize: 100,
|
||||
headerSize: 22,
|
||||
},
|
||||
{
|
||||
size: 22,
|
||||
data: {
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
title: 'Panel 2',
|
||||
},
|
||||
expanded: true,
|
||||
headerSize: 22,
|
||||
},
|
||||
{
|
||||
size: 456,
|
||||
data: {
|
||||
id: 'panel3',
|
||||
component: 'default',
|
||||
title: 'Panel 3',
|
||||
},
|
||||
expanded: true,
|
||||
headerSize: 22,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('that disableAutoResizing is false by default', () => {
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(paneview.disableResizing).toBeFalsy();
|
||||
});
|
||||
|
||||
test('that disableAutoResizing can be enabled', () => {
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
disableAutoResizing: true,
|
||||
});
|
||||
|
||||
expect(paneview.disableResizing).toBeTruthy();
|
||||
});
|
||||
|
||||
test('that setVisible toggles visiblity', () => {
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
disableAutoResizing: true,
|
||||
});
|
||||
|
||||
paneview.layout(1000, 1000);
|
||||
|
||||
const panel1 = paneview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
title: 'panel1',
|
||||
});
|
||||
const panel2 = paneview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
title: 'panel2',
|
||||
});
|
||||
|
||||
expect(panel1.api.isVisible).toBeTruthy();
|
||||
expect(panel2.api.isVisible).toBeTruthy();
|
||||
|
||||
panel1.api.setVisible(false);
|
||||
expect(panel1.api.isVisible).toBeFalsy();
|
||||
expect(panel2.api.isVisible).toBeTruthy();
|
||||
|
||||
panel1.api.setVisible(true);
|
||||
expect(panel1.api.isVisible).toBeTruthy();
|
||||
expect(panel2.api.isVisible).toBeTruthy();
|
||||
});
|
||||
|
||||
test('update className', () => {
|
||||
const paneview = new PaneviewComponent(container, {
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
disableAutoResizing: true,
|
||||
className: 'test-a test-b',
|
||||
});
|
||||
|
||||
expect(paneview.element.className).toBe('test-a test-b');
|
||||
|
||||
paneview.updateOptions({ className: 'test-b test-c' });
|
||||
|
||||
expect(paneview.element.className).toBe('test-b test-c');
|
||||
});
|
||||
});
|
901
packages/dockview-core/src/__tests__/splitview/splitview.spec.ts
Normal file
901
packages/dockview-core/src/__tests__/splitview/splitview.spec.ts
Normal file
@ -0,0 +1,901 @@
|
||||
import { Emitter } from '../../events';
|
||||
import { CompositeDisposable } from '../../lifecycle';
|
||||
import {
|
||||
IView,
|
||||
LayoutPriority,
|
||||
Orientation,
|
||||
Sizing,
|
||||
Splitview,
|
||||
} from '../../splitview/splitview';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
class Testview implements IView {
|
||||
private _element: HTMLElement = document.createElement('div');
|
||||
private _size = 0;
|
||||
private _orthogonalSize = 0;
|
||||
private _priority: LayoutPriority | undefined;
|
||||
|
||||
private readonly _onDidChange = new Emitter<{
|
||||
size?: number;
|
||||
orthogonalSize?: number;
|
||||
}>();
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
private readonly _onLayoutCalled = new Emitter<void>();
|
||||
readonly onLayoutCalled = this._onLayoutCalled.event;
|
||||
|
||||
private readonly _onRendered = new Emitter<void>();
|
||||
readonly onRenderered = this._onRendered.event;
|
||||
|
||||
get minimumSize() {
|
||||
return this._minimumSize;
|
||||
}
|
||||
|
||||
get maximumSize() {
|
||||
return this._maxiumSize;
|
||||
}
|
||||
|
||||
get element() {
|
||||
this._onRendered.fire();
|
||||
return this._element;
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
get orthogonalSize() {
|
||||
return this._orthogonalSize;
|
||||
}
|
||||
|
||||
get priority() {
|
||||
return this._priority;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _minimumSize: number,
|
||||
private _maxiumSize: number,
|
||||
priority?: LayoutPriority
|
||||
) {
|
||||
this._priority = priority;
|
||||
}
|
||||
|
||||
layout(size: number, orthogonalSize: number) {
|
||||
this._size = size;
|
||||
this._orthogonalSize = orthogonalSize;
|
||||
this._onLayoutCalled.fire();
|
||||
}
|
||||
|
||||
fireChangeEvent(value: { size?: number; orthogonalSize?: number }) {
|
||||
this._onDidChange.fire(value);
|
||||
}
|
||||
|
||||
setVisible(isVisible: boolean) {
|
||||
//
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onDidChange.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
describe('splitview', () => {
|
||||
let container: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
container.className = 'container';
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('vertical splitview', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
expect(splitview.orientation).toBe(Orientation.HORIZONTAL);
|
||||
|
||||
const viewQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container dv-horizontal'
|
||||
);
|
||||
expect(viewQuery).toBeTruthy();
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('horiziontal splitview', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
});
|
||||
|
||||
expect(splitview.orientation).toBe(Orientation.VERTICAL);
|
||||
|
||||
const viewQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container dv-vertical'
|
||||
);
|
||||
expect(viewQuery).toBeTruthy();
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('has views and sashes', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
splitview.addView(new Testview(50, 50));
|
||||
splitview.addView(new Testview(50, 50));
|
||||
splitview.addView(new Testview(50, 50));
|
||||
|
||||
let viewQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-view-container > .dv-view'
|
||||
);
|
||||
expect(viewQuery.length).toBe(3);
|
||||
|
||||
let sashQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-sash-container > .dv-sash'
|
||||
);
|
||||
expect(sashQuery.length).toBe(2);
|
||||
|
||||
splitview.removeView(2);
|
||||
|
||||
viewQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-view-container > .dv-view'
|
||||
);
|
||||
expect(viewQuery.length).toBe(2);
|
||||
|
||||
sashQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-sash-container > .dv-sash'
|
||||
);
|
||||
expect(sashQuery.length).toBe(1);
|
||||
|
||||
splitview.removeView(0);
|
||||
|
||||
viewQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-view-container > .dv-view'
|
||||
);
|
||||
expect(viewQuery.length).toBe(1);
|
||||
|
||||
sashQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-sash-container > .dv-sash'
|
||||
);
|
||||
expect(sashQuery.length).toBe(0);
|
||||
|
||||
splitview.removeView(0);
|
||||
|
||||
viewQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-view-container > .dv-view'
|
||||
);
|
||||
expect(viewQuery.length).toBe(0);
|
||||
|
||||
sashQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-sash-container > .dv-sash'
|
||||
);
|
||||
expect(sashQuery.length).toBe(0);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('visiblity classnames', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
const view1 = new Testview(50, 50);
|
||||
const view2 = new Testview(50, 50);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
|
||||
let viewQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-view-container > .dv-view.visible'
|
||||
);
|
||||
expect(viewQuery.length).toBe(2);
|
||||
|
||||
splitview.setViewVisible(1, false);
|
||||
|
||||
viewQuery = container.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-view-container > .dv-view.visible'
|
||||
);
|
||||
expect(viewQuery.length).toBe(1);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('calls lifecycle methods on view', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
splitview.layout(200, 500);
|
||||
|
||||
let rendered = false;
|
||||
let layout = false;
|
||||
|
||||
const view = new Testview(50, Number.POSITIVE_INFINITY);
|
||||
const layoutDisposable = view.onLayoutCalled(() => {
|
||||
layout = true;
|
||||
});
|
||||
const renderDisposable = view.onRenderered(() => {
|
||||
rendered = true;
|
||||
});
|
||||
|
||||
splitview.addView(view);
|
||||
|
||||
splitview.layout(100, 100);
|
||||
|
||||
expect(rendered).toBeTruthy();
|
||||
expect(layout).toBeTruthy();
|
||||
|
||||
layoutDisposable.dispose();
|
||||
renderDisposable.dispose();
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('add view at specified index', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
splitview.layout(200, 500);
|
||||
|
||||
const view1 = new Testview(50, 200);
|
||||
const view2 = new Testview(50, 200);
|
||||
const view3 = new Testview(50, 200);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2, Sizing.Distribute, 0);
|
||||
splitview.addView(view3, Sizing.Distribute, 1);
|
||||
|
||||
expect(splitview.getViews()).toEqual([view2, view3, view1]);
|
||||
});
|
||||
|
||||
test('streches to viewport', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
splitview.layout(200, 500);
|
||||
|
||||
const view = new Testview(50, Number.POSITIVE_INFINITY);
|
||||
|
||||
splitview.addView(view);
|
||||
expect(view.size).toBe(200);
|
||||
|
||||
splitview.layout(100, 500);
|
||||
expect(view.size).toBe(100);
|
||||
|
||||
splitview.layout(50, 500);
|
||||
expect(view.size).toBe(50);
|
||||
|
||||
splitview.layout(30, 500);
|
||||
expect(view.size).toBe(50);
|
||||
|
||||
splitview.layout(100, 500);
|
||||
expect(view.size).toBe(100);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('can resize views 1', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
splitview.layout(200, 500);
|
||||
|
||||
const view1 = new Testview(50, 200);
|
||||
const view2 = new Testview(50, 200);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
|
||||
expect(view1.size).toBe(100);
|
||||
expect(view2.size).toBe(100);
|
||||
|
||||
view1.fireChangeEvent({ size: 65 });
|
||||
|
||||
expect(view1.size).toBe(65);
|
||||
expect(view2.size).toBe(135);
|
||||
|
||||
view2.fireChangeEvent({ size: 75 });
|
||||
|
||||
expect(view1.size).toBe(125);
|
||||
expect(view2.size).toBe(75);
|
||||
|
||||
view2.fireChangeEvent({});
|
||||
|
||||
expect(view1.size).toBe(125);
|
||||
expect(view2.size).toBe(75);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('can resize views 2', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
splitview.layout(200, 500);
|
||||
|
||||
const view1 = new Testview(50, 200);
|
||||
const view2 = new Testview(50, 200);
|
||||
const view3 = new Testview(50, 200);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
splitview.addView(view3);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([66, 66, 68]);
|
||||
|
||||
splitview.resizeView(1, 100);
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([50, 100, 50]);
|
||||
|
||||
splitview.resizeView(2, 60);
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([50, 90, 60]);
|
||||
|
||||
expect([
|
||||
splitview.getViewSize(0),
|
||||
splitview.getViewSize(1),
|
||||
splitview.getViewSize(2),
|
||||
splitview.getViewSize(3),
|
||||
]).toEqual([50, 90, 60, -1]);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('move view', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
splitview.layout(200, 500);
|
||||
|
||||
const view1 = new Testview(50, 200);
|
||||
const view2 = new Testview(50, 200);
|
||||
const view3 = new Testview(50, 200);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
splitview.addView(view3);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([66, 66, 68]);
|
||||
|
||||
splitview.moveView(2, 0);
|
||||
expect(splitview.getViews()).toEqual([view3, view1, view2]);
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([66, 66, 68]);
|
||||
|
||||
splitview.moveView(0, 2);
|
||||
expect(splitview.getViews()).toEqual([view1, view2, view3]);
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([66, 66, 68]);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('layout called after views added', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
const view1 = new Testview(50, 200);
|
||||
const view2 = new Testview(50, 200);
|
||||
const view3 = new Testview(50, 200);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
splitview.addView(view3);
|
||||
|
||||
splitview.layout(200, 500);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([67, 67, 66]);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('proportional layout', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
});
|
||||
|
||||
splitview.layout(200, 500);
|
||||
|
||||
const view1 = new Testview(20, Number.POSITIVE_INFINITY);
|
||||
const view2 = new Testview(20, Number.POSITIVE_INFINITY);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
|
||||
expect([view1.size, view2.size]).toEqual([100, 100]);
|
||||
|
||||
splitview.layout(100, 500);
|
||||
|
||||
expect([view1.size, view2.size]).toEqual([50, 50]);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('disable proportional layout', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
});
|
||||
|
||||
splitview.layout(200, 500);
|
||||
|
||||
const view1 = new Testview(20, Number.POSITIVE_INFINITY);
|
||||
const view2 = new Testview(20, Number.POSITIVE_INFINITY);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
|
||||
expect([view1.size, view2.size]).toEqual([100, 100]);
|
||||
|
||||
splitview.layout(100, 500);
|
||||
|
||||
expect([view1.size, view2.size]).toEqual([80, 20]);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('high priority', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
});
|
||||
|
||||
splitview.layout(300, 500);
|
||||
|
||||
const view1 = new Testview(50, Number.POSITIVE_INFINITY);
|
||||
const view2 = new Testview(
|
||||
50,
|
||||
Number.POSITIVE_INFINITY,
|
||||
LayoutPriority.High
|
||||
);
|
||||
const view3 = new Testview(50, Number.POSITIVE_INFINITY);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
splitview.addView(view3);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([100, 100, 100]);
|
||||
|
||||
splitview.layout(400, 500);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([100, 200, 100]);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('low priority', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
});
|
||||
|
||||
splitview.layout(300, 500);
|
||||
|
||||
const view1 = new Testview(
|
||||
50,
|
||||
Number.POSITIVE_INFINITY,
|
||||
LayoutPriority.Low
|
||||
);
|
||||
const view2 = new Testview(50, Number.POSITIVE_INFINITY);
|
||||
const view3 = new Testview(50, Number.POSITIVE_INFINITY);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
splitview.addView(view3);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([100, 100, 100]);
|
||||
|
||||
splitview.layout(400, 500);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([100, 100, 200]);
|
||||
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('from descriptor', () => {
|
||||
const descriptor = {
|
||||
size: 300,
|
||||
views: [
|
||||
{
|
||||
size: 80,
|
||||
view: new Testview(0, Number.POSITIVE_INFINITY),
|
||||
},
|
||||
{
|
||||
size: 100,
|
||||
view: new Testview(0, Number.POSITIVE_INFINITY),
|
||||
},
|
||||
{
|
||||
size: 120,
|
||||
view: new Testview(0, Number.POSITIVE_INFINITY),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
descriptor,
|
||||
});
|
||||
|
||||
expect([
|
||||
descriptor.views[0].size,
|
||||
descriptor.views[1].size,
|
||||
descriptor.views[2].size,
|
||||
]).toEqual([80, 100, 120]);
|
||||
expect(splitview.size).toBe(300);
|
||||
});
|
||||
|
||||
test('onDidAddView and onDidRemoveView events', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
});
|
||||
|
||||
const added: IView[] = [];
|
||||
const removed: IView[] = [];
|
||||
|
||||
const disposable = new CompositeDisposable(
|
||||
splitview.onDidAddView((view) => added.push(view)),
|
||||
splitview.onDidRemoveView((view) => removed.push(view))
|
||||
);
|
||||
|
||||
const view1 = new Testview(0, 100);
|
||||
const view2 = new Testview(0, 100);
|
||||
|
||||
expect(added.length).toBe(0);
|
||||
expect(removed.length).toBe(0);
|
||||
|
||||
splitview.addView(view1);
|
||||
expect(added.length).toBe(1);
|
||||
expect(removed.length).toBe(0);
|
||||
expect(added[0]).toBe(view1);
|
||||
|
||||
splitview.addView(view2);
|
||||
expect(added.length).toBe(2);
|
||||
expect(removed.length).toBe(0);
|
||||
expect(added[1]).toBe(view2);
|
||||
|
||||
splitview.removeView(0);
|
||||
expect(added.length).toBe(2);
|
||||
expect(removed.length).toBe(1);
|
||||
expect(removed[0]).toBe(view1);
|
||||
|
||||
splitview.removeView(0);
|
||||
expect(added.length).toBe(2);
|
||||
expect(removed.length).toBe(2);
|
||||
expect(removed[1]).toBe(view2);
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('dispose of splitview', () => {
|
||||
expect(container.childNodes.length).toBe(0);
|
||||
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
});
|
||||
|
||||
const view1 = new Testview(0, 100);
|
||||
const view2 = new Testview(0, 100);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
|
||||
expect(container.childNodes.length).toBeGreaterThan(0);
|
||||
|
||||
let anyEvents = false;
|
||||
const listener = splitview.onDidRemoveView((e) => {
|
||||
anyEvents = true; // disposing of the splitview shouldn't fire onDidRemoveView events
|
||||
});
|
||||
|
||||
splitview.dispose();
|
||||
listener.dispose();
|
||||
|
||||
expect(anyEvents).toBeFalsy();
|
||||
expect(container.childNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
test('dnd: pointer events to move sash', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
});
|
||||
splitview.layout(400, 500);
|
||||
|
||||
const view1 = new Testview(0, 1000);
|
||||
const view2 = new Testview(0, 1000);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
|
||||
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
|
||||
const removeEventListenerSpy = jest.spyOn(
|
||||
document,
|
||||
'removeEventListener'
|
||||
);
|
||||
|
||||
const sashElement = container
|
||||
.getElementsByClassName('dv-sash')
|
||||
.item(0) as HTMLElement;
|
||||
|
||||
// validate the expected state before drag
|
||||
expect([view1.size, view2.size]).toEqual([200, 200]);
|
||||
expect(sashElement).toBeTruthy();
|
||||
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
|
||||
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
|
||||
|
||||
// start the drag event
|
||||
fireEvent(
|
||||
sashElement,
|
||||
new MouseEvent('pointerdown', { clientX: 50, clientY: 100 })
|
||||
);
|
||||
|
||||
expect(addEventListenerSpy).toBeCalledTimes(3);
|
||||
|
||||
// during a sash drag the views should have pointer-events disabled
|
||||
expect(view1.element.parentElement!.style.pointerEvents).toBe('none');
|
||||
expect(view2.element.parentElement!.style.pointerEvents).toBe('none');
|
||||
|
||||
// expect a delta move of 70 - 50 = 20
|
||||
fireEvent(
|
||||
document,
|
||||
new MouseEvent('pointermove', { clientX: 70, clientY: 110 })
|
||||
);
|
||||
expect([view1.size, view2.size]).toEqual([220, 180]);
|
||||
|
||||
// expect a delta move of 75 - 70 = 5
|
||||
fireEvent(
|
||||
document,
|
||||
new MouseEvent('pointermove', { clientX: 75, clientY: 110 })
|
||||
);
|
||||
expect([view1.size, view2.size]).toEqual([225, 175]);
|
||||
|
||||
// end the drag event
|
||||
fireEvent(
|
||||
document,
|
||||
new MouseEvent('pointerup', { clientX: 70, clientY: 110 })
|
||||
);
|
||||
|
||||
expect(removeEventListenerSpy).toBeCalledTimes(3);
|
||||
|
||||
// expect pointer-eventes on views to be restored
|
||||
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
|
||||
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
|
||||
|
||||
fireEvent(
|
||||
document,
|
||||
new MouseEvent('pointermove', { clientX: 100, clientY: 100 })
|
||||
);
|
||||
// expect no additional resizes
|
||||
expect([view1.size, view2.size]).toEqual([225, 175]);
|
||||
// expect no additional document listeners
|
||||
expect(addEventListenerSpy).toBeCalledTimes(3);
|
||||
expect(removeEventListenerSpy).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
test('setViewVisible', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
});
|
||||
splitview.layout(900, 500);
|
||||
|
||||
const view1 = new Testview(0, 1000);
|
||||
const view2 = new Testview(0, 1000);
|
||||
const view3 = new Testview(0, 1000);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
splitview.addView(view3);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
|
||||
|
||||
splitview.setViewVisible(0, false);
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([0, 300, 600]);
|
||||
|
||||
splitview.setViewVisible(0, true);
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
|
||||
});
|
||||
|
||||
test('setViewVisible with one view having high layout priority', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
});
|
||||
splitview.layout(900, 500);
|
||||
|
||||
const view1 = new Testview(0, 1000);
|
||||
const view2 = new Testview(0, 1000, LayoutPriority.High);
|
||||
const view3 = new Testview(0, 1000);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
splitview.addView(view3);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
|
||||
|
||||
splitview.setViewVisible(0, false);
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([0, 600, 300]);
|
||||
|
||||
splitview.setViewVisible(0, true);
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
|
||||
});
|
||||
|
||||
test('set view size', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
});
|
||||
splitview.layout(900, 500);
|
||||
|
||||
const view1 = new Testview(0, 1000);
|
||||
const view2 = new Testview(0, 1000);
|
||||
const view3 = new Testview(0, 1000);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
splitview.addView(view3);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
|
||||
|
||||
view1.fireChangeEvent({ size: 0 });
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([0, 300, 600]);
|
||||
|
||||
view1.fireChangeEvent({ size: 300 });
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
|
||||
});
|
||||
|
||||
test('set view size with one view having high layout priority', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
});
|
||||
splitview.layout(900, 500);
|
||||
|
||||
const view1 = new Testview(0, 1000);
|
||||
const view2 = new Testview(0, 1000, LayoutPriority.High);
|
||||
const view3 = new Testview(0, 1000);
|
||||
|
||||
splitview.addView(view1);
|
||||
splitview.addView(view2);
|
||||
splitview.addView(view3);
|
||||
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
|
||||
|
||||
view1.fireChangeEvent({ size: 0 });
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([0, 600, 300]);
|
||||
|
||||
view1.fireChangeEvent({ size: 300 });
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
|
||||
});
|
||||
|
||||
test('that margins are applied to view sizing', () => {
|
||||
const splitview = new Splitview(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
proportionalLayout: false,
|
||||
margin: 24,
|
||||
});
|
||||
splitview.layout(924, 500);
|
||||
|
||||
const view1 = new Testview(0, 1000);
|
||||
const view2 = new Testview(0, 1000);
|
||||
const view3 = new Testview(0, 1000);
|
||||
const view4 = new Testview(0, 1000);
|
||||
|
||||
splitview.addView(view1);
|
||||
expect([view1.size]).toEqual([924]);
|
||||
|
||||
splitview.addView(view2);
|
||||
expect([view1.size, view2.size]).toEqual([450, 450]); // 450 + 24 + 450 = 924
|
||||
|
||||
splitview.addView(view3);
|
||||
expect([view1.size, view2.size, view3.size]).toEqual([292, 292, 292]); // 292 + 24 + 292 + 24 + 292 = 924
|
||||
|
||||
splitview.addView(view4);
|
||||
expect([view1.size, view2.size, view3.size, view4.size]).toEqual([
|
||||
213, 213, 213, 213,
|
||||
]); // 213 + 24 + 213 + 24 + 213 + 24 + 213 = 924
|
||||
|
||||
let viewQuery = Array.from(
|
||||
container
|
||||
.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-view-container > .dv-view'
|
||||
)
|
||||
.entries()
|
||||
)
|
||||
.map(([i, e]) => e as HTMLElement)
|
||||
.map((e) => ({
|
||||
left: e.style.left,
|
||||
top: e.style.top,
|
||||
height: e.style.height,
|
||||
width: e.style.width,
|
||||
}));
|
||||
|
||||
let sashQuery = Array.from(
|
||||
container
|
||||
.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-sash-container > .dv-sash'
|
||||
)
|
||||
.entries()
|
||||
)
|
||||
.map(([i, e]) => e as HTMLElement)
|
||||
.map((e) => ({
|
||||
left: e.style.left,
|
||||
top: e.style.top,
|
||||
}));
|
||||
|
||||
// check HTMLElement positions since these are the ones that really matter
|
||||
|
||||
expect(viewQuery).toEqual([
|
||||
{ left: '0px', top: '', width: '213px', height: '' },
|
||||
// 213 + 24 = 237
|
||||
{ left: '237px', top: '', width: '213px', height: '' },
|
||||
// 237 + 213 + 24 = 474
|
||||
{ left: '474px', top: '', width: '213px', height: '' },
|
||||
// 474 + 213 + 24 = 474
|
||||
{ left: '711px', top: '', width: '213px', height: '' },
|
||||
// 711 + 213 = 924
|
||||
]);
|
||||
|
||||
// 924 / 4 = 231 view size
|
||||
// 231 - (24*3/4) = 213 margin adjusted view size
|
||||
// 213 - 4/2 + 24/2 = 223
|
||||
expect(sashQuery).toEqual([
|
||||
// 213 - 4/2 + 24/2 = 223
|
||||
{ left: '223px', top: '0px' },
|
||||
// 213 + 24 + 213 = 450
|
||||
// 450 - 4/2 + 24/2 = 460
|
||||
{ left: '460px', top: '0px' },
|
||||
// 213 + 24 + 213 + 24 + 213 = 687
|
||||
// 687 - 4/2 + 24/2 = 697
|
||||
{ left: '697px', top: '0px' },
|
||||
]);
|
||||
|
||||
splitview.setViewVisible(0, false);
|
||||
|
||||
viewQuery = Array.from(
|
||||
container
|
||||
.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-view-container > .dv-view'
|
||||
)
|
||||
.entries()
|
||||
)
|
||||
.map(([i, e]) => e as HTMLElement)
|
||||
.map((e) => ({
|
||||
left: e.style.left,
|
||||
top: e.style.top,
|
||||
height: e.style.height,
|
||||
width: e.style.width,
|
||||
}));
|
||||
|
||||
sashQuery = Array.from(
|
||||
container
|
||||
.querySelectorAll(
|
||||
'.dv-split-view-container > .dv-sash-container > .dv-sash'
|
||||
)
|
||||
.entries()
|
||||
)
|
||||
.map(([i, e]) => e as HTMLElement)
|
||||
.map((e) => ({
|
||||
left: e.style.left,
|
||||
top: e.style.top,
|
||||
}));
|
||||
|
||||
expect(viewQuery).toEqual([
|
||||
{ left: '0px', top: '', width: '0px', height: '' },
|
||||
{ left: '0px', top: '', width: '215px', height: '' },
|
||||
{ left: '239px', top: '', width: '215px', height: '' },
|
||||
{ left: '478px', top: '', width: '446px', height: '' },
|
||||
]);
|
||||
|
||||
expect(sashQuery).toEqual([
|
||||
{ left: '0px', top: '0px' },
|
||||
{ left: '225px', top: '0px' },
|
||||
{ left: '464px', top: '0px' },
|
||||
]);
|
||||
});
|
||||
});
|
@ -0,0 +1,743 @@
|
||||
import { PanelDimensionChangeEvent } from '../../api/panelApi';
|
||||
import { Emitter } from '../../events';
|
||||
import { CompositeDisposable } from '../../lifecycle';
|
||||
import { Orientation } from '../../splitview/splitview';
|
||||
import { SplitviewComponent } from '../../splitview/splitviewComponent';
|
||||
import { SplitviewPanel } from '../../splitview/splitviewPanel';
|
||||
|
||||
class TestPanel extends SplitviewPanel {
|
||||
getComponent() {
|
||||
return {
|
||||
update: () => {
|
||||
//
|
||||
},
|
||||
dispose: () => {
|
||||
//
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('componentSplitview', () => {
|
||||
let container: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
container.className = 'container';
|
||||
});
|
||||
|
||||
test('that the container is not removed when grid is disposed', () => {
|
||||
const root = document.createElement('div');
|
||||
const container = document.createElement('div');
|
||||
root.appendChild(container);
|
||||
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
splitview.dispose();
|
||||
|
||||
expect(container.parentElement).toBe(root);
|
||||
expect(container.children.length).toBe(0);
|
||||
});
|
||||
|
||||
test('event leakage', () => {
|
||||
Emitter.setLeakageMonitorEnabled(true);
|
||||
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
splitview.layout(600, 400);
|
||||
|
||||
const panel1 = splitview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
});
|
||||
const panel2 = splitview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
});
|
||||
|
||||
splitview.movePanel(0, 1);
|
||||
|
||||
splitview.removePanel(panel1);
|
||||
|
||||
splitview.dispose();
|
||||
|
||||
if (Emitter.MEMORY_LEAK_WATCHER.size > 0) {
|
||||
for (const entry of Array.from(
|
||||
Emitter.MEMORY_LEAK_WATCHER.events
|
||||
)) {
|
||||
console.log(entry[1]);
|
||||
}
|
||||
throw new Error('not all listeners disposed');
|
||||
}
|
||||
|
||||
Emitter.setLeakageMonitorEnabled(false);
|
||||
});
|
||||
|
||||
test('remove panel', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
splitview.layout(600, 400);
|
||||
|
||||
splitview.addPanel({ id: 'panel1', component: 'default' });
|
||||
splitview.addPanel({ id: 'panel2', component: 'default' });
|
||||
splitview.addPanel({ id: 'panel3', component: 'default' });
|
||||
|
||||
const panel1 = splitview.getPanel('panel1')!;
|
||||
const panel2 = splitview.getPanel('panel2')!;
|
||||
const panel3 = splitview.getPanel('panel3')!;
|
||||
|
||||
expect(panel1.api.isActive).toBeFalsy();
|
||||
expect(panel2.api.isActive).toBeFalsy();
|
||||
expect(panel3.api.isActive).toBeTruthy();
|
||||
|
||||
splitview.removePanel(panel3);
|
||||
|
||||
expect(panel1.api.isActive).toBeFalsy();
|
||||
expect(panel2.api.isActive).toBeTruthy();
|
||||
expect(splitview.length).toBe(2);
|
||||
|
||||
splitview.removePanel(panel1);
|
||||
expect(panel2.api.isActive).toBeTruthy();
|
||||
expect(splitview.length).toBe(1);
|
||||
|
||||
splitview.removePanel(panel2);
|
||||
expect(splitview.length).toBe(0);
|
||||
});
|
||||
|
||||
test('horizontal dimensions', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
splitview.layout(600, 400);
|
||||
|
||||
expect(splitview.height).toBe(400);
|
||||
expect(splitview.width).toBe(600);
|
||||
});
|
||||
|
||||
test('vertical dimensions', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
splitview.layout(600, 400);
|
||||
|
||||
expect(splitview.height).toBe(400);
|
||||
expect(splitview.width).toBe(600);
|
||||
});
|
||||
|
||||
test('api resize', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
splitview.layout(400, 600);
|
||||
splitview.addPanel({ id: 'panel1', component: 'default' });
|
||||
splitview.addPanel({ id: 'panel2', component: 'default' });
|
||||
splitview.addPanel({ id: 'panel3', component: 'default' });
|
||||
|
||||
const panel1 = splitview.getPanel('panel1')!;
|
||||
const panel2 = splitview.getPanel('panel2')!;
|
||||
const panel3 = splitview.getPanel('panel3')!;
|
||||
|
||||
expect(panel1.width).toBe(400);
|
||||
expect(panel1.height).toBe(200);
|
||||
expect(panel2.width).toBe(400);
|
||||
expect(panel2.height).toBe(200);
|
||||
expect(panel3.width).toBe(400);
|
||||
expect(panel3.height).toBe(200);
|
||||
|
||||
panel1.api.setSize({ size: 100 });
|
||||
|
||||
expect(panel1.width).toBe(400);
|
||||
expect(panel1.height).toBe(100);
|
||||
expect(panel2.width).toBe(400);
|
||||
expect(panel2.height).toBe(200);
|
||||
expect(panel3.width).toBe(400);
|
||||
expect(panel3.height).toBe(300);
|
||||
|
||||
panel2.api.setSize({ size: 100 });
|
||||
|
||||
expect(panel1.width).toBe(400);
|
||||
expect(panel1.height).toBe(100);
|
||||
expect(panel2.width).toBe(400);
|
||||
expect(panel2.height).toBe(100);
|
||||
expect(panel3.width).toBe(400);
|
||||
expect(panel3.height).toBe(400);
|
||||
|
||||
panel3.api.setSize({ size: 100 });
|
||||
|
||||
expect(panel1.width).toBe(400);
|
||||
expect(panel1.height).toBe(100);
|
||||
expect(panel2.width).toBe(400);
|
||||
expect(panel2.height).toBe(400);
|
||||
expect(panel3.width).toBe(400);
|
||||
expect(panel3.height).toBe(100);
|
||||
});
|
||||
|
||||
test('api', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
splitview.layout(600, 400);
|
||||
splitview.addPanel({ id: 'panel1', component: 'default' });
|
||||
|
||||
const panel1 = splitview.getPanel('panel1');
|
||||
|
||||
expect(panel1!.api.height).toBe(400);
|
||||
expect(panel1!.api.width).toBe(600);
|
||||
expect(panel1!.api.id).toBe('panel1');
|
||||
expect(panel1!.api.isActive).toBeTruthy();
|
||||
// expect(panel1?.api.isFocused).toBeFalsy();
|
||||
expect(panel1!.api.isVisible).toBeTruthy();
|
||||
|
||||
splitview.addPanel({ id: 'panel2', component: 'default' });
|
||||
|
||||
const panel2 = splitview.getPanel('panel2');
|
||||
|
||||
expect(panel1!.api.isActive).toBeFalsy();
|
||||
|
||||
expect(panel2!.api.height).toBe(400);
|
||||
expect(panel2!.api.width).toBe(300);
|
||||
expect(panel2!.api.id).toBe('panel2');
|
||||
expect(panel2!.api.isActive).toBeTruthy();
|
||||
// expect(panel2!.api.isFocused).toBeFalsy();
|
||||
expect(panel2!.api.isVisible).toBeTruthy();
|
||||
|
||||
panel1?.api.setActive();
|
||||
|
||||
expect(panel1!.api.isActive).toBeTruthy();
|
||||
expect(panel2!.api.isActive).toBeFalsy();
|
||||
});
|
||||
|
||||
test('vertical panels', () => {
|
||||
const disposables = new CompositeDisposable();
|
||||
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
splitview.layout(300, 200);
|
||||
|
||||
splitview.addPanel({ id: 'panel1', component: 'default' });
|
||||
splitview.addPanel({ id: 'panel2', component: 'default' });
|
||||
|
||||
const panel1 = splitview.getPanel('panel1') as SplitviewPanel;
|
||||
const panel2 = splitview.getPanel('panel2') as SplitviewPanel;
|
||||
|
||||
let panel1Dimensions: PanelDimensionChangeEvent | undefined;
|
||||
disposables.addDisposables(
|
||||
panel1.api.onDidDimensionsChange((event) => {
|
||||
panel1Dimensions = event;
|
||||
})
|
||||
);
|
||||
|
||||
let panel2Dimensions: PanelDimensionChangeEvent | undefined;
|
||||
disposables.addDisposables(
|
||||
panel2.api.onDidDimensionsChange((event) => {
|
||||
panel2Dimensions = event;
|
||||
})
|
||||
);
|
||||
|
||||
splitview.layout(600, 400);
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 600, height: 200 });
|
||||
expect(panel2Dimensions).toEqual({ width: 600, height: 200 });
|
||||
|
||||
panel1.api.setSize({ size: 300 });
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 600, height: 300 });
|
||||
expect(panel2Dimensions).toEqual({ width: 600, height: 100 });
|
||||
|
||||
splitview.layout(200, 600);
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 200, height: 450 });
|
||||
expect(panel2Dimensions).toEqual({ width: 200, height: 150 });
|
||||
|
||||
disposables.dispose();
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('horizontal panels', () => {
|
||||
const disposables = new CompositeDisposable();
|
||||
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
splitview.layout(300, 200);
|
||||
|
||||
splitview.addPanel({ id: 'panel1', component: 'default' });
|
||||
splitview.addPanel({ id: 'panel2', component: 'default' });
|
||||
|
||||
const panel1 = splitview.getPanel('panel1') as SplitviewPanel;
|
||||
const panel2 = splitview.getPanel('panel2') as SplitviewPanel;
|
||||
|
||||
let panel1Dimensions: PanelDimensionChangeEvent | undefined;
|
||||
disposables.addDisposables(
|
||||
panel1.api.onDidDimensionsChange((event) => {
|
||||
panel1Dimensions = event;
|
||||
})
|
||||
);
|
||||
|
||||
let panel2Dimensions: PanelDimensionChangeEvent | undefined;
|
||||
disposables.addDisposables(
|
||||
panel2.api.onDidDimensionsChange((event) => {
|
||||
panel2Dimensions = event;
|
||||
})
|
||||
);
|
||||
|
||||
splitview.layout(600, 400);
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 300, height: 400 });
|
||||
expect(panel2Dimensions).toEqual({ width: 300, height: 400 });
|
||||
|
||||
panel1.api.setSize({ size: 200 });
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 200, height: 400 });
|
||||
expect(panel2Dimensions).toEqual({ width: 400, height: 400 });
|
||||
|
||||
splitview.layout(200, 600);
|
||||
|
||||
expect(panel1Dimensions).toEqual({ width: 67, height: 600 });
|
||||
expect(panel2Dimensions).toEqual({ width: 133, height: 600 });
|
||||
|
||||
disposables.dispose();
|
||||
splitview.dispose();
|
||||
});
|
||||
|
||||
test('serialization', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
splitview.layout(400, 6);
|
||||
|
||||
expect(
|
||||
container.querySelectorAll('.dv-split-view-container').length
|
||||
).toBe(1);
|
||||
|
||||
splitview.fromJSON({
|
||||
views: [
|
||||
{
|
||||
size: 1,
|
||||
data: { id: 'panel1', component: 'default' },
|
||||
snap: false,
|
||||
},
|
||||
{
|
||||
size: 2,
|
||||
data: { id: 'panel2', component: 'default' },
|
||||
snap: true,
|
||||
},
|
||||
{ size: 3, data: { id: 'panel3', component: 'default' } },
|
||||
],
|
||||
size: 6,
|
||||
orientation: Orientation.VERTICAL,
|
||||
activeView: 'panel1',
|
||||
});
|
||||
|
||||
expect(
|
||||
container.querySelectorAll('.dv-split-view-container').length
|
||||
).toBe(1);
|
||||
|
||||
expect(splitview.length).toBe(3);
|
||||
|
||||
expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({
|
||||
views: [
|
||||
{
|
||||
size: 1,
|
||||
data: { id: 'panel1', component: 'default' },
|
||||
snap: false,
|
||||
},
|
||||
{
|
||||
size: 2,
|
||||
data: { id: 'panel2', component: 'default' },
|
||||
snap: true,
|
||||
},
|
||||
{
|
||||
size: 3,
|
||||
data: { id: 'panel3', component: 'default' },
|
||||
snap: false,
|
||||
},
|
||||
],
|
||||
size: 6,
|
||||
orientation: Orientation.VERTICAL,
|
||||
activeView: 'panel1',
|
||||
});
|
||||
});
|
||||
|
||||
test('toJSON shouldnt fire any layout events', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
splitview.layout(1000, 1000);
|
||||
|
||||
splitview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
});
|
||||
splitview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
});
|
||||
|
||||
const disposable = splitview.onDidLayoutChange(() => {
|
||||
fail('onDidLayoutChange shouldnt have been called');
|
||||
});
|
||||
|
||||
const result = splitview.toJSON();
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('panel is disposed of when component is disposed', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
splitview.layout(1000, 1000);
|
||||
|
||||
splitview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
});
|
||||
splitview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
});
|
||||
|
||||
const panel1 = splitview.getPanel('panel1')!;
|
||||
const panel2 = splitview.getPanel('panel2')!;
|
||||
|
||||
const panel1Spy = jest.spyOn(panel1, 'dispose');
|
||||
const panel2Spy = jest.spyOn(panel2, 'dispose');
|
||||
|
||||
splitview.dispose();
|
||||
|
||||
expect(panel1Spy).toHaveBeenCalledTimes(1);
|
||||
expect(panel2Spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('panel is disposed of when removed', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
splitview.layout(1000, 1000);
|
||||
|
||||
splitview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
});
|
||||
splitview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
});
|
||||
|
||||
const panel1 = splitview.getPanel('panel1')!;
|
||||
const panel2 = splitview.getPanel('panel2')!;
|
||||
|
||||
const panel1Spy = jest.spyOn(panel1, 'dispose');
|
||||
const panel2Spy = jest.spyOn(panel2, 'dispose');
|
||||
|
||||
splitview.removePanel(panel2);
|
||||
|
||||
expect(panel1Spy).not.toHaveBeenCalled();
|
||||
expect(panel2Spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('panel is disposed of when fromJSON is called', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
splitview.layout(1000, 1000);
|
||||
|
||||
splitview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
});
|
||||
splitview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
});
|
||||
|
||||
const panel1 = splitview.getPanel('panel1')!;
|
||||
const panel2 = splitview.getPanel('panel2')!;
|
||||
|
||||
const panel1Spy = jest.spyOn(panel1, 'dispose');
|
||||
const panel2Spy = jest.spyOn(panel2, 'dispose');
|
||||
|
||||
splitview.fromJSON({
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
size: 0,
|
||||
views: [],
|
||||
});
|
||||
|
||||
expect(panel1Spy).toHaveBeenCalledTimes(1);
|
||||
expect(panel2Spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('that fromJSON layouts are resized to the current dimensions', async () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
splitview.layout(400, 600);
|
||||
|
||||
splitview.fromJSON({
|
||||
views: [
|
||||
{
|
||||
size: 1,
|
||||
data: { id: 'panel1', component: 'default' },
|
||||
snap: false,
|
||||
},
|
||||
{
|
||||
size: 2,
|
||||
data: { id: 'panel2', component: 'default' },
|
||||
snap: true,
|
||||
},
|
||||
{ size: 3, data: { id: 'panel3', component: 'default' } },
|
||||
],
|
||||
size: 6,
|
||||
orientation: Orientation.VERTICAL,
|
||||
activeView: 'panel1',
|
||||
});
|
||||
|
||||
expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({
|
||||
views: [
|
||||
{
|
||||
size: 100,
|
||||
data: { id: 'panel1', component: 'default' },
|
||||
snap: false,
|
||||
},
|
||||
{
|
||||
size: 200,
|
||||
data: { id: 'panel2', component: 'default' },
|
||||
snap: true,
|
||||
},
|
||||
{
|
||||
size: 300,
|
||||
data: { id: 'panel3', component: 'default' },
|
||||
snap: false,
|
||||
},
|
||||
],
|
||||
size: 600,
|
||||
orientation: Orientation.VERTICAL,
|
||||
activeView: 'panel1',
|
||||
});
|
||||
});
|
||||
|
||||
test('that disableAutoResizing is false by default', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(splitview.disableResizing).toBeFalsy();
|
||||
});
|
||||
|
||||
test('that disableAutoResizing can be enabled', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
disableAutoResizing: true,
|
||||
});
|
||||
|
||||
expect(splitview.disableResizing).toBeTruthy();
|
||||
});
|
||||
|
||||
test('that setVisible toggles visiblity', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
splitview.layout(1000, 1000);
|
||||
|
||||
const panel1 = splitview.addPanel({
|
||||
id: 'panel1',
|
||||
component: 'default',
|
||||
});
|
||||
const panel2 = splitview.addPanel({
|
||||
id: 'panel2',
|
||||
component: 'default',
|
||||
});
|
||||
|
||||
expect(panel1.api.isVisible).toBeTruthy();
|
||||
expect(panel2.api.isVisible).toBeTruthy();
|
||||
|
||||
panel1.api.setVisible(false);
|
||||
expect(panel1.api.isVisible).toBeFalsy();
|
||||
expect(panel2.api.isVisible).toBeTruthy();
|
||||
|
||||
panel1.api.setVisible(true);
|
||||
expect(panel1.api.isVisible).toBeTruthy();
|
||||
expect(panel2.api.isVisible).toBeTruthy();
|
||||
});
|
||||
|
||||
test('update className', () => {
|
||||
const splitview = new SplitviewComponent(container, {
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
createComponent: (options) => {
|
||||
switch (options.name) {
|
||||
case 'default':
|
||||
return new TestPanel(options.id, options.name);
|
||||
default:
|
||||
throw new Error('unsupported');
|
||||
}
|
||||
},
|
||||
className: 'test-a test-b',
|
||||
});
|
||||
|
||||
expect(splitview.element.className).toBe('test-a test-b');
|
||||
|
||||
splitview.updateOptions({ className: 'test-b test-c' });
|
||||
|
||||
expect(splitview.element.className).toBe('test-b test-c');
|
||||
});
|
||||
});
|
933
packages/dockview-core/src/api/component.api.ts
Normal file
933
packages/dockview-core/src/api/component.api.ts
Normal file
@ -0,0 +1,933 @@
|
||||
import {
|
||||
DockviewMaximizedGroupChanged,
|
||||
FloatingGroupOptions,
|
||||
IDockviewComponent,
|
||||
MovePanelEvent,
|
||||
PopoutGroupChangePositionEvent,
|
||||
PopoutGroupChangeSizeEvent,
|
||||
SerializedDockview,
|
||||
} from '../dockview/dockviewComponent';
|
||||
import {
|
||||
AddGroupOptions,
|
||||
AddPanelOptions,
|
||||
DockviewComponentOptions,
|
||||
DockviewDndOverlayEvent,
|
||||
MovementOptions,
|
||||
} from '../dockview/options';
|
||||
import { Parameters } from '../panel/types';
|
||||
import { Direction } from '../gridview/baseComponentGridview';
|
||||
import {
|
||||
AddComponentOptions,
|
||||
IGridviewComponent,
|
||||
SerializedGridviewComponent,
|
||||
} from '../gridview/gridviewComponent';
|
||||
import { IGridviewPanel } from '../gridview/gridviewPanel';
|
||||
|
||||
import {
|
||||
AddPaneviewComponentOptions,
|
||||
SerializedPaneview,
|
||||
IPaneviewComponent,
|
||||
} from '../paneview/paneviewComponent';
|
||||
import { IPaneviewPanel } from '../paneview/paneviewPanel';
|
||||
import {
|
||||
AddSplitviewComponentOptions,
|
||||
ISplitviewComponent,
|
||||
SerializedSplitview,
|
||||
} from '../splitview/splitviewComponent';
|
||||
import { IView, Orientation, Sizing } from '../splitview/splitview';
|
||||
import { ISplitviewPanel } from '../splitview/splitviewPanel';
|
||||
import {
|
||||
DockviewGroupPanel,
|
||||
IDockviewGroupPanel,
|
||||
} from '../dockview/dockviewGroupPanel';
|
||||
import { Event } from '../events';
|
||||
import { IDockviewPanel } from '../dockview/dockviewPanel';
|
||||
import { PaneviewDidDropEvent } from '../paneview/draggablePaneviewPanel';
|
||||
import {
|
||||
GroupDragEvent,
|
||||
TabDragEvent,
|
||||
} from '../dockview/components/titlebar/tabsContainer';
|
||||
import { Box } from '../types';
|
||||
import {
|
||||
DockviewDidDropEvent,
|
||||
DockviewWillDropEvent,
|
||||
WillShowOverlayLocationEvent,
|
||||
} from '../dockview/dockviewGroupPanelModel';
|
||||
import {
|
||||
PaneviewComponentOptions,
|
||||
PaneviewDndOverlayEvent,
|
||||
} from '../paneview/options';
|
||||
import { SplitviewComponentOptions } from '../splitview/options';
|
||||
import { GridviewComponentOptions } from '../gridview/options';
|
||||
|
||||
export interface CommonApi<T = any> {
|
||||
readonly height: number;
|
||||
readonly width: number;
|
||||
readonly onDidLayoutChange: Event<void>;
|
||||
readonly onDidLayoutFromJSON: Event<void>;
|
||||
focus(): void;
|
||||
layout(width: number, height: number): void;
|
||||
fromJSON(data: T): void;
|
||||
toJSON(): T;
|
||||
clear(): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export class SplitviewApi implements CommonApi<SerializedSplitview> {
|
||||
/**
|
||||
* The minimum size the component can reach where size is measured in the direction of orientation provided.
|
||||
*/
|
||||
get minimumSize(): number {
|
||||
return this.component.minimumSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum size the component can reach where size is measured in the direction of orientation provided.
|
||||
*/
|
||||
get maximumSize(): number {
|
||||
return this.component.maximumSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the component.
|
||||
*/
|
||||
get width(): number {
|
||||
return this.component.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Height of the component.
|
||||
*/
|
||||
get height(): number {
|
||||
return this.component.height;
|
||||
}
|
||||
/**
|
||||
* The current number of panels.
|
||||
*/
|
||||
get length(): number {
|
||||
return this.component.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current orientation of the component.
|
||||
*/
|
||||
get orientation(): Orientation {
|
||||
return this.component.orientation;
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of current panels.
|
||||
*/
|
||||
get panels(): ISplitviewPanel[] {
|
||||
return this.component.panels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after a layout is loaded through the `fromJSON` method.
|
||||
*/
|
||||
get onDidLayoutFromJSON(): Event<void> {
|
||||
return this.component.onDidLayoutFromJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked whenever any aspect of the layout changes.
|
||||
* If listening to this event it may be worth debouncing ouputs.
|
||||
*/
|
||||
get onDidLayoutChange(): Event<void> {
|
||||
return this.component.onDidLayoutChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a view is added.
|
||||
*/
|
||||
get onDidAddView(): Event<IView> {
|
||||
return this.component.onDidAddView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a view is removed.
|
||||
*/
|
||||
get onDidRemoveView(): Event<IView> {
|
||||
return this.component.onDidRemoveView;
|
||||
}
|
||||
|
||||
constructor(private readonly component: ISplitviewComponent) {}
|
||||
|
||||
/**
|
||||
* Removes an existing panel and optionally provide a `Sizing` method
|
||||
* for the subsequent resize.
|
||||
*/
|
||||
removePanel(panel: ISplitviewPanel, sizing?: Sizing): void {
|
||||
this.component.removePanel(panel, sizing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus the component.
|
||||
*/
|
||||
focus(): void {
|
||||
this.component.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reference to a panel given it's `string` id.
|
||||
*/
|
||||
getPanel(id: string): ISplitviewPanel | undefined {
|
||||
return this.component.getPanel(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout the panel with a width and height.
|
||||
*/
|
||||
layout(width: number, height: number): void {
|
||||
return this.component.layout(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new panel and return the created instance.
|
||||
*/
|
||||
addPanel<T extends object = Parameters>(
|
||||
options: AddSplitviewComponentOptions<T>
|
||||
): ISplitviewPanel {
|
||||
return this.component.addPanel(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a panel given it's current and desired index.
|
||||
*/
|
||||
movePanel(from: number, to: number): void {
|
||||
this.component.movePanel(from, to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a layout to built a splitivew.
|
||||
*/
|
||||
fromJSON(data: SerializedSplitview): void {
|
||||
this.component.fromJSON(data);
|
||||
}
|
||||
|
||||
/** Serialize a layout */
|
||||
toJSON(): SerializedSplitview {
|
||||
return this.component.toJSON();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all panels and clear the component.
|
||||
*/
|
||||
clear(): void {
|
||||
this.component.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuratable options.
|
||||
*/
|
||||
updateOptions(options: Partial<SplitviewComponentOptions>): void {
|
||||
this.component.updateOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release resources and teardown component. Do not call when using framework versions of dockview.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.component.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class PaneviewApi implements CommonApi<SerializedPaneview> {
|
||||
/**
|
||||
* The minimum size the component can reach where size is measured in the direction of orientation provided.
|
||||
*/
|
||||
get minimumSize(): number {
|
||||
return this.component.minimumSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum size the component can reach where size is measured in the direction of orientation provided.
|
||||
*/
|
||||
get maximumSize(): number {
|
||||
return this.component.maximumSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the component.
|
||||
*/
|
||||
get width(): number {
|
||||
return this.component.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Height of the component.
|
||||
*/
|
||||
get height(): number {
|
||||
return this.component.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* All panel objects.
|
||||
*/
|
||||
get panels(): IPaneviewPanel[] {
|
||||
return this.component.panels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when any layout change occures, an aggregation of many events.
|
||||
*/
|
||||
get onDidLayoutChange(): Event<void> {
|
||||
return this.component.onDidLayoutChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after a layout is deserialzied using the `fromJSON` method.
|
||||
*/
|
||||
get onDidLayoutFromJSON(): Event<void> {
|
||||
return this.component.onDidLayoutFromJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a panel is added. May be called multiple times when moving panels.
|
||||
*/
|
||||
get onDidAddView(): Event<IPaneviewPanel> {
|
||||
return this.component.onDidAddView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a panel is removed. May be called multiple times when moving panels.
|
||||
*/
|
||||
get onDidRemoveView(): Event<IPaneviewPanel> {
|
||||
return this.component.onDidRemoveView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a Drag'n'Drop event occurs that the component was unable to handle. Exposed for custom Drag'n'Drop functionality.
|
||||
*/
|
||||
get onDidDrop(): Event<PaneviewDidDropEvent> {
|
||||
return this.component.onDidDrop;
|
||||
}
|
||||
|
||||
get onUnhandledDragOverEvent(): Event<PaneviewDndOverlayEvent> {
|
||||
return this.component.onUnhandledDragOverEvent;
|
||||
}
|
||||
|
||||
constructor(private readonly component: IPaneviewComponent) {}
|
||||
|
||||
/**
|
||||
* Remove a panel given the panel object.
|
||||
*/
|
||||
removePanel(panel: IPaneviewPanel): void {
|
||||
this.component.removePanel(panel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a panel object given a `string` id. May return `undefined`.
|
||||
*/
|
||||
getPanel(id: string): IPaneviewPanel | undefined {
|
||||
return this.component.getPanel(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a panel given it's current and desired index.
|
||||
*/
|
||||
movePanel(from: number, to: number): void {
|
||||
this.component.movePanel(from, to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus the component. Will try to focus an active panel if one exists.
|
||||
*/
|
||||
focus(): void {
|
||||
this.component.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force resize the component to an exact width and height. Read about auto-resizing before using.
|
||||
*/
|
||||
layout(width: number, height: number): void {
|
||||
this.component.layout(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a panel and return the created object.
|
||||
*/
|
||||
addPanel<T extends object = Parameters>(
|
||||
options: AddPaneviewComponentOptions<T>
|
||||
): IPaneviewPanel {
|
||||
return this.component.addPanel(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a component from a serialized object.
|
||||
*/
|
||||
fromJSON(data: SerializedPaneview): void {
|
||||
this.component.fromJSON(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a serialized object of the current component.
|
||||
*/
|
||||
toJSON(): SerializedPaneview {
|
||||
return this.component.toJSON();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the component back to an empty and default state.
|
||||
*/
|
||||
clear(): void {
|
||||
this.component.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuratable options.
|
||||
*/
|
||||
updateOptions(options: Partial<PaneviewComponentOptions>): void {
|
||||
this.component.updateOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release resources and teardown component. Do not call when using framework versions of dockview.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.component.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class GridviewApi implements CommonApi<SerializedGridviewComponent> {
|
||||
/**
|
||||
* Width of the component.
|
||||
*/
|
||||
get width(): number {
|
||||
return this.component.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Height of the component.
|
||||
*/
|
||||
get height(): number {
|
||||
return this.component.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum height of the component.
|
||||
*/
|
||||
get minimumHeight(): number {
|
||||
return this.component.minimumHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum height of the component.
|
||||
*/
|
||||
get maximumHeight(): number {
|
||||
return this.component.maximumHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum width of the component.
|
||||
*/
|
||||
get minimumWidth(): number {
|
||||
return this.component.minimumWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum width of the component.
|
||||
*/
|
||||
get maximumWidth(): number {
|
||||
return this.component.maximumWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when any layout change occures, an aggregation of many events.
|
||||
*/
|
||||
get onDidLayoutChange(): Event<void> {
|
||||
return this.component.onDidLayoutChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a panel is added. May be called multiple times when moving panels.
|
||||
*/
|
||||
get onDidAddPanel(): Event<IGridviewPanel> {
|
||||
return this.component.onDidAddGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a panel is removed. May be called multiple times when moving panels.
|
||||
*/
|
||||
get onDidRemovePanel(): Event<IGridviewPanel> {
|
||||
return this.component.onDidRemoveGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the active panel changes. May be undefined if no panel is active.
|
||||
*/
|
||||
get onDidActivePanelChange(): Event<IGridviewPanel | undefined> {
|
||||
return this.component.onDidActiveGroupChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after a layout is deserialzied using the `fromJSON` method.
|
||||
*/
|
||||
get onDidLayoutFromJSON(): Event<void> {
|
||||
return this.component.onDidLayoutFromJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* All panel objects.
|
||||
*/
|
||||
get panels(): IGridviewPanel[] {
|
||||
return this.component.groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current orientation. Can be changed after initialization.
|
||||
*/
|
||||
get orientation(): Orientation {
|
||||
return this.component.orientation;
|
||||
}
|
||||
|
||||
set orientation(value: Orientation) {
|
||||
this.component.updateOptions({ orientation: value });
|
||||
}
|
||||
|
||||
constructor(private readonly component: IGridviewComponent) {}
|
||||
|
||||
/**
|
||||
* Focus the component. Will try to focus an active panel if one exists.
|
||||
*/
|
||||
focus(): void {
|
||||
this.component.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force resize the component to an exact width and height. Read about auto-resizing before using.
|
||||
*/
|
||||
layout(width: number, height: number, force = false): void {
|
||||
this.component.layout(width, height, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a panel and return the created object.
|
||||
*/
|
||||
addPanel<T extends object = Parameters>(
|
||||
options: AddComponentOptions<T>
|
||||
): IGridviewPanel {
|
||||
return this.component.addPanel(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a panel given the panel object.
|
||||
*/
|
||||
removePanel(panel: IGridviewPanel, sizing?: Sizing): void {
|
||||
this.component.removePanel(panel, sizing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a panel in a particular direction relative to another panel.
|
||||
*/
|
||||
movePanel(
|
||||
panel: IGridviewPanel,
|
||||
options: { direction: Direction; reference: string; size?: number }
|
||||
): void {
|
||||
this.component.movePanel(panel, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a panel object given a `string` id. May return `undefined`.
|
||||
*/
|
||||
getPanel(id: string): IGridviewPanel | undefined {
|
||||
return this.component.getPanel(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a component from a serialized object.
|
||||
*/
|
||||
fromJSON(data: SerializedGridviewComponent): void {
|
||||
return this.component.fromJSON(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a serialized object of the current component.
|
||||
*/
|
||||
toJSON(): SerializedGridviewComponent {
|
||||
return this.component.toJSON();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the component back to an empty and default state.
|
||||
*/
|
||||
clear(): void {
|
||||
this.component.clear();
|
||||
}
|
||||
|
||||
updateOptions(options: Partial<GridviewComponentOptions>) {
|
||||
this.component.updateOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release resources and teardown component. Do not call when using framework versions of dockview.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.component.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class DockviewApi implements CommonApi<SerializedDockview> {
|
||||
/**
|
||||
* The unique identifier for this instance. Used to manage scope of Drag'n'Drop events.
|
||||
*/
|
||||
get id(): string {
|
||||
return this.component.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the component.
|
||||
*/
|
||||
get width(): number {
|
||||
return this.component.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Height of the component.
|
||||
*/
|
||||
get height(): number {
|
||||
return this.component.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum height of the component.
|
||||
*/
|
||||
get minimumHeight(): number {
|
||||
return this.component.minimumHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum height of the component.
|
||||
*/
|
||||
get maximumHeight(): number {
|
||||
return this.component.maximumHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum width of the component.
|
||||
*/
|
||||
get minimumWidth(): number {
|
||||
return this.component.minimumWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum width of the component.
|
||||
*/
|
||||
get maximumWidth(): number {
|
||||
return this.component.maximumWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total number of groups.
|
||||
*/
|
||||
get size(): number {
|
||||
return this.component.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total number of panels.
|
||||
*/
|
||||
get totalPanels(): number {
|
||||
return this.component.totalPanels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the active group changes. May be undefined if no group is active.
|
||||
*/
|
||||
get onDidActiveGroupChange(): Event<DockviewGroupPanel | undefined> {
|
||||
return this.component.onDidActiveGroupChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a group is added. May be called multiple times when moving groups.
|
||||
*/
|
||||
get onDidAddGroup(): Event<DockviewGroupPanel> {
|
||||
return this.component.onDidAddGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a group is removed. May be called multiple times when moving groups.
|
||||
*/
|
||||
get onDidRemoveGroup(): Event<DockviewGroupPanel> {
|
||||
return this.component.onDidRemoveGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the active panel changes. May be undefined if no panel is active.
|
||||
*/
|
||||
get onDidActivePanelChange(): Event<IDockviewPanel | undefined> {
|
||||
return this.component.onDidActivePanelChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a panel is added. May be called multiple times when moving panels.
|
||||
*/
|
||||
get onDidAddPanel(): Event<IDockviewPanel> {
|
||||
return this.component.onDidAddPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a panel is removed. May be called multiple times when moving panels.
|
||||
*/
|
||||
get onDidRemovePanel(): Event<IDockviewPanel> {
|
||||
return this.component.onDidRemovePanel;
|
||||
}
|
||||
|
||||
get onDidMovePanel(): Event<MovePanelEvent> {
|
||||
return this.component.onDidMovePanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after a layout is deserialzied using the `fromJSON` method.
|
||||
*/
|
||||
get onDidLayoutFromJSON(): Event<void> {
|
||||
return this.component.onDidLayoutFromJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when any layout change occures, an aggregation of many events.
|
||||
*/
|
||||
get onDidLayoutChange(): Event<void> {
|
||||
return this.component.onDidLayoutChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a Drag'n'Drop event occurs that the component was unable to handle. Exposed for custom Drag'n'Drop functionality.
|
||||
*/
|
||||
get onDidDrop(): Event<DockviewDidDropEvent> {
|
||||
return this.component.onDidDrop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a Drag'n'Drop event occurs but before dockview handles it giving the user an opportunity to intecept and
|
||||
* prevent the event from occuring using the standard `preventDefault()` syntax.
|
||||
*
|
||||
* Preventing certain events may causes unexpected behaviours, use carefully.
|
||||
*/
|
||||
get onWillDrop(): Event<DockviewWillDropEvent> {
|
||||
return this.component.onWillDrop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked before an overlay is shown indicating a drop target.
|
||||
*
|
||||
* Calling `event.preventDefault()` will prevent the overlay being shown and prevent
|
||||
* the any subsequent drop event.
|
||||
*/
|
||||
get onWillShowOverlay(): Event<WillShowOverlayLocationEvent> {
|
||||
return this.component.onWillShowOverlay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked before a group is dragged.
|
||||
*
|
||||
* Calling `event.nativeEvent.preventDefault()` will prevent the group drag starting.
|
||||
*
|
||||
*/
|
||||
get onWillDragGroup(): Event<GroupDragEvent> {
|
||||
return this.component.onWillDragGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked before a panel is dragged.
|
||||
*
|
||||
* Calling `event.nativeEvent.preventDefault()` will prevent the panel drag starting.
|
||||
*/
|
||||
get onWillDragPanel(): Event<TabDragEvent> {
|
||||
return this.component.onWillDragPanel;
|
||||
}
|
||||
|
||||
get onUnhandledDragOverEvent(): Event<DockviewDndOverlayEvent> {
|
||||
return this.component.onUnhandledDragOverEvent;
|
||||
}
|
||||
|
||||
get onDidPopoutGroupSizeChange(): Event<PopoutGroupChangeSizeEvent> {
|
||||
return this.component.onDidPopoutGroupSizeChange;
|
||||
}
|
||||
|
||||
get onDidPopoutGroupPositionChange(): Event<PopoutGroupChangePositionEvent> {
|
||||
return this.component.onDidPopoutGroupPositionChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* All panel objects.
|
||||
*/
|
||||
get panels(): IDockviewPanel[] {
|
||||
return this.component.panels;
|
||||
}
|
||||
|
||||
/**
|
||||
* All group objects.
|
||||
*/
|
||||
get groups(): DockviewGroupPanel[] {
|
||||
return this.component.groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active panel object.
|
||||
*/
|
||||
get activePanel(): IDockviewPanel | undefined {
|
||||
return this.component.activePanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active group object.
|
||||
*/
|
||||
get activeGroup(): DockviewGroupPanel | undefined {
|
||||
return this.component.activeGroup;
|
||||
}
|
||||
|
||||
constructor(private readonly component: IDockviewComponent) {}
|
||||
|
||||
/**
|
||||
* Focus the component. Will try to focus an active panel if one exists.
|
||||
*/
|
||||
focus(): void {
|
||||
this.component.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a panel object given a `string` id. May return `undefined`.
|
||||
*/
|
||||
getPanel(id: string): IDockviewPanel | undefined {
|
||||
return this.component.getGroupPanel(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force resize the component to an exact width and height. Read about auto-resizing before using.
|
||||
*/
|
||||
layout(width: number, height: number, force = false): void {
|
||||
this.component.layout(width, height, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a panel and return the created object.
|
||||
*/
|
||||
addPanel<T extends object = Parameters>(
|
||||
options: AddPanelOptions<T>
|
||||
): IDockviewPanel {
|
||||
return this.component.addPanel(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a panel given the panel object.
|
||||
*/
|
||||
removePanel(panel: IDockviewPanel): void {
|
||||
this.component.removePanel(panel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a group and return the created object.
|
||||
*/
|
||||
addGroup(options?: AddGroupOptions): DockviewGroupPanel {
|
||||
return this.component.addGroup(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all groups and panels.
|
||||
*/
|
||||
closeAllGroups(): void {
|
||||
return this.component.closeAllGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a group and any panels within the group.
|
||||
*/
|
||||
removeGroup(group: IDockviewGroupPanel): void {
|
||||
this.component.removeGroup(<DockviewGroupPanel>group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a group object given a `string` id. May return undefined.
|
||||
*/
|
||||
getGroup(id: string): DockviewGroupPanel | undefined {
|
||||
return this.component.getPanel(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a floating group
|
||||
*/
|
||||
addFloatingGroup(
|
||||
item: IDockviewPanel | DockviewGroupPanel,
|
||||
options?: FloatingGroupOptions
|
||||
): void {
|
||||
return this.component.addFloatingGroup(item, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a component from a serialized object.
|
||||
*/
|
||||
fromJSON(data: SerializedDockview): void {
|
||||
this.component.fromJSON(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a serialized object of the current component.
|
||||
*/
|
||||
toJSON(): SerializedDockview {
|
||||
return this.component.toJSON();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the component back to an empty and default state.
|
||||
*/
|
||||
clear(): void {
|
||||
this.component.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the focus progmatically to the next panel or group.
|
||||
*/
|
||||
moveToNext(options?: MovementOptions): void {
|
||||
this.component.moveToNext(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the focus progmatically to the previous panel or group.
|
||||
*/
|
||||
moveToPrevious(options?: MovementOptions): void {
|
||||
this.component.moveToPrevious(options);
|
||||
}
|
||||
|
||||
maximizeGroup(panel: IDockviewPanel): void {
|
||||
this.component.maximizeGroup(panel.group);
|
||||
}
|
||||
|
||||
hasMaximizedGroup(): boolean {
|
||||
return this.component.hasMaximizedGroup();
|
||||
}
|
||||
|
||||
exitMaximizedGroup(): void {
|
||||
this.component.exitMaximizedGroup();
|
||||
}
|
||||
|
||||
get onDidMaximizedGroupChange(): Event<DockviewMaximizedGroupChanged> {
|
||||
return this.component.onDidMaximizedGroupChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a popout group in a new Window
|
||||
*/
|
||||
addPopoutGroup(
|
||||
item: IDockviewPanel | DockviewGroupPanel,
|
||||
options?: {
|
||||
position?: Box;
|
||||
popoutUrl?: string;
|
||||
onDidOpen?: (event: { id: string; window: Window }) => void;
|
||||
onWillClose?: (event: { id: string; window: Window }) => void;
|
||||
}
|
||||
): Promise<boolean> {
|
||||
return this.component.addPopoutGroup(item, options);
|
||||
}
|
||||
|
||||
updateOptions(options: Partial<DockviewComponentOptions>) {
|
||||
this.component.updateOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release resources and teardown component. Do not call when using framework versions of dockview.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.component.dispose();
|
||||
}
|
||||
}
|
140
packages/dockview-core/src/api/dockviewGroupPanelApi.ts
Normal file
140
packages/dockview-core/src/api/dockviewGroupPanelApi.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { Position, positionToDirection } from '../dnd/droptarget';
|
||||
import { DockviewComponent } from '../dockview/dockviewComponent';
|
||||
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
|
||||
import {
|
||||
DockviewGroupChangeEvent,
|
||||
DockviewGroupLocation,
|
||||
} from '../dockview/dockviewGroupPanelModel';
|
||||
import { Emitter, Event } from '../events';
|
||||
import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi';
|
||||
|
||||
export interface DockviewGroupMoveParams {
|
||||
group?: DockviewGroupPanel;
|
||||
position?: Position;
|
||||
/**
|
||||
* The index to place the panel within a group, only applicable if the placement is within an existing group
|
||||
*/
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export interface DockviewGroupPanelApi extends GridviewPanelApi {
|
||||
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent>;
|
||||
readonly onDidActivePanelChange: Event<DockviewGroupChangeEvent>;
|
||||
readonly location: DockviewGroupLocation;
|
||||
/**
|
||||
* If you require the Window object
|
||||
*/
|
||||
getWindow(): Window;
|
||||
moveTo(options: DockviewGroupMoveParams): void;
|
||||
maximize(): void;
|
||||
isMaximized(): boolean;
|
||||
exitMaximized(): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export interface DockviewGroupPanelFloatingChangeEvent {
|
||||
readonly location: DockviewGroupLocation;
|
||||
}
|
||||
|
||||
const NOT_INITIALIZED_MESSAGE =
|
||||
'dockview: DockviewGroupPanelApiImpl not initialized';
|
||||
|
||||
export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
|
||||
private _group: DockviewGroupPanel | undefined;
|
||||
|
||||
readonly _onDidLocationChange =
|
||||
new Emitter<DockviewGroupPanelFloatingChangeEvent>();
|
||||
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent> =
|
||||
this._onDidLocationChange.event;
|
||||
|
||||
readonly _onDidActivePanelChange = new Emitter<DockviewGroupChangeEvent>();
|
||||
readonly onDidActivePanelChange = this._onDidActivePanelChange.event;
|
||||
|
||||
get location(): DockviewGroupLocation {
|
||||
if (!this._group) {
|
||||
throw new Error(NOT_INITIALIZED_MESSAGE);
|
||||
}
|
||||
return this._group.model.location;
|
||||
}
|
||||
|
||||
constructor(id: string, private readonly accessor: DockviewComponent) {
|
||||
super(id, '__dockviewgroup__');
|
||||
|
||||
this.addDisposables(
|
||||
this._onDidLocationChange,
|
||||
this._onDidActivePanelChange
|
||||
);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (!this._group) {
|
||||
return;
|
||||
}
|
||||
return this.accessor.removeGroup(this._group);
|
||||
}
|
||||
|
||||
getWindow(): Window {
|
||||
return this.location.type === 'popout'
|
||||
? this.location.getWindow()
|
||||
: window;
|
||||
}
|
||||
|
||||
moveTo(options: DockviewGroupMoveParams): void {
|
||||
if (!this._group) {
|
||||
throw new Error(NOT_INITIALIZED_MESSAGE);
|
||||
}
|
||||
|
||||
const group =
|
||||
options.group ??
|
||||
this.accessor.addGroup({
|
||||
direction: positionToDirection(options.position ?? 'right'),
|
||||
skipSetActive: true,
|
||||
});
|
||||
|
||||
this.accessor.moveGroupOrPanel({
|
||||
from: { groupId: this._group.id },
|
||||
to: {
|
||||
group,
|
||||
position: options.group
|
||||
? options.position ?? 'center'
|
||||
: 'center',
|
||||
index: options.index,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
maximize(): void {
|
||||
if (!this._group) {
|
||||
throw new Error(NOT_INITIALIZED_MESSAGE);
|
||||
}
|
||||
|
||||
if (this.location.type !== 'grid') {
|
||||
// only grid groups can be maximized
|
||||
return;
|
||||
}
|
||||
|
||||
this.accessor.maximizeGroup(this._group);
|
||||
}
|
||||
|
||||
isMaximized(): boolean {
|
||||
if (!this._group) {
|
||||
throw new Error(NOT_INITIALIZED_MESSAGE);
|
||||
}
|
||||
|
||||
return this.accessor.isMaximizedGroup(this._group);
|
||||
}
|
||||
|
||||
exitMaximized(): void {
|
||||
if (!this._group) {
|
||||
throw new Error(NOT_INITIALIZED_MESSAGE);
|
||||
}
|
||||
|
||||
if (this.isMaximized()) {
|
||||
this.accessor.exitMaximizedGroup();
|
||||
}
|
||||
}
|
||||
|
||||
initialize(group: DockviewGroupPanel): void {
|
||||
this._group = group;
|
||||
}
|
||||
}
|
236
packages/dockview-core/src/api/dockviewPanelApi.ts
Normal file
236
packages/dockview-core/src/api/dockviewPanelApi.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import { Emitter, Event } from '../events';
|
||||
import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi';
|
||||
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
|
||||
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
|
||||
import { DockviewPanel } from '../dockview/dockviewPanel';
|
||||
import { DockviewComponent } from '../dockview/dockviewComponent';
|
||||
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
|
||||
import {
|
||||
DockviewGroupMoveParams,
|
||||
DockviewGroupPanelFloatingChangeEvent,
|
||||
} from './dockviewGroupPanelApi';
|
||||
import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel';
|
||||
|
||||
export interface TitleEvent {
|
||||
readonly title: string;
|
||||
}
|
||||
|
||||
export interface RendererChangedEvent {
|
||||
readonly renderer: DockviewPanelRenderer;
|
||||
}
|
||||
|
||||
export interface ActiveGroupEvent {
|
||||
readonly isActive: boolean;
|
||||
}
|
||||
|
||||
export interface GroupChangedEvent {
|
||||
// empty
|
||||
}
|
||||
|
||||
export type DockviewPanelMoveParams = DockviewGroupMoveParams;
|
||||
|
||||
export interface DockviewPanelApi
|
||||
extends Omit<
|
||||
GridviewPanelApi,
|
||||
// omit properties that do not make sense here
|
||||
'setVisible' | 'onDidConstraintsChange' | 'setConstraints'
|
||||
> {
|
||||
/**
|
||||
* The id of the tab component renderer
|
||||
*
|
||||
* Undefined if no custom tab renderer is provided
|
||||
*/
|
||||
readonly tabComponent: string | undefined;
|
||||
readonly group: DockviewGroupPanel;
|
||||
readonly isGroupActive: boolean;
|
||||
readonly renderer: DockviewPanelRenderer;
|
||||
readonly title: string | undefined;
|
||||
readonly onDidActiveGroupChange: Event<ActiveGroupEvent>;
|
||||
readonly onDidGroupChange: Event<GroupChangedEvent>;
|
||||
readonly onDidTitleChange: Event<TitleEvent>;
|
||||
readonly onDidRendererChange: Event<RendererChangedEvent>;
|
||||
readonly location: DockviewGroupLocation;
|
||||
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent>;
|
||||
close(): void;
|
||||
setTitle(title: string): void;
|
||||
setRenderer(renderer: DockviewPanelRenderer): void;
|
||||
moveTo(options: DockviewPanelMoveParams): void;
|
||||
maximize(): void;
|
||||
isMaximized(): boolean;
|
||||
exitMaximized(): void;
|
||||
/**
|
||||
* If you require the Window object
|
||||
*/
|
||||
getWindow(): Window;
|
||||
}
|
||||
|
||||
export class DockviewPanelApiImpl
|
||||
extends GridviewPanelApiImpl
|
||||
implements DockviewPanelApi
|
||||
{
|
||||
private _group: DockviewGroupPanel;
|
||||
private readonly _tabComponent: string | undefined;
|
||||
|
||||
readonly _onDidTitleChange = new Emitter<TitleEvent>();
|
||||
readonly onDidTitleChange = this._onDidTitleChange.event;
|
||||
|
||||
private readonly _onDidActiveGroupChange = new Emitter<ActiveGroupEvent>();
|
||||
readonly onDidActiveGroupChange = this._onDidActiveGroupChange.event;
|
||||
|
||||
private readonly _onDidGroupChange = new Emitter<GroupChangedEvent>();
|
||||
readonly onDidGroupChange = this._onDidGroupChange.event;
|
||||
|
||||
readonly _onDidRendererChange = new Emitter<RendererChangedEvent>();
|
||||
readonly onDidRendererChange = this._onDidRendererChange.event;
|
||||
|
||||
private readonly _onDidLocationChange =
|
||||
new Emitter<DockviewGroupPanelFloatingChangeEvent>();
|
||||
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent> =
|
||||
this._onDidLocationChange.event;
|
||||
|
||||
private readonly groupEventsDisposable = new MutableDisposable();
|
||||
|
||||
get location(): DockviewGroupLocation {
|
||||
return this.group.api.location;
|
||||
}
|
||||
|
||||
get title(): string | undefined {
|
||||
return this.panel.title;
|
||||
}
|
||||
|
||||
get isGroupActive(): boolean {
|
||||
return this.group.isActive;
|
||||
}
|
||||
|
||||
get renderer(): DockviewPanelRenderer {
|
||||
return this.panel.renderer;
|
||||
}
|
||||
|
||||
set group(value: DockviewGroupPanel) {
|
||||
const oldGroup = this._group;
|
||||
|
||||
if (this._group !== value) {
|
||||
this._group = value;
|
||||
|
||||
this._onDidGroupChange.fire({});
|
||||
|
||||
this.setupGroupEventListeners(oldGroup);
|
||||
|
||||
this._onDidLocationChange.fire({
|
||||
location: this.group.api.location,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get group(): DockviewGroupPanel {
|
||||
return this._group;
|
||||
}
|
||||
|
||||
get tabComponent(): string | undefined {
|
||||
return this._tabComponent;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly panel: DockviewPanel,
|
||||
group: DockviewGroupPanel,
|
||||
private readonly accessor: DockviewComponent,
|
||||
component: string,
|
||||
tabComponent?: string
|
||||
) {
|
||||
super(panel.id, component);
|
||||
|
||||
this._tabComponent = tabComponent;
|
||||
|
||||
this.initialize(panel);
|
||||
|
||||
this._group = group;
|
||||
this.setupGroupEventListeners();
|
||||
|
||||
this.addDisposables(
|
||||
this.groupEventsDisposable,
|
||||
this._onDidRendererChange,
|
||||
this._onDidTitleChange,
|
||||
this._onDidGroupChange,
|
||||
this._onDidActiveGroupChange,
|
||||
this._onDidLocationChange
|
||||
);
|
||||
}
|
||||
|
||||
getWindow(): Window {
|
||||
return this.group.api.getWindow();
|
||||
}
|
||||
|
||||
moveTo(options: DockviewPanelMoveParams): void {
|
||||
this.accessor.moveGroupOrPanel({
|
||||
from: { groupId: this._group.id, panelId: this.panel.id },
|
||||
to: {
|
||||
group: options.group ?? this._group,
|
||||
position: options.group
|
||||
? options.position ?? 'center'
|
||||
: 'center',
|
||||
index: options.index,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setTitle(title: string): void {
|
||||
this.panel.setTitle(title);
|
||||
}
|
||||
|
||||
setRenderer(renderer: DockviewPanelRenderer): void {
|
||||
this.panel.setRenderer(renderer);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.group.model.closePanel(this.panel);
|
||||
}
|
||||
|
||||
maximize(): void {
|
||||
this.group.api.maximize();
|
||||
}
|
||||
|
||||
isMaximized(): boolean {
|
||||
return this.group.api.isMaximized();
|
||||
}
|
||||
|
||||
exitMaximized(): void {
|
||||
this.group.api.exitMaximized();
|
||||
}
|
||||
|
||||
private setupGroupEventListeners(previousGroup?: DockviewGroupPanel) {
|
||||
let _trackGroupActive = previousGroup?.isActive ?? false; // prevent duplicate events with same state
|
||||
|
||||
this.groupEventsDisposable.value = new CompositeDisposable(
|
||||
this.group.api.onDidVisibilityChange((event) => {
|
||||
const hasBecomeHidden = !event.isVisible && this.isVisible;
|
||||
const hasBecomeVisible = event.isVisible && !this.isVisible;
|
||||
|
||||
const isActivePanel = this.group.model.isPanelActive(
|
||||
this.panel
|
||||
);
|
||||
|
||||
if (hasBecomeHidden || (hasBecomeVisible && isActivePanel)) {
|
||||
this._onDidVisibilityChange.fire(event);
|
||||
}
|
||||
}),
|
||||
this.group.api.onDidLocationChange((event) => {
|
||||
if (this.group !== this.panel.group) {
|
||||
return;
|
||||
}
|
||||
this._onDidLocationChange.fire(event);
|
||||
}),
|
||||
this.group.api.onDidActiveChange(() => {
|
||||
if (this.group !== this.panel.group) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_trackGroupActive !== this.isGroupActive) {
|
||||
_trackGroupActive = this.isGroupActive;
|
||||
this._onDidActiveGroupChange.fire({
|
||||
isActive: this.isGroupActive,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
46
packages/dockview-core/src/api/entryPoints.ts
Normal file
46
packages/dockview-core/src/api/entryPoints.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import {
|
||||
DockviewApi,
|
||||
GridviewApi,
|
||||
PaneviewApi,
|
||||
SplitviewApi,
|
||||
} from '../api/component.api';
|
||||
import { DockviewComponent } from '../dockview/dockviewComponent';
|
||||
import { DockviewComponentOptions } from '../dockview/options';
|
||||
import { GridviewComponent } from '../gridview/gridviewComponent';
|
||||
import { GridviewComponentOptions } from '../gridview/options';
|
||||
import { PaneviewComponentOptions } from '../paneview/options';
|
||||
import { PaneviewComponent } from '../paneview/paneviewComponent';
|
||||
import { SplitviewComponentOptions } from '../splitview/options';
|
||||
import { SplitviewComponent } from '../splitview/splitviewComponent';
|
||||
|
||||
export function createDockview(
|
||||
element: HTMLElement,
|
||||
options: DockviewComponentOptions
|
||||
): DockviewApi {
|
||||
const component = new DockviewComponent(element, options);
|
||||
return component.api;
|
||||
}
|
||||
|
||||
export function createSplitview(
|
||||
element: HTMLElement,
|
||||
options: SplitviewComponentOptions
|
||||
): SplitviewApi {
|
||||
const component = new SplitviewComponent(element, options);
|
||||
return new SplitviewApi(component);
|
||||
}
|
||||
|
||||
export function createGridview(
|
||||
element: HTMLElement,
|
||||
options: GridviewComponentOptions
|
||||
): GridviewApi {
|
||||
const component = new GridviewComponent(element, options);
|
||||
return new GridviewApi(component);
|
||||
}
|
||||
|
||||
export function createPaneview(
|
||||
element: HTMLElement,
|
||||
options: PaneviewComponentOptions
|
||||
): PaneviewApi {
|
||||
const component = new PaneviewComponent(element, options);
|
||||
return new PaneviewApi(component);
|
||||
}
|
68
packages/dockview-core/src/api/gridviewPanelApi.ts
Normal file
68
packages/dockview-core/src/api/gridviewPanelApi.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Emitter, Event } from '../events';
|
||||
import { IPanel } from '../panel/types';
|
||||
import { FunctionOrValue } from '../types';
|
||||
import { PanelApiImpl, PanelApi } from './panelApi';
|
||||
|
||||
export interface GridConstraintChangeEvent {
|
||||
readonly minimumWidth?: number;
|
||||
readonly minimumHeight?: number;
|
||||
readonly maximumWidth?: number;
|
||||
readonly maximumHeight?: number;
|
||||
}
|
||||
|
||||
interface GridConstraintChangeEvent2 {
|
||||
readonly minimumWidth?: FunctionOrValue<number>;
|
||||
readonly minimumHeight?: FunctionOrValue<number>;
|
||||
readonly maximumWidth?: FunctionOrValue<number>;
|
||||
readonly maximumHeight?: FunctionOrValue<number>;
|
||||
}
|
||||
|
||||
export interface SizeEvent {
|
||||
readonly width?: number;
|
||||
readonly height?: number;
|
||||
}
|
||||
|
||||
export interface GridviewPanelApi extends PanelApi {
|
||||
readonly onDidConstraintsChange: Event<GridConstraintChangeEvent>;
|
||||
setConstraints(value: GridConstraintChangeEvent2): void;
|
||||
setSize(event: SizeEvent): void;
|
||||
}
|
||||
|
||||
export class GridviewPanelApiImpl
|
||||
extends PanelApiImpl
|
||||
implements GridviewPanelApi
|
||||
{
|
||||
private readonly _onDidConstraintsChangeInternal =
|
||||
new Emitter<GridConstraintChangeEvent2>();
|
||||
readonly onDidConstraintsChangeInternal: Event<GridConstraintChangeEvent2> =
|
||||
this._onDidConstraintsChangeInternal.event;
|
||||
|
||||
readonly _onDidConstraintsChange = new Emitter<GridConstraintChangeEvent>();
|
||||
readonly onDidConstraintsChange: Event<GridConstraintChangeEvent> =
|
||||
this._onDidConstraintsChange.event;
|
||||
|
||||
private readonly _onDidSizeChange = new Emitter<SizeEvent>();
|
||||
readonly onDidSizeChange: Event<SizeEvent> = this._onDidSizeChange.event;
|
||||
|
||||
constructor(id: string, component: string, panel?: IPanel) {
|
||||
super(id, component);
|
||||
|
||||
this.addDisposables(
|
||||
this._onDidConstraintsChangeInternal,
|
||||
this._onDidConstraintsChange,
|
||||
this._onDidSizeChange
|
||||
);
|
||||
|
||||
if (panel) {
|
||||
this.initialize(panel);
|
||||
}
|
||||
}
|
||||
|
||||
public setConstraints(value: GridConstraintChangeEvent): void {
|
||||
this._onDidConstraintsChangeInternal.fire(value);
|
||||
}
|
||||
|
||||
public setSize(event: SizeEvent): void {
|
||||
this._onDidSizeChange.fire(event);
|
||||
}
|
||||
}
|
188
packages/dockview-core/src/api/panelApi.ts
Normal file
188
packages/dockview-core/src/api/panelApi.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import { DockviewEvent, Emitter, Event } from '../events';
|
||||
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
|
||||
import { IPanel, Parameters } from '../panel/types';
|
||||
|
||||
export interface FocusEvent {
|
||||
readonly isFocused: boolean;
|
||||
}
|
||||
export interface PanelDimensionChangeEvent {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
}
|
||||
|
||||
export interface VisibilityEvent {
|
||||
readonly isVisible: boolean;
|
||||
}
|
||||
|
||||
export interface ActiveEvent {
|
||||
readonly isActive: boolean;
|
||||
}
|
||||
|
||||
export interface PanelApi {
|
||||
// events
|
||||
readonly onDidDimensionsChange: Event<PanelDimensionChangeEvent>;
|
||||
readonly onDidFocusChange: Event<FocusEvent>;
|
||||
readonly onDidVisibilityChange: Event<VisibilityEvent>;
|
||||
readonly onDidActiveChange: Event<ActiveEvent>;
|
||||
readonly onDidParametersChange: Event<Parameters>;
|
||||
setActive(): void;
|
||||
setVisible(isVisible: boolean): void;
|
||||
updateParameters(parameters: Parameters): void;
|
||||
/**
|
||||
* The id of the component renderer
|
||||
*/
|
||||
readonly component: string;
|
||||
/**
|
||||
* The id of the panel that would have been assigned when the panel was created
|
||||
*/
|
||||
readonly id: string;
|
||||
/**
|
||||
* Whether the panel holds the current focus
|
||||
*/
|
||||
readonly isFocused: boolean;
|
||||
/**
|
||||
* Whether the panel is the actively selected panel
|
||||
*/
|
||||
readonly isActive: boolean;
|
||||
/**
|
||||
* Whether the panel is visible
|
||||
*/
|
||||
readonly isVisible: boolean;
|
||||
/**
|
||||
* The panel width in pixels
|
||||
*/
|
||||
readonly width: number;
|
||||
/**
|
||||
* The panel height in pixels
|
||||
*/
|
||||
readonly height: number;
|
||||
|
||||
readonly onWillFocus: Event<WillFocusEvent>;
|
||||
|
||||
getParameters<T extends Parameters = Parameters>(): T;
|
||||
}
|
||||
|
||||
export class WillFocusEvent extends DockviewEvent {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A core api implementation that should be used across all panel-like objects
|
||||
*/
|
||||
export class PanelApiImpl extends CompositeDisposable implements PanelApi {
|
||||
private _isFocused = false;
|
||||
private _isActive = false;
|
||||
private _isVisible = true;
|
||||
private _width = 0;
|
||||
private _height = 0;
|
||||
private _parameters: Parameters = {};
|
||||
|
||||
private readonly panelUpdatesDisposable = new MutableDisposable();
|
||||
|
||||
readonly _onDidDimensionChange = new Emitter<PanelDimensionChangeEvent>();
|
||||
readonly onDidDimensionsChange = this._onDidDimensionChange.event;
|
||||
|
||||
readonly _onDidChangeFocus = new Emitter<FocusEvent>();
|
||||
readonly onDidFocusChange: Event<FocusEvent> = this._onDidChangeFocus.event;
|
||||
//
|
||||
readonly _onWillFocus = new Emitter<WillFocusEvent>();
|
||||
readonly onWillFocus: Event<WillFocusEvent> = this._onWillFocus.event;
|
||||
//
|
||||
readonly _onDidVisibilityChange = new Emitter<VisibilityEvent>();
|
||||
readonly onDidVisibilityChange: Event<VisibilityEvent> =
|
||||
this._onDidVisibilityChange.event;
|
||||
|
||||
readonly _onWillVisibilityChange = new Emitter<VisibilityEvent>();
|
||||
readonly onWillVisibilityChange: Event<VisibilityEvent> =
|
||||
this._onWillVisibilityChange.event;
|
||||
|
||||
readonly _onDidActiveChange = new Emitter<ActiveEvent>();
|
||||
readonly onDidActiveChange: Event<ActiveEvent> =
|
||||
this._onDidActiveChange.event;
|
||||
|
||||
readonly _onActiveChange = new Emitter<void>();
|
||||
readonly onActiveChange: Event<void> = this._onActiveChange.event;
|
||||
|
||||
readonly _onDidParametersChange = new Emitter<Parameters>();
|
||||
readonly onDidParametersChange: Event<Parameters> =
|
||||
this._onDidParametersChange.event;
|
||||
|
||||
get isFocused(): boolean {
|
||||
return this._isFocused;
|
||||
}
|
||||
|
||||
get isActive(): boolean {
|
||||
return this._isActive;
|
||||
}
|
||||
|
||||
get isVisible(): boolean {
|
||||
return this._isVisible;
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
constructor(readonly id: string, readonly component: string) {
|
||||
super();
|
||||
|
||||
this.addDisposables(
|
||||
this.onDidFocusChange((event) => {
|
||||
this._isFocused = event.isFocused;
|
||||
}),
|
||||
this.onDidActiveChange((event) => {
|
||||
this._isActive = event.isActive;
|
||||
}),
|
||||
this.onDidVisibilityChange((event) => {
|
||||
this._isVisible = event.isVisible;
|
||||
}),
|
||||
this.onDidDimensionsChange((event) => {
|
||||
this._width = event.width;
|
||||
this._height = event.height;
|
||||
}),
|
||||
this.panelUpdatesDisposable,
|
||||
this._onDidDimensionChange,
|
||||
this._onDidChangeFocus,
|
||||
this._onDidVisibilityChange,
|
||||
this._onDidActiveChange,
|
||||
this._onWillFocus,
|
||||
this._onActiveChange,
|
||||
this._onWillFocus,
|
||||
this._onWillVisibilityChange,
|
||||
this._onDidParametersChange
|
||||
);
|
||||
}
|
||||
|
||||
getParameters<T extends Parameters = Parameters>(): T {
|
||||
return this._parameters as T;
|
||||
}
|
||||
|
||||
public initialize(panel: IPanel): void {
|
||||
this.panelUpdatesDisposable.value = this._onDidParametersChange.event(
|
||||
(parameters) => {
|
||||
this._parameters = parameters;
|
||||
panel.update({
|
||||
params: parameters,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setVisible(isVisible: boolean): void {
|
||||
this._onWillVisibilityChange.fire({ isVisible });
|
||||
}
|
||||
|
||||
setActive(): void {
|
||||
this._onActiveChange.fire();
|
||||
}
|
||||
|
||||
updateParameters(parameters: Parameters): void {
|
||||
this._onDidParametersChange.fire(parameters);
|
||||
}
|
||||
}
|
55
packages/dockview-core/src/api/paneviewPanelApi.ts
Normal file
55
packages/dockview-core/src/api/paneviewPanelApi.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Emitter, Event } from '../events';
|
||||
import { PaneviewPanel } from '../paneview/paneviewPanel';
|
||||
import { SplitviewPanelApi, SplitviewPanelApiImpl } from './splitviewPanelApi';
|
||||
|
||||
export interface ExpansionEvent {
|
||||
readonly isExpanded: boolean;
|
||||
}
|
||||
|
||||
export interface PaneviewPanelApi extends SplitviewPanelApi {
|
||||
readonly isExpanded: boolean;
|
||||
readonly onDidExpansionChange: Event<ExpansionEvent>;
|
||||
readonly onMouseEnter: Event<MouseEvent>;
|
||||
readonly onMouseLeave: Event<MouseEvent>;
|
||||
setExpanded(isExpanded: boolean): void;
|
||||
}
|
||||
|
||||
export class PaneviewPanelApiImpl
|
||||
extends SplitviewPanelApiImpl
|
||||
implements PaneviewPanelApi
|
||||
{
|
||||
readonly _onDidExpansionChange = new Emitter<ExpansionEvent>({
|
||||
replay: true,
|
||||
});
|
||||
readonly onDidExpansionChange: Event<ExpansionEvent> =
|
||||
this._onDidExpansionChange.event;
|
||||
|
||||
readonly _onMouseEnter = new Emitter<MouseEvent>({});
|
||||
readonly onMouseEnter: Event<MouseEvent> = this._onMouseEnter.event;
|
||||
readonly _onMouseLeave = new Emitter<MouseEvent>({});
|
||||
readonly onMouseLeave: Event<MouseEvent> = this._onMouseLeave.event;
|
||||
|
||||
private _pane: PaneviewPanel | undefined;
|
||||
|
||||
set pane(pane: PaneviewPanel) {
|
||||
this._pane = pane;
|
||||
}
|
||||
|
||||
constructor(id: string, component: string) {
|
||||
super(id, component);
|
||||
|
||||
this.addDisposables(
|
||||
this._onDidExpansionChange,
|
||||
this._onMouseEnter,
|
||||
this._onMouseLeave
|
||||
);
|
||||
}
|
||||
|
||||
setExpanded(isExpanded: boolean): void {
|
||||
this._pane?.setExpanded(isExpanded);
|
||||
}
|
||||
|
||||
get isExpanded(): boolean {
|
||||
return !!this._pane?.isExpanded();
|
||||
}
|
||||
}
|
65
packages/dockview-core/src/api/splitviewPanelApi.ts
Normal file
65
packages/dockview-core/src/api/splitviewPanelApi.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { Emitter, Event } from '../events';
|
||||
import { IDisposable } from '../lifecycle';
|
||||
import { FunctionOrValue } from '../types';
|
||||
import { PanelApiImpl, PanelApi } from './panelApi';
|
||||
|
||||
interface PanelConstraintChangeEvent2 {
|
||||
readonly minimumSize?: FunctionOrValue<number>;
|
||||
readonly maximumSize?: FunctionOrValue<number>;
|
||||
}
|
||||
|
||||
export interface PanelConstraintChangeEvent {
|
||||
readonly minimumSize?: number;
|
||||
readonly maximumSize?: number;
|
||||
}
|
||||
|
||||
export interface PanelSizeEvent {
|
||||
readonly size: number;
|
||||
}
|
||||
|
||||
export interface SplitviewPanelApi extends PanelApi {
|
||||
readonly onDidConstraintsChange: Event<PanelConstraintChangeEvent>;
|
||||
setConstraints(value: PanelConstraintChangeEvent2): void;
|
||||
setSize(event: PanelSizeEvent): void;
|
||||
}
|
||||
|
||||
export class SplitviewPanelApiImpl
|
||||
extends PanelApiImpl
|
||||
implements SplitviewPanelApi, IDisposable
|
||||
{
|
||||
readonly _onDidConstraintsChangeInternal =
|
||||
new Emitter<PanelConstraintChangeEvent2>();
|
||||
readonly onDidConstraintsChangeInternal: Event<PanelConstraintChangeEvent2> =
|
||||
this._onDidConstraintsChangeInternal.event;
|
||||
//
|
||||
|
||||
readonly _onDidConstraintsChange = new Emitter<PanelConstraintChangeEvent>({
|
||||
replay: true,
|
||||
});
|
||||
readonly onDidConstraintsChange: Event<PanelConstraintChangeEvent> =
|
||||
this._onDidConstraintsChange.event;
|
||||
//
|
||||
|
||||
readonly _onDidSizeChange = new Emitter<PanelSizeEvent>();
|
||||
readonly onDidSizeChange: Event<PanelSizeEvent> =
|
||||
this._onDidSizeChange.event;
|
||||
//
|
||||
|
||||
constructor(id: string, component: string) {
|
||||
super(id, component);
|
||||
|
||||
this.addDisposables(
|
||||
this._onDidConstraintsChangeInternal,
|
||||
this._onDidConstraintsChange,
|
||||
this._onDidSizeChange
|
||||
);
|
||||
}
|
||||
|
||||
setConstraints(value: PanelConstraintChangeEvent2) {
|
||||
this._onDidConstraintsChangeInternal.fire(value);
|
||||
}
|
||||
|
||||
setSize(event: PanelSizeEvent) {
|
||||
this._onDidSizeChange.fire(event);
|
||||
}
|
||||
}
|
@ -47,27 +47,6 @@ export function pushToEnd<T>(arr: T[], value: T): void {
|
||||
}
|
||||
}
|
||||
|
||||
export const range = (from: number, to?: number): number[] => {
|
||||
const result: number[] = [];
|
||||
|
||||
if (typeof to !== 'number') {
|
||||
to = from;
|
||||
from = 0;
|
||||
}
|
||||
|
||||
if (from <= to) {
|
||||
for (let i = from; i < to; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
} else {
|
||||
for (let i = from; i > to; i--) {
|
||||
result.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export function firstIndex<T>(
|
||||
array: T[] | ReadonlyArray<T>,
|
||||
fn: (item: T) => boolean
|
||||
@ -82,3 +61,13 @@ export function firstIndex<T>(
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function remove<T>(array: T[], value: T): boolean {
|
||||
const index = array.findIndex((t) => t === value);
|
||||
|
||||
if (index > -1) {
|
||||
array.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
3
packages/dockview-core/src/constants.ts
Normal file
3
packages/dockview-core/src/constants.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;
|
||||
|
||||
export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100, width: 300, height: 300 };
|
84
packages/dockview-core/src/dnd/abstractDragHandler.ts
Normal file
84
packages/dockview-core/src/dnd/abstractDragHandler.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { disableIframePointEvents } from '../dom';
|
||||
import { addDisposableListener, Emitter } from '../events';
|
||||
import {
|
||||
CompositeDisposable,
|
||||
IDisposable,
|
||||
MutableDisposable,
|
||||
} from '../lifecycle';
|
||||
|
||||
export abstract class DragHandler extends CompositeDisposable {
|
||||
private readonly dataDisposable = new MutableDisposable();
|
||||
private readonly pointerEventsDisposable = new MutableDisposable();
|
||||
|
||||
private readonly _onDragStart = new Emitter<DragEvent>();
|
||||
readonly onDragStart = this._onDragStart.event;
|
||||
|
||||
constructor(protected readonly el: HTMLElement) {
|
||||
super();
|
||||
|
||||
this.addDisposables(
|
||||
this._onDragStart,
|
||||
this.dataDisposable,
|
||||
this.pointerEventsDisposable
|
||||
);
|
||||
|
||||
this.configure();
|
||||
}
|
||||
|
||||
abstract getData(event: DragEvent): IDisposable;
|
||||
|
||||
protected isCancelled(_event: DragEvent): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
private configure(): void {
|
||||
this.addDisposables(
|
||||
this._onDragStart,
|
||||
addDisposableListener(this.el, 'dragstart', (event) => {
|
||||
if (event.defaultPrevented || this.isCancelled(event)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const iframes = disableIframePointEvents();
|
||||
|
||||
this.pointerEventsDisposable.value = {
|
||||
dispose: () => {
|
||||
iframes.release();
|
||||
},
|
||||
};
|
||||
|
||||
this.el.classList.add('dv-dragged');
|
||||
setTimeout(() => this.el.classList.remove('dv-dragged'), 0);
|
||||
|
||||
this.dataDisposable.value = this.getData(event);
|
||||
this._onDragStart.fire(event);
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
|
||||
const hasData = event.dataTransfer.items.length > 0;
|
||||
|
||||
if (!hasData) {
|
||||
/**
|
||||
* Although this is not used by dockview many third party dnd libraries will check
|
||||
* dataTransfer.types to determine valid drag events.
|
||||
*
|
||||
* For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled
|
||||
* through .preventDefault(). Since this is applied globally to all drag events this would break dockviews
|
||||
* dnd logic. You can see the code at
|
||||
P * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542
|
||||
*/
|
||||
event.dataTransfer.setData('text/plain', '');
|
||||
}
|
||||
}
|
||||
}),
|
||||
addDisposableListener(this.el, 'dragend', () => {
|
||||
this.pointerEventsDisposable.dispose();
|
||||
setTimeout(() => {
|
||||
this.dataDisposable.dispose(); // allow the data to be read by other handlers before disposing
|
||||
}, 0);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
88
packages/dockview-core/src/dnd/dataTransfer.ts
Normal file
88
packages/dockview-core/src/dnd/dataTransfer.ts
Normal file
@ -0,0 +1,88 @@
|
||||
class TransferObject {
|
||||
// intentionally empty class
|
||||
}
|
||||
|
||||
export class PanelTransfer extends TransferObject {
|
||||
constructor(
|
||||
public readonly viewId: string,
|
||||
public readonly groupId: string,
|
||||
public readonly panelId: string | null
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class PaneTransfer extends TransferObject {
|
||||
constructor(
|
||||
public readonly viewId: string,
|
||||
public readonly paneId: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A singleton to store transfer data during drag & drop operations that are only valid within the application.
|
||||
*/
|
||||
export class LocalSelectionTransfer<T> {
|
||||
private static readonly INSTANCE = new LocalSelectionTransfer();
|
||||
|
||||
private data?: T[];
|
||||
private proto?: T;
|
||||
|
||||
private constructor() {
|
||||
// protect against external instantiation
|
||||
}
|
||||
|
||||
static getInstance<T>(): LocalSelectionTransfer<T> {
|
||||
return LocalSelectionTransfer.INSTANCE as LocalSelectionTransfer<T>;
|
||||
}
|
||||
|
||||
hasData(proto: T): boolean {
|
||||
return proto && proto === this.proto;
|
||||
}
|
||||
|
||||
clearData(proto: T): void {
|
||||
if (this.hasData(proto)) {
|
||||
this.proto = undefined;
|
||||
this.data = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getData(proto: T): T[] | undefined {
|
||||
if (this.hasData(proto)) {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setData(data: T[], proto: T): void {
|
||||
if (proto) {
|
||||
this.data = data;
|
||||
this.proto = proto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPanelData(): PanelTransfer | undefined {
|
||||
const panelTransfer = LocalSelectionTransfer.getInstance<PanelTransfer>();
|
||||
const isPanelEvent = panelTransfer.hasData(PanelTransfer.prototype);
|
||||
|
||||
if (!isPanelEvent) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return panelTransfer.getData(PanelTransfer.prototype)![0];
|
||||
}
|
||||
|
||||
export function getPaneData(): PaneTransfer | undefined {
|
||||
const paneTransfer = LocalSelectionTransfer.getInstance<PaneTransfer>();
|
||||
const isPanelEvent = paneTransfer.hasData(PaneTransfer.prototype);
|
||||
|
||||
if (!isPanelEvent) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return paneTransfer.getData(PaneTransfer.prototype)![0];
|
||||
}
|
109
packages/dockview-core/src/dnd/dnd.ts
Normal file
109
packages/dockview-core/src/dnd/dnd.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { addDisposableListener } from '../events';
|
||||
import { CompositeDisposable } from '../lifecycle';
|
||||
|
||||
export interface IDragAndDropObserverCallbacks {
|
||||
onDragEnter: (e: DragEvent) => void;
|
||||
onDragLeave: (e: DragEvent) => void;
|
||||
onDrop: (e: DragEvent) => void;
|
||||
onDragEnd: (e: DragEvent) => void;
|
||||
onDragOver?: (e: DragEvent) => void;
|
||||
}
|
||||
|
||||
export class DragAndDropObserver extends CompositeDisposable {
|
||||
private target: EventTarget | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly element: HTMLElement,
|
||||
private readonly callbacks: IDragAndDropObserverCallbacks
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
onDragEnter(e: DragEvent): void {
|
||||
this.target = e.target;
|
||||
this.callbacks.onDragEnter(e);
|
||||
}
|
||||
|
||||
onDragOver(e: DragEvent): void {
|
||||
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
|
||||
|
||||
if (this.callbacks.onDragOver) {
|
||||
this.callbacks.onDragOver(e);
|
||||
}
|
||||
}
|
||||
|
||||
onDragLeave(e: DragEvent): void {
|
||||
if (this.target === e.target) {
|
||||
this.target = null;
|
||||
|
||||
this.callbacks.onDragLeave(e);
|
||||
}
|
||||
}
|
||||
|
||||
onDragEnd(e: DragEvent): void {
|
||||
this.target = null;
|
||||
this.callbacks.onDragEnd(e);
|
||||
}
|
||||
|
||||
onDrop(e: DragEvent): void {
|
||||
this.callbacks.onDrop(e);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.addDisposables(
|
||||
addDisposableListener(
|
||||
this.element,
|
||||
'dragenter',
|
||||
(e: DragEvent) => {
|
||||
this.onDragEnter(e);
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
this.addDisposables(
|
||||
addDisposableListener(
|
||||
this.element,
|
||||
'dragover',
|
||||
(e: DragEvent) => {
|
||||
this.onDragOver(e);
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
this.addDisposables(
|
||||
addDisposableListener(this.element, 'dragleave', (e: DragEvent) => {
|
||||
this.onDragLeave(e);
|
||||
})
|
||||
);
|
||||
|
||||
this.addDisposables(
|
||||
addDisposableListener(this.element, 'dragend', (e: DragEvent) => {
|
||||
this.onDragEnd(e);
|
||||
})
|
||||
);
|
||||
|
||||
this.addDisposables(
|
||||
addDisposableListener(this.element, 'drop', (e: DragEvent) => {
|
||||
this.onDrop(e);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDraggedCompositeData {
|
||||
eventData: DragEvent;
|
||||
dragAndDropData: any;
|
||||
}
|
||||
|
||||
export interface ICompositeDragAndDropObserverCallbacks {
|
||||
onDragEnter?: (e: IDraggedCompositeData) => void;
|
||||
onDragLeave?: (e: IDraggedCompositeData) => void;
|
||||
onDrop?: (e: IDraggedCompositeData) => void;
|
||||
onDragOver?: (e: IDraggedCompositeData) => void;
|
||||
onDragStart?: (e: IDraggedCompositeData) => void;
|
||||
onDragEnd?: (e: IDraggedCompositeData) => void;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
.dv-drop-target-container {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
--dv-transition-duration: 300ms;
|
||||
|
||||
.dv-drop-target-anchor {
|
||||
position: relative;
|
||||
border: var(--dv-drag-over-border);
|
||||
transition: opacity var(--dv-transition-duration) ease-in,
|
||||
top var(--dv-transition-duration) ease-out,
|
||||
left var(--dv-transition-duration) ease-out,
|
||||
width var(--dv-transition-duration) ease-out,
|
||||
height var(--dv-transition-duration) ease-out;
|
||||
background-color: var(--dv-drag-over-background-color);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
102
packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts
Normal file
102
packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { CompositeDisposable, Disposable } from '../lifecycle';
|
||||
import { DropTargetTargetModel } from './droptarget';
|
||||
|
||||
export class DropTargetAnchorContainer extends CompositeDisposable {
|
||||
private _model:
|
||||
| { root: HTMLElement; overlay: HTMLElement; changed: boolean }
|
||||
| undefined;
|
||||
|
||||
private _outline: HTMLElement | undefined;
|
||||
|
||||
private _disabled = false;
|
||||
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(value: boolean) {
|
||||
if (this.disabled === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._disabled = value;
|
||||
|
||||
if (value) {
|
||||
this.model?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
get model(): DropTargetTargetModel | undefined {
|
||||
if (this.disabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
clear: () => {
|
||||
if (this._model) {
|
||||
this._model.root.parentElement?.removeChild(
|
||||
this._model.root
|
||||
);
|
||||
}
|
||||
this._model = undefined;
|
||||
},
|
||||
exists: () => {
|
||||
return !!this._model;
|
||||
},
|
||||
getElements: (event?: DragEvent, outline?: HTMLElement) => {
|
||||
const changed = this._outline !== outline;
|
||||
this._outline = outline;
|
||||
|
||||
if (this._model) {
|
||||
this._model.changed = changed;
|
||||
return this._model;
|
||||
}
|
||||
|
||||
const container = this.createContainer();
|
||||
const anchor = this.createAnchor();
|
||||
|
||||
this._model = { root: container, overlay: anchor, changed };
|
||||
|
||||
container.appendChild(anchor);
|
||||
this.element.appendChild(container);
|
||||
|
||||
if (event?.target instanceof HTMLElement) {
|
||||
const targetBox = event.target.getBoundingClientRect();
|
||||
const box = this.element.getBoundingClientRect();
|
||||
|
||||
anchor.style.left = `${targetBox.left - box.left}px`;
|
||||
anchor.style.top = `${targetBox.top - box.top}px`;
|
||||
}
|
||||
|
||||
return this._model;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
constructor(readonly element: HTMLElement, options: { disabled: boolean }) {
|
||||
super();
|
||||
|
||||
this._disabled = options.disabled;
|
||||
|
||||
this.addDisposables(
|
||||
Disposable.from(() => {
|
||||
this.model?.clear();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private createContainer(): HTMLElement {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'dv-drop-target-container';
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
private createAnchor(): HTMLElement {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'dv-drop-target-anchor';
|
||||
el.style.visibility = 'hidden';
|
||||
|
||||
return el;
|
||||
}
|
||||
}
|
54
packages/dockview-core/src/dnd/droptarget.scss
Normal file
54
packages/dockview-core/src/dnd/droptarget.scss
Normal file
@ -0,0 +1,54 @@
|
||||
.dv-drop-target {
|
||||
position: relative;
|
||||
--dv-transition-duration: 70ms;
|
||||
|
||||
> .dv-drop-target-dropzone {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
|
||||
> .dv-drop-target-selection {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: var(--dv-drag-over-border);
|
||||
background-color: var(--dv-drag-over-background-color);
|
||||
transition: top var(--dv-transition-duration) ease-out,
|
||||
left var(--dv-transition-duration) ease-out,
|
||||
width var(--dv-transition-duration) ease-out,
|
||||
height var(--dv-transition-duration) ease-out,
|
||||
opacity var(--dv-transition-duration) ease-out;
|
||||
will-change: transform;
|
||||
pointer-events: none;
|
||||
|
||||
&.dv-drop-target-top {
|
||||
&.dv-drop-target-small-vertical {
|
||||
border-top: 1px solid var(--dv-drag-over-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.dv-drop-target-bottom {
|
||||
&.dv-drop-target-small-vertical {
|
||||
border-bottom: 1px solid var(--dv-drag-over-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.dv-drop-target-left {
|
||||
&.dv-drop-target-small-horizontal {
|
||||
border-left: 1px solid var(--dv-drag-over-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.dv-drop-target-right {
|
||||
&.dv-drop-target-small-horizontal {
|
||||
border-right: 1px solid var(--dv-drag-over-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
644
packages/dockview-core/src/dnd/droptarget.ts
Normal file
644
packages/dockview-core/src/dnd/droptarget.ts
Normal file
@ -0,0 +1,644 @@
|
||||
import { toggleClass } from '../dom';
|
||||
import { DockviewEvent, Emitter, Event } from '../events';
|
||||
import { CompositeDisposable } from '../lifecycle';
|
||||
import { DragAndDropObserver } from './dnd';
|
||||
import { clamp } from '../math';
|
||||
import { Direction } from '../gridview/baseComponentGridview';
|
||||
|
||||
export interface DroptargetEvent {
|
||||
readonly position: Position;
|
||||
readonly nativeEvent: DragEvent;
|
||||
}
|
||||
|
||||
export class WillShowOverlayEvent
|
||||
extends DockviewEvent
|
||||
implements DroptargetEvent
|
||||
{
|
||||
get nativeEvent(): DragEvent {
|
||||
return this.options.nativeEvent;
|
||||
}
|
||||
|
||||
get position(): Position {
|
||||
return this.options.position;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly options: {
|
||||
nativeEvent: DragEvent;
|
||||
position: Position;
|
||||
}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export function directionToPosition(direction: Direction): Position {
|
||||
switch (direction) {
|
||||
case 'above':
|
||||
return 'top';
|
||||
case 'below':
|
||||
return 'bottom';
|
||||
case 'left':
|
||||
return 'left';
|
||||
case 'right':
|
||||
return 'right';
|
||||
case 'within':
|
||||
return 'center';
|
||||
default:
|
||||
throw new Error(`invalid direction '${direction}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export function positionToDirection(position: Position): Direction {
|
||||
switch (position) {
|
||||
case 'top':
|
||||
return 'above';
|
||||
case 'bottom':
|
||||
return 'below';
|
||||
case 'left':
|
||||
return 'left';
|
||||
case 'right':
|
||||
return 'right';
|
||||
case 'center':
|
||||
return 'within';
|
||||
default:
|
||||
throw new Error(`invalid position '${position}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export type Position = 'top' | 'bottom' | 'left' | 'right' | 'center';
|
||||
|
||||
export type CanDisplayOverlay = (
|
||||
dragEvent: DragEvent,
|
||||
state: Position
|
||||
) => boolean;
|
||||
|
||||
export type MeasuredValue = { value: number; type: 'pixels' | 'percentage' };
|
||||
|
||||
export type DroptargetOverlayModel = {
|
||||
size?: MeasuredValue;
|
||||
activationSize?: MeasuredValue;
|
||||
};
|
||||
|
||||
const DEFAULT_ACTIVATION_SIZE: MeasuredValue = {
|
||||
value: 20,
|
||||
type: 'percentage',
|
||||
};
|
||||
|
||||
const DEFAULT_SIZE: MeasuredValue = {
|
||||
value: 50,
|
||||
type: 'percentage',
|
||||
};
|
||||
|
||||
const SMALL_WIDTH_BOUNDARY = 100;
|
||||
const SMALL_HEIGHT_BOUNDARY = 100;
|
||||
|
||||
export interface DropTargetTargetModel {
|
||||
getElements(
|
||||
event?: DragEvent,
|
||||
outline?: HTMLElement
|
||||
): {
|
||||
root: HTMLElement;
|
||||
overlay: HTMLElement;
|
||||
changed: boolean;
|
||||
};
|
||||
exists(): boolean;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
export interface DroptargetOptions {
|
||||
canDisplayOverlay: CanDisplayOverlay;
|
||||
acceptedTargetZones: Position[];
|
||||
overlayModel?: DroptargetOverlayModel;
|
||||
getOverrideTarget?: () => DropTargetTargetModel | undefined;
|
||||
className?: string;
|
||||
getOverlayOutline?: () => HTMLElement | null;
|
||||
}
|
||||
|
||||
export class Droptarget extends CompositeDisposable {
|
||||
private targetElement: HTMLElement | undefined;
|
||||
private overlayElement: HTMLElement | undefined;
|
||||
private _state: Position | undefined;
|
||||
private _acceptedTargetZonesSet: Set<Position>;
|
||||
|
||||
private readonly _onDrop = new Emitter<DroptargetEvent>();
|
||||
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
|
||||
|
||||
private readonly _onWillShowOverlay = new Emitter<WillShowOverlayEvent>();
|
||||
readonly onWillShowOverlay: Event<WillShowOverlayEvent> =
|
||||
this._onWillShowOverlay.event;
|
||||
|
||||
readonly dnd: DragAndDropObserver;
|
||||
|
||||
private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__';
|
||||
|
||||
private static ACTUAL_TARGET: Droptarget | undefined;
|
||||
|
||||
private _disabled: boolean;
|
||||
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(value: boolean) {
|
||||
this._disabled = value;
|
||||
}
|
||||
|
||||
get state(): Position | undefined {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly element: HTMLElement,
|
||||
private readonly options: DroptargetOptions
|
||||
) {
|
||||
super();
|
||||
|
||||
this._disabled = false;
|
||||
|
||||
// use a set to take advantage of #<set>.has
|
||||
this._acceptedTargetZonesSet = new Set(
|
||||
this.options.acceptedTargetZones
|
||||
);
|
||||
|
||||
this.dnd = new DragAndDropObserver(this.element, {
|
||||
onDragEnter: () => {
|
||||
this.options.getOverrideTarget?.()?.getElements();
|
||||
},
|
||||
onDragOver: (e) => {
|
||||
Droptarget.ACTUAL_TARGET = this;
|
||||
|
||||
const overrideTraget = this.options.getOverrideTarget?.();
|
||||
|
||||
if (this._acceptedTargetZonesSet.size === 0) {
|
||||
if (overrideTraget) {
|
||||
return;
|
||||
}
|
||||
this.removeDropTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
const target =
|
||||
this.options.getOverlayOutline?.() ?? this.element;
|
||||
|
||||
const width = target.offsetWidth;
|
||||
const height = target.offsetHeight;
|
||||
|
||||
if (width === 0 || height === 0) {
|
||||
return; // avoid div!0
|
||||
}
|
||||
|
||||
const rect = (
|
||||
e.currentTarget as HTMLElement
|
||||
).getBoundingClientRect();
|
||||
const x = (e.clientX ?? 0) - rect.left;
|
||||
const y = (e.clientY ?? 0) - rect.top;
|
||||
|
||||
const quadrant = this.calculateQuadrant(
|
||||
this._acceptedTargetZonesSet,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
);
|
||||
|
||||
/**
|
||||
* If the event has already been used by another DropTarget instance
|
||||
* then don't show a second drop target, only one target should be
|
||||
* active at any one time
|
||||
*/
|
||||
if (this.isAlreadyUsed(e) || quadrant === null) {
|
||||
// no drop target should be displayed
|
||||
this.removeDropTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.options.canDisplayOverlay(e, quadrant)) {
|
||||
if (overrideTraget) {
|
||||
return;
|
||||
}
|
||||
this.removeDropTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
const willShowOverlayEvent = new WillShowOverlayEvent({
|
||||
nativeEvent: e,
|
||||
position: quadrant,
|
||||
});
|
||||
|
||||
/**
|
||||
* Provide an opportunity to prevent the overlay appearing and in turn
|
||||
* any dnd behaviours
|
||||
*/
|
||||
this._onWillShowOverlay.fire(willShowOverlayEvent);
|
||||
|
||||
if (willShowOverlayEvent.defaultPrevented) {
|
||||
this.removeDropTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
this.markAsUsed(e);
|
||||
|
||||
if (overrideTraget) {
|
||||
//
|
||||
} else if (!this.targetElement) {
|
||||
this.targetElement = document.createElement('div');
|
||||
this.targetElement.className = 'dv-drop-target-dropzone';
|
||||
this.overlayElement = document.createElement('div');
|
||||
this.overlayElement.className = 'dv-drop-target-selection';
|
||||
this._state = 'center';
|
||||
this.targetElement.appendChild(this.overlayElement);
|
||||
|
||||
target.classList.add('dv-drop-target');
|
||||
target.append(this.targetElement);
|
||||
|
||||
// this.overlayElement.style.opacity = '0';
|
||||
|
||||
// requestAnimationFrame(() => {
|
||||
// if (this.overlayElement) {
|
||||
// this.overlayElement.style.opacity = '';
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
this.toggleClasses(quadrant, width, height);
|
||||
|
||||
this._state = quadrant;
|
||||
},
|
||||
onDragLeave: () => {
|
||||
const target = this.options.getOverrideTarget?.();
|
||||
|
||||
if (target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeDropTarget();
|
||||
},
|
||||
onDragEnd: (e) => {
|
||||
const target = this.options.getOverrideTarget?.();
|
||||
|
||||
if (target && Droptarget.ACTUAL_TARGET === this) {
|
||||
if (this._state) {
|
||||
// only stop the propagation of the event if we are dealing with it
|
||||
// which is only when the target has state
|
||||
e.stopPropagation();
|
||||
this._onDrop.fire({
|
||||
position: this._state,
|
||||
nativeEvent: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.removeDropTarget();
|
||||
|
||||
target?.clear();
|
||||
},
|
||||
onDrop: (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const state = this._state;
|
||||
|
||||
this.removeDropTarget();
|
||||
|
||||
this.options.getOverrideTarget?.()?.clear();
|
||||
|
||||
if (state) {
|
||||
// only stop the propagation of the event if we are dealing with it
|
||||
// which is only when the target has state
|
||||
e.stopPropagation();
|
||||
this._onDrop.fire({ position: state, nativeEvent: e });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.addDisposables(this._onDrop, this._onWillShowOverlay, this.dnd);
|
||||
}
|
||||
|
||||
setTargetZones(acceptedTargetZones: Position[]): void {
|
||||
this._acceptedTargetZonesSet = new Set(acceptedTargetZones);
|
||||
}
|
||||
|
||||
setOverlayModel(model: DroptargetOverlayModel): void {
|
||||
this.options.overlayModel = model;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.removeDropTarget();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a property to the event object for other potential listeners to check
|
||||
*/
|
||||
private markAsUsed(event: DragEvent): void {
|
||||
(event as any)[Droptarget.USED_EVENT_ID] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is the event has already been used by another instance of DropTarget
|
||||
*/
|
||||
private isAlreadyUsed(event: DragEvent): boolean {
|
||||
const value = (event as any)[Droptarget.USED_EVENT_ID];
|
||||
return typeof value === 'boolean' && value;
|
||||
}
|
||||
|
||||
private toggleClasses(
|
||||
quadrant: Position,
|
||||
width: number,
|
||||
height: number
|
||||
): void {
|
||||
const target = this.options.getOverrideTarget?.();
|
||||
|
||||
if (!target && !this.overlayElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSmallX = width < SMALL_WIDTH_BOUNDARY;
|
||||
const isSmallY = height < SMALL_HEIGHT_BOUNDARY;
|
||||
|
||||
const isLeft = quadrant === 'left';
|
||||
const isRight = quadrant === 'right';
|
||||
const isTop = quadrant === 'top';
|
||||
const isBottom = quadrant === 'bottom';
|
||||
|
||||
const rightClass = !isSmallX && isRight;
|
||||
const leftClass = !isSmallX && isLeft;
|
||||
const topClass = !isSmallY && isTop;
|
||||
const bottomClass = !isSmallY && isBottom;
|
||||
|
||||
let size = 1;
|
||||
|
||||
const sizeOptions = this.options.overlayModel?.size ?? DEFAULT_SIZE;
|
||||
|
||||
if (sizeOptions.type === 'percentage') {
|
||||
size = clamp(sizeOptions.value, 0, 100) / 100;
|
||||
} else {
|
||||
if (rightClass || leftClass) {
|
||||
size = clamp(0, sizeOptions.value, width) / width;
|
||||
}
|
||||
if (topClass || bottomClass) {
|
||||
size = clamp(0, sizeOptions.value, height) / height;
|
||||
}
|
||||
}
|
||||
|
||||
if (target) {
|
||||
const outlineEl =
|
||||
this.options.getOverlayOutline?.() ?? this.element;
|
||||
const elBox = outlineEl.getBoundingClientRect();
|
||||
|
||||
const ta = target.getElements(undefined, outlineEl);
|
||||
const el = ta.root;
|
||||
const overlay = ta.overlay;
|
||||
|
||||
const bigbox = el.getBoundingClientRect();
|
||||
|
||||
const rootTop = elBox.top - bigbox.top;
|
||||
const rootLeft = elBox.left - bigbox.left;
|
||||
|
||||
const box = {
|
||||
top: rootTop,
|
||||
left: rootLeft,
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
|
||||
if (rightClass) {
|
||||
box.left = rootLeft + width * (1 - size);
|
||||
box.width = width * size;
|
||||
} else if (leftClass) {
|
||||
box.width = width * size;
|
||||
} else if (topClass) {
|
||||
box.height = height * size;
|
||||
} else if (bottomClass) {
|
||||
box.top = rootTop + height * (1 - size);
|
||||
box.height = height * size;
|
||||
}
|
||||
|
||||
if (isSmallX && isLeft) {
|
||||
box.width = 4;
|
||||
}
|
||||
if (isSmallX && isRight) {
|
||||
box.left = rootLeft + width - 4;
|
||||
box.width = 4;
|
||||
}
|
||||
|
||||
const topPx = `${Math.round(box.top)}px`;
|
||||
const leftPx = `${Math.round(box.left)}px`;
|
||||
const widthPx = `${Math.round(box.width)}px`;
|
||||
const heightPx = `${Math.round(box.height)}px`;
|
||||
|
||||
if (
|
||||
overlay.style.top === topPx &&
|
||||
overlay.style.left === leftPx &&
|
||||
overlay.style.width === widthPx &&
|
||||
overlay.style.height === heightPx
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
overlay.style.top = topPx;
|
||||
overlay.style.left = leftPx;
|
||||
overlay.style.width = widthPx;
|
||||
overlay.style.height = heightPx;
|
||||
overlay.style.visibility = 'visible';
|
||||
|
||||
overlay.className = `dv-drop-target-anchor${
|
||||
this.options.className ? ` ${this.options.className}` : ''
|
||||
}`;
|
||||
|
||||
toggleClass(overlay, 'dv-drop-target-left', isLeft);
|
||||
toggleClass(overlay, 'dv-drop-target-right', isRight);
|
||||
toggleClass(overlay, 'dv-drop-target-top', isTop);
|
||||
toggleClass(overlay, 'dv-drop-target-bottom', isBottom);
|
||||
toggleClass(
|
||||
overlay,
|
||||
'dv-drop-target-center',
|
||||
quadrant === 'center'
|
||||
);
|
||||
|
||||
if (ta.changed) {
|
||||
toggleClass(
|
||||
overlay,
|
||||
'dv-drop-target-anchor-container-changed',
|
||||
true
|
||||
);
|
||||
setTimeout(() => {
|
||||
toggleClass(
|
||||
overlay,
|
||||
'dv-drop-target-anchor-container-changed',
|
||||
false
|
||||
);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.overlayElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const box = { top: '0px', left: '0px', width: '100%', height: '100%' };
|
||||
|
||||
/**
|
||||
* You can also achieve the overlay placement using the transform CSS property
|
||||
* to translate and scale the element however this has the undesired effect of
|
||||
* 'skewing' the element. Comment left here for anybody that ever revisits this.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/transform
|
||||
*
|
||||
* right
|
||||
* translateX(${100 * (1 - size) / 2}%) scaleX(${scale})
|
||||
*
|
||||
* left
|
||||
* translateX(-${100 * (1 - size) / 2}%) scaleX(${scale})
|
||||
*
|
||||
* top
|
||||
* translateY(-${100 * (1 - size) / 2}%) scaleY(${scale})
|
||||
*
|
||||
* bottom
|
||||
* translateY(${100 * (1 - size) / 2}%) scaleY(${scale})
|
||||
*/
|
||||
if (rightClass) {
|
||||
box.left = `${100 * (1 - size)}%`;
|
||||
box.width = `${100 * size}%`;
|
||||
} else if (leftClass) {
|
||||
box.width = `${100 * size}%`;
|
||||
} else if (topClass) {
|
||||
box.height = `${100 * size}%`;
|
||||
} else if (bottomClass) {
|
||||
box.top = `${100 * (1 - size)}%`;
|
||||
box.height = `${100 * size}%`;
|
||||
}
|
||||
|
||||
this.overlayElement.style.top = box.top;
|
||||
this.overlayElement.style.left = box.left;
|
||||
this.overlayElement.style.width = box.width;
|
||||
this.overlayElement.style.height = box.height;
|
||||
|
||||
toggleClass(
|
||||
this.overlayElement,
|
||||
'dv-drop-target-small-vertical',
|
||||
isSmallY
|
||||
);
|
||||
toggleClass(
|
||||
this.overlayElement,
|
||||
'dv-drop-target-small-horizontal',
|
||||
isSmallX
|
||||
);
|
||||
toggleClass(this.overlayElement, 'dv-drop-target-left', isLeft);
|
||||
toggleClass(this.overlayElement, 'dv-drop-target-right', isRight);
|
||||
toggleClass(this.overlayElement, 'dv-drop-target-top', isTop);
|
||||
toggleClass(this.overlayElement, 'dv-drop-target-bottom', isBottom);
|
||||
toggleClass(
|
||||
this.overlayElement,
|
||||
'dv-drop-target-center',
|
||||
quadrant === 'center'
|
||||
);
|
||||
}
|
||||
|
||||
private calculateQuadrant(
|
||||
overlayType: Set<Position>,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
): Position | null {
|
||||
const activationSizeOptions =
|
||||
this.options.overlayModel?.activationSize ??
|
||||
DEFAULT_ACTIVATION_SIZE;
|
||||
|
||||
const isPercentage = activationSizeOptions.type === 'percentage';
|
||||
|
||||
if (isPercentage) {
|
||||
return calculateQuadrantAsPercentage(
|
||||
overlayType,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
activationSizeOptions.value
|
||||
);
|
||||
}
|
||||
|
||||
return calculateQuadrantAsPixels(
|
||||
overlayType,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
activationSizeOptions.value
|
||||
);
|
||||
}
|
||||
|
||||
private removeDropTarget(): void {
|
||||
if (this.targetElement) {
|
||||
this._state = undefined;
|
||||
this.targetElement.parentElement?.classList.remove(
|
||||
'dv-drop-target'
|
||||
);
|
||||
this.targetElement.remove();
|
||||
this.targetElement = undefined;
|
||||
this.overlayElement = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateQuadrantAsPercentage(
|
||||
overlayType: Set<Position>,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
threshold: number
|
||||
): Position | null {
|
||||
const xp = (100 * x) / width;
|
||||
const yp = (100 * y) / height;
|
||||
|
||||
if (overlayType.has('left') && xp < threshold) {
|
||||
return 'left';
|
||||
}
|
||||
if (overlayType.has('right') && xp > 100 - threshold) {
|
||||
return 'right';
|
||||
}
|
||||
if (overlayType.has('top') && yp < threshold) {
|
||||
return 'top';
|
||||
}
|
||||
if (overlayType.has('bottom') && yp > 100 - threshold) {
|
||||
return 'bottom';
|
||||
}
|
||||
|
||||
if (!overlayType.has('center')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'center';
|
||||
}
|
||||
|
||||
export function calculateQuadrantAsPixels(
|
||||
overlayType: Set<Position>,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
threshold: number
|
||||
): Position | null {
|
||||
if (overlayType.has('left') && x < threshold) {
|
||||
return 'left';
|
||||
}
|
||||
if (overlayType.has('right') && x > width - threshold) {
|
||||
return 'right';
|
||||
}
|
||||
if (overlayType.has('top') && y < threshold) {
|
||||
return 'top';
|
||||
}
|
||||
if (overlayType.has('bottom') && y > height - threshold) {
|
||||
return 'bottom';
|
||||
}
|
||||
|
||||
if (!overlayType.has('center')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'center';
|
||||
}
|
21
packages/dockview-core/src/dnd/ghost.ts
Normal file
21
packages/dockview-core/src/dnd/ghost.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { addClasses, removeClasses } from '../dom';
|
||||
|
||||
export function addGhostImage(
|
||||
dataTransfer: DataTransfer,
|
||||
ghostElement: HTMLElement,
|
||||
options?: { x?: number; y?: number }
|
||||
): void {
|
||||
// class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues
|
||||
addClasses(ghostElement, 'dv-dragged');
|
||||
|
||||
// move the element off-screen initially otherwise it may in some cases be rendered at (0,0) momentarily
|
||||
ghostElement.style.top = '-9999px';
|
||||
|
||||
document.body.appendChild(ghostElement);
|
||||
dataTransfer.setDragImage(ghostElement, options?.x ?? 0, options?.y ?? 0);
|
||||
|
||||
setTimeout(() => {
|
||||
removeClasses(ghostElement, 'dv-dragged');
|
||||
ghostElement.remove();
|
||||
}, 0);
|
||||
}
|
88
packages/dockview-core/src/dnd/groupDragHandler.ts
Normal file
88
packages/dockview-core/src/dnd/groupDragHandler.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { DockviewComponent } from '../dockview/dockviewComponent';
|
||||
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
|
||||
import { quasiPreventDefault } from '../dom';
|
||||
import { addDisposableListener } from '../events';
|
||||
import { IDisposable } from '../lifecycle';
|
||||
import { DragHandler } from './abstractDragHandler';
|
||||
import { LocalSelectionTransfer, PanelTransfer } from './dataTransfer';
|
||||
import { addGhostImage } from './ghost';
|
||||
|
||||
export class GroupDragHandler extends DragHandler {
|
||||
private readonly panelTransfer =
|
||||
LocalSelectionTransfer.getInstance<PanelTransfer>();
|
||||
|
||||
constructor(
|
||||
element: HTMLElement,
|
||||
private readonly accessor: DockviewComponent,
|
||||
private readonly group: DockviewGroupPanel
|
||||
) {
|
||||
super(element);
|
||||
|
||||
this.addDisposables(
|
||||
addDisposableListener(
|
||||
element,
|
||||
'pointerdown',
|
||||
(e) => {
|
||||
if (e.shiftKey) {
|
||||
/**
|
||||
* You cannot call e.preventDefault() because that will prevent drag events from firing
|
||||
* but we also need to stop any group overlay drag events from occuring
|
||||
* Use a custom event marker that can be checked by the overlay drag events
|
||||
*/
|
||||
quasiPreventDefault(e);
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override isCancelled(_event: DragEvent): boolean {
|
||||
if (this.group.api.location.type === 'floating' && !_event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getData(dragEvent: DragEvent): IDisposable {
|
||||
const dataTransfer = dragEvent.dataTransfer;
|
||||
|
||||
this.panelTransfer.setData(
|
||||
[new PanelTransfer(this.accessor.id, this.group.id, null)],
|
||||
PanelTransfer.prototype
|
||||
);
|
||||
|
||||
const style = window.getComputedStyle(this.el);
|
||||
|
||||
const bgColor = style.getPropertyValue(
|
||||
'--dv-activegroup-visiblepanel-tab-background-color'
|
||||
);
|
||||
const color = style.getPropertyValue(
|
||||
'--dv-activegroup-visiblepanel-tab-color'
|
||||
);
|
||||
|
||||
if (dataTransfer) {
|
||||
const ghostElement = document.createElement('div');
|
||||
|
||||
ghostElement.style.backgroundColor = bgColor;
|
||||
ghostElement.style.color = color;
|
||||
ghostElement.style.padding = '2px 8px';
|
||||
ghostElement.style.height = '24px';
|
||||
ghostElement.style.fontSize = '11px';
|
||||
ghostElement.style.lineHeight = '20px';
|
||||
ghostElement.style.borderRadius = '12px';
|
||||
ghostElement.style.position = 'absolute';
|
||||
ghostElement.style.pointerEvents = 'none';
|
||||
ghostElement.style.top = '-9999px';
|
||||
ghostElement.textContent = `Multiple Panels (${this.group.size})`;
|
||||
|
||||
addGhostImage(dataTransfer, ghostElement, { y: -10, x: 30 });
|
||||
}
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
this.panelTransfer.clearData(PanelTransfer.prototype);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user