mirror of
https://github.com/wanglin2/mind-map.git
synced 2026-02-18 22:38:46 +08:00
Compare commits
522 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
161
README.md
161
README.md
@@ -11,13 +11,13 @@
|
||||
|
||||
本项目包含两部分:
|
||||
|
||||
1.一个js思维导图库,不依赖任何框架,你可以使用它来快速完成Web思维导图产品的开发。
|
||||
1.一个 js 思维导图库,不依赖任何框架,你可以使用它来快速完成 Web 思维导图产品的开发。
|
||||
|
||||
开发文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)
|
||||
开发文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)。
|
||||
|
||||
2.一个Web思维导图,基于思维导图库、Vue2.x、ElementUI开发,可以操作电脑本地文件,所以你可以直接把它当做一个在线版思维导图应用使用,如果觉得github的响应速度慢,你也可以部署到你的服务器上。
|
||||
2.一个 Web 思维导图,基于思维导图库、Vue2.x、ElementUI 开发,可以操作电脑本地文件,所以你可以直接把它当做一个在线版思维导图应用使用,如果觉得 github 的响应速度慢,你也可以部署到你的服务器上。
|
||||
|
||||
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
|
||||
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)。
|
||||
|
||||
另外也提供了客户端可供下载使用,支持`Windows`、`Mac`及`Linux`,下载地址:
|
||||
|
||||
@@ -25,21 +25,21 @@ Github:[releases](https://github.com/wanglin2/mind-map/releases)。
|
||||
|
||||
百度云盘:[地址](https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3)。
|
||||
|
||||
> 客户端版本会落后于在线版本,尝试最新功能请优先使用在线版。
|
||||
|
||||
# 特性
|
||||
|
||||
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
|
||||
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴、鱼骨图六种结构
|
||||
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴(横向、竖向)、鱼骨图等结构
|
||||
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
|
||||
- [x] 支持快捷键
|
||||
- [x] 节点内容支持图片、图标、超链接、备注、标签、概要
|
||||
- [x] 支持前进后退
|
||||
- [x] 支持拖动、缩放
|
||||
- [x] 支持右键和Ctrl+左键两种多选方式
|
||||
- [x] 支持节点自由拖拽、拖拽调整
|
||||
- [x] 支持多种节点形状
|
||||
- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`,支持从`json`、`xmind`、`markdown`导入
|
||||
- [x] 支持小地图、支持水印
|
||||
- [x] 支持关联线
|
||||
- [x] 节点内容支持文本(普通文本、富文本)、图片、图标、超链接、备注、标签、概要、数学公式
|
||||
- [x] 节点支持拖拽(拖拽移动、自由调整)、多种节点形状,支持使用 DDM 完全自定义节点内容
|
||||
- [x] 支持画布拖动、缩放
|
||||
- [x] 支持鼠标按键拖动选择和Ctrl+左键两种多选节点方式
|
||||
- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`、`xmind`,支持从`json`、`xmind`、`markdown`导入
|
||||
- [x] 支持快捷键、前进后退、关联线、搜索替换、小地图、水印、滚动条
|
||||
- [x] 提供丰富的配置,满足各种场景各种使用习惯
|
||||
- [x] 支持协同编辑
|
||||
|
||||
# 安装
|
||||
|
||||
@@ -86,19 +86,144 @@ const mindMap = new MindMap({
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
[MIT](./LICENSE)
|
||||
|
||||
# 微信交流群
|
||||
|
||||
<img src="./qrcode.jpg" style="width: 300px" />
|
||||
群聊人数较多,无法通过二维码入群,可以微信添加`wanglinguanfang`拉你入群。
|
||||
|
||||
# 请作者喝杯咖啡
|
||||
|
||||
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡~
|
||||
|
||||
> 厚椰乳一盒 + 纯牛奶半盒 + 冰块 + 咖啡液 = 生椰拿铁 yyds
|
||||
|
||||
> 转账请备注哦~你的头像和名称会出现在[文档页面](https://wanglin2.github.io/mind-map/#/doc/zh/introduction/%E8%AF%B7%E4%BD%9C%E8%80%85%E5%96%9D%E6%9D%AF%E5%92%96%E5%95%A1)
|
||||
> 推荐使用支付宝,微信获取不到头像。转账请备注【思维导图】。
|
||||
|
||||
<p>
|
||||
<img src="./web/src/assets/img/alipay.jpg" style="width: 300px" />
|
||||
<img src="./web/src/assets/img/wechat.jpg" style="width: 300px" />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/Think.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>Think</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/志斌.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>志斌</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/小土渣的宇宙.jpeg" style="width: 50px;height: 50px;" />
|
||||
<span>小土渣的宇宙</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/qp.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>qp</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/ZXR.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>ZXR</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/花儿朵朵.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>花儿朵朵</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/suka.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>suka</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/Chris.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>Chris</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/水车.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>水车</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>仓鼠</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/千帆.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>千帆</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/才镇.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>才镇</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/小米.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>小米bbᯤ²ᴳ</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/棐.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>*棐</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
||||
<span>Luke</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/布林.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>布林</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/南风.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>南风</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/蜉蝣撼大叔.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>蜉蝣撼大叔</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/乙.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>乙</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/敏.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>敏</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/沐风牧草.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>沐风牧草</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/有希.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>有希</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/樊笼.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>樊笼</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/达仁科技.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>达仁科技</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/小逗比.png" style="width: 50px;height: 50px;" />
|
||||
<span>小逗比</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/天清如愿.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>天清如愿</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/敬明朗.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>敬明朗</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
||||
<span>飞箭</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/戚永峰.png" style="width: 50px;height: 50px;" />
|
||||
<span>戚永峰</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/moom.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>moom</span>
|
||||
</span>
|
||||
</p>
|
||||
70
index.html
70
index.html
File diff suppressed because one or more lines are too long
BIN
qrcode.jpg
BIN
qrcode.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
152
simple-mind-map/bin/wsServer.mjs
Normal file
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)
|
||||
@@ -2,29 +2,48 @@ 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)
|
||||
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
|
||||
export default MindMap
|
||||
|
||||
@@ -7,157 +7,51 @@ 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 } from './src/constants/constant'
|
||||
import {
|
||||
layoutValueList,
|
||||
CONSTANTS,
|
||||
commonCaches,
|
||||
ERROR_TYPES,
|
||||
cssContent
|
||||
} from './src/constants/constant'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import { simpleDeepClone } from './src/utils'
|
||||
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
|
||||
|
||||
// 默认选项配置
|
||||
const defaultOpt = {
|
||||
// 是否只读
|
||||
readonly: false,
|
||||
// 布局
|
||||
layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
|
||||
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
|
||||
fishboneDeg: 45,
|
||||
// 主题
|
||||
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(){}
|
||||
}
|
||||
*/
|
||||
// 是否开启节点自由拖拽
|
||||
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.ZOOM,// zoom(放大缩小)、move(上下移动)
|
||||
// 当mousewheelAction设为move时,可以通过该属性控制鼠标滚动一下视图移动的步长,单位px
|
||||
mousewheelMoveStep: 100,
|
||||
// 默认插入的二级节点的文字
|
||||
defaultInsertSecondLevelNodeText: '二级节点',
|
||||
// 默认插入的二级以下节点的文字
|
||||
defaultInsertBelowSecondLevelNodeText: '分支主题',
|
||||
// 展开收起按钮的颜色
|
||||
expandBtnStyle: {
|
||||
color: '#808080',
|
||||
fill: '#fff'
|
||||
},
|
||||
// 自定义展开收起按钮的图标
|
||||
expandBtnIcon: {
|
||||
open: '',// svg字符串
|
||||
close: ''
|
||||
},
|
||||
// 是否只有当鼠标在画布内才响应快捷键事件
|
||||
enableShortcutOnlyWhenMouseInSvg: true,
|
||||
// 是否开启节点动画过渡
|
||||
enableNodeTransitionMove: true,
|
||||
// 如果开启节点动画过渡,可以通过该属性设置过渡的时间,单位ms
|
||||
nodeTransitionMoveDuration: 300,
|
||||
// 初始根节点的位置
|
||||
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
|
||||
}
|
||||
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 = 1
|
||||
this.initContainer()
|
||||
|
||||
// 初始化主题
|
||||
this.initTheme()
|
||||
|
||||
// 初始化缓存数据
|
||||
this.initCache()
|
||||
|
||||
// 事件类
|
||||
this.event = new Event({
|
||||
mindMap: this
|
||||
@@ -180,20 +74,19 @@ class MindMap {
|
||||
|
||||
// 视图操作类
|
||||
this.view = new View({
|
||||
mindMap: this,
|
||||
draw: this.draw
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 批量执行类
|
||||
this.batchExecution = new BatchExecution()
|
||||
|
||||
// 注册插件
|
||||
MindMap.pluginList.forEach((plugin) => {
|
||||
MindMap.pluginList.forEach(plugin => {
|
||||
this.initPlugin(plugin)
|
||||
})
|
||||
|
||||
// 初始渲染
|
||||
this.render()
|
||||
this.render(this.opt.fit ? () => this.view.fit() : () => {})
|
||||
setTimeout(() => {
|
||||
this.command.addHistory()
|
||||
}, 0)
|
||||
@@ -201,6 +94,8 @@ class MindMap {
|
||||
|
||||
// 配置参数处理
|
||||
handleOpt(opt) {
|
||||
// 深拷贝一份节点数据
|
||||
opt.data = simpleDeepClone(opt.data || {})
|
||||
// 检查布局配置
|
||||
if (!layoutValueList.includes(opt.layout)) {
|
||||
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
@@ -210,6 +105,59 @@ class MindMap {
|
||||
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(callback, source = '') {
|
||||
this.batchExecution.push('render', () => {
|
||||
@@ -222,19 +170,27 @@ class MindMap {
|
||||
// 重新渲染
|
||||
reRender(callback, source = '') {
|
||||
this.batchExecution.push('render', () => {
|
||||
this.draw.clear()
|
||||
this.clearDraw()
|
||||
this.initTheme()
|
||||
this.renderer.reRender = true
|
||||
this.renderer.render(callback, source)
|
||||
})
|
||||
}
|
||||
|
||||
// 容器尺寸变化,调整尺寸
|
||||
resize() {
|
||||
// 获取或更新容器尺寸位置信息
|
||||
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.getElRectInfo()
|
||||
this.svg.size(this.width, this.height)
|
||||
this.emit('resize')
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
@@ -252,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() {
|
||||
// 合并主题配置
|
||||
@@ -261,10 +234,13 @@ class MindMap {
|
||||
}
|
||||
|
||||
// 设置主题
|
||||
setTheme(theme) {
|
||||
this.renderer.clearAllActive()
|
||||
setTheme(theme, notRender = false) {
|
||||
this.execCommand('CLEAR_ACTIVE_NODE')
|
||||
this.opt.theme = theme
|
||||
this.render(null, CONSTANTS.CHANGE_THEME)
|
||||
if (!notRender) {
|
||||
this.render(null, CONSTANTS.CHANGE_THEME)
|
||||
}
|
||||
this.emit('view_theme_change', theme)
|
||||
}
|
||||
|
||||
// 获取当前主题
|
||||
@@ -273,11 +249,15 @@ class MindMap {
|
||||
}
|
||||
|
||||
// 设置主题配置
|
||||
setThemeConfig(config) {
|
||||
setThemeConfig(config, notRender = false) {
|
||||
// 计算改变了的配置
|
||||
const changedConfig = getObjectChangedProps(this.themeConfig, config)
|
||||
this.opt.themeConfig = config
|
||||
// 检查改变的是否是节点大小无关的主题属性
|
||||
let res = checkIsNodeSizeIndependenceConfig(config)
|
||||
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
|
||||
if (!notRender) {
|
||||
// 检查改变的是否是节点大小无关的主题属性
|
||||
let res = checkIsNodeSizeIndependenceConfig(changedConfig)
|
||||
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取自定义主题配置
|
||||
@@ -306,7 +286,7 @@ class MindMap {
|
||||
}
|
||||
|
||||
// 设置布局结构
|
||||
setLayout(layout) {
|
||||
setLayout(layout, notRender = false) {
|
||||
// 检查布局配置
|
||||
if (!layoutValueList.includes(layout)) {
|
||||
layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
@@ -314,7 +294,9 @@ class MindMap {
|
||||
this.opt.layout = layout
|
||||
this.view.reset()
|
||||
this.renderer.setLayout()
|
||||
this.render()
|
||||
if (!notRender) {
|
||||
this.render(null, CONSTANTS.CHANGE_LAYOUT)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
@@ -324,15 +306,13 @@ class MindMap {
|
||||
|
||||
// 动态设置思维导图数据,纯节点数据
|
||||
setData(data) {
|
||||
data = simpleDeepClone(data || {})
|
||||
this.execCommand('CLEAR_ACTIVE_NODE')
|
||||
this.command.clearHistory()
|
||||
this.command.addHistory()
|
||||
if (this.richText) {
|
||||
this.renderer.renderTree = this.richText.handleSetData(data)
|
||||
} else {
|
||||
this.renderer.renderTree = data
|
||||
}
|
||||
this.renderer.setData(data)
|
||||
this.reRender(() => {}, CONSTANTS.SET_DATA)
|
||||
this.emit('set_data', data)
|
||||
}
|
||||
|
||||
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
|
||||
@@ -358,7 +338,7 @@ class MindMap {
|
||||
|
||||
// 获取思维导图数据,节点树、主题、布局等
|
||||
getData(withConfig) {
|
||||
let nodeData = this.command.removeDataUid(this.command.getCopyData())
|
||||
let nodeData = this.command.getCopyData()
|
||||
let data = {}
|
||||
if (withConfig) {
|
||||
data = {
|
||||
@@ -378,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)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换位置
|
||||
@@ -398,28 +382,28 @@ class MindMap {
|
||||
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 } = {}) {
|
||||
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.el.getBoundingClientRect()
|
||||
const elRect = this.elRect
|
||||
// 去除放大缩小的变换效果
|
||||
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
|
||||
// 获取变换后的位置尺寸信息,其实是getBoundingClientRect方法的包装方法
|
||||
const rect = draw.rbox()
|
||||
// 内边距
|
||||
rect.width += paddingX
|
||||
rect.height += paddingY
|
||||
draw.translate(paddingX / 2, paddingY / 2)
|
||||
rect.width += paddingX * 2
|
||||
rect.height += paddingY * 2
|
||||
draw.translate(paddingX, paddingY)
|
||||
// 将svg设置为实际内容的宽高
|
||||
svg.size(rect.width, rect.height)
|
||||
// 把实际内容变换
|
||||
@@ -427,7 +411,12 @@ class MindMap {
|
||||
// 克隆一份数据
|
||||
let clone = svg.clone()
|
||||
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
|
||||
if ((rect.width > origWidth || rect.height > origHeight) && this.watermark && this.watermark.hasWatermark()) {
|
||||
if (
|
||||
!ignoreWatermark &&
|
||||
(rect.width > origWidth || rect.height > origHeight) &&
|
||||
this.watermark &&
|
||||
this.watermark.hasWatermark()
|
||||
) {
|
||||
this.width = rect.width
|
||||
this.height = rect.height
|
||||
this.watermark.draw()
|
||||
@@ -436,6 +425,16 @@ class MindMap {
|
||||
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)
|
||||
@@ -487,8 +486,12 @@ class MindMap {
|
||||
|
||||
// 销毁
|
||||
destroy() {
|
||||
this.emit('beforeDestroy')
|
||||
// 移除插件
|
||||
[...MindMap.pluginList].forEach((plugin) => {
|
||||
;[...MindMap.pluginList].forEach(plugin => {
|
||||
if (this[plugin.instanceName].beforePluginDestroy) {
|
||||
this[plugin.instanceName].beforePluginDestroy()
|
||||
}
|
||||
this[plugin.instanceName] = null
|
||||
})
|
||||
// 解绑事件
|
||||
@@ -497,19 +500,22 @@ class MindMap {
|
||||
this.svg.remove()
|
||||
// 去除给容器元素设置的背景样式
|
||||
Style.removeBackgroundStyle(this.el)
|
||||
this.el.innerHTML = ''
|
||||
this.el = null
|
||||
this.removeCss()
|
||||
}
|
||||
}
|
||||
|
||||
// 插件列表
|
||||
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) => {
|
||||
MindMap.hasPlugin = plugin => {
|
||||
return MindMap.pluginList.findIndex(item => {
|
||||
return item === plugin
|
||||
})
|
||||
}
|
||||
|
||||
749
simple-mind-map/package-lock.json
generated
749
simple-mind-map/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.6.2",
|
||||
"version": "0.9.0",
|
||||
"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,21 +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",
|
||||
"main": "./dist/simpleMindMap.umd.min.js",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.0.16",
|
||||
"deepmerge": "^1.5.2",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"jszip": "^3.10.1",
|
||||
"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"
|
||||
"xml-js": "^1.6.11",
|
||||
"y-webrtc": "^10.2.5",
|
||||
"yjs": "^13.6.8"
|
||||
},
|
||||
"keywords": [
|
||||
"javascript",
|
||||
|
||||
@@ -17,7 +17,7 @@ const transform = dir => {
|
||||
}
|
||||
|
||||
const transformFile = file => {
|
||||
console.log(file);
|
||||
console.log(file)
|
||||
let content = fs.readFileSync(file, 'utf-8')
|
||||
countCodeLines(content)
|
||||
// transformComments(file, content)
|
||||
@@ -25,7 +25,7 @@ const transformFile = file => {
|
||||
|
||||
// 统计代码行数
|
||||
let totalLines = 0
|
||||
const countCodeLines = (content) => {
|
||||
const countCodeLines = content => {
|
||||
totalLines += content.split(/\n/).length
|
||||
}
|
||||
|
||||
@@ -43,4 +43,4 @@ const transformComments = (file, content) => {
|
||||
|
||||
transform(entryPath)
|
||||
transformFile(path.join(__dirname, '../index.js'))
|
||||
console.log(totalLines);
|
||||
console.log(totalLines)
|
||||
|
||||
@@ -1,162 +1,171 @@
|
||||
// 标签颜色列表
|
||||
export const tagColorList = [
|
||||
{
|
||||
color: 'rgb(77, 65, 0)',
|
||||
background: 'rgb(255, 244, 179)'
|
||||
},
|
||||
{
|
||||
color: 'rgb(0, 50, 77)',
|
||||
background: 'rgb(179, 229, 255)'
|
||||
},
|
||||
{
|
||||
color: 'rgb(77, 0, 73)',
|
||||
background: 'rgb(255, 179, 251)'
|
||||
},
|
||||
{
|
||||
color: 'rgb(57, 77, 0)',
|
||||
background: 'rgb(236, 255, 179)'
|
||||
},
|
||||
{
|
||||
color: 'rgb(0, 77, 47)',
|
||||
background: 'rgb(179, 255, 226)'
|
||||
}
|
||||
]
|
||||
|
||||
// 主题列表
|
||||
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: {
|
||||
@@ -170,7 +179,8 @@ export const CONSTANTS = {
|
||||
CATALOG_ORGANIZATION: 'catalogOrganization',
|
||||
TIMELINE: 'timeline',
|
||||
TIMELINE2: 'timeline2',
|
||||
FISHBONE: 'fishbone'
|
||||
FISHBONE: 'fishbone',
|
||||
VERTICAL_TIMELINE: 'verticalTimeline'
|
||||
},
|
||||
DIR: {
|
||||
UP: 'up',
|
||||
@@ -206,9 +216,19 @@ export const CONSTANTS = {
|
||||
BOTTOM: 'bottom',
|
||||
CENTER: 'center'
|
||||
},
|
||||
TIMELINE_DIR: {
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,38 +237,42 @@ export const initRootNodePositionMap = {
|
||||
[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,
|
||||
[CONSTANTS.INIT_ROOT_NODE_POSITION.CENTER]: 0.5
|
||||
}
|
||||
|
||||
// 布局结构列表
|
||||
export const layoutList = [
|
||||
{
|
||||
name: '逻辑结构图',
|
||||
value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
|
||||
value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
},
|
||||
{
|
||||
name: '思维导图',
|
||||
value: CONSTANTS.LAYOUT.MIND_MAP,
|
||||
value: CONSTANTS.LAYOUT.MIND_MAP
|
||||
},
|
||||
{
|
||||
name: '组织结构图',
|
||||
value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
|
||||
value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE
|
||||
},
|
||||
{
|
||||
name: '目录组织图',
|
||||
value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION,
|
||||
value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION
|
||||
},
|
||||
{
|
||||
name: '时间轴',
|
||||
value: CONSTANTS.LAYOUT.TIMELINE,
|
||||
value: CONSTANTS.LAYOUT.TIMELINE
|
||||
},
|
||||
{
|
||||
name: '时间轴2',
|
||||
value: CONSTANTS.LAYOUT.TIMELINE2,
|
||||
value: CONSTANTS.LAYOUT.TIMELINE2
|
||||
},
|
||||
{
|
||||
name: '竖向时间轴',
|
||||
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE
|
||||
},
|
||||
{
|
||||
name: '鱼骨图',
|
||||
value: CONSTANTS.LAYOUT.FISHBONE,
|
||||
value: CONSTANTS.LAYOUT.FISHBONE
|
||||
}
|
||||
]
|
||||
export const layoutValueList = [
|
||||
@@ -258,6 +282,7 @@ export const layoutValueList = [
|
||||
CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
|
||||
CONSTANTS.LAYOUT.TIMELINE,
|
||||
CONSTANTS.LAYOUT.TIMELINE2,
|
||||
CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
|
||||
CONSTANTS.LAYOUT.FISHBONE
|
||||
]
|
||||
|
||||
@@ -278,5 +303,51 @@ export const nodeDataNoStylePropList = [
|
||||
'richText',
|
||||
'resetRichText',
|
||||
'uid',
|
||||
'activeStyle'
|
||||
]
|
||||
'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;
|
||||
}
|
||||
`
|
||||
|
||||
233
simple-mind-map/src/constants/defaultOptions.js
Normal file
233
simple-mind-map/src/constants/defaultOptions.js
Normal file
@@ -0,0 +1,233 @@
|
||||
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.ZOOM, // zoom(放大缩小)、move(上下移动)
|
||||
// 当mousewheelAction设为move时,可以通过该属性控制鼠标滚动一下视图移动的步长,单位px
|
||||
mousewheelMoveStep: 100,
|
||||
// 当mousewheelAction设为zoom时,默认向前滚动是缩小,向后滚动是放大,如果该属性设为true,那么会反过来
|
||||
mousewheelZoomActionReverse: false,
|
||||
// 默认插入的二级节点的文字
|
||||
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'
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { copyRenderTree, simpleDeepClone, nextTick } from '../../utils'
|
||||
import { copyRenderTree, simpleDeepClone, throttle } from '../../utils'
|
||||
|
||||
// 命令类
|
||||
class Command {
|
||||
@@ -11,7 +11,11 @@ class Command {
|
||||
this.activeHistoryIndex = 0
|
||||
// 注册快捷键
|
||||
this.registerShortcutKeys()
|
||||
this.addHistory = nextTick(this.addHistory, this)
|
||||
this.addHistory = throttle(
|
||||
this.addHistory,
|
||||
this.mindMap.opt.addHistoryTime,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
// 清空历史数据
|
||||
@@ -37,7 +41,11 @@ class Command {
|
||||
this.commands[name].forEach(fn => {
|
||||
fn(...args)
|
||||
})
|
||||
if (['BACK', 'FORWARD', 'SET_NODE_ACTIVE', 'CLEAR_ACTIVE_NODE'].includes(name)) {
|
||||
if (
|
||||
['BACK', 'FORWARD', 'SET_NODE_ACTIVE', 'CLEAR_ACTIVE_NODE'].includes(
|
||||
name
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.addHistory()
|
||||
@@ -78,7 +86,11 @@ class Command {
|
||||
}
|
||||
let data = this.getCopyData()
|
||||
// 此次数据和上次一样则不重复添加
|
||||
if (this.history.length > 0 && JSON.stringify(this.history[this.history.length - 1]) === JSON.stringify(data)) {
|
||||
if (
|
||||
this.history.length > 0 &&
|
||||
JSON.stringify(this.history[this.history.length - 1]) ===
|
||||
JSON.stringify(data)
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 删除当前历史指针后面的数据
|
||||
@@ -89,7 +101,7 @@ class Command {
|
||||
this.history.shift()
|
||||
}
|
||||
this.activeHistoryIndex = this.history.length - 1
|
||||
this.mindMap.emit('data_change', this.removeDataUid(data))
|
||||
this.mindMap.emit('data_change', data)
|
||||
this.mindMap.emit(
|
||||
'back_forward',
|
||||
this.activeHistoryIndex,
|
||||
@@ -110,7 +122,7 @@ class Command {
|
||||
this.history.length
|
||||
)
|
||||
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.mindMap.emit('data_change', this.removeDataUid(data))
|
||||
this.mindMap.emit('data_change', data)
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -123,9 +135,13 @@ class Command {
|
||||
let len = this.history.length
|
||||
if (this.activeHistoryIndex + step <= len - 1) {
|
||||
this.activeHistoryIndex += step
|
||||
this.mindMap.emit('back_forward', this.activeHistoryIndex, this.history.length)
|
||||
this.mindMap.emit(
|
||||
'back_forward',
|
||||
this.activeHistoryIndex,
|
||||
this.history.length
|
||||
)
|
||||
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.mindMap.emit('data_change', this.removeDataUid(data))
|
||||
this.mindMap.emit('data_change', data)
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -138,10 +154,10 @@ class Command {
|
||||
// 移除节点数据中的uid
|
||||
removeDataUid(data) {
|
||||
data = simpleDeepClone(data)
|
||||
let walk = (root) => {
|
||||
let walk = root => {
|
||||
delete root.data.uid
|
||||
if (root.children && root.children.length > 0) {
|
||||
root.children.forEach((item) => {
|
||||
root.children.forEach(item => {
|
||||
walk(item)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export default class KeyCommand {
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
this.onKeydown = this.onKeydown.bind(this)
|
||||
// 只有当鼠标在画布内才响应快捷键
|
||||
this.mindMap.on('svg_mouseenter', () => {
|
||||
this.isInSvg = true
|
||||
@@ -46,24 +47,45 @@ export default class KeyCommand {
|
||||
if (this.mindMap.richText && this.mindMap.richText.showTextEdit) {
|
||||
return
|
||||
}
|
||||
if (this.mindMap.renderer.textEdit.showTextEdit || (this.mindMap.associativeLine && this.mindMap.associativeLine.showTextEdit)) {
|
||||
if (
|
||||
this.mindMap.renderer.textEdit.showTextEdit ||
|
||||
(this.mindMap.associativeLine &&
|
||||
this.mindMap.associativeLine.showTextEdit)
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.isInSvg = false
|
||||
})
|
||||
window.addEventListener('keydown', e => {
|
||||
if (this.isPause || (this.mindMap.opt.enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) {
|
||||
return
|
||||
}
|
||||
Object.keys(this.shortcutMap).forEach(key => {
|
||||
if (this.checkKey(e, key)) {
|
||||
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()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,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*/)
|
||||
|
||||
@@ -10,6 +10,7 @@ class Event extends EventEmitter {
|
||||
this.mindMap = opt.mindMap
|
||||
this.isLeftMousedown = false
|
||||
this.isRightMousedown = false
|
||||
this.isMiddleMousedown = false
|
||||
this.mousedownPos = {
|
||||
x: 0,
|
||||
y: 0
|
||||
@@ -92,6 +93,8 @@ class Event extends EventEmitter {
|
||||
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
|
||||
@@ -107,9 +110,10 @@ class Event extends EventEmitter {
|
||||
this.mousemoveOffset.y = e.clientY - this.mousedownPos.y
|
||||
this.emit('mousemove', e, this)
|
||||
if (
|
||||
useLeftKeySelectionRightKeyDrag
|
||||
this.isMiddleMousedown ||
|
||||
(useLeftKeySelectionRightKeyDrag
|
||||
? this.isRightMousedown
|
||||
: this.isLeftMousedown
|
||||
: this.isLeftMousedown)
|
||||
) {
|
||||
e.preventDefault()
|
||||
this.emit('drag', e, this)
|
||||
@@ -120,6 +124,7 @@ class Event extends EventEmitter {
|
||||
onMouseup(e) {
|
||||
this.isLeftMousedown = false
|
||||
this.isRightMousedown = false
|
||||
this.isMiddleMousedown = false
|
||||
this.emit('mouseup', e, this)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,11 @@
|
||||
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils'
|
||||
import {
|
||||
getStrWithBrFromHtml,
|
||||
checkNodeOuter,
|
||||
focusInput,
|
||||
selectAllInput,
|
||||
htmlEscape
|
||||
} from '../../utils'
|
||||
import { ERROR_TYPES } from '../../constants/constant'
|
||||
|
||||
// 节点文字编辑类
|
||||
export default class TextEdit {
|
||||
@@ -21,6 +28,7 @@ export default class TextEdit {
|
||||
bindEvent() {
|
||||
this.show = this.show.bind(this)
|
||||
this.onScale = this.onScale.bind(this)
|
||||
this.onKeydown = this.onKeydown.bind(this)
|
||||
// 节点双击事件
|
||||
this.mindMap.on('node_dblclick', this.show)
|
||||
// 点击事件
|
||||
@@ -54,6 +62,40 @@ export default class TextEdit {
|
||||
this.show(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, false, true)
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否是自动进入文本编模式的按钮
|
||||
checkIsAutoEnterTextEditKey(e) {
|
||||
const keyCode = e.keyCode
|
||||
return (
|
||||
(keyCode === 229 ||
|
||||
(keyCode >= 65 && keyCode <= 90) ||
|
||||
(keyCode >= 48 && keyCode <= 57)) &&
|
||||
!this.mindMap.keyCommand.hasCombinationKey(e)
|
||||
)
|
||||
}
|
||||
|
||||
// 注册临时快捷键
|
||||
@@ -62,16 +104,27 @@ export default class TextEdit {
|
||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Tab', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
async show(node) {
|
||||
if (typeof this.mindMap.opt.beforeTextEdit === 'function') {
|
||||
// isInserting:是否是刚创建的节点
|
||||
// isFromKeyDown:是否是在按键事件进入的编辑
|
||||
async show(node, e, isInserting = false, isFromKeyDown = false) {
|
||||
// 使用了自定义节点内容那么不响应编辑事件
|
||||
if (node.isUseCustomNodeContent()) {
|
||||
return
|
||||
}
|
||||
let { beforeTextEdit } = this.mindMap.opt
|
||||
if (typeof beforeTextEdit === 'function') {
|
||||
let isShow = false
|
||||
try {
|
||||
isShow = await this.mindMap.opt.beforeTextEdit(node)
|
||||
isShow = await beforeTextEdit(node, isInserting)
|
||||
} catch (error) {
|
||||
isShow = false
|
||||
this.mindMap.opt.errorHandler(ERROR_TYPES.BEFORE_TEXT_EDIT_ERROR, error)
|
||||
}
|
||||
if (!isShow) return
|
||||
}
|
||||
@@ -80,17 +133,18 @@ export default class TextEdit {
|
||||
this.mindMap.view.translateXY(offsetLeft, offsetTop)
|
||||
let rect = node._textData.node.node.getBoundingClientRect()
|
||||
if (this.mindMap.richText) {
|
||||
this.mindMap.richText.showEditText(node, rect)
|
||||
this.mindMap.richText.showEditText(node, rect, isInserting, isFromKeyDown)
|
||||
return
|
||||
}
|
||||
this.showEditTextBox(node, rect)
|
||||
this.showEditTextBox(node, rect, isInserting, isFromKeyDown)
|
||||
}
|
||||
|
||||
// 处理画布缩放
|
||||
onScale() {
|
||||
if (!this.currentNode) return
|
||||
if (this.mindMap.richText) {
|
||||
this.mindMap.richText.cacheEditingText = this.mindMap.richText.getEditText()
|
||||
this.mindMap.richText.cacheEditingText =
|
||||
this.mindMap.richText.getEditText()
|
||||
this.mindMap.richText.showTextEdit = false
|
||||
} else {
|
||||
this.cacheEditingText = this.getEditText()
|
||||
@@ -100,7 +154,10 @@ export default class TextEdit {
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
showEditTextBox(node, rect) {
|
||||
showEditTextBox(node, rect, isInserting, isFromKeyDown) {
|
||||
if (this.showTextEdit) return
|
||||
const { nodeTextEditZIndex, textAutoWrapWidth, selectTextOnEnterEditText } =
|
||||
this.mindMap.opt
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
this.registerTmpShortcut()
|
||||
if (!this.textEditNode) {
|
||||
@@ -113,42 +170,54 @@ export default class TextEdit {
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
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.nodeData.data.text).split(/\n/gim)
|
||||
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 = this.mindMap.opt.nodeTextEditZIndex
|
||||
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 = this.mindMap.opt.textAutoWrapWidth * scale + 'px'
|
||||
this.textEditNode.style.maxWidth = textAutoWrapWidth * scale + 'px'
|
||||
if (isMultiLine && lineHeight !== 1) {
|
||||
this.textEditNode.style.transform = `translateY(${-((lineHeight * fontSize - fontSize) / 2) * scale}px)`
|
||||
this.textEditNode.style.transform = `translateY(${
|
||||
-((lineHeight * fontSize - fontSize) / 2) * scale
|
||||
}px)`
|
||||
}
|
||||
this.showTextEdit = true
|
||||
// 选中文本
|
||||
if (!this.cacheEditingText) {
|
||||
this.selectNodeText()
|
||||
// if (!this.cacheEditingText) {
|
||||
// selectAllInput(this.textEditNode)
|
||||
// }
|
||||
if (isInserting || (selectTextOnEnterEditText && !isFromKeyDown)) {
|
||||
selectAllInput(this.textEditNode)
|
||||
} else {
|
||||
focusInput(this.textEditNode)
|
||||
}
|
||||
this.cacheEditingText = ''
|
||||
}
|
||||
|
||||
// 选中文本
|
||||
selectNodeText() {
|
||||
let selection = window.getSelection()
|
||||
let range = document.createRange()
|
||||
range.selectNodeContents(this.textEditNode)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
|
||||
// 获取当前正在编辑的内容
|
||||
getEditText() {
|
||||
return getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Style from './Style'
|
||||
import Shape from './Shape'
|
||||
import { asyncRun } from '../../../utils'
|
||||
import { G, Rect } from '@svgdotjs/svg.js'
|
||||
import { G, ForeignObject, Rect } from '@svgdotjs/svg.js'
|
||||
import nodeGeneralizationMethods from './nodeGeneralization'
|
||||
import nodeExpandBtnMethods from './nodeExpandBtn'
|
||||
import nodeCommandWrapsMethods from './nodeCommandWraps'
|
||||
import nodeCreateContentsMethods from './nodeCreateContents'
|
||||
import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect'
|
||||
import nodeCooperateMethods from './nodeCooperate'
|
||||
import { CONSTANTS } from '../../../constants/constant'
|
||||
import { copyNodeTree } from '../../../utils/index'
|
||||
|
||||
// 节点类
|
||||
class Node {
|
||||
@@ -14,14 +16,16 @@ class Node {
|
||||
constructor(opt = {}) {
|
||||
// 节点数据
|
||||
this.nodeData = this.handleData(opt.data || {})
|
||||
// id
|
||||
// uid
|
||||
this.uid = opt.uid
|
||||
// 控制实例
|
||||
this.mindMap = opt.mindMap
|
||||
// 渲染实例
|
||||
this.renderer = opt.renderer
|
||||
// 渲染器
|
||||
this.draw = opt.draw || null
|
||||
this.draw = this.mindMap.draw
|
||||
this.nodeDraw = this.mindMap.nodeDraw
|
||||
this.lineDraw = this.mindMap.lineDraw
|
||||
// 样式实例
|
||||
this.style = new Style(this)
|
||||
// 形状实例
|
||||
@@ -55,10 +59,14 @@ class Node {
|
||||
this.parent = opt.parent || null
|
||||
// 子节点
|
||||
this.children = opt.children || []
|
||||
// 当前同时操作该节点的用户列表
|
||||
this.userList = []
|
||||
// 节点内容的容器
|
||||
this.group = null
|
||||
this.shapeNode = null // 节点形状节点
|
||||
this.hoverNode = null // 节点hover和激活的节点
|
||||
// 节点内容对象
|
||||
this._customNodeContent = null
|
||||
this._imgData = null
|
||||
this._iconData = null
|
||||
this._textData = null
|
||||
@@ -66,15 +74,16 @@ class Node {
|
||||
this._tagData = null
|
||||
this._noteData = null
|
||||
this.noteEl = null
|
||||
this.noteContentIsShow = false
|
||||
this._expandBtn = null
|
||||
this._lastExpandBtnType = null
|
||||
this._showExpandBtn = false
|
||||
this._openExpandNode = null
|
||||
this._closeExpandNode = null
|
||||
this._fillExpandNode = null
|
||||
this._userListGroup = null
|
||||
this._lines = []
|
||||
this._generalizationLine = null
|
||||
this._generalizationNode = null
|
||||
this._generalizationList = []
|
||||
this._unVisibleRectRegionNode = null
|
||||
this._isMouseenter = false
|
||||
// 尺寸信息
|
||||
@@ -97,6 +106,8 @@ class Node {
|
||||
this.isMultipleChoice = false
|
||||
// 是否需要重新layout
|
||||
this.needLayout = false
|
||||
// 当前是否是隐藏状态
|
||||
this.isHide = false
|
||||
// 概要相关方法
|
||||
Object.keys(nodeGeneralizationMethods).forEach(item => {
|
||||
this[item] = nodeGeneralizationMethods[item].bind(this)
|
||||
@@ -105,6 +116,10 @@ class Node {
|
||||
Object.keys(nodeExpandBtnMethods).forEach(item => {
|
||||
this[item] = nodeExpandBtnMethods[item].bind(this)
|
||||
})
|
||||
// 展开收起按钮占位元素相关方法
|
||||
Object.keys(nodeExpandBtnPlaceholderRectMethods).forEach(item => {
|
||||
this[item] = nodeExpandBtnPlaceholderRectMethods[item].bind(this)
|
||||
})
|
||||
// 命令的相关方法
|
||||
Object.keys(nodeCommandWrapsMethods).forEach(item => {
|
||||
this[item] = nodeCommandWrapsMethods[item].bind(this)
|
||||
@@ -113,6 +128,12 @@ class Node {
|
||||
Object.keys(nodeCreateContentsMethods).forEach(item => {
|
||||
this[item] = nodeCreateContentsMethods[item].bind(this)
|
||||
})
|
||||
// 协同相关
|
||||
if (this.mindMap.cooperate) {
|
||||
Object.keys(nodeCooperateMethods).forEach(item => {
|
||||
this[item] = nodeCooperateMethods[item].bind(this)
|
||||
})
|
||||
}
|
||||
// 初始化
|
||||
this.getSize()
|
||||
}
|
||||
@@ -154,6 +175,13 @@ class Node {
|
||||
|
||||
// 创建节点的各个内容对象数据
|
||||
createNodeData() {
|
||||
// 自定义节点内容
|
||||
let { isUseCustomNodeContent, customCreateNodeContent } = this.mindMap.opt
|
||||
if (isUseCustomNodeContent && customCreateNodeContent) {
|
||||
this._customNodeContent = customCreateNodeContent(this)
|
||||
}
|
||||
// 如果没有返回内容,那么还是使用内置的节点内容
|
||||
if (this._customNodeContent) return
|
||||
this._imgData = this.createImgNode()
|
||||
this._iconData = this.createIconNode()
|
||||
this._textData = this.createTextNode()
|
||||
@@ -176,6 +204,14 @@ class Node {
|
||||
|
||||
// 计算节点尺寸信息
|
||||
getNodeRect() {
|
||||
// 自定义节点内容
|
||||
if (this.isUseCustomNodeContent()) {
|
||||
let rect = this.measureCustomNodeContentSize(this._customNodeContent)
|
||||
return {
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
}
|
||||
// 宽高
|
||||
let imgContentWidth = 0
|
||||
let imgContentHeight = 0
|
||||
@@ -235,9 +271,11 @@ class Node {
|
||||
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
|
||||
this.shapePadding.paddingX = shapePaddingX
|
||||
this.shapePadding.paddingY = shapePaddingY
|
||||
// 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点
|
||||
const borderWidth = this.getBorderWidth()
|
||||
return {
|
||||
width: _width + paddingX * 2 + shapePaddingX * 2,
|
||||
height: _height + paddingY * 2 + margin + shapePaddingY * 2
|
||||
width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth,
|
||||
height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,27 +283,45 @@ class Node {
|
||||
layout() {
|
||||
// 清除之前的内容
|
||||
this.group.clear()
|
||||
const { hoverRectPadding } = this.mindMap.opt
|
||||
let { width, height, textContentItemMargin } = this
|
||||
let { paddingY } = this.getPaddingVale()
|
||||
paddingY += this.shapePadding.paddingY
|
||||
const halfBorderWidth = this.getBorderWidth() / 2
|
||||
paddingY += this.shapePadding.paddingY + halfBorderWidth
|
||||
// 节点形状
|
||||
this.shapeNode = this.shapeInstance.createShape()
|
||||
this.shapeNode.addClass('smm-node-shape')
|
||||
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
|
||||
this.style.shape(this.shapeNode)
|
||||
this.group.add(this.shapeNode)
|
||||
this.updateNodeShape()
|
||||
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
this._unVisibleRectRegionNode = new Rect()
|
||||
}
|
||||
this._unVisibleRectRegionNode.fill({
|
||||
color: 'transparent'
|
||||
}).size(this.expandBtnSize, height).x(width).y(0)
|
||||
this.group.add(this._unVisibleRectRegionNode)
|
||||
}
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
// 创建协同头像节点
|
||||
if (this.createUserListNode) this.createUserListNode()
|
||||
// 概要节点添加一个带所属节点id的类名
|
||||
if (this.isGeneralization && this.generalizationBelongNode) {
|
||||
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
|
||||
}
|
||||
// 激活hover和激活边框
|
||||
const addHoverNode = () => {
|
||||
this.hoverNode = new Rect()
|
||||
.size(width + hoverRectPadding * 2, height + hoverRectPadding * 2)
|
||||
.x(-hoverRectPadding)
|
||||
.y(-hoverRectPadding)
|
||||
this.hoverNode.addClass('smm-hover-node')
|
||||
this.style.hoverNode(this.hoverNode, width, height)
|
||||
this.group.add(this.hoverNode)
|
||||
}
|
||||
// 如果存在自定义节点内容,那么使用自定义节点内容
|
||||
if (this.isUseCustomNodeContent()) {
|
||||
let foreignObject = new ForeignObject()
|
||||
foreignObject.width(width)
|
||||
foreignObject.height(height)
|
||||
foreignObject.add(this._customNodeContent)
|
||||
this.group.add(foreignObject)
|
||||
addHoverNode()
|
||||
return
|
||||
}
|
||||
// 图片节点
|
||||
let imgHeight = 0
|
||||
if (this._imgData) {
|
||||
@@ -337,6 +393,7 @@ class Node {
|
||||
: 0)
|
||||
)
|
||||
this.group.add(textContentNested)
|
||||
addHoverNode()
|
||||
}
|
||||
|
||||
// 给节点绑定事件
|
||||
@@ -352,49 +409,67 @@ class Node {
|
||||
this.active(e)
|
||||
})
|
||||
this.group.on('mousedown', e => {
|
||||
if (this.isRoot && e.which === 3) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
if (!this.isRoot) {
|
||||
e.stopPropagation()
|
||||
const {
|
||||
readonly,
|
||||
enableCtrlKeyNodeSelection,
|
||||
useLeftKeySelectionRightKeyDrag
|
||||
} = this.mindMap.opt
|
||||
// 只读模式不需要阻止冒泡
|
||||
if (!readonly) {
|
||||
if (this.isRoot) {
|
||||
// 根节点,右键拖拽画布模式下不需要阻止冒泡
|
||||
if (e.which === 3 && !useLeftKeySelectionRightKeyDrag) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
} else {
|
||||
// 非根节点,且按下的是非鼠标中键,需要阻止事件冒泡
|
||||
if (e.which !== 2) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
// 多选和取消多选
|
||||
if (e.ctrlKey && this.mindMap.opt.enableCtrlKeyNodeSelection) {
|
||||
if (e.ctrlKey && enableCtrlKeyNodeSelection) {
|
||||
this.isMultipleChoice = true
|
||||
let isActive = this.nodeData.data.isActive
|
||||
let isActive = this.getData('isActive')
|
||||
if (!isActive)
|
||||
this.mindMap.emit(
|
||||
'before_node_active',
|
||||
this,
|
||||
this.renderer.activeNodeList
|
||||
)
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this, !isActive)
|
||||
this.mindMap.renderer[isActive ? 'removeActiveNode' : 'addActiveNode'](
|
||||
this
|
||||
)
|
||||
this.mindMap.emit(
|
||||
'node_active',
|
||||
isActive ? null : this,
|
||||
this.mindMap.renderer.activeNodeList
|
||||
)
|
||||
this.mindMap.renderer[
|
||||
isActive ? 'removeNodeFromActiveList' : 'addNodeToActiveList'
|
||||
](this)
|
||||
this.mindMap.emit('node_active', isActive ? null : this, [
|
||||
...this.mindMap.renderer.activeNodeList
|
||||
])
|
||||
}
|
||||
this.mindMap.emit('node_mousedown', this, e)
|
||||
})
|
||||
this.group.on('mouseup', e => {
|
||||
if (!this.isRoot) {
|
||||
if (!this.isRoot && e.which !== 2 && !this.mindMap.opt.readonly) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
this.mindMap.emit('node_mouseup', this, e)
|
||||
})
|
||||
this.group.on('mouseenter', e => {
|
||||
if (this.isDrag) return
|
||||
this._isMouseenter = true
|
||||
// 显示展开收起按钮
|
||||
this.showExpandBtn()
|
||||
if (this.isGeneralization) {
|
||||
this.handleGeneralizationMouseenter()
|
||||
}
|
||||
this.mindMap.emit('node_mouseenter', this, e)
|
||||
})
|
||||
this.group.on('mouseleave', e => {
|
||||
if (!this._isMouseenter) return
|
||||
this._isMouseenter = false
|
||||
this.hideExpandBtn()
|
||||
if (this.isGeneralization) {
|
||||
this.handleGeneralizationMouseleave()
|
||||
}
|
||||
this.mindMap.emit('node_mouseleave', this, e)
|
||||
})
|
||||
// 双击事件
|
||||
@@ -407,16 +482,28 @@ class Node {
|
||||
})
|
||||
// 右键菜单事件
|
||||
this.group.on('contextmenu', e => {
|
||||
const { readonly, useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
|
||||
// 按住ctrl键点击鼠标左键不知为何触发的是contextmenu事件
|
||||
if (this.mindMap.opt.readonly || e.ctrlKey) {// || this.isGeneralization
|
||||
if (readonly || e.ctrlKey) {
|
||||
return
|
||||
}
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
if (this.nodeData.data.isActive) {
|
||||
this.renderer.clearActive()
|
||||
// 如果是多选节点结束,那么不要触发右键菜单事件
|
||||
if (
|
||||
this.mindMap.select &&
|
||||
!useLeftKeySelectionRightKeyDrag &&
|
||||
this.mindMap.select.hasSelectRange()
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 如果有且只有当前节点激活了,那么不需要重新激活
|
||||
if (
|
||||
!(this.getData('isActive') && this.renderer.activeNodeList.length === 1)
|
||||
) {
|
||||
this.renderer.clearActiveNodeList()
|
||||
this.active(e)
|
||||
}
|
||||
this.active(e)
|
||||
this.mindMap.emit('node_contextmenu', e, this)
|
||||
})
|
||||
}
|
||||
@@ -427,23 +514,22 @@ class Node {
|
||||
return
|
||||
}
|
||||
e && e.stopPropagation()
|
||||
if (this.nodeData.data.isActive) {
|
||||
if (this.getData('isActive')) {
|
||||
return
|
||||
}
|
||||
this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList)
|
||||
this.renderer.clearActive()
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this, true)
|
||||
this.renderer.addActiveNode(this)
|
||||
this.mindMap.emit('node_active', this, this.renderer.activeNodeList)
|
||||
this.renderer.clearActiveNodeList()
|
||||
this.renderer.addNodeToActiveList(this)
|
||||
this.mindMap.emit('node_active', this, [...this.renderer.activeNodeList])
|
||||
}
|
||||
|
||||
// 更新节点
|
||||
update(isLayout = false) {
|
||||
update() {
|
||||
if (!this.group) {
|
||||
return
|
||||
}
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration, alwaysShowExpandBtn } =
|
||||
this.mindMap.opt
|
||||
this.updateNodeActiveClass()
|
||||
let { alwaysShowExpandBtn } = this.mindMap.opt
|
||||
if (alwaysShowExpandBtn) {
|
||||
// 需要移除展开收缩按钮
|
||||
if (this._expandBtn && this.nodeData.children.length <= 0) {
|
||||
@@ -453,7 +539,7 @@ class Node {
|
||||
this.renderExpandBtn()
|
||||
}
|
||||
} else {
|
||||
let { isActive, expand } = this.nodeData.data
|
||||
let { isActive, expand } = this.getData()
|
||||
// 展开状态且非激活状态,且当前鼠标不在它上面,才隐藏
|
||||
if (expand && !isActive && !this._isMouseenter) {
|
||||
this.hideExpandBtn()
|
||||
@@ -463,16 +549,49 @@ class Node {
|
||||
}
|
||||
// 更新概要
|
||||
this.renderGeneralization()
|
||||
// 更新协同头像
|
||||
if (this.updateUserListNode) this.updateUserListNode()
|
||||
// 更新节点位置
|
||||
let t = this.group.transform()
|
||||
// // 如果上次不在可视区内,且本次也不在,那么直接返回
|
||||
// let { left: ox, top: oy } = this.getNodePosInClient(
|
||||
// t.translateX,
|
||||
// t.translateY
|
||||
// )
|
||||
// let oldIsInClient =
|
||||
// ox > 0 && oy > 0 && ox < this.mindMap.width && oy < this.mindMap.height
|
||||
// let { left: nx, top: ny } = this.getNodePosInClient(this.left, this.top)
|
||||
// let newIsNotInClient =
|
||||
// nx + this.width < 0 ||
|
||||
// ny + this.height < 0 ||
|
||||
// nx > this.mindMap.width ||
|
||||
// ny > this.mindMap.height
|
||||
// if (!oldIsInClient && newIsNotInClient) {
|
||||
// if (!this.isHide) {
|
||||
// this.isHide = true
|
||||
// this.group.hide()
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
// // 如果当前是隐藏状态,那么先显示
|
||||
// if (this.isHide) {
|
||||
// this.isHide = false
|
||||
// this.group.show()
|
||||
// }
|
||||
// 如果节点位置没有变化,则返回
|
||||
if (this.left === t.translateX && this.top === t.translateY) return
|
||||
if (!isLayout && enableNodeTransitionMove) {
|
||||
this.group
|
||||
.animate(nodeTransitionMoveDuration)
|
||||
.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
} else {
|
||||
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
}
|
||||
|
||||
// 获取节点相当于画布的位置
|
||||
getNodePosInClient(_left, _top) {
|
||||
let drawTransform = this.mindMap.draw.transform()
|
||||
let { scaleX, scaleY, translateX, translateY } = drawTransform
|
||||
let left = _left * scaleX + translateX
|
||||
let top = _top * scaleY + translateY
|
||||
return {
|
||||
left,
|
||||
top
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,76 +603,75 @@ class Node {
|
||||
return sizeChange
|
||||
}
|
||||
|
||||
// 更新节点形状样式
|
||||
updateNodeShape() {
|
||||
if (!this.shapeNode) return
|
||||
const shape = this.getShape()
|
||||
this.style[shape === CONSTANTS.SHAPE.RECTANGLE ? 'rect' : 'shape'](
|
||||
this.shapeNode
|
||||
)
|
||||
// 更新节点激活状态
|
||||
updateNodeActiveClass() {
|
||||
if (!this.group) return
|
||||
const isActive = this.getData('isActive')
|
||||
this.group[isActive ? 'addClass' : 'removeClass']('active')
|
||||
}
|
||||
|
||||
// 根据是否激活更新节点
|
||||
updateNodeByActive(active) {
|
||||
if (this.group) {
|
||||
// 切换激活状态,需要切换展开收起按钮的显隐
|
||||
if (active) {
|
||||
this.showExpandBtn()
|
||||
} else {
|
||||
this.hideExpandBtn()
|
||||
}
|
||||
this.updateNodeActiveClass()
|
||||
}
|
||||
}
|
||||
|
||||
// 递归渲染
|
||||
render(callback = () => {}) {
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration } =
|
||||
this.mindMap.opt
|
||||
// 节点
|
||||
// 重新渲染连线
|
||||
this.renderLine()
|
||||
let isLayout = false
|
||||
if (!this.group) {
|
||||
isLayout = true
|
||||
// 创建组
|
||||
this.group = new G()
|
||||
this.group.addClass('smm-node')
|
||||
this.group.css({
|
||||
cursor: 'default'
|
||||
})
|
||||
this.bindGroupEvent()
|
||||
this.draw.add(this.group)
|
||||
this.nodeDraw.add(this.group)
|
||||
this.layout()
|
||||
this.update(isLayout)
|
||||
this.update()
|
||||
} else {
|
||||
this.draw.add(this.group)
|
||||
this.nodeDraw.add(this.group)
|
||||
if (this.needLayout) {
|
||||
this.needLayout = false
|
||||
this.layout()
|
||||
}
|
||||
this.updateExpandBtnPlaceholderRect()
|
||||
this.update()
|
||||
}
|
||||
// 子节点
|
||||
if (
|
||||
this.children &&
|
||||
this.children.length &&
|
||||
this.nodeData.data.expand !== false
|
||||
this.getData('expand') !== false
|
||||
) {
|
||||
let index = 0
|
||||
asyncRun(
|
||||
this.children.map(item => {
|
||||
return () => {
|
||||
item.render(() => {
|
||||
index++
|
||||
if (index >= this.children.length) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
this.children.forEach(item => {
|
||||
item.render(() => {
|
||||
index++
|
||||
if (index >= this.children.length) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
} else {
|
||||
if (enableNodeTransitionMove && !isLayout) {
|
||||
setTimeout(() => {
|
||||
callback()
|
||||
}, nodeTransitionMoveDuration)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
callback()
|
||||
}
|
||||
// 手动插入的节点立即获得焦点并且开启编辑模式
|
||||
if (this.nodeData.inserting) {
|
||||
delete this.nodeData.inserting
|
||||
this.active()
|
||||
setTimeout(() => {
|
||||
this.mindMap.emit('node_dblclick', this)
|
||||
this.mindMap.emit('node_dblclick', this, null, true)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
@@ -566,13 +684,9 @@ class Node {
|
||||
this.removeLine()
|
||||
// 子节点
|
||||
if (this.children && this.children.length) {
|
||||
asyncRun(
|
||||
this.children.map(item => {
|
||||
return () => {
|
||||
item.remove()
|
||||
}
|
||||
})
|
||||
)
|
||||
this.children.forEach(item => {
|
||||
item.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,6 +697,9 @@ class Node {
|
||||
this.removeGeneralization()
|
||||
this.removeLine()
|
||||
this.group = null
|
||||
if (this.parent) {
|
||||
this.parent.removeLine()
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏节点
|
||||
@@ -598,13 +715,9 @@ class Node {
|
||||
}
|
||||
// 子节点
|
||||
if (this.children && this.children.length) {
|
||||
asyncRun(
|
||||
this.children.map(item => {
|
||||
return () => {
|
||||
item.hide()
|
||||
}
|
||||
})
|
||||
)
|
||||
this.children.forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,19 +737,67 @@ class Node {
|
||||
}
|
||||
// 子节点
|
||||
if (this.children && this.children.length) {
|
||||
asyncRun(
|
||||
this.children.map(item => {
|
||||
return () => {
|
||||
item.show()
|
||||
}
|
||||
})
|
||||
)
|
||||
this.children.forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 设置节点透明度
|
||||
// 包括连接线和下级节点
|
||||
setOpacity(val) {
|
||||
// 自身及连线
|
||||
this.group.opacity(val)
|
||||
this._lines.forEach(line => {
|
||||
line.opacity(val)
|
||||
})
|
||||
// 子节点
|
||||
this.children.forEach(item => {
|
||||
item.setOpacity(val)
|
||||
})
|
||||
// 概要节点
|
||||
this.setGeneralizationOpacity(val)
|
||||
}
|
||||
|
||||
// 隐藏子节点
|
||||
hideChildren() {
|
||||
this._lines.forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
if (this.children && this.children.length) {
|
||||
this.children.forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 显示子节点
|
||||
showChildren() {
|
||||
this._lines.forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
if (this.children && this.children.length) {
|
||||
this.children.forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 被拖拽中
|
||||
startDrag() {
|
||||
this.isDrag = true
|
||||
this.group.addClass('smm-node-dragging')
|
||||
}
|
||||
|
||||
// 拖拽结束
|
||||
endDrag() {
|
||||
this.isDrag = false
|
||||
this.group.removeClass('smm-node-dragging')
|
||||
}
|
||||
|
||||
// 连线
|
||||
renderLine(deep = false) {
|
||||
if (this.nodeData.data.expand === false) {
|
||||
if (this.getData('expand') === false) {
|
||||
return
|
||||
}
|
||||
let childrenLen = this.nodeData.children.length
|
||||
@@ -650,7 +811,7 @@ class Node {
|
||||
if (childrenLen > this._lines.length) {
|
||||
// 创建缺少的线
|
||||
new Array(childrenLen - this._lines.length).fill(0).forEach(() => {
|
||||
this._lines.push(this.draw.path())
|
||||
this._lines.push(this.lineDraw.path())
|
||||
})
|
||||
} else if (childrenLen < this._lines.length) {
|
||||
// 删除多余的线
|
||||
@@ -690,7 +851,7 @@ class Node {
|
||||
return this.customLeft !== undefined && this.customTop !== undefined
|
||||
}
|
||||
|
||||
// 检查节点是否存在自定义位置的祖先节点
|
||||
// 检查节点是否存在自定义位置的祖先节点,包含自身
|
||||
ancestorHasCustomPosition() {
|
||||
let node = this
|
||||
while (node) {
|
||||
@@ -702,6 +863,18 @@ class Node {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否存在有概要的祖先节点
|
||||
ancestorHasGeneralization() {
|
||||
let node = this.parent
|
||||
while (node) {
|
||||
if (node.checkHasGeneralization()) {
|
||||
return true
|
||||
}
|
||||
node = node.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 添加子节点
|
||||
addChildren(node) {
|
||||
this.children.push(node)
|
||||
@@ -732,13 +905,13 @@ class Node {
|
||||
}
|
||||
|
||||
// 检测当前节点是否是某个节点的祖先节点
|
||||
isParent(node) {
|
||||
if (this === node) {
|
||||
isAncestor(node) {
|
||||
if (this.uid === node.uid) {
|
||||
return false
|
||||
}
|
||||
let parent = node.parent
|
||||
while (parent) {
|
||||
if (this === parent) {
|
||||
if (this.uid === parent.uid) {
|
||||
return true
|
||||
}
|
||||
parent = parent.parent
|
||||
@@ -746,19 +919,40 @@ class Node {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查当前节点是否是某个节点的父节点
|
||||
isParent(node) {
|
||||
if (this.uid === node.uid) {
|
||||
return false
|
||||
}
|
||||
const parent = node.parent
|
||||
if (parent && this.uid === parent.uid) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 检测当前节点是否是某个节点的兄弟节点
|
||||
isBrother(node) {
|
||||
if (!this.parent || this === node) {
|
||||
if (!this.parent || this.uid === node.uid) {
|
||||
return false
|
||||
}
|
||||
return this.parent.children.find(item => {
|
||||
return item === node
|
||||
return item.uid === node.uid
|
||||
})
|
||||
}
|
||||
|
||||
// 获取该节点在兄弟节点列表中的索引
|
||||
getIndexInBrothers() {
|
||||
return this.parent && this.parent.children
|
||||
? this.parent.children.findIndex(item => {
|
||||
return item.uid === this.uid
|
||||
})
|
||||
: -1
|
||||
}
|
||||
|
||||
// 获取padding值
|
||||
getPaddingVale() {
|
||||
let { isActive }= this.nodeData.data
|
||||
let { isActive } = this.getData()
|
||||
return {
|
||||
paddingX: this.getStyle('paddingX', true, isActive),
|
||||
paddingY: this.getStyle('paddingY', true, isActive)
|
||||
@@ -766,8 +960,8 @@ class Node {
|
||||
}
|
||||
|
||||
// 获取某个样式
|
||||
getStyle(prop, root, isActive) {
|
||||
let v = this.style.merge(prop, root, isActive)
|
||||
getStyle(prop, root) {
|
||||
let v = this.style.merge(prop, root)
|
||||
return v === undefined ? '' : v
|
||||
}
|
||||
|
||||
@@ -794,15 +988,49 @@ class Node {
|
||||
) // 父级
|
||||
}
|
||||
|
||||
// 获取节点非节点状态的边框大小
|
||||
getBorderWidth() {
|
||||
return this.style.merge('borderWidth', false) || 0
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
getData(key) {
|
||||
return key ? this.nodeData.data[key] || '' : this.nodeData.data
|
||||
return key ? this.nodeData.data[key] : this.nodeData.data
|
||||
}
|
||||
|
||||
// 获取该节点的纯数据,即不包含对节点实例的引用
|
||||
getPureData(removeActiveState = true, removeId = false) {
|
||||
return copyNodeTree({}, this, removeActiveState, removeId)
|
||||
}
|
||||
|
||||
// 是否存在自定义样式
|
||||
hasCustomStyle() {
|
||||
return this.style.hasCustomStyle()
|
||||
}
|
||||
|
||||
// 获取节点的尺寸和位置信息,宽高是应用了缩放效果后的实际宽高,位置是相对于浏览器窗口左上角的位置
|
||||
getRect() {
|
||||
return this.group.rbox()
|
||||
}
|
||||
|
||||
// 获取节点的尺寸和位置信息,宽高是应用了缩放效果后的实际宽高,位置信息相对于画布
|
||||
getRectInSvg() {
|
||||
let { scaleX, scaleY, translateX, translateY } =
|
||||
this.mindMap.draw.transform()
|
||||
let { left, top, width, height } = this
|
||||
let right = (left + width) * scaleX + translateX
|
||||
let bottom = (top + height) * scaleY + translateY
|
||||
left = left * scaleX + translateX
|
||||
top = top * scaleY + translateY
|
||||
return {
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
width: width * scaleX,
|
||||
height: height * scaleY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Node
|
||||
|
||||
@@ -62,11 +62,10 @@ export default class Shape {
|
||||
// 创建形状节点
|
||||
createShape() {
|
||||
const shape = this.node.getShape()
|
||||
let { width, height } = this.node
|
||||
let node = null
|
||||
// 矩形
|
||||
if (shape === CONSTANTS.SHAPE.RECTANGLE) {
|
||||
node = new Rect().size(width, height)
|
||||
node = this.createRect()
|
||||
} else if (shape === CONSTANTS.SHAPE.DIAMOND) {
|
||||
// 菱形
|
||||
node = this.createDiamond()
|
||||
@@ -95,9 +94,41 @@ export default class Shape {
|
||||
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.node
|
||||
let { width, height } = this.getNodeSize()
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
let topX = halfWidth
|
||||
@@ -120,7 +151,7 @@ export default class Shape {
|
||||
createParallelogram() {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.node
|
||||
let { width, height } = this.getNodeSize()
|
||||
return new Polygon().plot([
|
||||
[paddingX, 0],
|
||||
[width, 0],
|
||||
@@ -131,7 +162,7 @@ export default class Shape {
|
||||
|
||||
// 创建圆角矩形
|
||||
createRoundedRectangle() {
|
||||
let { width, height } = this.node
|
||||
let { width, height } = this.getNodeSize()
|
||||
let halfHeight = height / 2
|
||||
return new Path().plot(`
|
||||
M${halfHeight},0
|
||||
@@ -145,7 +176,7 @@ export default class Shape {
|
||||
// 创建八角矩形
|
||||
createOctagonalRectangle() {
|
||||
let w = 5
|
||||
let { width, height } = this.node
|
||||
let { width, height } = this.getNodeSize()
|
||||
return new Polygon().plot([
|
||||
[0, w],
|
||||
[w, 0],
|
||||
@@ -162,7 +193,7 @@ export default class Shape {
|
||||
createOuterTriangularRectangle() {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.node
|
||||
let { width, height } = this.getNodeSize()
|
||||
return new Polygon().plot([
|
||||
[paddingX, 0],
|
||||
[width - paddingX, 0],
|
||||
@@ -177,7 +208,7 @@ export default class Shape {
|
||||
createInnerTriangularRectangle() {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.node
|
||||
let { width, height } = this.getNodeSize()
|
||||
return new Polygon().plot([
|
||||
[0, 0],
|
||||
[width, 0],
|
||||
@@ -190,7 +221,7 @@ export default class Shape {
|
||||
|
||||
// 创建椭圆
|
||||
createEllipse() {
|
||||
let { width, height } = this.node
|
||||
let { width, height } = this.getNodeSize()
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
return new Path().plot(`
|
||||
@@ -203,7 +234,7 @@ export default class Shape {
|
||||
|
||||
// 创建圆
|
||||
createCircle() {
|
||||
let { width, height } = this.node
|
||||
let { width, height } = this.getNodeSize()
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
return new Path().plot(`
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { tagColorList, nodeDataNoStylePropList } from '../../../constants/constant'
|
||||
import {
|
||||
checkIsNodeStyleDataKey,
|
||||
generateColorByContent
|
||||
} from '../../../utils/index'
|
||||
|
||||
const rootProp = ['paddingX', 'paddingY']
|
||||
const backgroundStyleProps = ['backgroundColor', 'backgroundImage', 'backgroundRepeat', 'backgroundPosition', 'backgroundSize']
|
||||
const backgroundStyleProps = [
|
||||
'backgroundColor',
|
||||
'backgroundImage',
|
||||
'backgroundRepeat',
|
||||
'backgroundPosition',
|
||||
'backgroundSize'
|
||||
]
|
||||
|
||||
// 样式类
|
||||
class Style {
|
||||
@@ -10,14 +20,20 @@ class Style {
|
||||
if (!Style.cacheStyle) {
|
||||
Style.cacheStyle = {}
|
||||
let style = window.getComputedStyle(el)
|
||||
backgroundStyleProps.forEach((prop) => {
|
||||
backgroundStyleProps.forEach(prop => {
|
||||
Style.cacheStyle[prop] = style[prop]
|
||||
})
|
||||
}
|
||||
// 设置新样式
|
||||
let { backgroundColor, backgroundImage, backgroundRepeat, backgroundPosition, backgroundSize } = themeConfig
|
||||
let {
|
||||
backgroundColor,
|
||||
backgroundImage,
|
||||
backgroundRepeat,
|
||||
backgroundPosition,
|
||||
backgroundSize
|
||||
} = themeConfig
|
||||
el.style.backgroundColor = backgroundColor
|
||||
if (backgroundImage) {
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
el.style.backgroundImage = `url(${backgroundImage})`
|
||||
el.style.backgroundRepeat = backgroundRepeat
|
||||
el.style.backgroundPosition = backgroundPosition
|
||||
@@ -30,7 +46,7 @@ class Style {
|
||||
// 移除背景样式
|
||||
static removeBackgroundStyle(el) {
|
||||
if (!Style.cacheStyle) return
|
||||
backgroundStyleProps.forEach((prop) => {
|
||||
backgroundStyleProps.forEach(prop => {
|
||||
el.style[prop] = Style.cacheStyle[prop]
|
||||
})
|
||||
Style.cacheStyle = null
|
||||
@@ -42,7 +58,7 @@ class Style {
|
||||
}
|
||||
|
||||
// 合并样式
|
||||
merge(prop, root, isActive) {
|
||||
merge(prop, root) {
|
||||
let themeConfig = this.ctx.mindMap.themeConfig
|
||||
// 三级及以下节点
|
||||
let defaultConfig = themeConfig.node
|
||||
@@ -59,17 +75,6 @@ class Style {
|
||||
// 二级节点
|
||||
defaultConfig = 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)
|
||||
@@ -77,13 +82,13 @@ class Style {
|
||||
}
|
||||
|
||||
// 获取某个样式值
|
||||
getStyle(prop, root, isActive) {
|
||||
return this.merge(prop, root, isActive)
|
||||
getStyle(prop, root) {
|
||||
return this.merge(prop, root)
|
||||
}
|
||||
|
||||
// 获取自身自定义样式
|
||||
getSelfStyle(prop) {
|
||||
return this.ctx.nodeData.data[prop]
|
||||
return this.ctx.getData(prop)
|
||||
}
|
||||
|
||||
// 矩形
|
||||
@@ -102,7 +107,7 @@ class Style {
|
||||
// !this.ctx.isRoot &&
|
||||
// !this.ctx.isGeneralization &&
|
||||
// this.ctx.mindMap.themeConfig.nodeUseLineStyle &&
|
||||
// !this.ctx.nodeData.data.isActive
|
||||
// !this.ctx.getData('isActive')
|
||||
// ) {
|
||||
// return
|
||||
// }
|
||||
@@ -142,10 +147,10 @@ class Style {
|
||||
|
||||
// 获取文本样式
|
||||
getTextFontStyle() {
|
||||
return {
|
||||
italic: this.merge('fontStyle') === 'italic',
|
||||
bold: this.merge('fontWeight'),
|
||||
fontSize: this.merge('fontSize'),
|
||||
return {
|
||||
italic: this.merge('fontStyle') === 'italic',
|
||||
bold: this.merge('fontWeight'),
|
||||
fontSize: this.merge('fontSize'),
|
||||
fontFamily: this.merge('fontFamily')
|
||||
}
|
||||
}
|
||||
@@ -160,10 +165,10 @@ class Style {
|
||||
}
|
||||
|
||||
// 标签文字
|
||||
tagText(node, index) {
|
||||
tagText(node) {
|
||||
node
|
||||
.fill({
|
||||
color: tagColorList[index].color
|
||||
color: '#fff'
|
||||
})
|
||||
.css({
|
||||
'font-size': '12px'
|
||||
@@ -171,9 +176,9 @@ class Style {
|
||||
}
|
||||
|
||||
// 标签矩形
|
||||
tagRect(node, index) {
|
||||
tagRect(node, text, color) {
|
||||
node.fill({
|
||||
color: tagColorList[index].background
|
||||
color: color || generateColorByContent(text.node.textContent)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -201,25 +206,40 @@ class Style {
|
||||
|
||||
// 展开收起按钮
|
||||
iconBtn(node, node2, fillNode) {
|
||||
let { color, fill } = this.ctx.mindMap.opt.expandBtnStyle || {
|
||||
let { color, fill, fontSize, fontColor } = this.ctx.mindMap.opt
|
||||
.expandBtnStyle || {
|
||||
color: '#808080',
|
||||
fill: '#fff'
|
||||
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.nodeData.data).forEach((item) => {
|
||||
if (!nodeDataNoStylePropList.includes(item)) {
|
||||
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
|
||||
|
||||
@@ -4,8 +4,8 @@ function setData(data = {}) {
|
||||
}
|
||||
|
||||
// 设置文本
|
||||
function setText(text, richText) {
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText)
|
||||
function setText(text, richText, resetRichText) {
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText, resetRichText)
|
||||
}
|
||||
|
||||
// 设置图片
|
||||
@@ -39,8 +39,13 @@ function setShape(shape) {
|
||||
}
|
||||
|
||||
// 修改某个样式
|
||||
function setStyle(prop, value, isActive) {
|
||||
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value, isActive)
|
||||
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 {
|
||||
@@ -52,5 +57,6 @@ export default {
|
||||
setNote,
|
||||
setTag,
|
||||
setShape,
|
||||
setStyle
|
||||
setStyle,
|
||||
setStyles
|
||||
}
|
||||
|
||||
104
simple-mind-map/src/core/render/node/nodeCooperate.js
Normal file
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
|
||||
}
|
||||
@@ -1,22 +1,38 @@
|
||||
import { measureText, resizeImgSize, getTextFromHtml } from '../../../utils'
|
||||
import {
|
||||
measureText,
|
||||
resizeImgSize,
|
||||
removeHtmlStyle,
|
||||
addHtmlStyle,
|
||||
checkIsRichText,
|
||||
isUndef
|
||||
} from '../../../utils'
|
||||
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
|
||||
import iconsSvg from '../../../svg/icons'
|
||||
import { CONSTANTS } from '../../../constants/constant'
|
||||
import { CONSTANTS, commonCaches } from '../../../constants/constant'
|
||||
|
||||
// 创建图片节点
|
||||
function createImgNode() {
|
||||
let img = this.nodeData.data.image
|
||||
let img = this.getData('image')
|
||||
if (!img) {
|
||||
return
|
||||
}
|
||||
let imgSize = this.getImgShowSize()
|
||||
let node = new Image().load(img).size(...imgSize)
|
||||
if (this.nodeData.data.imageTitle) {
|
||||
node.attr('title', this.nodeData.data.imageTitle)
|
||||
if (this.getData('imageTitle')) {
|
||||
node.attr('title', this.getData('imageTitle'))
|
||||
}
|
||||
node.on('dblclick', e => {
|
||||
this.mindMap.emit('node_img_dblclick', this, e)
|
||||
})
|
||||
node.on('mouseenter', e => {
|
||||
this.mindMap.emit('node_img_mouseenter', this, node, e)
|
||||
})
|
||||
node.on('mouseleave', e => {
|
||||
this.mindMap.emit('node_img_mouseleave', this, node, e)
|
||||
})
|
||||
node.on('mousemove', e => {
|
||||
this.mindMap.emit('node_img_mousemove', this, node, e)
|
||||
})
|
||||
return {
|
||||
node,
|
||||
width: imgSize[0],
|
||||
@@ -26,9 +42,12 @@ function createImgNode() {
|
||||
|
||||
// 获取图片显示宽高
|
||||
function getImgShowSize() {
|
||||
const { custom, width, height } = this.getData('imageSize')
|
||||
// 如果是自定义了图片的宽高,那么不受最大宽高限制
|
||||
if (custom) return [width, height]
|
||||
return resizeImgSize(
|
||||
this.nodeData.data.imageSize.width,
|
||||
this.nodeData.data.imageSize.height,
|
||||
width,
|
||||
height,
|
||||
this.mindMap.themeConfig.imgMaxWidth,
|
||||
this.mindMap.themeConfig.imgMaxHeight
|
||||
)
|
||||
@@ -36,13 +55,16 @@ function getImgShowSize() {
|
||||
|
||||
// 创建icon节点
|
||||
function createIconNode() {
|
||||
let _data = this.nodeData.data
|
||||
let _data = this.getData()
|
||||
if (!_data.icon || _data.icon.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let iconSize = this.mindMap.themeConfig.iconSize
|
||||
return _data.icon.map(item => {
|
||||
let src = iconsSvg.getNodeIconListIcon(item, this.mindMap.opt.iconList || [])
|
||||
let src = iconsSvg.getNodeIconListIcon(
|
||||
item,
|
||||
this.mindMap.opt.iconList || []
|
||||
)
|
||||
let node = null
|
||||
// svg图标
|
||||
if (/^<svg/.test(src)) {
|
||||
@@ -52,6 +74,9 @@ function createIconNode() {
|
||||
node = new Image().load(src)
|
||||
}
|
||||
node.size(iconSize, iconSize)
|
||||
node.on('click', e => {
|
||||
this.mindMap.emit('node_icon_click', this, item, e)
|
||||
})
|
||||
return {
|
||||
node,
|
||||
width: iconSize,
|
||||
@@ -62,10 +87,11 @@ function createIconNode() {
|
||||
|
||||
// 创建富文本节点
|
||||
function createRichTextNode() {
|
||||
const { textAutoWrapWidth } = this.mindMap.opt
|
||||
let g = new G()
|
||||
// 重新设置富文本节点内容
|
||||
let recoverText = false
|
||||
if (this.nodeData.data.resetRichText) {
|
||||
if (this.getData('resetRichText')) {
|
||||
delete this.nodeData.data.resetRichText
|
||||
recoverText = true
|
||||
}
|
||||
@@ -76,29 +102,53 @@ function createRichTextNode() {
|
||||
}
|
||||
}
|
||||
if (recoverText) {
|
||||
let text = getTextFromHtml(this.nodeData.data.text)
|
||||
this.nodeData.data.text = `<p><span style="${this.style.createStyleText()}">${text}</span></p>`
|
||||
let text = this.getData('text')
|
||||
// 判断节点内容是否是富文本
|
||||
let isRichText = checkIsRichText(text)
|
||||
// 样式字符串
|
||||
let style = this.style.createStyleText()
|
||||
if (isRichText) {
|
||||
// 如果是富文本那么线移除内联样式
|
||||
text = removeHtmlStyle(text)
|
||||
// 再添加新的内联样式
|
||||
text = addHtmlStyle(text, 'span', style)
|
||||
} else {
|
||||
// 非富文本
|
||||
text = `<p><span style="${style}">${text}</span></p>`
|
||||
}
|
||||
this.setData({
|
||||
text: text
|
||||
})
|
||||
}
|
||||
let html = `<div>${this.nodeData.data.text}</div>`
|
||||
let div = document.createElement('div')
|
||||
let html = `<div>${this.getData('text')}</div>`
|
||||
if (!commonCaches.measureRichtextNodeTextSizeEl) {
|
||||
commonCaches.measureRichtextNodeTextSizeEl = document.createElement('div')
|
||||
commonCaches.measureRichtextNodeTextSizeEl.style.position = 'fixed'
|
||||
commonCaches.measureRichtextNodeTextSizeEl.style.left = '-999999px'
|
||||
this.mindMap.el.appendChild(commonCaches.measureRichtextNodeTextSizeEl)
|
||||
}
|
||||
let div = commonCaches.measureRichtextNodeTextSizeEl
|
||||
div.innerHTML = html
|
||||
div.style.cssText = `position: fixed; left: -999999px;`
|
||||
let el = div.children[0]
|
||||
el.classList.add('smm-richtext-node-wrap')
|
||||
el.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
|
||||
el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px'
|
||||
this.mindMap.el.appendChild(div)
|
||||
el.style.maxWidth = textAutoWrapWidth + 'px'
|
||||
let { width, height } = el.getBoundingClientRect()
|
||||
width = Math.ceil(width)
|
||||
// 如果文本为空,那么需要计算一个默认高度
|
||||
if (height <= 0) {
|
||||
div.innerHTML = '<p>abc123我和你</p>'
|
||||
let elTmp = div.children[0]
|
||||
elTmp.classList.add('smm-richtext-node-wrap')
|
||||
height = elTmp.getBoundingClientRect().height
|
||||
}
|
||||
width = Math.ceil(width) + 1 // 修复getBoundingClientRect方法对实际宽度是小数的元素获取到的值是整数,导致宽度不够文本发生换行的问题
|
||||
height = Math.ceil(height)
|
||||
g.attr('data-width', width)
|
||||
g.attr('data-height', height)
|
||||
html = div.innerHTML
|
||||
this.mindMap.el.removeChild(div)
|
||||
let foreignObject = new ForeignObject()
|
||||
foreignObject.width(width)
|
||||
foreignObject.height(height)
|
||||
foreignObject.add(SVG(html))
|
||||
foreignObject.add(div.children[0])
|
||||
g.add(foreignObject)
|
||||
return {
|
||||
node: g,
|
||||
@@ -109,19 +159,18 @@ function createRichTextNode() {
|
||||
|
||||
// 创建文本节点
|
||||
function createTextNode() {
|
||||
if (this.nodeData.data.richText) {
|
||||
if (this.getData('richText')) {
|
||||
return this.createRichTextNode()
|
||||
}
|
||||
let g = new G()
|
||||
let fontSize = this.getStyle('fontSize', false, this.nodeData.data.isActive)
|
||||
let lineHeight = this.getStyle(
|
||||
'lineHeight',
|
||||
false,
|
||||
this.nodeData.data.isActive
|
||||
)
|
||||
let fontSize = this.getStyle('fontSize', false)
|
||||
let lineHeight = this.getStyle('lineHeight', false)
|
||||
// 文本超长自动换行
|
||||
let textStyle = this.style.getTextFontStyle()
|
||||
let textArr = this.nodeData.data.text.split(/\n/gim)
|
||||
let textArr = []
|
||||
if (!isUndef(this.getData('text'))) {
|
||||
textArr = String(this.getData('text')).split(/\n/gim)
|
||||
}
|
||||
let maxWidth = this.mindMap.opt.textAutoWrapWidth
|
||||
let isMultiLine = false
|
||||
textArr.forEach((item, index) => {
|
||||
@@ -168,7 +217,7 @@ function createTextNode() {
|
||||
|
||||
// 创建超链接节点
|
||||
function createHyperlinkNode() {
|
||||
let { hyperlink, hyperlinkTitle } = this.nodeData.data
|
||||
let { hyperlink, hyperlinkTitle } = this.getData()
|
||||
if (!hyperlink) {
|
||||
return
|
||||
}
|
||||
@@ -198,7 +247,7 @@ function createHyperlinkNode() {
|
||||
|
||||
// 创建标签节点
|
||||
function createTagNode() {
|
||||
let tagData = this.nodeData.data.tag
|
||||
let tagData = this.getData('tag')
|
||||
if (!tagData || tagData.length <= 0) {
|
||||
return []
|
||||
}
|
||||
@@ -206,12 +255,15 @@ function createTagNode() {
|
||||
tagData.slice(0, this.mindMap.opt.maxTag).forEach((item, index) => {
|
||||
let tag = new G()
|
||||
// 标签文本
|
||||
let text = new Text().text(item).x(8).cy(10)
|
||||
let text = new Text().text(item).x(8).cy(8)
|
||||
this.style.tagText(text, index)
|
||||
let { width } = text.bbox()
|
||||
// 标签矩形
|
||||
let rect = new Rect().size(width + 16, 20)
|
||||
this.style.tagRect(rect, index)
|
||||
// 先从自定义的颜色中获取颜色,没有的话就按照内容生成
|
||||
const tagsColorList = this.mindMap.opt.tagsColorMap || {}
|
||||
const color = tagsColorList[text.node.textContent]
|
||||
this.style.tagRect(rect, text, color)
|
||||
tag.add(rect).add(text)
|
||||
nodes.push({
|
||||
node: tag,
|
||||
@@ -224,7 +276,7 @@ function createTagNode() {
|
||||
|
||||
// 创建备注节点
|
||||
function createNoteNode() {
|
||||
if (!this.nodeData.data.note) {
|
||||
if (!this.getData('note')) {
|
||||
return null
|
||||
}
|
||||
let iconSize = this.mindMap.themeConfig.iconSize
|
||||
@@ -240,29 +292,32 @@ function createNoteNode() {
|
||||
if (!this.noteEl) {
|
||||
this.noteEl = document.createElement('div')
|
||||
this.noteEl.style.cssText = `
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
|
||||
display: none;
|
||||
background-color: #fff;
|
||||
z-index: ${ this.mindMap.opt.nodeNoteTooltipZIndex }
|
||||
z-index: ${this.mindMap.opt.nodeNoteTooltipZIndex}
|
||||
`
|
||||
document.body.appendChild(this.noteEl)
|
||||
const targetNode =
|
||||
this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.noteEl)
|
||||
}
|
||||
this.noteEl.innerText = this.nodeData.data.note
|
||||
this.noteEl.innerText = this.getData('note')
|
||||
}
|
||||
node.on('mouseover', () => {
|
||||
let { left, top } = node.node.getBoundingClientRect()
|
||||
const { left, top } = this.getNoteContentPosition()
|
||||
if (!this.mindMap.opt.customNoteContentShow) {
|
||||
this.noteEl.style.left = left + 'px'
|
||||
this.noteEl.style.top = top + iconSize + 'px'
|
||||
this.noteEl.style.top = top + 'px'
|
||||
this.noteEl.style.display = 'block'
|
||||
} else {
|
||||
this.mindMap.opt.customNoteContentShow.show(
|
||||
this.nodeData.data.note,
|
||||
this.getData('note'),
|
||||
left,
|
||||
top + iconSize
|
||||
top,
|
||||
this
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -280,13 +335,54 @@ function createNoteNode() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点备注显示位置
|
||||
function getNoteContentPosition() {
|
||||
const iconSize = this.mindMap.themeConfig.iconSize
|
||||
const { scaleY } = this.mindMap.view.getTransformData().transform
|
||||
const iconSizeAddScale = iconSize * scaleY
|
||||
let { left, top } = this._noteData.node.node.getBoundingClientRect()
|
||||
top += iconSizeAddScale
|
||||
return {
|
||||
left,
|
||||
top
|
||||
}
|
||||
}
|
||||
|
||||
// 测量自定义节点内容元素的宽高
|
||||
function measureCustomNodeContentSize(content) {
|
||||
if (!commonCaches.measureCustomNodeContentSizeEl) {
|
||||
commonCaches.measureCustomNodeContentSizeEl = document.createElement('div')
|
||||
commonCaches.measureCustomNodeContentSizeEl.style.cssText = `
|
||||
position: fixed;
|
||||
left: -99999px;
|
||||
top: -99999px;
|
||||
`
|
||||
this.mindMap.el.appendChild(commonCaches.measureCustomNodeContentSizeEl)
|
||||
}
|
||||
commonCaches.measureCustomNodeContentSizeEl.innerHTML = ''
|
||||
commonCaches.measureCustomNodeContentSizeEl.appendChild(content)
|
||||
let rect = commonCaches.measureCustomNodeContentSizeEl.getBoundingClientRect()
|
||||
return {
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
}
|
||||
|
||||
// 是否使用的是自定义节点内容
|
||||
function isUseCustomNodeContent() {
|
||||
return !!this._customNodeContent
|
||||
}
|
||||
|
||||
export default {
|
||||
createImgNode,
|
||||
getImgShowSize,
|
||||
createIconNode,
|
||||
createRichTextNode,
|
||||
createTextNode,
|
||||
createHyperlinkNode,
|
||||
createTagNode,
|
||||
createNoteNode
|
||||
}
|
||||
createImgNode,
|
||||
getImgShowSize,
|
||||
createIconNode,
|
||||
createRichTextNode,
|
||||
createTextNode,
|
||||
createHyperlinkNode,
|
||||
createTagNode,
|
||||
createNoteNode,
|
||||
getNoteContentPosition,
|
||||
measureCustomNodeContentSize,
|
||||
isUseCustomNodeContent
|
||||
}
|
||||
|
||||
@@ -6,13 +6,27 @@ function createExpandNodeContent() {
|
||||
if (this._openExpandNode) {
|
||||
return
|
||||
}
|
||||
let { open, close } = this.mindMap.opt.expandBtnIcon || {}
|
||||
// 展开的节点
|
||||
this._openExpandNode = SVG(open || btnsSvg.open).size(
|
||||
this.expandBtnSize,
|
||||
this.expandBtnSize
|
||||
)
|
||||
this._openExpandNode.x(0).y(-this.expandBtnSize / 2)
|
||||
let { close, open } = this.mindMap.opt.expandBtnIcon || {}
|
||||
// 根据配置判断是否显示数量按钮
|
||||
if (this.mindMap.opt.isShowExpandNum) {
|
||||
// 展开的节点
|
||||
this._openExpandNode = SVG()
|
||||
.text()
|
||||
.size(this.expandBtnSize, this.expandBtnSize)
|
||||
// 文本垂直居中
|
||||
this._openExpandNode.attr({
|
||||
'text-anchor': 'middle',
|
||||
'dominant-baseline': 'middle',
|
||||
x: this.expandBtnSize / 2,
|
||||
y: 2
|
||||
})
|
||||
} else {
|
||||
this._openExpandNode = SVG(open || btnsSvg.open).size(
|
||||
this.expandBtnSize,
|
||||
this.expandBtnSize
|
||||
)
|
||||
this._openExpandNode.x(0).y(-this.expandBtnSize / 2)
|
||||
}
|
||||
// 收起的节点
|
||||
this._closeExpandNode = SVG(close || btnsSvg.close).size(
|
||||
this.expandBtnSize,
|
||||
@@ -22,6 +36,7 @@ function createExpandNodeContent() {
|
||||
// 填充节点
|
||||
this._fillExpandNode = new Circle().size(this.expandBtnSize)
|
||||
this._fillExpandNode.x(0).y(-this.expandBtnSize / 2)
|
||||
|
||||
// 设置样式
|
||||
this.style.iconBtn(
|
||||
this._openExpandNode,
|
||||
@@ -29,10 +44,15 @@ function createExpandNodeContent() {
|
||||
this._fillExpandNode
|
||||
)
|
||||
}
|
||||
|
||||
function sumNode(data = []) {
|
||||
return data.reduce(
|
||||
(total, cur) => total + this.sumNode(cur.children || []),
|
||||
data.length
|
||||
)
|
||||
}
|
||||
// 创建或更新展开收缩按钮内容
|
||||
function updateExpandBtnNode() {
|
||||
let { expand } = this.nodeData.data
|
||||
let { expand } = this.getData()
|
||||
// 如果本次和上次的展开状态一样则返回
|
||||
if (expand === this._lastExpandBtnType) return
|
||||
if (this._expandBtn) {
|
||||
@@ -47,7 +67,27 @@ function updateExpandBtnNode() {
|
||||
node = this._closeExpandNode
|
||||
this._lastExpandBtnType = true
|
||||
}
|
||||
if (this._expandBtn) this._expandBtn.add(this._fillExpandNode).add(node)
|
||||
|
||||
if (this._expandBtn) {
|
||||
// 如果是收起按钮加上边框
|
||||
let { isShowExpandNum, expandBtnStyle, expandBtnNumHandler } =
|
||||
this.mindMap.opt
|
||||
if (isShowExpandNum) {
|
||||
if (!expand) {
|
||||
// 数字按钮添加边框
|
||||
this._fillExpandNode.stroke({
|
||||
color: expandBtnStyle.strokeColor
|
||||
})
|
||||
// 计算子节点数量
|
||||
let count = this.sumNode(this.nodeData.children)
|
||||
count = expandBtnNumHandler(count)
|
||||
node.text(count)
|
||||
} else {
|
||||
this._fillExpandNode.stroke('none')
|
||||
}
|
||||
}
|
||||
this._expandBtn.add(this._fillExpandNode).add(node)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新展开收缩按钮位置
|
||||
@@ -89,13 +129,14 @@ function renderExpandBtn() {
|
||||
this.mindMap.execCommand(
|
||||
'SET_NODE_EXPAND',
|
||||
this,
|
||||
!this.nodeData.data.expand
|
||||
!this.getData('expand')
|
||||
)
|
||||
this.mindMap.emit('expand_btn_click', this)
|
||||
})
|
||||
this._expandBtn.on('dblclick', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this._expandBtn.addClass('smm-expand-btn')
|
||||
this.group.add(this._expandBtn)
|
||||
}
|
||||
this._showExpandBtn = true
|
||||
@@ -123,7 +164,7 @@ function showExpandBtn() {
|
||||
function hideExpandBtn() {
|
||||
if (this.mindMap.opt.alwaysShowExpandBtn || this._isMouseenter) return
|
||||
// 非激活状态且展开状态鼠标移出才隐藏按钮
|
||||
let { isActive, expand } = this.nodeData.data
|
||||
let { isActive, expand } = this.getData()
|
||||
if (!isActive && expand) {
|
||||
setTimeout(() => {
|
||||
this.removeExpandBtn()
|
||||
@@ -138,5 +179,6 @@ export default {
|
||||
renderExpandBtn,
|
||||
removeExpandBtn,
|
||||
showExpandBtn,
|
||||
hideExpandBtn
|
||||
hideExpandBtn,
|
||||
sumNode
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Rect } from '@svgdotjs/svg.js'
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
function renderExpandBtnPlaceholderRect() {
|
||||
// 根节点或没有子节点不需要渲染
|
||||
if (
|
||||
!this.nodeData.children ||
|
||||
this.nodeData.children.length <= 0 ||
|
||||
this.isRoot
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 默认显示展开按钮的情况下也不需要渲染
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
let { width, height } = this
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
this._unVisibleRectRegionNode = new Rect()
|
||||
this._unVisibleRectRegionNode.fill({
|
||||
color: 'transparent'
|
||||
})
|
||||
}
|
||||
this.group.add(this._unVisibleRectRegionNode)
|
||||
this.renderer.layout.renderExpandBtnRect(
|
||||
this._unVisibleRectRegionNode,
|
||||
this.expandBtnSize,
|
||||
width,
|
||||
height,
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除展开收起按钮的隐藏占位元素
|
||||
function clearExpandBtnPlaceholderRect() {
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
return
|
||||
}
|
||||
this._unVisibleRectRegionNode.remove()
|
||||
this._unVisibleRectRegionNode = null
|
||||
}
|
||||
|
||||
// 更新展开收起按钮的隐藏占位元素
|
||||
function updateExpandBtnPlaceholderRect() {
|
||||
// 布局改变需要重新渲染
|
||||
if (this.needRerenderExpandBtnPlaceholderRect) {
|
||||
this.needRerenderExpandBtnPlaceholderRect = false
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
}
|
||||
// 没有子节点到有子节点需要渲染
|
||||
if (this.nodeData.children && this.nodeData.children.length > 0) {
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
}
|
||||
} else {
|
||||
// 有子节点到没子节点,需要删除
|
||||
if (this._unVisibleRectRegionNode) {
|
||||
this.clearExpandBtnPlaceholderRect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
renderExpandBtnPlaceholderRect,
|
||||
clearExpandBtnPlaceholderRect,
|
||||
updateExpandBtnPlaceholderRect
|
||||
}
|
||||
@@ -1,115 +1,218 @@
|
||||
import Node from './Node'
|
||||
import { createUid } from '../../../utils/index'
|
||||
|
||||
// 获取节点概要数据
|
||||
function formatGetGeneralization() {
|
||||
const data = this.getData('generalization')
|
||||
return Array.isArray(data) ? data : data ? [data] : []
|
||||
}
|
||||
|
||||
// 检查是否存在概要
|
||||
function checkHasGeneralization () {
|
||||
return !!this.nodeData.data.generalization
|
||||
function checkHasGeneralization() {
|
||||
return this.formatGetGeneralization().length > 0
|
||||
}
|
||||
|
||||
// 检查是否存在自身的概要,非子节点区间
|
||||
function checkHasSelfGeneralization() {
|
||||
const list = this.formatGetGeneralization()
|
||||
return !!list.find(item => {
|
||||
return !item.range || item.range.length <= 0
|
||||
})
|
||||
}
|
||||
|
||||
// 获取概要节点所在的概要列表里的索引
|
||||
function getGeneralizationNodeIndex(node) {
|
||||
return this._generalizationList.findIndex(item => {
|
||||
return item.generalizationNode.uid === node.uid
|
||||
})
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
function createGeneralizationNode () {
|
||||
function createGeneralizationNode() {
|
||||
if (this.isGeneralization || !this.checkHasGeneralization()) {
|
||||
return
|
||||
}
|
||||
if (!this._generalizationLine) {
|
||||
this._generalizationLine = this.draw.path()
|
||||
}
|
||||
if (!this._generalizationNode) {
|
||||
this._generalizationNode = new Node({
|
||||
data: {
|
||||
data: this.nodeData.data.generalization
|
||||
},
|
||||
uid: this.mindMap.uid++,
|
||||
renderer: this.renderer,
|
||||
mindMap: this.mindMap,
|
||||
draw: this.draw,
|
||||
isGeneralization: true
|
||||
})
|
||||
this._generalizationNodeWidth = this._generalizationNode.width
|
||||
this._generalizationNodeHeight = this._generalizationNode.height
|
||||
this._generalizationNode.generalizationBelongNode = this
|
||||
if (this.nodeData.data.generalization.isActive) {
|
||||
this.renderer.addActiveNode(this._generalizationNode)
|
||||
let maxWidth = 0
|
||||
let maxHeight = 0
|
||||
const list = this.formatGetGeneralization()
|
||||
list.forEach((item, index) => {
|
||||
let cur = this._generalizationList[index]
|
||||
if (!cur) {
|
||||
cur = this._generalizationList[index] = {}
|
||||
}
|
||||
}
|
||||
// 所属节点
|
||||
cur.node = this
|
||||
// 区间范围
|
||||
cur.range = item.range
|
||||
// 线和节点
|
||||
if (!cur.generalizationLine) {
|
||||
cur.generalizationLine = this.lineDraw.path()
|
||||
}
|
||||
if (!cur.generalizationNode) {
|
||||
cur.generalizationNode = new Node({
|
||||
data: {
|
||||
data: item
|
||||
},
|
||||
uid: createUid(),
|
||||
renderer: this.renderer,
|
||||
mindMap: this.mindMap,
|
||||
isGeneralization: true
|
||||
})
|
||||
}
|
||||
// 关联所属节点
|
||||
cur.generalizationNode.generalizationBelongNode = this
|
||||
// 大小
|
||||
if (cur.generalizationNode.width > maxWidth)
|
||||
maxWidth = cur.generalizationNode.width
|
||||
if (cur.generalizationNode.height > maxHeight)
|
||||
maxHeight = cur.generalizationNode.height
|
||||
// 如果该概要为激活状态,那么加入激活节点列表
|
||||
if (item.isActive) {
|
||||
this.renderer.addNodeToActiveList(cur.generalizationNode)
|
||||
}
|
||||
})
|
||||
this._generalizationNodeWidth = maxWidth
|
||||
this._generalizationNodeHeight = maxHeight
|
||||
}
|
||||
|
||||
// 更新概要节点
|
||||
function updateGeneralization () {
|
||||
function updateGeneralization() {
|
||||
if (this.isGeneralization) return
|
||||
this.removeGeneralization()
|
||||
this.createGeneralizationNode()
|
||||
}
|
||||
|
||||
// 渲染概要节点
|
||||
function renderGeneralization () {
|
||||
if (this.isGeneralization) {
|
||||
return
|
||||
}
|
||||
if (!this.checkHasGeneralization()) {
|
||||
this.removeGeneralization()
|
||||
this._generalizationNodeWidth = 0
|
||||
this._generalizationNodeHeight = 0
|
||||
return
|
||||
}
|
||||
if (this.nodeData.data.expand === false) {
|
||||
function renderGeneralization() {
|
||||
if (this.isGeneralization) return
|
||||
this.updateGeneralizationData()
|
||||
const list = this.formatGetGeneralization()
|
||||
if (list.length <= 0 || this.getData('expand') === false) {
|
||||
this.removeGeneralization()
|
||||
return
|
||||
}
|
||||
if (list.length !== this._generalizationList.length) {
|
||||
this.removeGeneralization()
|
||||
}
|
||||
this.createGeneralizationNode()
|
||||
this.renderer.layout.renderGeneralization(
|
||||
this,
|
||||
this._generalizationLine,
|
||||
this._generalizationNode
|
||||
)
|
||||
this.style.generalizationLine(this._generalizationLine)
|
||||
this._generalizationNode.render()
|
||||
this.renderer.layout.renderGeneralization(this._generalizationList)
|
||||
this._generalizationList.forEach(item => {
|
||||
this.style.generalizationLine(item.generalizationLine)
|
||||
item.generalizationNode.render()
|
||||
})
|
||||
}
|
||||
|
||||
// 更新节点概要数据
|
||||
function updateGeneralizationData() {
|
||||
const childrenLength = this.children.length
|
||||
const list = this.formatGetGeneralization()
|
||||
const newList = []
|
||||
list.forEach(item => {
|
||||
if (!item.range) {
|
||||
newList.push(item)
|
||||
return
|
||||
}
|
||||
if (
|
||||
item.range.length > 0 &&
|
||||
item.range[0] <= childrenLength - 1 &&
|
||||
item.range[1] <= childrenLength - 1
|
||||
) {
|
||||
newList.push(item)
|
||||
}
|
||||
})
|
||||
if (newList.length !== list.length) {
|
||||
this.setData({
|
||||
generalization: newList
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除概要节点
|
||||
function removeGeneralization () {
|
||||
if (this._generalizationLine) {
|
||||
this._generalizationLine.remove()
|
||||
this._generalizationLine = null
|
||||
}
|
||||
if (this._generalizationNode) {
|
||||
// 删除概要节点时要同步从激活节点里删除
|
||||
this.renderer.removeActiveNode(this._generalizationNode)
|
||||
this._generalizationNode.remove()
|
||||
this._generalizationNode = null
|
||||
}
|
||||
function removeGeneralization() {
|
||||
if (this.isGeneralization) return
|
||||
this._generalizationList.forEach(item => {
|
||||
if (item.generalizationLine) {
|
||||
item.generalizationLine.remove()
|
||||
item.generalizationLine = null
|
||||
}
|
||||
if (item.generalizationNode) {
|
||||
// 删除概要节点时要同步从激活节点里删除
|
||||
this.renderer.removeNodeFromActiveList(item.generalizationNode)
|
||||
item.generalizationNode.remove()
|
||||
item.generalizationNode = null
|
||||
}
|
||||
})
|
||||
this._generalizationList = []
|
||||
// hack修复当激活一个节点时创建概要,然后立即激活创建的概要节点后会重复创建概要节点并且无法删除的问题
|
||||
if (this.generalizationBelongNode) {
|
||||
this.draw
|
||||
this.nodeDraw
|
||||
.find('.generalization_' + this.generalizationBelongNode.uid)
|
||||
.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏概要节点
|
||||
function hideGeneralization () {
|
||||
if (this._generalizationLine) {
|
||||
this._generalizationLine.hide()
|
||||
}
|
||||
if (this._generalizationNode) {
|
||||
this._generalizationNode.hide()
|
||||
}
|
||||
function hideGeneralization() {
|
||||
if (this.isGeneralization) return
|
||||
this._generalizationList.forEach(item => {
|
||||
if (item.generalizationLine) item.generalizationLine.hide()
|
||||
if (item.generalizationNode) item.generalizationNode.hide()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示概要节点
|
||||
function showGeneralization () {
|
||||
if (this._generalizationLine) {
|
||||
this._generalizationLine.show()
|
||||
}
|
||||
if (this._generalizationNode) {
|
||||
this._generalizationNode.show()
|
||||
function showGeneralization() {
|
||||
if (this.isGeneralization) return
|
||||
this._generalizationList.forEach(item => {
|
||||
if (item.generalizationLine) item.generalizationLine.show()
|
||||
if (item.generalizationNode) item.generalizationNode.show()
|
||||
})
|
||||
}
|
||||
|
||||
// 设置概要节点的透明度
|
||||
function setGeneralizationOpacity(val) {
|
||||
this._generalizationList.forEach(item => {
|
||||
item.generalizationLine.opacity(val)
|
||||
item.generalizationNode.group.opacity(val)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理概要节点鼠标移入事件
|
||||
function handleGeneralizationMouseenter() {
|
||||
const belongNode = this.generalizationBelongNode
|
||||
const list = belongNode.formatGetGeneralization()
|
||||
const index = belongNode.getGeneralizationNodeIndex(this)
|
||||
const generalizationData = list[index]
|
||||
// 区间概要,框子节点
|
||||
if (
|
||||
Array.isArray(generalizationData.range) &&
|
||||
generalizationData.range.length > 0
|
||||
) {
|
||||
this.mindMap.renderer.highlightNode(belongNode, generalizationData.range)
|
||||
} else {
|
||||
// 否则框自己
|
||||
this.mindMap.renderer.highlightNode(belongNode)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理概要节点鼠标移出事件
|
||||
function handleGeneralizationMouseleave() {
|
||||
this.mindMap.renderer.closeHighlightNode()
|
||||
}
|
||||
|
||||
export default {
|
||||
checkHasGeneralization,
|
||||
createGeneralizationNode,
|
||||
updateGeneralization,
|
||||
renderGeneralization,
|
||||
removeGeneralization,
|
||||
hideGeneralization,
|
||||
showGeneralization
|
||||
}
|
||||
formatGetGeneralization,
|
||||
checkHasGeneralization,
|
||||
checkHasSelfGeneralization,
|
||||
getGeneralizationNodeIndex,
|
||||
createGeneralizationNode,
|
||||
updateGeneralization,
|
||||
updateGeneralizationData,
|
||||
renderGeneralization,
|
||||
removeGeneralization,
|
||||
hideGeneralization,
|
||||
showGeneralization,
|
||||
setGeneralizationOpacity,
|
||||
handleGeneralizationMouseenter,
|
||||
handleGeneralizationMouseleave
|
||||
}
|
||||
|
||||
@@ -25,23 +25,19 @@ class View {
|
||||
this.mindMap.keyCommand.addShortcut('Control+-', () => {
|
||||
this.narrow()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Control+Enter', () => {
|
||||
this.reset()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Control+i', () => {
|
||||
this.fit()
|
||||
})
|
||||
this.mindMap.svg.on('dblclick', () => {
|
||||
this.reset()
|
||||
})
|
||||
// 拖动视图
|
||||
this.mindMap.event.on('mousedown', () => {
|
||||
if (this.mindMap.opt.isDisableDrag) return
|
||||
this.sx = this.x
|
||||
this.sy = this.y
|
||||
})
|
||||
this.mindMap.event.on('drag', (e, event) => {
|
||||
if (e.ctrlKey) {
|
||||
// 按住ctrl键拖动为多选
|
||||
// 按住ctrl键拖动为多选
|
||||
// 禁用拖拽
|
||||
if (e.ctrlKey || this.mindMap.opt.isDisableDrag) {
|
||||
return
|
||||
}
|
||||
if (this.firstDrag) {
|
||||
@@ -60,29 +56,49 @@ class View {
|
||||
})
|
||||
// 放大缩小视图
|
||||
this.mindMap.event.on('mousewheel', (e, dir, event, isTouchPad) => {
|
||||
let {
|
||||
customHandleMousewheel,
|
||||
mousewheelAction,
|
||||
mouseScaleCenterUseMousePosition,
|
||||
mousewheelMoveStep,
|
||||
mousewheelZoomActionReverse,
|
||||
disableMouseWheelZoom
|
||||
} = this.mindMap.opt
|
||||
// 是否自定义鼠标滚轮事件
|
||||
if (
|
||||
this.mindMap.opt.customHandleMousewheel &&
|
||||
typeof this.mindMap.opt.customHandleMousewheel === 'function'
|
||||
customHandleMousewheel &&
|
||||
typeof customHandleMousewheel === 'function'
|
||||
) {
|
||||
return this.mindMap.opt.customHandleMousewheel(e)
|
||||
return customHandleMousewheel(e)
|
||||
}
|
||||
if (
|
||||
this.mindMap.opt.mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM
|
||||
) {
|
||||
// 鼠标滚轮事件控制缩放
|
||||
if (mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
|
||||
if (disableMouseWheelZoom) return
|
||||
const { x: clientX, y: clientY } = this.mindMap.toPos(
|
||||
e.clientX,
|
||||
e.clientY
|
||||
)
|
||||
let cx = mouseScaleCenterUseMousePosition ? clientX : undefined
|
||||
let cy = mouseScaleCenterUseMousePosition ? clientY : undefined
|
||||
switch (dir) {
|
||||
// 鼠标滚轮,向上和向左,都是缩小
|
||||
case CONSTANTS.DIR.UP:
|
||||
case CONSTANTS.DIR.LEFT:
|
||||
this.narrow()
|
||||
mousewheelZoomActionReverse
|
||||
? this.enlarge(cx, cy, isTouchPad)
|
||||
: this.narrow(cx, cy, isTouchPad)
|
||||
break
|
||||
// 鼠标滚轮,向下和向右,都是放大
|
||||
case CONSTANTS.DIR.DOWN:
|
||||
case CONSTANTS.DIR.RIGHT:
|
||||
this.enlarge()
|
||||
mousewheelZoomActionReverse
|
||||
? this.narrow(cx, cy, isTouchPad)
|
||||
: this.enlarge(cx, cy, isTouchPad)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
let step = this.mindMap.opt.mousewheelMoveStep
|
||||
// 鼠标滚轮事件控制画布移动
|
||||
let step = mousewheelMoveStep
|
||||
if (isTouchPad) {
|
||||
step = 5
|
||||
}
|
||||
@@ -138,6 +154,7 @@ class View {
|
||||
|
||||
// 平移x,y方向
|
||||
translateXY(x, y) {
|
||||
if (x === 0 && y === 0) return
|
||||
this.x += x
|
||||
this.y += y
|
||||
this.transform()
|
||||
@@ -145,6 +162,7 @@ class View {
|
||||
|
||||
// 平移x方向
|
||||
translateX(step) {
|
||||
if (step === 0) return
|
||||
this.x += step
|
||||
this.transform()
|
||||
}
|
||||
@@ -157,6 +175,7 @@ class View {
|
||||
|
||||
// 平移y方向
|
||||
translateY(step) {
|
||||
if (step === 0) return
|
||||
this.y += step
|
||||
this.transform()
|
||||
}
|
||||
@@ -170,8 +189,8 @@ class View {
|
||||
// 应用变换
|
||||
transform() {
|
||||
this.mindMap.draw.transform({
|
||||
origin: [0, 0],
|
||||
scale: this.scale,
|
||||
// origin: 'center center',
|
||||
translate: [this.x, this.y]
|
||||
})
|
||||
this.mindMap.emit('view_data_change', this.getTransformData())
|
||||
@@ -190,26 +209,45 @@ class View {
|
||||
}
|
||||
|
||||
// 缩小
|
||||
narrow() {
|
||||
if (this.scale - this.mindMap.opt.scaleRatio > 0.1) {
|
||||
this.scale -= this.mindMap.opt.scaleRatio
|
||||
} else {
|
||||
this.scale = 0.1
|
||||
}
|
||||
narrow(cx, cy, isTouchPad) {
|
||||
const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1)
|
||||
const scale = Math.max(this.scale - scaleRatio, 0.1)
|
||||
this.scaleInCenter(scale, cx, cy)
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
|
||||
// 放大
|
||||
enlarge() {
|
||||
this.scale += this.mindMap.opt.scaleRatio
|
||||
enlarge(cx, cy, isTouchPad) {
|
||||
const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1)
|
||||
const scale = this.scale + scaleRatio
|
||||
this.scaleInCenter(scale, cx, cy)
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
|
||||
// 设置缩放
|
||||
setScale(scale) {
|
||||
// 基于指定中心进行缩放,cx,cy 可不指定,此时会使用画布中心点
|
||||
scaleInCenter(scale, cx, cy) {
|
||||
if (cx === undefined || cy === undefined) {
|
||||
cx = this.mindMap.width / 2
|
||||
cy = this.mindMap.height / 2
|
||||
}
|
||||
const prevScale = this.scale
|
||||
const ratio = 1 - scale / prevScale
|
||||
const dx = (cx - this.x) * ratio
|
||||
const dy = (cy - this.y) * ratio
|
||||
this.x += dx
|
||||
this.y += dy
|
||||
this.scale = scale
|
||||
}
|
||||
|
||||
// 设置缩放
|
||||
setScale(scale, cx, cy) {
|
||||
if (cx !== undefined && cy !== undefined) {
|
||||
this.scaleInCenter(scale, cx, cy)
|
||||
} else {
|
||||
this.scale = scale
|
||||
}
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
@@ -224,7 +262,7 @@ class View {
|
||||
let drawHeight = rect.height / origTransform.scaleY
|
||||
let drawRatio = drawWidth / drawHeight
|
||||
let { width: elWidth, height: elHeight } =
|
||||
this.mindMap.el.getBoundingClientRect()
|
||||
this.mindMap.elRect
|
||||
elWidth = elWidth - fitPadding * 2
|
||||
elHeight = elHeight - fitPadding * 2
|
||||
let elRatio = elWidth / elHeight
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Node from '../core/render/node/Node'
|
||||
import { CONSTANTS, initRootNodePositionMap } from '../constants/constant'
|
||||
import Lru from '../utils/Lru'
|
||||
import { createUid } from '../utils/index'
|
||||
|
||||
// 布局基类
|
||||
class Base {
|
||||
@@ -12,6 +13,7 @@ class Base {
|
||||
this.mindMap = renderer.mindMap
|
||||
// 绘图对象
|
||||
this.draw = this.mindMap.draw
|
||||
this.lineDraw = this.mindMap.lineDraw
|
||||
// 根节点
|
||||
this.root = null
|
||||
this.lru = new Lru(this.mindMap.opt.maxNodeCacheCount)
|
||||
@@ -45,7 +47,24 @@ class Base {
|
||||
|
||||
// 检查当前来源是否需要重新计算节点大小
|
||||
checkIsNeedResizeSources() {
|
||||
return [CONSTANTS.CHANGE_THEME, CONSTANTS.TRANSFORM_TO_NORMAL_NODE].includes(this.renderer.renderSource)
|
||||
return [
|
||||
CONSTANTS.CHANGE_THEME,
|
||||
CONSTANTS.TRANSFORM_TO_NORMAL_NODE
|
||||
].includes(this.renderer.renderSource)
|
||||
}
|
||||
|
||||
// 层级类型改变
|
||||
checkIsLayerTypeChange(oldIndex, newIndex) {
|
||||
if (oldIndex >= 2 && newIndex >= 2) return false
|
||||
if (oldIndex >= 2 && newIndex < 2) return true
|
||||
if (oldIndex < 2 && newIndex >= 2) return true
|
||||
}
|
||||
|
||||
// 检查是否是结构布局改变重新渲染展开收起按钮占位元素
|
||||
checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) {
|
||||
if (this.renderer.renderSource === CONSTANTS.CHANGE_LAYOUT) {
|
||||
node.needRerenderExpandBtnPlaceholderRect = true
|
||||
}
|
||||
}
|
||||
|
||||
// 创建节点实例
|
||||
@@ -55,11 +74,16 @@ class Base {
|
||||
// 数据上保存了节点引用,那么直接复用节点
|
||||
if (data && data._node && !this.renderer.reRender) {
|
||||
newNode = data._node
|
||||
let isLayerTypeChange = this.checkIsLayerTypeChange(
|
||||
newNode.layerIndex,
|
||||
layerIndex
|
||||
)
|
||||
newNode.reset()
|
||||
newNode.layerIndex = layerIndex
|
||||
this.cacheNode(data._node.uid, newNode)
|
||||
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
|
||||
// 主题或主题配置改变了需要重新计算节点大小和布局
|
||||
if (this.checkIsNeedResizeSources()) {
|
||||
if (this.checkIsNeedResizeSources() || isLayerTypeChange) {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
}
|
||||
@@ -67,23 +91,28 @@ class Base {
|
||||
// 数据上没有保存节点引用,但是通过uid找到了缓存的节点,也可以复用
|
||||
newNode = this.lru.get(data.data.uid)
|
||||
// 保存该节点上一次的数据
|
||||
let lastData = JSON.stringify(newNode.nodeData.data)
|
||||
let lastData = JSON.stringify(newNode.getData())
|
||||
let isLayerTypeChange = this.checkIsLayerTypeChange(
|
||||
newNode.layerIndex,
|
||||
layerIndex
|
||||
)
|
||||
newNode.reset()
|
||||
newNode.nodeData = newNode.handleData(data || {})
|
||||
newNode.layerIndex = layerIndex
|
||||
this.cacheNode(data.data.uid, newNode)
|
||||
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
|
||||
data._node = newNode
|
||||
// 主题或主题配置改变了需要重新计算节点大小和布局
|
||||
let isResizeSource = this.checkIsNeedResizeSources()
|
||||
// 节点数据改变了需要重新计算节点大小和布局
|
||||
let isNodeDataChange = lastData !== JSON.stringify(data.data)
|
||||
if (isResizeSource || isNodeDataChange) {
|
||||
if (isResizeSource || isNodeDataChange || isLayerTypeChange) {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
}
|
||||
} else {
|
||||
// 创建新节点
|
||||
let uid = this.mindMap.uid++
|
||||
let uid = data.data.uid || createUid()
|
||||
newNode = new Node({
|
||||
data,
|
||||
uid,
|
||||
@@ -98,9 +127,15 @@ class Base {
|
||||
// 数据关联实际节点
|
||||
data._node = newNode
|
||||
if (data.data.isActive) {
|
||||
this.renderer.addActiveNode(newNode)
|
||||
this.renderer.addNodeToActiveList(newNode)
|
||||
}
|
||||
}
|
||||
// 如果当前节点在激活节点列表里,那么添加上激活的状态
|
||||
if (this.mindMap.renderer.findActiveNodeIndex(newNode) !== -1) {
|
||||
newNode.setData({
|
||||
isActive: true
|
||||
})
|
||||
}
|
||||
// 根节点
|
||||
if (isRoot) {
|
||||
newNode.isRoot = true
|
||||
@@ -120,7 +155,7 @@ class Base {
|
||||
} else if (initRootNodePositionMap[value] !== undefined) {
|
||||
return size * initRootNodePositionMap[value]
|
||||
} else if (/^\d\d*%$/.test(value)) {
|
||||
return Number.parseFloat(value) / 100 * size
|
||||
return (Number.parseFloat(value) / 100) * size
|
||||
} else {
|
||||
return (size - nodeSize) / 2
|
||||
}
|
||||
@@ -129,12 +164,24 @@ class Base {
|
||||
// 定位节点到画布中间
|
||||
setNodeCenter(node) {
|
||||
let { initRootNodePosition } = this.mindMap.opt
|
||||
let { CENTER }= CONSTANTS.INIT_ROOT_NODE_POSITION
|
||||
if (!initRootNodePosition || !Array.isArray(initRootNodePosition) || initRootNodePosition.length < 2) {
|
||||
let { CENTER } = CONSTANTS.INIT_ROOT_NODE_POSITION
|
||||
if (
|
||||
!initRootNodePosition ||
|
||||
!Array.isArray(initRootNodePosition) ||
|
||||
initRootNodePosition.length < 2
|
||||
) {
|
||||
initRootNodePosition = [CENTER, CENTER]
|
||||
}
|
||||
node.left = this.formatPosition(initRootNodePosition[0], this.mindMap.width, node.width)
|
||||
node.top = this.formatPosition(initRootNodePosition[1], this.mindMap.height, node.height)
|
||||
node.left = this.formatPosition(
|
||||
initRootNodePosition[0],
|
||||
this.mindMap.width,
|
||||
node.width
|
||||
)
|
||||
node.top = this.formatPosition(
|
||||
initRootNodePosition[1],
|
||||
this.mindMap.height,
|
||||
node.height
|
||||
)
|
||||
}
|
||||
|
||||
// 更新子节点属性
|
||||
@@ -151,7 +198,7 @@ class Base {
|
||||
// 更新子节点多个属性
|
||||
updateChildrenPro(children, props) {
|
||||
children.forEach(item => {
|
||||
Object.keys(props).forEach((prop) => {
|
||||
Object.keys(props).forEach(prop => {
|
||||
item[prop] += props[prop]
|
||||
})
|
||||
if (item.children && item.children.length && !item.hasCustomPosition()) {
|
||||
@@ -162,9 +209,13 @@ class Base {
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaWidth(node) {
|
||||
getNodeAreaWidth(node, withGeneralization = false) {
|
||||
let widthArr = []
|
||||
let totalGeneralizationNodeWidth = 0
|
||||
let loop = (node, width) => {
|
||||
if (withGeneralization && node.checkHasGeneralization()) {
|
||||
totalGeneralizationNodeWidth += node._generalizationNodeWidth
|
||||
}
|
||||
if (node.children.length) {
|
||||
width += node.width / 2
|
||||
node.children.forEach(item => {
|
||||
@@ -176,7 +227,7 @@ class Base {
|
||||
}
|
||||
}
|
||||
loop(node, 0)
|
||||
return Math.max(...widthArr)
|
||||
return Math.max(...widthArr) + totalGeneralizationNodeWidth
|
||||
}
|
||||
|
||||
// 二次贝塞尔曲线
|
||||
@@ -197,16 +248,22 @@ class Base {
|
||||
|
||||
// 获取节点的marginX
|
||||
getMarginX(layerIndex) {
|
||||
const { themeConfig, opt } = this.mindMap
|
||||
const { second, node } = themeConfig
|
||||
const hoverRectPadding = opt.hoverRectPadding * 2
|
||||
return layerIndex === 1
|
||||
? this.mindMap.themeConfig.second.marginX
|
||||
: this.mindMap.themeConfig.node.marginX
|
||||
? second.marginX + hoverRectPadding
|
||||
: node.marginX + hoverRectPadding
|
||||
}
|
||||
|
||||
// 获取节点的marginY
|
||||
getMarginY(layerIndex) {
|
||||
const { themeConfig, opt } = this.mindMap
|
||||
const { second, node } = themeConfig
|
||||
const hoverRectPadding = opt.hoverRectPadding * 2
|
||||
return layerIndex === 1
|
||||
? this.mindMap.themeConfig.second.marginY
|
||||
: this.mindMap.themeConfig.node.marginY
|
||||
? second.marginY + hoverRectPadding
|
||||
: node.marginY + hoverRectPadding
|
||||
}
|
||||
|
||||
// 获取节点包括概要在内的宽度
|
||||
@@ -243,12 +300,12 @@ class Base {
|
||||
let { left, right, top, bottom } = walk(child)
|
||||
// 概要内容的宽度
|
||||
let generalizationWidth =
|
||||
child.checkHasGeneralization() && child.nodeData.data.expand
|
||||
child.checkHasGeneralization() && child.getData('expand')
|
||||
? child._generalizationNodeWidth + generalizationNodeMargin
|
||||
: 0
|
||||
// 概要内容的高度
|
||||
let generalizationHeight =
|
||||
child.checkHasGeneralization() && child.nodeData.data.expand
|
||||
child.checkHasGeneralization() && child.getData('expand')
|
||||
? child._generalizationNodeHeight + generalizationNodeMargin
|
||||
: 0
|
||||
if (left - (dir === 'h' ? generalizationWidth : 0) < _left) {
|
||||
@@ -289,6 +346,50 @@ class Base {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取指定索引区间的子节点的边界范围
|
||||
getChildrenBoundaries(node, dir, startIndex = 0, endIndex) {
|
||||
let { generalizationLineMargin, generalizationNodeMargin } =
|
||||
this.mindMap.themeConfig
|
||||
const children = node.children.slice(startIndex, endIndex + 1)
|
||||
let left = Infinity
|
||||
let right = -Infinity
|
||||
let top = Infinity
|
||||
let bottom = -Infinity
|
||||
children.forEach(item => {
|
||||
const cur = this.getNodeBoundaries(item, dir)
|
||||
left = cur.left < left ? cur.left : left
|
||||
right = cur.right > right ? cur.right : right
|
||||
top = cur.top < top ? cur.top : top
|
||||
bottom = cur.bottom > bottom ? cur.bottom : bottom
|
||||
})
|
||||
return {
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点概要的渲染边界
|
||||
getNodeGeneralizationRenderBoundaries(item, dir) {
|
||||
let res = null
|
||||
// 区间
|
||||
if (item.range) {
|
||||
res = this.getChildrenBoundaries(
|
||||
item.node,
|
||||
dir,
|
||||
item.range[0],
|
||||
item.range[1]
|
||||
)
|
||||
} else {
|
||||
// 整体概要
|
||||
res = this.getNodeBoundaries(item.node, dir)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 获取节点实际存在几个子节点
|
||||
getNodeActChildrenLength(node) {
|
||||
return node.nodeData.children && node.nodeData.children.length
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
||||
|
||||
// 目录组织图
|
||||
class CatalogOrganization extends Base {
|
||||
@@ -72,11 +72,7 @@ class CatalogOrganization extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
if (node.getData('expand') && node.children && node.children.length) {
|
||||
let marginX = this.getMarginX(layerIndex + 1)
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
if (isRoot) {
|
||||
@@ -87,11 +83,18 @@ class CatalogOrganization extends Base {
|
||||
totalLeft += cur.width + marginX
|
||||
})
|
||||
} else {
|
||||
let totalTop = node.top + node.height + marginY + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
let totalTop =
|
||||
node.top +
|
||||
this.getNodeHeightWithGeneralization(node) +
|
||||
marginY +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(cur => {
|
||||
cur.left = node.left + node.width * 0.5
|
||||
cur.top = totalTop
|
||||
totalTop += cur.height + marginY + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
|
||||
totalTop +=
|
||||
this.getNodeHeightWithGeneralization(cur) +
|
||||
marginY +
|
||||
(this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -107,12 +110,12 @@ class CatalogOrganization extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
// 调整left
|
||||
if (parent && parent.isRoot) {
|
||||
let areaWidth = this.getNodeAreaWidth(node)
|
||||
let areaWidth = this.getNodeAreaWidth(node, true)
|
||||
let difference = areaWidth - node.width
|
||||
if (difference > 0) {
|
||||
this.updateBrothersLeft(node, difference)
|
||||
@@ -124,7 +127,13 @@ class CatalogOrganization extends Base {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
let totalHeight =
|
||||
node.children.reduce((h, item) => {
|
||||
return h + item.height + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
return (
|
||||
h +
|
||||
this.getNodeHeightWithGeneralization(item) +
|
||||
(this.getNodeActChildrenLength(item) > 0
|
||||
? item.expandBtnSize
|
||||
: 0)
|
||||
)
|
||||
}, 0) +
|
||||
len * marginY
|
||||
this.updateBrothersTop(node, totalHeight)
|
||||
@@ -134,7 +143,7 @@ class CatalogOrganization extends Base {
|
||||
if (isRoot) {
|
||||
let { right, left } = this.getNodeBoundaries(node, 'h')
|
||||
let childrenWidth = right - left
|
||||
let offset = (node.left - left) - (childrenWidth - node.width) / 2
|
||||
let offset = node.left - left - (childrenWidth - node.width) / 2
|
||||
this.updateChildren(node.children, 'left', offset)
|
||||
}
|
||||
},
|
||||
@@ -146,9 +155,7 @@ class CatalogOrganization extends Base {
|
||||
updateBrothersLeft(node, addWidth) {
|
||||
if (node.parent) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition() || _index <= index) {
|
||||
// 适配自定义位置
|
||||
@@ -169,9 +176,7 @@ class CatalogOrganization extends Base {
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
@@ -234,14 +239,14 @@ class CatalogOrganization extends Base {
|
||||
minx = Math.min(minx, x1)
|
||||
maxx = Math.max(maxx, x1)
|
||||
// 父节点的竖线
|
||||
let line1 = this.draw.path()
|
||||
let line1 = this.lineDraw.path()
|
||||
node.style.line(line1)
|
||||
line1.plot(`M ${x1},${y1} L ${x1},${y1 + s1}`)
|
||||
node._lines.push(line1)
|
||||
style && style(line1, node)
|
||||
// 水平线
|
||||
if (len > 0) {
|
||||
let lin2 = this.draw.path()
|
||||
let lin2 = this.lineDraw.path()
|
||||
node.style.line(lin2)
|
||||
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
|
||||
node._lines.push(lin2)
|
||||
@@ -302,7 +307,7 @@ class CatalogOrganization extends Base {
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let lin2 = this.draw.path()
|
||||
let lin2 = this.lineDraw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
node.style.line(lin2)
|
||||
if (maxy < y1 + expandBtnSize) {
|
||||
@@ -330,24 +335,32 @@ class CatalogOrganization extends Base {
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
renderGeneralization(list) {
|
||||
list.forEach(item => {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
item.generalizationLine.plot(path)
|
||||
item.generalizationNode.left = right + generalizationNodeMargin
|
||||
item.generalizationNode.top =
|
||||
top + (bottom - top - item.generalizationNode.height) / 2
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun, degToRad } from '../utils'
|
||||
import { walk, asyncRun, degToRad, getNodeIndexInNodeList } from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
import utils from './fishboneUtils'
|
||||
|
||||
@@ -51,15 +51,16 @@ class Fishbone extends Base {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
let marginY = this.getMarginY(layerIndex)
|
||||
if (this.checkIsTop(newNode)) {
|
||||
newNode.top = parent._node.top - newNode.height
|
||||
newNode.top = parent._node.top - newNode.height - marginY
|
||||
} else {
|
||||
newNode.top = parent._node.top + parent._node.height
|
||||
newNode.top = parent._node.top + parent._node.height + marginY
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,15 +81,16 @@ class Fishbone extends Base {
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (node.isRoot) {
|
||||
let topTotalLeft = node.left + node.width + node.height
|
||||
let bottomTotalLeft = node.left + node.width + node.height
|
||||
let marginX = this.getMarginX(layerIndex + 1)
|
||||
let topTotalLeft = node.left + node.width + node.height + marginX
|
||||
let bottomTotalLeft = node.left + node.width + node.height + marginX
|
||||
node.children.forEach(item => {
|
||||
if (this.checkIsTop(item)) {
|
||||
item.left = topTotalLeft
|
||||
topTotalLeft += item.width
|
||||
topTotalLeft += item.width + marginX
|
||||
} else {
|
||||
item.left = bottomTotalLeft + 20
|
||||
bottomTotalLeft += item.width
|
||||
bottomTotalLeft += item.width + marginX
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -110,7 +112,7 @@ class Fishbone extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
let params = { node, parent, layerIndex, ctx: this }
|
||||
@@ -154,9 +156,11 @@ class Fishbone extends Base {
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
let marginY = this.getMarginY(node.layerIndex)
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
|
||||
marginY
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
@@ -189,9 +193,7 @@ class Fishbone extends Base {
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
@@ -222,7 +224,7 @@ class Fishbone extends Base {
|
||||
|
||||
// 检查节点是否是上方节点
|
||||
checkIsTop(node) {
|
||||
return node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
return node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
@@ -239,26 +241,27 @@ class Fishbone extends Base {
|
||||
// 当前节点是根节点
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
let maxx = -Infinity
|
||||
node.children.forEach((item) => {
|
||||
node.children.forEach(item => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
// 水平线段到二级节点的连线
|
||||
let marginY = this.getMarginY(item.layerIndex)
|
||||
let nodeLineX = item.left
|
||||
let offset = node.height / 2
|
||||
let offset = node.height / 2 + marginY
|
||||
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
let line = this.draw.path()
|
||||
let line = this.lineDraw.path()
|
||||
if (this.checkIsTop(item)) {
|
||||
line.plot(
|
||||
`M ${nodeLineX - offsetX},${item.top + item.height + offset} L ${item.left},${
|
||||
item.top + item.height
|
||||
}`
|
||||
`M ${nodeLineX - offsetX},${item.top + item.height + offset} L ${
|
||||
item.left
|
||||
},${item.top + item.height}`
|
||||
)
|
||||
} else {
|
||||
line.plot(
|
||||
`M ${nodeLineX - offsetX},${
|
||||
item.top - offset
|
||||
} L ${nodeLineX},${item.top}`
|
||||
`M ${nodeLineX - offsetX},${item.top - offset} L ${nodeLineX},${
|
||||
item.top
|
||||
}`
|
||||
)
|
||||
}
|
||||
node.style.line(line)
|
||||
@@ -267,8 +270,8 @@ class Fishbone extends Base {
|
||||
})
|
||||
// 从根节点出发的水平线
|
||||
let nodeHalfTop = node.top + node.height / 2
|
||||
let offset = node.height / 2
|
||||
let line = this.draw.path()
|
||||
let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1)
|
||||
let line = this.lineDraw.path()
|
||||
line.plot(
|
||||
`M ${node.left + node.width},${nodeHalfTop} L ${
|
||||
maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
@@ -303,7 +306,7 @@ class Fishbone extends Base {
|
||||
})
|
||||
// 斜线
|
||||
if (len >= 0) {
|
||||
let line = this.draw.path()
|
||||
let line = this.lineDraw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * this.indent
|
||||
lineLength = Math.max(lineLength, 0)
|
||||
@@ -354,24 +357,48 @@ class Fishbone extends Base {
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
renderGeneralization(list) {
|
||||
list.forEach(item => {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
item.generalizationLine.plot(path)
|
||||
item.generalizationNode.left = right + generalizationNodeMargin
|
||||
item.generalizationNode.top =
|
||||
top + (bottom - top - item.generalizationNode.height) / 2
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
let dir = ''
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
|
||||
dir =
|
||||
node.layerIndex === 1
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
} else {
|
||||
dir =
|
||||
node.layerIndex === 1
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
}
|
||||
if (dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
|
||||
rect.size(width, expandBtnSize).x(0).y(-expandBtnSize)
|
||||
} else {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
|
||||
const degToRad = deg => {
|
||||
@@ -52,8 +52,8 @@ class Fishbone extends Base {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
@@ -127,7 +127,7 @@ class Fishbone extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
// 调整top
|
||||
@@ -170,7 +170,8 @@ class Fishbone extends Base {
|
||||
item.top += _top
|
||||
// 调整left
|
||||
let offsetLeft =
|
||||
(totalHeight2 + nodeTotalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
(totalHeight2 + nodeTotalHeight) /
|
||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
item.left += offsetLeft
|
||||
totalHeight += offset
|
||||
totalHeight2 += nodeTotalHeight
|
||||
@@ -236,9 +237,7 @@ class Fishbone extends Base {
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
@@ -306,13 +305,15 @@ class Fishbone extends Base {
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.draw.path()
|
||||
let line = this.lineDraw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * 0.3
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top + height} L ${x + lineLength},${
|
||||
top + height + Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
top +
|
||||
height +
|
||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
|
||||
const degToRad = deg => {
|
||||
@@ -52,8 +52,8 @@ class Fishbone extends Base {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
@@ -110,7 +110,7 @@ class Fishbone extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
// 调整top
|
||||
@@ -140,7 +140,8 @@ class Fishbone extends Base {
|
||||
node.top - (item.top - node.top) - nodeTotalHeight + node.height
|
||||
// 调整left
|
||||
let offsetLeft =
|
||||
(nodeTotalHeight + totalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
(nodeTotalHeight + totalHeight) /
|
||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
item.left += offsetLeft
|
||||
totalHeight += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
@@ -205,9 +206,7 @@ class Fishbone extends Base {
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
@@ -275,24 +274,26 @@ class Fishbone extends Base {
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.draw.path()
|
||||
let line = this.lineDraw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * 0.3
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
top - Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
top -
|
||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
top - Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
top -
|
||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
||||
|
||||
// 逻辑结构图
|
||||
class LogicalStructure extends Base {
|
||||
@@ -56,6 +56,15 @@ class LogicalStructure extends Base {
|
||||
}, 0) +
|
||||
(len + 1) * this.getMarginY(layerIndex + 1)
|
||||
: 0
|
||||
// 如果存在概要,则和概要的高度取最大值
|
||||
let generalizationNodeHeight = cur._node.checkHasGeneralization()
|
||||
? cur._node._generalizationNodeHeight +
|
||||
this.getMarginY(layerIndex + 1)
|
||||
: 0
|
||||
cur._node.childrenAreaHeight2 = Math.max(
|
||||
cur._node.childrenAreaHeight,
|
||||
generalizationNodeHeight
|
||||
)
|
||||
},
|
||||
true,
|
||||
0
|
||||
@@ -68,11 +77,7 @@ class LogicalStructure extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
if (node.getData('expand') && node.children && node.children.length) {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
// 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半
|
||||
let top = node.top + node.height / 2 - node.childrenAreaHeight / 2
|
||||
@@ -94,12 +99,12 @@ class LogicalStructure extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
|
||||
let difference =
|
||||
node.childrenAreaHeight -
|
||||
node.childrenAreaHeight2 -
|
||||
this.getMarginY(layerIndex + 1) * 2 -
|
||||
node.height
|
||||
if (difference > 0) {
|
||||
@@ -115,11 +120,9 @@ class LogicalStructure extends Base {
|
||||
updateBrothers(node, addHeight) {
|
||||
if (node.parent) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item === node || item.hasCustomPosition()) {
|
||||
if (item.uid === node.uid || item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
@@ -172,9 +175,7 @@ class LogicalStructure extends Base {
|
||||
let x2 = item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStyleOffset = nodeUseLineStyle
|
||||
? item.width
|
||||
: 0
|
||||
let nodeUseLineStyleOffset = nodeUseLineStyle ? item.width : 0
|
||||
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
|
||||
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
|
||||
let path = `M ${x1},${y1} L ${x1 + s1},${y1} L ${x1 + s1},${y2} L ${
|
||||
@@ -236,7 +237,7 @@ class LogicalStructure extends Base {
|
||||
let nodeUseLineStylePath = nodeUseLineStyle
|
||||
? ` L ${item.left + item.width},${y2}`
|
||||
: ''
|
||||
if (node.isRoot) {
|
||||
if (node.isRoot && !this.mindMap.themeConfig.rootLineKeepSameInCurve) {
|
||||
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
} else {
|
||||
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
@@ -260,31 +261,36 @@ class LogicalStructure extends Base {
|
||||
if (_x === translateX && _y === translateY) {
|
||||
return
|
||||
}
|
||||
btn.translate(
|
||||
_x - translateX,
|
||||
_y - translateY
|
||||
)
|
||||
btn.translate(_x - translateX, _y - translateY)
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
renderGeneralization(list) {
|
||||
list.forEach(item => {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
item.generalizationLine.plot(path)
|
||||
item.generalizationNode.left = right + generalizationNodeMargin
|
||||
item.generalizationNode.top =
|
||||
top + (bottom - top - item.generalizationNode.height) / 2
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
rect.size(expandBtnSize, height).x(width).y(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
// 思维导图
|
||||
class MindMap extends Base {
|
||||
@@ -45,11 +46,14 @@ class MindMap extends Base {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir = index % 2 === 0 ? 'right' : 'left'
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
}
|
||||
// 根据生长方向定位到父节点的左侧或右侧
|
||||
newNode.left =
|
||||
newNode.dir === 'right'
|
||||
newNode.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
? parent._node.left +
|
||||
parent._node.width +
|
||||
this.getMarginX(layerIndex)
|
||||
@@ -72,7 +76,7 @@ class MindMap extends Base {
|
||||
let leftChildrenAreaHeight = 0
|
||||
let rightChildrenAreaHeight = 0
|
||||
cur._node.children.forEach(item => {
|
||||
if (item.dir === 'left') {
|
||||
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
leftLen++
|
||||
leftChildrenAreaHeight += item.height
|
||||
} else {
|
||||
@@ -86,6 +90,20 @@ class MindMap extends Base {
|
||||
cur._node.rightChildrenAreaHeight =
|
||||
rightChildrenAreaHeight +
|
||||
(rightLen + 1) * this.getMarginY(layerIndex + 1)
|
||||
|
||||
// 如果存在概要,则和概要的高度取最大值
|
||||
let generalizationNodeHeight = cur._node.checkHasGeneralization()
|
||||
? cur._node._generalizationNodeHeight +
|
||||
this.getMarginY(layerIndex + 1)
|
||||
: 0
|
||||
cur._node.leftChildrenAreaHeight2 = Math.max(
|
||||
cur._node.leftChildrenAreaHeight,
|
||||
generalizationNodeHeight
|
||||
)
|
||||
cur._node.rightChildrenAreaHeight2 = Math.max(
|
||||
cur._node.rightChildrenAreaHeight,
|
||||
generalizationNodeHeight
|
||||
)
|
||||
},
|
||||
true,
|
||||
0
|
||||
@@ -99,7 +117,7 @@ class MindMap extends Base {
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.getData('expand') &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
@@ -109,7 +127,7 @@ class MindMap extends Base {
|
||||
let leftTotalTop = baseTop - node.leftChildrenAreaHeight / 2
|
||||
let rightTotalTop = baseTop - node.rightChildrenAreaHeight / 2
|
||||
node.children.forEach(cur => {
|
||||
if (cur.dir === 'left') {
|
||||
if (cur.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
cur.top = leftTotalTop
|
||||
leftTotalTop += cur.height + marginY
|
||||
} else {
|
||||
@@ -130,13 +148,13 @@ class MindMap extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
|
||||
let base = this.getMarginY(layerIndex + 1) * 2 + node.height
|
||||
let leftDifference = node.leftChildrenAreaHeight - base
|
||||
let rightDifference = node.rightChildrenAreaHeight - base
|
||||
let leftDifference = node.leftChildrenAreaHeight2 - base
|
||||
let rightDifference = node.rightChildrenAreaHeight2 - base
|
||||
if (leftDifference > 0 || rightDifference > 0) {
|
||||
this.updateBrothers(node, leftDifference / 2, rightDifference / 2)
|
||||
}
|
||||
@@ -153,16 +171,17 @@ class MindMap extends Base {
|
||||
let childrenList = node.parent.children.filter(item => {
|
||||
return item.dir === node.dir
|
||||
})
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
let addHeight = item.dir === 'left' ? leftAddHeight : rightAddHeight
|
||||
let addHeight =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? leftAddHeight
|
||||
: rightAddHeight
|
||||
// 上面的节点往上移
|
||||
if (_index < index) {
|
||||
_offset = -addHeight
|
||||
@@ -208,10 +227,8 @@ class MindMap extends Base {
|
||||
let x1 = 0
|
||||
let _s = 0
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStyleOffset = nodeUseLineStyle
|
||||
? item.width
|
||||
: 0
|
||||
if (item.dir === 'left') {
|
||||
let nodeUseLineStyleOffset = nodeUseLineStyle ? item.width : 0
|
||||
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
_s = -s1
|
||||
x1 = node.layerIndex === 0 ? left : left - expandBtnSize
|
||||
nodeUseLineStyleOffset = -nodeUseLineStyleOffset
|
||||
@@ -220,7 +237,10 @@ class MindMap extends Base {
|
||||
x1 = node.layerIndex === 0 ? left + width : left + width + expandBtnSize
|
||||
}
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.dir === 'left' ? item.left + item.width : item.left
|
||||
let x2 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? item.left + item.width
|
||||
: item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
|
||||
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
|
||||
@@ -246,18 +266,21 @@ class MindMap extends Base {
|
||||
let x1 =
|
||||
node.layerIndex === 0
|
||||
? left + width / 2
|
||||
: item.dir === 'left'
|
||||
: item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? left - expandBtnSize
|
||||
: left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.dir === 'left' ? item.left + item.width : item.left
|
||||
let x2 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? item.left + item.width
|
||||
: item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
|
||||
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = ''
|
||||
if (nodeUseLineStyle) {
|
||||
if (item.dir === 'left') {
|
||||
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
nodeUseLineStylePath = ` L ${item.left},${y2}`
|
||||
} else {
|
||||
nodeUseLineStylePath = ` L ${item.left + item.width},${y2}`
|
||||
@@ -283,11 +306,14 @@ class MindMap extends Base {
|
||||
let x1 =
|
||||
node.layerIndex === 0
|
||||
? left + width / 2
|
||||
: item.dir === 'left'
|
||||
: item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? left - expandBtnSize
|
||||
: left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.dir === 'left' ? item.left + item.width : item.left
|
||||
let x2 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? item.left + item.width
|
||||
: item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
let path = ''
|
||||
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
|
||||
@@ -295,13 +321,13 @@ class MindMap extends Base {
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = ''
|
||||
if (this.mindMap.themeConfig.nodeUseLineStyle) {
|
||||
if (item.dir === 'left') {
|
||||
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
nodeUseLineStylePath = ` L ${item.left},${y2}`
|
||||
} else {
|
||||
nodeUseLineStylePath = ` L ${item.left + item.width},${y2}`
|
||||
}
|
||||
}
|
||||
if (node.isRoot) {
|
||||
if (node.isRoot && !this.mindMap.themeConfig.rootLineKeepSameInCurve) {
|
||||
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
} else {
|
||||
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
@@ -320,7 +346,8 @@ class MindMap extends Base {
|
||||
? height / 2
|
||||
: 0
|
||||
// 位置没有变化则返回
|
||||
let _x = (node.dir === 'left' ? 0 - expandBtnSize : width)
|
||||
let _x =
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT ? 0 - expandBtnSize : width
|
||||
let _y = height / 2 + nodeUseLineStyleOffset
|
||||
if (_x === translateX && _y === translateY) {
|
||||
return
|
||||
@@ -331,32 +358,43 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let isLeft = node.dir === 'left'
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h', isLeft)
|
||||
let x = isLeft
|
||||
? left - generalizationLineMargin
|
||||
: right + generalizationLineMargin
|
||||
let x1 = x
|
||||
let y1 = top
|
||||
let x2 = x
|
||||
let y2 = bottom
|
||||
let cx = x1 + (isLeft ? -20 : 20)
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left =
|
||||
x +
|
||||
(isLeft ? -generalizationNodeMargin : generalizationNodeMargin) -
|
||||
(isLeft ? gNode.width : 0)
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
renderGeneralization(list) {
|
||||
list.forEach(item => {
|
||||
let isLeft = item.node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
|
||||
let x = isLeft
|
||||
? left - generalizationLineMargin
|
||||
: right + generalizationLineMargin
|
||||
let x1 = x
|
||||
let y1 = top
|
||||
let x2 = x
|
||||
let y2 = bottom
|
||||
let cx = x1 + (isLeft ? -20 : 20)
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
item.generalizationLine.plot(path)
|
||||
item.generalizationNode.left =
|
||||
x +
|
||||
(isLeft ? -generalizationNodeMargin : generalizationNodeMargin) -
|
||||
(isLeft ? item.generalizationNode.width : 0)
|
||||
item.generalizationNode.top = top + (bottom - top - item.generalizationNode.height) / 2
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
rect.size(expandBtnSize, height).x(-expandBtnSize).y(0)
|
||||
} else {
|
||||
rect.size(expandBtnSize, height).x(width).y(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
||||
|
||||
// 组织结构图
|
||||
// 和逻辑结构图基本一样,只是方向变成向下生长,所以先计算节点的top,后计算节点的left、最后调整节点的left即可
|
||||
@@ -57,6 +57,15 @@ class OrganizationStructure extends Base {
|
||||
}, 0) +
|
||||
(len + 1) * this.getMarginY(layerIndex + 1)
|
||||
: 0
|
||||
|
||||
// 如果存在概要,则和概要的高度取最大值
|
||||
let generalizationNodeWidth = cur._node.checkHasGeneralization()
|
||||
? cur._node._generalizationNodeWidth + this.getMarginY(layerIndex + 1)
|
||||
: 0
|
||||
cur._node.childrenAreaWidth2 = Math.max(
|
||||
cur._node.childrenAreaWidth,
|
||||
generalizationNodeWidth
|
||||
)
|
||||
},
|
||||
true,
|
||||
0
|
||||
@@ -70,7 +79,7 @@ class OrganizationStructure extends Base {
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.getData('expand') &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
@@ -95,12 +104,12 @@ class OrganizationStructure extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
// 判断子节点所占的宽度之和是否大于该节点自身,大于则需要调整位置
|
||||
let difference =
|
||||
node.childrenAreaWidth -
|
||||
node.childrenAreaWidth2 -
|
||||
this.getMarginY(layerIndex + 1) * 2 -
|
||||
node.width
|
||||
if (difference > 0) {
|
||||
@@ -116,9 +125,7 @@ class OrganizationStructure extends Base {
|
||||
updateBrothers(node, addWidth) {
|
||||
if (node.parent) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
@@ -209,7 +216,7 @@ class OrganizationStructure extends Base {
|
||||
minx = Math.min(x1, minx)
|
||||
maxx = Math.max(x1, maxx)
|
||||
// 父节点的竖线
|
||||
let line1 = this.draw.path()
|
||||
let line1 = this.lineDraw.path()
|
||||
node.style.line(line1)
|
||||
expandBtnSize = len > 0 && !isRoot ? expandBtnSize : 0
|
||||
line1.plot(`M ${x1},${y1 + expandBtnSize} L ${x1},${y1 + s1}`)
|
||||
@@ -217,7 +224,7 @@ class OrganizationStructure extends Base {
|
||||
style && style(line1, node)
|
||||
// 水平线
|
||||
if (len > 0) {
|
||||
let lin2 = this.draw.path()
|
||||
let lin2 = this.lineDraw.path()
|
||||
node.style.line(lin2)
|
||||
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
|
||||
node._lines.push(lin2)
|
||||
@@ -236,24 +243,32 @@ class OrganizationStructure extends Base {
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'v')
|
||||
let x1 = left
|
||||
let y1 = bottom + generalizationLineMargin
|
||||
let x2 = right
|
||||
let y2 = bottom + generalizationLineMargin
|
||||
let cx = x1 + (x2 - x1) / 2
|
||||
let cy = y1 + 20
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.top = bottom + generalizationNodeMargin
|
||||
gNode.left = left + (right - left - gNode.width) / 2
|
||||
renderGeneralization(list) {
|
||||
list.forEach(item => {
|
||||
let {
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeGeneralizationRenderBoundaries(item, 'v')
|
||||
let x1 = left
|
||||
let y1 = bottom + generalizationLineMargin
|
||||
let x2 = right
|
||||
let y2 = bottom + generalizationLineMargin
|
||||
let cx = x1 + (x2 - x1) / 2
|
||||
let cy = y1 + 20
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
item.generalizationLine.plot(path)
|
||||
item.generalizationNode.top = bottom + generalizationNodeMargin
|
||||
item.generalizationNode.left = left + (right - left - item.generalizationNode.width) / 2
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
// 时间轴
|
||||
@@ -50,8 +50,8 @@ class Timeline extends Base {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
: CONSTANTS.TIMELINE_DIR.TOP
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
}
|
||||
} else {
|
||||
newNode.dir = ''
|
||||
@@ -80,11 +80,7 @@ class Timeline extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
if (node.getData('expand') && node.children && node.children.length) {
|
||||
let marginX = this.getMarginX(layerIndex + 1)
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
if (isRoot) {
|
||||
@@ -122,7 +118,7 @@ class Timeline extends Base {
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
// 调整left
|
||||
@@ -151,7 +147,7 @@ class Timeline extends Base {
|
||||
if (
|
||||
parent &&
|
||||
parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
) {
|
||||
// 遍历二级节点的子节点
|
||||
node.children.forEach(item => {
|
||||
@@ -208,9 +204,7 @@ class Timeline extends Base {
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
@@ -275,12 +269,12 @@ class Timeline extends Base {
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.draw.path()
|
||||
let line = this.lineDraw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
) {
|
||||
line.plot(`M ${x},${top} L ${x},${miny}`)
|
||||
} else {
|
||||
@@ -301,7 +295,7 @@ class Timeline extends Base {
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
@@ -317,24 +311,48 @@ class Timeline extends Base {
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
renderGeneralization(list) {
|
||||
list.forEach(item => {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
item.generalizationLine.plot(path)
|
||||
item.generalizationNode.left = right + generalizationNodeMargin
|
||||
item.generalizationNode.top = top + (bottom - top - item.generalizationNode.height) / 2
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
if (this.layout === CONSTANTS.LAYOUT.TIMELINE) {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
} else {
|
||||
let dir = ''
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
|
||||
dir =
|
||||
node.layerIndex === 1
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
} else {
|
||||
dir = CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
}
|
||||
if (dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
|
||||
rect.size(width, expandBtnSize).x(0).y(-expandBtnSize)
|
||||
} else {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
426
simple-mind-map/src/layouts/VerticalTimeline.js
Normal file
426
simple-mind-map/src/layouts/VerticalTimeline.js
Normal file
@@ -0,0 +1,426 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
// 竖向时间轴
|
||||
class VerticalTimeline extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}, layout) {
|
||||
super(opt)
|
||||
this.layout = layout
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(cur, parent, isRoot, layerIndex, index) => {
|
||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 节点生长方向
|
||||
// 三级及以下节点以上级为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
}
|
||||
// 定位二级节点的left
|
||||
if (parent._node.isRoot) {
|
||||
newNode.left =
|
||||
parent._node.left +
|
||||
(cur._node.width > parent._node.width
|
||||
? -(cur._node.width - parent._node.width) / 2
|
||||
: (parent._node.width - cur._node.width) / 2)
|
||||
} else {
|
||||
newNode.left =
|
||||
newNode.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
? parent._node.left +
|
||||
parent._node.width +
|
||||
this.getMarginX(layerIndex)
|
||||
: parent._node.left -
|
||||
this.getMarginX(layerIndex) -
|
||||
newNode.width
|
||||
}
|
||||
}
|
||||
if (!cur.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
(cur, parent, isRoot, layerIndex) => {
|
||||
// 返回时计算节点的areaHeight,也就是子节点所占的高度之和,包括外边距
|
||||
if (isRoot) {
|
||||
return
|
||||
}
|
||||
let len = cur.data.expand === false ? 0 : cur._node.children.length
|
||||
cur._node.childrenAreaHeight = len
|
||||
? cur._node.children.reduce((h, item) => {
|
||||
return h + item.height
|
||||
}, 0) +
|
||||
(len + 1) * this.getMarginY(layerIndex + 1)
|
||||
: 0
|
||||
},
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的top
|
||||
computedTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
if (node.getData('expand') && node.children && node.children.length) {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
// 定位二级节点的top
|
||||
if (isRoot) {
|
||||
let top = node.top + node.height
|
||||
let totalTop = top + marginY
|
||||
node.children.forEach(cur => {
|
||||
cur.top = totalTop
|
||||
totalTop += cur.height + marginY
|
||||
})
|
||||
} else {
|
||||
// 定位三级及以下节点的top
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
let baseTop = node.top + node.height / 2 + marginY
|
||||
// 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半
|
||||
let totalTop = baseTop - node.childrenAreaHeight / 2
|
||||
node.children.forEach(cur => {
|
||||
cur.top = totalTop
|
||||
totalTop += cur.height + marginY
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
if (isRoot) return
|
||||
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
|
||||
let base = this.getMarginY(layerIndex + 1) * 2 + node.height
|
||||
let difference = node.childrenAreaHeight - base
|
||||
if (difference > 0) {
|
||||
this.updateBrothers(node, difference / 2)
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 更新兄弟节点的top
|
||||
updateBrothers(node, addHeight) {
|
||||
if (node.parent) {
|
||||
let childrenList = node.parent.children
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
// 自定义节点位置
|
||||
if (item.hasCustomPosition()) return
|
||||
// 三级或三级以下节点自身位置不需要动
|
||||
if (!node.parent.isRoot && item.uid === node.uid) return
|
||||
let _offset = 0
|
||||
// 二级节点上面的兄弟节点不需要移动,自身需要往下移动
|
||||
if (node.parent.isRoot) {
|
||||
// 上面的节点不用移
|
||||
if (_index < index) {
|
||||
_offset = 0
|
||||
} else if (_index > index) {
|
||||
// 下面的节点往下移
|
||||
_offset = addHeight * 2
|
||||
} else {
|
||||
// 自身也要移动
|
||||
_offset = addHeight
|
||||
}
|
||||
} else {
|
||||
// 三级或三级以下节点两侧的兄弟节点向两侧移动
|
||||
// 上面的节点往上移
|
||||
if (_index < index) {
|
||||
_offset = -addHeight
|
||||
} else if (_index > index) {
|
||||
// 下面的节点往下移
|
||||
_offset = addHeight
|
||||
}
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothers(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style, lineStyle) {
|
||||
if (lineStyle === 'curve') {
|
||||
this.renderLineCurve(node, lines, style)
|
||||
} else if (lineStyle === 'direct') {
|
||||
this.renderLineDirect(node, lines, style)
|
||||
} else {
|
||||
this.renderLineStraight(node, lines, style)
|
||||
}
|
||||
}
|
||||
|
||||
// 直线连接
|
||||
renderLineStraight(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let y1 = prevBother.top + prevBother.height
|
||||
let y2 = item.top
|
||||
let x = node.left + node.width / 2
|
||||
let path = `M ${x},${y1} L ${x},${y2}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT) {
|
||||
let nodeRight = node.left + node.width
|
||||
let nodeYCenter = node.top + node.height / 2
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
let offset = (marginX - expandBtnSize) * 0.6
|
||||
node.children.forEach((item, index) => {
|
||||
let itemLeft = item.left
|
||||
let itemYCenter = item.top + item.height / 2
|
||||
let path = `
|
||||
M ${nodeRight},${nodeYCenter}
|
||||
L ${nodeRight + offset},${nodeYCenter}
|
||||
L ${nodeRight + offset},${itemYCenter}
|
||||
L ${itemLeft},${itemYCenter}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
} else {
|
||||
let nodeLeft = node.left
|
||||
let nodeYCenter = node.top + node.height / 2
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
let offset = (marginX - expandBtnSize) * 0.6
|
||||
node.children.forEach((item, index) => {
|
||||
let itemRight = item.left + item.width
|
||||
let itemYCenter = item.top + item.height / 2
|
||||
let path = `
|
||||
M ${nodeLeft},${nodeYCenter}
|
||||
L ${nodeLeft - offset},${nodeYCenter}
|
||||
L ${nodeLeft - offset},${itemYCenter}
|
||||
L ${itemRight},${itemYCenter}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 直连
|
||||
renderLineDirect(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
node.children.forEach((item, index) => {
|
||||
if (node.isRoot) {
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let y1 = prevBother.top + prevBother.height
|
||||
let y2 = item.top
|
||||
let x = node.left + node.width / 2
|
||||
let path = `M ${x},${y1} L ${x},${y2}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
let x1 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? left - expandBtnSize
|
||||
: left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? item.left + item.width
|
||||
: item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
let path = `M ${x1},${y1} L ${x2},${y2}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 曲线风格连线
|
||||
renderLineCurve(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
node.children.forEach((item, index) => {
|
||||
if (node.isRoot) {
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let y1 = prevBother.top + prevBother.height
|
||||
let y2 = item.top
|
||||
let x = node.left + node.width / 2
|
||||
let path = `M ${x},${y1} L ${x},${y2}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
let x1 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? left - expandBtnSize
|
||||
: left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? item.left + item.width
|
||||
: item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
let path = this.cubicBezierPath(x1, y1, x2, y2)
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT) {
|
||||
btn.translate(width - translateX, height / 2 - translateY)
|
||||
} else {
|
||||
btn.translate(-expandBtnSize - translateX, height / 2 - translateY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(list) {
|
||||
list.forEach(item => {
|
||||
let isLeft = item.node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
|
||||
let x = isLeft
|
||||
? left - generalizationLineMargin
|
||||
: right + generalizationLineMargin
|
||||
let x1 = x
|
||||
let y1 = top
|
||||
let x2 = x
|
||||
let y2 = bottom
|
||||
let cx = x1 + (isLeft ? -20 : 20)
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
item.generalizationLine.plot(path)
|
||||
item.generalizationNode.left =
|
||||
x +
|
||||
(isLeft ? -generalizationNodeMargin : generalizationNodeMargin) -
|
||||
(isLeft ? item.generalizationNode.width : 0)
|
||||
item.generalizationNode.top =
|
||||
top + (bottom - top - item.generalizationNode.height) / 2
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
rect.size(expandBtnSize, height).x(-expandBtnSize).y(0)
|
||||
} else {
|
||||
rect.size(expandBtnSize, height).x(width).y(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default VerticalTimeline
|
||||
@@ -47,30 +47,35 @@ export default {
|
||||
computedLeftTopValue({ layerIndex, node, ctx }) {
|
||||
if (layerIndex >= 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let marginY = ctx.getMarginY(layerIndex + 1)
|
||||
let startLeft = node.left + node.width * ctx.childIndent
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
|
||||
marginY
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top += totalTop
|
||||
totalTop +=
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
|
||||
marginY
|
||||
})
|
||||
}
|
||||
},
|
||||
adjustLeftTopValueBefore({ node, parent, ctx }) {
|
||||
adjustLeftTopValueBefore({ node, parent, ctx, layerIndex }) {
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
let marginY = ctx.getMarginY(layerIndex + 1)
|
||||
// 调整三级及以下节点的top
|
||||
if (parent && !parent.isRoot && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
|
||||
marginY
|
||||
)
|
||||
}, 0)
|
||||
ctx.updateBrothersTop(node, totalHeight)
|
||||
@@ -80,7 +85,8 @@ export default {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = node.expandBtnSize
|
||||
let marginY = ctx.getMarginY(node.layerIndex + 1)
|
||||
let totalHeight = node.expandBtnSize + marginY
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
|
||||
@@ -89,7 +95,11 @@ export default {
|
||||
item.top =
|
||||
node.top - (item.top - node.top) - nodeTotalHeight + node.height
|
||||
// 调整left
|
||||
item.left = node.left + node.width * ctx.indent + (nodeTotalHeight + totalHeight) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
|
||||
item.left =
|
||||
node.left +
|
||||
node.width * ctx.indent +
|
||||
(nodeTotalHeight + totalHeight) /
|
||||
Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
|
||||
totalHeight += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
ctx.updateChildrenPro(item.children, {
|
||||
@@ -126,7 +136,9 @@ export default {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top + height} L ${x + lineLength},${
|
||||
top + height + Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
|
||||
top +
|
||||
height +
|
||||
Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
@@ -134,13 +146,15 @@ export default {
|
||||
}
|
||||
},
|
||||
computedLeftTopValue({ layerIndex, node, ctx }) {
|
||||
let marginY = ctx.getMarginY(layerIndex + 1)
|
||||
if (layerIndex === 1 && node.children) {
|
||||
// 遍历二级节点的子节点
|
||||
let startLeft = node.left + node.width * ctx.childIndent
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
|
||||
marginY
|
||||
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
@@ -149,7 +163,8 @@ export default {
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
totalTop +=
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
|
||||
marginY
|
||||
})
|
||||
}
|
||||
if (layerIndex > 1 && node.children) {
|
||||
@@ -157,25 +172,29 @@ export default {
|
||||
let startLeft = node.left + node.width * ctx.childIndent
|
||||
let totalTop =
|
||||
node.top -
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) -
|
||||
marginY
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top = totalTop - item.height
|
||||
totalTop -=
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
|
||||
marginY
|
||||
})
|
||||
}
|
||||
},
|
||||
adjustLeftTopValueBefore({ node, ctx, layerIndex }) {
|
||||
// 调整top
|
||||
let marginY = ctx.getMarginY(layerIndex + 1)
|
||||
let len = node.children.length
|
||||
if (layerIndex > 2 && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
|
||||
marginY
|
||||
)
|
||||
}, 0)
|
||||
ctx.updateBrothersTop(node, -totalHeight)
|
||||
@@ -185,23 +204,28 @@ export default {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let marginY = ctx.getMarginY(node.layerIndex + 1)
|
||||
let totalHeight = 0
|
||||
let totalHeight2 = node.expandBtnSize
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let hasChildren = ctx.getNodeActChildrenLength(item) > 0
|
||||
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
|
||||
let offset =
|
||||
hasChildren > 0
|
||||
? nodeTotalHeight -
|
||||
item.height -
|
||||
(hasChildren ? item.expandBtnSize : 0)
|
||||
: 0
|
||||
let offset = hasChildren
|
||||
? nodeTotalHeight -
|
||||
item.height -
|
||||
(hasChildren ? item.expandBtnSize : 0)
|
||||
: 0
|
||||
offset -= hasChildren ? marginY : 0
|
||||
let _top = totalHeight + offset
|
||||
let _left = item.left
|
||||
item.top += _top
|
||||
// 调整left
|
||||
item.left = node.left + node.width * ctx.indent + (nodeTotalHeight + totalHeight2) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
|
||||
item.left =
|
||||
node.left +
|
||||
node.width * ctx.indent +
|
||||
(nodeTotalHeight + totalHeight2) /
|
||||
Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
|
||||
totalHeight += offset
|
||||
totalHeight2 += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
|
||||
@@ -28,7 +28,7 @@ const handleList = node => {
|
||||
}
|
||||
|
||||
// 将markdown转换成节点树
|
||||
export const transformMarkdownTo = async md => {
|
||||
export const transformMarkdownTo = md => {
|
||||
const tree = fromMarkdown(md)
|
||||
let root = {
|
||||
children: []
|
||||
|
||||
@@ -50,4 +50,4 @@ export const transformToMarkdown = root => {
|
||||
true
|
||||
)
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import JSZip from 'jszip'
|
||||
import xmlConvert from 'xml-js'
|
||||
import { getTextFromHtml, isUndef } from '../utils/index'
|
||||
import {
|
||||
getSummaryText,
|
||||
getSummaryText2,
|
||||
getRoot,
|
||||
getItemByName,
|
||||
getElementsByType,
|
||||
addSummaryData,
|
||||
handleNodeImageFromXmind,
|
||||
handleNodeImageToXmind,
|
||||
getXmindContentXmlData,
|
||||
parseNodeGeneralizationToXmind
|
||||
} from '../utils/xmind'
|
||||
|
||||
// 解析.xmind文件
|
||||
const parseXmindFile = file => {
|
||||
@@ -8,11 +21,13 @@ const parseXmindFile = file => {
|
||||
async zip => {
|
||||
try {
|
||||
let content = ''
|
||||
if (zip.files['content.json']) {
|
||||
let json = await zip.files['content.json'].async('string')
|
||||
content = transformXmind(json)
|
||||
} else if (zip.files['content.xml']) {
|
||||
let xml = await zip.files['content.xml'].async('string')
|
||||
let jsonFile = zip.files['content.json']
|
||||
let xmlFile = zip.files['content.xml'] || zip.files['/content.xml']
|
||||
if (jsonFile) {
|
||||
let json = await jsonFile.async('string')
|
||||
content = await transformXmind(json, zip.files)
|
||||
} else if (xmlFile) {
|
||||
let xml = await xmlFile.async('string')
|
||||
let json = xmlConvert.xml2json(xml)
|
||||
content = transformOldXmind(json)
|
||||
}
|
||||
@@ -33,18 +48,20 @@ const parseXmindFile = file => {
|
||||
}
|
||||
|
||||
// 转换xmind数据
|
||||
const transformXmind = content => {
|
||||
let data = JSON.parse(content)[0]
|
||||
let nodeTree = data.rootTopic
|
||||
let newTree = {}
|
||||
let walk = (node, newNode) => {
|
||||
const transformXmind = async (content, files) => {
|
||||
const data = JSON.parse(content)[0]
|
||||
const nodeTree = data.rootTopic
|
||||
const newTree = {}
|
||||
const waitLoadImageList = []
|
||||
const walk = async (node, newNode) => {
|
||||
newNode.data = {
|
||||
// 节点内容
|
||||
text: node.title
|
||||
text: isUndef(node.title) ? '' : node.title
|
||||
}
|
||||
// 节点备注
|
||||
if (node.notes) {
|
||||
newNode.data.note = (node.notes.realHTML || node.notes.plain).content
|
||||
const notesData = node.notes.realHTML || node.notes.plain
|
||||
newNode.data.note = notesData ? notesData.content || '' : ''
|
||||
}
|
||||
// 超链接
|
||||
if (node.href && /^https?:\/\//.test(node.href)) {
|
||||
@@ -54,6 +71,27 @@ const transformXmind = content => {
|
||||
if (node.labels && node.labels.length > 0) {
|
||||
newNode.data.tag = node.labels
|
||||
}
|
||||
// 图片
|
||||
handleNodeImageFromXmind(node, newNode, waitLoadImageList, files)
|
||||
// 概要
|
||||
const selfSummary = []
|
||||
const childrenSummary = []
|
||||
if (newNode._summary) {
|
||||
selfSummary.push(newNode._summary)
|
||||
}
|
||||
if (Array.isArray(node.summaries) && node.summaries.length > 0) {
|
||||
node.summaries.forEach(item => {
|
||||
addSummaryData(
|
||||
selfSummary,
|
||||
childrenSummary,
|
||||
() => {
|
||||
return getSummaryText(node, item.topicId)
|
||||
},
|
||||
item.range
|
||||
)
|
||||
})
|
||||
}
|
||||
newNode.data.generalization = selfSummary
|
||||
// 子节点
|
||||
newNode.children = []
|
||||
if (
|
||||
@@ -61,49 +99,38 @@ const transformXmind = content => {
|
||||
node.children.attached &&
|
||||
node.children.attached.length > 0
|
||||
) {
|
||||
node.children.attached.forEach(item => {
|
||||
let newChild = {}
|
||||
node.children.attached.forEach((item, index) => {
|
||||
const newChild = {}
|
||||
newNode.children.push(newChild)
|
||||
if (childrenSummary[index]) {
|
||||
newChild._summary = childrenSummary[index]
|
||||
}
|
||||
walk(item, newChild)
|
||||
})
|
||||
}
|
||||
}
|
||||
walk(nodeTree, newTree)
|
||||
await Promise.all(waitLoadImageList)
|
||||
return newTree
|
||||
}
|
||||
|
||||
// 转换旧版xmind数据,xmind8
|
||||
const transformOldXmind = content => {
|
||||
let data = JSON.parse(content)
|
||||
let elements = data.elements
|
||||
let root = null
|
||||
let getRoot = arr => {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (!root && arr[i].name === 'topic') {
|
||||
root = arr[i]
|
||||
return
|
||||
}
|
||||
}
|
||||
arr.forEach(item => {
|
||||
getRoot(item.elements)
|
||||
})
|
||||
}
|
||||
getRoot(elements)
|
||||
let newTree = {}
|
||||
let getItemByName = (arr, name) => {
|
||||
return arr.find(item => {
|
||||
return item.name === name
|
||||
})
|
||||
}
|
||||
let walk = (node, newNode) => {
|
||||
let nodeElements = node.elements
|
||||
const data = JSON.parse(content)
|
||||
const elements = data.elements
|
||||
const root = getRoot(elements)
|
||||
const newTree = {}
|
||||
const walk = (node, newNode) => {
|
||||
const nodeElements = node.elements
|
||||
let nodeTitle = getItemByName(nodeElements, 'title')
|
||||
nodeTitle = nodeTitle && nodeTitle.elements && nodeTitle.elements[0].text
|
||||
// 节点内容
|
||||
newNode.data = {
|
||||
// 节点内容
|
||||
text: getItemByName(nodeElements, 'title').elements[0].text
|
||||
text: isUndef(nodeTitle) ? '' : nodeTitle
|
||||
}
|
||||
// 节点备注
|
||||
try {
|
||||
// 节点备注
|
||||
let notesElement = getItemByName(nodeElements, 'notes')
|
||||
const notesElement = getItemByName(nodeElements, 'notes')
|
||||
if (notesElement) {
|
||||
newNode.data.note =
|
||||
notesElement.elements[0].elements[0].elements[0].text
|
||||
@@ -111,8 +138,8 @@ const transformOldXmind = content => {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
// 超链接
|
||||
try {
|
||||
// 超链接
|
||||
if (
|
||||
node.attributes &&
|
||||
node.attributes['xlink:href'] &&
|
||||
@@ -123,9 +150,9 @@ const transformOldXmind = content => {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
// 标签
|
||||
try {
|
||||
// 标签
|
||||
let labelsElement = getItemByName(nodeElements, 'labels')
|
||||
const labelsElement = getItemByName(nodeElements, 'labels')
|
||||
if (labelsElement) {
|
||||
newNode.data.tag = labelsElement.elements.map(item => {
|
||||
return item.elements[0].text
|
||||
@@ -134,22 +161,50 @@ const transformOldXmind = content => {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
const childrenItem = getItemByName(nodeElements, 'children')
|
||||
// 概要
|
||||
const selfSummary = []
|
||||
const childrenSummary = []
|
||||
try {
|
||||
if (newNode._summary) {
|
||||
selfSummary.push(newNode._summary)
|
||||
}
|
||||
const summariesItem = getItemByName(nodeElements, 'summaries')
|
||||
if (
|
||||
summariesItem &&
|
||||
Array.isArray(summariesItem.elements) &&
|
||||
summariesItem.elements.length > 0
|
||||
) {
|
||||
summariesItem.elements.forEach(item => {
|
||||
addSummaryData(
|
||||
selfSummary,
|
||||
childrenSummary,
|
||||
() => {
|
||||
return getSummaryText2(childrenItem, item.attributes['topic-id'])
|
||||
},
|
||||
item.attributes.range
|
||||
)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
newNode.data.generalization = selfSummary
|
||||
// 子节点
|
||||
newNode.children = []
|
||||
let _children = getItemByName(nodeElements, 'children')
|
||||
if (_children && _children.elements && _children.elements.length > 0) {
|
||||
_children.elements.forEach(item => {
|
||||
if (item.name === 'topics') {
|
||||
item.elements.forEach(item2 => {
|
||||
let newChild = {}
|
||||
newNode.children.push(newChild)
|
||||
walk(item2, newChild)
|
||||
})
|
||||
} else {
|
||||
let newChild = {}
|
||||
newNode.children.push(newChild)
|
||||
walk(item, newChild)
|
||||
if (
|
||||
childrenItem &&
|
||||
childrenItem.elements &&
|
||||
childrenItem.elements.length > 0
|
||||
) {
|
||||
const children = getElementsByType(childrenItem.elements, 'attached')
|
||||
children.forEach((item, index) => {
|
||||
const newChild = {}
|
||||
newNode.children.push(newChild)
|
||||
if (childrenSummary[index]) {
|
||||
newChild._summary = childrenSummary[index]
|
||||
}
|
||||
walk(item, newChild)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -157,8 +212,117 @@ const transformOldXmind = content => {
|
||||
return newTree
|
||||
}
|
||||
|
||||
// 数据转换为xmind文件
|
||||
// 直接转换为最新版本的xmind文件 2023.09.11172
|
||||
const transformToXmind = async (data, name) => {
|
||||
const id = 'simpleMindMap_' + Date.now()
|
||||
const imageList = []
|
||||
// 转换核心数据
|
||||
let newTree = {}
|
||||
let waitLoadImageList = []
|
||||
let walk = async (node, newNode, isRoot) => {
|
||||
let newData = {
|
||||
id: node.data.uid,
|
||||
structureClass: 'org.xmind.ui.logic.right',
|
||||
title: getTextFromHtml(node.data.text), // 节点文本
|
||||
children: {
|
||||
attached: []
|
||||
}
|
||||
}
|
||||
// 备注
|
||||
if (node.data.note !== undefined) {
|
||||
newData.notes = {
|
||||
realHTML: {
|
||||
content: node.data.note
|
||||
},
|
||||
plain: {
|
||||
content: node.data.note
|
||||
}
|
||||
}
|
||||
}
|
||||
// 超链接
|
||||
if (node.data.hyperlink !== undefined) {
|
||||
newData.href = node.data.hyperlink
|
||||
}
|
||||
// 标签
|
||||
if (node.data.tag !== undefined) {
|
||||
newData.labels = node.data.tag || []
|
||||
}
|
||||
// 图片
|
||||
handleNodeImageToXmind(node, newNode, waitLoadImageList, imageList)
|
||||
// 样式
|
||||
// 暂时不考虑样式
|
||||
if (isRoot) {
|
||||
newData.class = 'topic'
|
||||
newNode.id = id
|
||||
newNode.class = 'sheet'
|
||||
newNode.title = name
|
||||
newNode.extensions = []
|
||||
newNode.topicPositioning = 'fixed'
|
||||
newNode.topicOverlapping = 'overlap'
|
||||
newNode.coreVersion = '2.100.0'
|
||||
newNode.rootTopic = newData
|
||||
} else {
|
||||
Object.keys(newData).forEach(key => {
|
||||
newNode[key] = newData[key]
|
||||
})
|
||||
}
|
||||
// 概要
|
||||
const { summary, summaries } = parseNodeGeneralizationToXmind(node)
|
||||
if (isRoot) {
|
||||
if (summaries.length > 0) {
|
||||
newNode.rootTopic.children.summary = summary
|
||||
newNode.rootTopic.summaries = summaries
|
||||
}
|
||||
} else {
|
||||
if (summaries.length > 0) {
|
||||
newNode.children.summary = summary
|
||||
newNode.summaries = summaries
|
||||
}
|
||||
}
|
||||
// 子节点
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(child => {
|
||||
let newChild = {}
|
||||
walk(child, newChild)
|
||||
newData.children.attached.push(newChild)
|
||||
})
|
||||
}
|
||||
}
|
||||
walk(data, newTree, true)
|
||||
await Promise.all(waitLoadImageList)
|
||||
const contentData = [newTree]
|
||||
// 创建压缩包
|
||||
const zip = new JSZip()
|
||||
zip.file('content.json', JSON.stringify(contentData))
|
||||
zip.file(
|
||||
'metadata.json',
|
||||
`{"modifier":"","dataStructureVersion":"2","creator":{"name":"mind-map"},"layoutEngineVersion":"3","activeSheetId":"${id}"}`
|
||||
)
|
||||
zip.file('content.xml', getXmindContentXmlData())
|
||||
const manifestData = {
|
||||
'file-entries': {
|
||||
'content.json': {},
|
||||
'metadata.json': {},
|
||||
'Thumbnails/thumbnail.png': {}
|
||||
}
|
||||
}
|
||||
// 图片
|
||||
if (imageList.length > 0) {
|
||||
imageList.forEach(item => {
|
||||
manifestData['file-entries']['resources/' + item.name] = {}
|
||||
const img = zip.folder('resources')
|
||||
img.file(item.name, item.data, { base64: true })
|
||||
})
|
||||
}
|
||||
zip.file('manifest.json', JSON.stringify(manifestData))
|
||||
const zipData = await zip.generateAsync({ type: 'blob' })
|
||||
return zipData
|
||||
}
|
||||
|
||||
export default {
|
||||
parseXmindFile,
|
||||
transformXmind,
|
||||
transformOldXmind
|
||||
transformOldXmind,
|
||||
transformToXmind
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
import associativeLineControlsMethods from './associativeLine/associativeLineControls'
|
||||
import associativeLineTextMethods from './associativeLine/associativeLineText'
|
||||
|
||||
// 关联线类
|
||||
// 关联线插件
|
||||
class AssociativeLine {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
this.draw = this.mindMap.draw
|
||||
this.associativeLineDraw = this.mindMap.associativeLineDraw
|
||||
// 当前所有连接线
|
||||
this.lineList = []
|
||||
// 当前激活的连接线
|
||||
@@ -98,14 +98,35 @@ class AssociativeLine {
|
||||
|
||||
// 创建箭头
|
||||
createMarker() {
|
||||
return this.draw.marker(20, 20, add => {
|
||||
add.ref(2, 5)
|
||||
return this.associativeLineDraw.marker(20, 20, add => {
|
||||
add.ref(12, 5)
|
||||
add.size(10, 10)
|
||||
add.attr('orient', 'auto-start-reverse')
|
||||
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
|
||||
})
|
||||
}
|
||||
|
||||
// 判断关联线坐标是否变更,有变更则使用变化后的坐标,无则默认坐标
|
||||
updateAllLinesPos(node, toNode, associativeLinePoint) {
|
||||
associativeLinePoint = associativeLinePoint || {}
|
||||
let [startPoint, endPoint] = computeNodePoints(node, toNode)
|
||||
let nodeRange = 0
|
||||
let nodeDir = ''
|
||||
let toNodeRange = 0
|
||||
let toNodeDir = ''
|
||||
if (associativeLinePoint.startPoint) {
|
||||
nodeRange = associativeLinePoint.startPoint.range || 0
|
||||
nodeDir = associativeLinePoint.startPoint.dir || 'right'
|
||||
startPoint = getNodePoint(node, nodeDir, nodeRange)
|
||||
}
|
||||
if (associativeLinePoint.endPoint) {
|
||||
toNodeRange = associativeLinePoint.endPoint.range || 0
|
||||
toNodeDir = associativeLinePoint.endPoint.dir || 'right'
|
||||
endPoint = getNodePoint(toNode, toNodeDir, toNodeRange)
|
||||
}
|
||||
return [startPoint, endPoint]
|
||||
}
|
||||
|
||||
// 渲染所有连线
|
||||
renderAllLines() {
|
||||
// 先移除
|
||||
@@ -121,15 +142,15 @@ class AssociativeLine {
|
||||
null,
|
||||
cur => {
|
||||
if (!cur) return
|
||||
let data = cur.nodeData.data
|
||||
let data = cur.getData()
|
||||
if (
|
||||
data.associativeLineTargets &&
|
||||
data.associativeLineTargets.length > 0
|
||||
) {
|
||||
nodeToIds.set(cur, data.associativeLineTargets)
|
||||
}
|
||||
if (data.id) {
|
||||
idToNode.set(data.id, cur)
|
||||
if (data.uid) {
|
||||
idToNode.set(data.uid, cur)
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
@@ -137,10 +158,17 @@ class AssociativeLine {
|
||||
0
|
||||
)
|
||||
nodeToIds.forEach((ids, node) => {
|
||||
ids.forEach(id => {
|
||||
let toNode = idToNode.get(id)
|
||||
ids.forEach((uid, index) => {
|
||||
let toNode = idToNode.get(uid)
|
||||
if (!node || !toNode) return
|
||||
let [startPoint, endPoint] = computeNodePoints(node, toNode)
|
||||
const associativeLinePoint = (node.getData('associativeLinePoint') ||
|
||||
[])[index]
|
||||
// 切换结构和布局,都会更新坐标
|
||||
const [startPoint, endPoint] = this.updateAllLinesPos(
|
||||
node,
|
||||
toNode,
|
||||
associativeLinePoint
|
||||
)
|
||||
this.drawLine(startPoint, endPoint, node, toNode)
|
||||
})
|
||||
})
|
||||
@@ -166,7 +194,7 @@ class AssociativeLine {
|
||||
toNode
|
||||
)
|
||||
// 虚线
|
||||
let path = this.draw.path()
|
||||
let path = this.associativeLineDraw.path()
|
||||
path
|
||||
.stroke({
|
||||
width: associativeLineWidth,
|
||||
@@ -177,17 +205,39 @@ class AssociativeLine {
|
||||
path.plot(pathStr)
|
||||
path.marker('end', this.marker)
|
||||
// 不可见的点击线
|
||||
let clickPath = this.draw.path()
|
||||
let clickPath = this.associativeLineDraw.path()
|
||||
clickPath
|
||||
.stroke({ width: associativeLineActiveWidth, color: 'transparent' })
|
||||
.fill({ color: 'none' })
|
||||
clickPath.plot(pathStr)
|
||||
// 文字
|
||||
let text = this.createText({ path, clickPath, node, toNode, startPoint, endPoint, controlPoints })
|
||||
let text = this.createText({
|
||||
path,
|
||||
clickPath,
|
||||
node,
|
||||
toNode,
|
||||
startPoint,
|
||||
endPoint,
|
||||
controlPoints
|
||||
})
|
||||
// 点击事件
|
||||
clickPath.click(e => {
|
||||
e.stopPropagation()
|
||||
this.setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints })
|
||||
this.setActiveLine({
|
||||
path,
|
||||
clickPath,
|
||||
text,
|
||||
node,
|
||||
toNode,
|
||||
startPoint,
|
||||
endPoint,
|
||||
controlPoints
|
||||
})
|
||||
})
|
||||
// 双击进入关联线文本编辑状态
|
||||
clickPath.dblclick(() => {
|
||||
if (!this.activeLine) return
|
||||
this.showEditTextBox(text)
|
||||
})
|
||||
// 渲染关联线文字
|
||||
this.renderText(this.getText(node, toNode), path, text)
|
||||
@@ -195,33 +245,38 @@ class AssociativeLine {
|
||||
}
|
||||
|
||||
// 激活某根关联线
|
||||
setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints }) {
|
||||
let {
|
||||
associativeLineActiveColor
|
||||
} = this.mindMap.themeConfig
|
||||
setActiveLine({
|
||||
path,
|
||||
clickPath,
|
||||
text,
|
||||
node,
|
||||
toNode,
|
||||
startPoint,
|
||||
endPoint,
|
||||
controlPoints
|
||||
}) {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
// 如果当前存在激活节点,那么取消激活节点
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.clearActiveNodes()
|
||||
} else {
|
||||
// 否则清除当前的关联线的激活状态,如果有的话
|
||||
this.clearActiveLine()
|
||||
// 保存当前激活的关联线信息
|
||||
this.activeLine = [path, clickPath, text, node, toNode]
|
||||
// 让不可见的点击线显示
|
||||
clickPath.stroke({ color: associativeLineActiveColor })
|
||||
// 如果没有输入过关联线文字,那么显示默认文字
|
||||
if (!this.getText(node, toNode)) {
|
||||
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text)
|
||||
}
|
||||
// 渲染控制点和连线
|
||||
this.renderControls(
|
||||
startPoint,
|
||||
endPoint,
|
||||
controlPoints[0],
|
||||
controlPoints[1]
|
||||
)
|
||||
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
// 否则清除当前的关联线的激活状态,如果有的话
|
||||
this.clearActiveLine()
|
||||
// 保存当前激活的关联线信息
|
||||
this.activeLine = [path, clickPath, text, node, toNode]
|
||||
// 让不可见的点击线显示
|
||||
clickPath.stroke({ color: associativeLineActiveColor })
|
||||
// 如果没有输入过关联线文字,那么显示默认文字
|
||||
if (!this.getText(node, toNode)) {
|
||||
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text)
|
||||
}
|
||||
// 渲染控制点和连线
|
||||
this.renderControls(
|
||||
startPoint,
|
||||
endPoint,
|
||||
controlPoints[0],
|
||||
controlPoints[1]
|
||||
)
|
||||
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
|
||||
this.front()
|
||||
}
|
||||
|
||||
// 移除所有连接线
|
||||
@@ -246,9 +301,10 @@ class AssociativeLine {
|
||||
let { associativeLineWidth, associativeLineColor } =
|
||||
this.mindMap.themeConfig
|
||||
if (this.isCreatingLine || !fromNode) return
|
||||
this.front()
|
||||
this.isCreatingLine = true
|
||||
this.creatingStartNode = fromNode
|
||||
this.creatingLine = this.draw.path()
|
||||
this.creatingLine = this.associativeLineDraw.path()
|
||||
this.creatingLine
|
||||
.stroke({
|
||||
width: associativeLineWidth,
|
||||
@@ -287,14 +343,31 @@ class AssociativeLine {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算节点偏移位置
|
||||
getNodePos(node) {
|
||||
const { scaleX, scaleY, translateX, translateY } =
|
||||
this.mindMap.draw.transform()
|
||||
const { left, top, width, height } = node
|
||||
let translateLeft = left * scaleX + translateX
|
||||
let translateTop = top * scaleY + translateY
|
||||
return {
|
||||
left,
|
||||
top,
|
||||
translateLeft,
|
||||
translateTop,
|
||||
width,
|
||||
height
|
||||
}
|
||||
}
|
||||
|
||||
// 检测当前移动到的目标节点
|
||||
checkOverlapNode(x, y) {
|
||||
this.overlapNode = null
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
if (node.nodeData.data.isActive) {
|
||||
this.mindMap.renderer.setNodeActive(node, false)
|
||||
if (node.getData('isActive')) {
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', node, false)
|
||||
}
|
||||
if (node === this.creatingStartNode || this.overlapNode) {
|
||||
if (node.uid === this.creatingStartNode.uid || this.overlapNode) {
|
||||
return
|
||||
}
|
||||
let { left, top, width, height } = node
|
||||
@@ -304,39 +377,45 @@ class AssociativeLine {
|
||||
this.overlapNode = node
|
||||
}
|
||||
})
|
||||
if (this.overlapNode && !this.overlapNode.nodeData.data.isActive) {
|
||||
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
|
||||
if (this.overlapNode && !this.overlapNode.getData('isActive')) {
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
|
||||
}
|
||||
}
|
||||
|
||||
// 完成创建连接线
|
||||
completeCreateLine(node) {
|
||||
if (this.creatingStartNode === node) return
|
||||
if (this.creatingStartNode.uid === node.uid) return
|
||||
this.addLine(this.creatingStartNode, node)
|
||||
if (this.overlapNode && this.overlapNode.nodeData.data.isActive) {
|
||||
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
|
||||
if (this.overlapNode && this.overlapNode.getData('isActive')) {
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, false)
|
||||
}
|
||||
this.isCreatingLine = false
|
||||
this.creatingStartNode = null
|
||||
this.creatingLine.remove()
|
||||
this.creatingLine = null
|
||||
this.overlapNode = null
|
||||
this.back()
|
||||
}
|
||||
|
||||
// 添加连接线
|
||||
addLine(fromNode, toNode) {
|
||||
if (!fromNode || !toNode) return
|
||||
// 目标节点如果没有id,则生成一个id
|
||||
let id = toNode.nodeData.data.id
|
||||
if (!id) {
|
||||
id = uuid()
|
||||
let uid = toNode.getData('uid')
|
||||
if (!uid) {
|
||||
uid = uuid()
|
||||
this.mindMap.execCommand('SET_NODE_DATA', toNode, {
|
||||
id
|
||||
uid
|
||||
})
|
||||
}
|
||||
// 将目标节点id保存起来
|
||||
let list = fromNode.nodeData.data.associativeLineTargets || []
|
||||
list.push(id)
|
||||
let list = fromNode.getData('associativeLineTargets') || []
|
||||
// 连线节点是否存在相同的id,存在则阻止添加关联线
|
||||
const sameLine = list.some(item => item === uid)
|
||||
if (sameLine) {
|
||||
return
|
||||
}
|
||||
list.push(uid)
|
||||
// 保存控制点
|
||||
let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
|
||||
let controlPoints = computeCubicBezierPathPoints(
|
||||
@@ -346,7 +425,7 @@ class AssociativeLine {
|
||||
endPoint.y
|
||||
)
|
||||
let offsetList =
|
||||
fromNode.nodeData.data.associativeLineTargetControlOffsets || []
|
||||
fromNode.getData('associativeLineTargetControlOffsets') || []
|
||||
// 保存的实际是控制点和端点的差值,否则当节点位置改变了,控制点还是原来的位置,连线就不对了
|
||||
offsetList[list.length - 1] = [
|
||||
{
|
||||
@@ -358,9 +437,13 @@ class AssociativeLine {
|
||||
y: controlPoints[1].y - endPoint.y
|
||||
}
|
||||
]
|
||||
let associativeLinePoint = fromNode.getData('associativeLinePoint') || []
|
||||
// 记录关联的起始|结束坐标
|
||||
associativeLinePoint[list.length - 1] = { startPoint, endPoint }
|
||||
this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
|
||||
associativeLineTargets: list,
|
||||
associativeLineTargetControlOffsets: offsetList
|
||||
associativeLineTargetControlOffsets: offsetList,
|
||||
associativeLinePoint
|
||||
})
|
||||
}
|
||||
|
||||
@@ -369,14 +452,19 @@ class AssociativeLine {
|
||||
if (!this.activeLine) return
|
||||
let [, , , node, toNode] = this.activeLine
|
||||
this.removeControls()
|
||||
let { associativeLineTargets, associativeLineTargetControlOffsets, associativeLineText } =
|
||||
node.nodeData.data
|
||||
let {
|
||||
associativeLineTargets,
|
||||
associativeLinePoint,
|
||||
associativeLineTargetControlOffsets,
|
||||
associativeLineText
|
||||
} = node.getData()
|
||||
associativeLinePoint = associativeLinePoint || []
|
||||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||||
// 更新关联线文本数据
|
||||
let newAssociativeLineText = {}
|
||||
if (associativeLineText) {
|
||||
Object.keys(associativeLineText).forEach((item) => {
|
||||
if (item !== toNode.nodeData.data.id) {
|
||||
Object.keys(associativeLineText).forEach(item => {
|
||||
if (item !== toNode.getData('uid')) {
|
||||
newAssociativeLineText[item] = associativeLineText[item]
|
||||
}
|
||||
})
|
||||
@@ -386,6 +474,10 @@ class AssociativeLine {
|
||||
associativeLineTargets: associativeLineTargets.filter((_, index) => {
|
||||
return index !== targetIndex
|
||||
}),
|
||||
// 连接线坐标
|
||||
associativeLinePoint: associativeLinePoint.filter((_, index) => {
|
||||
return index !== targetIndex
|
||||
}),
|
||||
// 偏移量
|
||||
associativeLineTargetControlOffsets: associativeLineTargetControlOffsets
|
||||
? associativeLineTargetControlOffsets.filter((_, index) => {
|
||||
@@ -397,13 +489,6 @@ class AssociativeLine {
|
||||
})
|
||||
}
|
||||
|
||||
// 清除当前激活的节点
|
||||
clearActiveNodes() {
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
}
|
||||
}
|
||||
|
||||
// 清除激活的线
|
||||
clearActiveLine() {
|
||||
if (this.activeLine) {
|
||||
@@ -419,6 +504,7 @@ class AssociativeLine {
|
||||
}
|
||||
this.activeLine = null
|
||||
this.removeControls()
|
||||
this.back()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,6 +531,19 @@ class AssociativeLine {
|
||||
this.showControls()
|
||||
this.isNodeDragging = false
|
||||
}
|
||||
|
||||
// 关联线顶层显示
|
||||
front() {
|
||||
if (this.mindMap.opt.associativeLineIsAlwaysAboveNode) return
|
||||
this.associativeLineDraw.front()
|
||||
}
|
||||
|
||||
// 关联线回到原有层级
|
||||
back() {
|
||||
if (this.mindMap.opt.associativeLineIsAlwaysAboveNode) return
|
||||
this.associativeLineDraw.back() // 最底层
|
||||
this.associativeLineDraw.forward() // 连线层上面
|
||||
}
|
||||
}
|
||||
|
||||
AssociativeLine.instanceName = 'associativeLine'
|
||||
|
||||
345
simple-mind-map/src/plugins/Cooperate.js
Normal file
345
simple-mind-map/src/plugins/Cooperate.js
Normal file
@@ -0,0 +1,345 @@
|
||||
import * as Y from 'yjs'
|
||||
import { WebrtcProvider } from 'y-webrtc'
|
||||
import { isSameObject, simpleDeepClone, getType, isUndef } from '../utils/index'
|
||||
|
||||
// 协同插件
|
||||
class Cooperate {
|
||||
constructor(opt) {
|
||||
this.opt = opt
|
||||
this.mindMap = opt.mindMap
|
||||
// yjs文档
|
||||
this.ydoc = new Y.Doc()
|
||||
// 共享数据
|
||||
this.ymap = null
|
||||
// 连接提供者
|
||||
this.provider = null
|
||||
// 感知数据
|
||||
this.awareness = null
|
||||
this.currentAwarenessData = []
|
||||
this.waitNodeUidMap = {} // 该列表中的uid对应的节点还未渲染完毕
|
||||
// 当前的平级对象类型的思维导图数据
|
||||
this.currentData = null
|
||||
// 用户信息
|
||||
this.userInfo = null
|
||||
// 绑定事件
|
||||
this.bindEvent()
|
||||
// 处理实例化时传入的思维导图数据
|
||||
if (this.mindMap.opt.data) {
|
||||
this.initData(this.mindMap.opt.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
initData(data) {
|
||||
data = simpleDeepClone(data)
|
||||
// 解绑原来的数据
|
||||
if (this.ymap) {
|
||||
this.ymap.unobserve(this.onObserve)
|
||||
}
|
||||
// 创建共享数据
|
||||
this.ymap = this.ydoc.getMap()
|
||||
// 思维导图树结构转平级对象结构
|
||||
this.currentData = this.transformTreeDataToObject(data)
|
||||
// 将思维导图数据添加到共享数据中
|
||||
Object.keys(this.currentData).forEach(uid => {
|
||||
this.ymap.set(uid, this.currentData[uid])
|
||||
})
|
||||
// 监听数据同步
|
||||
this.onObserve = this.onObserve.bind(this)
|
||||
this.ymap.observe(this.onObserve)
|
||||
}
|
||||
|
||||
// 获取yjs doc实例
|
||||
getDoc() {
|
||||
return this.ydoc
|
||||
}
|
||||
|
||||
// 设置连接提供者
|
||||
setProvider(provider, webrtcProviderConfig = {}) {
|
||||
const { roomName, signalingList, ...otherConfig } = webrtcProviderConfig
|
||||
this.provider =
|
||||
provider ||
|
||||
new WebrtcProvider(roomName, this.ydoc, {
|
||||
signaling: signalingList,
|
||||
...otherConfig
|
||||
})
|
||||
this.awareness = this.provider.awareness
|
||||
|
||||
// 监听状态同步事件
|
||||
this.onAwareness = this.onAwareness.bind(this)
|
||||
this.awareness.on('change', this.onAwareness)
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
// 监听思维导图改变
|
||||
this.onDataChange = this.onDataChange.bind(this)
|
||||
this.mindMap.on('data_change', this.onDataChange)
|
||||
|
||||
// 监听思维导图节点激活事件
|
||||
this.onNodeActive = this.onNodeActive.bind(this)
|
||||
this.mindMap.on('node_active', this.onNodeActive)
|
||||
|
||||
// 监听思维导图渲染完毕事件
|
||||
this.onNodeTreeRenderEnd = this.onNodeTreeRenderEnd.bind(this)
|
||||
this.mindMap.on('node_tree_render_end', this.onNodeTreeRenderEnd)
|
||||
|
||||
// 监听设置思维导图数据事件
|
||||
this.initData = this.initData.bind(this)
|
||||
this.mindMap.on('set_data', this.initData)
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
unBindEvent() {
|
||||
if (this.ymap) {
|
||||
this.ymap.unobserve(this.onObserve)
|
||||
}
|
||||
this.mindMap.off('data_change', this.onDataChange)
|
||||
this.mindMap.off('node_active', this.onNodeActive)
|
||||
this.mindMap.off('node_tree_render_end', this.onNodeTreeRenderEnd)
|
||||
this.mindMap.off('set_data', this.initData)
|
||||
this.ydoc.destroy()
|
||||
}
|
||||
|
||||
// 数据同步时的处理,更新当前思维导图
|
||||
onObserve(event) {
|
||||
const data = event.target.toJSON()
|
||||
// 如果数据没有改变直接返回
|
||||
if (isSameObject(data, this.currentData)) return
|
||||
this.currentData = data
|
||||
// 平级对象转树结构
|
||||
const res = this.transformObjectToTreeData(data)
|
||||
if (!res) return
|
||||
// 更新思维导图画布
|
||||
this.mindMap.renderer.setData(res)
|
||||
this.mindMap.render()
|
||||
this.mindMap.command.addHistory()
|
||||
}
|
||||
|
||||
// 当前思维导图改变后的处理,触发同步
|
||||
onDataChange(data) {
|
||||
const res = this.transformTreeDataToObject(data)
|
||||
this.updateChanges(res)
|
||||
}
|
||||
|
||||
// 找出更新点
|
||||
updateChanges(data) {
|
||||
const oldData = this.currentData
|
||||
this.currentData = data
|
||||
this.ydoc.transact(() => {
|
||||
// 找出新增的或修改的
|
||||
Object.keys(data).forEach(uid => {
|
||||
// 新增的或已经存在的,如果数据发生了改变
|
||||
if (!oldData[uid] || !isSameObject(oldData[uid], data[uid])) {
|
||||
this.ymap.set(uid, data[uid])
|
||||
}
|
||||
})
|
||||
// 找出删除的
|
||||
Object.keys(oldData).forEach(uid => {
|
||||
if (!data[uid]) {
|
||||
this.ymap.delete(uid)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 节点激活状态改变后触发感知数据同步
|
||||
onNodeActive(node, nodeList) {
|
||||
if (this.userInfo) {
|
||||
this.awareness.setLocalStateField(this.userInfo.name, {
|
||||
// 用户信息
|
||||
userInfo: {
|
||||
...this.userInfo
|
||||
},
|
||||
// 当前激活的节点id列表
|
||||
nodeIdList: nodeList.map(item => {
|
||||
return item.uid
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 节点树渲染完毕事件
|
||||
onNodeTreeRenderEnd() {
|
||||
Object.keys(this.waitNodeUidMap).forEach(uid => {
|
||||
const node = this.mindMap.renderer.findNodeByUid(uid)
|
||||
if (node) {
|
||||
node.addUser(this.waitNodeUidMap[uid])
|
||||
}
|
||||
})
|
||||
this.waitNodeUidMap = {}
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
/**
|
||||
* {
|
||||
* id: '', // 必传,用户唯一的id
|
||||
* name: '', // 用户名称。name和avatar两个只传一个即可,如果都传了,会显示avatar
|
||||
* avatar: '', // 用户头像
|
||||
* color: '' // 如果没有传头像,那么会以一个圆形来显示名称的第一个字,文字的颜色为白色,圆的颜色可以通过该字段设置
|
||||
* }
|
||||
**/
|
||||
setUserInfo(userInfo) {
|
||||
if (
|
||||
getType(userInfo) !== 'Object' ||
|
||||
isUndef(userInfo.id) ||
|
||||
(isUndef(userInfo.name) && isUndef(userInfo.avatar))
|
||||
)
|
||||
return
|
||||
this.userInfo = userInfo || null
|
||||
}
|
||||
|
||||
// 监听感知数据同步事件
|
||||
onAwareness() {
|
||||
const walk = (list, callback) => {
|
||||
list.forEach(value => {
|
||||
const userName = Object.keys(value)[0]
|
||||
if (!userName) return
|
||||
const data = value[userName]
|
||||
const userInfo = data.userInfo
|
||||
const nodeIdList = data.nodeIdList
|
||||
nodeIdList.forEach(uid => {
|
||||
const node = this.mindMap.renderer.findNodeByUid(uid)
|
||||
callback(uid, node, userInfo)
|
||||
})
|
||||
})
|
||||
}
|
||||
// 清除之前的数据
|
||||
walk(this.currentAwarenessData, (uid, node, userInfo) => {
|
||||
if (node) {
|
||||
node.removeUser(userInfo)
|
||||
}
|
||||
})
|
||||
// 设置当前数据
|
||||
const data = Array.from(this.awareness.getStates().values())
|
||||
this.currentAwarenessData = data
|
||||
walk(data, (uid, node, userInfo) => {
|
||||
// 不显示自己
|
||||
if (userInfo.id === this.userInfo.id) return
|
||||
if (node) {
|
||||
node.addUser(userInfo)
|
||||
} else {
|
||||
this.waitNodeUidMap[uid] = userInfo
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 将树结构转平级对象
|
||||
/*
|
||||
{
|
||||
data: {
|
||||
uid: 'xxx'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
data: {
|
||||
uid: 'xxx'
|
||||
},
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
转为:
|
||||
{
|
||||
uid: {
|
||||
children: [uid1, uid2],
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
*/
|
||||
transformTreeDataToObject(data) {
|
||||
const res = {}
|
||||
const walk = (root, parent) => {
|
||||
const uid = root.data.uid
|
||||
if (parent) {
|
||||
parent.children.push(uid)
|
||||
}
|
||||
res[uid] = {
|
||||
isRoot: !parent,
|
||||
data: {
|
||||
...root.data
|
||||
},
|
||||
children: []
|
||||
}
|
||||
if (root.children && root.children.length > 0) {
|
||||
root.children.forEach(item => {
|
||||
walk(item, res[uid])
|
||||
})
|
||||
}
|
||||
}
|
||||
walk(data, null)
|
||||
return res
|
||||
}
|
||||
|
||||
// 找到父节点的uid
|
||||
findParentUid(data, targetUid) {
|
||||
const uids = Object.keys(data)
|
||||
let res = ''
|
||||
uids.forEach(uid => {
|
||||
const children = data[uid].children
|
||||
const isParent =
|
||||
children.findIndex(childUid => {
|
||||
return childUid === targetUid
|
||||
}) !== -1
|
||||
if (isParent) {
|
||||
res = uid
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 将平级对象转树结构
|
||||
transformObjectToTreeData(data) {
|
||||
const uids = Object.keys(data)
|
||||
if (uids.length <= 0) return null
|
||||
const rootKey = uids.find(uid => {
|
||||
return data[uid].isRoot
|
||||
})
|
||||
if (!rootKey || !data[rootKey]) return null
|
||||
// 根节点
|
||||
const res = {
|
||||
data: simpleDeepClone(data[rootKey].data),
|
||||
children: []
|
||||
}
|
||||
const map = {}
|
||||
map[rootKey] = res
|
||||
uids.forEach(uid => {
|
||||
const parentUid = this.findParentUid(data, uid)
|
||||
const cur = data[uid]
|
||||
const node = map[uid] || {
|
||||
data: simpleDeepClone(cur.data),
|
||||
children: []
|
||||
}
|
||||
if (!map[uid]) {
|
||||
map[uid] = node
|
||||
}
|
||||
if (parentUid) {
|
||||
const index = data[parentUid].children.findIndex(item => {
|
||||
return item === uid
|
||||
})
|
||||
if (!map[parentUid]) {
|
||||
map[parentUid] = {
|
||||
data: simpleDeepClone(data[parentUid].data),
|
||||
children: []
|
||||
}
|
||||
}
|
||||
map[parentUid].children[index] = node
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
Cooperate.instanceName = 'cooperate'
|
||||
|
||||
export default Cooperate
|
||||
@@ -1,8 +1,7 @@
|
||||
import { bfsWalk, throttle } from '../utils'
|
||||
import { bfsWalk, throttle, getTopAncestorsFomNodeList, getNodeIndexInNodeList } from '../utils'
|
||||
import Base from '../layouts/Base'
|
||||
|
||||
// 节点拖动类
|
||||
|
||||
// 节点拖动插件
|
||||
class Drag extends Base {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
@@ -14,8 +13,14 @@ class Drag extends Base {
|
||||
|
||||
// 复位
|
||||
reset() {
|
||||
// 当前拖拽节点
|
||||
this.node = null
|
||||
// 是否正在跳转中
|
||||
this.isDragging = false
|
||||
// 鼠标按下的节点
|
||||
this.mousedownNode = null
|
||||
// 被拖拽中的节点列表
|
||||
this.beingDragNodeList = []
|
||||
// 当前画布节点列表
|
||||
this.nodeList = []
|
||||
// 当前重叠节点
|
||||
this.overlapNode = null
|
||||
// 当前上一个同级节点
|
||||
@@ -26,16 +31,11 @@ class Drag extends Base {
|
||||
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
|
||||
// 拖拽的鼠标位置变量
|
||||
@@ -45,51 +45,50 @@ class Drag extends Base {
|
||||
this.mouseMoveY = 0
|
||||
// 鼠标移动的距离距鼠标按下的位置距离多少以上才认为是拖动事件
|
||||
this.checkDragOffset = 10
|
||||
this.minOffset = 10
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
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) {
|
||||
// 只读模式、不是鼠标左键按下、按下的是概要节点或根节点直接返回
|
||||
if (
|
||||
this.mindMap.opt.readonly ||
|
||||
e.which !== 1 ||
|
||||
node.isGeneralization ||
|
||||
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.mousedownNode = node
|
||||
// 记录鼠标按下的坐标
|
||||
const { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseDownX = x
|
||||
this.mouseDownY = y
|
||||
})
|
||||
this.mindMap.on('mousemove', e => {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
if (this.mindMap.opt.readonly || !this.isMousedown) {
|
||||
return
|
||||
}
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
this.mindMap.emit('node_dragging', this.node)
|
||||
e.preventDefault()
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
const { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseMoveX = x
|
||||
this.mouseMoveY = y
|
||||
// 还没开始移动时鼠标位移过小不认为是拖拽
|
||||
if (
|
||||
!this.isDragging &&
|
||||
Math.abs(x - this.mouseDownX) <= this.checkDragOffset &&
|
||||
Math.abs(y - this.mouseDownY) <= this.checkDragOffset &&
|
||||
!this.node.isDrag
|
||||
Math.abs(y - this.mouseDownY) <= this.checkDragOffset
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.clearAllActive()
|
||||
this.onMove(x, y)
|
||||
this.mindMap.emit('node_dragging')
|
||||
this.handleStartMove()
|
||||
this.onMove(x, y, e)
|
||||
})
|
||||
this.onMouseup = this.onMouseup.bind(this)
|
||||
this.mindMap.on('node_mouseup', this.onMouseup)
|
||||
@@ -102,24 +101,48 @@ class Drag extends Base {
|
||||
return
|
||||
}
|
||||
this.isMousedown = false
|
||||
let _nodeIsDrag = this.node.isDrag
|
||||
this.node.isDrag = false
|
||||
this.node.show()
|
||||
// 恢复被拖拽节点的临时设置
|
||||
this.beingDragNodeList.forEach(node => {
|
||||
node.setOpacity(1)
|
||||
node.showChildren()
|
||||
node.endDrag()
|
||||
})
|
||||
this.removeCloneNode()
|
||||
let overlapNodeUid = this.overlapNode
|
||||
? this.overlapNode.getData('uid')
|
||||
: ''
|
||||
let prevNodeUid = this.prevNode ? this.prevNode.getData('uid') : ''
|
||||
let nextNodeUid = this.nextNode ? this.nextNode.getData('uid') : ''
|
||||
// 存在重叠子节点,则移动作为其子节点
|
||||
if (this.overlapNode) {
|
||||
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
|
||||
this.mindMap.execCommand('MOVE_NODE_TO', this.node, this.overlapNode)
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, false)
|
||||
this.mindMap.execCommand(
|
||||
'MOVE_NODE_TO',
|
||||
this.beingDragNodeList,
|
||||
this.overlapNode
|
||||
)
|
||||
} else if (this.prevNode) {
|
||||
// 存在前一个相邻节点,作为其下一个兄弟节点
|
||||
this.mindMap.renderer.setNodeActive(this.prevNode, false)
|
||||
this.mindMap.execCommand('INSERT_AFTER', this.node, this.prevNode)
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.prevNode, false)
|
||||
this.mindMap.execCommand(
|
||||
'INSERT_AFTER',
|
||||
this.beingDragNodeList,
|
||||
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 && this.mindMap.opt.enableFreeDrag) {
|
||||
// 自定义位置
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.nextNode, false)
|
||||
this.mindMap.execCommand(
|
||||
'INSERT_BEFORE',
|
||||
this.beingDragNodeList,
|
||||
this.nextNode
|
||||
)
|
||||
} else if (
|
||||
this.clone &&
|
||||
this.mindMap.opt.enableFreeDrag &&
|
||||
this.beingDragNodeList.length === 1
|
||||
) {
|
||||
// 如果只拖拽了一个节点,那么设置自定义位置
|
||||
let { x, y } = this.mindMap.toPos(
|
||||
e.clientX - this.offsetX,
|
||||
e.clientY - this.offsetY
|
||||
@@ -127,35 +150,151 @@ class Drag extends Base {
|
||||
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.mousedownNode.left = x
|
||||
this.mousedownNode.top = y
|
||||
this.mousedownNode.customLeft = x
|
||||
this.mousedownNode.customTop = y
|
||||
this.mindMap.execCommand(
|
||||
'SET_NODE_CUSTOM_POSITION',
|
||||
this.mousedownNode,
|
||||
x,
|
||||
y
|
||||
)
|
||||
this.mindMap.render()
|
||||
}
|
||||
this.reset()
|
||||
this.mindMap.emit('node_dragend')
|
||||
this.mindMap.emit('node_dragend', {
|
||||
overlapNodeUid,
|
||||
prevNodeUid,
|
||||
nextNodeUid
|
||||
})
|
||||
}
|
||||
|
||||
// 拖动中
|
||||
onMove(x, y, e) {
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
// 更新克隆节点的位置
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
let cloneNodeLeft = x - this.offsetX
|
||||
let cloneNodeTop = y - this.offsetY
|
||||
x = (cloneNodeLeft - translateX) / scaleX
|
||||
y = (cloneNodeTop - translateY) / scaleY
|
||||
let t = this.clone.transform()
|
||||
this.clone.translate(x - t.translateX, y - t.translateY)
|
||||
// 检测新位置
|
||||
this.checkOverlapNode()
|
||||
// 如果注册了多选节点插件,那么复用它的边缘自动移动画布功能
|
||||
if (this.mindMap.opt.autoMoveWhenMouseInEdgeOnDrag && this.mindMap.select) {
|
||||
this.drawTransform = this.mindMap.draw.transform()
|
||||
this.mindMap.select.clearAutoMoveTimer()
|
||||
this.mindMap.select.onMove(e.clientX, e.clientY)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始拖拽时初始化一些数据
|
||||
handleStartMove() {
|
||||
if (!this.isDragging) {
|
||||
this.isDragging = true
|
||||
// 鼠标按下的节点
|
||||
let node = this.mousedownNode
|
||||
// 计算鼠标按下的位置距离节点左上角的距离
|
||||
this.drawTransform = this.mindMap.draw.transform()
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
this.offsetX = this.mouseDownX - (node.left * scaleX + translateX)
|
||||
this.offsetY = this.mouseDownY - (node.top * scaleY + translateY)
|
||||
// 如果鼠标按下的节点是激活节点,那么保存当前所有激活的节点
|
||||
if (node.getData('isActive')) {
|
||||
// 找出这些激活节点中的最顶层节点
|
||||
this.beingDragNodeList = getTopAncestorsFomNodeList(
|
||||
// 过滤掉根节点和概要节点
|
||||
this.mindMap.renderer.activeNodeList.filter(item => {
|
||||
return !item.isRoot && !item.isGeneralization
|
||||
})
|
||||
)
|
||||
} else {
|
||||
// 否则只拖拽按下的节点
|
||||
this.beingDragNodeList = [node]
|
||||
}
|
||||
// 将节点树转为节点数组
|
||||
this.nodeTreeToList()
|
||||
// 创建克隆节点
|
||||
this.createCloneNode()
|
||||
// 清除当前所有激活的节点
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
}
|
||||
}
|
||||
|
||||
// 节点由树转换成数组,从子节点到根节点
|
||||
nodeTreeToList() {
|
||||
const list = []
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
// 过滤掉当前被拖拽的节点
|
||||
if (this.checkIsInBeingDragNodeList(node)) {
|
||||
return
|
||||
}
|
||||
if (!list[node.layerIndex]) {
|
||||
list[node.layerIndex] = []
|
||||
}
|
||||
list[node.layerIndex].push(node)
|
||||
})
|
||||
this.nodeList = list.reduceRight((res, cur) => {
|
||||
return [...res, ...cur]
|
||||
}, [])
|
||||
}
|
||||
|
||||
// 创建克隆节点
|
||||
createCloneNode() {
|
||||
if (!this.clone) {
|
||||
// 节点
|
||||
this.clone = this.node.group.clone()
|
||||
this.clone.opacity(0.5)
|
||||
const {
|
||||
dragMultiNodeRectConfig,
|
||||
dragPlaceholderRectFill,
|
||||
dragOpacityConfig
|
||||
} = this.mindMap.opt
|
||||
const {
|
||||
width: rectWidth,
|
||||
height: rectHeight,
|
||||
fill: rectFill
|
||||
} = dragMultiNodeRectConfig
|
||||
const node = this.beingDragNodeList[0]
|
||||
const lineColor = node.style.merge('lineColor', true)
|
||||
// 如果当前被拖拽的节点数量大于1,那么创建一个矩形示意
|
||||
if (this.beingDragNodeList.length > 1) {
|
||||
this.clone = this.mindMap.otherDraw
|
||||
.rect()
|
||||
.size(rectWidth, rectHeight)
|
||||
.radius(rectHeight / 2)
|
||||
.fill({
|
||||
color: rectFill || lineColor
|
||||
})
|
||||
this.offsetX = rectWidth / 2
|
||||
this.offsetY = rectHeight / 2
|
||||
} else {
|
||||
// 否则克隆当前的节点
|
||||
this.clone = node.group.clone()
|
||||
// 删除展开收起按钮元素
|
||||
const expandEl = this.clone.findOne('.smm-expand-btn')
|
||||
if (expandEl) {
|
||||
expandEl.remove()
|
||||
}
|
||||
this.mindMap.otherDraw.add(this.clone)
|
||||
}
|
||||
this.clone.opacity(dragOpacityConfig.cloneNodeOpacity)
|
||||
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.placeholder = this.mindMap.otherDraw.rect().fill({
|
||||
color: dragPlaceholderRectFill || lineColor
|
||||
})
|
||||
// 当前被拖拽的节点的临时设置
|
||||
this.beingDragNodeList.forEach(node => {
|
||||
// 降低透明度
|
||||
node.setOpacity(dragOpacityConfig.beingDragNodeOpacity)
|
||||
// 隐藏连线及下级节点
|
||||
node.hideChildren()
|
||||
// 设置拖拽状态
|
||||
node.startDrag()
|
||||
})
|
||||
this.mindMap.draw.add(this.clone)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,131 +304,350 @@ class Drag extends Base {
|
||||
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) {
|
||||
if (!this.drawTransform || !this.placeholder) {
|
||||
return
|
||||
}
|
||||
let x = this.mouseMoveX
|
||||
let y = this.mouseMoveY
|
||||
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
|
||||
this.nodeList.forEach(node => {
|
||||
if (node.getData('isActive')) {
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', node, false)
|
||||
}
|
||||
if (this.overlapNode || (this.prevNode && this.nextNode)) {
|
||||
return
|
||||
}
|
||||
let nodeRect = this.getNodeRect(node)
|
||||
let oneFourthHeight = nodeRect.height / 4
|
||||
// 前一个和后一个节点
|
||||
let checkList = node.parent ? node.parent.children.filter((item) => {
|
||||
return item !== this.node
|
||||
}) : []
|
||||
let index = checkList.findIndex((item) => {
|
||||
return item === node
|
||||
})
|
||||
let prevBrother = null
|
||||
let nextBrother = null
|
||||
if (index !== -1) {
|
||||
if (index - 1 >= 0) {
|
||||
prevBrother = checkList[index - 1]
|
||||
}
|
||||
if (index + 1 <= checkList.length - 1) {
|
||||
nextBrother = checkList[index + 1]
|
||||
}
|
||||
}
|
||||
// 和前一个兄弟节点的距离
|
||||
let prevBrotherOffset = 0
|
||||
if (prevBrother) {
|
||||
let prevNodeRect = this.getNodeRect(prevBrother)
|
||||
prevBrotherOffset = nodeRect.top - prevNodeRect.bottom
|
||||
// 间距小于10就当它不存在
|
||||
prevBrotherOffset = prevBrotherOffset >= 10 ? prevBrotherOffset / 2 : 0
|
||||
} else {
|
||||
// 没有前一个兄弟节点,那么假设和前一个节点的距离为20
|
||||
prevBrotherOffset = 10
|
||||
}
|
||||
// 和后一个兄弟节点的距离
|
||||
let nextBrotherOffset = 0
|
||||
if (nextBrother) {
|
||||
let nextNodeRect = this.getNodeRect(nextBrother)
|
||||
nextBrotherOffset = nextNodeRect.top - nodeRect.bottom
|
||||
nextBrotherOffset = nextBrotherOffset >= 10 ? nextBrotherOffset / 2 : 0
|
||||
} else {
|
||||
nextBrotherOffset = 10
|
||||
}
|
||||
if (nodeRect.left <= x && nodeRect.right >= x) {
|
||||
// 检测兄弟节点位置
|
||||
if (!this.overlapNode && !this.prevNode && !this.nextNode && !node.isRoot) {
|
||||
let checkIsPrevNode = nextBrotherOffset > 0 ? // 距离下一个兄弟节点的距离大于0
|
||||
y > nodeRect.bottom && y <= (nodeRect.bottom + nextBrotherOffset) : // 那么在当前节点外底部判断
|
||||
y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断
|
||||
let checkIsNextNode = prevBrotherOffset > 0 ? // 距离上一个兄弟节点的距离大于0
|
||||
y < nodeRect.top && y >= (nodeRect.top - prevBrotherOffset) : // 那么在当前节点外底部判断
|
||||
y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
|
||||
if (checkIsPrevNode) {
|
||||
this.prevNode = node
|
||||
let size = nextBrotherOffset > 0 ? nextBrotherOffset : 5
|
||||
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom)
|
||||
} else if (checkIsNextNode) {
|
||||
this.nextNode = node
|
||||
let size = prevBrotherOffset > 0 ? prevBrotherOffset : 5
|
||||
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originTop - size)
|
||||
}
|
||||
}
|
||||
// 检测是否重叠
|
||||
if (!this.overlapNode && !this.prevNode && !this.nextNode) {
|
||||
if (
|
||||
nodeRect.top + (prevBrotherOffset > 0 ? 0 : oneFourthHeight) <= y &&
|
||||
nodeRect.bottom - (nextBrotherOffset > 0 ? 0 : oneFourthHeight) >= y
|
||||
) {
|
||||
this.overlapNode = node
|
||||
}
|
||||
}
|
||||
switch (this.mindMap.opt.layout) {
|
||||
case 'logicalStructure':
|
||||
this.handleLogicalStructure(node)
|
||||
break
|
||||
case 'mindMap':
|
||||
this.handleMindMap(node)
|
||||
break
|
||||
case 'organizationStructure':
|
||||
this.handleOrganizationStructure(node)
|
||||
break
|
||||
case 'catalogOrganization':
|
||||
this.handleCatalogOrganization(node)
|
||||
break
|
||||
case 'timeline':
|
||||
this.handleTimeLine(node)
|
||||
break
|
||||
case 'timeline2':
|
||||
this.handleTimeLine2(node)
|
||||
break
|
||||
case 'verticalTimeline':
|
||||
this.handleLogicalStructure(node)
|
||||
break
|
||||
case 'fishbone':
|
||||
this.handleFishbone(node)
|
||||
break
|
||||
default:
|
||||
this.handleLogicalStructure(node)
|
||||
}
|
||||
})
|
||||
if (this.overlapNode) {
|
||||
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
|
||||
}
|
||||
}
|
||||
|
||||
// 垂直方向比较
|
||||
// isReverse:是否反向
|
||||
handleVerticalCheck(node, checkList, isReverse = false) {
|
||||
let x = this.mouseMoveX
|
||||
let y = this.mouseMoveY
|
||||
let nodeRect = this.getNodeRect(node)
|
||||
if (isReverse) {
|
||||
checkList = checkList.reverse()
|
||||
}
|
||||
let oneFourthHeight = nodeRect.height / 4
|
||||
let { prevBrotherOffset, nextBrotherOffset } =
|
||||
this.getNodeDistanceToSiblingNode(checkList, node, nodeRect, 'v')
|
||||
if (nodeRect.left <= x && nodeRect.right >= x) {
|
||||
// 检测兄弟节点位置
|
||||
if (
|
||||
!this.overlapNode &&
|
||||
!this.prevNode &&
|
||||
!this.nextNode &&
|
||||
!node.isRoot
|
||||
) {
|
||||
let checkIsPrevNode =
|
||||
nextBrotherOffset > 0 // 距离下一个兄弟节点的距离大于0
|
||||
? y > nodeRect.bottom && y <= nodeRect.bottom + nextBrotherOffset // 那么在当前节点外底部判断
|
||||
: y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断
|
||||
let checkIsNextNode =
|
||||
prevBrotherOffset > 0 // 距离上一个兄弟节点的距离大于0
|
||||
? y < nodeRect.top && y >= nodeRect.top - prevBrotherOffset // 那么在当前节点外底部判断
|
||||
: y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
|
||||
if (checkIsPrevNode) {
|
||||
if (isReverse) {
|
||||
this.nextNode = node
|
||||
} else {
|
||||
this.prevNode = node
|
||||
}
|
||||
let size = this.formatPlaceholderSize(nextBrotherOffset)
|
||||
this.setPlaceholderRect(
|
||||
node.width,
|
||||
size,
|
||||
nodeRect.originLeft,
|
||||
nodeRect.originBottom
|
||||
)
|
||||
} else if (checkIsNextNode) {
|
||||
if (isReverse) {
|
||||
this.prevNode = node
|
||||
} else {
|
||||
this.nextNode = node
|
||||
}
|
||||
let size = this.formatPlaceholderSize(prevBrotherOffset)
|
||||
this.setPlaceholderRect(
|
||||
node.width,
|
||||
size,
|
||||
nodeRect.originLeft,
|
||||
nodeRect.originTop - size
|
||||
)
|
||||
}
|
||||
}
|
||||
// 检测是否重叠
|
||||
this.checkIsOverlap({
|
||||
node,
|
||||
dir: 'v',
|
||||
prevBrotherOffset,
|
||||
nextBrotherOffset,
|
||||
size: oneFourthHeight,
|
||||
pos: y,
|
||||
nodeRect
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 水平方向比较
|
||||
handleHorizontalCheck(node, checkList) {
|
||||
let x = this.mouseMoveX
|
||||
let y = this.mouseMoveY
|
||||
let nodeRect = this.getNodeRect(node)
|
||||
let oneFourthWidth = nodeRect.width / 4
|
||||
let { prevBrotherOffset, nextBrotherOffset } =
|
||||
this.getNodeDistanceToSiblingNode(checkList, node, nodeRect, 'h')
|
||||
if (nodeRect.top <= y && nodeRect.bottom >= y) {
|
||||
// 检测兄弟节点位置
|
||||
if (
|
||||
!this.overlapNode &&
|
||||
!this.prevNode &&
|
||||
!this.nextNode &&
|
||||
!node.isRoot
|
||||
) {
|
||||
let checkIsPrevNode =
|
||||
nextBrotherOffset > 0 // 距离下一个兄弟节点的距离大于0
|
||||
? x < nodeRect.right + nextBrotherOffset && x >= nodeRect.right // 那么在当前节点外底部判断
|
||||
: x <= nodeRect.right && x >= nodeRect.right - oneFourthWidth // 否则在当前节点内底部1/4区间判断
|
||||
let checkIsNextNode =
|
||||
prevBrotherOffset > 0 // 距离上一个兄弟节点的距离大于0
|
||||
? x > nodeRect.left - prevBrotherOffset && x <= nodeRect.left // 那么在当前节点外底部判断
|
||||
: x <= nodeRect.left + oneFourthWidth && x >= nodeRect.left
|
||||
if (checkIsPrevNode) {
|
||||
this.prevNode = node
|
||||
let size = this.formatPlaceholderSize(nextBrotherOffset)
|
||||
this.setPlaceholderRect(
|
||||
size,
|
||||
node.height,
|
||||
nodeRect.originRight,
|
||||
nodeRect.originTop
|
||||
)
|
||||
} else if (checkIsNextNode) {
|
||||
this.nextNode = node
|
||||
let size = this.formatPlaceholderSize(prevBrotherOffset)
|
||||
this.setPlaceholderRect(
|
||||
size,
|
||||
node.height,
|
||||
nodeRect.originLeft - size,
|
||||
nodeRect.originTop
|
||||
)
|
||||
}
|
||||
}
|
||||
// 检测是否重叠
|
||||
this.checkIsOverlap({
|
||||
node,
|
||||
dir: 'h',
|
||||
prevBrotherOffset,
|
||||
nextBrotherOffset,
|
||||
size: oneFourthWidth,
|
||||
pos: x,
|
||||
nodeRect
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点距前一个和后一个节点的距离
|
||||
getNodeDistanceToSiblingNode(checkList, node, nodeRect, dir) {
|
||||
let dir1 = dir === 'v' ? 'top' : 'left'
|
||||
let dir2 = dir === 'v' ? 'bottom' : 'right'
|
||||
let index = getNodeIndexInNodeList(node, checkList)
|
||||
let prevBrother = null
|
||||
let nextBrother = null
|
||||
if (index !== -1) {
|
||||
if (index - 1 >= 0) {
|
||||
prevBrother = checkList[index - 1]
|
||||
}
|
||||
if (index + 1 <= checkList.length - 1) {
|
||||
nextBrother = checkList[index + 1]
|
||||
}
|
||||
}
|
||||
// 和前一个兄弟节点的距离
|
||||
let prevBrotherOffset = 0
|
||||
if (prevBrother) {
|
||||
let prevNodeRect = this.getNodeRect(prevBrother)
|
||||
prevBrotherOffset = nodeRect[dir1] - prevNodeRect[dir2]
|
||||
// 间距小于10就当它不存在
|
||||
prevBrotherOffset =
|
||||
prevBrotherOffset >= this.minOffset ? prevBrotherOffset / 2 : 0
|
||||
} else {
|
||||
// 没有前一个兄弟节点,那么假设和前一个节点的距离为20
|
||||
prevBrotherOffset = this.minOffset
|
||||
}
|
||||
// 和后一个兄弟节点的距离
|
||||
let nextBrotherOffset = 0
|
||||
if (nextBrother) {
|
||||
let nextNodeRect = this.getNodeRect(nextBrother)
|
||||
nextBrotherOffset = nextNodeRect[dir1] - nodeRect[dir2]
|
||||
nextBrotherOffset =
|
||||
nextBrotherOffset >= this.minOffset ? nextBrotherOffset / 2 : 0
|
||||
} else {
|
||||
nextBrotherOffset = this.minOffset
|
||||
}
|
||||
return {
|
||||
prevBrotherOffset,
|
||||
nextBrotherOffset
|
||||
}
|
||||
}
|
||||
|
||||
// 处理提示元素的大小
|
||||
formatPlaceholderSize(size) {
|
||||
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt
|
||||
return size > 0 ? Math.min(size, nodeDragPlaceholderMaxSize) : 5
|
||||
}
|
||||
|
||||
// 设置提示元素的大小和位置
|
||||
setPlaceholderRect(w, h, x, y) {
|
||||
this.placeholder.size(w, h).move(x, y)
|
||||
}
|
||||
|
||||
// 检测是否重叠
|
||||
checkIsOverlap({
|
||||
node,
|
||||
dir,
|
||||
prevBrotherOffset,
|
||||
nextBrotherOffset,
|
||||
size,
|
||||
pos,
|
||||
nodeRect
|
||||
}) {
|
||||
let dir1 = dir === 'v' ? 'top' : 'left'
|
||||
let dir2 = dir === 'v' ? 'bottom' : 'right'
|
||||
if (!this.overlapNode && !this.prevNode && !this.nextNode) {
|
||||
if (
|
||||
nodeRect[dir1] + (prevBrotherOffset > 0 ? 0 : size) <= pos &&
|
||||
nodeRect[dir2] - (nextBrotherOffset > 0 ? 0 : size) >= pos
|
||||
) {
|
||||
this.overlapNode = node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理逻辑结构图
|
||||
handleLogicalStructure(node) {
|
||||
const checkList = this.commonGetNodeCheckList(node)
|
||||
this.handleVerticalCheck(node, checkList)
|
||||
}
|
||||
|
||||
// 处理思维导图
|
||||
handleMindMap(node) {
|
||||
const checkList = node.parent
|
||||
? node.parent.children.filter(item => {
|
||||
let sameDir = true
|
||||
if (node.layerIndex === 1) {
|
||||
sameDir = item.dir === node.dir
|
||||
}
|
||||
return sameDir && !this.checkIsInBeingDragNodeList(item)
|
||||
})
|
||||
: []
|
||||
this.handleVerticalCheck(node, checkList)
|
||||
}
|
||||
|
||||
// 处理组织结构图
|
||||
handleOrganizationStructure(node) {
|
||||
const checkList = this.commonGetNodeCheckList(node)
|
||||
this.handleHorizontalCheck(node, checkList)
|
||||
}
|
||||
|
||||
// 处理目录组织图
|
||||
handleCatalogOrganization(node) {
|
||||
const checkList = this.commonGetNodeCheckList(node)
|
||||
if (node.layerIndex === 1) {
|
||||
this.handleHorizontalCheck(node, checkList)
|
||||
} else {
|
||||
this.handleVerticalCheck(node, checkList)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理时间轴
|
||||
handleTimeLine(node) {
|
||||
let checkList = this.commonGetNodeCheckList(node)
|
||||
if (node.layerIndex === 1) {
|
||||
this.handleHorizontalCheck(node, checkList)
|
||||
} else {
|
||||
this.handleVerticalCheck(node, checkList)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理时间轴2
|
||||
handleTimeLine2(node) {
|
||||
let checkList = this.commonGetNodeCheckList(node)
|
||||
if (node.layerIndex === 1) {
|
||||
this.handleHorizontalCheck(node, checkList)
|
||||
} else {
|
||||
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
|
||||
if (node.dir === 'top' && node.layerIndex === 2) {
|
||||
this.handleVerticalCheck(node, checkList, true)
|
||||
} else {
|
||||
this.handleVerticalCheck(node, checkList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理鱼骨图
|
||||
handleFishbone(node) {
|
||||
let checkList = node.parent
|
||||
? node.parent.children.filter(item => {
|
||||
return item.layerIndex > 1 && !this.checkIsInBeingDragNodeList(item)
|
||||
})
|
||||
: []
|
||||
if (node.layerIndex === 1) {
|
||||
this.handleHorizontalCheck(node, checkList)
|
||||
} else {
|
||||
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
|
||||
if (node.dir === 'top' && node.layerIndex === 2) {
|
||||
this.handleVerticalCheck(node, checkList, true)
|
||||
} else {
|
||||
this.handleVerticalCheck(node, checkList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点的兄弟节点列表通用方法
|
||||
commonGetNodeCheckList(node) {
|
||||
return node.parent
|
||||
? [...node.parent.children].filter(item => {
|
||||
return !this.checkIsInBeingDragNodeList(item)
|
||||
})
|
||||
: []
|
||||
}
|
||||
|
||||
// 计算节点的位置尺寸信息
|
||||
getNodeRect(node) {
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
@@ -297,6 +655,7 @@ class Drag extends Base {
|
||||
let originLeft = left
|
||||
let originTop = top
|
||||
let originBottom = top + height
|
||||
let originRight = left + width
|
||||
let right = (left + width) * scaleX + translateX
|
||||
let bottom = (top + height) * scaleY + translateY
|
||||
left = left * scaleX + translateX
|
||||
@@ -310,9 +669,17 @@ class Drag extends Base {
|
||||
bottom,
|
||||
originLeft,
|
||||
originTop,
|
||||
originBottom
|
||||
originBottom,
|
||||
originRight
|
||||
}
|
||||
}
|
||||
|
||||
// 检查某个节点是否在被拖拽节点内
|
||||
checkIsInBeingDragNodeList(node) {
|
||||
return !!this.beingDragNodeList.find(item => {
|
||||
return item.uid === node.uid || item.isAncestor(node)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Drag.instanceName = 'drag'
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { imgToDataUrl, downloadFile, readBlob } from '../utils'
|
||||
import {
|
||||
imgToDataUrl,
|
||||
downloadFile,
|
||||
readBlob,
|
||||
removeHTMLEntities,
|
||||
resizeImgSize
|
||||
} from '../utils'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
|
||||
import { transformToMarkdown } from '../parse/toMarkdown'
|
||||
import { a4Size } from '../constants/constant'
|
||||
|
||||
// 导出类
|
||||
// 导出插件
|
||||
class Export {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
this.mindMap = opt.mindMap
|
||||
this.exportPadding = this.mindMap.opt.exportPadding
|
||||
}
|
||||
|
||||
// 导出
|
||||
@@ -35,6 +41,10 @@ class Export {
|
||||
let imageList = svg.find('image')
|
||||
let task = imageList.map(async item => {
|
||||
let imgUlr = item.attr('href') || item.attr('xlink:href')
|
||||
// 已经是data:URL形式不用转换
|
||||
if (/^data:/.test(imgUlr) || imgUlr === 'none') {
|
||||
return
|
||||
}
|
||||
let imgData = await imgToDataUrl(imgUlr)
|
||||
item.attr('href', imgData)
|
||||
})
|
||||
@@ -49,33 +59,63 @@ class Export {
|
||||
}
|
||||
|
||||
// svg转png
|
||||
svgToPng(svgSrc, transparent) {
|
||||
svgToPng(
|
||||
svgSrc,
|
||||
transparent,
|
||||
checkRotate = () => {
|
||||
return false
|
||||
},
|
||||
compress
|
||||
) {
|
||||
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')
|
||||
const canvas = document.createElement('canvas')
|
||||
const dpr = Math.max(
|
||||
window.devicePixelRatio,
|
||||
this.mindMap.opt.minExportImgCanvasScale
|
||||
)
|
||||
let imgWidth = img.width
|
||||
let imgHeight = img.height
|
||||
// 压缩图片
|
||||
if (compress) {
|
||||
const compressedSize = resizeImgSize(
|
||||
imgWidth,
|
||||
imgHeight,
|
||||
compress.width,
|
||||
compress.height
|
||||
)
|
||||
imgWidth = compressedSize[0]
|
||||
imgHeight = compressedSize[1]
|
||||
}
|
||||
// 如果宽比高长,那么旋转90度
|
||||
const needRotate = checkRotate(imgWidth, imgHeight)
|
||||
if (needRotate) {
|
||||
canvas.width = imgHeight * dpr
|
||||
canvas.height = imgWidth * dpr
|
||||
canvas.style.width = imgHeight + 'px'
|
||||
canvas.style.height = imgWidth + 'px'
|
||||
} else {
|
||||
canvas.width = imgWidth * dpr
|
||||
canvas.height = imgHeight * dpr
|
||||
canvas.style.width = imgWidth + 'px'
|
||||
canvas.style.height = imgHeight + 'px'
|
||||
}
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.scale(dpr, dpr)
|
||||
if (needRotate) {
|
||||
ctx.rotate(0.5 * Math.PI)
|
||||
ctx.translate(0, -imgHeight)
|
||||
}
|
||||
// 绘制背景
|
||||
if (!transparent) {
|
||||
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
|
||||
await this.drawBackgroundToCanvas(ctx, imgWidth, imgHeight)
|
||||
}
|
||||
// 图片绘制到canvas里
|
||||
ctx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
img.width,
|
||||
img.height,
|
||||
this.exportPadding,
|
||||
this.exportPadding,
|
||||
img.width,
|
||||
img.height
|
||||
)
|
||||
ctx.drawImage(img, 0, 0, imgWidth, imgHeight)
|
||||
resolve(canvas.toDataURL())
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
@@ -96,7 +136,7 @@ class Export {
|
||||
backgroundImage,
|
||||
backgroundRepeat = 'no-repeat',
|
||||
backgroundPosition = 'center center',
|
||||
backgroundSize = 'cover',
|
||||
backgroundSize = 'cover'
|
||||
} = this.mindMap.themeConfig
|
||||
// 背景颜色
|
||||
ctx.save()
|
||||
@@ -107,18 +147,25 @@ class Export {
|
||||
// 背景图片
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
ctx.save()
|
||||
drawBackgroundImageToCanvas(ctx, width, height, backgroundImage, {
|
||||
backgroundRepeat,
|
||||
backgroundPosition,
|
||||
backgroundSize
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
drawBackgroundImageToCanvas(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
backgroundImage,
|
||||
{
|
||||
backgroundRepeat,
|
||||
backgroundPosition,
|
||||
backgroundSize
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
ctx.restore()
|
||||
})
|
||||
)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
@@ -152,14 +199,28 @@ class Export {
|
||||
* 方法1.把svg的图片都转化成data:url格式,再转换
|
||||
* 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换
|
||||
*/
|
||||
async png(name, transparent = false) {
|
||||
async png(name, transparent = false, checkRotate, compress) {
|
||||
let { node, str } = await this.getSvgData()
|
||||
// 如果开启了富文本,则使用htmltocanvas转换为图片
|
||||
if (this.mindMap.richText) {
|
||||
let res = await this.mindMap.richText.handleExportPng(node.node)
|
||||
let imgDataUrl = await this.svgToPng(res, transparent)
|
||||
return imgDataUrl
|
||||
// 覆盖html默认的样式
|
||||
let foreignObjectList = node.find('foreignObject')
|
||||
if (foreignObjectList.length > 0) {
|
||||
foreignObjectList[0].add(
|
||||
SVG(`<style>${this.mindMap.opt.resetCss}</style>`)
|
||||
)
|
||||
}
|
||||
str = node.svg()
|
||||
// 使用其他库(html2canvas、dom-to-image-more等)来完成导出
|
||||
// let res = await this.mindMap.richText.handleExportPng(node.node)
|
||||
// let imgDataUrl = await this.svgToPng(
|
||||
// res,
|
||||
// transparent,
|
||||
// checkRotate
|
||||
// )
|
||||
// return imgDataUrl
|
||||
}
|
||||
str = removeHTMLEntities(str)
|
||||
// 转换成blob数据
|
||||
let blob = new Blob([str], {
|
||||
type: 'image/svg+xml'
|
||||
@@ -167,35 +228,57 @@ class Export {
|
||||
// 转换成data:url数据
|
||||
let svgUrl = await readBlob(blob)
|
||||
// 绘制到canvas上
|
||||
let res = await this.svgToPng(svgUrl, transparent)
|
||||
let res = await this.svgToPng(svgUrl, transparent, checkRotate, compress)
|
||||
return res
|
||||
}
|
||||
|
||||
// 导出为pdf
|
||||
async pdf(name) {
|
||||
async pdf(name, useMultiPageExport, maxImageWidth) {
|
||||
if (!this.mindMap.doExportPDF) {
|
||||
throw new Error('请注册ExportPDF插件')
|
||||
}
|
||||
let img = await this.png()
|
||||
this.mindMap.doExportPDF.pdf(name, img)
|
||||
let img = await this.png(
|
||||
'',
|
||||
false,
|
||||
(width, height) => {
|
||||
if (width <= a4Size.width && height && a4Size.height) return false
|
||||
return width / height > 1
|
||||
},
|
||||
{
|
||||
width: maxImageWidth || a4Size.width * 2
|
||||
}
|
||||
)
|
||||
await this.mindMap.doExportPDF.pdf(name, img, useMultiPageExport)
|
||||
}
|
||||
|
||||
// 导出为xmind
|
||||
async xmind(name) {
|
||||
if (!this.mindMap.doExportXMind) {
|
||||
throw new Error('请注册ExportXMind插件')
|
||||
}
|
||||
const data = this.mindMap.getData()
|
||||
const blob = await this.mindMap.doExportXMind.xmind(data, name)
|
||||
const res = await readBlob(blob)
|
||||
return res
|
||||
}
|
||||
|
||||
// 导出为svg
|
||||
// plusCssText:附加的css样式,如果svg中存在dom节点,想要设置一些针对节点的样式可以通过这个参数传入
|
||||
async svg(name, plusCssText) {
|
||||
async svg(name) {
|
||||
let { node } = await this.getSvgData()
|
||||
// 开启了节点富文本编辑
|
||||
if (this.mindMap.richText) {
|
||||
if (plusCssText) {
|
||||
let foreignObjectList = node.find('foreignObject')
|
||||
if (foreignObjectList.length > 0) {
|
||||
foreignObjectList[0].add(SVG(`<style>${plusCssText}</style>`))
|
||||
}
|
||||
let foreignObjectList = node.find('foreignObject')
|
||||
if (foreignObjectList.length > 0) {
|
||||
foreignObjectList[0].add(
|
||||
SVG(`<style>${this.mindMap.opt.resetCss}</style>`)
|
||||
)
|
||||
}
|
||||
}
|
||||
node.first().before(SVG(`<title>${name}</title>`))
|
||||
await this.drawBackgroundToSvg(node)
|
||||
let str = node.svg()
|
||||
str = removeHTMLEntities(str)
|
||||
// 转换成blob数据
|
||||
let blob = new Blob([str], {
|
||||
type: 'image/svg+xml'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import JsPDF from 'jspdf'
|
||||
import JsPDF from '../utils/jspdf'
|
||||
import { a4Size } from '../constants/constant'
|
||||
|
||||
// 导出PDF类,需要通过Export插件使用
|
||||
// 导出PDF插件,需要通过Export插件使用
|
||||
class ExportPDF {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
@@ -8,34 +9,104 @@ class ExportPDF {
|
||||
}
|
||||
|
||||
// 导出为pdf
|
||||
pdf(name, img) {
|
||||
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)
|
||||
async pdf(name, img, useMultiPageExport = false) {
|
||||
if (useMultiPageExport) {
|
||||
await this.multiPageExport(name, img)
|
||||
} else {
|
||||
await this.onePageExport(name, img)
|
||||
}
|
||||
image.src = img
|
||||
}
|
||||
|
||||
// 单页导出
|
||||
onePageExport(name, img) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let pdf = new JsPDF('', 'pt', 'a4')
|
||||
let a4Ratio = a4Size.width / a4Size.height
|
||||
let image = new Image()
|
||||
image.onload = () => {
|
||||
let imageWidth = image.width
|
||||
let imageHeight = image.height
|
||||
let imageRatio = imageWidth / imageHeight
|
||||
let w, h
|
||||
if (imageWidth <= a4Size.width && imageHeight <= a4Size.height) {
|
||||
// 使用图片原始宽高
|
||||
w = imageWidth
|
||||
h = imageHeight
|
||||
} else if (a4Ratio > imageRatio) {
|
||||
// 以a4Height为高度,缩放图片宽度
|
||||
w = imageRatio * a4Size.height
|
||||
h = a4Size.height
|
||||
} else {
|
||||
// 以a4Width为宽度,缩放图片高度
|
||||
w = a4Size.width
|
||||
h = a4Size.width / imageRatio
|
||||
}
|
||||
pdf.addImage(
|
||||
img,
|
||||
'PNG',
|
||||
(a4Size.width - w) / 2,
|
||||
(a4Size.height - h) / 2,
|
||||
w,
|
||||
h
|
||||
)
|
||||
pdf.save(name)
|
||||
resolve()
|
||||
}
|
||||
image.onerror = e => {
|
||||
reject(e)
|
||||
}
|
||||
image.src = img
|
||||
})
|
||||
}
|
||||
|
||||
// 多页导出
|
||||
multiPageExport(name, img) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let image = new Image()
|
||||
image.onload = () => {
|
||||
let imageWidth = image.width
|
||||
let imageHeight = image.height
|
||||
// 一页pdf显示高度
|
||||
let pageHeight = (imageWidth / a4Size.width) * a4Size.height
|
||||
// 未生成pdf的高度
|
||||
let leftHeight = imageHeight
|
||||
// 偏移
|
||||
let position = 0
|
||||
// a4纸的尺寸[595.28,841.89],图片在pdf中图片的宽高
|
||||
let imgWidth = a4Size.width
|
||||
let imgHeight = (a4Size.width / imageWidth) * imageHeight
|
||||
let pdf = new JsPDF('', 'pt', 'a4')
|
||||
// 有两个高度需要区分,一个是图片的实际高度,和生成pdf的页面高度(841.89)
|
||||
// 当内容未超过pdf一页显示的范围,无需分页
|
||||
if (leftHeight < pageHeight) {
|
||||
pdf.addImage(
|
||||
img,
|
||||
'PNG',
|
||||
(a4Size.width - imgWidth) / 2,
|
||||
(a4Size.height - imgHeight) / 2,
|
||||
imgWidth,
|
||||
imgHeight
|
||||
)
|
||||
} else {
|
||||
// 分页
|
||||
while (leftHeight > 0) {
|
||||
pdf.addImage(img, 'PNG', 0, position, imgWidth, imgHeight)
|
||||
leftHeight -= pageHeight
|
||||
position -= a4Size.height
|
||||
// 避免添加空白页
|
||||
if (leftHeight > 0) {
|
||||
pdf.addPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
pdf.save(name)
|
||||
resolve()
|
||||
}
|
||||
image.onerror = (e) => {
|
||||
reject(e)
|
||||
}
|
||||
image.src = img
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
24
simple-mind-map/src/plugins/ExportXMind.js
Normal file
24
simple-mind-map/src/plugins/ExportXMind.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import xmind from '../parse/xmind'
|
||||
|
||||
// 导出XMind插件,需要通过Export插件使用
|
||||
class ExportXMind {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
this.mindMap = opt.mindMap
|
||||
}
|
||||
|
||||
// 导出xmind
|
||||
async xmind(data, name) {
|
||||
const zipData = await xmind.transformToXmind(data, name)
|
||||
return zipData
|
||||
}
|
||||
|
||||
// 获取解析器
|
||||
getXmind() {
|
||||
return xmind
|
||||
}
|
||||
}
|
||||
|
||||
ExportXMind.instanceName = 'doExportXMind'
|
||||
|
||||
export default ExportXMind
|
||||
53
simple-mind-map/src/plugins/Formula.js
Normal file
53
simple-mind-map/src/plugins/Formula.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import katex from 'katex'
|
||||
import Quill from 'quill'
|
||||
|
||||
// 数学公式支持插件
|
||||
// 该插件在富文本模式下可用
|
||||
class Formula {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
this.opt = opt
|
||||
this.mindMap = opt.mindMap
|
||||
window.katex = katex
|
||||
this.extendQuill()
|
||||
}
|
||||
|
||||
// 修改formula格式工具
|
||||
extendQuill() {
|
||||
const QuillFormula = Quill.import('formats/formula')
|
||||
|
||||
class CustomFormulaBlot extends QuillFormula {
|
||||
static create(value) {
|
||||
let node = super.create(value)
|
||||
if (typeof value === 'string') {
|
||||
katex.render(value, node, {
|
||||
throwOnError: false,
|
||||
errorColor: '#f00',
|
||||
output: 'mathml' // 增加该配置,默认只输出公式
|
||||
})
|
||||
node.setAttribute('data-value', value)
|
||||
}
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
Quill.register('formats/formula', CustomFormulaBlot, true)
|
||||
}
|
||||
|
||||
// 给指定的节点插入指定公式
|
||||
insertFormulaToNode(node, formula) {
|
||||
let richTextPlugin = this.mindMap.richText
|
||||
richTextPlugin.showEditText(node)
|
||||
richTextPlugin.quill.insertEmbed(
|
||||
richTextPlugin.quill.getLength() - 1,
|
||||
'formula',
|
||||
formula
|
||||
)
|
||||
richTextPlugin.setTextStyleIfNotRichText(richTextPlugin.node)
|
||||
richTextPlugin.hideEditText([node])
|
||||
}
|
||||
}
|
||||
|
||||
Formula.instanceName = 'formula'
|
||||
|
||||
export default Formula
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bfsWalk } from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
// 键盘导航类
|
||||
// 键盘导航插件
|
||||
class KeyboardNavigation {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
@@ -28,8 +28,7 @@ class KeyboardNavigation {
|
||||
this.focus(dir)
|
||||
} else {
|
||||
let root = this.mindMap.renderer.root
|
||||
this.mindMap.renderer.moveNodeToCenter(root)
|
||||
root.active()
|
||||
this.mindMap.execCommand('GO_TARGET_NODE', root)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +80,7 @@ class KeyboardNavigation {
|
||||
|
||||
// 找到了则让目标节点聚焦
|
||||
if (targetNode) {
|
||||
this.mindMap.renderer.moveNodeToCenter(targetNode)
|
||||
targetNode.active()
|
||||
this.mindMap.execCommand('GO_TARGET_NODE', targetNode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +94,7 @@ class KeyboardNavigation {
|
||||
// 遍历节点树
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
// 跳过当前聚焦的节点
|
||||
if (node === currentActiveNode) return
|
||||
if (node.uid === currentActiveNode.uid) return
|
||||
// 当前遍历到的节点的位置信息
|
||||
let rect = this.getNodeRect(node)
|
||||
let { left, top, right, bottom } = rect
|
||||
@@ -133,7 +131,7 @@ class KeyboardNavigation {
|
||||
checkNodeDis
|
||||
}) {
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
if (node === currentActiveNode) return
|
||||
if (node.uid === currentActiveNode.uid) return
|
||||
let rect = this.getNodeRect(node)
|
||||
let { left, top, right, bottom } = rect
|
||||
let match = false
|
||||
@@ -175,7 +173,7 @@ class KeyboardNavigation {
|
||||
let cX = (currentActiveNodeRect.right + currentActiveNodeRect.left) / 2
|
||||
let cY = (currentActiveNodeRect.bottom + currentActiveNodeRect.top) / 2
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
if (node === currentActiveNode) return
|
||||
if (node.uid === currentActiveNode.uid) return
|
||||
let rect = this.getNodeRect(node)
|
||||
let { left, top, right, bottom } = rect
|
||||
// 遍历到的节点的中心点
|
||||
@@ -234,4 +232,4 @@ class KeyboardNavigation {
|
||||
|
||||
KeyboardNavigation.instanceName = 'keyboardNavigation'
|
||||
|
||||
export default KeyboardNavigation
|
||||
export default KeyboardNavigation
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
// 小地图类
|
||||
import {
|
||||
isWhite,
|
||||
isTransparent,
|
||||
getVisibleColorFromTheme,
|
||||
readBlob
|
||||
} from '../utils/index'
|
||||
|
||||
// 小地图插件
|
||||
class MiniMap {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
@@ -20,9 +27,16 @@ class MiniMap {
|
||||
* boxHeight:小地图容器的高度
|
||||
*/
|
||||
calculationMiniMap(boxWidth, boxHeight) {
|
||||
let { svgHTML, rect, origWidth, origHeight, scaleX, scaleY } =
|
||||
this.mindMap.getSvgData()
|
||||
let { svg, rect, origWidth, origHeight, scaleX, scaleY } =
|
||||
this.mindMap.getSvgData({
|
||||
ignoreWatermark: true
|
||||
})
|
||||
// 计算数据
|
||||
const elRect = this.mindMap.elRect
|
||||
rect.x -= elRect.left
|
||||
rect.x2 -= elRect.left
|
||||
rect.y -= elRect.top
|
||||
rect.y2 -= elRect.top
|
||||
let boxRatio = boxWidth / boxHeight
|
||||
let actWidth = 0
|
||||
let actHeight = 0
|
||||
@@ -53,20 +67,39 @@ class MiniMap {
|
||||
bottom: 0
|
||||
}
|
||||
viewBoxStyle.left =
|
||||
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft + 'px'
|
||||
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft
|
||||
viewBoxStyle.right =
|
||||
Math.max(0, ((_rectX2 - origWidth) / _rectWidth) * actWidth) +
|
||||
miniMapBoxLeft +
|
||||
'px'
|
||||
miniMapBoxLeft
|
||||
|
||||
viewBoxStyle.top =
|
||||
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop + 'px'
|
||||
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop
|
||||
viewBoxStyle.bottom =
|
||||
Math.max(0, ((_rectY2 - origHeight) / _rectHeight) * actHeight) +
|
||||
miniMapBoxTop +
|
||||
'px'
|
||||
miniMapBoxTop
|
||||
|
||||
if (viewBoxStyle.top > miniMapBoxTop + actHeight) {
|
||||
viewBoxStyle.top = miniMapBoxTop + actHeight
|
||||
}
|
||||
if (viewBoxStyle.left > miniMapBoxLeft + actWidth) {
|
||||
viewBoxStyle.left = miniMapBoxLeft + actWidth
|
||||
}
|
||||
|
||||
Object.keys(viewBoxStyle).forEach(key => {
|
||||
viewBoxStyle[key] = viewBoxStyle[key] + 'px'
|
||||
})
|
||||
this.removeNodeContent(svg)
|
||||
const svgStr = svg.svg()
|
||||
|
||||
return {
|
||||
svgHTML, // 小地图html
|
||||
getImgUrl: async callback => {
|
||||
const blob = new Blob([svgStr], {
|
||||
type: 'image/svg+xml'
|
||||
})
|
||||
const res = await readBlob(blob)
|
||||
callback(res)
|
||||
},
|
||||
svgHTML: svgStr, // 小地图html
|
||||
viewBoxStyle, // 视图框的位置信息
|
||||
miniMapBoxScale, // 视图框的缩放值
|
||||
miniMapBoxLeft, // 视图框的left值
|
||||
@@ -74,6 +107,26 @@ class MiniMap {
|
||||
}
|
||||
}
|
||||
|
||||
// 移除节点的内容
|
||||
removeNodeContent(svg) {
|
||||
if (svg.hasClass('smm-node')) {
|
||||
let shape = svg.findOne('.smm-node-shape')
|
||||
let fill = shape.attr('fill')
|
||||
if (isWhite(fill) || isTransparent(fill)) {
|
||||
shape.attr('fill', getVisibleColorFromTheme(this.mindMap.themeConfig))
|
||||
}
|
||||
svg.clear()
|
||||
svg.add(shape)
|
||||
return
|
||||
}
|
||||
let children = svg.children()
|
||||
if (children && children.length > 0) {
|
||||
children.forEach(node => {
|
||||
this.removeNodeContent(node)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 小地图鼠标按下事件
|
||||
onMousedown(e) {
|
||||
this.isMousedown = true
|
||||
|
||||
275
simple-mind-map/src/plugins/NodeImgAdjust.js
Normal file
275
simple-mind-map/src/plugins/NodeImgAdjust.js
Normal file
@@ -0,0 +1,275 @@
|
||||
// 节点图片大小调整插件
|
||||
import { resizeImgSizeByOriginRatio } from '../utils/index'
|
||||
import btnsSvg from '../svg/btns'
|
||||
|
||||
class NodeImgAdjust {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
this.mindMap = mindMap
|
||||
this.resizeBtnSize = 26 // 调整按钮的大小
|
||||
this.handleEl = null // 自定义元素,用来渲染临时图片、调整按钮
|
||||
this.isShowHandleEl = false // 自定义元素是否在显示中
|
||||
this.node = null // 当前节点实例
|
||||
this.img = null // 当前节点的图片节点
|
||||
this.rect = null // 当前图片节点的尺寸信息
|
||||
this.isMousedown = false // 当前是否是按住调整按钮状态
|
||||
this.currentImgWidth = 0 // 当前拖拽实时图片的大小
|
||||
this.currentImgHeight = 0
|
||||
this.isAdjusted = false // 是否是拖拽结束后的渲染期间
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
bindEvent() {
|
||||
this.onNodeImgMouseleave = this.onNodeImgMouseleave.bind(this)
|
||||
this.onNodeImgMousemove = this.onNodeImgMousemove.bind(this)
|
||||
this.onMousemove = this.onMousemove.bind(this)
|
||||
this.onMouseup = this.onMouseup.bind(this)
|
||||
this.onRenderEnd = this.onRenderEnd.bind(this)
|
||||
this.mindMap.on('node_img_mouseleave', this.onNodeImgMouseleave)
|
||||
this.mindMap.on('node_img_mousemove', this.onNodeImgMousemove)
|
||||
this.mindMap.on('mousemove', this.onMousemove)
|
||||
this.mindMap.on('mouseup', this.onMouseup)
|
||||
this.mindMap.on('node_mouseup', this.onMouseup)
|
||||
this.mindMap.on('node_tree_render_end', this.onRenderEnd)
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
unBindEvent() {
|
||||
this.mindMap.off('node_img_mouseleave', this.onNodeImgMouseleave)
|
||||
this.mindMap.off('node_img_mousemove', this.onNodeImgMousemove)
|
||||
this.mindMap.off('mousemove', this.onMousemove)
|
||||
this.mindMap.off('mouseup', this.onMouseup)
|
||||
this.mindMap.off('node_mouseup', this.onMouseup)
|
||||
this.mindMap.off('node_tree_render_end', this.onRenderEnd)
|
||||
}
|
||||
|
||||
// 节点图片鼠标移动事件
|
||||
onNodeImgMousemove(node, img) {
|
||||
// 如果当前正在拖动调整中那么直接返回
|
||||
if (this.isMousedown || this.isAdjusted || this.mindMap.opt.readonly) return
|
||||
// 如果在当前节点内移动,以及自定义元素已经是显示状态,那么直接返回
|
||||
if (this.node && this.node.uid === node.uid && this.isShowHandleEl) return
|
||||
// 更新当前节点信息
|
||||
this.node = node
|
||||
this.img = img
|
||||
this.rect = this.img.rbox()
|
||||
// 显示自定义元素
|
||||
this.showHandleEl()
|
||||
}
|
||||
|
||||
// 节点图片鼠标移出事件
|
||||
onNodeImgMouseleave() {
|
||||
if (this.isMousedown) return
|
||||
this.hideHandleEl()
|
||||
}
|
||||
|
||||
// 隐藏节点实际的图片
|
||||
hideNodeImage() {
|
||||
if (!this.img) return
|
||||
this.img.hide()
|
||||
}
|
||||
|
||||
// 显示节点实际的图片
|
||||
showNodeImage() {
|
||||
if (!this.img) return
|
||||
this.img.show()
|
||||
}
|
||||
|
||||
// 显示自定义元素
|
||||
showHandleEl() {
|
||||
if (!this.handleEl) {
|
||||
this.createResizeBtnEl()
|
||||
}
|
||||
this.setHandleElRect()
|
||||
this.handleEl.style.display = 'block'
|
||||
this.isShowHandleEl = true
|
||||
}
|
||||
|
||||
// 隐藏自定义元素
|
||||
hideHandleEl() {
|
||||
if (!this.isShowHandleEl) return
|
||||
this.isShowHandleEl = false
|
||||
this.handleEl.style.display = 'none'
|
||||
this.handleEl.style.backgroundImage = ``
|
||||
this.handleEl.style.width = 0
|
||||
this.handleEl.style.height = 0
|
||||
this.handleEl.style.left = 0
|
||||
this.handleEl.style.top = 0
|
||||
}
|
||||
|
||||
// 设置自定义元素尺寸位置信息
|
||||
setHandleElRect() {
|
||||
let { width, height, x, y } = this.rect
|
||||
this.handleEl.style.left = `${x}px`
|
||||
this.handleEl.style.top = `${y}px`
|
||||
this.currentImgWidth = width
|
||||
this.currentImgHeight = height
|
||||
this.updateHandleElSize()
|
||||
}
|
||||
|
||||
// 更新自定义元素宽高
|
||||
updateHandleElSize() {
|
||||
this.handleEl.style.width = `${this.currentImgWidth}px`
|
||||
this.handleEl.style.height = `${this.currentImgHeight}px`
|
||||
}
|
||||
|
||||
// 创建调整按钮元素
|
||||
createResizeBtnEl() {
|
||||
// 容器元素
|
||||
this.handleEl = document.createElement('div')
|
||||
this.handleEl.style.cssText = `
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
display:none;
|
||||
background-size: cover;
|
||||
`
|
||||
this.handleEl.className = 'node-img-handle'
|
||||
// 调整按钮元素
|
||||
const btnEl = document.createElement('div')
|
||||
btnEl.innerHTML = btnsSvg.imgAdjust
|
||||
btnEl.style.cssText = `
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: auto;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
width: ${this.resizeBtnSize}px;
|
||||
height: ${this.resizeBtnSize}px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: nwse-resize;
|
||||
`
|
||||
btnEl.className = 'node-image-resize'
|
||||
// 给按钮元素绑定事件
|
||||
btnEl.addEventListener('mouseenter', () => {
|
||||
// 移入按钮,会触发节点图片的移出事件,所以需要再次显示按钮
|
||||
this.showHandleEl()
|
||||
})
|
||||
btnEl.addEventListener('mouseleave', () => {
|
||||
// 移除按钮,需要隐藏按钮
|
||||
if (this.isMousedown) return
|
||||
this.hideHandleEl()
|
||||
})
|
||||
btnEl.addEventListener('mousedown', e => {
|
||||
e.stopPropagation()
|
||||
this.onMousedown(e)
|
||||
})
|
||||
btnEl.addEventListener('mouseup', e => {
|
||||
setTimeout(() => {
|
||||
//点击后直接松开异常处理; 其他事件响应之后处理
|
||||
this.hideHandleEl()
|
||||
this.isAdjusted = false
|
||||
}, 0)
|
||||
})
|
||||
btnEl.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.handleEl.appendChild(btnEl)
|
||||
// 删除按钮
|
||||
const btnRemove = document.createElement('div')
|
||||
this.handleEl.prepend(btnRemove)
|
||||
btnRemove.className = 'node-image-remove'
|
||||
btnRemove.innerHTML = btnsSvg.remove
|
||||
btnRemove.style.cssText = `
|
||||
position: absolute;
|
||||
right: 0;top:0;color:#fff;
|
||||
pointer-events: auto;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
width: ${this.resizeBtnSize}px;
|
||||
height: ${this.resizeBtnSize}px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
`
|
||||
btnRemove.addEventListener('mouseenter', e => {
|
||||
this.showHandleEl()
|
||||
})
|
||||
btnRemove.addEventListener('mouseleave', e => {
|
||||
if (this.isMousedown) return
|
||||
this.hideHandleEl()
|
||||
})
|
||||
btnRemove.addEventListener('click', e => {
|
||||
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null })
|
||||
})
|
||||
// 添加元素到页面
|
||||
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.handleEl)
|
||||
}
|
||||
|
||||
// 鼠标按钮按下事件
|
||||
onMousedown() {
|
||||
this.isMousedown = true
|
||||
// 隐藏节点实际图片
|
||||
this.hideNodeImage()
|
||||
// 将节点图片渲染到自定义元素上
|
||||
this.handleEl.style.backgroundImage = `url(${this.node.getData('image')})`
|
||||
}
|
||||
|
||||
// 鼠标移动
|
||||
onMousemove(e) {
|
||||
if (!this.isMousedown) return
|
||||
e.preventDefault()
|
||||
// 计算当前拖拽位置对应的图片的实时大小
|
||||
let { width: imageOriginWidth, height: imageOriginHeight } =
|
||||
this.node.getData('imageSize')
|
||||
let newWidth = e.clientX - this.rect.x
|
||||
let newHeight = e.clientY - this.rect.y
|
||||
if (newWidth <= 0 || newHeight <= 0) return
|
||||
let [actWidth, actHeight] = resizeImgSizeByOriginRatio(
|
||||
imageOriginWidth,
|
||||
imageOriginHeight,
|
||||
newWidth,
|
||||
newHeight
|
||||
)
|
||||
this.currentImgWidth = actWidth
|
||||
this.currentImgHeight = actHeight
|
||||
this.updateHandleElSize()
|
||||
}
|
||||
|
||||
// 鼠标松开
|
||||
onMouseup() {
|
||||
if (!this.isMousedown) return
|
||||
// 显示节点实际图片
|
||||
this.showNodeImage()
|
||||
// 隐藏自定义元素
|
||||
this.hideHandleEl()
|
||||
// 更新节点图片为新的大小
|
||||
let { image, imageTitle } = this.node.getData()
|
||||
let { scaleX, scaleY } = this.mindMap.draw.transform()
|
||||
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, {
|
||||
url: image,
|
||||
title: imageTitle,
|
||||
width: this.currentImgWidth / scaleX,
|
||||
height: this.currentImgHeight / scaleY,
|
||||
custom: true // 代表自定义了图片大小
|
||||
})
|
||||
this.isAdjusted = true
|
||||
this.isMousedown = false
|
||||
}
|
||||
|
||||
// 渲染完成事件
|
||||
onRenderEnd() {
|
||||
if (!this.isAdjusted) {
|
||||
this.hideHandleEl()
|
||||
return
|
||||
}
|
||||
this.isAdjusted = false
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
NodeImgAdjust.instanceName = 'nodeImgAdjust'
|
||||
|
||||
export default NodeImgAdjust
|
||||
81
simple-mind-map/src/plugins/Painter.js
Normal file
81
simple-mind-map/src/plugins/Painter.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { checkIsNodeStyleDataKey } from '../utils/index'
|
||||
|
||||
// 格式刷插件
|
||||
class Painter {
|
||||
constructor({ mindMap }) {
|
||||
this.mindMap = mindMap
|
||||
this.isInPainter = false
|
||||
this.painterNode = null
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
bindEvent() {
|
||||
this.painterOneNode = this.painterOneNode.bind(this)
|
||||
this.onEndPainter = this.onEndPainter.bind(this)
|
||||
this.mindMap.on('node_click', this.painterOneNode)
|
||||
this.mindMap.on('draw_click', this.onEndPainter)
|
||||
}
|
||||
|
||||
unBindEvent() {
|
||||
this.mindMap.off('node_click', this.painterOneNode)
|
||||
this.mindMap.off('draw_click', this.onEndPainter)
|
||||
}
|
||||
|
||||
// 开始格式刷
|
||||
startPainter() {
|
||||
if (this.mindMap.opt.readonly) return
|
||||
let activeNodeList = this.mindMap.renderer.activeNodeList
|
||||
if (activeNodeList.length <= 0) return
|
||||
this.painterNode = activeNodeList[0]
|
||||
this.isInPainter = true
|
||||
this.mindMap.emit('painter_start')
|
||||
}
|
||||
|
||||
// 结束格式刷
|
||||
endPainter() {
|
||||
this.painterNode = null
|
||||
this.isInPainter = false
|
||||
}
|
||||
|
||||
onEndPainter() {
|
||||
this.endPainter()
|
||||
this.mindMap.emit('painter_end')
|
||||
}
|
||||
|
||||
// 格式刷某个节点
|
||||
painterOneNode(node) {
|
||||
if (
|
||||
!node ||
|
||||
!this.isInPainter ||
|
||||
!this.painterNode ||
|
||||
!node ||
|
||||
node.uid === this.painterNode.uid
|
||||
)
|
||||
return
|
||||
const style = {}
|
||||
const painterNodeData = this.painterNode.getData()
|
||||
Object.keys(painterNodeData).forEach(key => {
|
||||
if (checkIsNodeStyleDataKey(key)) {
|
||||
style[key] = painterNodeData[key]
|
||||
}
|
||||
})
|
||||
node.setStyles(style)
|
||||
if (painterNodeData.activeStyle) {
|
||||
node.setStyles(painterNodeData.activeStyle, true)
|
||||
}
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
Painter.instanceName = 'painter'
|
||||
|
||||
export default Painter
|
||||
@@ -1,7 +1,13 @@
|
||||
import Quill from 'quill'
|
||||
import Delta from 'quill-delta'
|
||||
import 'quill/dist/quill.snow.css'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { walk, getTextFromHtml } from '../utils'
|
||||
import {
|
||||
walk,
|
||||
getTextFromHtml,
|
||||
isWhite,
|
||||
getVisibleColorFromTheme,
|
||||
isUndef
|
||||
} from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
let extended = false
|
||||
@@ -28,7 +34,7 @@ let fontSizeList = new Array(100).fill(0).map((_, index) => {
|
||||
return index + 'px'
|
||||
})
|
||||
|
||||
// 节点支持富文本编辑功能
|
||||
// 富文本编辑插件
|
||||
class RichText {
|
||||
constructor({ mindMap, pluginOpt }) {
|
||||
this.mindMap = mindMap
|
||||
@@ -38,7 +44,9 @@ class RichText {
|
||||
this.quill = null
|
||||
this.range = null
|
||||
this.lastRange = null
|
||||
this.pasteUseRange = null
|
||||
this.node = null
|
||||
this.isInserting = false
|
||||
this.styleEl = null
|
||||
this.cacheEditingText = ''
|
||||
this.lostStyle = false
|
||||
@@ -145,11 +153,19 @@ class RichText {
|
||||
}
|
||||
|
||||
// 显示文本编辑控件
|
||||
showEditText(node, rect) {
|
||||
showEditText(node, rect, isInserting, isFromKeyDown) {
|
||||
if (this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
const {
|
||||
richTextEditFakeInPlace,
|
||||
customInnerElsAppendTo,
|
||||
nodeTextEditZIndex,
|
||||
textAutoWrapWidth,
|
||||
selectTextOnEnterEditText
|
||||
} = this.mindMap.opt
|
||||
this.node = node
|
||||
this.isInserting = isInserting
|
||||
if (!rect) rect = node._textData.node.node.getBoundingClientRect()
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
this.mindMap.renderer.textEdit.registerTmpShortcut()
|
||||
@@ -161,47 +177,86 @@ class RichText {
|
||||
let scaleX = rect.width / originWidth
|
||||
let scaleY = rect.height / originHeight
|
||||
// 内边距
|
||||
const paddingX = 6
|
||||
const paddingY = 4
|
||||
let paddingX = 6
|
||||
let paddingY = 4
|
||||
if (richTextEditFakeInPlace) {
|
||||
let paddingValue = node.getPaddingVale()
|
||||
paddingX = paddingValue.paddingX
|
||||
paddingY = paddingValue.paddingY
|
||||
}
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.classList.add('smm-richtext-node-edit-wrap')
|
||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;box-shadow: 0 0 20px rgba(0,0,0,.5);outline: none; word-break: break-all;padding: ${paddingY}px ${paddingX}px;`
|
||||
this.textEditNode.style.cssText = `
|
||||
position:fixed;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,.5);
|
||||
outline: none;
|
||||
word-break:
|
||||
break-all;padding: ${paddingY}px ${paddingX}px;
|
||||
`
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
this.textEditNode.addEventListener('mousedown', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.textEditNode.addEventListener('keydown', e => {
|
||||
if (this.mindMap.renderer.textEdit.checkIsAutoEnterTextEditKey(e)) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
})
|
||||
const targetNode = customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
// 使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
|
||||
let bgColor = node.style.merge('fillColor')
|
||||
let color = node.style.merge('color')
|
||||
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
|
||||
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
|
||||
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
|
||||
this.textEditNode.style.zIndex = nodeTextEditZIndex
|
||||
this.textEditNode.style.backgroundColor =
|
||||
bgColor === 'transparent' ? '#fff' : bgColor
|
||||
bgColor === 'transparent'
|
||||
? isWhite(color)
|
||||
? getVisibleColorFromTheme(this.mindMap.themeConfig)
|
||||
: '#fff'
|
||||
: bgColor
|
||||
this.textEditNode.style.minWidth = originWidth + paddingX * 2 + 'px'
|
||||
this.textEditNode.style.minHeight = originHeight + 'px'
|
||||
this.textEditNode.style.left = rect.left + 'px'
|
||||
this.textEditNode.style.top = rect.top + 'px'
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.textEditNode.style.maxWidth =
|
||||
this.mindMap.opt.textAutoWrapWidth + paddingX * 2 + 'px'
|
||||
this.textEditNode.style.maxWidth = textAutoWrapWidth + paddingX * 2 + 'px'
|
||||
this.textEditNode.style.transform = `scale(${scaleX}, ${scaleY})`
|
||||
this.textEditNode.style.transformOrigin = 'left top'
|
||||
if (!node.nodeData.data.richText) {
|
||||
if (richTextEditFakeInPlace) {
|
||||
this.textEditNode.style.borderRadius =
|
||||
(node.style.merge('borderRadius') || 5) + 'px'
|
||||
if (node.style.merge('shape') == 'roundedRectangle') {
|
||||
this.textEditNode.style.borderRadius = (node.height || 50) + 'px'
|
||||
}
|
||||
}
|
||||
if (!node.getData('richText')) {
|
||||
// 还不是富文本的情况
|
||||
let text = node.nodeData.data.text.split(/\n/gim).join('<br>')
|
||||
let text = ''
|
||||
if (!isUndef(node.getData('text'))) {
|
||||
text = String(node.getData('text')).split(/\n/gim).join('<br>')
|
||||
}
|
||||
let html = `<p>${text}</p>`
|
||||
this.textEditNode.innerHTML = this.cacheEditingText || html
|
||||
} else {
|
||||
this.textEditNode.innerHTML =
|
||||
this.cacheEditingText || node.nodeData.data.text
|
||||
this.cacheEditingText || node.getData('text')
|
||||
}
|
||||
this.initQuillEditor()
|
||||
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
|
||||
this.showTextEdit = true
|
||||
this.focus()
|
||||
if (!node.nodeData.data.richText) {
|
||||
// 如果是刚创建的节点,那么默认全选,否则普通激活不全选,除非selectTextOnEnterEditText配置为true
|
||||
// 在selectTextOnEnterEditText时,如果是在keydown事件进入的节点编辑,也不需要全选
|
||||
this.focus(
|
||||
isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null
|
||||
)
|
||||
if (!node.getData('richText')) {
|
||||
// 如果是非富文本的情况,需要手动应用文本样式
|
||||
this.setTextStyleIfNotRichText(node)
|
||||
}
|
||||
@@ -250,6 +305,7 @@ class RichText {
|
||||
this.showTextEdit = false
|
||||
this.mindMap.emit('rich_text_selection_change', false)
|
||||
this.node = null
|
||||
this.isInserting = false
|
||||
}
|
||||
|
||||
// 初始化Quill富文本编辑器
|
||||
@@ -264,6 +320,12 @@ class RichText {
|
||||
handler: function () {
|
||||
// 覆盖默认的回车键换行
|
||||
}
|
||||
},
|
||||
tab: {
|
||||
key: 9,
|
||||
handler: function () {
|
||||
// 覆盖默认的tab键
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,9 +333,12 @@ class RichText {
|
||||
theme: 'snow'
|
||||
})
|
||||
this.quill.on('selection-change', range => {
|
||||
// 刚创建的节点全选不需要显示操作条
|
||||
if (this.isInserting) return
|
||||
this.lastRange = this.range
|
||||
this.range = null
|
||||
if (range) {
|
||||
this.pasteUseRange = range
|
||||
let bounds = this.quill.getBounds(range.index, range.length)
|
||||
let rect = this.textEditNode.getBoundingClientRect()
|
||||
let rectInfo = {
|
||||
@@ -313,6 +378,38 @@ class RichText {
|
||||
this.lostStyle = false
|
||||
}
|
||||
})
|
||||
// 拦截粘贴,只允许粘贴纯文本
|
||||
this.quill.clipboard.addMatcher(Node.TEXT_NODE, node => {
|
||||
let style = this.getPasteTextStyle()
|
||||
return new Delta().insert(node.data, style)
|
||||
})
|
||||
this.quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
|
||||
let ops = []
|
||||
let style = this.getPasteTextStyle()
|
||||
delta.ops.forEach(op => {
|
||||
// 过滤出文本内容,过滤掉换行
|
||||
if (op.insert && typeof op.insert === 'string' && op.insert !== '\n') {
|
||||
ops.push({
|
||||
attributes: { ...style },
|
||||
insert: op.insert
|
||||
})
|
||||
}
|
||||
})
|
||||
delta.ops = ops
|
||||
return delta
|
||||
})
|
||||
}
|
||||
|
||||
// 获取粘贴的文本的样式
|
||||
getPasteTextStyle() {
|
||||
// 粘贴的数据使用当前光标位置处的文本样式
|
||||
if (this.pasteUseRange) {
|
||||
return this.quill.getFormat(
|
||||
this.pasteUseRange.index,
|
||||
this.pasteUseRange.length
|
||||
)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// 正则输入中文
|
||||
@@ -325,10 +422,13 @@ class RichText {
|
||||
|
||||
// 中文输入结束
|
||||
onCompositionEnd() {
|
||||
if (!this.showTextEdit || !this.lostStyle) {
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
this.isCompositing = false
|
||||
if (!this.lostStyle) {
|
||||
return
|
||||
}
|
||||
this.setTextStyleIfNotRichText(this.node)
|
||||
}
|
||||
|
||||
@@ -338,9 +438,9 @@ class RichText {
|
||||
}
|
||||
|
||||
// 聚焦
|
||||
focus() {
|
||||
focus(start) {
|
||||
let len = this.quill.getLength()
|
||||
this.quill.setSelection(len, len)
|
||||
this.quill.setSelection(typeof start === 'number' ? start : len, len)
|
||||
}
|
||||
|
||||
// 格式化当前选中的文本
|
||||
@@ -397,7 +497,7 @@ class RichText {
|
||||
})
|
||||
} else {
|
||||
let data = this.richTextStyleToNormalStyle(config)
|
||||
this.mindMap.renderer.setNodeData(this.node, data)
|
||||
this.mindMap.execCommand('SET_NODE_DATA', this.node, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +567,16 @@ class RichText {
|
||||
return data
|
||||
}
|
||||
|
||||
// 给未激活的节点设置富文本样式
|
||||
setNotActiveNodeStyle(node, style) {
|
||||
const config = this.normalStyleToRichTextStyle(style)
|
||||
if (Object.keys(config).length > 0) {
|
||||
this.showEditText(node)
|
||||
this.formatAllText(config)
|
||||
this.hideEditText([node])
|
||||
}
|
||||
}
|
||||
|
||||
// 处理导出为图片
|
||||
async handleExportPng(node) {
|
||||
let el = document.createElement('div')
|
||||
@@ -485,11 +595,16 @@ class RichText {
|
||||
}
|
||||
}
|
||||
walk(node)
|
||||
let canvas = await html2canvas(el, {
|
||||
backgroundColor: null
|
||||
})
|
||||
|
||||
// 如果使用html2canvas
|
||||
// let canvas = await html2canvas(el, {
|
||||
// backgroundColor: null
|
||||
// })
|
||||
// return canvas.toDataURL()
|
||||
|
||||
const res = await domtoimage.toPng(el)
|
||||
this.mindMap.el.removeChild(el)
|
||||
return canvas.toDataURL()
|
||||
return res
|
||||
}
|
||||
|
||||
// 将所有节点转换成非富文本节点
|
||||
@@ -518,7 +633,7 @@ class RichText {
|
||||
// 处理导入数据
|
||||
handleSetData(data) {
|
||||
let walk = root => {
|
||||
if (!root.data.richText) {
|
||||
if (root.data && !root.data.richText) {
|
||||
root.data.richText = true
|
||||
root.data.resetRichText = true
|
||||
}
|
||||
@@ -536,6 +651,13 @@ class RichText {
|
||||
beforePluginRemove() {
|
||||
this.transformAllNodesToNormalNode()
|
||||
document.head.removeChild(this.styleEl)
|
||||
this.unbindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
document.head.removeChild(this.styleEl)
|
||||
this.unbindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
258
simple-mind-map/src/plugins/Scrollbar.js
Normal file
258
simple-mind-map/src/plugins/Scrollbar.js
Normal file
@@ -0,0 +1,258 @@
|
||||
import { throttle } from '../utils/index'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
// 滚动条插件
|
||||
class Scrollbar {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
this.mindMap = opt.mindMap
|
||||
this.scrollbarWrapSize = {
|
||||
width: 0, // 水平滚动条的容器宽度
|
||||
height: 0 // 垂直滚动条的容器高度
|
||||
}
|
||||
// 思维导图实际高度
|
||||
this.chartHeight = 0
|
||||
this.chartWidth = 0
|
||||
this.reset()
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 复位数据
|
||||
reset() {
|
||||
// 当前拖拽的滚动条类型
|
||||
this.currentScrollType = ''
|
||||
this.isMousedown = false
|
||||
this.mousedownPos = {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
// 鼠标按下时,滚动条位置
|
||||
this.mousedownScrollbarPos = 0
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
this.onMousemove = this.onMousemove.bind(this)
|
||||
this.onMouseup = this.onMouseup.bind(this)
|
||||
this.updateScrollbar = this.updateScrollbar.bind(this)
|
||||
this.updateScrollbar = throttle(this.updateScrollbar, 16, this) // 加个节流
|
||||
this.mindMap.on('mousemove', this.onMousemove)
|
||||
this.mindMap.on('mouseup', this.onMouseup)
|
||||
this.mindMap.on('node_tree_render_end', this.updateScrollbar)
|
||||
this.mindMap.on('view_data_change', this.updateScrollbar)
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
unBindEvent() {
|
||||
this.mindMap.off('mousemove', this.onMousemove)
|
||||
this.mindMap.off('mouseup', this.onMouseup)
|
||||
this.mindMap.off('node_tree_render_end', this.updateScrollbar)
|
||||
this.mindMap.off('view_data_change', this.updateScrollbar)
|
||||
}
|
||||
|
||||
// 渲染后、数据改变需要更新滚动条
|
||||
updateScrollbar() {
|
||||
// 当前正在拖拽滚动条时不需要更新
|
||||
if (this.isMousedown) return
|
||||
const res = this.calculationScrollbar()
|
||||
this.emitEvent(res)
|
||||
}
|
||||
|
||||
// 发送滚动条改变事件
|
||||
emitEvent(data) {
|
||||
this.mindMap.emit('scrollbar_change', data)
|
||||
}
|
||||
|
||||
// 设置滚动条容器的大小,指滚动条容器的大小,对于水平滚动条,即宽度,对于垂直滚动条,即高度
|
||||
setScrollBarWrapSize(width, height) {
|
||||
this.scrollbarWrapSize.width = width
|
||||
this.scrollbarWrapSize.height = height
|
||||
}
|
||||
|
||||
// 计算滚动条大小和位置
|
||||
calculationScrollbar() {
|
||||
const rect = this.mindMap.draw.rbox()
|
||||
// 减去画布距离浏览器窗口左上角的距离
|
||||
const elRect = this.mindMap.elRect
|
||||
rect.x -= elRect.left
|
||||
rect.y -= elRect.top
|
||||
|
||||
// 垂直滚动条
|
||||
const canvasHeight = this.mindMap.height // 画布高度
|
||||
const paddingY = canvasHeight / 2 // 首尾允许超出的距离,默认为高度的一半
|
||||
const chartHeight = rect.height + paddingY * 2 // 思维导图高度
|
||||
this.chartHeight = chartHeight
|
||||
const chartTop = rect.y - paddingY // 思维导图顶部距画布顶部的距离
|
||||
const height = Math.min((canvasHeight / chartHeight) * 100, 100) // 滚动条高度 = 画布高度 / 思维导图高度
|
||||
let top = (-chartTop / chartHeight) * 100 // 滚动条距离 = 思维导图顶部距画布顶部的距离 / 思维导图高度
|
||||
// 判断是否到达边界
|
||||
if (top < 0) {
|
||||
top = 0
|
||||
}
|
||||
if (top > 100 - height) {
|
||||
top = 100 - height
|
||||
}
|
||||
|
||||
// 水平滚动条
|
||||
const canvasWidth = this.mindMap.width
|
||||
const paddingX = canvasWidth / 2
|
||||
const chartWidth = rect.width + paddingX * 2
|
||||
this.chartWidth = chartWidth
|
||||
const chartLeft = rect.x - paddingX
|
||||
const width = Math.min((canvasWidth / chartWidth) * 100, 100)
|
||||
let left = (-chartLeft / chartWidth) * 100
|
||||
if (left < 0) {
|
||||
left = 0
|
||||
}
|
||||
if (left > 100 - width) {
|
||||
left = 100 - width
|
||||
}
|
||||
|
||||
const res = {
|
||||
// 垂直滚动条
|
||||
vertical: {
|
||||
top,
|
||||
height
|
||||
},
|
||||
// 水平滚动条
|
||||
horizontal: {
|
||||
left,
|
||||
width
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// 滚动条鼠标按下事件处理函数
|
||||
onMousedown(e, type) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
this.currentScrollType = type
|
||||
this.isMousedown = true
|
||||
this.mousedownPos = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
}
|
||||
// 保存滚动条当前的位置
|
||||
const styles = window.getComputedStyle(e.target)
|
||||
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
|
||||
this.mousedownScrollbarPos = Number.parseFloat(styles.top)
|
||||
} else {
|
||||
this.mousedownScrollbarPos = Number.parseFloat(styles.left)
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标移动事件处理函数
|
||||
onMousemove(e) {
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (this.currentScrollType === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
|
||||
const oy = e.clientY - this.mousedownPos.y + this.mousedownScrollbarPos
|
||||
this.updateMindMapView(CONSTANTS.SCROLL_BAR_DIR.VERTICAL, oy)
|
||||
} else {
|
||||
const ox = e.clientX - this.mousedownPos.x + this.mousedownScrollbarPos
|
||||
this.updateMindMapView(CONSTANTS.SCROLL_BAR_DIR.HORIZONTAL, ox)
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标松开事件处理函数
|
||||
onMouseup() {
|
||||
this.isMousedown = false
|
||||
this.reset()
|
||||
}
|
||||
|
||||
// 更新视图
|
||||
updateMindMapView(type, offset) {
|
||||
const scrollbarData = this.calculationScrollbar()
|
||||
const t = this.mindMap.draw.transform()
|
||||
const drawRect = this.mindMap.draw.rbox()
|
||||
const rootRect = this.mindMap.renderer.root.group.rbox()
|
||||
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
|
||||
// 滚动条新位置
|
||||
let oy = offset
|
||||
// 判断是否达到首尾
|
||||
if (oy <= 0) {
|
||||
oy = 0
|
||||
}
|
||||
let max =
|
||||
((100 - scrollbarData.vertical.height) / 100) *
|
||||
this.scrollbarWrapSize.height
|
||||
if (oy >= max) {
|
||||
oy = max
|
||||
}
|
||||
// 转换成百分比
|
||||
const oyPercentage = (oy / this.scrollbarWrapSize.height) * 100
|
||||
// 转换成相对于图形高度的距离
|
||||
const oyPx = (-oyPercentage / 100) * this.chartHeight
|
||||
// 节点中心点到图形最上方的距离
|
||||
const yOffset = rootRect.y - drawRect.y
|
||||
// 内边距
|
||||
const paddingY = this.mindMap.height / 2
|
||||
// 图形新位置
|
||||
let chartTop = oyPx + yOffset - paddingY * t.scaleY + paddingY
|
||||
this.mindMap.view.translateYTo(chartTop)
|
||||
this.emitEvent({
|
||||
horizontal: scrollbarData.horizontal,
|
||||
vertical: {
|
||||
top: oyPercentage,
|
||||
height: scrollbarData.vertical.height
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 滚动条新位置
|
||||
let ox = offset
|
||||
// 判断是否达到首尾
|
||||
if (ox <= 0) {
|
||||
ox = 0
|
||||
}
|
||||
let max =
|
||||
((100 - scrollbarData.horizontal.width) / 100) *
|
||||
this.scrollbarWrapSize.width
|
||||
if (ox >= max) {
|
||||
ox = max
|
||||
}
|
||||
// 转换成百分比
|
||||
const oxPercentage = (ox / this.scrollbarWrapSize.width) * 100
|
||||
// 转换成相对于图形高度的距离
|
||||
const oxPx = (-oxPercentage / 100) * this.chartWidth
|
||||
// 节点中心点到图形最左边的距离
|
||||
const xOffset = rootRect.x - drawRect.x
|
||||
// 内边距
|
||||
const paddingX = this.mindMap.width / 2
|
||||
// 图形新位置
|
||||
let chartLeft = oxPx + xOffset - paddingX * t.scaleX + paddingX
|
||||
this.mindMap.view.translateXTo(chartLeft)
|
||||
this.emitEvent({
|
||||
vertical: scrollbarData.vertical,
|
||||
horizontal: {
|
||||
left: oxPercentage,
|
||||
width: scrollbarData.horizontal.width
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动条的点击事件
|
||||
onClick(e, type) {
|
||||
let offset = 0
|
||||
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
|
||||
offset = e.clientY - e.currentTarget.getBoundingClientRect().top
|
||||
} else {
|
||||
offset = e.clientX - e.currentTarget.getBoundingClientRect().left
|
||||
}
|
||||
this.updateMindMapView(type, offset)
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
Scrollbar.instanceName = 'scrollbar'
|
||||
|
||||
export default Scrollbar
|
||||
177
simple-mind-map/src/plugins/Search.js
Normal file
177
simple-mind-map/src/plugins/Search.js
Normal file
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
bfsWalk,
|
||||
getTextFromHtml,
|
||||
isUndef,
|
||||
replaceHtmlText
|
||||
} from '../utils/index'
|
||||
|
||||
// 搜索插件
|
||||
class Search {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
this.mindMap = mindMap
|
||||
// 是否正在搜索
|
||||
this.isSearching = false
|
||||
// 搜索文本
|
||||
this.searchText = ''
|
||||
// 匹配的节点列表
|
||||
this.matchNodeList = []
|
||||
// 当前所在的节点列表索引
|
||||
this.currentIndex = -1
|
||||
// 不要复位搜索文本
|
||||
this.notResetSearchText = false
|
||||
// 是否自动跳转下一个匹配节点
|
||||
this.isJumpNext = false
|
||||
this.onDataChange = this.onDataChange.bind(this)
|
||||
this.mindMap.on('data_change', this.onDataChange)
|
||||
}
|
||||
|
||||
// 节点数据改变了,需要重新搜索
|
||||
onDataChange() {
|
||||
if (this.isJumpNext) {
|
||||
this.isJumpNext = false
|
||||
this.search(this.searchText)
|
||||
return
|
||||
}
|
||||
if (this.notResetSearchText) {
|
||||
this.notResetSearchText = false
|
||||
return
|
||||
}
|
||||
this.searchText = ''
|
||||
}
|
||||
|
||||
// 搜索
|
||||
search(text, callback = () => {}) {
|
||||
if (isUndef(text)) return this.endSearch()
|
||||
text = String(text)
|
||||
this.isSearching = true
|
||||
if (this.searchText === text) {
|
||||
// 和上一次搜索文本一样,那么搜索下一个
|
||||
this.searchNext(callback)
|
||||
} else {
|
||||
// 和上次搜索文本不一样,那么重新开始
|
||||
this.searchText = text
|
||||
this.doSearch()
|
||||
this.searchNext(callback)
|
||||
}
|
||||
this.emitEvent()
|
||||
}
|
||||
|
||||
// 结束搜索
|
||||
endSearch() {
|
||||
if (!this.isSearching) return
|
||||
this.searchText = ''
|
||||
this.matchNodeList = []
|
||||
this.currentIndex = -1
|
||||
this.notResetSearchText = false
|
||||
this.isSearching = false
|
||||
this.emitEvent()
|
||||
}
|
||||
|
||||
// 搜索匹配的节点
|
||||
doSearch() {
|
||||
this.matchNodeList = []
|
||||
this.currentIndex = -1
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
let { richText, text } = node.getData()
|
||||
if (richText) {
|
||||
text = getTextFromHtml(text)
|
||||
}
|
||||
if (text.includes(this.searchText)) {
|
||||
this.matchNodeList.push(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索下一个,定位到下一个匹配节点
|
||||
searchNext(callback) {
|
||||
if (!this.isSearching || this.matchNodeList.length <= 0) return
|
||||
if (this.currentIndex < this.matchNodeList.length - 1) {
|
||||
this.currentIndex++
|
||||
} else {
|
||||
this.currentIndex = 0
|
||||
}
|
||||
let currentNode = this.matchNodeList[this.currentIndex]
|
||||
this.notResetSearchText = true
|
||||
this.mindMap.execCommand('GO_TARGET_NODE', currentNode, () => {
|
||||
this.notResetSearchText = false
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
// 替换当前节点
|
||||
replace(replaceText, jumpNext = false) {
|
||||
if (
|
||||
replaceText === null ||
|
||||
replaceText === undefined ||
|
||||
!this.isSearching ||
|
||||
this.matchNodeList.length <= 0
|
||||
)
|
||||
return
|
||||
// 自动跳转下一个匹配节点
|
||||
this.isJumpNext = jumpNext
|
||||
replaceText = String(replaceText)
|
||||
let currentNode = this.matchNodeList[this.currentIndex]
|
||||
if (!currentNode) return
|
||||
let text = this.getReplacedText(currentNode, this.searchText, replaceText)
|
||||
this.notResetSearchText = true
|
||||
currentNode.setText(text, currentNode.getData('richText'), true)
|
||||
this.matchNodeList = this.matchNodeList.filter(node => {
|
||||
return currentNode !== node
|
||||
})
|
||||
if (this.currentIndex > this.matchNodeList.length - 1) {
|
||||
this.currentIndex = -1
|
||||
} else {
|
||||
this.currentIndex--
|
||||
}
|
||||
this.emitEvent()
|
||||
}
|
||||
|
||||
// 替换所有
|
||||
replaceAll(replaceText) {
|
||||
if (
|
||||
replaceText === null ||
|
||||
replaceText === undefined ||
|
||||
!this.isSearching ||
|
||||
this.matchNodeList.length <= 0
|
||||
)
|
||||
return
|
||||
replaceText = String(replaceText)
|
||||
this.matchNodeList.forEach(node => {
|
||||
let text = this.getReplacedText(node, this.searchText, replaceText)
|
||||
this.mindMap.renderer.setNodeDataRender(
|
||||
node,
|
||||
{
|
||||
text,
|
||||
resetRichText: !!node.getData('richText')
|
||||
},
|
||||
true
|
||||
)
|
||||
})
|
||||
this.mindMap.render()
|
||||
this.mindMap.command.addHistory()
|
||||
this.endSearch()
|
||||
}
|
||||
|
||||
// 获取某个节点替换后的文本
|
||||
getReplacedText(node, searchText, replaceText) {
|
||||
let { richText, text } = node.getData()
|
||||
if (richText) {
|
||||
return replaceHtmlText(text, searchText, replaceText)
|
||||
} else {
|
||||
return text.replaceAll(searchText, replaceText)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送事件
|
||||
emitEvent() {
|
||||
this.mindMap.emit('search_info_change', {
|
||||
currentIndex: this.currentIndex,
|
||||
total: this.matchNodeList.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Search.instanceName = 'search'
|
||||
|
||||
export default Search
|
||||
@@ -1,7 +1,6 @@
|
||||
import { bfsWalk, throttle } from '../utils'
|
||||
|
||||
// 选择节点类
|
||||
import { bfsWalk, throttle, checkTwoRectIsOverlap } from '../utils'
|
||||
|
||||
// 节点选择插件
|
||||
class Select {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
@@ -12,6 +11,8 @@ class Select {
|
||||
this.mouseDownY = 0
|
||||
this.mouseMoveX = 0
|
||||
this.mouseMoveY = 0
|
||||
this.isSelecting = false
|
||||
this.cacheActiveList = []
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ class Select {
|
||||
}
|
||||
e.preventDefault()
|
||||
this.isMousedown = true
|
||||
this.cacheActiveList = [...this.mindMap.renderer.activeNodeList]
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseDownX = x
|
||||
this.mouseDownY = y
|
||||
@@ -52,80 +54,142 @@ class Select {
|
||||
) {
|
||||
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
|
||||
this.clearAutoMoveTimer()
|
||||
this.onMove(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
() => {
|
||||
this.isSelecting = true
|
||||
// 绘制矩形
|
||||
this.rect.plot([
|
||||
[this.mouseDownX, this.mouseDownY],
|
||||
[this.mouseMoveX, this.mouseDownY],
|
||||
[this.mouseMoveX, this.mouseMoveY],
|
||||
[this.mouseDownX, this.mouseMoveY]
|
||||
])
|
||||
this.checkInNodes()
|
||||
},
|
||||
(dir, step) => {
|
||||
switch (dir) {
|
||||
case 'left':
|
||||
this.mouseDownX += step
|
||||
break
|
||||
case 'top':
|
||||
this.mouseDownY += step
|
||||
break
|
||||
case 'right':
|
||||
this.mouseDownX -= step
|
||||
break
|
||||
case 'bottom':
|
||||
this.mouseDownY -= step
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
clearTimeout(this.autoMoveTimer)
|
||||
this.isMousedown = false
|
||||
if (this.rect) this.rect.remove()
|
||||
this.rect = null
|
||||
})
|
||||
this.onMouseup = this.onMouseup.bind(this)
|
||||
this.mindMap.on('mouseup', this.onMouseup)
|
||||
this.mindMap.on('node_mouseup', this.onMouseup)
|
||||
}
|
||||
|
||||
// 结束框选
|
||||
onMouseup() {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
this.checkTriggerNodeActiveEvent()
|
||||
clearTimeout(this.autoMoveTimer)
|
||||
this.isMousedown = false
|
||||
this.cacheActiveList = []
|
||||
if (this.rect) this.rect.remove()
|
||||
this.rect = null
|
||||
setTimeout(() => {
|
||||
this.isSelecting = false
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 如果激活节点改变了,那么触发事件
|
||||
checkTriggerNodeActiveEvent() {
|
||||
let isNumChange =
|
||||
this.cacheActiveList.length !==
|
||||
this.mindMap.renderer.activeNodeList.length
|
||||
let isNodeChange = false
|
||||
if (!isNumChange) {
|
||||
for (let i = 0; i < this.cacheActiveList.length; i++) {
|
||||
let cur = this.cacheActiveList[i]
|
||||
if (
|
||||
!this.mindMap.renderer.activeNodeList.find(item => {
|
||||
return item.getData('uid') === cur.getData('uid')
|
||||
})
|
||||
) {
|
||||
isNodeChange = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isNumChange || isNodeChange) {
|
||||
this.mindMap.emit('node_active', null, [
|
||||
...this.mindMap.renderer.activeNodeList
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标移动事件
|
||||
onMove(x, y) {
|
||||
// 绘制矩形
|
||||
this.rect.plot([
|
||||
[this.mouseDownX, this.mouseDownY],
|
||||
[this.mouseMoveX, this.mouseDownY],
|
||||
[this.mouseMoveX, this.mouseMoveY],
|
||||
[this.mouseDownX, this.mouseMoveY]
|
||||
])
|
||||
this.checkInNodes()
|
||||
onMove(x, y, callback = () => {}, handle = () => {}) {
|
||||
callback()
|
||||
// 检测边缘移动
|
||||
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
|
||||
handle('left', step)
|
||||
this.mindMap.view.translateX(step)
|
||||
count++
|
||||
}
|
||||
// 右边缘
|
||||
if (x >= this.mindMap.elRect.right - limit) {
|
||||
this.mouseDownX -= step
|
||||
handle('right', step)
|
||||
this.mindMap.view.translateX(-step)
|
||||
count++
|
||||
}
|
||||
// 上边缘
|
||||
if (y <= this.mindMap.elRect.top + limit) {
|
||||
this.mouseDownY += step
|
||||
handle('top', step)
|
||||
this.mindMap.view.translateY(step)
|
||||
count++
|
||||
}
|
||||
// 下边缘
|
||||
if (y >= this.mindMap.elRect.bottom - limit) {
|
||||
this.mouseDownY -= step
|
||||
handle('bottom', step)
|
||||
this.mindMap.view.translateY(-step)
|
||||
count++
|
||||
}
|
||||
if (count > 0) {
|
||||
this.startAutoMove(x, y)
|
||||
this.startAutoMove(x, y, callback, handle)
|
||||
}
|
||||
}
|
||||
|
||||
// 开启自动移动
|
||||
startAutoMove(x, y) {
|
||||
startAutoMove(x, y, callback, handle) {
|
||||
this.autoMoveTimer = setTimeout(() => {
|
||||
this.onMove(x, y)
|
||||
this.onMove(x, y, callback, handle)
|
||||
}, 20)
|
||||
}
|
||||
|
||||
// 清除自动移动定时器
|
||||
clearAutoMoveTimer() {
|
||||
clearTimeout(this.autoMoveTimer)
|
||||
}
|
||||
|
||||
// 创建矩形
|
||||
createRect(x, y) {
|
||||
if (this.rect) this.rect.remove()
|
||||
this.rect = this.mindMap.svg
|
||||
.polygon()
|
||||
.stroke({
|
||||
@@ -152,27 +216,25 @@ class Select {
|
||||
left = left * scaleX + translateX
|
||||
top = top * scaleY + translateY
|
||||
if (
|
||||
((left >= minx && left <= maxx) || (right >= minx && right <= maxx)) &&
|
||||
((top >= miny && top <= maxy) || (bottom >= miny && bottom <= maxy))
|
||||
checkTwoRectIsOverlap(minx, maxx, miny, maxy, left, right, top, bottom)
|
||||
) {
|
||||
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
|
||||
if (node.nodeData.data.isActive) {
|
||||
if (node.getData('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) {
|
||||
this.mindMap.renderer.addNodeToActiveList(node)
|
||||
} else if (node.getData('isActive')) {
|
||||
if (!node.getData('isActive')) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.setNodeActive(node, false)
|
||||
this.mindMap.renderer.removeActiveNode(node)
|
||||
// })
|
||||
this.mindMap.renderer.removeNodeFromActiveList(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 是否存在选区
|
||||
hasSelectRange() {
|
||||
return this.isSelecting
|
||||
}
|
||||
}
|
||||
|
||||
Select.instanceName = 'select'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// 手势事件支持类
|
||||
|
||||
// 手势事件支持插件
|
||||
class TouchEvent {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
@@ -7,7 +6,7 @@ class TouchEvent {
|
||||
this.touchesNum = 0
|
||||
this.singleTouchstartEvent = null
|
||||
this.clickNum = 0
|
||||
this.doubleTouchmoveDistance = 0
|
||||
this.touchStartScaleView = null
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@@ -34,6 +33,7 @@ class TouchEvent {
|
||||
// 手指按下事件
|
||||
onTouchstart(e) {
|
||||
this.touchesNum = e.touches.length
|
||||
this.touchStartScaleView = null
|
||||
if (this.touchesNum === 1) {
|
||||
let touch = e.touches[0]
|
||||
this.singleTouchstartEvent = touch
|
||||
@@ -48,20 +48,53 @@ class TouchEvent {
|
||||
let touch = e.touches[0]
|
||||
this.dispatchMouseEvent('mousemove', touch.target, touch)
|
||||
} else if (len === 2) {
|
||||
if (this.mindMap.opt.disableTouchZoom) return
|
||||
let touch1 = e.touches[0]
|
||||
let touch2 = e.touches[1]
|
||||
let distance = Math.sqrt(
|
||||
Math.pow(touch1.clientX - touch2.clientX, 2) +
|
||||
Math.pow(touch1.clientY - touch2.clientY, 2)
|
||||
let ox = touch1.clientX - touch2.clientX
|
||||
let oy = touch1.clientY - touch2.clientY
|
||||
let distance = Math.sqrt(Math.pow(ox, 2) + Math.pow(oy, 2))
|
||||
// 以两指中心点进行缩放
|
||||
let { x: touch1ClientX, y: touch1ClientY } = this.mindMap.toPos(
|
||||
touch1.clientX,
|
||||
touch1.clientY
|
||||
)
|
||||
if (distance > this.doubleTouchmoveDistance) {
|
||||
// 放大
|
||||
this.mindMap.view.enlarge()
|
||||
} else {
|
||||
// 缩小
|
||||
this.mindMap.view.narrow()
|
||||
let { x: touch2ClientX, y: touch2ClientY } = this.mindMap.toPos(
|
||||
touch2.clientX,
|
||||
touch2.clientY
|
||||
)
|
||||
let cx = (touch1ClientX + touch2ClientX) / 2
|
||||
let cy = (touch1ClientY + touch2ClientY) / 2
|
||||
// 手势缩放,基于最开始的位置进行缩放(基于前一个位置缩放不是线性关系); 缩放同时支持位置拖动
|
||||
const view = this.mindMap.view
|
||||
if (!this.touchStartScaleView) {
|
||||
this.touchStartScaleView = {
|
||||
distance: distance,
|
||||
scale: view.scale,
|
||||
x: view.x,
|
||||
y: view.y,
|
||||
cx: cx,
|
||||
cy: cy
|
||||
}
|
||||
return
|
||||
}
|
||||
this.doubleTouchmoveDistance = distance
|
||||
const viewBefore = this.touchStartScaleView
|
||||
let scale = viewBefore.scale * (distance / viewBefore.distance)
|
||||
if (Math.abs(distance - viewBefore.distance) <= 10) {
|
||||
scale = viewBefore.scale
|
||||
}
|
||||
const ratio = 1 - scale / viewBefore.scale
|
||||
view.scale = scale < 0.1 ? 0.1 : scale
|
||||
view.x =
|
||||
viewBefore.x +
|
||||
(cx - viewBefore.x) * ratio +
|
||||
(cx - viewBefore.cx) * scale
|
||||
view.y =
|
||||
viewBefore.y +
|
||||
(cy - viewBefore.y) * ratio +
|
||||
(cy - viewBefore.cy) * scale
|
||||
view.transform()
|
||||
this.mindMap.emit('scale', scale)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,12 +115,13 @@ class TouchEvent {
|
||||
this.clickNum = 0
|
||||
this.dispatchMouseEvent('dblclick', ev.target, ev)
|
||||
} else {
|
||||
this.dispatchMouseEvent('click', ev.target, ev)
|
||||
// 点击事件应该不用模拟
|
||||
// this.dispatchMouseEvent('click', ev.target, ev)
|
||||
}
|
||||
}
|
||||
this.touchesNum = 0
|
||||
this.singleTouchstartEvent = null
|
||||
this.doubleTouchmoveDistance = 0
|
||||
this.touchStartScaleView = null
|
||||
}
|
||||
|
||||
// 发送鼠标事件
|
||||
@@ -115,6 +149,11 @@ class TouchEvent {
|
||||
beforePluginRemove() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
TouchEvent.instanceName = 'touchEvent'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Text, G } from '@svgdotjs/svg.js'
|
||||
import { degToRad, camelCaseToHyphen } from '../utils'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 水印类
|
||||
// 水印插件
|
||||
class Watermark {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
@@ -11,13 +11,48 @@ class Watermark {
|
||||
this.angle = 0 // 旋转角度
|
||||
this.text = '' // 水印文字
|
||||
this.textStyle = {} // 水印文字样式
|
||||
this.watermarkDraw = null // 容器
|
||||
this.maxLong = this.getMaxLong()
|
||||
this.updateWatermark(this.mindMap.opt.watermarkConfig || {})
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
getMaxLong() {
|
||||
return Math.sqrt(
|
||||
Math.pow(this.mindMap.width, 2) + Math.pow(this.mindMap.height, 2)
|
||||
)
|
||||
}
|
||||
|
||||
bindEvent() {
|
||||
this.onResize = this.onResize.bind(this)
|
||||
this.mindMap.on('resize', this.onResize)
|
||||
}
|
||||
|
||||
unBindEvent() {
|
||||
this.mindMap.off('resize', this.onResize)
|
||||
}
|
||||
|
||||
onResize() {
|
||||
this.maxLong = this.getMaxLong()
|
||||
this.draw()
|
||||
}
|
||||
|
||||
// 创建水印容器
|
||||
createContainer() {
|
||||
if (this.watermarkDraw) return
|
||||
this.watermarkDraw = this.mindMap.svg
|
||||
.group()
|
||||
.css({ 'pointer-events': 'none', 'user-select': 'none' })
|
||||
this.maxLong = Math.sqrt(
|
||||
Math.pow(this.mindMap.width, 2) + Math.pow(this.mindMap.height, 2)
|
||||
)
|
||||
this.updateWatermark(this.mindMap.opt.watermarkConfig || {})
|
||||
.addClass('smm-water-mark-container')
|
||||
}
|
||||
|
||||
// 删除水印容器
|
||||
removeContainer() {
|
||||
if (!this.watermarkDraw) {
|
||||
return
|
||||
}
|
||||
this.watermarkDraw.remove()
|
||||
this.watermarkDraw = null
|
||||
}
|
||||
|
||||
// 获取是否存在水印
|
||||
@@ -40,10 +75,14 @@ class Watermark {
|
||||
// 绘制水印
|
||||
// 非精确绘制,会绘制一些超出可视区域的水印
|
||||
draw() {
|
||||
this.watermarkDraw.clear()
|
||||
// 清空之前的水印
|
||||
if (this.watermarkDraw) this.watermarkDraw.clear()
|
||||
// 如果没有水印数据,那么水印容器也删除掉
|
||||
if (!this.hasWatermark()) {
|
||||
this.removeContainer()
|
||||
return
|
||||
}
|
||||
this.createContainer()
|
||||
let x = 0
|
||||
while (x < this.mindMap.width) {
|
||||
this.drawText(x)
|
||||
@@ -109,12 +148,25 @@ class Watermark {
|
||||
|
||||
// 更新水印
|
||||
updateWatermark(config) {
|
||||
this.mindMap.opt.watermarkConfig = merge(this.mindMap.opt.watermarkConfig, config)
|
||||
this.mindMap.opt.watermarkConfig = merge(
|
||||
this.mindMap.opt.watermarkConfig,
|
||||
config
|
||||
)
|
||||
this.handleConfig(config)
|
||||
this.draw()
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
Watermark.instanceName = 'watermark'
|
||||
|
||||
export default Watermark
|
||||
export default Watermark
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
getAssociativeLineTargetIndex,
|
||||
joinCubicBezierPath,
|
||||
computeNodePoints,
|
||||
getNodePoint,
|
||||
getDefaultControlPointOffsets
|
||||
} from './associativeLineUtils'
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
function createControlNodes() {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
// 连线
|
||||
this.controlLine1 = this.draw
|
||||
this.controlLine1 = this.associativeLineDraw
|
||||
.line()
|
||||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||||
this.controlLine2 = this.draw
|
||||
this.controlLine2 = this.associativeLineDraw
|
||||
.line()
|
||||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||||
// 控制点
|
||||
@@ -23,7 +23,7 @@ function createControlNodes() {
|
||||
// 创建控制点
|
||||
function createOneControlNode(pointKey) {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
return this.draw
|
||||
return this.associativeLineDraw
|
||||
.circle(this.controlPointDiameter)
|
||||
.stroke({ color: associativeLineActiveColor })
|
||||
.fill({ color: '#fff' })
|
||||
@@ -61,15 +61,22 @@ function onControlPointMousemove(e) {
|
||||
}
|
||||
// 更新当前拖拽的控制点的位置
|
||||
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
|
||||
let [path, clickPath, text, node, toNode] = this.activeLine
|
||||
let [startPoint, endPoint] = computeNodePoints(node, toNode)
|
||||
let [, , , node, toNode] = this.activeLine
|
||||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||||
let { associativeLinePoint, associativeLineTargetControlOffsets } =
|
||||
node.getData()
|
||||
associativeLinePoint = associativeLinePoint || []
|
||||
const nodePos = this.getNodePos(node)
|
||||
const toNodePos = this.getNodePos(toNode)
|
||||
let [startPoint, endPoint] = this.updateAllLinesPos(
|
||||
node,
|
||||
toNode,
|
||||
associativeLinePoint[targetIndex]
|
||||
)
|
||||
this.controlPointMousemoveState.startPoint = startPoint
|
||||
this.controlPointMousemoveState.endPoint = endPoint
|
||||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||||
this.controlPointMousemoveState.targetIndex = targetIndex
|
||||
let offsets = []
|
||||
let associativeLineTargetControlOffsets =
|
||||
node.nodeData.data.associativeLineTargetControlOffsets
|
||||
if (!associativeLineTargetControlOffsets) {
|
||||
// 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况
|
||||
offsets = getDefaultControlPointOffsets(startPoint, endPoint)
|
||||
@@ -78,8 +85,14 @@ function onControlPointMousemove(e) {
|
||||
}
|
||||
let point1 = null
|
||||
let point2 = null
|
||||
const { x: clientX, y: clientY } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
const _e = {
|
||||
clientX,
|
||||
clientY
|
||||
}
|
||||
// 拖拽的是控制点1
|
||||
if (this.mousedownControlPointKey === 'controlPoint1') {
|
||||
startPoint = getNodePoint(nodePos, '', 0, _e)
|
||||
point1 = {
|
||||
x,
|
||||
y
|
||||
@@ -88,10 +101,15 @@ function onControlPointMousemove(e) {
|
||||
x: endPoint.x + offsets[1].x,
|
||||
y: endPoint.y + offsets[1].y
|
||||
}
|
||||
// 更新控制点1的连线
|
||||
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
|
||||
if (startPoint) {
|
||||
// 保存更新后的坐标
|
||||
this.controlPointMousemoveState.startPoint = startPoint
|
||||
// 更新控制点1的连线
|
||||
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
|
||||
}
|
||||
} else {
|
||||
// 拖拽的是控制点2
|
||||
endPoint = getNodePoint(toNodePos, '', 0, _e)
|
||||
point1 = {
|
||||
x: startPoint.x + offsets[0].x,
|
||||
y: startPoint.y + offsets[0].y
|
||||
@@ -100,18 +118,39 @@ function onControlPointMousemove(e) {
|
||||
x,
|
||||
y
|
||||
}
|
||||
// 更新控制点2的连线
|
||||
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
|
||||
if (endPoint) {
|
||||
// 保存更新后结束节点的坐标
|
||||
this.controlPointMousemoveState.endPoint = endPoint
|
||||
// 更新控制点2的连线
|
||||
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
|
||||
}
|
||||
}
|
||||
this.updataAassociativeLine(
|
||||
startPoint,
|
||||
endPoint,
|
||||
point1,
|
||||
point2,
|
||||
this.activeLine
|
||||
)
|
||||
}
|
||||
|
||||
function updataAassociativeLine(
|
||||
startPoint,
|
||||
endPoint,
|
||||
point1,
|
||||
point2,
|
||||
activeLine
|
||||
) {
|
||||
const [path, clickPath, text] = activeLine
|
||||
// 更新关联线
|
||||
let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
|
||||
const pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
|
||||
path.plot(pathStr)
|
||||
clickPath.plot(pathStr)
|
||||
this.updateTextPos(path, text)
|
||||
this.updateTextEditBoxPos(text)
|
||||
}
|
||||
|
||||
// 控制点的鼠标移动事件
|
||||
// 控制点的鼠标松开事件
|
||||
function onControlPointMouseup(e) {
|
||||
if (!this.isControlPointMousedown) return
|
||||
e.stopPropagation()
|
||||
@@ -120,8 +159,15 @@ function onControlPointMouseup(e) {
|
||||
this.controlPointMousemoveState
|
||||
let [, , , node] = this.activeLine
|
||||
let offsetList = []
|
||||
let associativeLineTargetControlOffsets =
|
||||
node.nodeData.data.associativeLineTargetControlOffsets
|
||||
let { associativeLinePoint, associativeLineTargetControlOffsets } =
|
||||
node.getData()
|
||||
if (!associativeLinePoint) {
|
||||
associativeLinePoint = []
|
||||
}
|
||||
associativeLinePoint[targetIndex] = associativeLinePoint[targetIndex] || {
|
||||
startPoint,
|
||||
endPoint
|
||||
}
|
||||
if (!associativeLineTargetControlOffsets) {
|
||||
// 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况
|
||||
offsetList[targetIndex] = getDefaultControlPointOffsets(
|
||||
@@ -140,6 +186,7 @@ function onControlPointMouseup(e) {
|
||||
y: pos.y - startPoint.y
|
||||
}
|
||||
offset2 = offsetList[targetIndex][1]
|
||||
associativeLinePoint[targetIndex].startPoint = startPoint
|
||||
} else {
|
||||
// 更新控制点2数据
|
||||
offset1 = offsetList[targetIndex][0]
|
||||
@@ -147,10 +194,12 @@ function onControlPointMouseup(e) {
|
||||
x: pos.x - endPoint.x,
|
||||
y: pos.y - endPoint.y
|
||||
}
|
||||
associativeLinePoint[targetIndex].endPoint = endPoint
|
||||
}
|
||||
offsetList[targetIndex] = [offset1, offset2]
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
associativeLineTargetControlOffsets: offsetList
|
||||
associativeLineTargetControlOffsets: offsetList,
|
||||
associativeLinePoint
|
||||
})
|
||||
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚,所以重置isControlPointMousedown需要等draw_click事件触发完以后
|
||||
setTimeout(() => {
|
||||
@@ -237,5 +286,6 @@ export default {
|
||||
renderControls,
|
||||
removeControls,
|
||||
hideControls,
|
||||
showControls
|
||||
showControls,
|
||||
updataAassociativeLine
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Text } from '@svgdotjs/svg.js'
|
||||
import { getStrWithBrFromHtml } from '../../utils/index'
|
||||
import {
|
||||
getStrWithBrFromHtml,
|
||||
focusInput,
|
||||
selectAllInput
|
||||
} from '../../utils/index'
|
||||
|
||||
// 创建文字节点
|
||||
function createText(data) {
|
||||
let g = this.draw.group()
|
||||
let g = this.associativeLineDraw.group()
|
||||
const setActive = () => {
|
||||
if (
|
||||
!this.activeLine ||
|
||||
@@ -36,7 +40,7 @@ function showEditTextBox(g) {
|
||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
|
||||
// 输入框元素没有创建过,则先创建
|
||||
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;`
|
||||
@@ -47,26 +51,35 @@ function showEditTextBox(g) {
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
let {
|
||||
associativeLineTextFontSize,
|
||||
associativeLineTextFontFamily,
|
||||
associativeLineTextLineHeight
|
||||
} = this.mindMap.themeConfig
|
||||
let { defaultAssociativeLineText, nodeTextEditZIndex } = this.mindMap.opt
|
||||
let scale = this.mindMap.view.scale
|
||||
let [, , , node, toNode] = this.activeLine
|
||||
let textLines = (
|
||||
this.getText(node, toNode) || this.mindMap.opt.defaultAssociativeLineText
|
||||
).split(/\n/gim)
|
||||
let text = this.getText(node, toNode)
|
||||
let textLines = (text || defaultAssociativeLineText).split(/\n/gim)
|
||||
this.textEditNode.style.fontFamily = associativeLineTextFontFamily
|
||||
this.textEditNode.style.fontSize = associativeLineTextFontSize * scale + 'px'
|
||||
this.textEditNode.style.lineHeight = textLines.length > 1 ? associativeLineTextLineHeight : 'normal'
|
||||
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
|
||||
this.textEditNode.style.lineHeight =
|
||||
textLines.length > 1 ? associativeLineTextLineHeight : 'normal'
|
||||
this.textEditNode.style.zIndex = nodeTextEditZIndex
|
||||
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.updateTextEditBoxPos(g)
|
||||
this.showTextEdit = true
|
||||
// 如果是默认文本要全选输入框
|
||||
if (text === '' || text === defaultAssociativeLineText) {
|
||||
selectAllInput(this.textEditNode)
|
||||
} else {
|
||||
// 否则聚焦即可
|
||||
focusInput(this.textEditNode)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理画布缩放
|
||||
@@ -77,10 +90,12 @@ function onScale() {
|
||||
// 更新文本编辑框位置
|
||||
function updateTextEditBoxPos(g) {
|
||||
let rect = g.node.getBoundingClientRect()
|
||||
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'
|
||||
if (this.textEditNode) {
|
||||
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`
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏文本编辑框
|
||||
@@ -90,10 +105,13 @@ function hideEditTextBox() {
|
||||
}
|
||||
let [path, , text, node, toNode] = this.activeLine
|
||||
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||
// 如果是默认文本,那么不保存
|
||||
let isDefaultText = str === this.mindMap.opt.defaultAssociativeLineText
|
||||
str = isDefaultText ? '' : str
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
associativeLineText: {
|
||||
...(node.nodeData.data.associativeLineText || {}),
|
||||
[toNode.nodeData.data.id]: str
|
||||
...(node.getData('associativeLineText') || {}),
|
||||
[toNode.getData('uid')]: str
|
||||
}
|
||||
})
|
||||
this.textEditNode.style.display = 'none'
|
||||
@@ -105,11 +123,11 @@ function hideEditTextBox() {
|
||||
|
||||
// 获取某根关联线的文字
|
||||
function getText(node, toNode) {
|
||||
let obj = node.nodeData.data.associativeLineText
|
||||
let obj = node.getData('associativeLineText')
|
||||
if (!obj) {
|
||||
return ''
|
||||
}
|
||||
return obj[toNode.nodeData.data.id] || ''
|
||||
return obj[toNode.getData('uid')] || ''
|
||||
}
|
||||
|
||||
// 渲染关联线文字
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 获取目标节点在起始节点的目标数组中的索引
|
||||
export const getAssociativeLineTargetIndex = (node, toNode) => {
|
||||
return node.nodeData.data.associativeLineTargets.findIndex(item => {
|
||||
return item === toNode.nodeData.data.id
|
||||
return node.getData('associativeLineTargets').findIndex(item => {
|
||||
return item === toNode.getData('uid')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,28 +54,136 @@ export const cubicBezierPath = (x1, y1, x2, y2) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const calcPoint = (node, e) => {
|
||||
const { left, top, translateLeft, translateTop, width, height } = node
|
||||
const clientX = e.clientX
|
||||
const clientY = e.clientY
|
||||
// 中心点的坐标
|
||||
const centerX = translateLeft + width / 2
|
||||
const centerY = translateTop + height / 2
|
||||
const translateCenterX = left + width / 2
|
||||
const translateCenterY = top + height / 2
|
||||
const theta = Math.atan(height / width)
|
||||
// 矩形左上角坐标
|
||||
const deltaX = clientX - centerX
|
||||
const deltaY = centerY - clientY
|
||||
// 方向值
|
||||
const direction = Math.atan2(deltaY, deltaX)
|
||||
// 默认坐标
|
||||
let x = left + width
|
||||
let y = top + height
|
||||
if (direction < theta && direction >= -theta) {
|
||||
// 右边
|
||||
// 正切值 = 对边/邻边,对边 = 正切值*邻边
|
||||
const range = direction * (width / 2)
|
||||
if (direction < theta && direction >= 0) {
|
||||
// 中心点上边
|
||||
y = translateCenterY - range
|
||||
} else if (direction >= -theta && direction < 0) {
|
||||
// 中心点下方
|
||||
y = translateCenterY - range
|
||||
}
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
dir: 'right',
|
||||
range
|
||||
}
|
||||
} else if (direction >= theta && direction < Math.PI - theta) {
|
||||
// 上边
|
||||
y = top
|
||||
let range = 0
|
||||
if (direction < Math.PI / 2 - theta && direction >= theta) {
|
||||
// 正切值 = 对边/邻边,邻边 = 对边/正切值
|
||||
const side = height / 2 / direction
|
||||
range = -side
|
||||
// 中心点右侧
|
||||
x = translateCenterX + side
|
||||
} else if (
|
||||
direction >= Math.PI / 2 - theta &&
|
||||
direction < Math.PI - theta
|
||||
) {
|
||||
// 中心点左侧
|
||||
const tanValue = (centerX - clientX) / (centerY - clientY)
|
||||
const side = (height / 2) * tanValue
|
||||
range = side
|
||||
x = translateCenterX - side
|
||||
}
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
dir: 'top',
|
||||
range
|
||||
}
|
||||
} else if (direction < -theta && direction >= theta - Math.PI) {
|
||||
// 下边
|
||||
let range = 0
|
||||
if (direction >= theta - Math.PI / 2 && direction < -theta) {
|
||||
// 中心点右侧
|
||||
// 正切值 = 对边/邻边,邻边 = 对边/正切值
|
||||
const side = height / 2 / direction
|
||||
range = side
|
||||
x = translateCenterX - side
|
||||
} else if (
|
||||
direction < theta - Math.PI / 2 &&
|
||||
direction >= theta - Math.PI
|
||||
) {
|
||||
// 中心点左侧
|
||||
const tanValue = (centerX - clientX) / (centerY - clientY)
|
||||
const side = (height / 2) * tanValue
|
||||
range = -side
|
||||
x = translateCenterX + side
|
||||
}
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
dir: 'bottom',
|
||||
range
|
||||
}
|
||||
}
|
||||
// 左边
|
||||
x = left
|
||||
const tanValue = (centerY - clientY) / (centerX - clientX)
|
||||
const range = tanValue * (width / 2)
|
||||
if (direction >= -Math.PI && direction < theta - Math.PI) {
|
||||
// 中心点右侧
|
||||
y = translateCenterY - range
|
||||
} else if (direction < Math.PI && direction >= Math.PI - theta) {
|
||||
// 中心点左侧
|
||||
y = translateCenterY - range
|
||||
}
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
dir: 'left',
|
||||
range
|
||||
}
|
||||
}
|
||||
// 获取节点的连接点
|
||||
export const getNodePoint = (node, dir = 'right') => {
|
||||
export const getNodePoint = (node, dir = 'right', range = 0, e = null) => {
|
||||
let { left, top, width, height } = node
|
||||
if (e) {
|
||||
return calcPoint(node, e)
|
||||
}
|
||||
switch (dir) {
|
||||
case 'left':
|
||||
return {
|
||||
x: left,
|
||||
y: top + height / 2
|
||||
y: top + height / 2 - range
|
||||
}
|
||||
case 'right':
|
||||
return {
|
||||
x: left + width,
|
||||
y: top + height / 2
|
||||
y: top + height / 2 - range
|
||||
}
|
||||
case 'top':
|
||||
return {
|
||||
x: left + width / 2,
|
||||
x: left + width / 2 - range,
|
||||
y: top
|
||||
}
|
||||
case 'bottom':
|
||||
return {
|
||||
x: left + width / 2,
|
||||
x: left + width / 2 - range,
|
||||
y: top + height
|
||||
}
|
||||
default:
|
||||
@@ -94,7 +202,7 @@ export const computeNodePoints = (fromNode, toNode) => {
|
||||
// 中心点坐标的差值
|
||||
let offsetX = toCx - fromCx
|
||||
let offsetY = toCy - fromCy
|
||||
if (offsetX === 0 && offsetY === 0) return
|
||||
if (offsetX === 0 && offsetY === 0) return []
|
||||
let fromDir = ''
|
||||
let toDir = ''
|
||||
if (offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY) {
|
||||
@@ -111,8 +219,8 @@ export const computeNodePoints = (fromNode, toNode) => {
|
||||
toDir = 'bottom'
|
||||
} else if (offsetY > 0 && -offsetY < offsetX && offsetY > offsetX) {
|
||||
// down
|
||||
fromDir = 'bottom'
|
||||
toDir = 'top'
|
||||
fromDir = 'right'
|
||||
toDir = 'right'
|
||||
}
|
||||
return [getNodePoint(fromNode, fromDir), getNodePoint(toNode, toDir)]
|
||||
}
|
||||
@@ -123,7 +231,7 @@ export const getNodeLinePath = (startPoint, endPoint, node, toNode) => {
|
||||
// 控制点
|
||||
let controlPoints = []
|
||||
let associativeLineTargetControlOffsets =
|
||||
node.nodeData.data.associativeLineTargetControlOffsets
|
||||
node.getData('associativeLineTargetControlOffsets')
|
||||
if (
|
||||
associativeLineTargetControlOffsets &&
|
||||
associativeLineTargetControlOffsets[targetIndex]
|
||||
@@ -179,4 +287,4 @@ export const getDefaultControlPointOffsets = (startPoint, endPoint) => {
|
||||
y: controlPoints[1].y - endPoint.y
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,15 @@ const open = `<svg t="1618141562310" class="icon" viewBox="0 0 1024 1024" versio
|
||||
// 收缩按钮
|
||||
const close = `<svg t="1618141589243" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13611" width="200" height="200"><path d="M512 105.472c225.28 0 407.04 181.76 407.04 407.04s-181.76 407.04-407.04 407.04-407.04-181.76-407.04-407.04 181.76-407.04 407.04-407.04z m0-74.24c-265.216 0-480.768 215.552-480.768 480.768s215.552 480.768 480.768 480.768 480.768-215.552 480.768-480.768-215.552-480.768-480.768-480.768z" p-id="13612"></path><path d="M252.928 474.624h518.144v74.24h-518.144z" p-id="13613"></path></svg>`
|
||||
|
||||
// 删除按钮
|
||||
const remove = `<svg width="14px" height="14px" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13611" width="200" height="200"><path fill="#ffffff" d="M512 105.472c225.28 0 407.04 181.76 407.04 407.04s-181.76 407.04-407.04 407.04-407.04-181.76-407.04-407.04 181.76-407.04 407.04-407.04z m0-74.24c-265.216 0-480.768 215.552-480.768 480.768s215.552 480.768 480.768 480.768 480.768-215.552 480.768-480.768-215.552-480.768-480.768-480.768z" p-id="13612"></path><path fill="#ffffff" d="M252.928 474.624h518.144v74.24h-518.144z" p-id="13613"></path></svg>`
|
||||
|
||||
// 图片调整按钮
|
||||
const imgAdjust = `<svg width="12px" height="12px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M1008.128 614.4a25.6 25.6 0 0 0-27.648 5.632l-142.848 142.848L259.072 186.88 401.92 43.52A25.6 25.6 0 0 0 384 0h-358.4a25.6 25.6 0 0 0-25.6 25.6v358.4a25.6 25.6 0 0 0 43.52 17.92l143.36-142.848 578.048 578.048-142.848 142.848a25.6 25.6 0 0 0 17.92 43.52h358.4a25.6 25.6 0 0 0 25.6-25.6v-358.4a25.6 25.6 0 0 0-15.872-25.088z" /></svg>`
|
||||
|
||||
export default {
|
||||
open,
|
||||
close
|
||||
close,
|
||||
remove,
|
||||
imgAdjust
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { mergerIconList } from '../utils'
|
||||
|
||||
// 超链接图标
|
||||
const hyperlink =
|
||||
'<svg t="1624174958075" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7982" ><path d="M435.484444 251.733333v68.892445L295.822222 320.682667a168.504889 168.504889 0 0 0-2.844444 336.952889h142.506666v68.892444H295.822222a237.397333 237.397333 0 0 1 0-474.794667h139.662222z m248.945778 0a237.397333 237.397333 0 0 1 0 474.851556H544.654222v-69.006222l139.776 0.056889a168.504889 168.504889 0 0 0 2.844445-336.952889H544.597333V251.676444h139.776z m-25.827555 203.946667a34.474667 34.474667 0 0 1 0 68.892444H321.649778a34.474667 34.474667 0 0 1 0-68.892444h336.952889z" p-id="7983"></path></svg>'
|
||||
@@ -281,12 +283,21 @@ export const nodeIconList = [
|
||||
// 获取nodeIconList icon内容
|
||||
const getNodeIconListIcon = (name, extendIconList = []) => {
|
||||
let arr = name.split('_')
|
||||
let typeData = [...nodeIconList, ...extendIconList].find(item => {
|
||||
const iconList = mergerIconList([...nodeIconList, ...extendIconList])
|
||||
let typeData = iconList.find(item => {
|
||||
return item.type === arr[0]
|
||||
})
|
||||
return typeData.list.find(item => {
|
||||
return item.name === arr[1]
|
||||
}).icon
|
||||
if (typeData) {
|
||||
let typeName = typeData.list.find(item => {
|
||||
return item.name === arr[1]
|
||||
})
|
||||
if (typeName) {
|
||||
return typeName.icon
|
||||
}
|
||||
return ''
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
|
||||
color: '#fff',
|
||||
borderColor: '#e68112',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: '#b0bc47',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 24
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -30,18 +26,12 @@ export default merge(defaultTheme, {
|
||||
color: '#8c5416',
|
||||
borderColor: '#b0bc47',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: '#e68112'
|
||||
}
|
||||
fontSize: 18
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: '#8c5416',
|
||||
active: {
|
||||
borderColor: '#b0bc47'
|
||||
}
|
||||
color: '#8c5416'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -49,9 +39,6 @@ export default merge(defaultTheme, {
|
||||
fillColor: '#ffd683',
|
||||
borderColor: '#b0bc47',
|
||||
borderWidth: 2,
|
||||
color: '#8c5416',
|
||||
active: {
|
||||
borderColor: '#e68112'
|
||||
}
|
||||
color: '#8c5416'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
|
||||
color: '#fff',
|
||||
borderColor: '#94c143',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: '#749336',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 24
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -30,18 +26,12 @@ export default merge(defaultTheme, {
|
||||
color: '#749336',
|
||||
borderColor: '#aec668',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: '#749336'
|
||||
}
|
||||
fontSize: 18
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: '#749336',
|
||||
active: {
|
||||
borderColor: '#749336'
|
||||
}
|
||||
color: '#749336'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -49,9 +39,6 @@ export default merge(defaultTheme, {
|
||||
fillColor: '#cee498',
|
||||
borderColor: '#aec668',
|
||||
borderWidth: 2,
|
||||
color: '#749336',
|
||||
active: {
|
||||
borderColor: '#749336'
|
||||
}
|
||||
color: '#749336'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(111, 61, 6)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 24
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -30,18 +26,12 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(225, 201, 158)',
|
||||
borderColor: 'rgb(245, 224, 191)',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 208, 124)'
|
||||
}
|
||||
fontSize: 18
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(231, 203, 155)',
|
||||
active: {
|
||||
borderColor: 'rgb(255, 208, 124)'
|
||||
}
|
||||
color: 'rgb(231, 203, 155)'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -49,9 +39,6 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(56, 45, 34)',
|
||||
borderColor: 'rgb(104, 84, 61)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(242, 216, 176)',
|
||||
active: {
|
||||
borderColor: 'rgb(255, 208, 124)'
|
||||
}
|
||||
color: 'rgb(242, 216, 176)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
|
||||
color: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: 'rgb(254, 199, 13)',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 24
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -30,19 +26,12 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(0, 0, 0)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(36, 179, 96)',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 18
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(204, 204, 204)',
|
||||
active: {
|
||||
borderColor: 'rgb(254, 199, 13)'
|
||||
}
|
||||
color: 'rgb(204, 204, 204)'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -50,9 +39,6 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(27, 31, 34)',
|
||||
borderColor: 'rgb(255, 119, 34)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(204, 204, 204)',
|
||||
active: {
|
||||
borderColor: 'rgb(36, 179, 96)'
|
||||
}
|
||||
color: 'rgb(204, 204, 204)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,10 +13,7 @@ export default merge(defaultTheme, {
|
||||
generalizationLineColor: '#333',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(115, 161, 191)',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)'
|
||||
}
|
||||
fillColor: 'rgb(115, 161, 191)'
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -24,26 +21,17 @@ export default merge(defaultTheme, {
|
||||
color: '#333',
|
||||
borderColor: 'rgb(115, 161, 191)',
|
||||
borderWidth: 1,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)'
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)'
|
||||
}
|
||||
color: '#333'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: '#333',
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)'
|
||||
}
|
||||
color: '#333'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,10 +13,7 @@ export default merge(defaultTheme, {
|
||||
generalizationLineColor: '#333',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(191, 115, 148)',
|
||||
active: {
|
||||
borderColor: 'rgb(96, 57, 74)'
|
||||
}
|
||||
fillColor: 'rgb(191, 115, 148)'
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -24,26 +21,17 @@ export default merge(defaultTheme, {
|
||||
color: '#333',
|
||||
borderColor: 'rgb(191, 115, 148)',
|
||||
borderWidth: 1,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(96, 57, 74)'
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(96, 57, 74)'
|
||||
}
|
||||
color: '#333'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: '#333',
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(96, 57, 74)'
|
||||
}
|
||||
color: '#333'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,16 +18,13 @@ export default merge(defaultTheme, {
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=',
|
||||
// 背景重复
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: 'auto',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(233, 223, 152)',
|
||||
color: '#333',
|
||||
fontSize: 24,
|
||||
borderRadius: 21,
|
||||
active: {
|
||||
fillColor: 'rgb(254, 219, 0)',
|
||||
borderColor: 'transparent'
|
||||
}
|
||||
borderRadius: 21
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -35,30 +32,18 @@ export default merge(defaultTheme, {
|
||||
borderColor: 'transparent',
|
||||
color: '#333',
|
||||
fontSize: 16,
|
||||
borderRadius: 10,
|
||||
active: {
|
||||
fillColor: 'rgb(254, 219, 0)',
|
||||
borderColor: 'transparent'
|
||||
}
|
||||
borderRadius: 10
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
active: {
|
||||
fillColor: 'rgb(254, 219, 0)',
|
||||
borderColor: 'transparent'
|
||||
}
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: 'transparent',
|
||||
color: '#333',
|
||||
active: {
|
||||
fillColor: 'rgb(254, 219, 0)',
|
||||
borderColor: 'transparent'
|
||||
}
|
||||
color: '#333'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,10 +18,7 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(18, 187, 55)',
|
||||
color: '#fff',
|
||||
fontSize: 24,
|
||||
borderRadius: 10,
|
||||
active: {
|
||||
borderColor: 'rgb(51, 51, 51)'
|
||||
}
|
||||
borderRadius: 10
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -29,27 +26,18 @@ export default merge(defaultTheme, {
|
||||
borderColor: 'transparent',
|
||||
color: '#1a1a1a',
|
||||
fontSize: 18,
|
||||
borderRadius: 10,
|
||||
active: {
|
||||
borderColor: 'rgb(51, 51, 51)'
|
||||
}
|
||||
borderRadius: 10
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: '#1a1a1a',
|
||||
active: {
|
||||
borderColor: 'rgb(51, 51, 51)'
|
||||
}
|
||||
color: '#1a1a1a'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: 'rgb(51, 51, 51)',
|
||||
borderWidth: 2,
|
||||
color: '#1a1a1a',
|
||||
active: {
|
||||
borderColor: 'rgb(18, 187, 55)'
|
||||
}
|
||||
color: '#1a1a1a'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,10 +20,7 @@ export default merge(defaultTheme, {
|
||||
fontSize: 24,
|
||||
borderRadius: 10,
|
||||
borderColor: 'rgb(249, 199, 84)',
|
||||
borderWidth: 1,
|
||||
active: {
|
||||
borderColor: 'rgb(94, 202, 110)'
|
||||
}
|
||||
borderWidth: 1
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -32,27 +29,18 @@ export default merge(defaultTheme, {
|
||||
borderWidth: 1,
|
||||
color: '#1a1a1a',
|
||||
fontSize: 18,
|
||||
borderRadius: 10,
|
||||
active: {
|
||||
borderColor: 'rgb(94, 202, 110)'
|
||||
}
|
||||
borderRadius: 10
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: '#1a1a1a',
|
||||
active: {
|
||||
borderColor: 'rgb(94, 202, 110)'
|
||||
}
|
||||
color: '#1a1a1a'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: '#1a1a1a',
|
||||
color: '#1a1a1a',
|
||||
borderWidth: 2,
|
||||
active: {
|
||||
borderColor: 'rgb(94, 202, 110)'
|
||||
}
|
||||
borderWidth: 2
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,10 +20,7 @@ export default merge(defaultTheme, {
|
||||
fontSize: 24,
|
||||
borderRadius: 10,
|
||||
borderColor: 'rgb(189, 197, 201)',
|
||||
borderWidth: 2,
|
||||
active: {
|
||||
borderColor: 'rgb(169, 218, 218)'
|
||||
}
|
||||
borderWidth: 2
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -32,10 +29,7 @@ export default merge(defaultTheme, {
|
||||
borderWidth: 2,
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
borderRadius: 10,
|
||||
active: {
|
||||
borderColor: 'rgb(56, 123, 233)'
|
||||
}
|
||||
borderRadius: 10
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
@@ -43,19 +37,13 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(30, 53, 86)',
|
||||
borderColor: 'rgb(30, 53, 86)',
|
||||
borderWidth: 1,
|
||||
marginY: 20,
|
||||
active: {
|
||||
borderColor: 'rgb(169, 218, 218)'
|
||||
}
|
||||
marginY: 20
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: 'rgb(56, 123, 233)',
|
||||
borderColor: 'rgb(56, 123, 233)',
|
||||
color: '#fff',
|
||||
borderWidth: 0,
|
||||
active: {
|
||||
borderColor: 'rgb(169, 218, 218)'
|
||||
}
|
||||
borderWidth: 0
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,10 +16,7 @@ export default merge(defaultTheme, {
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(255, 255, 255)',
|
||||
color: '#222',
|
||||
active: {
|
||||
borderColor: 'rgb(94, 199, 248)'
|
||||
}
|
||||
color: '#222'
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -27,26 +24,17 @@ export default merge(defaultTheme, {
|
||||
color: '#222',
|
||||
borderColor: 'rgb(255, 255, 255)',
|
||||
borderWidth: 1,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(94, 199, 248)'
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(94, 199, 248)'
|
||||
}
|
||||
color: '#333'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: 'rgb(51, 51, 51)',
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(94, 199, 248)'
|
||||
}
|
||||
color: '#333'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,10 +14,7 @@ export default merge(defaultTheme, {
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(253, 244, 217)',
|
||||
color: '#222',
|
||||
active: {
|
||||
borderColor: 'rgb(94, 199, 248)'
|
||||
}
|
||||
color: '#222'
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -25,27 +22,18 @@ export default merge(defaultTheme, {
|
||||
color: '#222',
|
||||
borderColor: 'rgb(242, 200, 104)',
|
||||
borderWidth: 1,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(94, 199, 248)'
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(94, 199, 248)'
|
||||
}
|
||||
color: '#333'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: 'rgb(123, 199, 120)',
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
color: '#fff',
|
||||
active: {
|
||||
borderColor: 'rgb(94, 199, 248)'
|
||||
}
|
||||
color: '#fff'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,11 +16,7 @@ export default merge(defaultTheme, {
|
||||
color: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: 'rgb(173, 123, 91)',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 24
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -28,18 +24,12 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(125, 86, 42)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(173, 123, 91)'
|
||||
}
|
||||
fontSize: 18
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(96, 71, 47)',
|
||||
active: {
|
||||
borderColor: 'rgb(173, 123, 91)'
|
||||
}
|
||||
color: 'rgb(96, 71, 47)'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -47,9 +37,6 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(255, 249, 239)',
|
||||
borderColor: 'rgb(173, 123, 91)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(122, 83, 44)',
|
||||
active: {
|
||||
borderColor: 'rgb(202, 117, 79)'
|
||||
}
|
||||
color: 'rgb(122, 83, 44)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,11 +16,7 @@ export default merge(defaultTheme, {
|
||||
color: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: 'rgb(173, 91, 12)',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 24
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -28,18 +24,12 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(50, 113, 96)',
|
||||
borderColor: 'rgb(113, 195, 169)',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(173, 91, 12)'
|
||||
}
|
||||
fontSize: 18
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(10, 59, 43)',
|
||||
active: {
|
||||
borderColor: 'rgb(173, 91, 12)'
|
||||
}
|
||||
color: 'rgb(10, 59, 43)'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -47,9 +37,6 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(246, 238, 211)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
color: 'rgb(173, 91, 12)',
|
||||
active: {
|
||||
borderColor: 'rgb(113, 195, 169)'
|
||||
}
|
||||
color: 'rgb(173, 91, 12)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,10 +18,7 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(28, 178, 43)',
|
||||
color: '#fff',
|
||||
fontSize: 24,
|
||||
borderRadius: 10,
|
||||
active: {
|
||||
borderColor: 'rgb(17, 68, 23)'
|
||||
}
|
||||
borderRadius: 10
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -29,26 +26,17 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(147,148,149)',
|
||||
fontSize: 18,
|
||||
borderRadius: 10,
|
||||
borderWidth: 0,
|
||||
active: {
|
||||
borderColor: 'rgb(17, 68, 23)'
|
||||
}
|
||||
borderWidth: 0
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(147, 148, 149)',
|
||||
active: {
|
||||
borderColor: 'rgb(17, 68, 23)'
|
||||
}
|
||||
color: 'rgb(147, 148, 149)'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: 'transparent',
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(17, 68, 23)'
|
||||
}
|
||||
color: '#333'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -17,11 +17,7 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(36, 179, 96)',
|
||||
color: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
active: {
|
||||
borderColor: 'rgb(254, 199, 13)',
|
||||
borderWidth: 3
|
||||
}
|
||||
borderWidth: 0
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -29,28 +25,18 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(0, 0, 0)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(36, 179, 96)',
|
||||
borderWidth: 2
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: 'rgb(204, 204, 204)',
|
||||
active: {
|
||||
borderColor: 'rgb(254, 199, 13)'
|
||||
}
|
||||
color: 'rgb(204, 204, 204)'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: 'transparent',
|
||||
borderColor: 'rgb(255, 119, 34)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(204, 204, 204)',
|
||||
active: {
|
||||
borderColor: 'rgb(254, 199, 13)'
|
||||
}
|
||||
color: 'rgb(204, 204, 204)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,6 +18,8 @@ export default {
|
||||
lineDasharray: 'none',
|
||||
// 连线风格
|
||||
lineStyle: 'straight', // 针对logicalStructure、mindMap两种结构。曲线(curve)、直线(straight)、直连(direct)
|
||||
// 曲线连接时,根节点和其他节点的连接线样式保持统一,默认根节点为 ( 型,其他节点为 { 型,设为true后,都为 { 型
|
||||
rootLineKeepSameInCurve: true,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 1,
|
||||
// 概要连线的颜色
|
||||
@@ -68,12 +70,7 @@ export default {
|
||||
borderWidth: 0,
|
||||
borderDasharray: 'none',
|
||||
borderRadius: 5,
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
textDecoration: 'none'
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -91,12 +88,7 @@ export default {
|
||||
borderWidth: 1,
|
||||
borderDasharray: 'none',
|
||||
borderRadius: 5,
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
textDecoration: 'none'
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
@@ -114,12 +106,7 @@ export default {
|
||||
borderWidth: 0,
|
||||
borderRadius: 5,
|
||||
borderDasharray: 'none',
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
textDecoration: 'none'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -137,12 +124,7 @@ export default {
|
||||
borderWidth: 1,
|
||||
borderDasharray: 'none',
|
||||
borderRadius: 5,
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
textDecoration: 'none'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,14 +158,17 @@ const nodeSizeIndependenceList = [
|
||||
'backgroundImage',
|
||||
'backgroundRepeat',
|
||||
'backgroundPosition',
|
||||
'backgroundSize'
|
||||
'backgroundSize',
|
||||
'rootLineKeepSameInCurve'
|
||||
]
|
||||
export const checkIsNodeSizeIndependenceConfig = (config) => {
|
||||
export const checkIsNodeSizeIndependenceConfig = config => {
|
||||
let keys = Object.keys(config)
|
||||
for(let i = 0; i < keys.length; i++) {
|
||||
if (!nodeSizeIndependenceList.find((item) => {
|
||||
return item === keys[i]
|
||||
})) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (
|
||||
!nodeSizeIndependenceList.find(item => {
|
||||
return item === keys[i]
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@ export default merge(defaultTheme, {
|
||||
generalizationLineColor: '#333',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(191, 147, 115)',
|
||||
active: {
|
||||
borderColor: 'rgb(96, 73, 57)'
|
||||
}
|
||||
fillColor: 'rgb(191, 147, 115)'
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -24,26 +21,17 @@ export default merge(defaultTheme, {
|
||||
color: '#333',
|
||||
borderColor: 'rgb(191, 147, 115)',
|
||||
borderWidth: 1,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(96, 73, 57)'
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(96, 73, 57)'
|
||||
}
|
||||
color: '#333'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: '#333',
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(96, 73, 57)'
|
||||
}
|
||||
color: '#333'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -26,11 +26,6 @@ export default merge(defaultTheme, {
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: '#333',
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
color: '#333'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,10 +13,7 @@ export default merge(defaultTheme, {
|
||||
generalizationLineColor: '#333',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(191, 115, 115)',
|
||||
active: {
|
||||
borderColor: 'rgb(96, 57, 57)'
|
||||
}
|
||||
fillColor: 'rgb(191, 115, 115)'
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -24,26 +21,17 @@ export default merge(defaultTheme, {
|
||||
color: '#333',
|
||||
borderColor: 'rgb(191, 115, 115)',
|
||||
borderWidth: 1,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(96, 57, 57)'
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(96, 57, 57)'
|
||||
}
|
||||
color: '#333'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: '#333',
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(96, 57, 57)'
|
||||
}
|
||||
color: '#333'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -17,11 +17,7 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(51, 56, 62)',
|
||||
color: 'rgb(247, 208, 160)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
active: {
|
||||
borderColor: 'rgb(247, 208, 160)',
|
||||
borderWidth: 3
|
||||
}
|
||||
borderWidth: 0
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -29,27 +25,17 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(81, 58, 42)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(51, 56, 62)',
|
||||
borderWidth: 2
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#222',
|
||||
active: {
|
||||
borderColor: 'rgb(0, 192, 184)'
|
||||
}
|
||||
color: '#222'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: 'rgb(127, 93, 64)',
|
||||
borderColor: 'transparent',
|
||||
color: 'rgb(255, 214, 175)',
|
||||
active: {
|
||||
borderColor: 'rgb(51, 56, 62)'
|
||||
}
|
||||
color: 'rgb(255, 214, 175)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -17,11 +17,7 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(25, 193, 73)',
|
||||
color: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
active: {
|
||||
borderColor: '#222',
|
||||
borderWidth: 3
|
||||
}
|
||||
borderWidth: 0
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -29,28 +25,18 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(69, 149, 96)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(25, 193, 73)',
|
||||
borderWidth: 2
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#222',
|
||||
active: {
|
||||
borderColor: 'rgb(25, 193, 73)'
|
||||
}
|
||||
color: '#222'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: 'rgb(251, 158, 0)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(51, 51, 51)',
|
||||
active: {
|
||||
borderColor: 'rgb(25, 193, 73)'
|
||||
}
|
||||
color: 'rgb(51, 51, 51)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(255, 255, 255)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 119, 34)',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 24
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -30,19 +26,12 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(209, 210, 210)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 119, 34)',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 18
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(204, 204, 204)',
|
||||
active: {
|
||||
borderColor: 'rgb(255, 119, 34)'
|
||||
}
|
||||
color: 'rgb(204, 204, 204)'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -50,9 +39,6 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(255, 119, 34)',
|
||||
borderColor: '',
|
||||
borderWidth: 2,
|
||||
color: '#fff',
|
||||
active: {
|
||||
borderColor: 'rgb(23, 153, 243)'
|
||||
}
|
||||
color: '#fff'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,10 +16,7 @@ export default merge(defaultTheme, {
|
||||
root: {
|
||||
fillColor: 'rgb(55, 165, 255)',
|
||||
borderColor: 'rgb(51, 51, 51)',
|
||||
borderWidth: 3,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 160, 36)'
|
||||
}
|
||||
borderWidth: 3
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -27,26 +24,17 @@ export default merge(defaultTheme, {
|
||||
color: '#222',
|
||||
borderColor: 'rgb(51, 51, 51)',
|
||||
borderWidth: 3,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(55, 165, 255)'
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#222',
|
||||
active: {
|
||||
borderColor: 'rgb(55, 165, 255)'
|
||||
}
|
||||
color: '#222'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
borderColor: '#222',
|
||||
borderWidth: 3,
|
||||
color: '#222',
|
||||
active: {
|
||||
borderColor: 'rgb(55, 165, 255)'
|
||||
}
|
||||
color: '#222'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,11 +16,7 @@ export default merge(defaultTheme, {
|
||||
root: {
|
||||
fillColor: 'rgb(0, 192, 184)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 160, 36)',
|
||||
borderWidth: 3
|
||||
}
|
||||
borderWidth: 0
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -28,26 +24,17 @@ export default merge(defaultTheme, {
|
||||
color: '#222',
|
||||
borderColor: 'rgb(184, 235, 233)',
|
||||
borderWidth: 2,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(0, 192, 184)'
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#222',
|
||||
active: {
|
||||
borderColor: 'rgb(0, 192, 184)'
|
||||
}
|
||||
color: '#222'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: 'rgb(90, 206, 241)',
|
||||
borderColor: 'transparent',
|
||||
color: '#fff',
|
||||
active: {
|
||||
borderColor: 'rgb(0, 192, 184)'
|
||||
}
|
||||
color: '#fff'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
|
||||
color: '#110501',
|
||||
borderColor: '#ff6811',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: '#a9a4a9',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 24
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -30,18 +26,12 @@ export default merge(defaultTheme, {
|
||||
color: '#a9a4a9',
|
||||
borderColor: '#ff6811',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: '#110501'
|
||||
}
|
||||
fontSize: 18
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: '#a9a4a9',
|
||||
active: {
|
||||
borderColor: '#ff6811'
|
||||
}
|
||||
color: '#a9a4a9'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -49,9 +39,6 @@ export default merge(defaultTheme, {
|
||||
fillColor: '',
|
||||
borderColor: '#ff6811',
|
||||
borderWidth: 2,
|
||||
color: '#a9a4a9',
|
||||
active: {
|
||||
borderColor: '#110501'
|
||||
}
|
||||
color: '#a9a4a9'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,11 +16,7 @@ export default merge(defaultTheme, {
|
||||
root: {
|
||||
fillColor: 'rgb(139, 109, 225)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
active: {
|
||||
borderColor: 'rgb(243, 104, 138)',
|
||||
borderWidth: 2
|
||||
}
|
||||
borderWidth: 0
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -28,28 +24,17 @@ export default merge(defaultTheme, {
|
||||
color: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(139, 109, 225)',
|
||||
borderWidth: 2
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#222',
|
||||
active: {
|
||||
borderColor: 'rgb(139, 109, 225)'
|
||||
}
|
||||
color: '#222'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: 'transparent',
|
||||
color: '#222',
|
||||
active: {
|
||||
borderColor: 'rgb(139, 109, 225)',
|
||||
borderWidth: 2
|
||||
}
|
||||
color: '#222'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(255, 233, 157)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 233, 157)',
|
||||
borderWidth: 3
|
||||
}
|
||||
fontSize: 24
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -30,18 +26,12 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(211, 58, 21)',
|
||||
borderColor: 'rgb(222, 101, 85)',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 233, 157)'
|
||||
}
|
||||
fontSize: 18
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(144, 71, 43)',
|
||||
active: {
|
||||
borderColor: 'rgb(255, 233, 157)'
|
||||
}
|
||||
color: 'rgb(144, 71, 43)'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -49,9 +39,6 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(255, 247, 211)',
|
||||
borderColor: 'rgb(255, 202, 162)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(187, 101, 69)',
|
||||
active: {
|
||||
borderColor: 'rgb(222, 101, 85)'
|
||||
}
|
||||
color: 'rgb(187, 101, 69)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,10 +13,7 @@ export default merge(defaultTheme, {
|
||||
generalizationLineColor: '#333',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(123, 115, 191)',
|
||||
active: {
|
||||
borderColor: 'rgb(61, 57, 96)'
|
||||
}
|
||||
fillColor: 'rgb(123, 115, 191)'
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -24,26 +21,17 @@ export default merge(defaultTheme, {
|
||||
color: '#333',
|
||||
borderColor: 'rgb(123, 115, 191)',
|
||||
borderWidth: 1,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(61, 57, 96)'
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(61, 57, 96)'
|
||||
}
|
||||
color: '#333'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: '#333',
|
||||
color: '#333',
|
||||
active: {
|
||||
borderColor: 'rgb(61, 57, 96)'
|
||||
}
|
||||
color: '#333'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,10 +16,7 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(34, 34, 34)',
|
||||
borderColor: 'rgb(34, 34, 34)',
|
||||
borderWidth: 3,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: '#a13600'
|
||||
}
|
||||
fontSize: 24
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -27,18 +24,12 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(34, 34, 34)',
|
||||
borderColor: 'rgb(34, 34, 34)',
|
||||
borderWidth: 3,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: '#a13600'
|
||||
}
|
||||
fontSize: 18
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(34, 34, 34)',
|
||||
active: {
|
||||
borderColor: '#a13600'
|
||||
}
|
||||
color: 'rgb(34, 34, 34)'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
@@ -46,9 +37,6 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'transparent',
|
||||
borderColor: 'rgb(34, 34, 34)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(34, 34, 34)',
|
||||
active: {
|
||||
borderColor: '#a13600'
|
||||
}
|
||||
color: 'rgb(34, 34, 34)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -17,11 +17,7 @@ export default merge(defaultTheme, {
|
||||
fillColor: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
color: 'rgb(65, 89, 158)',
|
||||
active: {
|
||||
borderColor: 'rgb(251, 227, 188)',
|
||||
borderWidth: 3
|
||||
}
|
||||
color: 'rgb(65, 89, 158)'
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -29,27 +25,17 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(65, 89, 158)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: 'rgb(65, 89, 158)',
|
||||
active: {
|
||||
borderColor: 'rgb(251, 227, 188)'
|
||||
}
|
||||
color: 'rgb(65, 89, 158)'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: '#fff',
|
||||
borderColor: 'transparent',
|
||||
color: 'rgb(65, 89, 158)',
|
||||
active: {
|
||||
borderColor: 'rgb(251, 227, 188)'
|
||||
}
|
||||
color: 'rgb(65, 89, 158)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -17,11 +17,7 @@ export default merge(defaultTheme, {
|
||||
fillColor: 'rgb(255, 112, 52)',
|
||||
color: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
active: {
|
||||
borderColor: 'rgb(51, 51, 51)',
|
||||
borderWidth: 3
|
||||
}
|
||||
borderWidth: 0
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
@@ -29,27 +25,17 @@ export default merge(defaultTheme, {
|
||||
color: 'rgb(51, 51, 51)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 14,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 112, 52)',
|
||||
borderWidth: 2
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 12,
|
||||
color: '#222',
|
||||
active: {
|
||||
borderColor: 'rgb(255, 112, 52)'
|
||||
}
|
||||
color: '#222'
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fillColor: 'rgb(255, 222, 69)',
|
||||
borderColor: 'transparent',
|
||||
color: 'rgb(51, 51, 51)',
|
||||
active: {
|
||||
borderColor: 'rgb(255, 112, 52)'
|
||||
}
|
||||
color: 'rgb(51, 51, 51)'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
// LRU缓存类
|
||||
export default class CRU {
|
||||
constructor(max) {
|
||||
this.max = max || 1000
|
||||
this.size = 0
|
||||
this.pool = new Map()
|
||||
}
|
||||
export default class Lru {
|
||||
constructor(max) {
|
||||
this.max = max || 1000
|
||||
this.size = 0
|
||||
this.pool = new Map()
|
||||
}
|
||||
|
||||
add(key, value) {
|
||||
// 如果该key是否已经存在,则先删除
|
||||
this.delete(key)
|
||||
this.pool.set(key, value)
|
||||
this.size++
|
||||
// 如果数量超出最大值,则删除最早的
|
||||
if (this.size > this.max) {
|
||||
let keys = this.pool.keys()
|
||||
let last = keys.next()
|
||||
this.delete(last.value)
|
||||
}
|
||||
|
||||
add(key, value) {
|
||||
// 如果该key是否已经存在,则先删除
|
||||
this.delete(key)
|
||||
this.pool.set(key, value)
|
||||
this.size++
|
||||
// 如果数量超出最大值,则删除最早的
|
||||
if (this.size > this.max) {
|
||||
let keys = this.pool.keys()
|
||||
let last = keys.next()
|
||||
this.delete(last.value)
|
||||
}
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
if (this.pool.has(key)) {
|
||||
this.pool.delete(key)
|
||||
this.size--
|
||||
}
|
||||
delete(key) {
|
||||
if (this.pool.has(key)) {
|
||||
this.pool.delete(key)
|
||||
this.size--
|
||||
}
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this.pool.has(key)
|
||||
}
|
||||
has(key) {
|
||||
return this.pool.has(key)
|
||||
}
|
||||
|
||||
get(key) {
|
||||
if (this.pool.has(key)) {
|
||||
return this.pool.get(key)
|
||||
}
|
||||
get(key) {
|
||||
if (this.pool.has(key)) {
|
||||
return this.pool.get(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { nodeDataNoStylePropList } from '../constants/constant'
|
||||
import MersenneTwister from './mersenneTwister'
|
||||
// 深度优先遍历树
|
||||
export const walk = (
|
||||
root,
|
||||
@@ -31,9 +34,11 @@ export const walk = (
|
||||
|
||||
// 广度优先遍历树
|
||||
export const bfsWalk = (root, callback) => {
|
||||
callback(root)
|
||||
let stack = [root]
|
||||
let isStop = false
|
||||
if (callback(root, null) === 'stop') {
|
||||
isStop = true
|
||||
}
|
||||
while (stack.length) {
|
||||
if (isStop) {
|
||||
break
|
||||
@@ -41,8 +46,9 @@ export const bfsWalk = (root, callback) => {
|
||||
let cur = stack.shift()
|
||||
if (cur.children && cur.children.length) {
|
||||
cur.children.forEach(item => {
|
||||
if (isStop) return
|
||||
stack.push(item)
|
||||
if (callback(item) === 'stop') {
|
||||
if (callback(item, cur) === 'stop') {
|
||||
isStop = true
|
||||
}
|
||||
})
|
||||
@@ -50,6 +56,26 @@ export const bfsWalk = (root, callback) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 按原比例缩放图片
|
||||
export const resizeImgSizeByOriginRatio = (
|
||||
width,
|
||||
height,
|
||||
newWidth,
|
||||
newHeight
|
||||
) => {
|
||||
let arr = []
|
||||
let nRatio = width / height
|
||||
let mRatio = newWidth / newHeight
|
||||
if (nRatio > mRatio) {
|
||||
// 固定高度
|
||||
arr = [nRatio * newHeight, newHeight]
|
||||
} else {
|
||||
// 固定宽度
|
||||
arr = [newWidth, newWidth / nRatio]
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// 缩放图片尺寸
|
||||
export const resizeImgSize = (width, height, maxWidth, maxHeight) => {
|
||||
let nRatio = width / height
|
||||
@@ -126,6 +152,9 @@ export const copyRenderTree = (tree, root, removeActiveState = false) => {
|
||||
tree.data = simpleDeepClone(root.data)
|
||||
if (removeActiveState) {
|
||||
tree.data.isActive = false
|
||||
if (tree.data.generalization) {
|
||||
tree.data.generalization.isActive = false
|
||||
}
|
||||
}
|
||||
tree.children = []
|
||||
if (root.children && root.children.length > 0) {
|
||||
@@ -137,18 +166,27 @@ export const copyRenderTree = (tree, root, removeActiveState = false) => {
|
||||
}
|
||||
|
||||
// 复制节点树数据
|
||||
export const copyNodeTree = (tree, root, removeActiveState = false, keepId = false) => {
|
||||
export const copyNodeTree = (
|
||||
tree,
|
||||
root,
|
||||
removeActiveState = false,
|
||||
removeId = true
|
||||
) => {
|
||||
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data)
|
||||
// 去除节点id,因为节点id不能重复
|
||||
if (tree.data.id && !keepId) delete tree.data.id
|
||||
if (tree.data.uid) delete tree.data.uid
|
||||
// 移除节点uid
|
||||
if (removeId) {
|
||||
delete tree.data.uid
|
||||
} else if (!tree.data.uid) {
|
||||
// 否则保留或生成
|
||||
tree.data.uid = createUid()
|
||||
}
|
||||
if (removeActiveState) {
|
||||
tree.data.isActive = false
|
||||
}
|
||||
tree.children = []
|
||||
if (root.children && root.children.length > 0) {
|
||||
root.children.forEach((item, index) => {
|
||||
tree.children[index] = copyNodeTree({}, item, removeActiveState, keepId)
|
||||
tree.children[index] = copyNodeTree({}, item, removeActiveState, removeId)
|
||||
})
|
||||
} else if (
|
||||
root.nodeData &&
|
||||
@@ -156,7 +194,7 @@ export const copyNodeTree = (tree, root, removeActiveState = false, keepId = fal
|
||||
root.nodeData.children.length > 0
|
||||
) {
|
||||
root.nodeData.children.forEach((item, index) => {
|
||||
tree.children[index] = copyNodeTree({}, item, removeActiveState, keepId)
|
||||
tree.children[index] = copyNodeTree({}, item, removeActiveState, removeId)
|
||||
})
|
||||
}
|
||||
return tree
|
||||
@@ -188,6 +226,18 @@ export const imgToDataUrl = src => {
|
||||
})
|
||||
}
|
||||
|
||||
// 解析dataUrl
|
||||
export const parseDataUrl = data => {
|
||||
if (!/^data:/.test(data)) return data
|
||||
let [typeStr, base64] = data.split(',')
|
||||
let res = /^data:[^/]+\/([^;]+);/.exec(typeStr)
|
||||
let type = res[1]
|
||||
return {
|
||||
type,
|
||||
base64
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
export const downloadFile = (file, fileName) => {
|
||||
let a = document.createElement('a')
|
||||
@@ -236,8 +286,8 @@ export const degToRad = deg => {
|
||||
return deg * (Math.PI / 180)
|
||||
}
|
||||
|
||||
// 驼峰转连字符
|
||||
export const camelCaseToHyphen = (str) => {
|
||||
// 驼峰转连字符
|
||||
export const camelCaseToHyphen = str => {
|
||||
return str.replace(/([a-z])([A-Z])/g, (...args) => {
|
||||
return args[1] + '-' + args[2].toLowerCase()
|
||||
})
|
||||
@@ -258,11 +308,8 @@ export const measureText = (text, { italic, bold, fontSize, fontFamily }) => {
|
||||
}
|
||||
measureTextContext.save()
|
||||
measureTextContext.font = font
|
||||
const {
|
||||
width,
|
||||
actualBoundingBoxAscent,
|
||||
actualBoundingBoxDescent
|
||||
} = measureTextContext.measureText(text)
|
||||
const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } =
|
||||
measureTextContext.measureText(text)
|
||||
measureTextContext.restore()
|
||||
const height = actualBoundingBoxAscent + actualBoundingBoxDescent
|
||||
return { width, height }
|
||||
@@ -270,7 +317,9 @@ export const measureText = (text, { italic, bold, fontSize, fontFamily }) => {
|
||||
|
||||
// 拼接font字符串
|
||||
export const joinFontStr = ({ italic, bold, fontSize, fontFamily }) => {
|
||||
return `${italic ? 'italic ' : ''} ${bold ? 'bold ' : ''} ${fontSize}px ${fontFamily} `
|
||||
return `${italic ? 'italic ' : ''} ${
|
||||
bold ? 'bold ' : ''
|
||||
} ${fontSize}px ${fontFamily} `
|
||||
}
|
||||
|
||||
// 在下一个事件循环里执行任务
|
||||
@@ -336,7 +385,7 @@ export const checkNodeOuter = (mindMap, node) => {
|
||||
|
||||
// 提取html字符串里的纯文本
|
||||
let getTextFromHtmlEl = null
|
||||
export const getTextFromHtml = (html) => {
|
||||
export const getTextFromHtml = html => {
|
||||
if (!getTextFromHtmlEl) {
|
||||
getTextFromHtmlEl = document.createElement('div')
|
||||
}
|
||||
@@ -345,15 +394,598 @@ export const getTextFromHtml = (html) => {
|
||||
}
|
||||
|
||||
// 将blob转成data:url
|
||||
export const readBlob = (blob) => {
|
||||
export const readBlob = blob => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader()
|
||||
reader.onload = (evt) => {
|
||||
reader.onload = evt => {
|
||||
resolve(evt.target.result)
|
||||
}
|
||||
reader.onerror = (err) => {
|
||||
reader.onerror = err => {
|
||||
reject(err)
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 将dom节点转换成html字符串
|
||||
let nodeToHTMLWrapEl = null
|
||||
export const nodeToHTML = node => {
|
||||
if (!nodeToHTMLWrapEl) {
|
||||
nodeToHTMLWrapEl = document.createElement('div')
|
||||
}
|
||||
nodeToHTMLWrapEl.innerHTML = ''
|
||||
nodeToHTMLWrapEl.appendChild(node)
|
||||
return nodeToHTMLWrapEl.innerHTML
|
||||
}
|
||||
|
||||
// 获取图片大小
|
||||
export const getImageSize = src => {
|
||||
return new Promise(resolve => {
|
||||
let img = new Image()
|
||||
img.src = src
|
||||
img.onload = () => {
|
||||
resolve({
|
||||
width: img.width,
|
||||
height: img.height
|
||||
})
|
||||
}
|
||||
img.onerror = () => {
|
||||
resolve({
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建节点唯一的id
|
||||
export const createUid = () => {
|
||||
return uuidv4()
|
||||
}
|
||||
|
||||
// 加载图片文件
|
||||
export const loadImage = imgFile => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let fr = new FileReader()
|
||||
fr.readAsDataURL(imgFile)
|
||||
fr.onload = async e => {
|
||||
let url = e.target.result
|
||||
let size = await getImageSize(url)
|
||||
resolve({
|
||||
url,
|
||||
size
|
||||
})
|
||||
}
|
||||
fr.onerror = error => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 移除字符串中的html实体
|
||||
export const removeHTMLEntities = str => {
|
||||
;[[' ', ' ']].forEach(item => {
|
||||
str = str.replaceAll(item[0], item[1])
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
// 获取一个数据的类型
|
||||
export const getType = data => {
|
||||
return Object.prototype.toString.call(data).slice(8, -1)
|
||||
}
|
||||
|
||||
// 判断一个数据是否是null和undefined和空字符串
|
||||
export const isUndef = data => {
|
||||
return data === null || data === undefined || data === ''
|
||||
}
|
||||
|
||||
// 移除html字符串中节点的内联样式
|
||||
export const removeHtmlStyle = html => {
|
||||
return html.replaceAll(/(<[^\s]+)\s+style=["'][^'"]+["']\s*(>)/g, '$1$2')
|
||||
}
|
||||
|
||||
// 给html标签中指定的标签添加内联样式
|
||||
export const addHtmlStyle = (html, tag, style) => {
|
||||
const reg = new RegExp(`(<${tag}[^>]*)(>[^<>]*</${tag}>)`, 'g')
|
||||
return html.replaceAll(reg, `$1 style="${style}"$2`)
|
||||
}
|
||||
|
||||
// 检查一个字符串是否是富文本字符
|
||||
let checkIsRichTextEl = null
|
||||
export const checkIsRichText = str => {
|
||||
if (!checkIsRichTextEl) {
|
||||
checkIsRichTextEl = document.createElement('div')
|
||||
}
|
||||
checkIsRichTextEl.innerHTML = str
|
||||
for (let c = checkIsRichTextEl.childNodes, i = c.length; i--; ) {
|
||||
if (c[i].nodeType == 1) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 搜索和替换html字符串中指定的文本
|
||||
let replaceHtmlTextEl = null
|
||||
export const replaceHtmlText = (html, searchText, replaceText) => {
|
||||
if (!replaceHtmlTextEl) {
|
||||
replaceHtmlTextEl = document.createElement('div')
|
||||
}
|
||||
replaceHtmlTextEl.innerHTML = html
|
||||
let walk = root => {
|
||||
let childNodes = root.childNodes
|
||||
childNodes.forEach(node => {
|
||||
if (node.nodeType === 1) {
|
||||
// 元素节点
|
||||
walk(node)
|
||||
} else if (node.nodeType === 3) {
|
||||
// 文本节点
|
||||
root.replaceChild(
|
||||
document.createTextNode(
|
||||
node.nodeValue.replaceAll(searchText, replaceText)
|
||||
),
|
||||
node
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
walk(replaceHtmlTextEl)
|
||||
return replaceHtmlTextEl.innerHTML
|
||||
}
|
||||
|
||||
// 判断一个颜色是否是白色
|
||||
export const isWhite = color => {
|
||||
color = String(color).replaceAll(/\s+/g, '')
|
||||
return (
|
||||
['#fff', '#ffffff', '#FFF', '#FFFFFF', 'rgb(255,255,255)'].includes(
|
||||
color
|
||||
) || /rgba\(255,255,255,[^)]+\)/.test(color)
|
||||
)
|
||||
}
|
||||
|
||||
// 判断一个颜色是否是透明
|
||||
export const isTransparent = color => {
|
||||
color = String(color).replaceAll(/\s+/g, '')
|
||||
return (
|
||||
['', 'transparent'].includes(color) || /rgba\(\d+,\d+,\d+,0\)/.test(color)
|
||||
)
|
||||
}
|
||||
|
||||
// 从当前主题里获取一个非透明非白色的颜色
|
||||
export const getVisibleColorFromTheme = themeConfig => {
|
||||
let { lineColor, root, second, node } = themeConfig
|
||||
let list = [
|
||||
lineColor,
|
||||
root.fillColor,
|
||||
root.color,
|
||||
second.fillColor,
|
||||
second.color,
|
||||
node.fillColor,
|
||||
node.color,
|
||||
root.borderColor,
|
||||
second.borderColor,
|
||||
node.borderColor
|
||||
]
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let color = list[i]
|
||||
if (!isTransparent(color) && !isWhite(color)) {
|
||||
return color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本
|
||||
let nodeRichTextToTextWithWrapEl = null
|
||||
export const nodeRichTextToTextWithWrap = html => {
|
||||
if (!nodeRichTextToTextWithWrapEl) {
|
||||
nodeRichTextToTextWithWrapEl = document.createElement('div')
|
||||
}
|
||||
nodeRichTextToTextWithWrapEl.innerHTML = html
|
||||
const childNodes = nodeRichTextToTextWithWrapEl.childNodes
|
||||
let res = ''
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const node = childNodes[i]
|
||||
if (node.nodeType === 1) {
|
||||
// 元素节点
|
||||
if (node.tagName.toLowerCase() === 'p') {
|
||||
res += node.textContent + '\n'
|
||||
} else {
|
||||
res += node.textContent
|
||||
}
|
||||
} else if (node.nodeType === 3) {
|
||||
// 文本节点
|
||||
res += node.nodeValue
|
||||
}
|
||||
}
|
||||
return res.replace(/\n$/, '')
|
||||
}
|
||||
|
||||
// 将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容
|
||||
let textToNodeRichTextWithWrapEl = null
|
||||
export const textToNodeRichTextWithWrap = html => {
|
||||
if (!textToNodeRichTextWithWrapEl) {
|
||||
textToNodeRichTextWithWrapEl = document.createElement('div')
|
||||
}
|
||||
textToNodeRichTextWithWrapEl.innerHTML = html
|
||||
const childNodes = textToNodeRichTextWithWrapEl.childNodes
|
||||
let list = []
|
||||
let str = ''
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const node = childNodes[i]
|
||||
if (node.nodeType === 1) {
|
||||
// 元素节点
|
||||
if (node.tagName.toLowerCase() === 'br') {
|
||||
list.push(str)
|
||||
str = ''
|
||||
} else {
|
||||
str += node.textContent
|
||||
}
|
||||
} else if (node.nodeType === 3) {
|
||||
// 文本节点
|
||||
str += node.nodeValue
|
||||
}
|
||||
}
|
||||
if (str) {
|
||||
list.push(str)
|
||||
}
|
||||
return list
|
||||
.map(item => {
|
||||
return `<p><span>${htmlEscape(item)}</span></p>`
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
|
||||
// 判断是否是移动端环境
|
||||
export const isMobile = () => {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
}
|
||||
|
||||
// 获取对象改变了的的属性
|
||||
export const getObjectChangedProps = (oldObject, newObject) => {
|
||||
const res = {}
|
||||
Object.keys(newObject).forEach(prop => {
|
||||
const oldVal = oldObject[prop]
|
||||
const newVal = newObject[prop]
|
||||
if (getType(oldVal) !== getType(newVal)) {
|
||||
res[prop] = newVal
|
||||
return
|
||||
}
|
||||
if (getType(oldVal) === 'Object') {
|
||||
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
||||
res[prop] = newVal
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (oldVal !== newVal) {
|
||||
res[prop] = newVal
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 判断一个字段是否是节点数据中的样式字段
|
||||
export const checkIsNodeStyleDataKey = key => {
|
||||
// 用户自定义字段
|
||||
if (/^_/.test(key)) return false
|
||||
// 不在节点非样式字段列表里,那么就是样式字段
|
||||
if (!nodeDataNoStylePropList.includes(key)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 合并图标数组
|
||||
// const data = [
|
||||
// { type: 'priority', name: '优先级图标', list: [{ name: '1', icon: 'a' }, { name: 2, icon: 'b' }] },
|
||||
// { type: 'priority', name: '优先级图标', list: [{ name: '2', icon: 'c' }, { name: 3, icon: 'd' }] },
|
||||
// ];
|
||||
|
||||
// mergerIconList(data) 结果
|
||||
|
||||
// [
|
||||
// { type: 'priority', name: '优先级图标', list: [{ name: '1', icon: 'a' }, { name: 2, icon: 'c' }, { name: 3, icon: 'd' }] },
|
||||
// ]
|
||||
export const mergerIconList = list => {
|
||||
return list.reduce((result, item) => {
|
||||
const existingItem = result.find(x => x.type === item.type)
|
||||
if (existingItem) {
|
||||
item.list.forEach(newObj => {
|
||||
const existingObj = existingItem.list.find(x => x.name === newObj.name)
|
||||
if (existingObj) {
|
||||
existingObj.icon = newObj.icon
|
||||
} else {
|
||||
existingItem.list.push(newObj)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
result.push({ ...item })
|
||||
}
|
||||
return result
|
||||
}, [])
|
||||
}
|
||||
|
||||
// 从节点实例列表里找出顶层的节点
|
||||
export const getTopAncestorsFomNodeList = list => {
|
||||
let res = []
|
||||
list.forEach(node => {
|
||||
if (
|
||||
!list.find(item => {
|
||||
return item.uid !== node.uid && item.isAncestor(node)
|
||||
})
|
||||
) {
|
||||
res.push(node)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 从给定的节点实例列表里判断是否存在上下级关系
|
||||
export const checkHasSupSubRelation = list => {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const cur = list[i]
|
||||
if (
|
||||
list.find(item => {
|
||||
return item.uid !== cur.uid && cur.isAncestor(item)
|
||||
})
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 解析要添加概要的节点实例列表
|
||||
export const parseAddGeneralizationNodeList = list => {
|
||||
const cache = {}
|
||||
const uidToParent = {}
|
||||
list.forEach(node => {
|
||||
const parent = node.parent
|
||||
if (parent) {
|
||||
const pUid = parent.uid
|
||||
uidToParent[pUid] = parent
|
||||
const index = node.getIndexInBrothers()
|
||||
const data = {
|
||||
node,
|
||||
index
|
||||
}
|
||||
if (cache[pUid]) {
|
||||
if (
|
||||
!cache[pUid].find(item => {
|
||||
return item.index === data.index
|
||||
})
|
||||
) {
|
||||
cache[pUid].push(data)
|
||||
}
|
||||
} else {
|
||||
cache[pUid] = [data]
|
||||
}
|
||||
}
|
||||
})
|
||||
const res = []
|
||||
Object.keys(cache).forEach(uid => {
|
||||
if (cache[uid].length > 1) {
|
||||
const rangeList = cache[uid]
|
||||
.map(item => {
|
||||
return item.index
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a - b
|
||||
})
|
||||
res.push({
|
||||
node: uidToParent[uid],
|
||||
range: [rangeList[0], rangeList[rangeList.length - 1]]
|
||||
})
|
||||
} else {
|
||||
res.push({
|
||||
node: cache[uid][0].node
|
||||
})
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 判断两个矩形是否重叠
|
||||
export const checkTwoRectIsOverlap = (
|
||||
minx1,
|
||||
maxx1,
|
||||
miny1,
|
||||
maxy1,
|
||||
minx2,
|
||||
maxx2,
|
||||
miny2,
|
||||
maxy2
|
||||
) => {
|
||||
return maxx1 > minx2 && maxx2 > minx1 && maxy1 > miny2 && maxy2 > miny1
|
||||
}
|
||||
|
||||
// 聚焦指定输入框
|
||||
export const focusInput = el => {
|
||||
let selection = window.getSelection()
|
||||
let range = document.createRange()
|
||||
range.selectNodeContents(el)
|
||||
range.collapse()
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
|
||||
// 聚焦全选指定输入框
|
||||
export const selectAllInput = el => {
|
||||
let selection = window.getSelection()
|
||||
let range = document.createRange()
|
||||
range.selectNodeContents(el)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
|
||||
// 给指定的节点列表树数据添加附加数据,会修改原数据
|
||||
export const addDataToAppointNodes = (appointNodes, data = {}) => {
|
||||
const walk = list => {
|
||||
list.forEach(node => {
|
||||
node.data = {
|
||||
...node.data,
|
||||
...data
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
walk(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
walk(appointNodes)
|
||||
return appointNodes
|
||||
}
|
||||
|
||||
// 给指定的节点列表树数据添加uid,会修改原数据
|
||||
// createNewId默认为false,即如果节点不存在uid的话,会创建新的uid。如果传true,那么无论节点数据原来是否存在uid,都会创建新的uid
|
||||
export const createUidForAppointNodes = (appointNodes, createNewId = false) => {
|
||||
const walk = list => {
|
||||
list.forEach(node => {
|
||||
if (!node.data) {
|
||||
node.data = {}
|
||||
}
|
||||
if (createNewId || isUndef(node.data.uid)) {
|
||||
node.data.uid = createUid()
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
walk(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
walk(appointNodes)
|
||||
return appointNodes
|
||||
}
|
||||
|
||||
// 传入一个数据,如果该数据是数组,那么返回该数组,否则返回一个以该数据为成员的数组
|
||||
export const formatDataToArray = data => {
|
||||
if (!data) return []
|
||||
return Array.isArray(data) ? data : [data]
|
||||
}
|
||||
|
||||
// 获取节点在同级里的位置索引
|
||||
export const getNodeDataIndex = node => {
|
||||
return node.parent
|
||||
? node.parent.nodeData.children.findIndex(item => {
|
||||
return item.data.uid === node.uid
|
||||
})
|
||||
: 0
|
||||
}
|
||||
|
||||
// 从一个节点列表里找出某个节点的索引
|
||||
export const getNodeIndexInNodeList = (node, nodeList) => {
|
||||
return nodeList.findIndex(item => {
|
||||
return item.uid === node.uid
|
||||
})
|
||||
}
|
||||
|
||||
// 根据内容生成颜色
|
||||
export const generateColorByContent = str => {
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash)
|
||||
}
|
||||
// 这里使用伪随机数的原因是因为
|
||||
// 1. 如果字符串的内容差不多,根据hash生产的颜色就比较相近,不好区分,比如v1.1 v1.2,所以需要加入随机数来使得颜色能够区分开
|
||||
// 2. 普通的随机数每次数值不一样,就会导致每次新增标签原来的标签颜色就会发生改变,所以加入了这个方法,使得内容不变随机数也不变
|
||||
const rng = new MersenneTwister(hash)
|
||||
const h = rng.genrand_int32() % 360
|
||||
return 'hsla(' + h + ', 50%, 50%, 1)'
|
||||
}
|
||||
|
||||
// html转义
|
||||
export const htmlEscape = str => {
|
||||
;[
|
||||
['&', '&'],
|
||||
['<', '<'],
|
||||
['>', '>']
|
||||
].forEach(item => {
|
||||
str = str.replace(new RegExp(item[0], 'g'), item[1])
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
// 判断两个对象是否相同,只处理对象或数组
|
||||
export const isSameObject = (a, b) => {
|
||||
const type = getType(a)
|
||||
// a、b类型不一致,那么肯定不相同
|
||||
if (type !== getType(b)) return false
|
||||
// 如果都是对象
|
||||
if (type === 'Object') {
|
||||
const keysa = Object.keys(a)
|
||||
const keysb = Object.keys(b)
|
||||
// 对象字段数量不一样,肯定不相同
|
||||
if (keysa.length !== keysb.length) return false
|
||||
// 字段数量一样,那么需要遍历字段进行判断
|
||||
for (let i = 0; i < keysa.length; i++) {
|
||||
const key = keysa[i]
|
||||
// b没有a的一个字段,那么肯定不相同
|
||||
if (!keysb.includes(key)) return false
|
||||
// 字段名称一样,那么需要递归判断它们的值
|
||||
const isSame = isSameObject(a[key], b[key])
|
||||
if (!isSame) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else if (type === 'Array') {
|
||||
// 如果都是数组
|
||||
// 数组长度不一样,肯定不相同
|
||||
if (a.length !== b.length) return false
|
||||
// 长度一样,那么需要遍历进行判断
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const itema = a[i]
|
||||
const itemb = b[i]
|
||||
const typea = getType(itema)
|
||||
const typeb = getType(itemb)
|
||||
if (typea !== typeb) return false
|
||||
const isSame = isSameObject(itema, itemb)
|
||||
if (!isSame) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
// 其他类型,直接全等判断
|
||||
return a === b
|
||||
}
|
||||
}
|
||||
|
||||
// 将数据设置到用户剪切板中
|
||||
export const setDataToClipboard = data => {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(JSON.stringify(data))
|
||||
}
|
||||
}
|
||||
|
||||
// 从用户剪贴板中读取文字和图片
|
||||
export const getDataFromClipboard = async () => {
|
||||
let text = null
|
||||
let img = null
|
||||
if (navigator.clipboard) {
|
||||
text = await navigator.clipboard.readText()
|
||||
const items = await navigator.clipboard.read()
|
||||
if (items && items.length > 0) {
|
||||
for (const clipboardItem of items) {
|
||||
for (const type of clipboardItem.types) {
|
||||
if (/^image\//.test(type)) {
|
||||
img = await clipboardItem.getType(type)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
text,
|
||||
img
|
||||
}
|
||||
}
|
||||
|
||||
// 从节点的父节点的nodeData.children列表中移除该节点的数据
|
||||
export const removeFromParentNodeData = node => {
|
||||
if (!node || !node.parent) return
|
||||
const index = getNodeDataIndex(node)
|
||||
if (index === -1) return
|
||||
node.parent.nodeData.children.splice(index, 1)
|
||||
}
|
||||
|
||||
27908
simple-mind-map/src/utils/jspdf.js
Normal file
27908
simple-mind-map/src/utils/jspdf.js
Normal file
File diff suppressed because one or more lines are too long
65
simple-mind-map/src/utils/mersenneTwister.js
Normal file
65
simple-mind-map/src/utils/mersenneTwister.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @description 为了保证相同的内容每次生成的随机数都是一样的,我们可以使用一个伪随机数生成器(PRNG),并使用内容的哈希值作为种子。以下是一个使用Mersenne Twister算法的PRNG的实现:
|
||||
*
|
||||
* @param {*} seed
|
||||
*/
|
||||
|
||||
export default function MersenneTwister(seed) {
|
||||
this.N = 624
|
||||
this.M = 397
|
||||
this.MATRIX_A = 0x9908b0df
|
||||
this.UPPER_MASK = 0x80000000
|
||||
this.LOWER_MASK = 0x7fffffff
|
||||
|
||||
this.mt = new Array(this.N)
|
||||
this.mti = this.N + 1
|
||||
|
||||
this.init_genrand(seed)
|
||||
}
|
||||
|
||||
MersenneTwister.prototype.init_genrand = function (s) {
|
||||
this.mt[0] = s >>> 0
|
||||
for (this.mti = 1; this.mti < this.N; this.mti++) {
|
||||
s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30)
|
||||
this.mt[this.mti] =
|
||||
((((s & 0xffff0000) >>> 16) * 1812433253) << 16) +
|
||||
(s & 0x0000ffff) * 1812433253 +
|
||||
this.mti
|
||||
this.mt[this.mti] >>>= 0
|
||||
}
|
||||
}
|
||||
|
||||
MersenneTwister.prototype.genrand_int32 = function () {
|
||||
var y
|
||||
var mag01 = new Array(0x0, this.MATRIX_A)
|
||||
|
||||
if (this.mti >= this.N) {
|
||||
var kk
|
||||
|
||||
if (this.mti == this.N + 1) this.init_genrand(5489)
|
||||
|
||||
for (kk = 0; kk < this.N - this.M; kk++) {
|
||||
y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK)
|
||||
this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 0x1]
|
||||
}
|
||||
|
||||
for (; kk < this.N - 1; kk++) {
|
||||
y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK)
|
||||
this.mt[kk] = this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 0x1]
|
||||
}
|
||||
|
||||
y = (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK)
|
||||
this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 0x1]
|
||||
|
||||
this.mti = 0
|
||||
}
|
||||
|
||||
y = this.mt[this.mti++]
|
||||
|
||||
y ^= y >>> 11
|
||||
y ^= (y << 7) & 0x9d2c5680
|
||||
y ^= (y << 15) & 0xefc60000
|
||||
y ^= y >>> 18
|
||||
|
||||
return y >>> 0
|
||||
}
|
||||
@@ -351,4 +351,4 @@ const drawBackgroundImageToCanvas = (
|
||||
}
|
||||
}
|
||||
|
||||
export default drawBackgroundImageToCanvas
|
||||
export default drawBackgroundImageToCanvas
|
||||
|
||||
253
simple-mind-map/src/utils/xmind.js
Normal file
253
simple-mind-map/src/utils/xmind.js
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user