Compare commits
829 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fe94bfc21 | ||
|
|
6be6f6ee8a | ||
|
|
83ce090402 | ||
|
|
250ff30704 | ||
|
|
55d7d0a846 | ||
|
|
0b3d2cedbd | ||
|
|
de025d9bc7 | ||
|
|
79a39f993d | ||
|
|
0fa731c3a2 | ||
|
|
34eece8fcf | ||
|
|
790662ad0c | ||
|
|
7d83a3635f | ||
|
|
546cf27b33 | ||
|
|
e59d419708 | ||
|
|
2d50106ce7 | ||
|
|
70c32e3c74 | ||
|
|
a0f56473ee | ||
|
|
51dcb1f54f | ||
|
|
9e34fd6174 | ||
|
|
3b47d11b21 | ||
|
|
6e91df9d03 | ||
|
|
4c8f1bd69c | ||
|
|
1f993518fd | ||
|
|
7f9a1e9309 | ||
|
|
5df4a7edb8 | ||
|
|
ddd578d773 | ||
|
|
c8e63e433c | ||
|
|
cadd159a46 | ||
|
|
d32588763c | ||
|
|
6723250266 | ||
|
|
41ca8f8e46 | ||
|
|
a6d04ffa91 | ||
|
|
631892d785 | ||
|
|
3d7f0fcbe7 | ||
|
|
c716ec7294 | ||
|
|
4db6bda193 | ||
|
|
bc2a5b214f | ||
|
|
879de57b49 | ||
|
|
0d4cbc7344 | ||
|
|
217f5ee95d | ||
|
|
ad98c0f229 | ||
|
|
9a7f827301 | ||
|
|
211fa183d3 | ||
|
|
c0e0fb23e4 | ||
|
|
e0d46055a7 | ||
|
|
a1ec72b401 | ||
|
|
ffc7bf5b7e | ||
|
|
4e6e0e2221 | ||
|
|
a1bbe543ba | ||
|
|
48b5ff5f1c | ||
|
|
13dc61a585 | ||
|
|
6e476489f5 | ||
|
|
e6b82db674 | ||
|
|
4c60c52a8f | ||
|
|
29ed8f7a8d | ||
|
|
4b880e13a5 | ||
|
|
b5a08a2414 | ||
|
|
57e9379f49 | ||
|
|
134fd69ffa | ||
|
|
e2830ccfb1 | ||
|
|
b0dd90f5b3 | ||
|
|
32fc6937d2 | ||
|
|
23a3e26800 | ||
|
|
c7b0cbc128 | ||
|
|
9915479354 | ||
|
|
abddafa3cf | ||
|
|
5b057ff9de | ||
|
|
6dcbc0604d | ||
|
|
0e5fed6645 | ||
|
|
b6d91ec07a | ||
|
|
baf36acc97 | ||
|
|
e0db4ad4f6 | ||
|
|
4fc2d79616 | ||
|
|
9e4652b7b5 | ||
|
|
8466a3e99f | ||
|
|
e85f187199 | ||
|
|
77167c572d | ||
|
|
9aae8bf55c | ||
|
|
250fb2eb50 | ||
|
|
2a49dd9140 | ||
|
|
4dedaaea3b | ||
|
|
7bc666be36 | ||
|
|
93a56ef4ee | ||
|
|
83b916d3c9 | ||
|
|
20157fcc8d | ||
|
|
2c3fb4d7ea | ||
|
|
75ad40ffbc | ||
|
|
e263eb8252 | ||
|
|
8e43cd609f | ||
|
|
1a3401fd1a | ||
|
|
d1dcef2537 | ||
|
|
e732415aa3 | ||
|
|
ddbde0141a | ||
|
|
bc907f4b37 | ||
|
|
1caf2c7f15 | ||
|
|
22b56fb8dc | ||
|
|
87eccc298c | ||
|
|
ca9e47183d | ||
|
|
1fbfe6f5ac | ||
|
|
84d2a374d1 | ||
|
|
74a000723b | ||
|
|
5079ad2190 | ||
|
|
21053c43c9 | ||
|
|
4c6270881a | ||
|
|
c17e5430ed | ||
|
|
0a36555343 | ||
|
|
bce2bb8fc4 | ||
|
|
d6ae06dbd6 | ||
|
|
9221c404ee | ||
|
|
697e53ff7d | ||
|
|
9360aff6c9 | ||
|
|
c68d629b7a | ||
|
|
caedfb46a9 | ||
|
|
2e4c6bc08e | ||
|
|
777eafcd2f | ||
|
|
20780a0c59 | ||
|
|
5d433cce16 | ||
|
|
ba77fde93b | ||
|
|
45b8850493 | ||
|
|
39c2c15259 | ||
|
|
6d780c6c26 | ||
|
|
4cf66adc18 | ||
|
|
7bff14e1bb | ||
|
|
7986e0d0cc | ||
|
|
a316d0f0fe | ||
|
|
88fa6225eb | ||
|
|
7ec720823f | ||
|
|
c39daf72b4 | ||
|
|
9c4e72eb29 | ||
|
|
88e3c1f660 | ||
|
|
e6c92d4a5e | ||
|
|
745531f20f | ||
|
|
3acd425c09 | ||
|
|
8dcc7c985d | ||
|
|
253ded33bf | ||
|
|
2c6b8294f4 | ||
|
|
83a5ef8e2e | ||
|
|
b959e90723 | ||
|
|
89ebc9a1fa | ||
|
|
56d2e34fbd | ||
|
|
0f2aed7e8a | ||
|
|
288ceafa92 | ||
|
|
99dc443142 | ||
|
|
b6440eba1a | ||
|
|
545e46babc | ||
|
|
b95b6af1b1 | ||
|
|
ccef5fc581 | ||
|
|
ed82fe5a61 | ||
|
|
1550f032d9 | ||
|
|
7d2758a21c | ||
|
|
e4fab73017 | ||
|
|
1beb03eaa6 | ||
|
|
20f67efd58 | ||
|
|
8c3d66eb3c | ||
|
|
a4f6006efd | ||
|
|
1d297350cc | ||
|
|
07650f8978 | ||
|
|
b26e5625ce | ||
|
|
1e38731ecb | ||
|
|
ef9b9804cb | ||
|
|
9fe321a127 | ||
|
|
464e57d019 | ||
|
|
50f125471e | ||
|
|
6bbee4a5cc | ||
|
|
5052c0427a | ||
|
|
9528631ed1 | ||
|
|
2a816f62fa | ||
|
|
8596e3356d | ||
|
|
7422af7f3b | ||
|
|
0f047da78b | ||
|
|
f866abb34c | ||
|
|
01d7e36990 | ||
|
|
a9493b9c16 | ||
|
|
a59a283d74 | ||
|
|
aafcba8bb7 | ||
|
|
11ea7d452c | ||
|
|
47d21d85fb | ||
|
|
518b7642a0 | ||
|
|
a9ea4b8e33 | ||
|
|
443465eb86 | ||
|
|
e1172c8d0d | ||
|
|
7a2605fdad | ||
|
|
a97d549d69 | ||
|
|
fcf48ca3dc | ||
|
|
c0ad18cff8 | ||
|
|
73e7855575 | ||
|
|
69ef7faf49 | ||
|
|
036f845968 | ||
|
|
740e2e3410 | ||
|
|
ba44f69f9f | ||
|
|
977799a3ac | ||
|
|
76ddecee50 | ||
|
|
0ab495a161 | ||
|
|
fefbcfbbee | ||
|
|
c77c7403da | ||
|
|
b0044bb5fa | ||
|
|
e61749c1b3 | ||
|
|
43216ed925 | ||
|
|
385873b6e8 | ||
|
|
5583642961 | ||
|
|
e063724ab6 | ||
|
|
19d4788489 | ||
|
|
1452fd2a28 | ||
|
|
c296b99d5a | ||
|
|
e9de1e675f | ||
|
|
b3e254c0ee | ||
|
|
bd3d470e40 | ||
|
|
21564445d6 | ||
|
|
e53d1b1948 | ||
|
|
c428f166a6 | ||
|
|
f19704ec41 | ||
|
|
d47f013f84 | ||
|
|
612d675a19 | ||
|
|
13f571e0b5 | ||
|
|
b09d408ab3 | ||
|
|
ea95ae2b5d | ||
|
|
2590e21807 | ||
|
|
fb41653d79 | ||
|
|
4c9c34a0ea | ||
|
|
0d5602b832 | ||
|
|
af87f84ce8 | ||
|
|
a96c16aa45 | ||
|
|
e29e1c26e5 | ||
|
|
fee38cbe8a | ||
|
|
58ca173234 | ||
|
|
aee10c6810 | ||
|
|
fe43be3451 | ||
|
|
21868c7f44 | ||
|
|
5585d2a4f7 | ||
|
|
1d04038609 | ||
|
|
305c5e9f70 | ||
|
|
4ac91003bb | ||
|
|
44410900aa | ||
|
|
239b369391 | ||
|
|
d9638fef98 | ||
|
|
ecb9482b82 | ||
|
|
cd5556aad5 | ||
|
|
063af62cf6 | ||
|
|
a4611d11a8 | ||
|
|
5d1f460a94 | ||
|
|
075e578bb4 | ||
|
|
32c17921ca | ||
|
|
7c470c9b88 | ||
|
|
e472b840f1 | ||
|
|
726460c812 | ||
|
|
7ccf796c34 | ||
|
|
c183306ab2 | ||
|
|
29bb27aa23 | ||
|
|
7f9d03c8af | ||
|
|
c3a0e09f6d | ||
|
|
1d497b2f13 | ||
|
|
f43a2ebdef | ||
|
|
4c3f3cb1ab | ||
|
|
a404a71ba2 | ||
|
|
6b4c118a2b | ||
|
|
1157257911 | ||
|
|
f2642c9d63 | ||
|
|
91bc0351b8 | ||
|
|
df2dc96ba5 | ||
|
|
3d86650f22 | ||
|
|
a31e93bacf | ||
|
|
a3362e44fe | ||
|
|
f7f234b4cb | ||
|
|
48f1de5c25 | ||
|
|
c533459da1 | ||
|
|
d99895b845 | ||
|
|
c65393d1bc | ||
|
|
5d133f74cf | ||
|
|
5997c98b8f | ||
|
|
217d66f692 | ||
|
|
d14d887c1a | ||
|
|
e36e238c2f | ||
|
|
1b0646af6d | ||
|
|
a24f7a73c8 | ||
|
|
b35dd282ec | ||
|
|
8c0c2c5bc4 | ||
|
|
3763cd0efc | ||
|
|
4ce9533763 | ||
|
|
43ecdafdd1 | ||
|
|
5ff8704778 | ||
|
|
d206d6dd99 | ||
|
|
850b9ed936 | ||
|
|
26508b5a62 | ||
|
|
061c82459e | ||
|
|
d2bf5be7c7 | ||
|
|
b726aa30e5 | ||
|
|
6546ec090e | ||
|
|
4dc5754f1d | ||
|
|
7f199e6c2f | ||
|
|
2548ac4eb4 | ||
|
|
60e503ab1f | ||
|
|
c718cbc030 | ||
|
|
beb2b550f0 | ||
|
|
d3e1389f10 | ||
|
|
68cb24c0b6 | ||
|
|
e3959107bd | ||
|
|
4b3c81ab91 | ||
|
|
76b5d9d11d | ||
|
|
22d0fe5ac4 | ||
|
|
8b8d549abd | ||
|
|
731a5a504d | ||
|
|
008e697b74 | ||
|
|
fe1745e779 | ||
|
|
e6ac9be402 | ||
|
|
b427a9ed1b | ||
|
|
a4dc9210b3 | ||
|
|
fb681de1f5 | ||
|
|
3757622521 | ||
|
|
12265be7d4 | ||
|
|
df1aed7e04 | ||
|
|
c32c9d1ba1 | ||
|
|
26d75c9203 | ||
|
|
8d1e9fa8e9 | ||
|
|
ebc99e97af | ||
|
|
efe4aa0ec2 | ||
|
|
3f659af1e1 | ||
|
|
64209a6392 | ||
|
|
8d1e5dd2e9 | ||
|
|
d218122752 | ||
|
|
9af8afca22 | ||
|
|
002ec41ba8 | ||
|
|
ffff257f57 | ||
|
|
41d0b675a0 | ||
|
|
7601d6730d | ||
|
|
f0e9b76bb5 | ||
|
|
4ec33062a4 | ||
|
|
edc63b1293 | ||
|
|
ec83976818 | ||
|
|
393410757b | ||
|
|
f20748744a | ||
|
|
067131f76d | ||
|
|
f689d333f9 | ||
|
|
b3059bb6a3 | ||
|
|
b135f6a61c | ||
|
|
2a8b71497f | ||
|
|
726ebc5e88 | ||
|
|
2c56cd453c | ||
|
|
bf2f9b2697 | ||
|
|
fbd061e8b3 | ||
|
|
680320ba76 | ||
|
|
0d992bd6b1 | ||
|
|
c80eacc5e7 | ||
|
|
6d202b4b7d | ||
|
|
94478fe9f3 | ||
|
|
5745e4567b | ||
|
|
c6a3f4ac7b | ||
|
|
f6609b3050 | ||
|
|
4d69778a50 | ||
|
|
c8dfbd3b87 | ||
|
|
c0aab1e921 | ||
|
|
6efee6e859 | ||
|
|
dd82bf0879 | ||
|
|
2d41b5f9e6 | ||
|
|
7e7b6fae9d | ||
|
|
29020daaf0 | ||
|
|
6bcead7784 | ||
|
|
9625129691 | ||
|
|
765d0ee212 | ||
|
|
099d4cd78e | ||
|
|
d9a6981df4 | ||
|
|
bbf424c6d2 | ||
|
|
5cde3b76fe | ||
|
|
75af742053 | ||
|
|
69e192ea9d | ||
|
|
5a32c1d99d | ||
|
|
e81e0a5512 | ||
|
|
2fef76c55c | ||
|
|
49735950f2 | ||
|
|
c005455144 | ||
|
|
ca4afb5440 | ||
|
|
885647cedf | ||
|
|
dc8efbe3ef | ||
|
|
888b8e725a | ||
|
|
15d65db19d | ||
|
|
2ce0d4cd11 | ||
|
|
13f3c2c20c | ||
|
|
d707329526 | ||
|
|
69faa8bb3e | ||
|
|
86b184d5c1 | ||
|
|
2b6263acb4 | ||
|
|
187c940e56 | ||
|
|
4b59bec01c | ||
|
|
9427ee550c | ||
|
|
14975e117c | ||
|
|
d6254c0cc2 | ||
|
|
4a81ce9cc2 | ||
|
|
d17191c890 | ||
|
|
ff56fe3e68 | ||
|
|
6bdcec0fca | ||
|
|
27885aabe7 | ||
|
|
e345037f9b | ||
|
|
da5290e649 | ||
|
|
516676b484 | ||
|
|
20a8934da9 | ||
|
|
0075c44b29 | ||
|
|
ec7a8cdd43 | ||
|
|
1629bb7ccf | ||
|
|
35bff6ab57 | ||
|
|
ef9d8b0ea4 | ||
|
|
5d4c5703bb | ||
|
|
11bb519db8 | ||
|
|
1952280003 | ||
|
|
c845a0b7fa | ||
|
|
f1748e7e42 | ||
|
|
49063d257b | ||
|
|
803c83ac4f | ||
|
|
0bdf9f3add | ||
|
|
10ed3d4f7c | ||
|
|
8fc7f7d32c | ||
|
|
27a0efa4e0 | ||
|
|
7d227e901a | ||
|
|
080d7489e7 | ||
|
|
b11bd5a7ef | ||
|
|
80ae38d295 | ||
|
|
a4b7915196 | ||
|
|
4d4f1b993e | ||
|
|
b814bd35ca | ||
|
|
c57361a360 | ||
|
|
f8c2a62bd6 | ||
|
|
59241717f5 | ||
|
|
e90509cac9 | ||
|
|
8a8cc26c1d | ||
|
|
1d443b9f94 | ||
|
|
60a4f443a7 | ||
|
|
72c2540dcc | ||
|
|
18cec3b75a | ||
|
|
d4aae5268e | ||
|
|
99ca0738d4 | ||
|
|
408ca6d711 | ||
|
|
99dcb55397 | ||
|
|
e11b6647b8 | ||
|
|
78f5d4ec88 | ||
|
|
20eba7b29b | ||
|
|
4c9698a147 | ||
|
|
55da8eac83 | ||
|
|
17ea049393 | ||
|
|
d2e468287d | ||
|
|
ad6085890e | ||
|
|
cd4f1b1bd8 | ||
|
|
a2acf810cb | ||
|
|
14bd7c3705 | ||
|
|
5d0c9dcab1 | ||
|
|
def2f02eea | ||
|
|
a74a60c22d | ||
|
|
74d37f2cbc | ||
|
|
6ca9a116c2 | ||
|
|
8e59677623 | ||
|
|
d06f57a5dc | ||
|
|
07be48d342 | ||
|
|
5a5c7702f5 | ||
|
|
7e1a43143d | ||
|
|
5ff9137745 | ||
|
|
8e30d4a94f | ||
|
|
4b2ebb2e1c | ||
|
|
8c79ffd723 | ||
|
|
839c79405f | ||
|
|
d645adfd37 | ||
|
|
1b1551c6e3 | ||
|
|
78638dc291 | ||
|
|
e972924143 | ||
|
|
8871d8727b | ||
|
|
55945a1a2c | ||
|
|
d5e4044fb2 | ||
|
|
4ee458c509 | ||
|
|
a76ec0dad8 | ||
|
|
f2b247e85c | ||
|
|
3aed09a8c3 | ||
|
|
17b7a023ba | ||
|
|
10cb36829f | ||
|
|
90a55bb995 | ||
|
|
08a37d8971 | ||
|
|
89983f9f47 | ||
|
|
b71a80e383 | ||
|
|
2bf146816b | ||
|
|
daf9888da4 | ||
|
|
b42cee7a2f | ||
|
|
9ecb199608 | ||
|
|
adccef5699 | ||
|
|
94230f8ec6 | ||
|
|
a161661c6b | ||
|
|
08b971cd9a | ||
|
|
b64c8f132b | ||
|
|
d6181591c5 | ||
|
|
e093cb1741 | ||
|
|
a63a92c423 | ||
|
|
2c4f065626 | ||
|
|
ae5d4dd2a6 | ||
|
|
e574883e5f | ||
|
|
1bf60c49c7 | ||
|
|
ddc173cf84 | ||
|
|
2c71c7d102 | ||
|
|
f6cb08bdaa | ||
|
|
05728de21b | ||
|
|
b51027f641 | ||
|
|
ea4fdf8290 | ||
|
|
55796b4e39 | ||
|
|
e534c3138d | ||
|
|
e185abd223 | ||
|
|
3b73f72866 | ||
|
|
0db2f47133 | ||
|
|
80c7ec0fac | ||
|
|
780ce363de | ||
|
|
eaa8929457 | ||
|
|
ad0f62a5ac | ||
|
|
31f21ce013 | ||
|
|
7a20ce2f79 | ||
|
|
b98e7a97ec | ||
|
|
0bb50b3371 | ||
|
|
a03091b28c | ||
|
|
6c33984b8c | ||
|
|
28a4be0631 | ||
|
|
b8a3be7a62 | ||
|
|
259d4028f3 | ||
|
|
fb1251afc1 | ||
|
|
06e3fd428a | ||
|
|
a48e52f1f4 | ||
|
|
f8a345d8de | ||
|
|
d900c2f122 | ||
|
|
443a714549 | ||
|
|
74618a8a2b | ||
|
|
a25bd4c556 | ||
|
|
efe205ae70 | ||
|
|
a0d7473b1f | ||
|
|
7821781f20 | ||
|
|
b90d4ddf8a | ||
|
|
314562c167 | ||
|
|
5f0a9a7ce2 | ||
|
|
98e27841ad | ||
|
|
1b6467728c | ||
|
|
4bb349b2df | ||
|
|
b262336f08 | ||
|
|
2b59087461 | ||
|
|
66c9805efc | ||
|
|
710128901a | ||
|
|
61be0f7ac4 | ||
|
|
7289b3a0ad | ||
|
|
25243e2053 | ||
|
|
060a448cd5 | ||
|
|
bdb6078df6 | ||
|
|
749a4d0e81 | ||
|
|
1749705694 | ||
|
|
eec736be4d | ||
|
|
ffdf53941a | ||
|
|
5676e952f3 | ||
|
|
e049ee6260 | ||
|
|
f1355c9d2a | ||
|
|
d696e0fdc1 | ||
|
|
c8d2f284fd | ||
|
|
aa8ecd4f60 | ||
|
|
2323fe9bc0 | ||
|
|
b9a0b16fc8 | ||
|
|
b9c340afbf | ||
|
|
ee98d7128b | ||
|
|
d37febe306 | ||
|
|
5de97f05b3 | ||
|
|
662447bc69 | ||
|
|
51c1c46287 | ||
|
|
3b03d9798b | ||
|
|
17fbef810c | ||
|
|
e798975a9f | ||
|
|
2af65e322b | ||
|
|
23f09f9b4d | ||
|
|
d69668a488 | ||
|
|
5353888965 | ||
|
|
b33fd1908a | ||
|
|
97583ffcba | ||
|
|
360eca620e | ||
|
|
6d0682e821 | ||
|
|
834bdf37c3 | ||
|
|
e3d31f69bf | ||
|
|
ac55415de1 | ||
|
|
4d91be5be6 | ||
|
|
ee467cb155 | ||
|
|
6281f2360c | ||
|
|
cd8af6ae06 | ||
|
|
45cc199d7f | ||
|
|
b04469649a | ||
|
|
ee57dbdcc5 | ||
|
|
bca744f6fc | ||
|
|
442408dacf | ||
|
|
7297f0a082 | ||
|
|
f0d90941fb | ||
|
|
830962c044 | ||
|
|
eeec71dc65 | ||
|
|
9bd87a22f3 | ||
|
|
88c9f22a15 | ||
|
|
7380fc60d5 | ||
|
|
a178b99d73 | ||
|
|
af9ee04aaf | ||
|
|
01dc98f1f8 | ||
|
|
cbd07246bd | ||
|
|
5bb23ca738 | ||
|
|
0886ba7698 | ||
|
|
a65cffa58b | ||
|
|
02e2d432dd | ||
|
|
c8d23cab40 | ||
|
|
244ced83bc | ||
|
|
5c9c3d7934 | ||
|
|
4ca6713675 | ||
|
|
3d18404fd6 | ||
|
|
98dda26bf8 | ||
|
|
cd4c5ecd83 | ||
|
|
fdc0017ccb | ||
|
|
20a5c12bbb | ||
|
|
4eacec125e | ||
|
|
34322ba6d1 | ||
|
|
8210151a7b | ||
|
|
1e00088608 | ||
|
|
ee59b8002a | ||
|
|
b2ca5d0fba | ||
|
|
470604e567 | ||
|
|
f5f665ec0a | ||
|
|
5e865a4e33 | ||
|
|
38ad33b604 | ||
|
|
e1b4146171 | ||
|
|
65151f4b0a | ||
|
|
942706fb63 | ||
|
|
5f4492d4b7 | ||
|
|
ace1f62a40 | ||
|
|
706c88c7d5 | ||
|
|
4512fb16eb | ||
|
|
e446ff12e7 | ||
|
|
4318646abe | ||
|
|
2cbfe4f0e7 | ||
|
|
b7910c4665 | ||
|
|
fc1ba24834 | ||
|
|
cf16937160 | ||
|
|
c3393abed6 | ||
|
|
2abff3e21b | ||
|
|
e39a94c5e2 | ||
|
|
a6fff7f7a3 | ||
|
|
e584081b41 | ||
|
|
b91dde8084 | ||
|
|
077478d654 | ||
|
|
2be97cc1a0 | ||
|
|
0f7dc949a4 | ||
|
|
c7e91cc9eb | ||
|
|
6eacfab9c2 | ||
|
|
e36a408275 | ||
|
|
abda5b7d06 | ||
|
|
f815f71dd7 | ||
|
|
fa2c5b420c | ||
|
|
4c19bc76a7 | ||
|
|
d08a317920 | ||
|
|
bd805836cd | ||
|
|
e804a8f2f7 | ||
|
|
8bf876d446 | ||
|
|
f2521f663e | ||
|
|
e676bff453 | ||
|
|
8f2cc72d3c | ||
|
|
ec22656bee | ||
|
|
4acf8ba2ac | ||
|
|
d45a18904e | ||
|
|
9fc2dbabd4 | ||
|
|
b83b81f52e | ||
|
|
d1e2db993e | ||
|
|
ab931901e2 | ||
|
|
9879a25f9b | ||
|
|
16fb8eb092 | ||
|
|
de859927ed | ||
|
|
7bde59f664 | ||
|
|
be9668c7b8 | ||
|
|
95fe3189d5 | ||
|
|
9c60857c6a | ||
|
|
3b7cea9ee3 | ||
|
|
3f081e5021 | ||
|
|
d959420d6e | ||
|
|
79d81b92e6 | ||
|
|
940c60f23d | ||
|
|
965ab8151e | ||
|
|
87d55b31ca | ||
|
|
bb68575aca | ||
|
|
e561e804be | ||
|
|
5a8c3aa9d3 | ||
|
|
f84639debd | ||
|
|
de77a2b613 | ||
|
|
3825c3769f | ||
|
|
876908e922 | ||
|
|
25ecde8948 | ||
|
|
2de0334e3b | ||
|
|
1e32338d23 | ||
|
|
21f487321a | ||
|
|
750af31996 | ||
|
|
dc1aaee75d | ||
|
|
2122fa59d7 | ||
|
|
5b7c65adad | ||
|
|
fabf4535a8 | ||
|
|
04566080e0 | ||
|
|
0f9d9cdb21 | ||
|
|
11538d1789 | ||
|
|
49bae6cc6c | ||
|
|
9644ba0c8d | ||
|
|
ac2df3cb2e | ||
|
|
aad1f911c8 | ||
|
|
27b50bf4ae | ||
|
|
2d22837e32 | ||
|
|
866f2071ce | ||
|
|
9cb35eac29 | ||
|
|
6ff63d9d56 | ||
|
|
43539df25b | ||
|
|
cbafc24670 | ||
|
|
cc83a18b18 | ||
|
|
5ae931da92 | ||
|
|
853b999a7d | ||
|
|
505622f3dc | ||
|
|
0a5b41e3e0 | ||
|
|
8630c47b26 | ||
|
|
6105fd6e3d | ||
|
|
d7b1c4e4fe | ||
|
|
6ccafaa771 | ||
|
|
6b15e469a2 | ||
|
|
5675e29df3 | ||
|
|
52d094a7c7 | ||
|
|
633ed68f92 | ||
|
|
388594e29a | ||
|
|
af148fef27 | ||
|
|
983e55bd1d | ||
|
|
69dab99bf7 | ||
|
|
c94f459ff9 | ||
|
|
cd3baf411b | ||
|
|
042911371c | ||
|
|
8e2fb72309 | ||
|
|
77aacccdad | ||
|
|
5413c867e3 | ||
|
|
4fc7eb084e | ||
|
|
a9b04312d8 | ||
|
|
4cd9b66653 | ||
|
|
9b7b41597f | ||
|
|
e56ff8fdf2 | ||
|
|
8d9299aed7 | ||
|
|
f8506cb75b | ||
|
|
c90ee9e892 | ||
|
|
4d21b84882 | ||
|
|
331f0bdf97 | ||
|
|
0a4898697d | ||
|
|
79ae08fc9a | ||
|
|
1795773af9 | ||
|
|
747a781ad8 | ||
|
|
fcfcb1c3d1 | ||
|
|
d412ae8cce | ||
|
|
d834b76d42 | ||
|
|
3f9da8940f | ||
|
|
26d5f69af2 | ||
|
|
6efe4a3fd6 | ||
|
|
2b4ab4a322 | ||
|
|
cca42d1f89 | ||
|
|
724fef87b1 | ||
|
|
d50c0e4cd5 | ||
|
|
c18c037642 | ||
|
|
cda1da5fd0 | ||
|
|
eba1aa3a37 | ||
|
|
81018bb615 | ||
|
|
5fa0ff7b5c | ||
|
|
cabe286ebb | ||
|
|
e337da088b | ||
|
|
de97ea9e75 | ||
|
|
cc331065eb | ||
|
|
9a8e630654 | ||
|
|
17ab977efb | ||
|
|
6bd10d9451 | ||
|
|
5313b9b69c | ||
|
|
8dcfdcc44a | ||
|
|
67422df3ff | ||
|
|
2ad7536eb7 | ||
|
|
6f56e5c4e6 | ||
|
|
5ae8ebe590 | ||
|
|
6ecb97e4e5 | ||
|
|
c8f938dd3e | ||
|
|
0d29e29162 | ||
|
|
be1b3dffce | ||
|
|
3ba2dbe415 | ||
|
|
19a96c92a9 | ||
|
|
c265e3e437 | ||
|
|
7434ac2648 | ||
|
|
63d73a73aa | ||
|
|
00f9258a4d | ||
|
|
7087b43d39 | ||
|
|
945a78b7b1 | ||
|
|
55acc19ab8 | ||
|
|
410f0be05e | ||
|
|
2d7c091071 | ||
|
|
9bf58a54ce | ||
|
|
231adeea44 | ||
|
|
0ea618af39 | ||
|
|
f66297ff99 | ||
|
|
3113bf2e1f | ||
|
|
8c114dac02 | ||
|
|
06dba79272 | ||
|
|
8441986ca7 | ||
|
|
c8b50908e1 | ||
|
|
7e11fde892 | ||
|
|
3d9f3bd7a8 | ||
|
|
46e11649b0 | ||
|
|
161ef7590c | ||
|
|
ca660a3c74 | ||
|
|
7a24872331 | ||
|
|
eb4ea9fb3a | ||
|
|
64228c02ff | ||
|
|
f184a5d948 | ||
|
|
0bf5b2d6f7 | ||
|
|
74a52ea386 | ||
|
|
9914eef5b9 | ||
|
|
f547f741f2 | ||
|
|
c26149fa42 | ||
|
|
b2aa3648eb | ||
|
|
b997604ab2 | ||
|
|
c6929b82ad | ||
|
|
b40da53aef | ||
|
|
1e5dfd97e1 | ||
|
|
97bcc22abd | ||
|
|
bd655839cb | ||
|
|
a53a4e8e1d | ||
|
|
eb01646747 | ||
|
|
d27eee0fae | ||
|
|
92ee241ed8 | ||
|
|
660906e4c5 | ||
|
|
971f0d3446 | ||
|
|
63eccede7f | ||
|
|
5bd6adb488 | ||
|
|
fbdc5e0f39 | ||
|
|
5e994322fe | ||
|
|
fd0a594b56 | ||
|
|
d7b93ba0a0 | ||
|
|
c14fb5ad9d | ||
|
|
4d82dfdf9e | ||
|
|
7d3f08bed0 | ||
|
|
60597616ec | ||
|
|
6d758a988c | ||
|
|
503506dfd4 |
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
dist_electron
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-2023 The MindMap Team
|
||||
|
||||
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.
|
||||
1205
README.zh-Hans.md
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 254 KiB |
70
index.html
@@ -1 +1,69 @@
|
||||
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>一个简单的web思维导图实现</title><link href="dist/js/chunk-2d20ec02.917aff76.js" rel="prefetch"><link href="dist/js/chunk-2d216b67.2d06497a.js" rel="prefetch"><link href="dist/js/chunk-3a2f3e67.13278516.js" rel="prefetch"><link href="dist/css/app.92b546b0.css" rel="preload" as="style"><link href="dist/css/chunk-vendors.94891485.css" rel="preload" as="style"><link href="dist/js/app.aca24f03.js" rel="preload" as="script"><link href="dist/js/chunk-vendors.6cac1a4d.js" rel="preload" as="script"><link href="dist/css/chunk-vendors.94891485.css" rel="stylesheet"><link href="dist/css/app.92b546b0.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="dist/js/chunk-vendors.6cac1a4d.js"></script><script src="dist/js/app.aca24f03.js"></script></body></html>
|
||||
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"><link rel="icon" href="dist/logo.ico"><title>思绪思维导图</title><script>// 自定义静态资源的路径
|
||||
window.externalPublicPath = './dist/'
|
||||
// 接管应用
|
||||
window.takeOverApp = false</script><link href="dist/css/chunk-vendors.css?7820598bfce2a2653e5c" rel="stylesheet"><link href="dist/css/app.css?7820598bfce2a2653e5c" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
mindMapData: {
|
||||
root:{
|
||||
"data": {
|
||||
"text": "根节点"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
theme:{
|
||||
"template":"avocado",
|
||||
"config":{}
|
||||
},
|
||||
layout:"logicalStructure",
|
||||
config: {},
|
||||
view: null,
|
||||
},
|
||||
lang: 'zh',
|
||||
localConfig: null
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
}
|
||||
const setTakeOverAppMethods = (data) => {
|
||||
window.takeOverAppMethods = {}
|
||||
// 获取思维导图数据的函数
|
||||
window.takeOverAppMethods.getMindMapData = () => {
|
||||
return data.mindMapData
|
||||
}
|
||||
// 保存思维导图数据的函数
|
||||
window.takeOverAppMethods.saveMindMapData = (data) => {
|
||||
console.log(data)
|
||||
}
|
||||
// 获取语言的函数
|
||||
window.takeOverAppMethods.getLanguage = () => {
|
||||
return data.lang
|
||||
}
|
||||
// 保存语言的函数
|
||||
window.takeOverAppMethods.saveLanguage = (lang) => {
|
||||
console.log(lang)
|
||||
}
|
||||
// 获取本地配置的函数
|
||||
window.takeOverAppMethods.getLocalConfig = () => {
|
||||
return data.localConfig
|
||||
}
|
||||
// 保存本地配置的函数
|
||||
window.takeOverAppMethods.saveLocalConfig = (config) => {
|
||||
console.log(config)
|
||||
}
|
||||
}
|
||||
window.onload = async () => {
|
||||
if (!window.takeOverApp) return
|
||||
// 请求数据
|
||||
const data = await getDataFromBackend()
|
||||
// 设置全局的方法
|
||||
setTakeOverAppMethods(data)
|
||||
// 思维导图实例创建完成事件
|
||||
window.$bus.$on('app_inited', (mindMap) => {
|
||||
console.log(mindMap)
|
||||
})
|
||||
// 可以通过window.$bus.$on()来监听应用的一些事件
|
||||
// 实例化页面
|
||||
window.initApp()
|
||||
}</script><script src="dist/js/chunk-vendors.js?7820598bfce2a2653e5c"></script><script src="dist/js/app.js?7820598bfce2a2653e5c"></script></body></html>
|
||||
152
simple-mind-map/bin/wsServer.mjs
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import ws from 'ws'
|
||||
import http from 'http'
|
||||
import * as map from 'lib0/map'
|
||||
|
||||
const wsReadyStateConnecting = 0
|
||||
const wsReadyStateOpen = 1
|
||||
const wsReadyStateClosing = 2 // eslint-disable-line
|
||||
const wsReadyStateClosed = 3 // eslint-disable-line
|
||||
|
||||
const pingTimeout = 30000
|
||||
|
||||
const port = process.env.PORT || 4444
|
||||
// @ts-ignore
|
||||
const wss = new ws.Server({ noServer: true })
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
response.writeHead(200, { 'Content-Type': 'text/plain' })
|
||||
response.end('okay')
|
||||
})
|
||||
|
||||
/**
|
||||
* Map froms topic-name to set of subscribed clients.
|
||||
* @type {Map<string, Set<any>>}
|
||||
*/
|
||||
const topics = new Map()
|
||||
|
||||
/**
|
||||
* @param {any} conn
|
||||
* @param {object} message
|
||||
*/
|
||||
const send = (conn, message) => {
|
||||
if (
|
||||
conn.readyState !== wsReadyStateConnecting &&
|
||||
conn.readyState !== wsReadyStateOpen
|
||||
) {
|
||||
conn.close()
|
||||
}
|
||||
try {
|
||||
conn.send(JSON.stringify(message))
|
||||
} catch (e) {
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a new client
|
||||
* @param {any} conn
|
||||
*/
|
||||
const onconnection = conn => {
|
||||
/**
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
const subscribedTopics = new Set()
|
||||
let closed = false
|
||||
// Check if connection is still alive
|
||||
let pongReceived = true
|
||||
const pingInterval = setInterval(() => {
|
||||
if (!pongReceived) {
|
||||
conn.close()
|
||||
clearInterval(pingInterval)
|
||||
} else {
|
||||
pongReceived = false
|
||||
try {
|
||||
conn.ping()
|
||||
} catch (e) {
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
}, pingTimeout)
|
||||
conn.on('pong', () => {
|
||||
pongReceived = true
|
||||
})
|
||||
conn.on('close', () => {
|
||||
subscribedTopics.forEach(topicName => {
|
||||
const subs = topics.get(topicName) || new Set()
|
||||
subs.delete(conn)
|
||||
if (subs.size === 0) {
|
||||
topics.delete(topicName)
|
||||
}
|
||||
})
|
||||
subscribedTopics.clear()
|
||||
closed = true
|
||||
})
|
||||
conn.on(
|
||||
'message',
|
||||
/** @param {object} message */ message => {
|
||||
if (typeof message === 'string') {
|
||||
message = JSON.parse(message)
|
||||
}
|
||||
if (message && message.type && !closed) {
|
||||
switch (message.type) {
|
||||
case 'subscribe':
|
||||
/** @type {Array<string>} */ ;(message.topics || []).forEach(
|
||||
topicName => {
|
||||
if (typeof topicName === 'string') {
|
||||
// add conn to topic
|
||||
const topic = map.setIfUndefined(
|
||||
topics,
|
||||
topicName,
|
||||
() => new Set()
|
||||
)
|
||||
topic.add(conn)
|
||||
// add topic to conn
|
||||
subscribedTopics.add(topicName)
|
||||
}
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'unsubscribe':
|
||||
/** @type {Array<string>} */ ;(message.topics || []).forEach(
|
||||
topicName => {
|
||||
const subs = topics.get(topicName)
|
||||
if (subs) {
|
||||
subs.delete(conn)
|
||||
}
|
||||
}
|
||||
)
|
||||
break
|
||||
case 'publish':
|
||||
if (message.topic) {
|
||||
const receivers = topics.get(message.topic)
|
||||
if (receivers) {
|
||||
message.clients = receivers.size
|
||||
receivers.forEach(receiver => send(receiver, message))
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'ping':
|
||||
send(conn, { type: 'pong' })
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
wss.on('connection', onconnection)
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
// You may check auth of request here..
|
||||
/**
|
||||
* @param {any} ws
|
||||
*/
|
||||
const handleAuth = ws => {
|
||||
wss.emit('connection', ws, request)
|
||||
}
|
||||
wss.handleUpgrade(request, socket, head, handleAuth)
|
||||
})
|
||||
|
||||
server.listen(port)
|
||||
|
||||
console.log('Signaling server running on localhost:', port)
|
||||
BIN
simple-mind-map/dist/img/blueSky.3c7f8ccb.jpg
vendored
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 25 KiB |
BIN
simple-mind-map/dist/img/classic.733f273c.jpg
vendored
|
Before Width: | Height: | Size: 23 KiB |
BIN
simple-mind-map/dist/img/classic2.cdfe2a8d.jpg
vendored
|
Before Width: | Height: | Size: 10 KiB |
BIN
simple-mind-map/dist/img/classic3.19d6c347.jpg
vendored
|
Before Width: | Height: | Size: 11 KiB |
BIN
simple-mind-map/dist/img/classic4.087902fc.jpg
vendored
|
Before Width: | Height: | Size: 18 KiB |
BIN
simple-mind-map/dist/img/classicBlue.4b8243c6.jpg
vendored
|
Before Width: | Height: | Size: 8.4 KiB |
BIN
simple-mind-map/dist/img/classicGreen.c2ae7bde.jpg
vendored
|
Before Width: | Height: | Size: 8.7 KiB |
BIN
simple-mind-map/dist/img/dark.894c1d36.jpg
vendored
|
Before Width: | Height: | Size: 9.5 KiB |
BIN
simple-mind-map/dist/img/dark2.c49dc11c.jpg
vendored
|
Before Width: | Height: | Size: 7.6 KiB |
BIN
simple-mind-map/dist/img/default.1312a3ba.jpg
vendored
|
Before Width: | Height: | Size: 9.7 KiB |
BIN
simple-mind-map/dist/img/earthYellow.c35e546d.jpg
vendored
|
Before Width: | Height: | Size: 9.1 KiB |
BIN
simple-mind-map/dist/img/freshGreen.0e344e3e.jpg
vendored
|
Before Width: | Height: | Size: 9.5 KiB |
BIN
simple-mind-map/dist/img/freshRed.1c5bde77.jpg
vendored
|
Before Width: | Height: | Size: 9.2 KiB |
BIN
simple-mind-map/dist/img/gold.3093b3c8.jpg
vendored
|
Before Width: | Height: | Size: 7.2 KiB |
BIN
simple-mind-map/dist/img/greenLeaf.6789e8fc.jpg
vendored
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 21 KiB |
BIN
simple-mind-map/dist/img/mindMap.223b38aa.jpg
vendored
|
Before Width: | Height: | Size: 25 KiB |
BIN
simple-mind-map/dist/img/minions.c2a93f9e.jpg
vendored
|
Before Width: | Height: | Size: 8.3 KiB |
BIN
simple-mind-map/dist/img/mint.7933f60a.jpg
vendored
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 26 KiB |
BIN
simple-mind-map/dist/img/pinkGrape.32c2587b.jpg
vendored
|
Before Width: | Height: | Size: 6.9 KiB |
BIN
simple-mind-map/dist/img/romanticPurple.7607e58a.jpg
vendored
|
Before Width: | Height: | Size: 9.1 KiB |
BIN
simple-mind-map/dist/img/skyGreen.4cfa829a.jpg
vendored
|
Before Width: | Height: | Size: 7.0 KiB |
BIN
simple-mind-map/dist/img/vitalityOrange.5dd9014f.jpg
vendored
|
Before Width: | Height: | Size: 7.2 KiB |
@@ -900,6 +900,17 @@ const data5 = {
|
||||
}
|
||||
}
|
||||
|
||||
// 富文本数据v0.4.0+,需要使用RichText插件才支持富文本编辑
|
||||
const richTextData = {
|
||||
"root": {
|
||||
"data": {
|
||||
"text": "<a href='http://lxqnsys.com/' target='_blank'>理想去年实验室</a>",
|
||||
"richText": true
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
}
|
||||
|
||||
const rootData = {
|
||||
"root": {
|
||||
"data": {
|
||||
@@ -925,5 +936,6 @@ export default {
|
||||
"layout": "logicalStructure",
|
||||
// "layout": "mindMap",
|
||||
// "layout": "catalogOrganization"
|
||||
// "layout": "organizationStructure"
|
||||
// "layout": "organizationStructure",
|
||||
"config": {}
|
||||
}
|
||||
@@ -31,6 +31,12 @@
|
||||
"isActive": false
|
||||
},
|
||||
"children": []
|
||||
}, {
|
||||
"data": {
|
||||
"text": "<a href='http://lxqnsys.com/' target='_blank'>理想去年实验室</a>",
|
||||
"richText": true
|
||||
},
|
||||
"children": []
|
||||
}]
|
||||
}]
|
||||
},
|
||||
@@ -62,5 +68,6 @@
|
||||
"sx": 0,
|
||||
"sy": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {}
|
||||
}
|
||||
49
simple-mind-map/full.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import MindMap from './index'
|
||||
import MiniMap from './src/plugins/MiniMap.js'
|
||||
import Watermark from './src/plugins/Watermark.js'
|
||||
import KeyboardNavigation from './src/plugins/KeyboardNavigation.js'
|
||||
import ExportXMind from './src/plugins/ExportXMind.js'
|
||||
import ExportPDF from './src/plugins/ExportPDF.js'
|
||||
import Export from './src/plugins/Export.js'
|
||||
import Drag from './src/plugins/Drag.js'
|
||||
import Select from './src/plugins/Select.js'
|
||||
import AssociativeLine from './src/plugins/AssociativeLine'
|
||||
import RichText from './src/plugins/RichText'
|
||||
import NodeImgAdjust from './src/plugins/NodeImgAdjust.js'
|
||||
import TouchEvent from './src/plugins/TouchEvent.js'
|
||||
import Search from './src/plugins/Search.js'
|
||||
import Painter from './src/plugins/Painter.js'
|
||||
import Scrollbar from './src/plugins/Scrollbar.js'
|
||||
import Formula from './src/plugins/Formula.js'
|
||||
import xmind from './src/parse/xmind.js'
|
||||
import markdown from './src/parse/markdown.js'
|
||||
import icons from './src/svg/icons.js'
|
||||
import * as constants from './src/constants/constant.js'
|
||||
import themes from './src/themes/index.js'
|
||||
import * as defaultTheme from './src/themes/default.js'
|
||||
|
||||
MindMap.xmind = xmind
|
||||
MindMap.markdown = markdown
|
||||
MindMap.iconList = icons.nodeIconList
|
||||
MindMap.constants = constants
|
||||
MindMap.themes = themes
|
||||
MindMap.defaultTheme = defaultTheme
|
||||
|
||||
MindMap.usePlugin(MiniMap)
|
||||
.usePlugin(Watermark)
|
||||
.usePlugin(Drag)
|
||||
.usePlugin(KeyboardNavigation)
|
||||
.usePlugin(ExportXMind)
|
||||
.usePlugin(ExportPDF)
|
||||
.usePlugin(Export)
|
||||
.usePlugin(Select)
|
||||
.usePlugin(AssociativeLine)
|
||||
.usePlugin(RichText)
|
||||
.usePlugin(TouchEvent)
|
||||
.usePlugin(NodeImgAdjust)
|
||||
.usePlugin(Search)
|
||||
.usePlugin(Painter)
|
||||
.usePlugin(Scrollbar)
|
||||
.usePlugin(Formula)
|
||||
|
||||
export default MindMap
|
||||
@@ -1,83 +1,57 @@
|
||||
import View from './src/View'
|
||||
import Event from './src/Event'
|
||||
import Render from './src/Render'
|
||||
import View from './src/core/view/View'
|
||||
import Event from './src/core/event/Event'
|
||||
import Render from './src/core/render/Render'
|
||||
import merge from 'deepmerge'
|
||||
import theme from './src/themes'
|
||||
import Style from './src/Style'
|
||||
import KeyCommand from './src/KeyCommand'
|
||||
import Command from './src/Command'
|
||||
import BatchExecution from './src/BatchExecution'
|
||||
import Export from './src/Export'
|
||||
import Select from './src/Select'
|
||||
import Drag from './src/Drag'
|
||||
import MiniMap from './src/MiniMap'
|
||||
import { layoutValueList } from './src/utils/constant'
|
||||
import Style from './src/core/render/node/Style'
|
||||
import KeyCommand from './src/core/command/KeyCommand'
|
||||
import Command from './src/core/command/Command'
|
||||
import BatchExecution from './src/utils/BatchExecution'
|
||||
import {
|
||||
layoutValueList,
|
||||
CONSTANTS,
|
||||
commonCaches,
|
||||
ERROR_TYPES,
|
||||
cssContent
|
||||
} from './src/constants/constant'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import xmind from './src/parse/xmind'
|
||||
import { simpleDeepClone } from './src/utils'
|
||||
import KeyboardNavigation from './src/KeyboardNavigation'
|
||||
|
||||
// 默认选项配置
|
||||
const defaultOpt = {
|
||||
// 是否只读
|
||||
readonly: false,
|
||||
// 布局
|
||||
layout: 'logicalStructure',
|
||||
// 主题
|
||||
theme: 'default', // 内置主题:default(默认主题)
|
||||
// 主题配置,会和所选择的主题进行合并
|
||||
themeConfig: {},
|
||||
// 放大缩小的增量比例
|
||||
scaleRatio: 0.1,
|
||||
// 最多显示几个标签
|
||||
maxTag: 5,
|
||||
// 导出图片时的内边距
|
||||
exportPadding: 20,
|
||||
// 展开收缩按钮尺寸
|
||||
expandBtnSize: 20,
|
||||
// 节点里图片和文字的间距
|
||||
imgTextMargin: 5,
|
||||
// 节点里各种文字信息的间距,如图标和文字的间距
|
||||
textContentMargin: 2,
|
||||
// 多选节点时鼠标移动到边缘时的画布移动偏移量
|
||||
selectTranslateStep: 3,
|
||||
// 多选节点时鼠标移动距边缘多少距离时开始偏移
|
||||
selectTranslateLimit: 20,
|
||||
// 自定义节点备注内容显示
|
||||
customNoteContentShow: null
|
||||
/*
|
||||
{
|
||||
show(){},
|
||||
hide(){}
|
||||
}
|
||||
*/
|
||||
}
|
||||
import { simpleDeepClone, getType, getObjectChangedProps } from './src/utils'
|
||||
import defaultTheme, {
|
||||
checkIsNodeSizeIndependenceConfig
|
||||
} from './src/themes/default'
|
||||
import { defaultOpt } from './src/constants/defaultOptions'
|
||||
|
||||
// 思维导图
|
||||
class MindMap {
|
||||
// 构造函数
|
||||
/**
|
||||
*
|
||||
* @param {defaultOpt} opt
|
||||
*/
|
||||
constructor(opt = {}) {
|
||||
// 合并选项
|
||||
this.opt = this.handleOpt(merge(defaultOpt, opt))
|
||||
|
||||
// 容器元素
|
||||
this.el = this.opt.el
|
||||
this.elRect = this.el.getBoundingClientRect()
|
||||
if (!this.el) throw new Error('缺少容器元素el')
|
||||
|
||||
// 画布宽高
|
||||
this.width = this.elRect.width
|
||||
this.height = this.elRect.height
|
||||
// 获取容器尺寸位置信息
|
||||
this.getElRectInfo()
|
||||
|
||||
// 添加css
|
||||
this.cssEl = null
|
||||
this.addCss()
|
||||
|
||||
// 画布
|
||||
this.svg = SVG().addTo(this.el).size(this.width, this.height)
|
||||
this.draw = this.svg.group()
|
||||
|
||||
// 节点id
|
||||
this.uid = 0
|
||||
this.initContainer()
|
||||
|
||||
// 初始化主题
|
||||
this.initTheme()
|
||||
|
||||
// 初始化缓存数据
|
||||
this.initCache()
|
||||
|
||||
// 事件类
|
||||
this.event = new Event({
|
||||
mindMap: this
|
||||
@@ -100,40 +74,19 @@ class MindMap {
|
||||
|
||||
// 视图操作类
|
||||
this.view = new View({
|
||||
mindMap: this,
|
||||
draw: this.draw
|
||||
})
|
||||
|
||||
// 小地图类
|
||||
this.miniMap = new MiniMap({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 导出类
|
||||
this.doExport = new Export({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 选择类
|
||||
this.select = new Select({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 拖动类
|
||||
this.drag = new Drag({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 键盘导航类
|
||||
this.keyboardNavigation = new KeyboardNavigation({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 批量执行类
|
||||
this.batchExecution = new BatchExecution()
|
||||
|
||||
// 注册插件
|
||||
MindMap.pluginList.forEach(plugin => {
|
||||
this.initPlugin(plugin)
|
||||
})
|
||||
|
||||
// 初始渲染
|
||||
this.reRender()
|
||||
this.render(this.opt.fit ? () => this.view.fit() : () => {})
|
||||
setTimeout(() => {
|
||||
this.command.addHistory()
|
||||
}, 0)
|
||||
@@ -141,40 +94,103 @@ class MindMap {
|
||||
|
||||
// 配置参数处理
|
||||
handleOpt(opt) {
|
||||
// 深拷贝一份节点数据
|
||||
opt.data = simpleDeepClone(opt.data || {})
|
||||
// 检查布局配置
|
||||
if (!layoutValueList.includes(opt.layout)) {
|
||||
opt.layout = 'logicalStructure'
|
||||
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
}
|
||||
// 检查主题配置
|
||||
opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default'
|
||||
return opt
|
||||
}
|
||||
|
||||
// 创建容器元素
|
||||
initContainer() {
|
||||
const { associativeLineIsAlwaysAboveNode } = this.opt
|
||||
// 节点关联线容器
|
||||
const createAssociativeLineDraw = () => {
|
||||
this.associativeLineDraw = this.draw.group()
|
||||
this.associativeLineDraw.addClass('smm-associative-line-container')
|
||||
}
|
||||
// 画布
|
||||
this.svg = SVG().addTo(this.el).size(this.width, this.height)
|
||||
// 容器
|
||||
this.draw = this.svg.group()
|
||||
this.draw.addClass('smm-container')
|
||||
// 节点连线容器
|
||||
this.lineDraw = this.draw.group()
|
||||
this.lineDraw.addClass('smm-line-container')
|
||||
// 默认处于节点下方
|
||||
if (!associativeLineIsAlwaysAboveNode) {
|
||||
createAssociativeLineDraw()
|
||||
}
|
||||
// 节点容器
|
||||
this.nodeDraw = this.draw.group()
|
||||
this.nodeDraw.addClass('smm-node-container')
|
||||
// 关联线始终处于节点上方
|
||||
if (associativeLineIsAlwaysAboveNode) {
|
||||
createAssociativeLineDraw()
|
||||
}
|
||||
// 其他内容的容器
|
||||
this.otherDraw = this.draw.group()
|
||||
this.otherDraw.addClass('smm-other-container')
|
||||
}
|
||||
|
||||
// 清空各容器
|
||||
clearDraw() {
|
||||
this.lineDraw.clear()
|
||||
this.associativeLineDraw.clear()
|
||||
this.nodeDraw.clear()
|
||||
this.otherDraw.clear()
|
||||
}
|
||||
|
||||
// 添加必要的css样式到页面
|
||||
addCss() {
|
||||
this.cssEl = document.createElement('style')
|
||||
this.cssEl.type = 'text/css'
|
||||
this.cssEl.innerHTML = cssContent
|
||||
document.head.appendChild(this.cssEl)
|
||||
}
|
||||
|
||||
// 移除css
|
||||
removeCss() {
|
||||
document.head.removeChild(this.cssEl)
|
||||
}
|
||||
|
||||
// 渲染,部分渲染
|
||||
render() {
|
||||
render(callback, source = '') {
|
||||
this.batchExecution.push('render', () => {
|
||||
this.initTheme()
|
||||
this.renderer.reRender = false
|
||||
this.renderer.render()
|
||||
this.renderer.render(callback, source)
|
||||
})
|
||||
}
|
||||
|
||||
// 重新渲染
|
||||
reRender() {
|
||||
reRender(callback, source = '') {
|
||||
this.batchExecution.push('render', () => {
|
||||
this.draw.clear()
|
||||
this.clearDraw()
|
||||
this.initTheme()
|
||||
this.renderer.reRender = true
|
||||
this.renderer.render()
|
||||
this.renderer.render(callback, source)
|
||||
})
|
||||
}
|
||||
|
||||
// 获取或更新容器尺寸位置信息
|
||||
getElRectInfo() {
|
||||
this.elRect = this.el.getBoundingClientRect()
|
||||
this.width = this.elRect.width
|
||||
this.height = this.elRect.height
|
||||
if (this.width <= 0 || this.height <= 0)
|
||||
throw new Error('容器元素el的宽高不能为0')
|
||||
}
|
||||
|
||||
// 容器尺寸变化,调整尺寸
|
||||
resize() {
|
||||
this.elRect = this.el.getBoundingClientRect()
|
||||
this.width = this.elRect.width
|
||||
this.height = this.elRect.height
|
||||
this.getElRectInfo()
|
||||
this.svg.size(this.width, this.height)
|
||||
this.emit('resize')
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
@@ -192,6 +208,23 @@ class MindMap {
|
||||
this.event.off(event, fn)
|
||||
}
|
||||
|
||||
// 初始化缓存数据
|
||||
initCache() {
|
||||
Object.keys(commonCaches).forEach(key => {
|
||||
let type = getType(commonCaches[key])
|
||||
let value = ''
|
||||
switch (type) {
|
||||
case 'Boolean':
|
||||
value = false
|
||||
break
|
||||
default:
|
||||
value = null
|
||||
break
|
||||
}
|
||||
commonCaches[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
// 设置主题
|
||||
initTheme() {
|
||||
// 合并主题配置
|
||||
@@ -201,10 +234,13 @@ class MindMap {
|
||||
}
|
||||
|
||||
// 设置主题
|
||||
setTheme(theme) {
|
||||
this.renderer.clearAllActive()
|
||||
setTheme(theme, notRender = false) {
|
||||
this.execCommand('CLEAR_ACTIVE_NODE')
|
||||
this.opt.theme = theme
|
||||
this.reRender()
|
||||
if (!notRender) {
|
||||
this.render(null, CONSTANTS.CHANGE_THEME)
|
||||
}
|
||||
this.emit('view_theme_change', theme)
|
||||
}
|
||||
|
||||
// 获取当前主题
|
||||
@@ -213,9 +249,15 @@ class MindMap {
|
||||
}
|
||||
|
||||
// 设置主题配置
|
||||
setThemeConfig(config) {
|
||||
setThemeConfig(config, notRender = false) {
|
||||
// 计算改变了的配置
|
||||
const changedConfig = getObjectChangedProps(this.themeConfig, config)
|
||||
this.opt.themeConfig = config
|
||||
this.reRender()
|
||||
if (!notRender) {
|
||||
// 检查改变的是否是节点大小无关的主题属性
|
||||
let res = checkIsNodeSizeIndependenceConfig(changedConfig)
|
||||
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取自定义主题配置
|
||||
@@ -228,20 +270,33 @@ class MindMap {
|
||||
return prop === undefined ? this.themeConfig : this.themeConfig[prop]
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
getConfig(prop) {
|
||||
return prop === undefined ? this.opt : this.opt[prop]
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
updateConfig(opt = {}) {
|
||||
this.opt = this.handleOpt(merge.all([defaultOpt, this.opt, opt]))
|
||||
}
|
||||
|
||||
// 获取当前布局结构
|
||||
getLayout() {
|
||||
return this.opt.layout
|
||||
}
|
||||
|
||||
// 设置布局结构
|
||||
setLayout(layout) {
|
||||
setLayout(layout, notRender = false) {
|
||||
// 检查布局配置
|
||||
if (!layoutValueList.includes(layout)) {
|
||||
layout = 'logicalStructure'
|
||||
layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
}
|
||||
this.opt.layout = layout
|
||||
this.view.reset()
|
||||
this.renderer.setLayout()
|
||||
this.render()
|
||||
if (!notRender) {
|
||||
this.render(null, CONSTANTS.CHANGE_LAYOUT)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
@@ -251,10 +306,13 @@ class MindMap {
|
||||
|
||||
// 动态设置思维导图数据,纯节点数据
|
||||
setData(data) {
|
||||
data = simpleDeepClone(data || {})
|
||||
this.execCommand('CLEAR_ACTIVE_NODE')
|
||||
this.command.clearHistory()
|
||||
this.renderer.renderTree = data
|
||||
this.reRender()
|
||||
this.command.addHistory()
|
||||
this.renderer.setData(data)
|
||||
this.reRender(() => {}, CONSTANTS.SET_DATA)
|
||||
this.emit('set_data', data)
|
||||
}
|
||||
|
||||
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
|
||||
@@ -300,8 +358,12 @@ class MindMap {
|
||||
|
||||
// 导出
|
||||
async export(...args) {
|
||||
let result = await this.doExport.export(...args)
|
||||
return result
|
||||
try {
|
||||
let result = await this.doExport.export(...args)
|
||||
return result
|
||||
} catch (error) {
|
||||
this.opt.errorHandler(ERROR_TYPES.EXPORT_ERROR, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换位置
|
||||
@@ -314,18 +376,162 @@ class MindMap {
|
||||
|
||||
// 设置只读模式、编辑模式
|
||||
setMode(mode) {
|
||||
if (!['readonly', 'edit'].includes(mode)) {
|
||||
if (![CONSTANTS.MODE.READONLY, CONSTANTS.MODE.EDIT].includes(mode)) {
|
||||
return
|
||||
}
|
||||
this.opt.readonly = mode === 'readonly'
|
||||
this.opt.readonly = mode === CONSTANTS.MODE.READONLY
|
||||
if (this.opt.readonly) {
|
||||
// 取消当前激活的元素
|
||||
this.renderer.clearAllActive()
|
||||
this.execCommand('CLEAR_ACTIVE_NODE')
|
||||
}
|
||||
this.emit('mode_change', mode)
|
||||
}
|
||||
|
||||
// 获取svg数据
|
||||
getSvgData({ paddingX = 0, paddingY = 0, ignoreWatermark = false } = {}) {
|
||||
const svg = this.svg
|
||||
const draw = this.draw
|
||||
// 保存原始信息
|
||||
const origWidth = svg.width()
|
||||
const origHeight = svg.height()
|
||||
const origTransform = draw.transform()
|
||||
const elRect = this.elRect
|
||||
// 去除放大缩小的变换效果
|
||||
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
|
||||
// 获取变换后的位置尺寸信息,其实是getBoundingClientRect方法的包装方法
|
||||
const rect = draw.rbox()
|
||||
// 内边距
|
||||
rect.width += paddingX * 2
|
||||
rect.height += paddingY * 2
|
||||
draw.translate(paddingX, paddingY)
|
||||
// 将svg设置为实际内容的宽高
|
||||
svg.size(rect.width, rect.height)
|
||||
// 把实际内容变换
|
||||
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
|
||||
// 克隆一份数据
|
||||
let clone = svg.clone()
|
||||
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
|
||||
if (
|
||||
!ignoreWatermark &&
|
||||
(rect.width > origWidth || rect.height > origHeight) &&
|
||||
this.watermark &&
|
||||
this.watermark.hasWatermark()
|
||||
) {
|
||||
this.width = rect.width
|
||||
this.height = rect.height
|
||||
this.watermark.draw()
|
||||
clone = svg.clone()
|
||||
this.width = origWidth
|
||||
this.height = origHeight
|
||||
this.watermark.draw()
|
||||
}
|
||||
// 添加必要的样式
|
||||
clone.add(SVG(`<style>${cssContent}</style>`))
|
||||
// 修正关联线箭头marker的id
|
||||
const markerList = svg.find('marker')
|
||||
if (markerList && markerList.length > 0) {
|
||||
const id = markerList[0].attr('id')
|
||||
clone.find('marker').forEach(item => {
|
||||
item.attr('id', id)
|
||||
})
|
||||
}
|
||||
// 恢复原先的大小和变换信息
|
||||
svg.size(origWidth, origHeight)
|
||||
draw.transform(origTransform)
|
||||
|
||||
return {
|
||||
svg: clone, // 思维导图图形的整体svg元素,包括:svg(画布容器)、g(实际的思维导图组)
|
||||
svgHTML: clone.svg(), // svg字符串
|
||||
rect: {
|
||||
...rect, // 思维导图图形未缩放时的位置尺寸等信息
|
||||
ratio: rect.width / rect.height // 思维导图图形的宽高比
|
||||
},
|
||||
origWidth, // 画布宽度
|
||||
origHeight, // 画布高度
|
||||
scaleX: origTransform.scaleX, // 思维导图图形的水平缩放值
|
||||
scaleY: origTransform.scaleY // 思维导图图形的垂直缩放值
|
||||
}
|
||||
}
|
||||
|
||||
// 添加插件
|
||||
addPlugin(plugin, opt) {
|
||||
let index = MindMap.hasPlugin(plugin)
|
||||
if (index === -1) {
|
||||
MindMap.usePlugin(plugin, opt)
|
||||
this.initPlugin(plugin)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除插件
|
||||
removePlugin(plugin) {
|
||||
let index = MindMap.hasPlugin(plugin)
|
||||
if (index !== -1) {
|
||||
MindMap.pluginList.splice(index, 1)
|
||||
if (this[plugin.instanceName]) {
|
||||
if (this[plugin.instanceName].beforePluginRemove) {
|
||||
this[plugin.instanceName].beforePluginRemove()
|
||||
}
|
||||
delete this[plugin.instanceName]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 实例化插件
|
||||
initPlugin(plugin) {
|
||||
this[plugin.instanceName] = new plugin({
|
||||
mindMap: this,
|
||||
pluginOpt: plugin.pluginOpt
|
||||
})
|
||||
}
|
||||
|
||||
// 销毁
|
||||
destroy() {
|
||||
this.emit('beforeDestroy')
|
||||
// 清除节点编辑框
|
||||
this.renderer.textEdit.hideEditTextBox()
|
||||
// 清除关联线文字编辑框
|
||||
if (this.associativeLine) {
|
||||
this.associativeLine.hideEditTextBox()
|
||||
}
|
||||
// 移除插件
|
||||
;[...MindMap.pluginList].forEach(plugin => {
|
||||
if (this[plugin.instanceName].beforePluginDestroy) {
|
||||
this[plugin.instanceName].beforePluginDestroy()
|
||||
}
|
||||
this[plugin.instanceName] = null
|
||||
})
|
||||
// 解绑事件
|
||||
this.event.unbind()
|
||||
// 移除画布节点
|
||||
this.svg.remove()
|
||||
// 去除给容器元素设置的背景样式
|
||||
Style.removeBackgroundStyle(this.el)
|
||||
this.el.innerHTML = ''
|
||||
this.el = null
|
||||
this.removeCss()
|
||||
}
|
||||
}
|
||||
|
||||
MindMap.xmind = xmind
|
||||
// 插件列表
|
||||
MindMap.pluginList = []
|
||||
MindMap.usePlugin = (plugin, opt = {}) => {
|
||||
if (MindMap.hasPlugin(plugin) !== -1) return MindMap
|
||||
plugin.pluginOpt = opt
|
||||
MindMap.pluginList.push(plugin)
|
||||
return MindMap
|
||||
}
|
||||
MindMap.hasPlugin = plugin => {
|
||||
return MindMap.pluginList.findIndex(item => {
|
||||
return item === plugin
|
||||
})
|
||||
}
|
||||
|
||||
// 定义新主题
|
||||
MindMap.defineTheme = (name, config = {}) => {
|
||||
if (theme[name]) {
|
||||
return new Error('该主题名称已存在')
|
||||
}
|
||||
theme[name] = merge(defaultTheme, config)
|
||||
}
|
||||
|
||||
export default MindMap
|
||||
|
||||
2119
simple-mind-map/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.2.21",
|
||||
"version": "0.9.1",
|
||||
"description": "一个简单的web在线思维导图",
|
||||
"authors": [
|
||||
{
|
||||
@@ -12,6 +12,8 @@
|
||||
"url": "http://lxqnsys.com/"
|
||||
}
|
||||
],
|
||||
"types": "./types/index.d.ts",
|
||||
"typings": "./types/index.d.ts",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -19,18 +21,26 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/",
|
||||
"format": "prettier --write ."
|
||||
"format": "prettier --write .",
|
||||
"types": "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir types --target es2017 --skipLibCheck",
|
||||
"wsServe": "node ./bin/wsServer.mjs"
|
||||
},
|
||||
"module": "index.js",
|
||||
"main": "./dist/simpleMindMap.umd.min.js",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.0.16",
|
||||
"canvg": "^3.0.7",
|
||||
"deepmerge": "^1.5.2",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"jspdf": "^2.5.1",
|
||||
"jszip": "^3.10.1",
|
||||
"xml-js": "^1.6.11"
|
||||
"katex": "^0.16.8",
|
||||
"mdast-util-from-markdown": "^1.3.0",
|
||||
"quill": "^1.3.6",
|
||||
"tern": "^0.24.3",
|
||||
"uuid": "^9.0.0",
|
||||
"xml-js": "^1.6.11",
|
||||
"y-webrtc": "^10.2.5",
|
||||
"yjs": "^13.6.8"
|
||||
},
|
||||
"keywords": [
|
||||
"javascript",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 将/** */类型的注释转换为//类型
|
||||
// 遍历所有js文件
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
@@ -11,13 +11,26 @@ const transform = dir => {
|
||||
if (fs.statSync(file).isDirectory()) {
|
||||
transform(file)
|
||||
} else if (/\.js$/.test(file)) {
|
||||
rewriteComments(file)
|
||||
transformFile(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const rewriteComments = file => {
|
||||
const transformFile = file => {
|
||||
console.log(file)
|
||||
let content = fs.readFileSync(file, 'utf-8')
|
||||
countCodeLines(content)
|
||||
// transformComments(file, content)
|
||||
}
|
||||
|
||||
// 统计代码行数
|
||||
let totalLines = 0
|
||||
const countCodeLines = content => {
|
||||
totalLines += content.split(/\n/).length
|
||||
}
|
||||
|
||||
// 转换注释类型
|
||||
const transformComments = (file, content) => {
|
||||
console.log('当前转换文件:', file)
|
||||
content = content.replace(/\/\*\*[^/]+\*\//g, str => {
|
||||
let res = /@Desc:([^\n]+)\n/g.exec(str)
|
||||
@@ -29,4 +42,5 @@ const rewriteComments = file => {
|
||||
}
|
||||
|
||||
transform(entryPath)
|
||||
rewriteComments(path.join(__dirname, '../index.js'))
|
||||
transformFile(path.join(__dirname, '../index.js'))
|
||||
console.log(totalLines)
|
||||
@@ -1,65 +0,0 @@
|
||||
// 在下一个事件循环里执行任务
|
||||
const nextTick = function (fn, ctx) {
|
||||
let pending = false
|
||||
let timerFunc = null
|
||||
let handle = () => {
|
||||
pending = false
|
||||
ctx ? fn.call(ctx) : fn()
|
||||
}
|
||||
// 支持MutationObserver接口的话使用MutationObserver
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
let counter = 1
|
||||
let observer = new MutationObserver(handle)
|
||||
let textNode = document.createTextNode(counter)
|
||||
observer.observe(textNode, {
|
||||
characterData: true // 设为 true 表示监视指定目标节点或子节点树中节点所包含的字符数据的变化
|
||||
})
|
||||
timerFunc = function () {
|
||||
counter = (counter + 1) % 2 // counter会在0和1两者循环变化
|
||||
textNode.data = counter // 节点变化会触发回调handle,
|
||||
}
|
||||
} else {
|
||||
// 否则使用定时器
|
||||
timerFunc = setTimeout
|
||||
}
|
||||
return function () {
|
||||
if (pending) return
|
||||
pending = true
|
||||
timerFunc(handle, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量执行
|
||||
class BatchExecution {
|
||||
// 构造函数
|
||||
constructor() {
|
||||
this.has = {}
|
||||
this.queue = []
|
||||
this.nextTick = nextTick(this.flush, this)
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
push(name, fn) {
|
||||
if (this.has[name]) {
|
||||
return
|
||||
}
|
||||
this.has[name] = true
|
||||
this.queue.push({
|
||||
name,
|
||||
fn
|
||||
})
|
||||
this.nextTick()
|
||||
}
|
||||
|
||||
// 执行队列
|
||||
flush() {
|
||||
let fns = this.queue.slice(0)
|
||||
this.queue = []
|
||||
fns.forEach(({ name, fn }) => {
|
||||
this.has[name] = false
|
||||
fn()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default BatchExecution
|
||||
@@ -1,254 +0,0 @@
|
||||
import { bfsWalk, throttle } from './utils'
|
||||
import Base from './layouts/Base'
|
||||
|
||||
// 节点拖动类
|
||||
|
||||
class Drag extends Base {
|
||||
// 构造函数
|
||||
|
||||
constructor({ mindMap }) {
|
||||
super(mindMap.renderer)
|
||||
this.mindMap = mindMap
|
||||
this.reset()
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 复位
|
||||
|
||||
reset() {
|
||||
// 当前拖拽节点
|
||||
this.node = null
|
||||
// 当前重叠节点
|
||||
this.overlapNode = null
|
||||
// 当前上一个同级节点
|
||||
this.prevNode = null
|
||||
// 当前下一个同级节点
|
||||
this.nextNode = null
|
||||
// 画布的变换数据
|
||||
this.drawTransform = null
|
||||
// 克隆节点
|
||||
this.clone = null
|
||||
// 连接线
|
||||
this.line = null
|
||||
// 同级位置占位符
|
||||
this.placeholder = null
|
||||
// 鼠标按下位置和节点左上角的偏移量
|
||||
this.offsetX = 0
|
||||
this.offsetY = 0
|
||||
// 克隆节点左上角的坐标
|
||||
this.cloneNodeLeft = 0
|
||||
this.cloneNodeTop = 0
|
||||
// 当前鼠标是否按下
|
||||
this.isMousedown = false
|
||||
// 拖拽的鼠标位置变量
|
||||
this.mouseDownX = 0
|
||||
this.mouseDownY = 0
|
||||
this.mouseMoveX = 0
|
||||
this.mouseMoveY = 0
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
|
||||
bindEvent() {
|
||||
this.checkOverlapNode = throttle(this.checkOverlapNode, 300, this)
|
||||
this.mindMap.on('node_mousedown', (node, e) => {
|
||||
if (this.mindMap.opt.readonly || node.isGeneralization) {
|
||||
return
|
||||
}
|
||||
if (e.which !== 1 || node.isRoot) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
// 计算鼠标按下的位置距离节点左上角的距离
|
||||
this.drawTransform = this.mindMap.draw.transform()
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.offsetX = x - (node.left * scaleX + translateX)
|
||||
this.offsetY = y - (node.top * scaleY + translateY)
|
||||
this.node = node
|
||||
this.isMousedown = true
|
||||
this.mouseDownX = x
|
||||
this.mouseDownY = y
|
||||
})
|
||||
this.mindMap.on('mousemove', e => {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseMoveX = x
|
||||
this.mouseMoveY = y
|
||||
if (
|
||||
Math.abs(x - this.mouseDownX) <= 10 &&
|
||||
Math.abs(y - this.mouseDownY) <= 10 &&
|
||||
!this.node.isDrag
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.clearAllActive()
|
||||
this.onMove(x, y)
|
||||
})
|
||||
this.onMouseup = this.onMouseup.bind(this)
|
||||
this.mindMap.on('node_mouseup', this.onMouseup)
|
||||
this.mindMap.on('mouseup', this.onMouseup)
|
||||
}
|
||||
|
||||
// 鼠标松开事件
|
||||
|
||||
onMouseup(e) {
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
this.isMousedown = false
|
||||
let _nodeIsDrag = this.node.isDrag
|
||||
this.node.isDrag = false
|
||||
this.node.show()
|
||||
this.removeCloneNode()
|
||||
// 存在重叠子节点,则移动作为其子节点
|
||||
if (this.overlapNode) {
|
||||
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
|
||||
this.mindMap.execCommand('MOVE_NODE_TO', this.node, this.overlapNode)
|
||||
} else if (this.prevNode) {
|
||||
// 存在前一个相邻节点,作为其下一个兄弟节点
|
||||
this.mindMap.renderer.setNodeActive(this.prevNode, false)
|
||||
this.mindMap.execCommand('INSERT_AFTER', this.node, this.prevNode)
|
||||
} else if (this.nextNode) {
|
||||
// 存在下一个相邻节点,作为其前一个兄弟节点
|
||||
this.mindMap.renderer.setNodeActive(this.nextNode, false)
|
||||
this.mindMap.execCommand('INSERT_BEFORE', this.node, this.nextNode)
|
||||
} else if (_nodeIsDrag) {
|
||||
// 自定义位置
|
||||
let { x, y } = this.mindMap.toPos(
|
||||
e.clientX - this.offsetX,
|
||||
e.clientY - this.offsetY
|
||||
)
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
x = (x - translateX) / scaleX
|
||||
y = (y - translateY) / scaleY
|
||||
this.node.left = x
|
||||
this.node.top = y
|
||||
this.node.customLeft = x
|
||||
this.node.customTop = y
|
||||
this.mindMap.execCommand('SET_NODE_CUSTOM_POSITION', this.node, x, y)
|
||||
this.mindMap.render()
|
||||
}
|
||||
this.reset()
|
||||
}
|
||||
|
||||
// 创建克隆节点
|
||||
|
||||
createCloneNode() {
|
||||
if (!this.clone) {
|
||||
// 节点
|
||||
this.clone = this.node.group.clone()
|
||||
this.clone.opacity(0.5)
|
||||
this.clone.css('z-index', 99999)
|
||||
this.node.isDrag = true
|
||||
this.node.hide()
|
||||
// 连接线
|
||||
this.line = this.draw.path()
|
||||
this.line.opacity(0.5)
|
||||
this.node.styleLine(this.line, this.node)
|
||||
// 同级位置占位符
|
||||
this.placeholder = this.draw.rect().fill({
|
||||
color: this.node.style.merge('lineColor', true)
|
||||
})
|
||||
this.mindMap.draw.add(this.clone)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除克隆节点
|
||||
|
||||
removeCloneNode() {
|
||||
if (!this.clone) {
|
||||
return
|
||||
}
|
||||
this.clone.remove()
|
||||
this.line.remove()
|
||||
this.placeholder.remove()
|
||||
}
|
||||
|
||||
// 拖动中
|
||||
|
||||
onMove(x, y) {
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
this.createCloneNode()
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
this.cloneNodeLeft = x - this.offsetX
|
||||
this.cloneNodeTop = y - this.offsetY
|
||||
x = (this.cloneNodeLeft - translateX) / scaleX
|
||||
y = (this.cloneNodeTop - translateY) / scaleY
|
||||
let t = this.clone.transform()
|
||||
this.clone.translate(x - t.translateX, y - t.translateY)
|
||||
// 连接线
|
||||
let parent = this.node.parent
|
||||
this.line.plot(
|
||||
this.quadraticCurvePath(
|
||||
parent.left + parent.width / 2,
|
||||
parent.top + parent.height / 2,
|
||||
x + this.node.width / 2,
|
||||
y + this.node.height / 2
|
||||
)
|
||||
)
|
||||
this.checkOverlapNode()
|
||||
}
|
||||
|
||||
// 检测重叠节点
|
||||
|
||||
checkOverlapNode() {
|
||||
if (!this.drawTransform) {
|
||||
return
|
||||
}
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
let checkRight = this.cloneNodeLeft + this.node.width * scaleX
|
||||
let checkBottom = this.cloneNodeTop + this.node.height * scaleX
|
||||
this.overlapNode = null
|
||||
this.prevNode = null
|
||||
this.nextNode = null
|
||||
this.placeholder.size(0, 0)
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
if (node.nodeData.data.isActive) {
|
||||
this.mindMap.renderer.setNodeActive(node, false)
|
||||
}
|
||||
if (node === this.node || this.node.isParent(node)) {
|
||||
return
|
||||
}
|
||||
if (this.overlapNode || (this.prevNode && this.nextNode)) {
|
||||
return
|
||||
}
|
||||
let { left, top, width, height } = node
|
||||
let _left = left
|
||||
let _top = top
|
||||
let _bottom = top + height
|
||||
let right = (left + width) * scaleX + translateX
|
||||
let bottom = (top + height) * scaleY + translateY
|
||||
left = left * scaleX + translateX
|
||||
top = top * scaleY + translateY
|
||||
// 检测是否重叠
|
||||
if (!this.overlapNode) {
|
||||
if (
|
||||
left <= checkRight &&
|
||||
right >= this.cloneNodeLeft &&
|
||||
top <= checkBottom &&
|
||||
bottom >= this.cloneNodeTop
|
||||
) {
|
||||
this.overlapNode = node
|
||||
}
|
||||
}
|
||||
// 检测兄弟节点位置
|
||||
if (!this.prevNode && !this.nextNode && !node.isRoot) {
|
||||
// && this.node.isBrother(node)
|
||||
if (left <= checkRight && right >= this.cloneNodeLeft) {
|
||||
if (this.cloneNodeTop > bottom && this.cloneNodeTop <= bottom + 10) {
|
||||
this.prevNode = node
|
||||
this.placeholder.size(node.width, 10).move(_left, _bottom)
|
||||
} else if (checkBottom < top && checkBottom >= top - 10) {
|
||||
this.nextNode = node
|
||||
this.placeholder.size(node.width, 10).move(_left, _top - 10)
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import { imgToDataUrl, downloadFile } from './utils'
|
||||
import JsPDF from 'jspdf'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
const URL = window.URL || window.webkitURL || window
|
||||
|
||||
// 导出类
|
||||
class Export {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
this.mindMap = opt.mindMap
|
||||
this.exportPadding = this.mindMap.opt.exportPadding
|
||||
}
|
||||
|
||||
// 导出
|
||||
async export(type, isDownload = true, name = '思维导图', ...args) {
|
||||
if (this[type]) {
|
||||
let result = await this[type](name, ...args)
|
||||
if (isDownload && type !== 'pdf') {
|
||||
downloadFile(result, name + '.' + type)
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取svg数据
|
||||
async getSvgData() {
|
||||
let { svg, svgHTML } = this.mindMap.miniMap.getMiniMap()
|
||||
// 把图片的url转换成data:url类型,否则导出会丢失图片
|
||||
let imageList = svg.find('image')
|
||||
let task = imageList.map(async item => {
|
||||
let imgUlr = item.attr('href') || item.attr('xlink:href')
|
||||
let imgData = await imgToDataUrl(imgUlr)
|
||||
item.attr('href', imgData)
|
||||
})
|
||||
await Promise.all(task)
|
||||
return {
|
||||
node: svg,
|
||||
str: svgHTML
|
||||
}
|
||||
}
|
||||
|
||||
// svg转png
|
||||
svgToPng(svgSrc) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
|
||||
img.setAttribute('crossOrigin', 'anonymous')
|
||||
img.onload = async () => {
|
||||
try {
|
||||
let canvas = document.createElement('canvas')
|
||||
canvas.width = img.width + this.exportPadding * 2
|
||||
canvas.height = img.height + this.exportPadding * 2
|
||||
let ctx = canvas.getContext('2d')
|
||||
// 绘制背景
|
||||
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
|
||||
// 图片绘制到canvas里
|
||||
ctx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
img.width,
|
||||
img.height,
|
||||
this.exportPadding,
|
||||
this.exportPadding,
|
||||
img.width,
|
||||
img.height
|
||||
)
|
||||
resolve(canvas.toDataURL())
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
img.onerror = e => {
|
||||
reject(e)
|
||||
}
|
||||
img.src = svgSrc
|
||||
})
|
||||
}
|
||||
|
||||
// 在canvas上绘制思维导图背景
|
||||
drawBackgroundToCanvas(ctx, width, height) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let {
|
||||
backgroundColor = '#fff',
|
||||
backgroundImage,
|
||||
backgroundRepeat = 'repeat'
|
||||
} = this.mindMap.themeConfig
|
||||
// 背景颜色
|
||||
ctx.save()
|
||||
ctx.rect(0, 0, width, height)
|
||||
ctx.fillStyle = backgroundColor
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
// 背景图片
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
ctx.save()
|
||||
let img = new Image()
|
||||
img.src = backgroundImage
|
||||
img.onload = () => {
|
||||
let pat = ctx.createPattern(img, backgroundRepeat)
|
||||
ctx.rect(0, 0, width, height)
|
||||
ctx.fillStyle = pat
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
resolve()
|
||||
}
|
||||
img.onerror = e => {
|
||||
reject(e)
|
||||
}
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 导出为png
|
||||
/**
|
||||
* 方法1.把svg的图片都转化成data:url格式,再转换
|
||||
* 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换
|
||||
*/
|
||||
async png() {
|
||||
let { str } = await this.getSvgData()
|
||||
// 转换成blob数据
|
||||
let blob = new Blob([str], {
|
||||
type: 'image/svg+xml'
|
||||
})
|
||||
// 转换成data:url数据
|
||||
let svgUrl = URL.createObjectURL(blob)
|
||||
// 绘制到canvas上
|
||||
let imgDataUrl = await this.svgToPng(svgUrl)
|
||||
URL.revokeObjectURL(svgUrl)
|
||||
return imgDataUrl
|
||||
}
|
||||
|
||||
// 导出为pdf
|
||||
async pdf(name) {
|
||||
let img = await this.png()
|
||||
let pdf = new JsPDF('', 'pt', 'a4')
|
||||
let a4Width = 595
|
||||
let a4Height = 841
|
||||
let a4Ratio = a4Width / a4Height
|
||||
let image = new Image()
|
||||
image.onload = () => {
|
||||
let imageWidth = image.width
|
||||
let imageHeight = image.height
|
||||
let imageRatio = imageWidth / imageHeight
|
||||
let w, h
|
||||
if (imageWidth <= a4Width && imageHeight <= a4Height) {
|
||||
// 使用图片原始宽高
|
||||
w = imageWidth
|
||||
h = imageHeight
|
||||
} else if (a4Ratio > imageRatio) {
|
||||
// 以a4Height为高度,缩放图片宽度
|
||||
w = imageRatio * a4Height
|
||||
h = a4Height
|
||||
} else {
|
||||
// 以a4Width为宽度,缩放图片高度
|
||||
w = a4Width
|
||||
h = a4Width / imageRatio
|
||||
}
|
||||
pdf.addImage(img, 'PNG', (a4Width - w) / 2, (a4Height - h) / 2, w, h)
|
||||
pdf.save(name)
|
||||
}
|
||||
image.src = img
|
||||
}
|
||||
|
||||
// 在svg上绘制思维导图背景
|
||||
drawBackgroundToSvg(svg) {
|
||||
return new Promise(async resolve => {
|
||||
let {
|
||||
backgroundColor = '#fff',
|
||||
backgroundImage,
|
||||
backgroundRepeat = 'repeat'
|
||||
} = this.mindMap.themeConfig
|
||||
// 背景颜色
|
||||
svg.css('background-color', backgroundColor)
|
||||
// 背景图片
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
let imgDataUrl = await imgToDataUrl(backgroundImage)
|
||||
svg.css('background-image', `url(${imgDataUrl})`)
|
||||
svg.css('background-repeat', backgroundRepeat)
|
||||
resolve()
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 导出为svg
|
||||
async svg(name) {
|
||||
let { node } = await this.getSvgData()
|
||||
node.first().before(SVG(`<title>${name}</title>`))
|
||||
await this.drawBackgroundToSvg(node)
|
||||
let str = node.svg()
|
||||
// 转换成blob数据
|
||||
let blob = new Blob([str], {
|
||||
type: 'image/svg+xml'
|
||||
})
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
// 导出为json
|
||||
json(name, withConfig = true) {
|
||||
let data = this.mindMap.getData(withConfig)
|
||||
let str = JSON.stringify(data)
|
||||
let blob = new Blob([str])
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
// 专有文件,其实就是json文件
|
||||
smm(name, withConfig) {
|
||||
return this.json(name, withConfig)
|
||||
}
|
||||
}
|
||||
|
||||
export default Export
|
||||
@@ -1,893 +0,0 @@
|
||||
import merge from 'deepmerge'
|
||||
import LogicalStructure from './layouts/LogicalStructure'
|
||||
import MindMap from './layouts/MindMap'
|
||||
import CatalogOrganization from './layouts/CatalogOrganization'
|
||||
import OrganizationStructure from './layouts/OrganizationStructure'
|
||||
import TextEdit from './TextEdit'
|
||||
import { copyNodeTree, simpleDeepClone, walk } from './utils'
|
||||
import { shapeList } from './Shape'
|
||||
import { lineStyleProps } from './themes/default'
|
||||
|
||||
// 布局列表
|
||||
const layouts = {
|
||||
// 逻辑结构图
|
||||
logicalStructure: LogicalStructure,
|
||||
// 思维导图
|
||||
mindMap: MindMap,
|
||||
// 目录组织图
|
||||
catalogOrganization: CatalogOrganization,
|
||||
// 组织结构图
|
||||
organizationStructure: OrganizationStructure
|
||||
}
|
||||
|
||||
// 渲染
|
||||
|
||||
class Render {
|
||||
// 构造函数
|
||||
|
||||
constructor(opt = {}) {
|
||||
this.opt = opt
|
||||
this.mindMap = opt.mindMap
|
||||
this.themeConfig = this.mindMap.themeConfig
|
||||
this.draw = this.mindMap.draw
|
||||
// 渲染树,操作过程中修改的都是这里的数据
|
||||
this.renderTree = merge({}, this.mindMap.opt.data || {})
|
||||
// 是否重新渲染
|
||||
this.reRender = false
|
||||
// 当前激活的节点列表
|
||||
this.activeNodeList = []
|
||||
// 根节点
|
||||
this.root = null
|
||||
// 文本编辑框,需要再bindEvent之前实例化,否则单击事件只能触发隐藏文本编辑框,而无法保存文本修改
|
||||
this.textEdit = new TextEdit(this)
|
||||
// 布局
|
||||
this.setLayout()
|
||||
// 绑定事件
|
||||
this.bindEvent()
|
||||
// 注册命令
|
||||
this.registerCommands()
|
||||
// 注册快捷键
|
||||
this.registerShortcutKeys()
|
||||
}
|
||||
|
||||
// 设置布局结构
|
||||
|
||||
setLayout() {
|
||||
this.layout = new (
|
||||
layouts[this.mindMap.opt.layout]
|
||||
? layouts[this.mindMap.opt.layout]
|
||||
: layouts.logicalStructure
|
||||
)(this)
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
|
||||
bindEvent() {
|
||||
// 点击事件
|
||||
this.mindMap.on('draw_click', () => {
|
||||
// 清除激活状态
|
||||
if (this.activeNodeList.length > 0) {
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 注册命令
|
||||
|
||||
registerCommands() {
|
||||
// 全选
|
||||
this.selectAll = this.selectAll.bind(this)
|
||||
this.mindMap.command.add('SELECT_ALL', this.selectAll)
|
||||
// 回退
|
||||
this.back = this.back.bind(this)
|
||||
this.mindMap.command.add('BACK', this.back)
|
||||
// 前进
|
||||
this.forward = this.forward.bind(this)
|
||||
this.mindMap.command.add('FORWARD', this.forward)
|
||||
// 插入同级节点
|
||||
this.insertNode = this.insertNode.bind(this)
|
||||
this.mindMap.command.add('INSERT_NODE', this.insertNode)
|
||||
// 插入子节点
|
||||
this.insertChildNode = this.insertChildNode.bind(this)
|
||||
this.mindMap.command.add('INSERT_CHILD_NODE', this.insertChildNode)
|
||||
// 上移节点
|
||||
this.upNode = this.upNode.bind(this)
|
||||
this.mindMap.command.add('UP_NODE', this.upNode)
|
||||
// 下移节点
|
||||
this.downNode = this.downNode.bind(this)
|
||||
this.mindMap.command.add('DOWN_NODE', this.downNode)
|
||||
// 移动节点
|
||||
this.insertAfter = this.insertAfter.bind(this)
|
||||
this.mindMap.command.add('INSERT_AFTER', this.insertAfter)
|
||||
this.insertBefore = this.insertBefore.bind(this)
|
||||
this.mindMap.command.add('INSERT_BEFORE', this.insertBefore)
|
||||
this.moveNodeTo = this.moveNodeTo.bind(this)
|
||||
this.mindMap.command.add('MOVE_NODE_TO', this.moveNodeTo)
|
||||
// 删除节点
|
||||
this.removeNode = this.removeNode.bind(this)
|
||||
this.mindMap.command.add('REMOVE_NODE', this.removeNode)
|
||||
// 粘贴节点
|
||||
this.pasteNode = this.pasteNode.bind(this)
|
||||
this.mindMap.command.add('PASTE_NODE', this.pasteNode)
|
||||
// 剪切节点
|
||||
this.cutNode = this.cutNode.bind(this)
|
||||
this.mindMap.command.add('CUT_NODE', this.cutNode)
|
||||
// 修改节点样式
|
||||
this.setNodeStyle = this.setNodeStyle.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_STYLE', this.setNodeStyle)
|
||||
// 切换节点是否激活
|
||||
this.setNodeActive = this.setNodeActive.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_ACTIVE', this.setNodeActive)
|
||||
// 清除所有激活节点
|
||||
this.clearAllActive = this.clearAllActive.bind(this)
|
||||
this.mindMap.command.add('CLEAR_ACTIVE_NODE', this.clearAllActive)
|
||||
// 切换节点是否展开
|
||||
this.setNodeExpand = this.setNodeExpand.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_EXPAND', this.setNodeExpand)
|
||||
// 展开所有节点
|
||||
this.expandAllNode = this.expandAllNode.bind(this)
|
||||
this.mindMap.command.add('EXPAND_ALL', this.expandAllNode)
|
||||
// 收起所有节点
|
||||
this.unexpandAllNode = this.unexpandAllNode.bind(this)
|
||||
this.mindMap.command.add('UNEXPAND_ALL', this.unexpandAllNode)
|
||||
// 展开到指定层级
|
||||
this.expandToLevel = this.expandToLevel.bind(this)
|
||||
this.mindMap.command.add('UNEXPAND_TO_LEVEL', this.expandToLevel)
|
||||
// 设置节点数据
|
||||
this.setNodeData = this.setNodeData.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_DATA', this.setNodeData)
|
||||
// 设置节点文本
|
||||
this.setNodeText = this.setNodeText.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_TEXT', this.setNodeText)
|
||||
// 设置节点图片
|
||||
this.setNodeImage = this.setNodeImage.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_IMAGE', this.setNodeImage)
|
||||
// 设置节点图标
|
||||
this.setNodeIcon = this.setNodeIcon.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_ICON', this.setNodeIcon)
|
||||
// 设置节点超链接
|
||||
this.setNodeHyperlink = this.setNodeHyperlink.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_HYPERLINK', this.setNodeHyperlink)
|
||||
// 设置节点备注
|
||||
this.setNodeNote = this.setNodeNote.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_NOTE', this.setNodeNote)
|
||||
// 设置节点标签
|
||||
this.setNodeTag = this.setNodeTag.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_TAG', this.setNodeTag)
|
||||
// 添加节点概要
|
||||
this.addGeneralization = this.addGeneralization.bind(this)
|
||||
this.mindMap.command.add('ADD_GENERALIZATION', this.addGeneralization)
|
||||
// 删除节点概要
|
||||
this.removeGeneralization = this.removeGeneralization.bind(this)
|
||||
this.mindMap.command.add('REMOVE_GENERALIZATION', this.removeGeneralization)
|
||||
// 设置节点自定义位置
|
||||
this.setNodeCustomPosition = this.setNodeCustomPosition.bind(this)
|
||||
this.mindMap.command.add(
|
||||
'SET_NODE_CUSTOM_POSITION',
|
||||
this.setNodeCustomPosition
|
||||
)
|
||||
// 一键整理布局
|
||||
this.resetLayout = this.resetLayout.bind(this)
|
||||
this.mindMap.command.add('RESET_LAYOUT', this.resetLayout)
|
||||
// 设置节点形状
|
||||
this.setNodeShape = this.setNodeShape.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_SHAPE', this.setNodeShape)
|
||||
}
|
||||
|
||||
// 注册快捷键
|
||||
|
||||
registerShortcutKeys() {
|
||||
// 插入下级节点
|
||||
this.mindMap.keyCommand.addShortcut('Tab', () => {
|
||||
this.mindMap.execCommand('INSERT_CHILD_NODE')
|
||||
})
|
||||
// 插入同级节点
|
||||
this.insertNodeWrap = () => {
|
||||
if (this.textEdit.showTextEdit) {
|
||||
return
|
||||
}
|
||||
this.mindMap.execCommand('INSERT_NODE')
|
||||
}
|
||||
this.mindMap.keyCommand.addShortcut('Enter', this.insertNodeWrap)
|
||||
// 插入概要
|
||||
this.mindMap.keyCommand.addShortcut('Control+s', this.addGeneralization)
|
||||
// 展开/收起节点
|
||||
this.toggleActiveExpand = this.toggleActiveExpand.bind(this)
|
||||
this.mindMap.keyCommand.addShortcut('/', this.toggleActiveExpand)
|
||||
// 删除节点
|
||||
this.removeNodeWrap = () => {
|
||||
this.mindMap.execCommand('REMOVE_NODE')
|
||||
}
|
||||
this.mindMap.keyCommand.addShortcut('Del|Backspace', this.removeNodeWrap)
|
||||
// 节点编辑时某些快捷键会存在冲突,需要暂时去除
|
||||
this.mindMap.on('before_show_text_edit', () => {
|
||||
this.startTextEdit()
|
||||
})
|
||||
this.mindMap.on('hide_text_edit', () => {
|
||||
this.endTextEdit()
|
||||
})
|
||||
// 全选
|
||||
this.mindMap.keyCommand.addShortcut('Control+a', () => {
|
||||
this.mindMap.execCommand('SELECT_ALL')
|
||||
})
|
||||
// 一键整理布局
|
||||
this.mindMap.keyCommand.addShortcut('Control+l', this.resetLayout)
|
||||
// 上移节点
|
||||
this.mindMap.keyCommand.addShortcut('Control+Up', this.upNode)
|
||||
// 下移节点
|
||||
this.mindMap.keyCommand.addShortcut('Control+Down', this.downNode)
|
||||
// 复制节点、剪切节点、粘贴节点的快捷键需开发者自行注册实现,可参考demo
|
||||
}
|
||||
|
||||
// 开启文字编辑,会禁用回车键和删除键相关快捷键防止冲突
|
||||
|
||||
startTextEdit() {
|
||||
this.mindMap.keyCommand.save()
|
||||
// this.mindMap.keyCommand.removeShortcut('Del|Backspace')
|
||||
// this.mindMap.keyCommand.removeShortcut('/')
|
||||
// this.mindMap.keyCommand.removeShortcut('Enter', this.insertNodeWrap)
|
||||
}
|
||||
|
||||
// 结束文字编辑,会恢复回车键和删除键相关快捷键
|
||||
|
||||
endTextEdit() {
|
||||
this.mindMap.keyCommand.restore()
|
||||
// this.mindMap.keyCommand.addShortcut('Del|Backspace', this.removeNodeWrap)
|
||||
// this.mindMap.keyCommand.addShortcut('/', this.toggleActiveExpand)
|
||||
// this.mindMap.keyCommand.addShortcut('Enter', this.insertNodeWrap)
|
||||
}
|
||||
|
||||
// 渲染
|
||||
|
||||
render() {
|
||||
if (this.reRender) {
|
||||
this.clearActive()
|
||||
}
|
||||
this.layout.doLayout(root => {
|
||||
this.root = root
|
||||
this.root.render(() => {
|
||||
this.mindMap.emit('node_tree_render_end')
|
||||
})
|
||||
})
|
||||
this.mindMap.emit('node_active', null, this.activeNodeList)
|
||||
}
|
||||
|
||||
// 清除当前激活的节点
|
||||
|
||||
clearActive() {
|
||||
this.activeNodeList.forEach(item => {
|
||||
this.setNodeActive(item, false)
|
||||
})
|
||||
this.activeNodeList = []
|
||||
}
|
||||
|
||||
// 清除当前所有激活节点,并会触发事件
|
||||
|
||||
clearAllActive() {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
this.clearActive()
|
||||
this.mindMap.emit('node_active', null, [])
|
||||
}
|
||||
|
||||
// 添加节点到激活列表里
|
||||
|
||||
addActiveNode(node) {
|
||||
let index = this.findActiveNodeIndex(node)
|
||||
if (index === -1) {
|
||||
this.activeNodeList.push(node)
|
||||
}
|
||||
}
|
||||
|
||||
// 在激活列表里移除某个节点
|
||||
|
||||
removeActiveNode(node) {
|
||||
let index = this.findActiveNodeIndex(node)
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
this.activeNodeList.splice(index, 1)
|
||||
}
|
||||
|
||||
// 检索某个节点在激活列表里的索引
|
||||
|
||||
findActiveNodeIndex(node) {
|
||||
return this.activeNodeList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
}
|
||||
|
||||
// 获取节点在同级里的索引位置
|
||||
|
||||
getNodeIndex(node) {
|
||||
return node.parent
|
||||
? node.parent.children.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
: 0
|
||||
}
|
||||
|
||||
// 全选
|
||||
|
||||
selectAll() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
node => {
|
||||
if (!node.nodeData.data.isActive) {
|
||||
node.nodeData.data.isActive = true
|
||||
this.addActiveNode(node)
|
||||
setTimeout(() => {
|
||||
node.renderNode()
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 回退
|
||||
|
||||
back(step) {
|
||||
this.clearAllActive()
|
||||
let data = this.mindMap.command.back(step)
|
||||
if (data) {
|
||||
this.renderTree = data
|
||||
this.mindMap.reRender()
|
||||
}
|
||||
}
|
||||
|
||||
// 前进
|
||||
|
||||
forward(step) {
|
||||
this.clearAllActive()
|
||||
let data = this.mindMap.command.forward(step)
|
||||
if (data) {
|
||||
this.renderTree = data
|
||||
this.mindMap.reRender()
|
||||
}
|
||||
}
|
||||
|
||||
// 插入同级节点,多个节点只会操作第一个节点
|
||||
|
||||
insertNode() {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
let first = this.activeNodeList[0]
|
||||
if (first.isRoot) {
|
||||
this.insertChildNode()
|
||||
} else {
|
||||
let text = first.layerIndex === 1 ? '二级节点' : '分支主题'
|
||||
if (first.layerIndex === 1) {
|
||||
first.parent.initRender = true
|
||||
}
|
||||
let index = this.getNodeIndex(first)
|
||||
first.parent.nodeData.children.splice(index + 1, 0, {
|
||||
inserting: true,
|
||||
data: {
|
||||
text: text,
|
||||
expand: true
|
||||
},
|
||||
children: []
|
||||
})
|
||||
this.mindMap.render()
|
||||
}
|
||||
}
|
||||
|
||||
// 插入子节点
|
||||
|
||||
insertChildNode() {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
this.activeNodeList.forEach(node => {
|
||||
if (!node.nodeData.children) {
|
||||
node.nodeData.children = []
|
||||
}
|
||||
let text = node.isRoot ? '二级节点' : '分支主题'
|
||||
node.nodeData.children.push({
|
||||
inserting: true,
|
||||
data: {
|
||||
text: text,
|
||||
expand: true
|
||||
},
|
||||
children: []
|
||||
})
|
||||
// 插入子节点时自动展开子节点
|
||||
node.nodeData.data.expand = true
|
||||
if (node.isRoot) {
|
||||
node.initRender = true
|
||||
// this.mindMap.batchExecution.push('renderNode' + index, () => {
|
||||
// node.renderNode()
|
||||
// })
|
||||
}
|
||||
})
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 上移节点,多个节点只会操作第一个节点
|
||||
|
||||
upNode() {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
let node = this.activeNodeList[0]
|
||||
if (node.isRoot) {
|
||||
return
|
||||
}
|
||||
let parent = node.parent
|
||||
let childList = parent.children
|
||||
let index = childList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
if (index === -1 || index === 0) {
|
||||
return
|
||||
}
|
||||
let insertIndex = index - 1
|
||||
// 节点实例
|
||||
childList.splice(index, 1)
|
||||
childList.splice(insertIndex, 0, node)
|
||||
// 节点数据
|
||||
parent.nodeData.children.splice(index, 1)
|
||||
parent.nodeData.children.splice(insertIndex, 0, node.nodeData)
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 下移节点,多个节点只会操作第一个节点
|
||||
|
||||
downNode() {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
let node = this.activeNodeList[0]
|
||||
if (node.isRoot) {
|
||||
return
|
||||
}
|
||||
let parent = node.parent
|
||||
let childList = parent.children
|
||||
let index = childList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
if (index === -1 || index === childList.length - 1) {
|
||||
return
|
||||
}
|
||||
let insertIndex = index + 1
|
||||
// 节点实例
|
||||
childList.splice(index, 1)
|
||||
childList.splice(insertIndex, 0, node)
|
||||
// 节点数据
|
||||
parent.nodeData.children.splice(index, 1)
|
||||
parent.nodeData.children.splice(insertIndex, 0, node.nodeData)
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 将节点移动到另一个节点的前面
|
||||
|
||||
insertBefore(node, exist) {
|
||||
if (node.isRoot) {
|
||||
return
|
||||
}
|
||||
// 移动节点
|
||||
let nodeParent = node.parent
|
||||
let nodeBorthers = nodeParent.children
|
||||
let nodeIndex = nodeBorthers.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
if (nodeIndex === -1) {
|
||||
return
|
||||
}
|
||||
nodeBorthers.splice(nodeIndex, 1)
|
||||
nodeParent.nodeData.children.splice(nodeIndex, 1)
|
||||
|
||||
// 目标节点
|
||||
let existParent = exist.parent
|
||||
let existBorthers = existParent.children
|
||||
let existIndex = existBorthers.findIndex(item => {
|
||||
return item === exist
|
||||
})
|
||||
if (existIndex === -1) {
|
||||
return
|
||||
}
|
||||
existBorthers.splice(existIndex, 0, node)
|
||||
existParent.nodeData.children.splice(existIndex, 0, node.nodeData)
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 将节点移动到另一个节点的后面
|
||||
|
||||
insertAfter(node, exist) {
|
||||
if (node.isRoot) {
|
||||
return
|
||||
}
|
||||
// 移动节点
|
||||
let nodeParent = node.parent
|
||||
let nodeBorthers = nodeParent.children
|
||||
let nodeIndex = nodeBorthers.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
if (nodeIndex === -1) {
|
||||
return
|
||||
}
|
||||
nodeBorthers.splice(nodeIndex, 1)
|
||||
nodeParent.nodeData.children.splice(nodeIndex, 1)
|
||||
|
||||
// 目标节点
|
||||
let existParent = exist.parent
|
||||
let existBorthers = existParent.children
|
||||
let existIndex = existBorthers.findIndex(item => {
|
||||
return item === exist
|
||||
})
|
||||
if (existIndex === -1) {
|
||||
return
|
||||
}
|
||||
existIndex++
|
||||
existBorthers.splice(existIndex, 0, node)
|
||||
existParent.nodeData.children.splice(existIndex, 0, node.nodeData)
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 移除节点
|
||||
|
||||
removeNode() {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < this.activeNodeList.length; i++) {
|
||||
let node = this.activeNodeList[i]
|
||||
if (node.isGeneralization) {
|
||||
// 删除概要节点
|
||||
this.setNodeData(node.generalizationBelongNode, {
|
||||
generalization: null
|
||||
})
|
||||
node.generalizationBelongNode.update()
|
||||
this.removeActiveNode(node)
|
||||
i--
|
||||
} else if (node.isRoot) {
|
||||
node.children.forEach(child => {
|
||||
child.remove()
|
||||
})
|
||||
node.children = []
|
||||
node.nodeData.children = []
|
||||
break
|
||||
} else {
|
||||
this.removeActiveNode(node)
|
||||
this.removeOneNode(node)
|
||||
i--
|
||||
}
|
||||
}
|
||||
this.mindMap.emit('node_active', null, [])
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 移除某个指定节点
|
||||
|
||||
removeOneNode(node) {
|
||||
let index = this.getNodeIndex(node)
|
||||
node.remove()
|
||||
node.parent.children.splice(index, 1)
|
||||
node.parent.nodeData.children.splice(index, 1)
|
||||
}
|
||||
|
||||
// 复制节点,多个节点只会操作第一个节点
|
||||
|
||||
copyNode() {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
return copyNodeTree({}, this.activeNodeList[0], true)
|
||||
}
|
||||
|
||||
// 剪切节点,多个节点只会操作第一个节点
|
||||
|
||||
cutNode(callback) {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
let node = this.activeNodeList[0]
|
||||
if (node.isRoot) {
|
||||
return null
|
||||
}
|
||||
let copyData = copyNodeTree({}, node, true)
|
||||
this.removeActiveNode(node)
|
||||
this.removeOneNode(node)
|
||||
this.mindMap.emit('node_active', null, this.activeNodeList)
|
||||
this.mindMap.render()
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(copyData)
|
||||
}
|
||||
}
|
||||
|
||||
// 移动一个节点作为另一个节点的子节点
|
||||
|
||||
moveNodeTo(node, toNode) {
|
||||
if (node.isRoot) {
|
||||
return
|
||||
}
|
||||
let copyData = copyNodeTree({}, node)
|
||||
this.removeActiveNode(node)
|
||||
this.removeOneNode(node)
|
||||
this.mindMap.emit('node_active', null, this.activeNodeList)
|
||||
toNode.nodeData.children.push(copyData)
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 粘贴节点到节点
|
||||
|
||||
pasteNode(data) {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
this.activeNodeList.forEach(item => {
|
||||
item.nodeData.children.push(simpleDeepClone(data))
|
||||
})
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 设置节点样式
|
||||
|
||||
setNodeStyle(node, prop, value, isActive) {
|
||||
let data = {}
|
||||
if (isActive) {
|
||||
data = {
|
||||
activeStyle: {
|
||||
...(node.nodeData.data.activeStyle || {}),
|
||||
[prop]: value
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = {
|
||||
[prop]: value
|
||||
}
|
||||
}
|
||||
this.setNodeDataRender(node, data)
|
||||
// 更新了连线的样式
|
||||
if (lineStyleProps.includes(prop)) {
|
||||
;(node.parent || node).renderLine(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置节点是否激活
|
||||
|
||||
setNodeActive(node, active) {
|
||||
this.setNodeData(node, {
|
||||
isActive: active
|
||||
})
|
||||
node.renderNode()
|
||||
}
|
||||
|
||||
// 设置节点是否展开
|
||||
|
||||
setNodeExpand(node, expand) {
|
||||
this.setNodeData(node, {
|
||||
expand
|
||||
})
|
||||
if (expand) {
|
||||
// 展开
|
||||
node.children.forEach(item => {
|
||||
item.render()
|
||||
})
|
||||
node.renderLine()
|
||||
node.updateExpandBtnNode()
|
||||
} else {
|
||||
// 收缩
|
||||
node.children.forEach(item => {
|
||||
item.remove()
|
||||
})
|
||||
node.removeLine()
|
||||
node.updateExpandBtnNode()
|
||||
}
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 展开所有
|
||||
|
||||
expandAllNode() {
|
||||
walk(
|
||||
this.renderTree,
|
||||
null,
|
||||
node => {
|
||||
if (!node.data.expand) {
|
||||
node.data.expand = true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0,
|
||||
0
|
||||
)
|
||||
this.mindMap.reRender()
|
||||
}
|
||||
|
||||
// 收起所有
|
||||
|
||||
unexpandAllNode() {
|
||||
walk(
|
||||
this.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot) => {
|
||||
node._node = null
|
||||
if (!isRoot) {
|
||||
node.data.expand = false
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0,
|
||||
0
|
||||
)
|
||||
this.mindMap.reRender()
|
||||
}
|
||||
|
||||
// 展开到指定层级
|
||||
|
||||
expandToLevel(level) {
|
||||
walk(
|
||||
this.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
node._node = null
|
||||
node.data.expand = layerIndex < level
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0,
|
||||
0
|
||||
)
|
||||
this.mindMap.reRender()
|
||||
}
|
||||
|
||||
// 切换激活节点的展开状态
|
||||
|
||||
toggleActiveExpand() {
|
||||
this.activeNodeList.forEach(node => {
|
||||
if (node.nodeData.children.length <= 0) {
|
||||
return
|
||||
}
|
||||
this.toggleNodeExpand(node)
|
||||
})
|
||||
}
|
||||
|
||||
// 切换节点展开状态
|
||||
|
||||
toggleNodeExpand(node) {
|
||||
this.mindMap.execCommand(
|
||||
'SET_NODE_EXPAND',
|
||||
node,
|
||||
!node.nodeData.data.expand
|
||||
)
|
||||
}
|
||||
|
||||
// 设置节点文本
|
||||
|
||||
setNodeText(node, text) {
|
||||
this.setNodeDataRender(node, {
|
||||
text
|
||||
})
|
||||
}
|
||||
|
||||
// 设置节点图片
|
||||
|
||||
setNodeImage(node, { url, title, width, height }) {
|
||||
this.setNodeDataRender(node, {
|
||||
image: url,
|
||||
imageTitle: title || '',
|
||||
imageSize: {
|
||||
width,
|
||||
height
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 设置节点图标
|
||||
|
||||
setNodeIcon(node, icons) {
|
||||
this.setNodeDataRender(node, {
|
||||
icon: icons
|
||||
})
|
||||
}
|
||||
|
||||
// 设置节点超链接
|
||||
|
||||
setNodeHyperlink(node, link, title = '') {
|
||||
this.setNodeDataRender(node, {
|
||||
hyperlink: link,
|
||||
hyperlinkTitle: title
|
||||
})
|
||||
}
|
||||
|
||||
// 设置节点备注
|
||||
|
||||
setNodeNote(node, note) {
|
||||
this.setNodeDataRender(node, {
|
||||
note
|
||||
})
|
||||
}
|
||||
|
||||
// 设置节点标签
|
||||
|
||||
setNodeTag(node, tag) {
|
||||
this.setNodeDataRender(node, {
|
||||
tag
|
||||
})
|
||||
}
|
||||
|
||||
// 添加节点概要
|
||||
|
||||
addGeneralization(data) {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
this.activeNodeList.forEach(node => {
|
||||
if (node.nodeData.data.generalization || node.isRoot) {
|
||||
return
|
||||
}
|
||||
this.setNodeData(node, {
|
||||
generalization: data || {
|
||||
text: '概要'
|
||||
}
|
||||
})
|
||||
node.update()
|
||||
})
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 删除节点概要
|
||||
|
||||
removeGeneralization() {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
this.activeNodeList.forEach(node => {
|
||||
if (!node.nodeData.data.generalization) {
|
||||
return
|
||||
}
|
||||
this.setNodeData(node, {
|
||||
generalization: null
|
||||
})
|
||||
node.update()
|
||||
})
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 设置节点自定义位置
|
||||
|
||||
setNodeCustomPosition(node, left = undefined, top = undefined) {
|
||||
let nodeList = [node] || this.activeNodeList
|
||||
nodeList.forEach(item => {
|
||||
this.setNodeData(item, {
|
||||
customLeft: left,
|
||||
customTop: top
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 一键整理布局,即去除自定义位置
|
||||
|
||||
resetLayout() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
node => {
|
||||
node.customLeft = undefined
|
||||
node.customTop = undefined
|
||||
this.setNodeData(node, {
|
||||
customLeft: undefined,
|
||||
customTop: undefined
|
||||
})
|
||||
this.mindMap.render()
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 设置节点形状
|
||||
|
||||
setNodeShape(node, shape) {
|
||||
@@ -1,170 +0,0 @@
|
||||
import { bfsWalk, throttle } from './utils'
|
||||
|
||||
// 选择节点类
|
||||
|
||||
class Select {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
this.mindMap = mindMap
|
||||
this.rect = null
|
||||
this.isMousedown = false
|
||||
this.mouseDownX = 0
|
||||
this.mouseDownY = 0
|
||||
this.mouseMoveX = 0
|
||||
this.mouseMoveY = 0
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
this.checkInNodes = throttle(this.checkInNodes, 500, this)
|
||||
this.mindMap.on('mousedown', e => {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
if (!e.ctrlKey && e.which !== 3) {
|
||||
return
|
||||
}
|
||||
this.isMousedown = true
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseDownX = x
|
||||
this.mouseDownY = y
|
||||
this.createRect(x, y)
|
||||
})
|
||||
this.mindMap.on('mousemove', e => {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseMoveX = x
|
||||
this.mouseMoveY = y
|
||||
if (
|
||||
Math.abs(x - this.mouseDownX) <= 10 &&
|
||||
Math.abs(y - this.mouseDownY) <= 10
|
||||
) {
|
||||
return
|
||||
}
|
||||
clearTimeout(this.autoMoveTimer)
|
||||
this.onMove(x, y)
|
||||
})
|
||||
this.mindMap.on('mouseup', () => {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
this.mindMap.emit(
|
||||
'node_active',
|
||||
null,
|
||||
this.mindMap.renderer.activeNodeList
|
||||
)
|
||||
clearTimeout(this.autoMoveTimer)
|
||||
this.isMousedown = false
|
||||
if (this.rect) this.rect.remove()
|
||||
this.rect = null
|
||||
})
|
||||
}
|
||||
|
||||
// 鼠标移动事件
|
||||
onMove(x, y) {
|
||||
// 绘制矩形
|
||||
this.rect.plot([
|
||||
[this.mouseDownX, this.mouseDownY],
|
||||
[this.mouseMoveX, this.mouseDownY],
|
||||
[this.mouseMoveX, this.mouseMoveY],
|
||||
[this.mouseDownX, this.mouseMoveY]
|
||||
])
|
||||
this.checkInNodes()
|
||||
// 检测边缘移动
|
||||
let step = this.mindMap.opt.selectTranslateStep
|
||||
let limit = this.mindMap.opt.selectTranslateLimit
|
||||
let count = 0
|
||||
// 左边缘
|
||||
if (x <= this.mindMap.elRect.left + limit) {
|
||||
this.mouseDownX += step
|
||||
this.mindMap.view.translateX(step)
|
||||
count++
|
||||
}
|
||||
// 右边缘
|
||||
if (x >= this.mindMap.elRect.right - limit) {
|
||||
this.mouseDownX -= step
|
||||
this.mindMap.view.translateX(-step)
|
||||
count++
|
||||
}
|
||||
// 上边缘
|
||||
if (y <= this.mindMap.elRect.top + limit) {
|
||||
this.mouseDownY += step
|
||||
this.mindMap.view.translateY(step)
|
||||
count++
|
||||
}
|
||||
// 下边缘
|
||||
if (y >= this.mindMap.elRect.bottom - limit) {
|
||||
this.mouseDownY -= step
|
||||
this.mindMap.view.translateY(-step)
|
||||
count++
|
||||
}
|
||||
if (count > 0) {
|
||||
this.startAutoMove(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
// 开启自动移动
|
||||
startAutoMove(x, y) {
|
||||
this.autoMoveTimer = setTimeout(() => {
|
||||
this.onMove(x, y)
|
||||
}, 20)
|
||||
}
|
||||
|
||||
// 创建矩形
|
||||
createRect(x, y) {
|
||||
this.rect = this.mindMap.svg
|
||||
.polygon()
|
||||
.stroke({
|
||||
color: '#0984e3'
|
||||
})
|
||||
.fill({
|
||||
color: 'rgba(9,132,227,0.3)'
|
||||
})
|
||||
.plot([[x, y]])
|
||||
}
|
||||
|
||||
// 检测在选区里的节点
|
||||
checkInNodes() {
|
||||
let { scaleX, scaleY, translateX, translateY } =
|
||||
this.mindMap.draw.transform()
|
||||
let minx = Math.min(this.mouseDownX, this.mouseMoveX)
|
||||
let miny = Math.min(this.mouseDownY, this.mouseMoveY)
|
||||
let maxx = Math.max(this.mouseDownX, this.mouseMoveX)
|
||||
let maxy = Math.max(this.mouseDownY, this.mouseMoveY)
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
let { left, top, width, height } = node
|
||||
let right = (left + width) * scaleX + translateX
|
||||
let bottom = (top + height) * scaleY + translateY
|
||||
left = left * scaleX + translateX
|
||||
top = top * scaleY + translateY
|
||||
if (left >= minx && right <= maxx && top >= miny && bottom <= maxy) {
|
||||
this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
|
||||
if (node.nodeData.data.isActive) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.setNodeActive(node, true)
|
||||
this.mindMap.renderer.addActiveNode(node)
|
||||
})
|
||||
} else if (node.nodeData.data.isActive) {
|
||||
this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
|
||||
if (!node.nodeData.data.isActive) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.setNodeActive(node, false)
|
||||
this.mindMap.renderer.removeActiveNode(node)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Select
|
||||
@@ -1,226 +0,0 @@
|
||||
// 节点形状类
|
||||
export default class Shape {
|
||||
constructor(node) {
|
||||
this.node = node
|
||||
}
|
||||
|
||||
// 形状需要的padding
|
||||
getShapePadding(width, height, paddingX, paddingY) {
|
||||
const shape = this.node.getShape()
|
||||
const defaultPaddingX = 15
|
||||
const defaultPaddingY = 5
|
||||
const actWidth = width + paddingX * 2
|
||||
const actHeight = height + paddingY * 2
|
||||
const actOffset = Math.abs(actWidth - actHeight)
|
||||
switch (shape) {
|
||||
case 'roundedRectangle':
|
||||
return {
|
||||
paddingX: height > width ? (height - width) / 2 : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case 'diamond':
|
||||
return {
|
||||
paddingX: width / 2,
|
||||
paddingY: height / 2
|
||||
}
|
||||
case 'parallelogram':
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case 'outerTriangularRectangle':
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case 'innerTriangularRectangle':
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case 'ellipse':
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: paddingY <= 0 ? defaultPaddingY : 0
|
||||
}
|
||||
case 'circle':
|
||||
return {
|
||||
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
|
||||
paddingY: actHeight < actWidth ? actOffset / 2 : 0
|
||||
}
|
||||
default:
|
||||
return {
|
||||
paddingX: 0,
|
||||
paddingY: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建形状节点
|
||||
createShape() {
|
||||
const shape = this.node.getShape()
|
||||
let { width, height } = this.node
|
||||
let node = null
|
||||
// 矩形
|
||||
if (shape === 'rectangle') {
|
||||
node = this.node.group.rect(width, height)
|
||||
} else if (shape === 'diamond') {
|
||||
// 菱形
|
||||
node = this.createDiamond()
|
||||
} else if (shape === 'parallelogram') {
|
||||
// 平行四边形
|
||||
node = this.createParallelogram()
|
||||
} else if (shape === 'roundedRectangle') {
|
||||
// 圆角矩形
|
||||
node = this.createRoundedRectangle()
|
||||
} else if (shape === 'octagonalRectangle') {
|
||||
// 八角矩形
|
||||
node = this.createOctagonalRectangle()
|
||||
} else if (shape === 'outerTriangularRectangle') {
|
||||
// 外三角矩形
|
||||
node = this.createOuterTriangularRectangle()
|
||||
} else if (shape === 'innerTriangularRectangle') {
|
||||
// 内三角矩形
|
||||
node = this.createInnerTriangularRectangle()
|
||||
} else if (shape === 'ellipse') {
|
||||
// 椭圆
|
||||
node = this.createEllipse()
|
||||
} else if (shape === 'circle') {
|
||||
// 圆
|
||||
node = this.createCircle()
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// 创建菱形
|
||||
createDiamond() {
|
||||
let { width, height } = this.node
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
let topX = halfWidth
|
||||
let topY = 0
|
||||
let rightX = width
|
||||
let rightY = halfHeight
|
||||
let bottomX = halfWidth
|
||||
let bottomY = height
|
||||
let leftX = 0
|
||||
let leftY = halfHeight
|
||||
return this.node.group.polygon(`
|
||||
${topX}, ${topY}
|
||||
${rightX}, ${rightY}
|
||||
${bottomX}, ${bottomY}
|
||||
${leftX}, ${leftY}
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建平行四边形
|
||||
createParallelogram() {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.node
|
||||
return this.node.group.polygon(`
|
||||
${paddingX}, ${0}
|
||||
${width}, ${0}
|
||||
${width - paddingX}, ${height}
|
||||
${0}, ${height}
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建圆角矩形
|
||||
createRoundedRectangle() {
|
||||
let { width, height } = this.node
|
||||
let halfHeight = height / 2
|
||||
return this.node.group.path(`
|
||||
M${halfHeight},0
|
||||
L${width - halfHeight},0
|
||||
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
|
||||
L${halfHeight},${height}
|
||||
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建八角矩形
|
||||
createOctagonalRectangle() {
|
||||
let w = 5
|
||||
let { width, height } = this.node
|
||||
return this.node.group.polygon(`
|
||||
${0}, ${w}
|
||||
${w}, ${0}
|
||||
${width - w}, ${0}
|
||||
${width}, ${w}
|
||||
${width}, ${height - w}
|
||||
${width - w}, ${height}
|
||||
${w}, ${height}
|
||||
${0}, ${height - w}
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建外三角矩形
|
||||
createOuterTriangularRectangle() {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.node
|
||||
return this.node.group.polygon(`
|
||||
${paddingX}, ${0}
|
||||
${width - paddingX}, ${0}
|
||||
${width}, ${height / 2}
|
||||
${width - paddingX}, ${height}
|
||||
${paddingX}, ${height}
|
||||
${0}, ${height / 2}
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建内三角矩形
|
||||
createInnerTriangularRectangle() {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.node
|
||||
return this.node.group.polygon(`
|
||||
${0}, ${0}
|
||||
${width}, ${0}
|
||||
${width - paddingX / 2}, ${height / 2}
|
||||
${width}, ${height}
|
||||
${0}, ${height}
|
||||
${paddingX / 2}, ${height / 2}
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建椭圆
|
||||
createEllipse() {
|
||||
let { width, height } = this.node
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
return this.node.group.path(`
|
||||
M${halfWidth},0
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
|
||||
M${halfWidth},${height}
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建圆
|
||||
createCircle() {
|
||||
let { width, height } = this.node
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
return this.node.group.path(`
|
||||
M${halfWidth},0
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
|
||||
M${halfWidth},${height}
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
// 形状列表
|
||||
export const shapeList = [
|
||||
'rectangle',
|
||||
'diamond',
|
||||
'parallelogram',
|
||||
'roundedRectangle',
|
||||
'octagonalRectangle',
|
||||
'outerTriangularRectangle',
|
||||
'innerTriangularRectangle',
|
||||
'ellipse',
|
||||
'circle'
|
||||
]
|
||||
@@ -1,167 +0,0 @@
|
||||
import { tagColorList } from './utils/constant'
|
||||
const rootProp = ['paddingX', 'paddingY']
|
||||
|
||||
// 样式类
|
||||
|
||||
class Style {
|
||||
// 设置背景样式
|
||||
|
||||
static setBackgroundStyle(el, themeConfig) {
|
||||
let { backgroundColor, backgroundImage, backgroundRepeat } = themeConfig
|
||||
el.style.backgroundColor = backgroundColor
|
||||
if (backgroundImage) {
|
||||
el.style.backgroundImage = `url(${backgroundImage})`
|
||||
el.style.backgroundRepeat = backgroundRepeat
|
||||
}
|
||||
}
|
||||
|
||||
// 构造函数
|
||||
|
||||
constructor(ctx, themeConfig) {
|
||||
this.ctx = ctx
|
||||
this.themeConfig = themeConfig
|
||||
}
|
||||
|
||||
// 更新主题配置
|
||||
|
||||
updateThemeConfig(themeConfig) {
|
||||
this.themeConfig = themeConfig
|
||||
}
|
||||
|
||||
// 合并样式
|
||||
|
||||
merge(prop, root, isActive) {
|
||||
// 三级及以下节点
|
||||
let defaultConfig = this.themeConfig.node
|
||||
if (root || rootProp.includes(prop)) {
|
||||
// 直接使用最外层样式
|
||||
defaultConfig = this.themeConfig
|
||||
} else if (this.ctx.isGeneralization) {
|
||||
// 概要节点
|
||||
defaultConfig = this.themeConfig.generalization
|
||||
} else if (this.ctx.layerIndex === 0) {
|
||||
// 根节点
|
||||
defaultConfig = this.themeConfig.root
|
||||
} else if (this.ctx.layerIndex === 1) {
|
||||
// 二级节点
|
||||
defaultConfig = this.themeConfig.second
|
||||
}
|
||||
// 激活状态
|
||||
if (isActive !== undefined ? isActive : this.ctx.nodeData.data.isActive) {
|
||||
if (
|
||||
this.ctx.nodeData.data.activeStyle &&
|
||||
this.ctx.nodeData.data.activeStyle[prop] !== undefined
|
||||
) {
|
||||
return this.ctx.nodeData.data.activeStyle[prop]
|
||||
} else if (defaultConfig.active && defaultConfig.active[prop]) {
|
||||
return defaultConfig.active[prop]
|
||||
}
|
||||
}
|
||||
// 优先使用节点本身的样式
|
||||
return this.getSelfStyle(prop) !== undefined
|
||||
? this.getSelfStyle(prop)
|
||||
: defaultConfig[prop]
|
||||
}
|
||||
|
||||
// 获取某个样式值
|
||||
|
||||
getStyle(prop, root, isActive) {
|
||||
return this.merge(prop, root, isActive)
|
||||
}
|
||||
|
||||
// 获取自身自定义样式
|
||||
|
||||
getSelfStyle(prop) {
|
||||
return this.ctx.nodeData.data[prop]
|
||||
}
|
||||
|
||||
// 矩形
|
||||
|
||||
rect(node) {
|
||||
this.shape(node)
|
||||
node.radius(this.merge('borderRadius'))
|
||||
}
|
||||
|
||||
// 矩形外的其他形状
|
||||
|
||||
shape(node) {
|
||||
node.fill({
|
||||
color: this.merge('fillColor')
|
||||
})
|
||||
// 节点使用横线样式,不需要渲染非激活状态的边框样式
|
||||
if (
|
||||
!this.ctx.isRoot &&
|
||||
!this.ctx.isGeneralization &&
|
||||
this.themeConfig.nodeUseLineStyle &&
|
||||
!this.ctx.nodeData.data.isActive
|
||||
) {
|
||||
return
|
||||
}
|
||||
node.stroke({
|
||||
color: this.merge('borderColor'),
|
||||
width: this.merge('borderWidth'),
|
||||
dasharray: this.merge('borderDasharray')
|
||||
})
|
||||
}
|
||||
|
||||
// 文字
|
||||
|
||||
text(node) {
|
||||
node
|
||||
.fill({
|
||||
color: this.merge('color')
|
||||
})
|
||||
.css({
|
||||
'font-family': this.merge('fontFamily'),
|
||||
'font-size': this.merge('fontSize'),
|
||||
'font-weight': this.merge('fontWeight'),
|
||||
'font-style': this.merge('fontStyle'),
|
||||
'text-decoration': this.merge('textDecoration')
|
||||
})
|
||||
}
|
||||
|
||||
// html文字节点
|
||||
|
||||
domText(node, fontSizeScale = 1) {
|
||||
node.style.fontFamily = this.merge('fontFamily')
|
||||
node.style.fontSize = this.merge('fontSize') * fontSizeScale + 'px'
|
||||
node.style.fontWeight = this.merge('fontWeight') || 'normal'
|
||||
}
|
||||
|
||||
// 标签文字
|
||||
|
||||
tagText(node, index) {
|
||||
node
|
||||
.fill({
|
||||
color: tagColorList[index].color
|
||||
})
|
||||
.css({
|
||||
'font-size': '12px'
|
||||
})
|
||||
}
|
||||
|
||||
// 标签矩形
|
||||
|
||||
tagRect(node, index) {
|
||||
node.fill({
|
||||
color: tagColorList[index].background
|
||||
})
|
||||
}
|
||||
|
||||
// 内置图标
|
||||
|
||||
iconNode(node) {
|
||||
node.attr({
|
||||
fill: this.merge('color')
|
||||
})
|
||||
}
|
||||
|
||||
// 连线
|
||||
|
||||
line(node, { width, color, dasharray } = {}) {
|
||||
node.stroke({ width, color, dasharray }).fill({ color: 'none' })
|
||||
}
|
||||
|
||||
// 概要连线
|
||||
|
||||
generalizationLine(node) {
|
||||
@@ -1,118 +0,0 @@
|
||||
import { getStrWithBrFromHtml } from './utils'
|
||||
|
||||
// 节点文字编辑类
|
||||
export default class TextEdit {
|
||||
// 构造函数
|
||||
constructor(renderer) {
|
||||
this.renderer = renderer
|
||||
this.mindMap = renderer.mindMap
|
||||
// 文本编辑框
|
||||
this.textEditNode = null
|
||||
// 文本编辑框是否显示
|
||||
this.showTextEdit = false
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 事件
|
||||
bindEvent() {
|
||||
this.show = this.show.bind(this)
|
||||
// 节点双击事件
|
||||
this.mindMap.on('node_dblclick', this.show)
|
||||
// 点击事件
|
||||
this.mindMap.on('draw_click', () => {
|
||||
// 隐藏文本编辑框
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
// 展开收缩按钮点击事件
|
||||
this.mindMap.on('expand_btn_click', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
// 节点激活前事件
|
||||
this.mindMap.on('before_node_active', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
// 注册编辑快捷键
|
||||
this.mindMap.keyCommand.addShortcut('F2', () => {
|
||||
if (this.renderer.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
this.show(this.renderer.activeNodeList[0])
|
||||
})
|
||||
}
|
||||
|
||||
// 注册临时快捷键
|
||||
registerTmpShortcut() {
|
||||
// 注册回车快捷键
|
||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
show(node) {
|
||||
this.showEditTextBox(node, node._textData.node.node.getBoundingClientRect())
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
showEditTextBox(node, rect) {
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
this.registerTmpShortcut()
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none;`
|
||||
this.textEditNode.setAttribute('contenteditable', true)
|
||||
this.textEditNode.addEventListener('keyup', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
}
|
||||
node.style.domText(this.textEditNode, this.mindMap.view.scale)
|
||||
this.textEditNode.innerHTML = node.nodeData.data.text
|
||||
.split(/\n/gim)
|
||||
.join('<br>')
|
||||
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
|
||||
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
|
||||
this.textEditNode.style.left = rect.left + 'px'
|
||||
this.textEditNode.style.top = rect.top + 'px'
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.showTextEdit = true
|
||||
// 选中文本
|
||||
this.selectNodeText()
|
||||
}
|
||||
|
||||
// 选中文本
|
||||
selectNodeText() {
|
||||
let selection = window.getSelection()
|
||||
let range = document.createRange()
|
||||
range.selectNodeContents(this.textEditNode)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
|
||||
// 隐藏文本编辑框
|
||||
hideEditTextBox() {
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
this.renderer.activeNodeList.forEach(node => {
|
||||
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', node, str)
|
||||
if (node.isGeneralization) {
|
||||
// 概要节点
|
||||
node.generalizationBelongNode.updateGeneralization()
|
||||
}
|
||||
this.mindMap.render()
|
||||
})
|
||||
this.mindMap.emit(
|
||||
'hide_text_edit',
|
||||
this.textEditNode,
|
||||
this.renderer.activeNodeList
|
||||
)
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.textEditNode.innerHTML = ''
|
||||
this.textEditNode.style.fontFamily = 'inherit'
|
||||
this.textEditNode.style.fontSize = 'inherit'
|
||||
this.textEditNode.style.fontWeight = 'normal'
|
||||
this.showTextEdit = false
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
// 视图操作类
|
||||
class View {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
this.opt = opt
|
||||
this.mindMap = this.opt.mindMap
|
||||
this.scale = 1
|
||||
this.sx = 0
|
||||
this.sy = 0
|
||||
this.x = 0
|
||||
this.y = 0
|
||||
this.firstDrag = true
|
||||
this.setTransformData(this.mindMap.opt.viewData)
|
||||
this.bind()
|
||||
}
|
||||
|
||||
// 绑定
|
||||
bind() {
|
||||
// 快捷键
|
||||
this.mindMap.keyCommand.addShortcut('Control+=', () => {
|
||||
this.enlarge()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Control+-', () => {
|
||||
this.narrow()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Control+Enter', () => {
|
||||
this.reset()
|
||||
})
|
||||
this.mindMap.svg.on('dblclick', () => {
|
||||
this.reset()
|
||||
})
|
||||
// 拖动视图
|
||||
this.mindMap.event.on('mousedown', () => {
|
||||
this.sx = this.x
|
||||
this.sy = this.y
|
||||
})
|
||||
this.mindMap.event.on('drag', (e, event) => {
|
||||
if (e.ctrlKey) {
|
||||
// 按住ctrl键拖动为多选
|
||||
return
|
||||
}
|
||||
if (this.firstDrag) {
|
||||
this.firstDrag = false
|
||||
// 清除激活节点
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
}
|
||||
}
|
||||
this.x = this.sx + event.mousemoveOffset.x
|
||||
this.y = this.sy + event.mousemoveOffset.y
|
||||
this.transform()
|
||||
})
|
||||
this.mindMap.event.on('mouseup', () => {
|
||||
this.firstDrag = true
|
||||
})
|
||||
// 放大缩小视图
|
||||
this.mindMap.event.on('mousewheel', (e, dir) => {
|
||||
// // 放大
|
||||
if (dir === 'down') {
|
||||
this.enlarge()
|
||||
} else {
|
||||
// 缩小
|
||||
this.narrow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前变换状态数据
|
||||
getTransformData() {
|
||||
return {
|
||||
transform: this.mindMap.draw.transform(),
|
||||
state: {
|
||||
scale: this.scale,
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
sx: this.sx,
|
||||
sy: this.sy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动态设置变换状态数据
|
||||
setTransformData(viewData) {
|
||||
if (viewData) {
|
||||
Object.keys(viewData.state).forEach(prop => {
|
||||
this[prop] = viewData.state[prop]
|
||||
})
|
||||
this.mindMap.draw.transform({
|
||||
...viewData.transform
|
||||
})
|
||||
this.mindMap.emit('view_data_change', this.getTransformData())
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
}
|
||||
|
||||
// 平移x方向
|
||||
translateX(step) {
|
||||
this.x += step
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移x方式到
|
||||
translateXTo(x) {
|
||||
this.x = x
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移y方向
|
||||
translateY(step) {
|
||||
this.y += step
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移y方向到
|
||||
translateYTo(y) {
|
||||
this.y = y
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 应用变换
|
||||
transform() {
|
||||
this.mindMap.draw.transform({
|
||||
scale: this.scale,
|
||||
// origin: 'center center',
|
||||
translate: [this.x, this.y]
|
||||
})
|
||||
this.mindMap.emit('view_data_change', this.getTransformData())
|
||||
}
|
||||
|
||||
// 恢复
|
||||
reset() {
|
||||
this.scale = 1
|
||||
this.x = 0
|
||||
this.y = 0
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 缩小
|
||||
narrow() {
|
||||
if (this.scale - this.mindMap.opt.scaleRatio > 0.1) {
|
||||
this.scale -= this.mindMap.opt.scaleRatio
|
||||
} else {
|
||||
this.scale = 0.1
|
||||
}
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
|
||||
// 放大
|
||||
enlarge() {
|
||||
this.scale += this.mindMap.opt.scaleRatio
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
|
||||
// 设置缩放
|
||||
setScale(scale) {
|
||||
this.scale = scale
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
}
|
||||
|
||||
export default View
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB |
369
simple-mind-map/src/constants/constant.js
Normal file
@@ -0,0 +1,369 @@
|
||||
// 主题列表
|
||||
export const themeList = [
|
||||
{
|
||||
name: '默认',
|
||||
value: 'default',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '暗色2',
|
||||
value: 'dark2',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '天清绿',
|
||||
value: 'skyGreen',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '脑图经典2',
|
||||
value: 'classic2',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '脑图经典3',
|
||||
value: 'classic3',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '经典绿',
|
||||
value: 'classicGreen',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '经典蓝',
|
||||
value: 'classicBlue',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '天空蓝',
|
||||
value: 'blueSky',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '脑残粉',
|
||||
value: 'brainImpairedPink',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '暗色',
|
||||
value: 'dark',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '泥土黄',
|
||||
value: 'earthYellow',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '清新绿',
|
||||
value: 'freshGreen',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '清新红',
|
||||
value: 'freshRed',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '浪漫紫',
|
||||
value: 'romanticPurple',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '粉红葡萄',
|
||||
value: 'pinkGrape',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '薄荷',
|
||||
value: 'mint',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '金色vip',
|
||||
value: 'gold',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '活力橙',
|
||||
value: 'vitalityOrange',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '绿叶',
|
||||
value: 'greenLeaf',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '脑图经典',
|
||||
value: 'classic',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '脑图经典4',
|
||||
value: 'classic4',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '小黄人',
|
||||
value: 'minions',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '简约黑',
|
||||
value: 'simpleBlack',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '课程绿',
|
||||
value: 'courseGreen',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '咖啡',
|
||||
value: 'coffee',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '红色精神',
|
||||
value: 'redSpirit',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '黑色幽默',
|
||||
value: 'blackHumour',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '深夜办公室',
|
||||
value: 'lateNightOffice',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '黑金',
|
||||
value: 'blackGold',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '牛油果',
|
||||
value: 'avocado',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '秋天',
|
||||
value: 'autumn',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '橙汁',
|
||||
value: 'orangeJuice',
|
||||
dark: true
|
||||
}
|
||||
]
|
||||
|
||||
// 常量
|
||||
export const CONSTANTS = {
|
||||
CHANGE_THEME: 'changeTheme',
|
||||
CHANGE_LAYOUT: 'changeLayout',
|
||||
SET_DATA: 'setData',
|
||||
TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode',
|
||||
MODE: {
|
||||
READONLY: 'readonly',
|
||||
EDIT: 'edit'
|
||||
},
|
||||
LAYOUT: {
|
||||
LOGICAL_STRUCTURE: 'logicalStructure',
|
||||
MIND_MAP: 'mindMap',
|
||||
ORGANIZATION_STRUCTURE: 'organizationStructure',
|
||||
CATALOG_ORGANIZATION: 'catalogOrganization',
|
||||
TIMELINE: 'timeline',
|
||||
TIMELINE2: 'timeline2',
|
||||
FISHBONE: 'fishbone',
|
||||
VERTICAL_TIMELINE: 'verticalTimeline'
|
||||
},
|
||||
DIR: {
|
||||
UP: 'up',
|
||||
LEFT: 'left',
|
||||
DOWN: 'down',
|
||||
RIGHT: 'right'
|
||||
},
|
||||
KEY_DIR: {
|
||||
LEFT: 'Left',
|
||||
UP: 'Up',
|
||||
RIGHT: 'Right',
|
||||
DOWN: 'Down'
|
||||
},
|
||||
SHAPE: {
|
||||
RECTANGLE: 'rectangle',
|
||||
DIAMOND: 'diamond',
|
||||
PARALLELOGRAM: 'parallelogram',
|
||||
ROUNDED_RECTANGLE: 'roundedRectangle',
|
||||
OCTAGONAL_RECTANGLE: 'octagonalRectangle',
|
||||
OUTER_TRIANGULAR_RECTANGLE: 'outerTriangularRectangle',
|
||||
INNER_TRIANGULAR_RECTANGLE: 'innerTriangularRectangle',
|
||||
ELLIPSE: 'ellipse',
|
||||
CIRCLE: 'circle'
|
||||
},
|
||||
MOUSE_WHEEL_ACTION: {
|
||||
ZOOM: 'zoom',
|
||||
MOVE: 'move'
|
||||
},
|
||||
INIT_ROOT_NODE_POSITION: {
|
||||
LEFT: 'left',
|
||||
TOP: 'top',
|
||||
RIGHT: 'right',
|
||||
BOTTOM: 'bottom',
|
||||
CENTER: 'center'
|
||||
},
|
||||
LAYOUT_GROW_DIR: {
|
||||
LEFT: 'left',
|
||||
TOP: 'top',
|
||||
RIGHT: 'right',
|
||||
BOTTOM: 'bottom'
|
||||
},
|
||||
PASTE_TYPE: {
|
||||
CLIP_BOARD: 'clipBoard',
|
||||
CANVAS: 'canvas'
|
||||
},
|
||||
SCROLL_BAR_DIR: {
|
||||
VERTICAL: 'vertical',
|
||||
HORIZONTAL: 'horizontal'
|
||||
},
|
||||
CREATE_NEW_NODE_BEHAVIOR: {
|
||||
DEFAULT: 'default',
|
||||
NOT_ACTIVE: 'notActive',
|
||||
ACTIVE_ONLY: 'activeOnly'
|
||||
}
|
||||
}
|
||||
|
||||
export const initRootNodePositionMap = {
|
||||
[CONSTANTS.INIT_ROOT_NODE_POSITION.LEFT]: 0,
|
||||
[CONSTANTS.INIT_ROOT_NODE_POSITION.TOP]: 0,
|
||||
[CONSTANTS.INIT_ROOT_NODE_POSITION.RIGHT]: 1,
|
||||
[CONSTANTS.INIT_ROOT_NODE_POSITION.BOTTOM]: 1,
|
||||
[CONSTANTS.INIT_ROOT_NODE_POSITION.CENTER]: 0.5
|
||||
}
|
||||
|
||||
// 布局结构列表
|
||||
export const layoutList = [
|
||||
{
|
||||
name: '逻辑结构图',
|
||||
value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
},
|
||||
{
|
||||
name: '思维导图',
|
||||
value: CONSTANTS.LAYOUT.MIND_MAP
|
||||
},
|
||||
{
|
||||
name: '组织结构图',
|
||||
value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE
|
||||
},
|
||||
{
|
||||
name: '目录组织图',
|
||||
value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION
|
||||
},
|
||||
{
|
||||
name: '时间轴',
|
||||
value: CONSTANTS.LAYOUT.TIMELINE
|
||||
},
|
||||
{
|
||||
name: '时间轴2',
|
||||
value: CONSTANTS.LAYOUT.TIMELINE2
|
||||
},
|
||||
{
|
||||
name: '竖向时间轴',
|
||||
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE
|
||||
},
|
||||
{
|
||||
name: '鱼骨图',
|
||||
value: CONSTANTS.LAYOUT.FISHBONE
|
||||
}
|
||||
]
|
||||
export const layoutValueList = [
|
||||
CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
|
||||
CONSTANTS.LAYOUT.MIND_MAP,
|
||||
CONSTANTS.LAYOUT.CATALOG_ORGANIZATION,
|
||||
CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
|
||||
CONSTANTS.LAYOUT.TIMELINE,
|
||||
CONSTANTS.LAYOUT.TIMELINE2,
|
||||
CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
|
||||
CONSTANTS.LAYOUT.FISHBONE
|
||||
]
|
||||
|
||||
// 节点数据中非样式的字段
|
||||
export const nodeDataNoStylePropList = [
|
||||
'text',
|
||||
'image',
|
||||
'imageTitle',
|
||||
'imageSize',
|
||||
'icon',
|
||||
'tag',
|
||||
'hyperlink',
|
||||
'hyperlinkTitle',
|
||||
'note',
|
||||
'expand',
|
||||
'isActive',
|
||||
'generalization',
|
||||
'richText',
|
||||
'resetRichText',
|
||||
'uid',
|
||||
'activeStyle',
|
||||
'associativeLineTargets',
|
||||
'associativeLineTargetControlOffsets',
|
||||
'associativeLinePoint',
|
||||
'associativeLineText'
|
||||
]
|
||||
|
||||
// 数据缓存
|
||||
export const commonCaches = {
|
||||
measureCustomNodeContentSizeEl: null,
|
||||
measureRichtextNodeTextSizeEl: null
|
||||
}
|
||||
|
||||
// 错误类型
|
||||
export const ERROR_TYPES = {
|
||||
READ_CLIPBOARD_ERROR: 'read_clipboard_error',
|
||||
PARSE_PASTE_DATA_ERROR: 'parse_paste_data_error',
|
||||
CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR: 'custom_handle_clipboard_text_error',
|
||||
LOAD_CLIPBOARD_IMAGE_ERROR: 'load_clipboard_image_error',
|
||||
BEFORE_TEXT_EDIT_ERROR: 'before_text_edit_error',
|
||||
EXPORT_ERROR: 'export_error'
|
||||
}
|
||||
|
||||
// a4纸的宽高
|
||||
export const a4Size = {
|
||||
width: 592.28,
|
||||
height: 841.89
|
||||
}
|
||||
|
||||
// css
|
||||
export const cssContent = `
|
||||
/* 鼠标hover和激活时渲染的矩形 */
|
||||
.smm-hover-node{
|
||||
display: none;
|
||||
opacity: 0.6;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.smm-node:not(.smm-node-dragging):hover .smm-hover-node{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.smm-node.active .smm-hover-node{
|
||||
display: block;
|
||||
opacity: 1;
|
||||
stroke-width: 2;
|
||||
}
|
||||
`
|
||||
|
||||
// html自闭合标签列表
|
||||
export const selfCloseTagList = [
|
||||
'img',
|
||||
'br',
|
||||
'hr',
|
||||
'input',
|
||||
'link',
|
||||
'meta',
|
||||
'area'
|
||||
]
|
||||
240
simple-mind-map/src/constants/defaultOptions.js
Normal file
@@ -0,0 +1,240 @@
|
||||
import { CONSTANTS } from './constant'
|
||||
|
||||
// 默认选项配置
|
||||
export const defaultOpt = {
|
||||
// 是否只读
|
||||
readonly: false,
|
||||
// 布局
|
||||
layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
|
||||
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
|
||||
fishboneDeg: 45,
|
||||
// 主题
|
||||
theme: 'default', // 内置主题:default(默认主题)
|
||||
// 主题配置,会和所选择的主题进行合并
|
||||
themeConfig: {},
|
||||
// 放大缩小的增量比例
|
||||
scaleRatio: 0.2,
|
||||
// 鼠标缩放是否以鼠标当前位置为中心点,否则以画布中心点
|
||||
mouseScaleCenterUseMousePosition: true,
|
||||
// 最多显示几个标签
|
||||
maxTag: 5,
|
||||
// 展开收缩按钮尺寸
|
||||
expandBtnSize: 20,
|
||||
// 节点里图片和文字的间距
|
||||
imgTextMargin: 5,
|
||||
// 节点里各种文字信息的间距,如图标和文字的间距
|
||||
textContentMargin: 2,
|
||||
// 多选节点时鼠标移动到边缘时的画布移动偏移量
|
||||
selectTranslateStep: 3,
|
||||
// 多选节点时鼠标移动距边缘多少距离时开始偏移
|
||||
selectTranslateLimit: 20,
|
||||
// 自定义节点备注内容显示
|
||||
customNoteContentShow: null,
|
||||
/*
|
||||
{
|
||||
show(){},
|
||||
hide(){}
|
||||
}
|
||||
*/
|
||||
// 是否开启节点自由拖拽
|
||||
enableFreeDrag: false,
|
||||
// 水印配置
|
||||
watermarkConfig: {
|
||||
text: '',
|
||||
lineSpacing: 100,
|
||||
textSpacing: 100,
|
||||
angle: 30,
|
||||
textStyle: {
|
||||
color: '#999',
|
||||
opacity: 0.5,
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
// 达到该宽度文本自动换行
|
||||
textAutoWrapWidth: 500,
|
||||
// 自定义鼠标滚轮事件处理
|
||||
// 可以传一个函数,回调参数为事件对象
|
||||
customHandleMousewheel: null,
|
||||
// 鼠标滚动的行为,如果customHandleMousewheel传了自定义函数,这个属性不生效
|
||||
mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.MOVE, // zoom(放大缩小)、move(上下移动)
|
||||
// 当mousewheelAction设为move时,可以通过该属性控制鼠标滚动一下视图移动的步长,单位px
|
||||
mousewheelMoveStep: 100,
|
||||
// 当mousewheelAction设为zoom时,或者按住Ctrl键时,默认向前滚动是缩小,向后滚动是放大,如果该属性设为true,那么会反过来
|
||||
mousewheelZoomActionReverse: true,
|
||||
// 默认插入的二级节点的文字
|
||||
defaultInsertSecondLevelNodeText: '二级节点',
|
||||
// 默认插入的二级以下节点的文字
|
||||
defaultInsertBelowSecondLevelNodeText: '分支主题',
|
||||
// 展开收起按钮的颜色
|
||||
expandBtnStyle: {
|
||||
color: '#808080',
|
||||
fill: '#fff',
|
||||
fontSize: 13,
|
||||
strokeColor: '#333333'
|
||||
},
|
||||
// 自定义展开收起按钮的图标
|
||||
expandBtnIcon: {
|
||||
open: '', // svg字符串
|
||||
close: ''
|
||||
},
|
||||
// 处理收起节点数量
|
||||
expandBtnNumHandler: num => {
|
||||
return num
|
||||
},
|
||||
// 是否显示带数量的收起按钮
|
||||
isShowExpandNum: true,
|
||||
// 是否只有当鼠标在画布内才响应快捷键事件
|
||||
enableShortcutOnlyWhenMouseInSvg: true,
|
||||
// 初始根节点的位置
|
||||
initRootNodePosition: null,
|
||||
// 导出png、svg、pdf时的图形内边距,注意是单侧内边距
|
||||
exportPaddingX: 10,
|
||||
exportPaddingY: 10,
|
||||
// 节点文本编辑框的z-index
|
||||
nodeTextEditZIndex: 3000,
|
||||
// 节点备注浮层的z-index
|
||||
nodeNoteTooltipZIndex: 3000,
|
||||
// 是否在点击了画布外的区域时结束节点文本的编辑状态
|
||||
isEndNodeTextEditOnClickOuter: true,
|
||||
// 最大历史记录数
|
||||
maxHistoryCount: 1000,
|
||||
// 是否一直显示节点的展开收起按钮,默认为鼠标移上去和激活时才显示
|
||||
alwaysShowExpandBtn: false,
|
||||
// 扩展节点可插入的图标
|
||||
iconList: [
|
||||
// {
|
||||
// name: '',// 分组名称
|
||||
// type: '',// 分组的值
|
||||
// list: [// 分组下的图标列表
|
||||
// {
|
||||
// name: '',// 图标名称
|
||||
// icon:''// 图标,可以传svg或图片
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
],
|
||||
// 节点最大缓存数量
|
||||
maxNodeCacheCount: 1000,
|
||||
// 关联线默认文字
|
||||
defaultAssociativeLineText: '关联',
|
||||
// 思维导图适应画布大小时的内边距
|
||||
fitPadding: 50,
|
||||
// 是否开启按住ctrl键多选节点功能
|
||||
enableCtrlKeyNodeSelection: true,
|
||||
// 设置为左键多选节点,右键拖动画布
|
||||
useLeftKeySelectionRightKeyDrag: false,
|
||||
// 节点即将进入编辑前的回调方法,如果该方法返回true以外的值,那么将取消编辑,函数可以返回一个值,或一个Promise,回调参数为节点实例
|
||||
beforeTextEdit: null,
|
||||
// 是否开启自定义节点内容
|
||||
isUseCustomNodeContent: false,
|
||||
// 自定义返回节点内容的方法
|
||||
customCreateNodeContent: null,
|
||||
// 指定内部一些元素(节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素)添加到的位置,默认添加到document.body下
|
||||
customInnerElsAppendTo: null,
|
||||
// 拖拽元素时,指示元素新位置的块的最大高度
|
||||
nodeDragPlaceholderMaxSize: 20,
|
||||
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
|
||||
// 开启该特性后,需要给你的输入框绑定keydown事件,并禁止冒泡
|
||||
enableAutoEnterTextEditWhenKeydown: false,
|
||||
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
|
||||
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
|
||||
richTextEditFakeInPlace: false,
|
||||
// 自定义对剪贴板文本的处理。当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片,默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据,如果你想处理其他思维导图的数据,比如processon、zhixi等,那么可以传递一个函数,接受当前剪贴板中的文本为参数,返回处理后的数据,可以返回两种类型:
|
||||
/*
|
||||
1.返回一个纯文本,那么会直接以该文本创建一个子节点
|
||||
|
||||
2.返回一个节点对象,格式如下:
|
||||
{
|
||||
// 代表是simple-mind-map格式的数据
|
||||
simpleMindMap: true,
|
||||
// 节点数据,同simple-mind-map节点数据格式
|
||||
data: {
|
||||
data: {
|
||||
text: ''
|
||||
},
|
||||
children: []
|
||||
}
|
||||
}
|
||||
*/
|
||||
// 如果你的处理逻辑存在异步逻辑,也可以返回一个promise
|
||||
customHandleClipboardText: null,
|
||||
// 禁止鼠标滚轮缩放,你仍旧可以使用api进行缩放
|
||||
disableMouseWheelZoom: false,
|
||||
// 禁止双指缩放,你仍旧可以使用api进行缩放
|
||||
// 需要注册TouchEvent插件后生效
|
||||
disableTouchZoom: false,
|
||||
// 错误处理函数
|
||||
errorHandler: (code, error) => {
|
||||
console.error(code, error)
|
||||
},
|
||||
// 设置导出图片和svg时,针对富文本节点内容,也就是嵌入到svg中的html节点的默认样式覆盖
|
||||
// 如果不覆盖,会发生偏移问题
|
||||
resetCss: `
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`,
|
||||
// 是否在鼠标双击时回到根节点,也就是让根节点居中显示
|
||||
enableDblclickBackToRootNode: false,
|
||||
// 导出图片时canvas的缩放倍数,该配置会和window.devicePixelRatio值取最大值
|
||||
minExportImgCanvasScale: 2,
|
||||
// 节点鼠标hover和激活时显示的矩形边框的颜色
|
||||
hoverRectColor: 'rgb(94, 200, 248)',
|
||||
// 节点鼠标hover和激活时显示的矩形边框距节点内容的距离
|
||||
hoverRectPadding: 2,
|
||||
// 双击节点进入节点文本编辑时是否默认选中文本,默认只在创建新节点时会选中
|
||||
selectTextOnEnterEditText: false,
|
||||
// 删除节点后激活相邻节点
|
||||
deleteNodeActive: true,
|
||||
// 拖拽节点时鼠标移动到画布边缘是否开启画布自动移动
|
||||
autoMoveWhenMouseInEdgeOnDrag: true,
|
||||
// 是否首次加载fit view
|
||||
fit: false,
|
||||
// 拖拽多个节点时随鼠标移动的示意矩形的样式配置
|
||||
dragMultiNodeRectConfig: {
|
||||
width: 40,
|
||||
height: 20,
|
||||
fill: '' // 填充颜色,如果不传默认使用连线的颜色
|
||||
},
|
||||
// 节点拖拽时新位置的示意矩形的填充颜色,如果不传默认使用连线的颜色
|
||||
dragPlaceholderRectFill: '',
|
||||
// 节点拖拽时的透明度配置
|
||||
dragOpacityConfig: {
|
||||
cloneNodeOpacity: 0.5, // 跟随鼠标移动的克隆节点或矩形的透明度
|
||||
beingDragNodeOpacity: 0.3 // 被拖拽节点的透明度
|
||||
},
|
||||
// 自定义标签的颜色
|
||||
// {pass: 'green, unpass: 'red'}
|
||||
tagsColorMap: {},
|
||||
// 节点协作样式配置
|
||||
cooperateStyle: {
|
||||
avatarSize: 22, // 头像大小
|
||||
fontSize: 12 // 如果是文字头像,那么文字的大小
|
||||
},
|
||||
// 关联线是否始终显示在节点上层
|
||||
// false:即创建关联线和激活关联线时处于最顶层,其他情况下处于节点下方
|
||||
associativeLineIsAlwaysAboveNode: true,
|
||||
// 插入概要的默认文本
|
||||
defaultGeneralizationText: '概要',
|
||||
// 粘贴文本的方式创建新节点时,控制是否按换行自动分割节点,即如果存在换行,那么会根据换行创建多个节点,否则只会创建一个节点
|
||||
// 可以传递一个函数,返回promise,resolve代表根据换行分割,reject代表忽略换行
|
||||
handleIsSplitByWrapOnPasteCreateNewNode: null,
|
||||
// 多少时间内只允许添加一次历史记录,避免添加没有必要的中间状态,单位:ms
|
||||
addHistoryTime: 100,
|
||||
// 是否禁止拖动画布
|
||||
isDisableDrag: false,
|
||||
// 鼠标移入概要高亮所属节点时的高亮框样式
|
||||
highlightNodeBoxStyle: {
|
||||
stroke: 'rgb(94, 200, 248)',
|
||||
fill: 'transparent'
|
||||
},
|
||||
// 创建新节点时的行为
|
||||
/*
|
||||
DEFAULT :默认会激活新创建的节点,并且进入编辑模式。如果同时创建了多个新节点,那么只会激活而不会进入编辑模式
|
||||
NOT_ACTIVE : 不激活新创建的节点
|
||||
ACTIVE_ONLY : 只激活新创建的节点,不进入编辑模式
|
||||
*/
|
||||
createNewNodeBehavior: CONSTANTS.CREATE_NEW_NODE_BEHAVIOR.DEFAULT
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { copyRenderTree, simpleDeepClone } from './utils'
|
||||
import { copyRenderTree, simpleDeepClone, throttle } from '../../utils'
|
||||
|
||||
// 命令类
|
||||
class Command {
|
||||
@@ -11,6 +11,11 @@ class Command {
|
||||
this.activeHistoryIndex = 0
|
||||
// 注册快捷键
|
||||
this.registerShortcutKeys()
|
||||
this.addHistory = throttle(
|
||||
this.addHistory,
|
||||
this.mindMap.opt.addHistoryTime,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
// 清空历史数据
|
||||
@@ -36,7 +41,11 @@ class Command {
|
||||
this.commands[name].forEach(fn => {
|
||||
fn(...args)
|
||||
})
|
||||
if (name === 'BACK' || name === 'FORWARD') {
|
||||
if (
|
||||
['BACK', 'FORWARD', 'SET_NODE_ACTIVE', 'CLEAR_ACTIVE_NODE'].includes(
|
||||
name
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.addHistory()
|
||||
@@ -72,8 +81,25 @@ class Command {
|
||||
|
||||
// 添加回退数据
|
||||
addHistory() {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
let data = this.getCopyData()
|
||||
// 此次数据和上次一样则不重复添加
|
||||
if (
|
||||
this.history.length > 0 &&
|
||||
JSON.stringify(this.history[this.history.length - 1]) ===
|
||||
JSON.stringify(data)
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 删除当前历史指针后面的数据
|
||||
this.history = this.history.slice(0, this.activeHistoryIndex + 1)
|
||||
this.history.push(simpleDeepClone(data))
|
||||
// 历史记录数超过最大数量
|
||||
if (this.history.length > this.mindMap.opt.maxHistoryCount) {
|
||||
this.history.shift()
|
||||
}
|
||||
this.activeHistoryIndex = this.history.length - 1
|
||||
this.mindMap.emit('data_change', data)
|
||||
this.mindMap.emit(
|
||||
@@ -85,6 +111,9 @@ class Command {
|
||||
|
||||
// 回退
|
||||
back(step = 1) {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
if (this.activeHistoryIndex - step >= 0) {
|
||||
this.activeHistoryIndex -= step
|
||||
this.mindMap.emit(
|
||||
@@ -92,23 +121,49 @@ class Command {
|
||||
this.activeHistoryIndex,
|
||||
this.history.length
|
||||
)
|
||||
return simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.mindMap.emit('data_change', data)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
// 前进
|
||||
forward(step = 1) {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
let len = this.history.length
|
||||
if (this.activeHistoryIndex + step <= len - 1) {
|
||||
this.activeHistoryIndex += step
|
||||
this.mindMap.emit('back_forward', this.activeHistoryIndex)
|
||||
return simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.mindMap.emit(
|
||||
'back_forward',
|
||||
this.activeHistoryIndex,
|
||||
this.history.length
|
||||
)
|
||||
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.mindMap.emit('data_change', data)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
// 获取渲染树数据副本
|
||||
getCopyData() {
|
||||
return copyRenderTree({}, this.mindMap.renderer.renderTree)
|
||||
return copyRenderTree({}, this.mindMap.renderer.renderTree, true)
|
||||
}
|
||||
|
||||
// 移除节点数据中的uid
|
||||
removeDataUid(data) {
|
||||
data = simpleDeepClone(data)
|
||||
let walk = root => {
|
||||
delete root.data.uid
|
||||
if (root.children && root.children.length > 0) {
|
||||
root.children.forEach(item => {
|
||||
walk(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
walk(data)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { keyMap } from './utils/keyMap'
|
||||
import { keyMap } from './keyMap'
|
||||
// 快捷按键、命令处理类
|
||||
export default class KeyCommand {
|
||||
// 构造函数
|
||||
@@ -10,6 +10,7 @@ export default class KeyCommand {
|
||||
}
|
||||
this.shortcutMapCache = {}
|
||||
this.isPause = false
|
||||
this.isInSvg = false
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@@ -37,19 +38,54 @@ export default class KeyCommand {
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
window.addEventListener('keydown', e => {
|
||||
if (this.isPause) {
|
||||
this.onKeydown = this.onKeydown.bind(this)
|
||||
// 只有当鼠标在画布内才响应快捷键
|
||||
this.mindMap.on('svg_mouseenter', () => {
|
||||
this.isInSvg = true
|
||||
})
|
||||
this.mindMap.on('svg_mouseleave', () => {
|
||||
if (this.mindMap.richText && this.mindMap.richText.showTextEdit) {
|
||||
return
|
||||
}
|
||||
Object.keys(this.shortcutMap).forEach(key => {
|
||||
if (this.checkKey(e, key)) {
|
||||
if (
|
||||
this.mindMap.renderer.textEdit.showTextEdit ||
|
||||
(this.mindMap.associativeLine &&
|
||||
this.mindMap.associativeLine.showTextEdit)
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.isInSvg = false
|
||||
})
|
||||
window.addEventListener('keydown', this.onKeydown)
|
||||
this.mindMap.on('beforeDestroy', () => {
|
||||
this.unBindEvent()
|
||||
})
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
unBindEvent() {
|
||||
window.removeEventListener('keydown', this.onKeydown)
|
||||
}
|
||||
|
||||
// 按键事件
|
||||
onKeydown(e) {
|
||||
if (
|
||||
this.isPause ||
|
||||
(this.mindMap.opt.enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)
|
||||
) {
|
||||
return
|
||||
}
|
||||
Object.keys(this.shortcutMap).forEach(key => {
|
||||
if (this.checkKey(e, key)) {
|
||||
// 粘贴事件不组织,因为要监听paste事件
|
||||
if (!this.checkKey(e, 'Control+v')) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
this.shortcutMap[key].forEach(fn => {
|
||||
fn()
|
||||
})
|
||||
}
|
||||
})
|
||||
this.shortcutMap[key].forEach(fn => {
|
||||
fn()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -91,6 +127,11 @@ export default class KeyCommand {
|
||||
return arr
|
||||
}
|
||||
|
||||
// 判断是否按下了组合键
|
||||
hasCombinationKey(e) {
|
||||
return e.ctrlKey || e.metaKey || e.altKey || e.shiftKey
|
||||
}
|
||||
|
||||
// 获取快捷键对应的键值数组
|
||||
getKeyCodeArr(key) {
|
||||
let keyArr = key.split(/\s*\+\s*/)
|
||||
@@ -1,4 +1,5 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import { CONSTANTS } from '../../constants/constant'
|
||||
|
||||
// 事件类
|
||||
class Event extends EventEmitter {
|
||||
@@ -8,6 +9,8 @@ class Event extends EventEmitter {
|
||||
this.opt = opt
|
||||
this.mindMap = opt.mindMap
|
||||
this.isLeftMousedown = false
|
||||
this.isRightMousedown = false
|
||||
this.isMiddleMousedown = false
|
||||
this.mousedownPos = {
|
||||
x: 0,
|
||||
y: 0
|
||||
@@ -26,6 +29,7 @@ class Event extends EventEmitter {
|
||||
|
||||
// 绑定函数上下文
|
||||
bindFn() {
|
||||
this.onBodyClick = this.onBodyClick.bind(this)
|
||||
this.onDrawClick = this.onDrawClick.bind(this)
|
||||
this.onMousedown = this.onMousedown.bind(this)
|
||||
this.onMousemove = this.onMousemove.bind(this)
|
||||
@@ -34,33 +38,36 @@ class Event extends EventEmitter {
|
||||
this.onContextmenu = this.onContextmenu.bind(this)
|
||||
this.onSvgMousedown = this.onSvgMousedown.bind(this)
|
||||
this.onKeyup = this.onKeyup.bind(this)
|
||||
this.onMouseenter = this.onMouseenter.bind(this)
|
||||
this.onMouseleave = this.onMouseleave.bind(this)
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bind() {
|
||||
document.body.addEventListener('click', this.onBodyClick)
|
||||
this.mindMap.svg.on('click', this.onDrawClick)
|
||||
this.mindMap.el.addEventListener('mousedown', this.onMousedown)
|
||||
this.mindMap.svg.on('mousedown', this.onSvgMousedown)
|
||||
window.addEventListener('mousemove', this.onMousemove)
|
||||
window.addEventListener('mouseup', this.onMouseup)
|
||||
// 兼容火狐浏览器
|
||||
if (window.navigator.userAgent.toLowerCase().indexOf('firefox') != -1) {
|
||||
this.mindMap.el.addEventListener('DOMMouseScroll', this.onMousewheel)
|
||||
} else {
|
||||
this.mindMap.el.addEventListener('mousewheel', this.onMousewheel)
|
||||
}
|
||||
this.mindMap.el.addEventListener('wheel', this.onMousewheel)
|
||||
this.mindMap.svg.on('contextmenu', this.onContextmenu)
|
||||
this.mindMap.svg.on('mouseenter', this.onMouseenter)
|
||||
this.mindMap.svg.on('mouseleave', this.onMouseleave)
|
||||
window.addEventListener('keyup', this.onKeyup)
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
unbind() {
|
||||
document.body.removeEventListener('click', this.onBodyClick)
|
||||
this.mindMap.svg.off('click', this.onDrawClick)
|
||||
this.mindMap.el.removeEventListener('mousedown', this.onMousedown)
|
||||
window.removeEventListener('mousemove', this.onMousemove)
|
||||
window.removeEventListener('mouseup', this.onMouseup)
|
||||
this.mindMap.el.removeEventListener('mousewheel', this.onMousewheel)
|
||||
this.mindMap.el.removeEventListener('wheel', this.onMousewheel)
|
||||
this.mindMap.svg.off('contextmenu', this.onContextmenu)
|
||||
this.mindMap.svg.off('mouseenter', this.onMouseenter)
|
||||
this.mindMap.svg.off('mouseleave', this.onMouseleave)
|
||||
window.removeEventListener('keyup', this.onKeyup)
|
||||
}
|
||||
|
||||
@@ -69,6 +76,11 @@ class Event extends EventEmitter {
|
||||
this.emit('draw_click', e)
|
||||
}
|
||||
|
||||
// 页面的单击事件
|
||||
onBodyClick(e) {
|
||||
this.emit('body_click', e)
|
||||
}
|
||||
|
||||
// svg画布的鼠标按下事件
|
||||
onSvgMousedown(e) {
|
||||
this.emit('svg_mousedown', e)
|
||||
@@ -76,10 +88,13 @@ class Event extends EventEmitter {
|
||||
|
||||
// 鼠标按下事件
|
||||
onMousedown(e) {
|
||||
// e.preventDefault()
|
||||
// 鼠标左键
|
||||
if (e.which === 1) {
|
||||
this.isLeftMousedown = true
|
||||
} else if (e.which === 3) {
|
||||
this.isRightMousedown = true
|
||||
} else if (e.which === 2) {
|
||||
this.isMiddleMousedown = true
|
||||
}
|
||||
this.mousedownPos.x = e.clientX
|
||||
this.mousedownPos.y = e.clientY
|
||||
@@ -88,13 +103,19 @@ class Event extends EventEmitter {
|
||||
|
||||
// 鼠标移动事件
|
||||
onMousemove(e) {
|
||||
// e.preventDefault()
|
||||
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
|
||||
this.mousemovePos.x = e.clientX
|
||||
this.mousemovePos.y = e.clientY
|
||||
this.mousemoveOffset.x = e.clientX - this.mousedownPos.x
|
||||
this.mousemoveOffset.y = e.clientY - this.mousedownPos.y
|
||||
this.emit('mousemove', e, this)
|
||||
if (this.isLeftMousedown) {
|
||||
if (
|
||||
this.isMiddleMousedown ||
|
||||
(useLeftKeySelectionRightKeyDrag
|
||||
? this.isRightMousedown
|
||||
: this.isLeftMousedown)
|
||||
) {
|
||||
e.preventDefault()
|
||||
this.emit('drag', e, this)
|
||||
}
|
||||
}
|
||||
@@ -102,6 +123,8 @@ class Event extends EventEmitter {
|
||||
// 鼠标松开事件
|
||||
onMouseup(e) {
|
||||
this.isLeftMousedown = false
|
||||
this.isRightMousedown = false
|
||||
this.isMiddleMousedown = false
|
||||
this.emit('mouseup', e, this)
|
||||
}
|
||||
|
||||
@@ -110,12 +133,17 @@ class Event extends EventEmitter {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
let dir
|
||||
if ((e.wheelDeltaY || e.detail) > 0) {
|
||||
dir = 'up'
|
||||
} else {
|
||||
dir = 'down'
|
||||
if (e.deltaY < 0) dir = CONSTANTS.DIR.UP
|
||||
if (e.deltaY > 0) dir = CONSTANTS.DIR.DOWN
|
||||
if (e.deltaX < 0) dir = CONSTANTS.DIR.LEFT
|
||||
if (e.deltaX > 0) dir = CONSTANTS.DIR.RIGHT
|
||||
// 判断是否是触控板
|
||||
let isTouchPad = false
|
||||
// mac、windows
|
||||
if (e.wheelDeltaY === e.deltaY * -3 || Math.abs(e.wheelDeltaY) <= 10) {
|
||||
isTouchPad = true
|
||||
}
|
||||
this.emit('mousewheel', e, dir, this)
|
||||
this.emit('mousewheel', e, dir, this, isTouchPad)
|
||||
}
|
||||
|
||||
// 鼠标右键菜单事件
|
||||
@@ -128,6 +156,16 @@ class Event extends EventEmitter {
|
||||
onKeyup(e) {
|
||||
this.emit('keyup', e)
|
||||
}
|
||||
|
||||
// 进入
|
||||
onMouseenter(e) {
|
||||
this.emit('svg_mouseenter', e)
|
||||
}
|
||||
|
||||
// 离开
|
||||
onMouseleave(e) {
|
||||
this.emit('svg_mouseleave', e)
|
||||
}
|
||||
}
|
||||
|
||||
export default Event
|
||||
1727
simple-mind-map/src/core/render/Render.js
Normal file
291
simple-mind-map/src/core/render/TextEdit.js
Normal file
@@ -0,0 +1,291 @@
|
||||
import {
|
||||
getStrWithBrFromHtml,
|
||||
checkNodeOuter,
|
||||
focusInput,
|
||||
selectAllInput,
|
||||
htmlEscape
|
||||
} from '../../utils'
|
||||
import { ERROR_TYPES, CONSTANTS } from '../../constants/constant'
|
||||
|
||||
// 节点文字编辑类
|
||||
export default class TextEdit {
|
||||
// 构造函数
|
||||
constructor(renderer) {
|
||||
this.renderer = renderer
|
||||
this.mindMap = renderer.mindMap
|
||||
// 当前编辑的节点
|
||||
this.currentNode = null
|
||||
// 文本编辑框
|
||||
this.textEditNode = null
|
||||
// 文本编辑框是否显示
|
||||
this.showTextEdit = false
|
||||
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
|
||||
this.cacheEditingText = ''
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 事件
|
||||
bindEvent() {
|
||||
this.show = this.show.bind(this)
|
||||
this.onScale = this.onScale.bind(this)
|
||||
this.onKeydown = this.onKeydown.bind(this)
|
||||
// 节点双击事件
|
||||
this.mindMap.on('node_dblclick', (node, e, isInserting) => {
|
||||
this.show({ node, e, isInserting })
|
||||
})
|
||||
// 点击事件
|
||||
this.mindMap.on('draw_click', () => {
|
||||
// 隐藏文本编辑框
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
this.mindMap.on('body_click', () => {
|
||||
// 隐藏文本编辑框
|
||||
if (this.mindMap.opt.isEndNodeTextEditOnClickOuter) {
|
||||
this.hideEditTextBox()
|
||||
}
|
||||
})
|
||||
this.mindMap.on('svg_mousedown', () => {
|
||||
// 隐藏文本编辑框
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
// 展开收缩按钮点击事件
|
||||
this.mindMap.on('expand_btn_click', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
// 节点激活前事件
|
||||
this.mindMap.on('before_node_active', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
// 鼠标滚动事件
|
||||
this.mindMap.on('mousewheel', () => {
|
||||
if (
|
||||
this.mindMap.opt.mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.MOVE
|
||||
) {
|
||||
this.hideEditTextBox()
|
||||
}
|
||||
})
|
||||
// 注册编辑快捷键
|
||||
this.mindMap.keyCommand.addShortcut('F2', () => {
|
||||
if (this.renderer.activeNodeList.length <= 0) {
|
||||
return
|
||||
}
|
||||
this.show({
|
||||
node: this.renderer.activeNodeList[0]
|
||||
})
|
||||
})
|
||||
this.mindMap.on('scale', this.onScale)
|
||||
// // 监听按键事件,判断是否自动进入文本编辑模式
|
||||
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
|
||||
window.addEventListener('keydown', this.onKeydown)
|
||||
}
|
||||
this.mindMap.on('beforeDestroy', () => {
|
||||
this.unBindEvent()
|
||||
})
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
unBindEvent() {
|
||||
window.removeEventListener('keydown', this.onKeydown)
|
||||
}
|
||||
|
||||
// 按键事件
|
||||
onKeydown(e) {
|
||||
const activeNodeList = this.mindMap.renderer.activeNodeList
|
||||
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
|
||||
const node = activeNodeList[0]
|
||||
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
|
||||
if (node && this.checkIsAutoEnterTextEditKey(e)) {
|
||||
this.show({
|
||||
node,
|
||||
e,
|
||||
isInserting: false,
|
||||
isFromKeyDown: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否是自动进入文本编模式的按钮
|
||||
checkIsAutoEnterTextEditKey(e) {
|
||||
const keyCode = e.keyCode
|
||||
return (
|
||||
(keyCode === 229 ||
|
||||
(keyCode >= 65 && keyCode <= 90) ||
|
||||
(keyCode >= 48 && keyCode <= 57)) &&
|
||||
!this.mindMap.keyCommand.hasCombinationKey(e)
|
||||
)
|
||||
}
|
||||
|
||||
// 注册临时快捷键
|
||||
registerTmpShortcut() {
|
||||
// 注册回车快捷键
|
||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Tab', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
// isInserting:是否是刚创建的节点
|
||||
// isFromKeyDown:是否是在按键事件进入的编辑
|
||||
async show({
|
||||
node,
|
||||
isInserting = false,
|
||||
isFromKeyDown = false,
|
||||
isFromScale = false
|
||||
}) {
|
||||
// 使用了自定义节点内容那么不响应编辑事件
|
||||
if (node.isUseCustomNodeContent()) {
|
||||
return
|
||||
}
|
||||
const { beforeTextEdit } = this.mindMap.opt
|
||||
if (typeof beforeTextEdit === 'function') {
|
||||
let isShow = false
|
||||
try {
|
||||
isShow = await beforeTextEdit(node, isInserting)
|
||||
} catch (error) {
|
||||
isShow = false
|
||||
this.mindMap.opt.errorHandler(ERROR_TYPES.BEFORE_TEXT_EDIT_ERROR, error)
|
||||
}
|
||||
if (!isShow) return
|
||||
}
|
||||
this.currentNode = node
|
||||
const { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
|
||||
this.mindMap.view.translateXY(offsetLeft, offsetTop)
|
||||
const rect = node._textData.node.node.getBoundingClientRect()
|
||||
const params = {
|
||||
node,
|
||||
rect,
|
||||
isInserting,
|
||||
isFromKeyDown,
|
||||
isFromScale
|
||||
}
|
||||
if (this.mindMap.richText) {
|
||||
this.mindMap.richText.showEditText(params)
|
||||
return
|
||||
}
|
||||
this.showEditTextBox(params)
|
||||
}
|
||||
|
||||
// 处理画布缩放
|
||||
onScale() {
|
||||
if (!this.currentNode) return
|
||||
if (this.mindMap.richText) {
|
||||
this.mindMap.richText.cacheEditingText =
|
||||
this.mindMap.richText.getEditText()
|
||||
this.mindMap.richText.showTextEdit = false
|
||||
} else {
|
||||
this.cacheEditingText = this.getEditText()
|
||||
this.showTextEdit = false
|
||||
}
|
||||
this.show({
|
||||
node: this.currentNode,
|
||||
isFromScale: true
|
||||
})
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
showEditTextBox({ node, rect, isInserting, isFromKeyDown, isFromScale }) {
|
||||
if (this.showTextEdit) return
|
||||
const { nodeTextEditZIndex, textAutoWrapWidth, selectTextOnEnterEditText } =
|
||||
this.mindMap.opt
|
||||
if (!isFromScale) {
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
}
|
||||
this.registerTmpShortcut()
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
|
||||
this.textEditNode.setAttribute('contenteditable', true)
|
||||
this.textEditNode.addEventListener('keyup', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.textEditNode.addEventListener('mousedown', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.textEditNode.addEventListener('keydown', e => {
|
||||
if (this.checkIsAutoEnterTextEditKey(e)) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
})
|
||||
const targetNode =
|
||||
this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
let scale = this.mindMap.view.scale
|
||||
let lineHeight = node.style.merge('lineHeight')
|
||||
let fontSize = node.style.merge('fontSize')
|
||||
let textLines = (this.cacheEditingText || node.getData('text'))
|
||||
.split(/\n/gim)
|
||||
.map(item => {
|
||||
return htmlEscape(item)
|
||||
})
|
||||
let isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
|
||||
node.style.domText(this.textEditNode, scale, isMultiLine)
|
||||
this.textEditNode.style.zIndex = nodeTextEditZIndex
|
||||
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
|
||||
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
|
||||
this.textEditNode.style.left = rect.left + 'px'
|
||||
this.textEditNode.style.top = rect.top + 'px'
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.textEditNode.style.maxWidth = textAutoWrapWidth * scale + 'px'
|
||||
if (isMultiLine && lineHeight !== 1) {
|
||||
this.textEditNode.style.transform = `translateY(${
|
||||
-((lineHeight * fontSize - fontSize) / 2) * scale
|
||||
}px)`
|
||||
}
|
||||
this.showTextEdit = true
|
||||
// 选中文本
|
||||
// if (!this.cacheEditingText) {
|
||||
// selectAllInput(this.textEditNode)
|
||||
// }
|
||||
if (isInserting || (selectTextOnEnterEditText && !isFromKeyDown)) {
|
||||
selectAllInput(this.textEditNode)
|
||||
} else {
|
||||
focusInput(this.textEditNode)
|
||||
}
|
||||
this.cacheEditingText = ''
|
||||
}
|
||||
|
||||
// 获取当前正在编辑的内容
|
||||
getEditText() {
|
||||
return getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||
}
|
||||
|
||||
// 隐藏文本编辑框
|
||||
hideEditTextBox() {
|
||||
this.currentNode = null
|
||||
if (this.mindMap.richText) {
|
||||
return this.mindMap.richText.hideEditText()
|
||||
}
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
this.renderer.activeNodeList.forEach(node => {
|
||||
let str = this.getEditText()
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', node, str)
|
||||
if (node.isGeneralization) {
|
||||
// 概要节点
|
||||
node.generalizationBelongNode.updateGeneralization()
|
||||
}
|
||||
this.mindMap.render()
|
||||
})
|
||||
this.mindMap.emit(
|
||||
'hide_text_edit',
|
||||
this.textEditNode,
|
||||
this.renderer.activeNodeList
|
||||
)
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.textEditNode.innerHTML = ''
|
||||
this.textEditNode.style.fontFamily = 'inherit'
|
||||
this.textEditNode.style.fontSize = 'inherit'
|
||||
this.textEditNode.style.fontWeight = 'normal'
|
||||
this.textEditNode.style.transform = 'translateY(0)'
|
||||
this.showTextEdit = false
|
||||
}
|
||||
}
|
||||
1035
simple-mind-map/src/core/render/node/Node.js
Normal file
260
simple-mind-map/src/core/render/node/Shape.js
Normal file
@@ -0,0 +1,260 @@
|
||||
import { Rect, Polygon, Path } from '@svgdotjs/svg.js'
|
||||
import { CONSTANTS } from '../../../constants/constant'
|
||||
|
||||
// 节点形状类
|
||||
export default class Shape {
|
||||
constructor(node) {
|
||||
this.node = node
|
||||
}
|
||||
|
||||
// 形状需要的padding
|
||||
getShapePadding(width, height, paddingX, paddingY) {
|
||||
const shape = this.node.getShape()
|
||||
const defaultPaddingX = 15
|
||||
const defaultPaddingY = 5
|
||||
const actWidth = width + paddingX * 2
|
||||
const actHeight = height + paddingY * 2
|
||||
const actOffset = Math.abs(actWidth - actHeight)
|
||||
switch (shape) {
|
||||
case CONSTANTS.SHAPE.ROUNDED_RECTANGLE:
|
||||
return {
|
||||
paddingX: height > width ? (height - width) / 2 : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case CONSTANTS.SHAPE.DIAMOND:
|
||||
return {
|
||||
paddingX: width / 2,
|
||||
paddingY: height / 2
|
||||
}
|
||||
case CONSTANTS.SHAPE.PARALLELOGRAM:
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE:
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE:
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case CONSTANTS.SHAPE.ELLIPSE:
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: paddingY <= 0 ? defaultPaddingY : 0
|
||||
}
|
||||
case CONSTANTS.SHAPE.CIRCLE:
|
||||
return {
|
||||
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
|
||||
paddingY: actHeight < actWidth ? actOffset / 2 : 0
|
||||
}
|
||||
default:
|
||||
return {
|
||||
paddingX: 0,
|
||||
paddingY: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建形状节点
|
||||
createShape() {
|
||||
const shape = this.node.getShape()
|
||||
let node = null
|
||||
// 矩形
|
||||
if (shape === CONSTANTS.SHAPE.RECTANGLE) {
|
||||
node = this.createRect()
|
||||
} else if (shape === CONSTANTS.SHAPE.DIAMOND) {
|
||||
// 菱形
|
||||
node = this.createDiamond()
|
||||
} else if (shape === CONSTANTS.SHAPE.PARALLELOGRAM) {
|
||||
// 平行四边形
|
||||
node = this.createParallelogram()
|
||||
} else if (shape === CONSTANTS.SHAPE.ROUNDED_RECTANGLE) {
|
||||
// 圆角矩形
|
||||
node = this.createRoundedRectangle()
|
||||
} else if (shape === CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE) {
|
||||
// 八角矩形
|
||||
node = this.createOctagonalRectangle()
|
||||
} else if (shape === CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE) {
|
||||
// 外三角矩形
|
||||
node = this.createOuterTriangularRectangle()
|
||||
} else if (shape === CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE) {
|
||||
// 内三角矩形
|
||||
node = this.createInnerTriangularRectangle()
|
||||
} else if (shape === CONSTANTS.SHAPE.ELLIPSE) {
|
||||
// 椭圆
|
||||
node = this.createEllipse()
|
||||
} else if (shape === CONSTANTS.SHAPE.CIRCLE) {
|
||||
// 圆
|
||||
node = this.createCircle()
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// 获取节点减去节点边框宽度、hover节点边框宽度后的尺寸
|
||||
getNodeSize() {
|
||||
const borderWidth = this.node.getBorderWidth()
|
||||
let { width, height } = this.node
|
||||
width -= borderWidth
|
||||
height -= borderWidth
|
||||
return {
|
||||
width,
|
||||
height
|
||||
}
|
||||
}
|
||||
|
||||
// 创建矩形
|
||||
createRect() {
|
||||
let { width, height } = this.getNodeSize()
|
||||
let borderRadius = this.node.style.merge('borderRadius')
|
||||
return new Path().plot(`
|
||||
M${borderRadius},0
|
||||
L${width - borderRadius},0
|
||||
C${width - borderRadius},0 ${width},${0} ${width},${borderRadius}
|
||||
L${width},${height - borderRadius}
|
||||
C${width},${height - borderRadius} ${width},${height} ${
|
||||
width - borderRadius
|
||||
},${height}
|
||||
L${borderRadius},${height}
|
||||
C${borderRadius},${height} ${0},${height} ${0},${height - borderRadius}
|
||||
L${0},${borderRadius}
|
||||
C${0},${borderRadius} ${0},${0} ${borderRadius},${0}
|
||||
Z
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建菱形
|
||||
createDiamond() {
|
||||
let { width, height } = this.getNodeSize()
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
let topX = halfWidth
|
||||
let topY = 0
|
||||
let rightX = width
|
||||
let rightY = halfHeight
|
||||
let bottomX = halfWidth
|
||||
let bottomY = height
|
||||
let leftX = 0
|
||||
let leftY = halfHeight
|
||||
return new Polygon().plot([
|
||||
[topX, topY],
|
||||
[rightX, rightY],
|
||||
[bottomX, bottomY],
|
||||
[leftX, leftY]
|
||||
])
|
||||
}
|
||||
|
||||
// 创建平行四边形
|
||||
createParallelogram() {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.getNodeSize()
|
||||
return new Polygon().plot([
|
||||
[paddingX, 0],
|
||||
[width, 0],
|
||||
[width - paddingX, height],
|
||||
[0, height]
|
||||
])
|
||||
}
|
||||
|
||||
// 创建圆角矩形
|
||||
createRoundedRectangle() {
|
||||
let { width, height } = this.getNodeSize()
|
||||
let halfHeight = height / 2
|
||||
return new Path().plot(`
|
||||
M${halfHeight},0
|
||||
L${width - halfHeight},0
|
||||
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
|
||||
L${halfHeight},${height}
|
||||
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建八角矩形
|
||||
createOctagonalRectangle() {
|
||||
let w = 5
|
||||
let { width, height } = this.getNodeSize()
|
||||
return new Polygon().plot([
|
||||
[0, w],
|
||||
[w, 0],
|
||||
[width - w, 0],
|
||||
[width, w],
|
||||
[width, height - w],
|
||||
[width - w, height],
|
||||
[w, height],
|
||||
[0, height - w]
|
||||
])
|
||||
}
|
||||
|
||||
// 创建外三角矩形
|
||||
createOuterTriangularRectangle() {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.getNodeSize()
|
||||
return new Polygon().plot([
|
||||
[paddingX, 0],
|
||||
[width - paddingX, 0],
|
||||
[width, height / 2],
|
||||
[width - paddingX, height],
|
||||
[paddingX, height],
|
||||
[0, height / 2]
|
||||
])
|
||||
}
|
||||
|
||||
// 创建内三角矩形
|
||||
createInnerTriangularRectangle() {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.getNodeSize()
|
||||
return new Polygon().plot([
|
||||
[0, 0],
|
||||
[width, 0],
|
||||
[width - paddingX / 2, height / 2],
|
||||
[width, height],
|
||||
[0, height],
|
||||
[paddingX / 2, height / 2]
|
||||
])
|
||||
}
|
||||
|
||||
// 创建椭圆
|
||||
createEllipse() {
|
||||
let { width, height } = this.getNodeSize()
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
return new Path().plot(`
|
||||
M${halfWidth},0
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
|
||||
M${halfWidth},${height}
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建圆
|
||||
createCircle() {
|
||||
let { width, height } = this.getNodeSize()
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
return new Path().plot(`
|
||||
M${halfWidth},0
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
|
||||
M${halfWidth},${height}
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
// 形状列表
|
||||
export const shapeList = [
|
||||
CONSTANTS.SHAPE.RECTANGLE,
|
||||
CONSTANTS.SHAPE.DIAMOND,
|
||||
CONSTANTS.SHAPE.PARALLELOGRAM,
|
||||
CONSTANTS.SHAPE.ROUNDED_RECTANGLE,
|
||||
CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE,
|
||||
CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE,
|
||||
CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE,
|
||||
CONSTANTS.SHAPE.ELLIPSE,
|
||||
CONSTANTS.SHAPE.CIRCLE
|
||||
]
|
||||
247
simple-mind-map/src/core/render/node/Style.js
Normal file
@@ -0,0 +1,247 @@
|
||||
import {
|
||||
checkIsNodeStyleDataKey,
|
||||
generateColorByContent
|
||||
} from '../../../utils/index'
|
||||
|
||||
const rootProp = ['paddingX', 'paddingY']
|
||||
const backgroundStyleProps = [
|
||||
'backgroundColor',
|
||||
'backgroundImage',
|
||||
'backgroundRepeat',
|
||||
'backgroundPosition',
|
||||
'backgroundSize'
|
||||
]
|
||||
|
||||
// 样式类
|
||||
class Style {
|
||||
// 设置背景样式
|
||||
static setBackgroundStyle(el, themeConfig) {
|
||||
// 缓存容器元素原本的样式
|
||||
if (!Style.cacheStyle) {
|
||||
Style.cacheStyle = {}
|
||||
let style = window.getComputedStyle(el)
|
||||
backgroundStyleProps.forEach(prop => {
|
||||
Style.cacheStyle[prop] = style[prop]
|
||||
})
|
||||
}
|
||||
// 设置新样式
|
||||
let {
|
||||
backgroundColor,
|
||||
backgroundImage,
|
||||
backgroundRepeat,
|
||||
backgroundPosition,
|
||||
backgroundSize
|
||||
} = themeConfig
|
||||
el.style.backgroundColor = backgroundColor
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
el.style.backgroundImage = `url(${backgroundImage})`
|
||||
el.style.backgroundRepeat = backgroundRepeat
|
||||
el.style.backgroundPosition = backgroundPosition
|
||||
el.style.backgroundSize = backgroundSize
|
||||
} else {
|
||||
el.style.backgroundImage = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
// 移除背景样式
|
||||
static removeBackgroundStyle(el) {
|
||||
if (!Style.cacheStyle) return
|
||||
backgroundStyleProps.forEach(prop => {
|
||||
el.style[prop] = Style.cacheStyle[prop]
|
||||
})
|
||||
Style.cacheStyle = null
|
||||
}
|
||||
|
||||
// 构造函数
|
||||
constructor(ctx) {
|
||||
this.ctx = ctx
|
||||
}
|
||||
|
||||
// 合并样式
|
||||
merge(prop, root) {
|
||||
let themeConfig = this.ctx.mindMap.themeConfig
|
||||
// 三级及以下节点
|
||||
let defaultConfig = themeConfig.node
|
||||
if (root || rootProp.includes(prop)) {
|
||||
// 直接使用最外层样式
|
||||
defaultConfig = themeConfig
|
||||
} else if (this.ctx.isGeneralization) {
|
||||
// 概要节点
|
||||
defaultConfig = themeConfig.generalization
|
||||
} else if (this.ctx.layerIndex === 0) {
|
||||
// 根节点
|
||||
defaultConfig = themeConfig.root
|
||||
} else if (this.ctx.layerIndex === 1) {
|
||||
// 二级节点
|
||||
defaultConfig = themeConfig.second
|
||||
}
|
||||
// 优先使用节点本身的样式
|
||||
return this.getSelfStyle(prop) !== undefined
|
||||
? this.getSelfStyle(prop)
|
||||
: defaultConfig[prop]
|
||||
}
|
||||
|
||||
// 获取某个样式值
|
||||
getStyle(prop, root) {
|
||||
return this.merge(prop, root)
|
||||
}
|
||||
|
||||
// 获取自身自定义样式
|
||||
getSelfStyle(prop) {
|
||||
return this.ctx.getData(prop)
|
||||
}
|
||||
|
||||
// 矩形
|
||||
rect(node) {
|
||||
this.shape(node)
|
||||
node.radius(this.merge('borderRadius'))
|
||||
}
|
||||
|
||||
// 矩形外的其他形状
|
||||
shape(node) {
|
||||
node.fill({
|
||||
color: this.merge('fillColor')
|
||||
})
|
||||
// 节点使用横线样式,不需要渲染非激活状态的边框样式
|
||||
// if (
|
||||
// !this.ctx.isRoot &&
|
||||
// !this.ctx.isGeneralization &&
|
||||
// this.ctx.mindMap.themeConfig.nodeUseLineStyle &&
|
||||
// !this.ctx.getData('isActive')
|
||||
// ) {
|
||||
// return
|
||||
// }
|
||||
node.stroke({
|
||||
color: this.merge('borderColor'),
|
||||
width: this.merge('borderWidth'),
|
||||
dasharray: this.merge('borderDasharray')
|
||||
})
|
||||
}
|
||||
|
||||
// 文字
|
||||
text(node) {
|
||||
node
|
||||
.fill({
|
||||
color: this.merge('color')
|
||||
})
|
||||
.css({
|
||||
'font-family': this.merge('fontFamily'),
|
||||
'font-size': this.merge('fontSize'),
|
||||
'font-weight': this.merge('fontWeight'),
|
||||
'font-style': this.merge('fontStyle'),
|
||||
'text-decoration': this.merge('textDecoration')
|
||||
})
|
||||
}
|
||||
|
||||
// 生成内联样式
|
||||
createStyleText() {
|
||||
return `
|
||||
color: ${this.merge('color')};
|
||||
font-family: ${this.merge('fontFamily')};
|
||||
font-size: ${this.merge('fontSize') + 'px'};
|
||||
font-weight: ${this.merge('fontWeight')};
|
||||
font-style: ${this.merge('fontStyle')};
|
||||
text-decoration: ${this.merge('textDecoration')}
|
||||
`
|
||||
}
|
||||
|
||||
// 获取文本样式
|
||||
getTextFontStyle() {
|
||||
return {
|
||||
italic: this.merge('fontStyle') === 'italic',
|
||||
bold: this.merge('fontWeight'),
|
||||
fontSize: this.merge('fontSize'),
|
||||
fontFamily: this.merge('fontFamily')
|
||||
}
|
||||
}
|
||||
|
||||
// html文字节点
|
||||
domText(node, fontSizeScale = 1, isMultiLine) {
|
||||
node.style.fontFamily = this.merge('fontFamily')
|
||||
node.style.fontSize = this.merge('fontSize') * fontSizeScale + 'px'
|
||||
node.style.fontWeight = this.merge('fontWeight') || 'normal'
|
||||
node.style.lineHeight = !isMultiLine ? 'normal' : this.merge('lineHeight')
|
||||
node.style.fontStyle = this.merge('fontStyle')
|
||||
}
|
||||
|
||||
// 标签文字
|
||||
tagText(node) {
|
||||
node
|
||||
.fill({
|
||||
color: '#fff'
|
||||
})
|
||||
.css({
|
||||
'font-size': '12px'
|
||||
})
|
||||
}
|
||||
|
||||
// 标签矩形
|
||||
tagRect(node, text, color) {
|
||||
node.fill({
|
||||
color: color || generateColorByContent(text.node.textContent)
|
||||
})
|
||||
}
|
||||
|
||||
// 内置图标
|
||||
iconNode(node) {
|
||||
node.attr({
|
||||
fill: this.merge('color')
|
||||
})
|
||||
}
|
||||
|
||||
// 连线
|
||||
line(node, { width, color, dasharray } = {}) {
|
||||
node.stroke({ width, color, dasharray }).fill({ color: 'none' })
|
||||
}
|
||||
|
||||
// 概要连线
|
||||
generalizationLine(node) {
|
||||
node
|
||||
.stroke({
|
||||
width: this.merge('generalizationLineWidth', true),
|
||||
color: this.merge('generalizationLineColor', true)
|
||||
})
|
||||
.fill({ color: 'none' })
|
||||
}
|
||||
|
||||
// 展开收起按钮
|
||||
iconBtn(node, node2, fillNode) {
|
||||
let { color, fill, fontSize, fontColor } = this.ctx.mindMap.opt
|
||||
.expandBtnStyle || {
|
||||
color: '#808080',
|
||||
fill: '#fff',
|
||||
fontSize: 12,
|
||||
strokeColor: '#333333',
|
||||
fontColor: '#333333'
|
||||
}
|
||||
node.fill({ color: color })
|
||||
node2.fill({ color: color })
|
||||
fillNode.fill({ color: fill })
|
||||
if (this.ctx.mindMap.opt.isShowExpandNum) {
|
||||
node.attr({ 'font-size': fontSize, 'font-color': fontColor })
|
||||
}
|
||||
}
|
||||
|
||||
// 是否设置了自定义的样式
|
||||
hasCustomStyle() {
|
||||
let res = false
|
||||
Object.keys(this.ctx.getData()).forEach(item => {
|
||||
if (checkIsNodeStyleDataKey(item)) {
|
||||
res = true
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// hover和激活节点
|
||||
hoverNode(node) {
|
||||
const { hoverRectColor } = this.ctx.mindMap.opt
|
||||
node.radius(5).fill('none').stroke({
|
||||
color: hoverRectColor
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Style.cacheStyle = null
|
||||
|
||||
export default Style
|
||||
62
simple-mind-map/src/core/render/node/nodeCommandWraps.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// 设置数据
|
||||
function setData(data = {}) {
|
||||
this.mindMap.execCommand('SET_NODE_DATA', this, data)
|
||||
}
|
||||
|
||||
// 设置文本
|
||||
function setText(text, richText, resetRichText) {
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText, resetRichText)
|
||||
}
|
||||
|
||||
// 设置图片
|
||||
function setImage(imgData) {
|
||||
this.mindMap.execCommand('SET_NODE_IMAGE', this, imgData)
|
||||
}
|
||||
|
||||
// 设置图标
|
||||
function setIcon(icons) {
|
||||
this.mindMap.execCommand('SET_NODE_ICON', this, icons)
|
||||
}
|
||||
|
||||
// 设置超链接
|
||||
function setHyperlink(link, title) {
|
||||
this.mindMap.execCommand('SET_NODE_HYPERLINK', this, link, title)
|
||||
}
|
||||
|
||||
// 设置备注
|
||||
function setNote(note) {
|
||||
this.mindMap.execCommand('SET_NODE_NOTE', this, note)
|
||||
}
|
||||
|
||||
// 设置标签
|
||||
function setTag(tag) {
|
||||
this.mindMap.execCommand('SET_NODE_TAG', this, tag)
|
||||
}
|
||||
|
||||
// 设置形状
|
||||
function setShape(shape) {
|
||||
this.mindMap.execCommand('SET_NODE_SHAPE', this, shape)
|
||||
}
|
||||
|
||||
// 修改某个样式
|
||||
function setStyle(prop, value) {
|
||||
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value)
|
||||
}
|
||||
|
||||
// 修改多个样式
|
||||
function setStyles(style) {
|
||||
this.mindMap.execCommand('SET_NODE_STYLES', this, style)
|
||||
}
|
||||
|
||||
export default {
|
||||
setData,
|
||||
setText,
|
||||
setImage,
|
||||
setIcon,
|
||||
setHyperlink,
|
||||
setNote,
|
||||
setTag,
|
||||
setShape,
|
||||
setStyle,
|
||||
setStyles
|
||||
}
|
||||
104
simple-mind-map/src/core/render/node/nodeCooperate.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Circle, G, Text, Image } from '@svgdotjs/svg.js'
|
||||
import { generateColorByContent } from '../../../utils/index'
|
||||
|
||||
// 协同相关功能
|
||||
|
||||
// 创建容器
|
||||
function createUserListNode() {
|
||||
// 如果没有注册协作插件,那么需要创建
|
||||
if (!this.mindMap.cooperate) return
|
||||
this._userListGroup = new G()
|
||||
this.group.add(this._userListGroup)
|
||||
}
|
||||
|
||||
// 创建文本头像
|
||||
function createTextAvatar(item) {
|
||||
const { avatarSize, fontSize } = this.mindMap.opt.cooperateStyle
|
||||
const g = new G()
|
||||
const str = item.isMore ? item.name : String(item.name)[0]
|
||||
// 圆
|
||||
const circle = new Circle().size(avatarSize, avatarSize)
|
||||
circle.fill({
|
||||
color: item.color || generateColorByContent(str)
|
||||
})
|
||||
// 文本
|
||||
const text = new Text()
|
||||
.text(str)
|
||||
.fill({
|
||||
color: '#fff'
|
||||
})
|
||||
.css({
|
||||
'font-size': fontSize
|
||||
})
|
||||
.dx(-fontSize / 2)
|
||||
.dy((avatarSize - fontSize) / 2)
|
||||
g.add(circle).add(text)
|
||||
return g
|
||||
}
|
||||
|
||||
// 创建图片头像
|
||||
function createImageAvatar(item) {
|
||||
const { avatarSize } = this.mindMap.opt.cooperateStyle
|
||||
return new Image().load(item.avatar).size(avatarSize, avatarSize)
|
||||
}
|
||||
|
||||
// 更新渲染
|
||||
function updateUserListNode() {
|
||||
if (!this._userListGroup) return
|
||||
const { avatarSize } = this.mindMap.opt.cooperateStyle
|
||||
this._userListGroup.clear()
|
||||
// 根据当前节点长度计算最多能显示几个
|
||||
const length = this.userList.length
|
||||
const maxShowCount = Math.floor(this.width / avatarSize)
|
||||
const list = []
|
||||
if (length > maxShowCount) {
|
||||
// 如果当前用户数量比最多能显示的多,最后需要显示一个提示信息
|
||||
list.push(...this.userList.slice(0, maxShowCount - 1), {
|
||||
isMore: true,
|
||||
name: '+' + (length - maxShowCount + 1)
|
||||
})
|
||||
} else {
|
||||
list.push(...this.userList)
|
||||
}
|
||||
list.forEach((item, index) => {
|
||||
let node = null
|
||||
if (item.avatar) {
|
||||
node = this.createImageAvatar(item)
|
||||
} else {
|
||||
node = this.createTextAvatar(item)
|
||||
}
|
||||
node.x(index * avatarSize).cy(-avatarSize / 2)
|
||||
this._userListGroup.add(node)
|
||||
})
|
||||
}
|
||||
|
||||
// 添加用户
|
||||
function addUser(userInfo) {
|
||||
if (
|
||||
this.userList.find(item => {
|
||||
return item.id == userInfo.id
|
||||
})
|
||||
)
|
||||
return
|
||||
this.userList.push(userInfo)
|
||||
this.updateUserListNode()
|
||||
}
|
||||
|
||||
// 移除用户
|
||||
function removeUser(userInfo) {
|
||||
const index = this.userList.findIndex(item => {
|
||||
return item.id == userInfo.id
|
||||
})
|
||||
if (index === -1) return
|
||||
this.userList.splice(index, 1)
|
||||
this.updateUserListNode()
|
||||
}
|
||||
|
||||
export default {
|
||||
createUserListNode,
|
||||
updateUserListNode,
|
||||
createTextAvatar,
|
||||
createImageAvatar,
|
||||
addUser,
|
||||
removeUser
|
||||
}
|
||||