Compare commits

...

409 Commits

Author SHA1 Message Date
wanglin2
516676b484 更新群二维码 2023-08-04 20:14:26 +08:00
wanglin2
10ed3d4f7c Merge branch 'feature' into main 2023-08-01 18:55:55 +08:00
wanglin2
8fc7f7d32c 打包 2023-08-01 18:55:32 +08:00
wanglin2
27a0efa4e0 Demo:1.打包不修改静态资源的文件名;2.支持运行时设置静态资源路径 2023-08-01 18:54:40 +08:00
wanglin2
7d227e901a Merge branch 'feature' into main 2023-08-01 09:51:30 +08:00
wanglin2
080d7489e7 打包 2023-08-01 09:51:12 +08:00
wanglin2
b11bd5a7ef Demo:提供应用接管模式,方便对接自己的存储服务 2023-08-01 09:49:44 +08:00
wanglin2
80ae38d295 Merge branch 'feature' into main 2023-07-31 09:48:07 +08:00
wanglin2
a4b7915196 Doc:update 2023-07-31 09:45:20 +08:00
wanglin2
4d4f1b993e Merge branch 'feature' into main 2023-07-30 21:55:37 +08:00
wanglin2
b814bd35ca 打包0.6.11-fix.1 2023-07-30 21:55:20 +08:00
wanglin2
c57361a360 Fix:修复节点文字为白色时编辑的时候看不见的问题 2023-07-30 21:53:32 +08:00
wanglin2
f8c2a62bd6 Merge branch 'feature' into main 2023-07-30 10:58:19 +08:00
wanglin2
59241717f5 打包0.6.11 2023-07-30 10:57:42 +08:00
wanglin2
e90509cac9 Doc: update 2023-07-30 10:56:35 +08:00
wanglin2
8a8cc26c1d Feat:优化小地图,去除小地图内的节点内容,优化性能 2023-07-30 10:51:38 +08:00
wanglin2
1d443b9f94 Doc: update 2023-07-30 09:44:00 +08:00
wanglin2
60a4f443a7 Demo:节点图片上传支持输入网络图片 2023-07-30 09:41:58 +08:00
wanglin2
72c2540dcc Merge branch 'feature' into main 2023-07-29 23:18:46 +08:00
wanglin2
18cec3b75a 打包 2023-07-29 23:18:18 +08:00
wanglin2
d4aae5268e Demo:1.新增主题;2.主题列表新增tab分类 2023-07-29 23:16:59 +08:00
wanglin2
99ca0738d4 Merge branch 'feature' into main 2023-07-29 19:50:07 +08:00
wanglin2
408ca6d711 Doc:updagte 2023-07-29 19:49:08 +08:00
wanglin2
99dcb55397 Merge branch 'feature' into main 2023-07-29 19:23:43 +08:00
wanglin2
e11b6647b8 打包0.6.10 2023-07-29 19:22:40 +08:00
wanglin2
78f5d4ec88 Demo:支持点击节点内的图标显示一个图标快捷替换和删除悬浮面板 2023-07-29 16:08:12 +08:00
wanglin2
20eba7b29b Demo:修复在搜索框回车后输入框焦点丢失的问题 2023-07-29 14:24:50 +08:00
wanglin2
4c9698a147 Fix:1.修复内部数据深拷贝位置不正确的问题;2.修复富文本节点换行不生效的问题;3.修复切换主题等场景时节点换行会丢失的问题; 2023-07-29 14:23:57 +08:00
wanglin2
55da8eac83 Fix:修复调整图片大小的按钮在节点操作后没有更新的问题 2023-07-29 10:45:25 +08:00
wanglin2
17ea049393 Demo:调整添加节点图标的UI;新增节点图标;新增贴纸 2023-07-29 10:36:57 +08:00
wanglin2
d2e468287d Demo:优化搜索,鼠标不在搜索区域内不聚焦,解决鼠标不在搜索区域内无法删除输入的文字的问题 2023-07-28 21:29:05 +08:00
wanglin2
ad6085890e Demo:1.支持通过图标调出搜索;2.支持通过图标切换暗黑模式 2023-07-28 17:45:53 +08:00
wanglin2
cd4f1b1bd8 Feat:搜索支持搜索空白字符和替换为空白字符 2023-07-28 17:34:55 +08:00
wanglin2
a2acf810cb Fix:修复搜索定位到某个节点后删除该节点,再次搜索搜索结果未更新的问题 2023-07-28 17:05:44 +08:00
wanglin2
14bd7c3705 Merge branch 'feature' into main 2023-07-28 16:47:17 +08:00
wanglin2
5d0c9dcab1 打包0.6.9-fix.1 2023-07-28 16:46:38 +08:00
wanglin2
def2f02eea 1.修复搜索进行一次单个替换后再全部替换不正确的问题;2.不会再直接修改传入的data对象,内部会深拷贝一份 2023-07-28 16:36:56 +08:00
wanglin2
a74a60c22d 更新群二维码 2023-07-28 09:38:00 +08:00
wanglin2
74d37f2cbc Merge branch 'feature' into main 2023-07-28 09:36:54 +08:00
wanglin2
6ca9a116c2 打包0.6.9 2023-07-28 09:36:25 +08:00
wanglin2
8e59677623 Doc: update 2023-07-28 09:33:18 +08:00
wanglin2
d06f57a5dc Demo:优化暗黑模式 2023-07-28 08:55:35 +08:00
wanglin2
07be48d342 Feat:支持搜索和替换 2023-07-28 08:50:29 +08:00
wanglin2
5a5c7702f5 修改注释 2023-07-28 08:48:51 +08:00
wanglin2
7e1a43143d Demo:切换主题时支持选择是否覆盖设置过的基础样式 2023-07-26 09:17:48 +08:00
wanglin2
5ff9137745 Fix:1.修复节点处于编辑中时添加新节点时新节点的焦点丢失问题;2.修复连续按tab键无法连续创建子节点的问题 2023-07-26 09:04:24 +08:00
wanglin2
8e30d4a94f Fix:修复自定义节点内容时,二次创建根实例时节点内容不渲染的问题 2023-07-25 18:35:41 +08:00
wanglin2
4b2ebb2e1c Doc: update 2023-07-25 16:29:14 +08:00
wanglin2
8c79ffd723 Feat:导出svg时替换svg中存在的 字符,防止导出的svg报错 2023-07-25 09:38:50 +08:00
wanglin2
839c79405f Fix:修复给概要节点设置样式概要节点会消失的问题 2023-07-25 09:25:42 +08:00
wanglin2
d645adfd37 Merge branch 'feature' into main 2023-07-24 14:20:43 +08:00
wanglin2
1b1551c6e3 Demo:完善暗黑模式 2023-07-24 14:20:29 +08:00
wanglin2
78638dc291 更新群二维码 2023-07-24 11:26:55 +08:00
wanglin2
e972924143 打包0.6.8 2023-07-24 11:25:10 +08:00
wanglin2
8871d8727b Doc: update 2023-07-24 11:21:09 +08:00
wanglin2
55945a1a2c Demo:支持根据主题自动切换为暗黑模式 2023-07-24 10:25:02 +08:00
wanglin2
d5e4044fb2 Demo:支持夜间模式 2023-07-24 10:12:53 +08:00
wanglin2
4ee458c509 Demo:修复节点正在编辑时切换富文本编辑配置输入框出现异常的问题 2023-07-24 08:53:08 +08:00
wanglin2
a76ec0dad8 Feat:修改复制、剪切、粘贴逻辑;支持粘贴剪切板中的数据 2023-07-21 17:40:09 +08:00
wanglin2
f2b247e85c Demo:修复基础样式-设置节点外边距未保存的问题 2023-07-21 14:10:40 +08:00
wanglin2
3aed09a8c3 Feat:修改插入概要的快捷键为Ctrl+G 2023-07-21 11:28:49 +08:00
wanglin2
17b7a023ba Merge branch 'main' of https://github.com/wanglin2/mind-map into feature 2023-07-19 09:02:05 +08:00
wanglin2
10cb36829f Doc: update 2023-07-19 09:01:47 +08:00
wanglin2
90a55bb995 Merge branch 'feature' into main 2023-07-18 10:55:57 +08:00
wanglin2
08a37d8971 Doc: update 2023-07-18 10:55:28 +08:00
wanglin2
89983f9f47 Merge branch 'feature' into main 2023-07-18 09:30:32 +08:00
wanglin2
b71a80e383 打包0.6.7 2023-07-18 09:29:19 +08:00
wanglin2
2bf146816b Doc:update 2023-07-18 09:21:58 +08:00
wanglin2
daf9888da4 Fix:修复节点收起再展开后展开收起按钮占位元素丢失的问题 2023-07-18 08:44:44 +08:00
wanglin2
b42cee7a2f Feat:支持根据id定位到某个节点、优化大纲的节点定位 2023-07-18 08:35:40 +08:00
wanglin2
9ecb199608 更新群二维码 2023-07-17 22:16:08 +08:00
wanglin2
adccef5699 Feat:修改节点uid的创建 2023-07-17 09:59:47 +08:00
wanglin2
94230f8ec6 Fix:修复大纲里创建新节点时节点样式丢失的问题 2023-07-17 09:43:22 +08:00
wanglin2
a161661c6b 删除节点过渡动画效果 2023-07-17 09:07:58 +08:00
wanglin2
08b971cd9a Demo:优化大纲编辑 2023-07-16 22:20:09 +08:00
wanglin2
b64c8f132b Demo:优化页面:侧边栏按钮支持收起;优化小屏适配 2023-07-15 17:29:26 +08:00
wanglin2
d6181591c5 Fix:修复只读模式下可以缩放图片的bug 2023-07-15 15:26:57 +08:00
wanglin2
e093cb1741 Merge branch 'feature' into main 2023-07-13 09:50:01 +08:00
wanglin2
a63a92c423 打包 2023-07-13 09:48:30 +08:00
wanglin2
2c4f065626 新增文档首页 2023-07-13 09:44:55 +08:00
wanglin2
ae5d4dd2a6 Merge branch 'feature' into main 2023-07-11 16:36:51 +08:00
wanglin2
e574883e5f Doc: update 2023-07-11 16:31:48 +08:00
wanglin2
1bf60c49c7 Merge branch 'feature' into main 2023-07-11 14:52:35 +08:00
wanglin2
ddc173cf84 update README 2023-07-11 14:52:15 +08:00
wanglin2
2c71c7d102 Merge branch 'feature' into main 2023-07-11 14:44:01 +08:00
wanglin2
f6cb08bdaa update Doc and README 2023-07-11 14:08:00 +08:00
wanglin2
05728de21b Merge branch 'feature' into main 2023-07-10 16:08:26 +08:00
wanglin2
b51027f641 Doc: update 2023-07-10 16:07:48 +08:00
wanglin2
ea4fdf8290 更新群二维码 2023-07-10 09:16:20 +08:00
wanglin2
55796b4e39 Merge branch 'feature' into main 2023-07-06 17:13:07 +08:00
wanglin2
e534c3138d Doc: update 2023-07-06 17:12:42 +08:00
wanglin2
e185abd223 Merge branch 'feature' into main 2023-07-06 10:49:19 +08:00
wanglin2
3b73f72866 打包0.6.6 2023-07-06 10:45:44 +08:00
wanglin2
0db2f47133 Doc: update 2023-07-06 10:37:02 +08:00
wanglin2
80c7ec0fac Demo:更新结构示意图 2023-07-06 10:05:00 +08:00
wanglin2
780ce363de Fix:修复切换结构后展开收起按钮的隐藏占位元素不更新的问题 2023-07-06 09:31:58 +08:00
wanglin2
eaa8929457 Fix:修复除了向右生长的结构,其他结构鼠标移入展开收起按钮位置时不会触发按钮显示的问题 2023-07-06 09:12:18 +08:00
wanglin2
ad0f62a5ac Demo:增加竖向时间轴图片 2023-07-05 18:11:02 +08:00
wanglin2
31f21ce013 Feat:新增竖向时间轴 2023-07-05 16:49:32 +08:00
wanglin2
7a20ce2f79 Fix:修复二级节点拖拽成3级节点时节点边框样式未更新的问题 2023-07-05 14:21:15 +08:00
wanglin2
b98e7a97ec Demo:支持导出xmind文件 2023-07-05 14:00:57 +08:00
wanglin2
0bb50b3371 Feat:1.支持导出为xmind新版文件;2.导入xmind新版文件支持处理图片 2023-07-05 13:58:55 +08:00
wanglin2
a03091b28c Fix:修复拖拽移动一个节点为另一个节点的子节点时该节点的父节点指向未更新的问题 2023-07-04 09:10:32 +08:00
wanglin2
6c33984b8c 优化:刚创建的节点默认全选方便删除默认文本 2023-07-04 08:42:31 +08:00
wanglin2
28a4be0631 Fix:TouchEvent插件去除派发click事件,解决移动端点击超链接会打开两个窗口的问题 2023-07-03 22:39:47 +08:00
wanglin2
b8a3be7a62 优化触控板缩放画布时幅度过大的问题 2023-07-03 22:26:11 +08:00
wanglin2
259d4028f3 更新群二维码 2023-07-03 08:42:37 +08:00
wanglin2
fb1251afc1 Doc: update 2023-06-29 13:51:57 +08:00
wanglin2
06e3fd428a Fix:修复缩放情况下调整图片大小不正确的问题 2023-06-28 18:22:43 +08:00
wanglin2
a48e52f1f4 打包0.6.5 2023-06-28 16:37:19 +08:00
wanglin2
f8a345d8de Doc: update 2023-06-28 16:28:47 +08:00
wanglin2
d900c2f122 Feat:节点图片支持拖拽调整大小 2023-06-28 15:03:15 +08:00
wanglin2
443a714549 Feat:支持配置鼠标滚轮方向对应的缩放行为 2023-06-27 16:46:37 +08:00
wanglin2
74618a8a2b Feat:打包后的库支持获取内置常量、主题等数据 2023-06-27 16:27:46 +08:00
wanglin2
a25bd4c556 Fix:修复xmind导入报错 2023-06-27 16:15:42 +08:00
wanglin2
efe205ae70 打包0.6.4-fix.1 2023-06-27 09:08:45 +08:00
wanglin2
a0d7473b1f Doc: update 2023-06-27 09:05:39 +08:00
wanglin2
7821781f20 Feat:鼠标滚轮缩放时默认以鼠标当前位置为中心进行缩放,可以通过配置关闭该特性 2023-06-27 09:05:25 +08:00
街角小林
b90d4ddf8a Merge pull request #152 from F-star/feat/scale-in-wheel-curor
Feat: 支持滚轮情况下,以光标为中心进行缩放
2023-06-27 08:47:40 +08:00
Hao Huang
314562c167 Feat: 支持滚轮情况下,以光标为中心进行缩放 2023-06-26 13:32:33 +00:00
wanglin2
5f0a9a7ce2 打包0.6.4 2023-06-26 16:20:38 +08:00
wanglin2
98e27841ad Doc: update 2023-06-26 16:15:20 +08:00
wanglin2
1b6467728c Feat:优化指定中心点缩放 2023-06-26 15:57:45 +08:00
街角小林
4bb349b2df Merge pull request #151 from F-star/main
Feat: 以画布为中心进行缩放
2023-06-26 15:25:08 +08:00
Hao Huang
b262336f08 Feat: 以画布为中心进行缩放 2023-06-26 05:47:06 +00:00
wanglin2
2b59087461 Merge branch 'feature' into main 2023-06-26 10:33:03 +08:00
wanglin2
66c9805efc Doc: update 2023-06-26 10:32:36 +08:00
wanglin2
710128901a 更新群二维码 2023-06-26 09:56:10 +08:00
wanglin2
61be0f7ac4 Merge branch 'feature' into main 2023-06-24 17:08:18 +08:00
wanglin2
7289b3a0ad Doc:update 2023-06-24 17:06:52 +08:00
wanglin2
25243e2053 Merge branch 'feature' into main 2023-06-21 15:56:20 +08:00
wanglin2
060a448cd5 打包0.6.3 2023-06-21 15:55:38 +08:00
wanglin2
bdb6078df6 '更新文档' 2023-06-21 15:33:51 +08:00
wanglin2
749a4d0e81 Feat:支持自定义节点内容 2023-06-21 14:57:22 +08:00
wanglin2
1749705694 Fix:修复概要节点会响应快捷键添加节点的问题 2023-06-21 10:50:26 +08:00
wanglin2
eec736be4d README: update 2023-06-20 16:43:10 +08:00
wanglin2
ffdf53941a Merge branch 'feature' into main 2023-06-20 16:39:55 +08:00
wanglin2
5676e952f3 打包0.6.2 2023-06-20 16:38:29 +08:00
wanglin2
e049ee6260 Doc: update 2023-06-20 16:35:36 +08:00
wanglin2
f1355c9d2a Fix:修复切换主题时节点样式没有随之切换的问题 2023-06-20 16:29:54 +08:00
wanglin2
d696e0fdc1 合并 2023-06-19 22:37:13 +08:00
wanglin2
c8d2f284fd 打包0.6.1 2023-06-19 22:34:19 +08:00
wanglin2
aa8ecd4f60 合并 2023-06-19 22:32:37 +08:00
wanglin2
2323fe9bc0 Fix:修复将鼠标滚动改为移动画布行为后,使用触控板操作时移动灵敏度过高的问题 2023-06-19 22:31:12 +08:00
wanglin2
b9a0b16fc8 更新群二维码 2023-06-19 11:37:28 +08:00
wanglin2
b9c340afbf Doc:更新文档 2023-06-19 09:28:36 +08:00
wanglin2
ee98d7128b Merge branch 'feature' into main 2023-06-15 17:09:52 +08:00
wanglin2
d37febe306 打包0.6.0-fix.1 2023-06-15 17:09:27 +08:00
wanglin2
5de97f05b3 更新文档 2023-06-12 18:13:05 +08:00
wanglin2
662447bc69 Fix:修复没有设置过背景样式的情况下销毁思维导图报错的问题 2023-06-12 17:42:01 +08:00
wanglin2
51c1c46287 Merge branch 'feature' into main 2023-06-12 17:07:54 +08:00
wanglin2
3b03d9798b 打包0.6.0 2023-06-12 17:06:23 +08:00
wanglin2
17fbef810c Fix:修复清空节点再输入中文时发生抖动的问题 2023-06-12 17:02:14 +08:00
wanglin2
e798975a9f 更新群二维码 2023-06-12 15:14:37 +08:00
wanglin2
2af65e322b Merge branch 'feature' into main 2023-06-12 15:12:57 +08:00
wanglin2
23f09f9b4d 打包0.6.0 2023-06-12 14:42:44 +08:00
wanglin2
d69668a488 Feat:新增触摸事件支持插件 2023-06-12 14:14:55 +08:00
wanglin2
5353888965 更新文档 2023-06-12 09:10:04 +08:00
wanglin2
b33fd1908a Fix:修复富文本编辑时删除完所有文本后再输入时样式丢失问题 2023-06-11 22:21:03 +08:00
wanglin2
97583ffcba Feat:新增销毁思维导图的方法 2023-06-11 13:14:19 +08:00
wanglin2
360eca620e Feature:将导出pdf功能提取为一个单独的插件 2023-06-11 10:41:53 +08:00
wanglin2
6d0682e821 Fix:修复左键多选节点后多选状态被取消的问题 2023-06-11 10:21:49 +08:00
wanglin2
834bdf37c3 Feat:支持控制节点是否允许编辑 2023-06-09 17:18:02 +08:00
wanglin2
e3d31f69bf Feat:支持设置为左键多选节点,右键拖动画布 2023-06-09 16:49:15 +08:00
wanglin2
ac55415de1 Fix:1.去除不需要的依赖;2.修复按住ctrl键多选节点时不会触发节点的click事件的问题 2023-06-09 13:39:07 +08:00
wanglin2
4d91be5be6 Feat:可通过配置决定是否开启按住ctrl键多选节点的功能 2023-06-08 20:18:13 +08:00
wanglin2
ee467cb155 Feat:支持一键缩放思维导图至画布大小 2023-06-08 20:06:20 +08:00
wanglin2
6281f2360c 更新文档 2023-06-08 18:27:25 +08:00
wanglin2
cd8af6ae06 合并 2023-06-08 16:51:31 +08:00
wanglin2
45cc199d7f 重新调整核心库代码目录结构 2023-06-08 16:49:54 +08:00
街角小林
b04469649a Merge pull request #145 from clh021/main
add MIT LICENSE
2023-06-08 08:59:04 +08:00
chenlianghong
ee57dbdcc5 add MIT LICENSE 2023-06-07 17:40:47 +08:00
wanglin2
bca744f6fc 更新群二维码 2023-06-06 09:14:18 +08:00
wanglin2
442408dacf 更新群二维码 2023-05-30 08:54:11 +08:00
wanglin2
7297f0a082 更新群二维码 2023-05-23 14:24:29 +08:00
wanglin2
f0d90941fb 合并 2023-05-15 10:26:33 +08:00
wanglin2
830962c044 更新群二维码 2023-05-15 10:25:34 +08:00
wanglin2
eeec71dc65 更新群二维码 2023-05-05 21:48:58 +08:00
wanglin2
9bd87a22f3 更新群二维码 2023-05-05 21:39:43 +08:00
wanglin2
88c9f22a15 Merge branch 'feature' into main 2023-05-05 21:34:40 +08:00
wanglin2
7380fc60d5 打包 2023-05-05 21:34:26 +08:00
wanglin2
a178b99d73 更新群二维码 2023-05-05 16:10:31 +08:00
wanglin2
af9ee04aaf Merge branch 'feature' into main 2023-05-02 10:05:39 +08:00
wanglin2
01dc98f1f8 打包0.5.11 2023-05-02 10:01:08 +08:00
wanglin2
cbd07246bd 更新文档 2023-05-02 10:00:13 +08:00
wanglin2
5bb23ca738 优化主题配置更新,改变不涉及节点大小的配置不触发节点重新计算 2023-04-28 22:07:53 +08:00
wanglin2
0886ba7698 完善关联线文本编辑 2023-04-28 21:51:58 +08:00
wanglin2
a65cffa58b 完善关联线文本编辑 2023-04-28 17:26:37 +08:00
wanglin2
02e2d432dd Feature:关联线支持文本开发中 2023-04-28 17:10:39 +08:00
wanglin2
c8d23cab40 更新README 2023-04-28 09:04:20 +08:00
wanglin2
244ced83bc 更新群二维码 2023-04-28 08:59:54 +08:00
wanglin2
5c9c3d7934 打包0.5.10-fix.2 2023-04-26 16:02:47 +08:00
wanglin2
4ca6713675 更新文档 2023-04-26 15:54:28 +08:00
wanglin2
3d18404fd6 Fix:修复富文本模式下,切换主题、导入数据后没有触发数据改变的问题 2023-04-26 15:49:52 +08:00
wanglin2
98dda26bf8 新增三种主题 2023-04-26 15:48:58 +08:00
wanglin2
cd4c5ecd83 'Fix:修复导入出错的问题' 2023-04-26 11:20:34 +08:00
wanglin2
fdc0017ccb '打包0.5.10' 2023-04-26 11:02:51 +08:00
wanglin2
20a5c12bbb '更新文档' 2023-04-26 10:58:01 +08:00
wanglin2
4eacec125e Feature:使用LRU缓存算法优化节点复用 2023-04-26 10:37:32 +08:00
wanglin2
34322ba6d1 更新文档 2023-04-25 17:29:56 +08:00
wanglin2
8210151a7b 打包0.5.9 2023-04-25 17:19:14 +08:00
wanglin2
1e00088608 更新文档 2023-04-25 17:14:19 +08:00
wanglin2
ee59b8002a 修改导出,使用FileReader代替URL.createObjectURL转换blob数据 2023-04-25 17:13:39 +08:00
wanglin2
b2ca5d0fba 打包0.5.8 2023-04-25 10:12:41 +08:00
wanglin2
470604e567 更新文档 2023-04-25 10:00:40 +08:00
wanglin2
f5f665ec0a Demo:扩展节点图标列表 2023-04-25 09:05:46 +08:00
wanglin2
5e865a4e33 Feature:支持扩展节点图标 2023-04-25 09:05:10 +08:00
wanglin2
38ad33b604 修复隐藏模式下展开收起按钮的缺陷 2023-04-24 15:29:52 +08:00
wanglin2
e1b4146171 Feature:默认改为鼠标移上节点才显示展开收起按钮 2023-04-24 14:25:36 +08:00
wanglin2
65151f4b0a 优化性能:1.节点位置没有变化不触发位置设置;2.展开收起状态没有变化不触发按钮更新 2023-04-24 10:25:23 +08:00
wanglin2
942706fb63 Merge branch 'feature' into main 2023-04-24 09:09:33 +08:00
wanglin2
5f4492d4b7 打包0.5.7 2023-04-24 09:08:48 +08:00
wanglin2
ace1f62a40 更新文档 2023-04-24 09:02:01 +08:00
wanglin2
706c88c7d5 Featyre:富文本模式下,导入数据、初始化数据、切换主题场景节点样式支持跟随主题变化 2023-04-23 16:18:49 +08:00
wanglin2
4512fb16eb 优化富文本节点编辑 2023-04-23 15:20:33 +08:00
wanglin2
e446ff12e7 Demo:修复主题的标题显示错误问题 2023-04-23 14:39:16 +08:00
wanglin2
4318646abe 更新文档 2023-04-23 14:36:47 +08:00
wanglin2
2cbfe4f0e7 Feature:富文本模式导出改为使用html2canvas转换整个svg 2023-04-23 14:09:41 +08:00
wanglin2
b7910c4665 优化节点编辑 2023-04-22 14:24:05 +08:00
wanglin2
fc1ba24834 更新群二维码 2023-04-21 10:08:49 +08:00
wanglin2
cf16937160 Merge branch 'feature' into main 2023-04-21 10:08:11 +08:00
wanglin2
c3393abed6 打包0.5.6 2023-04-21 10:07:08 +08:00
wanglin2
2abff3e21b Demo:关闭新特性提示 2023-04-21 09:56:14 +08:00
wanglin2
e39a94c5e2 优化节点富文本编辑 2023-04-21 09:55:29 +08:00
wanglin2
a6fff7f7a3 Demo:修复节点文字数量计数不正确的问题 2023-04-21 09:37:49 +08:00
wanglin2
e584081b41 更新文档 2023-04-21 09:32:36 +08:00
wanglin2
b91dde8084 更新文档 2023-04-20 16:31:28 +08:00
wanglin2
077478d654 Feature:添加最大历史记录数限制 2023-04-20 16:10:25 +08:00
wanglin2
2be97cc1a0 Fix:修复节点正在编辑中时拖动画布导致编辑框和节点分离的问题 2023-04-20 15:29:16 +08:00
wanglin2
0f7dc949a4 优化富文本编辑 2023-04-20 09:40:47 +08:00
wanglin2
c7e91cc9eb Fix:修复短时间快速多次渲染时节点位置错乱的问题 2023-04-20 08:31:47 +08:00
wanglin2
6eacfab9c2 Demo:支持右键删除概要节点 2023-04-19 16:07:52 +08:00
wanglin2
e36a408275 Fix:修复快速多次渲染时节点位置错乱的问题 2023-04-19 15:45:51 +08:00
wanglin2
abda5b7d06 Merge branch 'feature' into main 2023-04-19 09:41:17 +08:00
wanglin2
f815f71dd7 更新文档 2023-04-19 09:40:58 +08:00
wanglin2
fa2c5b420c Merge branch 'feature' into main 2023-04-19 08:36:50 +08:00
wanglin2
4c19bc76a7 打包0.5.5-fix.1,修复小地图报错 2023-04-19 08:35:58 +08:00
wanglin2
d08a317920 Merge branch 'feature' into main 2023-04-18 17:38:38 +08:00
wanglin2
bd805836cd 打包0.5.5-fix.1 2023-04-18 17:37:10 +08:00
wanglin2
e804a8f2f7 Demo:修复富文本编辑时工具栏层级比节点编辑框低的问题 2023-04-18 17:19:43 +08:00
wanglin2
8bf876d446 优化:多选节点时改为节点只要和选区重叠就算被选中 2023-04-18 17:11:40 +08:00
wanglin2
f2521f663e 优化:切换结构时重置画布缩放,以修复当存在缩放时切换结构后第一次拖动会突变的问题 2023-04-18 16:59:12 +08:00
wanglin2
e676bff453 优化:当编辑节点文本时节点在画布外时移入画布内 2023-04-18 16:40:25 +08:00
wanglin2
8f2cc72d3c Merge branch 'feature' into main 2023-04-18 09:54:45 +08:00
wanglin2
ec22656bee 打包0.5.5 2023-04-18 09:52:15 +08:00
wanglin2
4acf8ba2ac 更新文档 2023-04-18 09:45:40 +08:00
wanglin2
d45a18904e Feature:1.支持配置节点文本编辑框、节点备注浮层的z-index。2.支持点击画布区域外结束节点编辑状态 2023-04-18 09:45:29 +08:00
wanglin2
9fc2dbabd4 Feature:支持配置导出为png、svg、pdf时的内边距。 2023-04-18 09:04:16 +08:00
wanglin2
b83b81f52e Merge branch 'feature' into main 2023-04-15 09:58:47 +08:00
wanglin2
d1e2db993e 打包0.5.4-fix.1 2023-04-15 09:54:50 +08:00
wanglin2
ab931901e2 优化鱼骨图布局 2023-04-15 09:51:12 +08:00
wanglin2
9879a25f9b Merge branch 'feature' into main 2023-04-14 23:23:45 +08:00
wanglin2
16fb8eb092 打包0.5.4 2023-04-14 23:11:00 +08:00
wanglin2
de859927ed 更新文档 2023-04-14 23:03:14 +08:00
wanglin2
7bde59f664 优化目录组织图布局 2023-04-14 23:02:04 +08:00
wanglin2
be9668c7b8 修改鱼骨结构 2023-04-14 22:04:58 +08:00
wanglin2
95fe3189d5 Fix:修复隐藏节点时隐藏连线没有做异常处理的问题 2023-04-14 20:32:11 +08:00
wanglin2
9c60857c6a 优化鱼骨结构,支持margin 2023-04-14 17:43:02 +08:00
wanglin2
3b7cea9ee3 优化:提取结构类公共方法 2023-04-14 10:40:01 +08:00
wanglin2
3f081e5021 更新群二维码 2023-04-14 10:29:49 +08:00
wanglin2
d959420d6e 基本完成鱼骨结构 2023-04-14 10:27:25 +08:00
wanglin2
79d81b92e6 鱼骨结构开发中 2023-04-13 17:31:54 +08:00
wanglin2
940c60f23d 鱼骨结构开发中:完成上方鱼骨图 2023-04-13 09:35:06 +08:00
wanglin2
965ab8151e 优化时间轴结构逻辑 2023-04-13 08:32:39 +08:00
wanglin2
87d55b31ca 优化目录组织图结构逻辑 2023-04-13 08:31:00 +08:00
wanglin2
bb68575aca 优化组织结构图逻辑 2023-04-12 22:49:19 +08:00
wanglin2
e561e804be Fix:修复组织结构图,目录组织图等节点拖拽时存在线段未隐藏的bug 2023-04-12 22:28:35 +08:00
wanglin2
5a8c3aa9d3 鱼骨结构开发中 2023-04-12 22:17:51 +08:00
wanglin2
f84639debd 测试提交 2023-04-12 17:42:18 +08:00
wanglin2
de77a2b613 Feature:新增时间轴结构 2023-04-12 14:15:46 +08:00
wanglin2
3825c3769f 精简时间轴结构的连线逻辑 2023-04-12 08:43:22 +08:00
wanglin2
876908e922 更新文档 2023-04-11 22:25:18 +08:00
wanglin2
25ecde8948 Fix:修复节点右键和画布右键的冲突问题 2023-04-11 22:23:26 +08:00
wanglin2
2de0334e3b Feature:新增时间轴结构 2023-04-11 16:52:38 +08:00
wanglin2
1e32338d23 Merge branch 'feature' into main 2023-04-10 22:11:00 +08:00
wanglin2
21f487321a 更新文档 2023-04-10 22:08:26 +08:00
wanglin2
750af31996 Merge branch 'main' of https://github.com/wanglin2/mind-map into main 2023-04-10 10:27:15 +08:00
wanglin2
dc1aaee75d Merge branch 'feature' into main 2023-04-10 10:26:49 +08:00
wanglin2
2122fa59d7 更新文档 2023-04-10 10:25:59 +08:00
wanglin2
5b7c65adad 添加新主题 2023-04-10 10:20:09 +08:00
wanglin2
fabf4535a8 Merge branch 'feature' into main 2023-04-09 22:11:11 +08:00
wanglin2
04566080e0 更新文档 2023-04-09 22:10:13 +08:00
wanglin2
0f9d9cdb21 Merge branch 'feature' into main 2023-04-09 08:53:58 +08:00
wanglin2
11538d1789 打包0.5.3-fix.2:修复导出图片时节点中的图片无法显示的问题;更新文档 2023-04-09 08:52:43 +08:00
wanglin2
49bae6cc6c Merge branch 'feature' into main 2023-04-08 21:09:49 +08:00
wanglin2
9644ba0c8d 打包0.5.3-fix.1,修复设置初始根节点位置不生效的问题;文档新增教程章节 2023-04-08 21:09:24 +08:00
wanglin2
ac2df3cb2e Merge branch 'feature' into main 2023-04-08 15:37:58 +08:00
wanglin2
aad1f911c8 支持打包成esm模块 2023-04-08 15:37:34 +08:00
wanglin2
27b50bf4ae Merge branch 'feature' into main 2023-04-07 20:14:47 +08:00
wanglin2
2d22837e32 Demo:修复导入存在主题配置的数据没有触发本地存储的问题 2023-04-07 20:14:30 +08:00
wanglin2
866f2071ce Merge branch 'feature' into main 2023-04-07 19:31:10 +08:00
wanglin2
9cb35eac29 更新文档 2023-04-07 19:30:52 +08:00
wanglin2
6ff63d9d56 Merge branch 'feature' into main 2023-04-07 15:56:14 +08:00
wanglin2
43539df25b 修改文档 2023-04-07 15:55:55 +08:00
wanglin2
cbafc24670 Merge branch 'feature' into main 2023-04-07 08:56:44 +08:00
wanglin2
cc83a18b18 打包0.5.3 2023-04-07 08:55:55 +08:00
wanglin2
5ae931da92 更新文档 2023-04-07 08:53:05 +08:00
wanglin2
853b999a7d Feature:支持设置初始中心节点的位置 2023-04-07 08:42:27 +08:00
wanglin2
505622f3dc Fix:修复富文本模式下选择了多个节点时修改样式时意外修改文本的问题 2023-04-06 20:14:54 +08:00
wanglin2
0a5b41e3e0 更新群二维码 2023-04-06 08:47:31 +08:00
wanglin2
8630c47b26 Merge branch 'feature' into main 2023-04-06 08:46:54 +08:00
wanglin2
6105fd6e3d 打包0.5.2,去除导出json数据中的uid字段 2023-04-06 08:45:43 +08:00
wanglin2
d7b1c4e4fe 优化:重新渲染时清空节点池 2023-04-05 21:22:39 +08:00
wanglin2
6ccafaa771 Merge branch 'feature' into main 2023-04-05 16:47:06 +08:00
wanglin2
6b15e469a2 打包0.5.1 2023-04-05 16:45:06 +08:00
wanglin2
5675e29df3 更新文档 2023-04-05 16:40:29 +08:00
wanglin2
52d094a7c7 Demo:窗口大小改变后修改画布大小 2023-04-05 09:55:39 +08:00
wanglin2
633ed68f92 Demo:导入数据后复位画布变换效果,解决导入后新节点在画布外的问题 2023-04-05 08:49:36 +08:00
wanglin2
388594e29a Fix:修复快速操作时节点位置不正确的问题 2023-04-05 08:45:23 +08:00
wanglin2
af148fef27 Demo:修复在大纲内添加新节点时节点错乱的问题 2023-04-04 22:54:24 +08:00
wanglin2
983e55bd1d 优化:只有当鼠标在画布内才响应快捷键 2023-04-04 22:53:38 +08:00
wanglin2
69dab99bf7 Merge branch 'feature' into main 2023-04-02 21:27:24 +08:00
wanglin2
c94f459ff9 打包0.5.0 2023-04-02 21:12:44 +08:00
wanglin2
cd3baf411b 更新文档 2023-04-02 21:08:55 +08:00
wanglin2
042911371c 优化节点拖拽移动逻辑 2023-04-02 11:21:35 +08:00
wanglin2
8e2fb72309 优化:收起所有节点后回到中心位置 2023-04-01 17:30:29 +08:00
wanglin2
77aacccdad FIX:setData后触发历史数据记录 2023-04-01 09:30:38 +08:00
wanglin2
5413c867e3 FEATURE:支持自定义展开收起按钮的颜色和图标 2023-04-01 09:19:51 +08:00
wanglin2
4fc7eb084e demo:优化工具栏的位置 2023-04-01 08:26:53 +08:00
wanglin2
a9b04312d8 移除重复的事件绑定 2023-03-31 22:30:06 +08:00
wanglin2
4cd9b66653 阻止短时间多次触发渲染 2023-03-31 22:25:34 +08:00
wanglin2
9b7b41597f Merge branch 'main' of https://github.com/wanglin2/mind-map into main 2023-03-31 14:39:30 +08:00
wanglin2
e56ff8fdf2 更新群二维码 2023-03-31 14:39:09 +08:00
wanglin2
8d9299aed7 优化和修复节点编辑的一些体验和问题 2023-03-31 14:37:32 +08:00
wanglin2
f8506cb75b 优化设置主题、前进回退等操作的性能问题 2023-03-31 14:36:08 +08:00
wanglin2
c90ee9e892 优化设置主题 2023-03-31 14:34:56 +08:00
wanglin2
4d21b84882 一些小调整 2023-03-31 14:32:43 +08:00
wanglin2
331f0bdf97 优化展开收起按钮的逻辑 2023-03-30 16:12:46 +08:00
wanglin2
0a4898697d 优化:提取代码中的常量 2023-03-30 14:36:30 +08:00
wanglin2
79ae08fc9a 1.修改主题不再完全重新渲染;2.重构Node类核心逻辑;3.Shape类不再直接添加节点,而是返回节点 2023-03-30 14:16:47 +08:00
wanglin2
1795773af9 Break change:节点激活样式只能修改形状相关样式 2023-03-28 20:01:52 +08:00
wanglin2
747a781ad8 删除注释后的空行 2023-03-28 19:35:57 +08:00
wanglin2
fcfcb1c3d1 优化:getSize以后不需要调用renderNode方法,直接layout即可 2023-03-28 19:30:13 +08:00
wanglin2
d412ae8cce 优化:节点实例、样式实例不再保存主题配置,直接从mindMap实例上获取 2023-03-28 19:15:47 +08:00
wanglin2
d834b76d42 FIX:修复创建节点时重复的函数调用,优化性能 2023-03-27 09:11:15 +08:00
wanglin2
3f9da8940f 打包0.4.7 2023-03-24 21:05:12 +08:00
wanglin2
26d5f69af2 Doc:更新文档 2023-03-24 15:26:44 +08:00
wanglin2
6efe4a3fd6 Feature:1.支持配置插入节点时的初始文字;2.优化历史记录添加逻辑;3.节点插入和删除命令支持传入指定节点和初始节点数据 2023-03-24 15:26:24 +08:00
wanglin2
2b4ab4a322 Fix:修复从富文本模式切换到非富文本模式时没有触发数据更新和历史记录的问题 2023-03-24 11:34:09 +08:00
wanglin2
cca42d1f89 Fix:修复从富文本模式切换到非富文本模式时 2023-03-24 11:33:17 +08:00
wanglin2
724fef87b1 Doc:更新文档 2023-03-24 11:22:18 +08:00
wanglin2
d50c0e4cd5 优化:1.节点激活状态切换不再触发历史记录;2.短时间多次触发历史记录只会添加最后一次的数据 2023-03-24 11:22:05 +08:00
wanglin2
c18c037642 Doc:更新文档 2023-03-24 10:31:21 +08:00
wanglin2
cda1da5fd0 Demo:Feature:支持导入和导出为markdown格式,优化导出弹窗视觉 2023-03-24 10:30:53 +08:00
wanglin2
eba1aa3a37 Demo:增加图标 2023-03-24 10:28:35 +08:00
wanglin2
81018bb615 Fix:修复富文本编辑模式下当没有富文本节点时无法导出为图片的问题 2023-03-24 10:27:13 +08:00
wanglin2
5fa0ff7b5c Feature:支持导入和导出为markdown格式文件 2023-03-24 10:13:24 +08:00
wanglin2
cabe286ebb 优化:富文本编辑时默认不再全选;使用节点填充色作为编辑时的背景色 2023-03-23 09:39:00 +08:00
wanglin2
e337da088b 打包0.4.6 2023-03-22 15:14:50 +08:00
wanglin2
de97ea9e75 兼容0.4.5版本的关联线 2023-03-22 14:40:07 +08:00
wanglin2
cc331065eb 修改文档 2023-03-22 13:49:47 +08:00
wanglin2
9a8e630654 Feature:支持调整关联线的控制点 2023-03-22 13:49:34 +08:00
wanglin2
17ab977efb 优化关联线的点击逻辑 2023-03-22 13:42:31 +08:00
wanglin2
6bd10d9451 1.demo:大纲支持点击后定位节点,支持添加节点;2.完善问题 2023-03-21 09:35:34 +08:00
wanglin2
5313b9b69c 1.优化重复的历史数据;2.修复节点编辑时的方向键冲突;3.修复拖拽节点的id丢失问题 2023-03-21 09:34:47 +08:00
wanglin2
8dcfdcc44a 更新二维码 2023-03-20 09:26:46 +08:00
wanglin2
67422df3ff 打包0.4.5 2023-03-18 21:54:20 +08:00
wanglin2
2ad7536eb7 优化关联线逻辑 2023-03-18 21:51:02 +08:00
wanglin2
6f56e5c4e6 完善文档 2023-03-17 23:06:14 +08:00
wanglin2
5ae8ebe590 Feature:1.优化关联线创建,2.支持按住ctrl键多选和取消多选节点 2023-03-17 23:00:25 +08:00
wanglin2
6ecb97e4e5 Feature:按住根节点也可以拖动画布 2023-03-17 17:03:56 +08:00
wanglin2
c8f938dd3e Fix:修复创建关联线时节点激活状态未清除的问题 2023-03-17 16:57:30 +08:00
wanglin2
0d29e29162 完善文档 2023-03-17 16:24:22 +08:00
wanglin2
be1b3dffce demo支持关联线功能 2023-03-17 15:39:07 +08:00
wanglin2
3ba2dbe415 Feature:新增关联线功能 2023-03-17 15:38:38 +08:00
wanglin2
19a96c92a9 新增图标 2023-03-17 14:43:23 +08:00
wanglin2
c265e3e437 打包0.4.4 2023-03-14 09:45:27 +08:00
wanglin2
7434ac2648 Feature:支持响应鼠标的横向滚动 2023-03-08 16:04:26 +08:00
wanglin2
63d73a73aa 打包0.4.3 2023-03-08 09:43:11 +08:00
wanglin2
00f9258a4d 更新文档 2023-03-08 09:39:05 +08:00
wanglin2
7087b43d39 Fix:修复前进后退没有触发data_change事件的问题;Feature:鼠标滚轮事件支持自定义 2023-03-08 09:38:46 +08:00
wanglin2
945a78b7b1 去除docs目录,更新群二维码 2023-03-06 14:26:02 +08:00
wanglin2
55acc19ab8 打包0.4.2 2023-03-01 09:24:51 +08:00
wanglin2
410f0be05e Fetaure:Node类的setText方法增加第二个参数,以支持设置富文本内容 2023-03-01 09:17:45 +08:00
wanglin2
2d7c091071 打包0.4.1 2023-02-27 15:31:52 +08:00
wanglin2
9bf58a54ce Feature:节点富文本编辑支持清除文字样式 2023-02-27 15:19:56 +08:00
wanglin2
231adeea44 Feature:富文本支持设置背景颜色 2023-02-27 14:58:01 +08:00
wanglin2
0ea618af39 Fix:1.Mac系统触控板缩放方向相反的问题;2.设备像素比不为1时导出图片中富文本节点尺寸变大的问题.Feater:节点抛出鼠标移入和移出事件 2023-02-26 10:07:03 +08:00
wanglin2
f66297ff99 打包0.4.0 2023-02-25 10:11:16 +08:00
wanglin2
3113bf2e1f 完善文档 2023-02-24 17:14:48 +08:00
wanglin2
8c114dac02 Feature:支持节点富文本编辑 2023-02-24 17:14:24 +08:00
wanglin2
06dba79272 新增图标 2023-02-24 17:13:07 +08:00
wanglin2
8441986ca7 修改文档 2023-02-20 19:25:59 +08:00
wanglin2
c8b50908e1 Fix:修复底边风格的情况下,节点高度过高会和其他节点重叠的问题 2023-02-20 15:10:27 +08:00
wanglin2
7e11fde892 打包0.3.4 2023-02-20 11:30:15 +08:00
wanglin2
3d9f3bd7a8 Fix:修复批量删除的节点中如果存在根节点会出现删除异常的问题 2023-02-20 11:26:26 +08:00
wanglin2
46e11649b0 优化节点文本编辑 2023-02-20 11:02:04 +08:00
wanglin2
161ef7590c feature:节点文本增加自动换行功能 2023-02-20 10:14:34 +08:00
wanglin2
ca660a3c74 打包0.3.3 2023-02-14 09:38:38 +08:00
wanglin2
7a24872331 Fix:修复根节点无法黄的问题 2023-02-14 09:35:56 +08:00
wanglin2
eb4ea9fb3a 打包0.3.2 2023-02-03 11:13:58 +08:00
wanglin2
64228c02ff 修复当思维导图实际内容大于屏幕宽高时,导出的时候超出的部分没有绘制水印的问题 2023-02-03 11:09:43 +08:00
wanglin2
f184a5d948 修复二级节点拖拽到其他节点或其他节点拖拽到二级节点时节点样式没有更新的问题 2023-02-03 10:40:06 +08:00
wanglin2
0bf5b2d6f7 打包0.3.1 2023-02-01 16:30:29 +08:00
wanglin2
74a52ea386 导出d的图片支持background-size/position/repeat属性效果 2023-02-01 16:25:57 +08:00
wanglin2
9914eef5b9 修改文档 2023-01-31 15:06:24 +08:00
wanglin2
f547f741f2 1.修复删除背景图片不生效的问题;2.背景图片展示增加位置和大小设置;3.修复节点拖拽到根节点时连接线跑到根节点上方的问题 2023-01-31 15:04:38 +08:00
wanglin2
c26149fa42 修复demo设置了水印页面刷新后生效的问题 2023-01-31 13:35:46 +08:00
wanglin2
b2aa3648eb 支持当修改文档后自动重新编译生成vue组件 2023-01-31 11:15:34 +08:00
wanglin2
b997604ab2 打包0.3.0 2023-01-17 17:39:37 +08:00
wanglin2
c6929b82ad 插件化架构基本升级完成 2023-01-17 16:53:28 +08:00
wanglin2
b40da53aef 只读模式下禁止添加历史记录、禁止响应前进后退操作 2023-01-17 15:49:23 +08:00
wanglin2
1e5dfd97e1 提取多选节点Select类作为插件 2023-01-17 15:26:35 +08:00
wanglin2
97bcc22abd 提取导出类作为插件 2023-01-17 15:21:02 +08:00
wanglin2
bd655839cb 提取出键盘导航类作为插件 2023-01-17 14:39:43 +08:00
wanglin2
a53a4e8e1d 提取拖拽类Drag作为插件 2023-01-17 14:30:13 +08:00
wanglin2
eb01646747 提取水印类作为插件 2023-01-17 14:11:07 +08:00
wanglin2
d27eee0fae 开始进行插件化转换,即提取非核心的类作为插件,不再内置;抽取出校地图类插件 2023-01-17 13:56:57 +08:00
wanglin2
92ee241ed8 将xmind解析方法从MindMap类上移除,改为按需引入方式使用 2023-01-17 11:19:59 +08:00
617 changed files with 32327 additions and 9040 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules
.DS_Store
.DS_Store
dist_electron

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021-2023 The MindMap Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

122
README.md
View File

@@ -7,25 +7,36 @@
[![GitHub forks](https://img.shields.io/github/forks/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/network/members)
![license](https://img.shields.io/npm/l/express.svg)
> 一个简单&强大的Web思维导图
> 一个简单&强大的Web思维导图
Demo[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
本项目包含两部分:
文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)
1.一个 js 思维导图库,不依赖任何框架,你可以使用它来快速完成 Web 思维导图产品的开发。
开发文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)。
2.一个 Web 思维导图基于思维导图库、Vue2.x、ElementUI 开发,可以操作电脑本地文件,所以你可以直接把它当做一个在线版思维导图应用使用,如果觉得 github 的响应速度慢,你也可以部署到你的服务器上。
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)。
另外也提供了客户端可供下载使用,支持`Windows``Mac``Linux`,下载地址:
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`,支持从`json``xmind`导入
- [x] 支持小地图
- [x] 节点内容支持文本(普通文本、富文本)、图片、图标、超链接、备注、标签、概要
- [x] 节点支持拖拽(拖拽移动、自由调整)、多种节点形状,支持使用 DDM 完全自定义节点内容
- [x] 支持画布拖动、缩放
- [x] 支持鼠标按键拖动选择和Ctrl+左键两种多选节点方式
- [x] 支持导出为`json``png``svg``pdf``markdown``xmind`,支持从`json``xmind``markdown`导入
- [x] 支持快捷键、前进后退、关联线、搜索替换、小地图、水印
- [x] 提供丰富的配置,满足各种场景各种使用习惯
# 安装
@@ -35,10 +46,23 @@ npm i simple-mind-map
# 使用
提供一个宽高不为0的容器元素
```html
<div id="mindMapContainer"></div>
```
另外再设置一下`css`样式:
```css
#mindMapContainer * {
margin: 0;
padding: 0;
}
```
然后创建一个实例:
```js
import MindMap from "simple-mind-map";
@@ -53,6 +77,76 @@ const mindMap = new MindMap({
});
```
即可得到一个思维导图。
想要实现更多功能?可以查看[开发文档](https://wanglin2.github.io/mind-map/#/doc/zh/)。
# 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>
</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,69 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="./dist/logo.png"><title>一个简单的web思维导图实现</title><link href="dist/js/chunk-2d0a3179.8bac0e6a.js" rel="prefetch"><link href="dist/js/chunk-2d0aa579.3848b480.js" rel="prefetch"><link href="dist/js/chunk-2d0aa978.0c0ab348.js" rel="prefetch"><link href="dist/js/chunk-2d0ab10b.89baf4c9.js" rel="prefetch"><link href="dist/js/chunk-2d0abe0f.ba566b9e.js" rel="prefetch"><link href="dist/js/chunk-2d0b361e.d57feb19.js" rel="prefetch"><link href="dist/js/chunk-2d0b91e5.9058553b.js" rel="prefetch"><link href="dist/js/chunk-2d0b92c3.ff912e01.js" rel="prefetch"><link href="dist/js/chunk-2d0bd54e.988f0b69.js" rel="prefetch"><link href="dist/js/chunk-2d0be174.b87008fb.js" rel="prefetch"><link href="dist/js/chunk-2d0c0a44.d53f93f5.js" rel="prefetch"><link href="dist/js/chunk-2d0c18d8.ef38347c.js" rel="prefetch"><link href="dist/js/chunk-2d0c191e.fe3d1357.js" rel="prefetch"><link href="dist/js/chunk-2d0c1a01.dc5d667f.js" rel="prefetch"><link href="dist/js/chunk-2d0d9fbc.7c0e8c3b.js" rel="prefetch"><link href="dist/js/chunk-2d0da701.7110cb06.js" rel="prefetch"><link href="dist/js/chunk-2d0dad5f.789c08d8.js" rel="prefetch"><link href="dist/js/chunk-2d0db0f2.773ee0f5.js" rel="prefetch"><link href="dist/js/chunk-2d0dddce.29899f8c.js" rel="prefetch"><link href="dist/js/chunk-2d0de01b.4f180262.js" rel="prefetch"><link href="dist/js/chunk-2d0e2326.7b20d38b.js" rel="prefetch"><link href="dist/js/chunk-2d0e268c.314fd603.js" rel="prefetch"><link href="dist/js/chunk-2d0e5089.cd1baa18.js" rel="prefetch"><link href="dist/js/chunk-2d0e9742.62e1ccbf.js" rel="prefetch"><link href="dist/js/chunk-2d0f026c.af35b5ce.js" rel="prefetch"><link href="dist/js/chunk-2d2082b9.ffe557f3.js" rel="prefetch"><link href="dist/js/chunk-2d208ffa.6b617b98.js" rel="prefetch"><link href="dist/js/chunk-2d20ec02.917aff76.js" rel="prefetch"><link href="dist/js/chunk-2d20f68f.66cba965.js" rel="prefetch"><link href="dist/js/chunk-2d210a7a.25884d5e.js" rel="prefetch"><link href="dist/js/chunk-2d216004.3416e02c.js" rel="prefetch"><link href="dist/js/chunk-2d216b67.2d06497a.js" rel="prefetch"><link href="dist/js/chunk-2d217907.96994ac6.js" rel="prefetch"><link href="dist/js/chunk-2d226d0a.d30a47fe.js" rel="prefetch"><link href="dist/js/chunk-2d2299c3.5aa1632c.js" rel="prefetch"><link href="dist/js/chunk-2d2308b0.d813ba33.js" rel="prefetch"><link href="dist/js/chunk-3a2f3e67.13278516.js" rel="prefetch"><link href="dist/css/app.a6908b68.css" rel="preload" as="style"><link href="dist/css/chunk-vendors.1790fe42.css" rel="preload" as="style"><link href="dist/js/app.2f5edb91.js" rel="preload" as="script"><link href="dist/js/chunk-vendors.7f3b8358.js" rel="preload" as="script"><link href="dist/css/chunk-vendors.1790fe42.css" rel="stylesheet"><link href="dist/css/app.a6908b68.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="dist/js/chunk-vendors.7f3b8358.js"></script><script src="dist/js/app.2f5edb91.js"></script></body></html>
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"><link rel="icon" href="dist/logo.ico"><title>思绪思维导图</title><script>// 自定义静态资源的路径
window.externalPublicPath = './dist/'
// 接管应用
window.takeOverApp = false</script><link href="dist/css/chunk-vendors.css?e577492ccbbe7e7070c3" rel="stylesheet"><link href="dist/css/app.css?e577492ccbbe7e7070c3" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
mindMapData: {
root:{
"data": {
"text": "根节点"
},
"children": []
},
theme:{
"template":"avocado",
"config":{}
},
layout:"logicalStructure",
config: {},
view: null,
},
lang: 'zh',
localConfig: null
})
}, 200)
})
}
const setTakeOverAppMethods = (data) => {
window.takeOverAppMethods = {}
// 获取思维导图数据的函数
window.takeOverAppMethods.getMindMapData = () => {
return data.mindMapData
}
// 保存思维导图数据的函数
window.takeOverAppMethods.saveMindMapData = (data) => {
console.log(data)
}
// 获取语言的函数
window.takeOverAppMethods.getLanguage = () => {
return data.lang
}
// 保存语言的函数
window.takeOverAppMethods.saveLanguage = (lang) => {
console.log(lang)
}
// 获取本地配置的函数
window.takeOverAppMethods.getLocalConfig = () => {
return data.localConfig
}
// 保存本地配置的函数
window.takeOverAppMethods.saveLocalConfig = (config) => {
console.log(config)
}
}
window.onload = async () => {
if (!window.takeOverApp) return
// 请求数据
const data = await getDataFromBackend()
// 设置全局的方法
setTakeOverAppMethods(data)
// 思维导图实例创建完成事件
window.$bus.$on('app_inited', (mindMap) => {
console.log(mindMap)
})
// 可以通过window.$bus.$on()来监听应用的一些事件
// 实例化页面
window.initApp()
}</script><script src="dist/js/chunk-vendors.js?e577492ccbbe7e7070c3"></script><script src="dist/js/app.js?e577492ccbbe7e7070c3"></script></body></html>

BIN
qrcode.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

View File

@@ -900,6 +900,17 @@ const data5 = {
}
}
// 富文本数据v0.4.0+需要使用RichText插件才支持富文本编辑
const richTextData = {
"root": {
"data": {
"text": "<a href='http://lxqnsys.com/' target='_blank'>理想去年实验室</a>",
"richText": true
},
"children": []
}
}
const rootData = {
"root": {
"data": {

View File

@@ -31,6 +31,12 @@
"isActive": false
},
"children": []
}, {
"data": {
"text": "<a href='http://lxqnsys.com/' target='_blank'>理想去年实验室</a>",
"richText": true
},
"children": []
}]
}]
},

42
simple-mind-map/full.js Normal file
View File

@@ -0,0 +1,42 @@
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 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 xmind from './src/parse/xmind.js'
import markdown from './src/parse/markdown.js'
import icons from './src/svg/icons.js'
import * as constants from './src/constants/constant.js'
import themes from './src/themes/index.js'
import * as defaultTheme from './src/themes/default.js'
MindMap.xmind = xmind
MindMap.markdown = markdown
MindMap.iconList = icons.nodeIconList
MindMap.constants = constants
MindMap.themes = themes
MindMap.defaultTheme = defaultTheme
MindMap
.usePlugin(MiniMap)
.usePlugin(Watermark)
.usePlugin(Drag)
.usePlugin(KeyboardNavigation)
.usePlugin(ExportPDF)
.usePlugin(Export)
.usePlugin(Select)
.usePlugin(AssociativeLine)
.usePlugin(RichText)
.usePlugin(TouchEvent)
.usePlugin(NodeImgAdjust)
.usePlugin(Search)
export default MindMap

View File

@@ -1,73 +1,17 @@
import View from './src/View'
import Event from './src/Event'
import Render from './src/Render'
import View from './src/core/view/View'
import Event from './src/core/event/Event'
import Render from './src/core/render/Render'
import merge from 'deepmerge'
import theme from './src/themes'
import Style from './src/Style'
import KeyCommand from './src/KeyCommand'
import Command from './src/Command'
import BatchExecution from './src/BatchExecution'
import Export from './src/Export'
import Select from './src/Select'
import Drag from './src/Drag'
import MiniMap from './src/MiniMap'
import Watermark from './src/Watermark'
import { layoutValueList } from './src/utils/constant'
import Style from './src/core/render/node/Style'
import KeyCommand from './src/core/command/KeyCommand'
import Command from './src/core/command/Command'
import BatchExecution from './src/utils/BatchExecution'
import { layoutValueList, CONSTANTS, commonCaches } from './src/constants/constant'
import { SVG } from '@svgdotjs/svg.js'
import xmind from './src/parse/xmind'
import { simpleDeepClone } from './src/utils'
import KeyboardNavigation from './src/KeyboardNavigation'
import defaultTheme from './src/themes/default'
// 默认选项配置
const defaultOpt = {
// 是否只读
readonly: false,
// 布局
layout: 'logicalStructure',
// 主题
theme: 'default', // 内置主题default默认主题
// 主题配置,会和所选择的主题进行合并
themeConfig: {},
// 放大缩小的增量比例
scaleRatio: 0.1,
// 最多显示几个标签
maxTag: 5,
// 导出图片时的内边距
exportPadding: 20,
// 展开收缩按钮尺寸
expandBtnSize: 20,
// 节点里图片和文字的间距
imgTextMargin: 5,
// 节点里各种文字信息的间距,如图标和文字的间距
textContentMargin: 2,
// 多选节点时鼠标移动到边缘时的画布移动偏移量
selectTranslateStep: 3,
// 多选节点时鼠标移动距边缘多少距离时开始偏移
selectTranslateLimit: 20,
// 自定义节点备注内容显示
customNoteContentShow: null,
/*
{
show(){},
hide(){}
}
*/
// 是否开启节点自由拖拽
enableFreeDrag: false,
// 水印配置
watermarkConfig: {
text: '',
lineSpacing: 100,
textSpacing: 100,
angle: 30,
textStyle: {
color: '#999',
opacity: 0.5,
fontSize: 14
}
}
}
import { simpleDeepClone, getType } from './src/utils'
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
import { defaultOpt } from './src/constants/defaultOptions'
// 思维导图
class MindMap {
@@ -88,12 +32,12 @@ class MindMap {
this.svg = SVG().addTo(this.el).size(this.width, this.height)
this.draw = this.svg.group()
// 节点id
this.uid = 0
// 初始化主题
this.initTheme()
// 初始化缓存数据
this.initCache()
// 事件类
this.event = new Event({
mindMap: this
@@ -120,41 +64,16 @@ class MindMap {
draw: this.draw
})
// 小地图类
this.miniMap = new MiniMap({
mindMap: this
})
// 导出类
this.doExport = new Export({
mindMap: this
})
// 选择类
this.select = new Select({
mindMap: this
})
// 拖动类
this.drag = new Drag({
mindMap: this
})
// 键盘导航类
this.keyboardNavigation = new KeyboardNavigation({
mindMap: this
})
// 水印类
this.watermark = new Watermark({
mindMap: this
})
// 批量执行类
this.batchExecution = new BatchExecution()
// 注册插件
MindMap.pluginList.forEach((plugin) => {
this.initPlugin(plugin)
})
// 初始渲染
this.reRender()
this.render()
setTimeout(() => {
this.command.addHistory()
}, 0)
@@ -162,9 +81,11 @@ class MindMap {
// 配置参数处理
handleOpt(opt) {
// 深拷贝一份节点数据
opt.data = simpleDeepClone(opt.data || {})
// 检查布局配置
if (!layoutValueList.includes(opt.layout)) {
opt.layout = 'logicalStructure'
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
}
// 检查主题配置
opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default'
@@ -172,21 +93,21 @@ class MindMap {
}
// 渲染,部分渲染
render() {
render(callback, source = '') {
this.batchExecution.push('render', () => {
this.initTheme()
this.renderer.reRender = false
this.renderer.render()
this.renderer.render(callback, source)
})
}
// 重新渲染
reRender() {
reRender(callback, source = '') {
this.batchExecution.push('render', () => {
this.draw.clear()
this.initTheme()
this.renderer.reRender = true
this.renderer.render()
this.renderer.render(callback, source)
})
}
@@ -213,6 +134,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() {
// 合并主题配置
@@ -225,7 +163,7 @@ class MindMap {
setTheme(theme) {
this.renderer.clearAllActive()
this.opt.theme = theme
this.reRender()
this.render(null, CONSTANTS.CHANGE_THEME)
}
// 获取当前主题
@@ -236,7 +174,9 @@ class MindMap {
// 设置主题配置
setThemeConfig(config) {
this.opt.themeConfig = config
this.reRender()
// 检查改变的是否是节点大小无关的主题属性
let res = checkIsNodeSizeIndependenceConfig(config)
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
}
// 获取自定义主题配置
@@ -268,11 +208,12 @@ class MindMap {
setLayout(layout) {
// 检查布局配置
if (!layoutValueList.includes(layout)) {
layout = 'logicalStructure'
layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
}
this.opt.layout = layout
this.view.reset()
this.renderer.setLayout()
this.render()
this.render(null, CONSTANTS.CHANGE_LAYOUT)
}
// 执行命令
@@ -284,8 +225,13 @@ class MindMap {
setData(data) {
this.execCommand('CLEAR_ACTIVE_NODE')
this.command.clearHistory()
this.renderer.renderTree = data
this.reRender()
this.command.addHistory()
if (this.richText) {
this.renderer.renderTree = this.richText.handleSetData(data)
} else {
this.renderer.renderTree = data
}
this.reRender(() => {}, CONSTANTS.SET_DATA)
}
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
@@ -345,19 +291,127 @@ class MindMap {
// 设置只读模式、编辑模式
setMode(mode) {
if (!['readonly', 'edit'].includes(mode)) {
if (![CONSTANTS.MODE.READONLY, CONSTANTS.MODE.EDIT].includes(mode)) {
return
}
this.opt.readonly = mode === 'readonly'
this.opt.readonly = mode === CONSTANTS.MODE.READONLY
if (this.opt.readonly) {
// 取消当前激活的元素
this.renderer.clearAllActive()
}
this.emit('mode_change', mode)
}
// 获取svg数据
getSvgData({ paddingX = 0, paddingY = 0 } = {}) {
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()
// 去除放大缩小的变换效果
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)
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
// 克隆一份数据
let clone = svg.clone()
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
if ((rect.width > origWidth || rect.height > origHeight) && this.watermark && this.watermark.hasWatermark()) {
this.width = rect.width
this.height = rect.height
this.watermark.draw()
clone = svg.clone()
this.width = origWidth
this.height = origHeight
this.watermark.draw()
}
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight)
draw.transform(origTransform)
return {
svg: clone, // 思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组
svgHTML: clone.svg(), // svg字符串
rect: {
...rect, // 思维导图图形未缩放时的位置尺寸等信息
ratio: rect.width / rect.height // 思维导图图形的宽高比
},
origWidth, // 画布宽度
origHeight, // 画布高度
scaleX: origTransform.scaleX, // 思维导图图形的水平缩放值
scaleY: origTransform.scaleY // 思维导图图形的垂直缩放值
}
}
// 添加插件
addPlugin(plugin, opt) {
let index = MindMap.hasPlugin(plugin)
if (index === -1) {
MindMap.usePlugin(plugin, opt)
this.initPlugin(plugin)
}
}
// 移除插件
removePlugin(plugin) {
let index = MindMap.hasPlugin(plugin)
if (index !== -1) {
MindMap.pluginList.splice(index, 1)
if (this[plugin.instanceName]) {
if (this[plugin.instanceName].beforePluginRemove) {
this[plugin.instanceName].beforePluginRemove()
}
delete this[plugin.instanceName]
}
}
}
// 实例化插件
initPlugin(plugin) {
this[plugin.instanceName] = new plugin({
mindMap: this,
pluginOpt: plugin.pluginOpt
})
}
// 销毁
destroy() {
// 移除插件
[...MindMap.pluginList].forEach((plugin) => {
this[plugin.instanceName] = null
})
// 解绑事件
this.event.unbind()
// 移除画布节点
this.svg.remove()
// 去除给容器元素设置的背景样式
Style.removeBackgroundStyle(this.el)
this.el = null
}
}
MindMap.xmind = xmind
// 插件列表
MindMap.pluginList = []
MindMap.usePlugin = (plugin, opt = {}) => {
plugin.pluginOpt = opt
MindMap.pluginList.push(plugin)
return MindMap
}
MindMap.hasPlugin = (plugin) => {
return MindMap.pluginList.findIndex((item) => {
return item === plugin
})
}
// 定义新主题
MindMap.defineTheme = (name, config = {}) => {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.2.24",
"version": "0.6.11-fix.1",
"description": "一个简单的web在线思维导图",
"authors": [
{
@@ -22,14 +22,17 @@
"format": "prettier --write ."
},
"module": "index.js",
"main": "./dist/simpleMindMap.umd.min.js",
"__main": "./dist/simpleMindMap.umd.min.js",
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",
"canvg": "^3.0.7",
"deepmerge": "^1.5.2",
"eventemitter3": "^4.0.7",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0",
"quill": "^1.3.6",
"uuid": "^9.0.0",
"xml-js": "^1.6.11"
},
"keywords": [

View File

@@ -1,65 +0,0 @@
// 在下一个事件循环里执行任务
const nextTick = function (fn, ctx) {
let pending = false
let timerFunc = null
let handle = () => {
pending = false
ctx ? fn.call(ctx) : fn()
}
// 支持MutationObserver接口的话使用MutationObserver
if (typeof MutationObserver !== 'undefined') {
let counter = 1
let observer = new MutationObserver(handle)
let textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true // 设为 true 表示监视指定目标节点或子节点树中节点所包含的字符数据的变化
})
timerFunc = function () {
counter = (counter + 1) % 2 // counter会在0和1两者循环变化
textNode.data = counter // 节点变化会触发回调handle
}
} else {
// 否则使用定时器
timerFunc = setTimeout
}
return function () {
if (pending) return
pending = true
timerFunc(handle, 0)
}
}
// 批量执行
class BatchExecution {
// 构造函数
constructor() {
this.has = {}
this.queue = []
this.nextTick = nextTick(this.flush, this)
}
// 添加任务
push(name, fn) {
if (this.has[name]) {
return
}
this.has[name] = true
this.queue.push({
name,
fn
})
this.nextTick()
}
// 执行队列
flush() {
let fns = this.queue.slice(0)
this.queue = []
fns.forEach(({ name, fn }) => {
this.has[name] = false
fn()
})
}
}
export default BatchExecution

File diff suppressed because it is too large Load Diff

View File

@@ -1,167 +0,0 @@
import { tagColorList } from './utils/constant'
const rootProp = ['paddingX', 'paddingY']
// 样式类
class Style {
// 设置背景样式
static setBackgroundStyle(el, themeConfig) {
let { backgroundColor, backgroundImage, backgroundRepeat } = themeConfig
el.style.backgroundColor = backgroundColor
if (backgroundImage) {
el.style.backgroundImage = `url(${backgroundImage})`
el.style.backgroundRepeat = backgroundRepeat
}
}
// 构造函数
constructor(ctx, themeConfig) {
this.ctx = ctx
this.themeConfig = themeConfig
}
// 更新主题配置
updateThemeConfig(themeConfig) {
this.themeConfig = themeConfig
}
// 合并样式
merge(prop, root, isActive) {
// 三级及以下节点
let defaultConfig = this.themeConfig.node
if (root || rootProp.includes(prop)) {
// 直接使用最外层样式
defaultConfig = this.themeConfig
} else if (this.ctx.isGeneralization) {
// 概要节点
defaultConfig = this.themeConfig.generalization
} else if (this.ctx.layerIndex === 0) {
// 根节点
defaultConfig = this.themeConfig.root
} else if (this.ctx.layerIndex === 1) {
// 二级节点
defaultConfig = this.themeConfig.second
}
// 激活状态
if (isActive !== undefined ? isActive : this.ctx.nodeData.data.isActive) {
if (
this.ctx.nodeData.data.activeStyle &&
this.ctx.nodeData.data.activeStyle[prop] !== undefined
) {
return this.ctx.nodeData.data.activeStyle[prop]
} else if (defaultConfig.active && defaultConfig.active[prop]) {
return defaultConfig.active[prop]
}
}
// 优先使用节点本身的样式
return this.getSelfStyle(prop) !== undefined
? this.getSelfStyle(prop)
: defaultConfig[prop]
}
// 获取某个样式值
getStyle(prop, root, isActive) {
return this.merge(prop, root, isActive)
}
// 获取自身自定义样式
getSelfStyle(prop) {
return this.ctx.nodeData.data[prop]
}
// 矩形
rect(node) {
this.shape(node)
node.radius(this.merge('borderRadius'))
}
// 矩形外的其他形状
shape(node) {
node.fill({
color: this.merge('fillColor')
})
// 节点使用横线样式,不需要渲染非激活状态的边框样式
if (
!this.ctx.isRoot &&
!this.ctx.isGeneralization &&
this.themeConfig.nodeUseLineStyle &&
!this.ctx.nodeData.data.isActive
) {
return
}
node.stroke({
color: this.merge('borderColor'),
width: this.merge('borderWidth'),
dasharray: this.merge('borderDasharray')
})
}
// 文字
text(node) {
node
.fill({
color: this.merge('color')
})
.css({
'font-family': this.merge('fontFamily'),
'font-size': this.merge('fontSize'),
'font-weight': this.merge('fontWeight'),
'font-style': this.merge('fontStyle'),
'text-decoration': this.merge('textDecoration')
})
}
// html文字节点
domText(node, fontSizeScale = 1) {
node.style.fontFamily = this.merge('fontFamily')
node.style.fontSize = this.merge('fontSize') * fontSizeScale + 'px'
node.style.fontWeight = this.merge('fontWeight') || 'normal'
}
// 标签文字
tagText(node, index) {
node
.fill({
color: tagColorList[index].color
})
.css({
'font-size': '12px'
})
}
// 标签矩形
tagRect(node, index) {
node.fill({
color: tagColorList[index].background
})
}
// 内置图标
iconNode(node) {
node.attr({
fill: this.merge('color')
})
}
// 连线
line(node, { width, color, dasharray } = {}) {
node.stroke({ width, color, dasharray }).fill({ color: 'none' })
}
// 概要连线
generalizationLine(node) {

View File

@@ -1,118 +0,0 @@
import { getStrWithBrFromHtml } from './utils'
// 节点文字编辑类
export default class TextEdit {
// 构造函数
constructor(renderer) {
this.renderer = renderer
this.mindMap = renderer.mindMap
// 文本编辑框
this.textEditNode = null
// 文本编辑框是否显示
this.showTextEdit = false
this.bindEvent()
}
// 事件
bindEvent() {
this.show = this.show.bind(this)
// 节点双击事件
this.mindMap.on('node_dblclick', this.show)
// 点击事件
this.mindMap.on('draw_click', () => {
// 隐藏文本编辑框
this.hideEditTextBox()
})
// 展开收缩按钮点击事件
this.mindMap.on('expand_btn_click', () => {
this.hideEditTextBox()
})
// 节点激活前事件
this.mindMap.on('before_node_active', () => {
this.hideEditTextBox()
})
// 注册编辑快捷键
this.mindMap.keyCommand.addShortcut('F2', () => {
if (this.renderer.activeNodeList.length <= 0) {
return
}
this.show(this.renderer.activeNodeList[0])
})
}
// 注册临时快捷键
registerTmpShortcut() {
// 注册回车快捷键
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
}
// 显示文本编辑框
show(node) {
this.showEditTextBox(node, node._textData.node.node.getBoundingClientRect())
}
// 显示文本编辑框
showEditTextBox(node, rect) {
this.mindMap.emit('before_show_text_edit')
this.registerTmpShortcut()
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none;`
this.textEditNode.setAttribute('contenteditable', true)
this.textEditNode.addEventListener('keyup', e => {
e.stopPropagation()
})
document.body.appendChild(this.textEditNode)
}
node.style.domText(this.textEditNode, this.mindMap.view.scale)
this.textEditNode.innerHTML = node.nodeData.data.text
.split(/\n/gim)
.join('<br>')
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
this.textEditNode.style.display = 'block'
this.showTextEdit = true
// 选中文本
this.selectNodeText()
}
// 选中文本
selectNodeText() {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.textEditNode)
selection.removeAllRanges()
selection.addRange(range)
}
// 隐藏文本编辑框
hideEditTextBox() {
if (!this.showTextEdit) {
return
}
this.renderer.activeNodeList.forEach(node => {
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
this.mindMap.execCommand('SET_NODE_TEXT', node, str)
if (node.isGeneralization) {
// 概要节点
node.generalizationBelongNode.updateGeneralization()
}
this.mindMap.render()
})
this.mindMap.emit(
'hide_text_edit',
this.textEditNode,
this.renderer.activeNodeList
)
this.textEditNode.style.display = 'none'
this.textEditNode.innerHTML = ''
this.textEditNode.style.fontFamily = 'inherit'
this.textEditNode.style.fontSize = 'inherit'
this.textEditNode.style.fontWeight = 'normal'
this.showTextEdit = false
}
}

View File

@@ -1,164 +0,0 @@
// 视图操作类
class View {
// 构造函数
constructor(opt = {}) {
this.opt = opt
this.mindMap = this.opt.mindMap
this.scale = 1
this.sx = 0
this.sy = 0
this.x = 0
this.y = 0
this.firstDrag = true
this.setTransformData(this.mindMap.opt.viewData)
this.bind()
}
// 绑定
bind() {
// 快捷键
this.mindMap.keyCommand.addShortcut('Control+=', () => {
this.enlarge()
})
this.mindMap.keyCommand.addShortcut('Control+-', () => {
this.narrow()
})
this.mindMap.keyCommand.addShortcut('Control+Enter', () => {
this.reset()
})
this.mindMap.svg.on('dblclick', () => {
this.reset()
})
// 拖动视图
this.mindMap.event.on('mousedown', () => {
this.sx = this.x
this.sy = this.y
})
this.mindMap.event.on('drag', (e, event) => {
if (e.ctrlKey) {
// 按住ctrl键拖动为多选
return
}
if (this.firstDrag) {
this.firstDrag = false
// 清除激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
}
this.x = this.sx + event.mousemoveOffset.x
this.y = this.sy + event.mousemoveOffset.y
this.transform()
})
this.mindMap.event.on('mouseup', () => {
this.firstDrag = true
})
// 放大缩小视图
this.mindMap.event.on('mousewheel', (e, dir) => {
// // 放大
if (dir === 'down') {
this.enlarge()
} else {
// 缩小
this.narrow()
}
})
}
// 获取当前变换状态数据
getTransformData() {
return {
transform: this.mindMap.draw.transform(),
state: {
scale: this.scale,
x: this.x,
y: this.y,
sx: this.sx,
sy: this.sy
}
}
}
// 动态设置变换状态数据
setTransformData(viewData) {
if (viewData) {
Object.keys(viewData.state).forEach(prop => {
this[prop] = viewData.state[prop]
})
this.mindMap.draw.transform({
...viewData.transform
})
this.mindMap.emit('view_data_change', this.getTransformData())
this.mindMap.emit('scale', this.scale)
}
}
// 平移x方向
translateX(step) {
this.x += step
this.transform()
}
// 平移x方式到
translateXTo(x) {
this.x = x
this.transform()
}
// 平移y方向
translateY(step) {
this.y += step
this.transform()
}
// 平移y方向到
translateYTo(y) {
this.y = y
this.transform()
}
// 应用变换
transform() {
this.mindMap.draw.transform({
scale: this.scale,
// origin: 'center center',
translate: [this.x, this.y]
})
this.mindMap.emit('view_data_change', this.getTransformData())
}
// 恢复
reset() {
this.scale = 1
this.x = 0
this.y = 0
this.transform()
}
// 缩小
narrow() {
if (this.scale - this.mindMap.opt.scaleRatio > 0.1) {
this.scale -= this.mindMap.opt.scaleRatio
} else {
this.scale = 0.1
}
this.transform()
this.mindMap.emit('scale', this.scale)
}
// 放大
enlarge() {
this.scale += this.mindMap.opt.scaleRatio
this.transform()
this.mindMap.emit('scale', this.scale)
}
// 设置缩放
setScale(scale) {
this.scale = scale
this.transform()
this.mindMap.emit('scale', this.scale)
}
}
export default View

View File

@@ -0,0 +1,332 @@
// 标签颜色列表
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: {
READONLY: 'readonly',
EDIT: 'edit'
},
LAYOUT: {
LOGICAL_STRUCTURE: 'logicalStructure',
MIND_MAP: 'mindMap',
ORGANIZATION_STRUCTURE: 'organizationStructure',
CATALOG_ORGANIZATION: 'catalogOrganization',
TIMELINE: 'timeline',
TIMELINE2: 'timeline2',
FISHBONE: 'fishbone',
VERTICAL_TIMELINE: 'verticalTimeline'
},
DIR: {
UP: 'up',
LEFT: 'left',
DOWN: 'down',
RIGHT: 'right'
},
KEY_DIR: {
LEFT: 'Left',
UP: 'Up',
RIGHT: 'Right',
DOWN: 'Down'
},
SHAPE: {
RECTANGLE: 'rectangle',
DIAMOND: 'diamond',
PARALLELOGRAM: 'parallelogram',
ROUNDED_RECTANGLE: 'roundedRectangle',
OCTAGONAL_RECTANGLE: 'octagonalRectangle',
OUTER_TRIANGULAR_RECTANGLE: 'outerTriangularRectangle',
INNER_TRIANGULAR_RECTANGLE: 'innerTriangularRectangle',
ELLIPSE: 'ellipse',
CIRCLE: 'circle'
},
MOUSE_WHEEL_ACTION: {
ZOOM: 'zoom',
MOVE: 'move'
},
INIT_ROOT_NODE_POSITION: {
LEFT: 'left',
TOP: 'top',
RIGHT: 'right',
BOTTOM: 'bottom',
CENTER: 'center'
},
LAYOUT_GROW_DIR: {
LEFT: 'left',
TOP: 'top',
RIGHT: 'right',
BOTTOM: 'bottom'
},
PASTE_TYPE: {
CLIP_BOARD: 'clipBoard',
CANVAS: 'canvas'
}
}
export const initRootNodePositionMap = {
[CONSTANTS.INIT_ROOT_NODE_POSITION.LEFT]: 0,
[CONSTANTS.INIT_ROOT_NODE_POSITION.TOP]: 0,
[CONSTANTS.INIT_ROOT_NODE_POSITION.RIGHT]: 1,
[CONSTANTS.INIT_ROOT_NODE_POSITION.BOTTOM]: 1,
[CONSTANTS.INIT_ROOT_NODE_POSITION.CENTER]: 0.5,
}
// 布局结构列表
export const layoutList = [
{
name: '逻辑结构图',
value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
},
{
name: '思维导图',
value: CONSTANTS.LAYOUT.MIND_MAP,
},
{
name: '组织结构图',
value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
},
{
name: '目录组织图',
value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION,
},
{
name: '时间轴',
value: CONSTANTS.LAYOUT.TIMELINE,
},
{
name: '时间轴2',
value: CONSTANTS.LAYOUT.TIMELINE2,
},
{
name: '竖向时间轴',
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
},
{
name: '鱼骨图',
value: CONSTANTS.LAYOUT.FISHBONE,
}
]
export const layoutValueList = [
CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
CONSTANTS.LAYOUT.MIND_MAP,
CONSTANTS.LAYOUT.CATALOG_ORGANIZATION,
CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
CONSTANTS.LAYOUT.TIMELINE,
CONSTANTS.LAYOUT.TIMELINE2,
CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
CONSTANTS.LAYOUT.FISHBONE
]
// 节点数据中非样式的字段
export const nodeDataNoStylePropList = [
'text',
'image',
'imageTitle',
'imageSize',
'icon',
'tag',
'hyperlink',
'hyperlinkTitle',
'note',
'expand',
'isActive',
'generalization',
'richText',
'resetRichText',
'uid',
'activeStyle'
]
// 数据缓存
export const commonCaches = {
measureCustomNodeContentSizeEl: null
}

View File

@@ -0,0 +1,126 @@
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,
// 导出图片时的内边距
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,
// 当mousewheelAction设为zoom时默认向前滚动是缩小向后滚动是放大如果该属性设为true那么会反过来
mousewheelZoomActionReverse: false,
// 默认插入的二级节点的文字
defaultInsertSecondLevelNodeText: '二级节点',
// 默认插入的二级以下节点的文字
defaultInsertBelowSecondLevelNodeText: '分支主题',
// 展开收起按钮的颜色
expandBtnStyle: {
color: '#808080',
fill: '#fff'
},
// 自定义展开收起按钮的图标
expandBtnIcon: {
open: '', // svg字符串
close: ''
},
// 是否只有当鼠标在画布内才响应快捷键事件
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
}

View File

@@ -1,4 +1,4 @@
import { copyRenderTree, simpleDeepClone } from './utils'
import { copyRenderTree, simpleDeepClone, nextTick } from '../../utils'
// 命令类
class Command {
@@ -11,6 +11,7 @@ class Command {
this.activeHistoryIndex = 0
// 注册快捷键
this.registerShortcutKeys()
this.addHistory = nextTick(this.addHistory, this)
}
// 清空历史数据
@@ -36,7 +37,7 @@ class Command {
this.commands[name].forEach(fn => {
fn(...args)
})
if (name === 'BACK' || name === 'FORWARD') {
if (['BACK', 'FORWARD', 'SET_NODE_ACTIVE', 'CLEAR_ACTIVE_NODE'].includes(name)) {
return
}
this.addHistory()
@@ -72,8 +73,21 @@ class Command {
// 添加回退数据
addHistory() {
if (this.mindMap.opt.readonly) {
return
}
let data = this.getCopyData()
// 此次数据和上次一样则不重复添加
if (this.history.length > 0 && JSON.stringify(this.history[this.history.length - 1]) === JSON.stringify(data)) {
return
}
// 删除当前历史指针后面的数据
this.history = this.history.slice(0, this.activeHistoryIndex + 1)
this.history.push(simpleDeepClone(data))
// 历史记录数超过最大数量
if (this.history.length > this.mindMap.opt.maxHistoryCount) {
this.history.shift()
}
this.activeHistoryIndex = this.history.length - 1
this.mindMap.emit('data_change', data)
this.mindMap.emit(
@@ -85,6 +99,9 @@ class Command {
// 回退
back(step = 1) {
if (this.mindMap.opt.readonly) {
return
}
if (this.activeHistoryIndex - step >= 0) {
this.activeHistoryIndex -= step
this.mindMap.emit(
@@ -92,23 +109,45 @@ class Command {
this.activeHistoryIndex,
this.history.length
)
return simpleDeepClone(this.history[this.activeHistoryIndex])
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
this.mindMap.emit('data_change', data)
return data
}
}
// 前进
forward(step = 1) {
if (this.mindMap.opt.readonly) {
return
}
let len = this.history.length
if (this.activeHistoryIndex + step <= len - 1) {
this.activeHistoryIndex += step
this.mindMap.emit('back_forward', this.activeHistoryIndex)
return simpleDeepClone(this.history[this.activeHistoryIndex])
this.mindMap.emit('back_forward', this.activeHistoryIndex, this.history.length)
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
this.mindMap.emit('data_change', data)
return data
}
}
// 获取渲染树数据副本
getCopyData() {
return copyRenderTree({}, this.mindMap.renderer.renderTree)
return copyRenderTree({}, this.mindMap.renderer.renderTree, true)
}
// 移除节点数据中的uid
removeDataUid(data) {
data = simpleDeepClone(data)
let walk = (root) => {
delete root.data.uid
if (root.children && root.children.length > 0) {
root.children.forEach((item) => {
walk(item)
})
}
}
walk(data)
return data
}
}

View File

@@ -1,4 +1,4 @@
import { keyMap } from './utils/keyMap'
import { keyMap } from './keyMap'
// 快捷按键、命令处理类
export default class KeyCommand {
// 构造函数
@@ -10,6 +10,7 @@ export default class KeyCommand {
}
this.shortcutMapCache = {}
this.isPause = false
this.isInSvg = false
this.bindEvent()
}
@@ -37,14 +38,30 @@ export default class KeyCommand {
// 绑定事件
bindEvent() {
// 只有当鼠标在画布内才响应快捷键
this.mindMap.on('svg_mouseenter', () => {
this.isInSvg = true
})
this.mindMap.on('svg_mouseleave', () => {
if (this.mindMap.richText && this.mindMap.richText.showTextEdit) {
return
}
if (this.mindMap.renderer.textEdit.showTextEdit || (this.mindMap.associativeLine && this.mindMap.associativeLine.showTextEdit)) {
return
}
this.isInSvg = false
})
window.addEventListener('keydown', e => {
if (this.isPause) {
if (this.isPause || (this.mindMap.opt.enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) {
return
}
Object.keys(this.shortcutMap).forEach(key => {
if (this.checkKey(e, key)) {
e.stopPropagation()
e.preventDefault()
// 粘贴事件不组织因为要监听paste事件
if (!this.checkKey(e, 'Control+v')) {
e.stopPropagation()
e.preventDefault()
}
this.shortcutMap[key].forEach(fn => {
fn()
})

View File

@@ -1,4 +1,5 @@
import EventEmitter from 'eventemitter3'
import { CONSTANTS } from '../../constants/constant'
// 事件类
class Event extends EventEmitter {
@@ -8,6 +9,7 @@ class Event extends EventEmitter {
this.opt = opt
this.mindMap = opt.mindMap
this.isLeftMousedown = false
this.isRightMousedown = false
this.mousedownPos = {
x: 0,
y: 0
@@ -26,6 +28,7 @@ class Event extends EventEmitter {
// 绑定函数上下文
bindFn() {
this.onBodyClick = this.onBodyClick.bind(this)
this.onDrawClick = this.onDrawClick.bind(this)
this.onMousedown = this.onMousedown.bind(this)
this.onMousemove = this.onMousemove.bind(this)
@@ -34,33 +37,36 @@ class Event extends EventEmitter {
this.onContextmenu = this.onContextmenu.bind(this)
this.onSvgMousedown = this.onSvgMousedown.bind(this)
this.onKeyup = this.onKeyup.bind(this)
this.onMouseenter = this.onMouseenter.bind(this)
this.onMouseleave = this.onMouseleave.bind(this)
}
// 绑定事件
bind() {
document.body.addEventListener('click', this.onBodyClick)
this.mindMap.svg.on('click', this.onDrawClick)
this.mindMap.el.addEventListener('mousedown', this.onMousedown)
this.mindMap.svg.on('mousedown', this.onSvgMousedown)
window.addEventListener('mousemove', this.onMousemove)
window.addEventListener('mouseup', this.onMouseup)
// 兼容火狐浏览器
if (window.navigator.userAgent.toLowerCase().indexOf('firefox') != -1) {
this.mindMap.el.addEventListener('DOMMouseScroll', this.onMousewheel)
} else {
this.mindMap.el.addEventListener('mousewheel', this.onMousewheel)
}
this.mindMap.el.addEventListener('wheel', this.onMousewheel)
this.mindMap.svg.on('contextmenu', this.onContextmenu)
this.mindMap.svg.on('mouseenter', this.onMouseenter)
this.mindMap.svg.on('mouseleave', this.onMouseleave)
window.addEventListener('keyup', this.onKeyup)
}
// 解绑事件
unbind() {
document.body.removeEventListener('click', this.onBodyClick)
this.mindMap.svg.off('click', this.onDrawClick)
this.mindMap.el.removeEventListener('mousedown', this.onMousedown)
window.removeEventListener('mousemove', this.onMousemove)
window.removeEventListener('mouseup', this.onMouseup)
this.mindMap.el.removeEventListener('mousewheel', this.onMousewheel)
this.mindMap.el.removeEventListener('wheel', this.onMousewheel)
this.mindMap.svg.off('contextmenu', this.onContextmenu)
this.mindMap.svg.off('mouseenter', this.onMouseenter)
this.mindMap.svg.off('mouseleave', this.onMouseleave)
window.removeEventListener('keyup', this.onKeyup)
}
@@ -69,6 +75,11 @@ class Event extends EventEmitter {
this.emit('draw_click', e)
}
// 页面的单击事件
onBodyClick(e) {
this.emit('body_click', e)
}
// svg画布的鼠标按下事件
onSvgMousedown(e) {
this.emit('svg_mousedown', e)
@@ -76,10 +87,11 @@ class Event extends EventEmitter {
// 鼠标按下事件
onMousedown(e) {
// e.preventDefault()
// 鼠标左键
if (e.which === 1) {
this.isLeftMousedown = true
} else if (e.which === 3) {
this.isRightMousedown = true
}
this.mousedownPos.x = e.clientX
this.mousedownPos.y = e.clientY
@@ -88,13 +100,18 @@ class Event extends EventEmitter {
// 鼠标移动事件
onMousemove(e) {
// e.preventDefault()
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
this.mousemovePos.x = e.clientX
this.mousemovePos.y = e.clientY
this.mousemoveOffset.x = e.clientX - this.mousedownPos.x
this.mousemoveOffset.y = e.clientY - this.mousedownPos.y
this.emit('mousemove', e, this)
if (this.isLeftMousedown) {
if (
useLeftKeySelectionRightKeyDrag
? this.isRightMousedown
: this.isLeftMousedown
) {
e.preventDefault()
this.emit('drag', e, this)
}
}
@@ -102,6 +119,7 @@ class Event extends EventEmitter {
// 鼠标松开事件
onMouseup(e) {
this.isLeftMousedown = false
this.isRightMousedown = false
this.emit('mouseup', e, this)
}
@@ -110,12 +128,25 @@ class Event extends EventEmitter {
e.stopPropagation()
e.preventDefault()
let dir
if ((e.wheelDeltaY || e.detail) > 0) {
dir = 'up'
// 解决mac触控板双指缩放方向相反的问题
if (e.ctrlKey) {
if (e.deltaY > 0) dir = CONSTANTS.DIR.UP
if (e.deltaY < 0) dir = CONSTANTS.DIR.DOWN
if (e.deltaX > 0) dir = CONSTANTS.DIR.LEFT
if (e.deltaX < 0) dir = CONSTANTS.DIR.RIGHT
} else {
dir = 'down'
if ((e.wheelDeltaY || e.detail) > 0) dir = CONSTANTS.DIR.UP
if ((e.wheelDeltaY || e.detail) < 0) dir = CONSTANTS.DIR.DOWN
if ((e.wheelDeltaX || e.detail) > 0) dir = CONSTANTS.DIR.LEFT
if ((e.wheelDeltaX || e.detail) < 0) dir = CONSTANTS.DIR.RIGHT
}
this.emit('mousewheel', e, dir, this)
// 判断是否是触控板
let isTouchPad = false
// mac、windows
if (e.wheelDeltaY === e.deltaY * -3 || Math.abs(e.wheelDeltaY) <= 10) {
isTouchPad = true
}
this.emit('mousewheel', e, dir, this, isTouchPad)
}
// 鼠标右键菜单事件
@@ -128,6 +159,16 @@ class Event extends EventEmitter {
onKeyup(e) {
this.emit('keyup', e)
}
// 进入
onMouseenter(e) {
this.emit('svg_mouseenter', e)
}
// 离开
onMouseleave(e) {
this.emit('svg_mouseleave', e)
}
}
export default Event

View File

@@ -0,0 +1,247 @@
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils'
// 节点文字编辑类
export default class TextEdit {
// 构造函数
constructor(renderer) {
this.renderer = renderer
this.mindMap = renderer.mindMap
// 当前编辑的节点
this.currentNode = null
// 文本编辑框
this.textEditNode = null
// 隐藏的文本输入框
this.hiddenInputEl = null
// 文本编辑框是否显示
this.showTextEdit = false
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
this.cacheEditingText = ''
this.bindEvent()
this.createHiddenInput()
}
// 事件
bindEvent() {
this.show = this.show.bind(this)
this.onScale = this.onScale.bind(this)
// 节点双击事件
this.mindMap.on('node_dblclick', this.show)
// 点击事件
this.mindMap.on('draw_click', () => {
// 隐藏文本编辑框
this.hideEditTextBox()
})
this.mindMap.on('body_click', () => {
// 隐藏文本编辑框
if (this.mindMap.opt.isEndNodeTextEditOnClickOuter) {
this.hideEditTextBox()
}
})
this.mindMap.on('svg_mousedown', () => {
// 隐藏文本编辑框
this.hideEditTextBox()
})
// 展开收缩按钮点击事件
this.mindMap.on('expand_btn_click', () => {
this.hideEditTextBox()
})
// 节点激活前事件
this.mindMap.on('before_node_active', () => {
this.hideEditTextBox()
})
// 节点激活事件
this.mindMap.on('node_active', () => {
this.focusHiddenInput()
})
// 注册编辑快捷键
this.mindMap.keyCommand.addShortcut('F2', () => {
if (this.renderer.activeNodeList.length <= 0) {
return
}
this.show(this.renderer.activeNodeList[0])
})
this.mindMap.on('scale', this.onScale)
}
// 创建一个隐藏的文本输入框
createHiddenInput() {
if (this.hiddenInputEl) return
this.hiddenInputEl = document.createElement('input')
this.hiddenInputEl.type = 'text'
this.hiddenInputEl.style.cssText = `
position: fixed;
left: -99999px;
top: -99999px;
`
// 监听粘贴事件
this.hiddenInputEl.addEventListener('paste', async event => {
event.preventDefault()
const text = (event.clipboardData || window.clipboardData).getData('text')
const files = event.clipboardData.files
let img = null
if (files.length > 0) {
for (let i = 0; i < files.length; i++) {
if (/^image\//.test(files[i].type)) {
img = files[i]
break
}
}
}
this.mindMap.emit('paste', {
text,
img
})
})
document.body.appendChild(this.hiddenInputEl)
}
// 让隐藏的文本输入框聚焦
focusHiddenInput() {
if (this.hiddenInputEl) this.hiddenInputEl.focus()
}
// 注册临时快捷键
registerTmpShortcut() {
// 注册回车快捷键
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
this.mindMap.keyCommand.addShortcut('Tab', () => {
this.hideEditTextBox()
})
}
// 显示文本编辑框
// isInserting是否是刚创建的节点
async show(node, e, isInserting = false) {
// 使用了自定义节点内容那么不响应编辑事件
if (node.isUseCustomNodeContent()) {
return
}
let { beforeTextEdit } = this.mindMap.opt
if (typeof beforeTextEdit === 'function') {
let isShow = false
try {
isShow = await beforeTextEdit(node, isInserting)
} catch (error) {
isShow = false
}
if (!isShow) return
}
this.currentNode = node
let { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
this.mindMap.view.translateXY(offsetLeft, offsetTop)
let rect = node._textData.node.node.getBoundingClientRect()
if (this.mindMap.richText) {
this.mindMap.richText.showEditText(node, rect, isInserting)
return
}
this.showEditTextBox(node, rect)
}
// 处理画布缩放
onScale() {
if (!this.currentNode) return
if (this.mindMap.richText) {
this.mindMap.richText.cacheEditingText =
this.mindMap.richText.getEditText()
this.mindMap.richText.showTextEdit = false
} else {
this.cacheEditingText = this.getEditText()
this.showTextEdit = false
}
this.show(this.currentNode)
}
// 显示文本编辑框
showEditTextBox(node, rect) {
this.mindMap.emit('before_show_text_edit')
this.registerTmpShortcut()
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
this.textEditNode.setAttribute('contenteditable', true)
this.textEditNode.addEventListener('keyup', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
document.body.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 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.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'
if (isMultiLine && lineHeight !== 1) {
this.textEditNode.style.transform = `translateY(${
-((lineHeight * fontSize - fontSize) / 2) * scale
}px)`
}
this.showTextEdit = true
// 选中文本
if (!this.cacheEditingText) {
this.selectNodeText()
}
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)
}
// 隐藏文本编辑框
hideEditTextBox() {
this.currentNode = null
if (this.mindMap.richText) {
return this.mindMap.richText.hideEditText()
}
if (!this.showTextEdit) {
return
}
this.renderer.activeNodeList.forEach(node => {
let str = this.getEditText()
this.mindMap.execCommand('SET_NODE_TEXT', node, str)
if (node.isGeneralization) {
// 概要节点
node.generalizationBelongNode.updateGeneralization()
}
this.mindMap.render()
})
this.mindMap.emit(
'hide_text_edit',
this.textEditNode,
this.renderer.activeNodeList
)
this.textEditNode.style.display = 'none'
this.textEditNode.innerHTML = ''
this.textEditNode.style.fontFamily = 'inherit'
this.textEditNode.style.fontSize = 'inherit'
this.textEditNode.style.fontWeight = 'normal'
this.textEditNode.style.transform = 'translateY(0)'
this.showTextEdit = false
}
}

View File

@@ -0,0 +1,834 @@
import Style from './Style'
import Shape from './Shape'
import { asyncRun, nodeToHTML } from '../../../utils'
import { G, Rect, ForeignObject, SVG } from '@svgdotjs/svg.js'
import nodeGeneralizationMethods from './nodeGeneralization'
import nodeExpandBtnMethods from './nodeExpandBtn'
import nodeCommandWrapsMethods from './nodeCommandWraps'
import nodeCreateContentsMethods from './nodeCreateContents'
import { CONSTANTS } from '../../../constants/constant'
// 节点类
class Node {
// 构造函数
constructor(opt = {}) {
// 节点数据
this.nodeData = this.handleData(opt.data || {})
// id
this.uid = opt.uid
// 控制实例
this.mindMap = opt.mindMap
// 渲染实例
this.renderer = opt.renderer
// 渲染器
this.draw = opt.draw || null
// 样式实例
this.style = new Style(this)
// 形状实例
this.shapeInstance = new Shape(this)
this.shapePadding = {
paddingX: 0,
paddingY: 0
}
// 是否是根节点
this.isRoot = opt.isRoot === undefined ? false : opt.isRoot
// 是否是概要节点
this.isGeneralization =
opt.isGeneralization === undefined ? false : opt.isGeneralization
this.generalizationBelongNode = null
// 节点层级
this.layerIndex = opt.layerIndex === undefined ? 0 : opt.layerIndex
// 节点宽
this.width = opt.width || 0
// 节点高
this.height = opt.height || 0
// left
this._left = opt.left || 0
// top
this._top = opt.top || 0
// 自定义位置
this.customLeft = opt.data.data.customLeft || undefined
this.customTop = opt.data.data.customTop || undefined
// 是否正在拖拽中
this.isDrag = false
// 父节点
this.parent = opt.parent || null
// 子节点
this.children = opt.children || []
// 节点内容的容器
this.group = null
this.shapeNode = null // 节点形状节点
// 节点内容对象
this._customNodeContent = null
this._imgData = null
this._iconData = null
this._textData = null
this._hyperlinkData = null
this._tagData = null
this._noteData = null
this.noteEl = null
this._expandBtn = null
this._lastExpandBtnType = null
this._showExpandBtn = false
this._openExpandNode = null
this._closeExpandNode = null
this._fillExpandNode = null
this._lines = []
this._generalizationLine = null
this._generalizationNode = null
this._unVisibleRectRegionNode = null
this._isMouseenter = false
// 尺寸信息
this._rectInfo = {
imgContentWidth: 0,
imgContentHeight: 0,
textContentWidth: 0,
textContentHeight: 0
}
// 概要节点的宽高
this._generalizationNodeWidth = 0
this._generalizationNodeHeight = 0
// 各种文字信息的间距
this.textContentItemMargin = this.mindMap.opt.textContentMargin
// 图片和文字节点的间距
this.blockContentMargin = this.mindMap.opt.imgTextMargin
// 展开收缩按钮尺寸
this.expandBtnSize = this.mindMap.opt.expandBtnSize
// 是否是多选节点
this.isMultipleChoice = false
// 是否需要重新layout
this.needLayout = false
// 概要相关方法
Object.keys(nodeGeneralizationMethods).forEach(item => {
this[item] = nodeGeneralizationMethods[item].bind(this)
})
// 展开收起按钮相关方法
Object.keys(nodeExpandBtnMethods).forEach(item => {
this[item] = nodeExpandBtnMethods[item].bind(this)
})
// 命令的相关方法
Object.keys(nodeCommandWrapsMethods).forEach(item => {
this[item] = nodeCommandWrapsMethods[item].bind(this)
})
// 创建节点内容的相关方法
Object.keys(nodeCreateContentsMethods).forEach(item => {
this[item] = nodeCreateContentsMethods[item].bind(this)
})
// 初始化
this.getSize()
}
// 支持自定义位置
get left() {
return this.customLeft || this._left
}
set left(val) {
this._left = val
}
get top() {
return this.customTop || this._top
}
set top(val) {
this._top = val
}
// 复位部分布局时会重新设置的数据
reset() {
this.children = []
this.parent = null
this.isRoot = false
this.layerIndex = 0
this.left = 0
this.top = 0
}
// 处理数据
handleData(data) {
data.data.expand = data.data.expand === false ? false : true
data.data.isActive = data.data.isActive === true ? true : false
data.children = data.children || []
return data
}
// 创建节点的各个内容对象数据
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()
this._hyperlinkData = this.createHyperlinkNode()
this._tagData = this.createTagNode()
this._noteData = this.createNoteNode()
}
// 计算节点的宽高
getSize() {
this.updateGeneralization()
this.createNodeData()
let { width, height } = this.getNodeRect()
// 判断节点尺寸是否有变化
let changed = this.width !== width || this.height !== height
this.width = width
this.height = height
return changed
}
// 计算节点尺寸信息
getNodeRect() {
// 自定义节点内容
if (this.isUseCustomNodeContent()) {
let rect = this.measureCustomNodeContentSize(this._customNodeContent)
return {
width: rect.width,
height: rect.height
}
}
// 宽高
let imgContentWidth = 0
let imgContentHeight = 0
let textContentWidth = 0
let textContentHeight = 0
// 存在图片
if (this._imgData) {
this._rectInfo.imgContentWidth = imgContentWidth = this._imgData.width
this._rectInfo.imgContentHeight = imgContentHeight = this._imgData.height
}
// 图标
if (this._iconData.length > 0) {
textContentWidth += this._iconData.reduce((sum, cur) => {
textContentHeight = Math.max(textContentHeight, cur.height)
return (sum += cur.width + this.textContentItemMargin)
}, 0)
}
// 文字
if (this._textData) {
textContentWidth += this._textData.width
textContentHeight = Math.max(textContentHeight, this._textData.height)
}
// 超链接
if (this._hyperlinkData) {
textContentWidth += this._hyperlinkData.width
textContentHeight = Math.max(
textContentHeight,
this._hyperlinkData.height
)
}
// 标签
if (this._tagData.length > 0) {
textContentWidth += this._tagData.reduce((sum, cur) => {
textContentHeight = Math.max(textContentHeight, cur.height)
return (sum += cur.width + this.textContentItemMargin)
}, 0)
}
// 备注
if (this._noteData) {
textContentWidth += this._noteData.width
textContentHeight = Math.max(textContentHeight, this._noteData.height)
}
// 文字内容部分的尺寸
this._rectInfo.textContentWidth = textContentWidth
this._rectInfo.textContentHeight = textContentHeight
// 间距
let margin =
imgContentHeight > 0 && textContentHeight > 0
? this.blockContentMargin
: 0
let { paddingX, paddingY } = this.getPaddingVale()
// 纯内容宽高
let _width = Math.max(imgContentWidth, textContentWidth)
let _height = imgContentHeight + textContentHeight
// 计算节点形状需要的附加内边距
let { paddingX: shapePaddingX, paddingY: shapePaddingY } =
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
this.shapePadding.paddingX = shapePaddingX
this.shapePadding.paddingY = shapePaddingY
return {
width: _width + paddingX * 2 + shapePaddingX * 2,
height: _height + paddingY * 2 + margin + shapePaddingY * 2
}
}
// 定位节点内容
layout() {
// 清除之前的内容
this.group.clear()
let { width, height, textContentItemMargin } = this
let { paddingY } = this.getPaddingVale()
paddingY += this.shapePadding.paddingY
// 节点形状
this.shapeNode = this.shapeInstance.createShape()
this.shapeNode.addClass('smm-node-shape')
this.group.add(this.shapeNode)
this.updateNodeShape()
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
this.renderExpandBtnPlaceholderRect()
// 概要节点添加一个带所属节点id的类名
if (this.isGeneralization && this.generalizationBelongNode) {
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
}
// 如果存在自定义节点内容,那么使用自定义节点内容
if (this.isUseCustomNodeContent()) {
let foreignObject = new ForeignObject()
foreignObject.width(width)
foreignObject.height(height)
foreignObject.add(SVG(this._customNodeContent))
this.group.add(foreignObject)
return
}
// 图片节点
let imgHeight = 0
if (this._imgData) {
imgHeight = this._imgData.height
this.group.add(this._imgData.node)
this._imgData.node.cx(width / 2).y(paddingY)
}
// 内容节点
let textContentNested = new G()
let textContentOffsetX = 0
// icon
let iconNested = new G()
if (this._iconData && this._iconData.length > 0) {
let iconLeft = 0
this._iconData.forEach(item => {
item.node
.x(textContentOffsetX + iconLeft)
.y((this._rectInfo.textContentHeight - item.height) / 2)
iconNested.add(item.node)
iconLeft += item.width + textContentItemMargin
})
textContentNested.add(iconNested)
textContentOffsetX += iconLeft
}
// 文字
if (this._textData) {
this._textData.node.attr('data-offsetx', textContentOffsetX)
this._textData.node.x(textContentOffsetX).y(0)
textContentNested.add(this._textData.node)
textContentOffsetX += this._textData.width + textContentItemMargin
}
// 超链接
if (this._hyperlinkData) {
this._hyperlinkData.node
.x(textContentOffsetX)
.y((this._rectInfo.textContentHeight - this._hyperlinkData.height) / 2)
textContentNested.add(this._hyperlinkData.node)
textContentOffsetX += this._hyperlinkData.width + textContentItemMargin
}
// 标签
let tagNested = new G()
if (this._tagData && this._tagData.length > 0) {
let tagLeft = 0
this._tagData.forEach(item => {
item.node
.x(textContentOffsetX + tagLeft)
.y((this._rectInfo.textContentHeight - item.height) / 2)
tagNested.add(item.node)
tagLeft += item.width + textContentItemMargin
})
textContentNested.add(tagNested)
textContentOffsetX += tagLeft
}
// 备注
if (this._noteData) {
this._noteData.node
.x(textContentOffsetX)
.y((this._rectInfo.textContentHeight - this._noteData.height) / 2)
textContentNested.add(this._noteData.node)
textContentOffsetX += this._noteData.width
}
// 文字内容整体
textContentNested.translate(
width / 2 - textContentNested.bbox().width / 2,
imgHeight +
paddingY +
(imgHeight > 0 && this._rectInfo.textContentHeight > 0
? this.blockContentMargin
: 0)
)
this.group.add(textContentNested)
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnPlaceholderRect() {
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)
}
}
// 给节点绑定事件
bindGroupEvent() {
// 单击事件,选中节点
this.group.on('click', e => {
this.mindMap.emit('node_click', this, e)
if (this.isMultipleChoice) {
e.stopPropagation()
this.isMultipleChoice = false
return
}
this.active(e)
})
this.group.on('mousedown', e => {
if (this.isRoot && e.which === 3) {
e.stopPropagation()
}
if (!this.isRoot) {
e.stopPropagation()
}
// 多选和取消多选
if (e.ctrlKey && this.mindMap.opt.enableCtrlKeyNodeSelection) {
this.isMultipleChoice = true
let isActive = this.nodeData.data.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.emit('node_mousedown', this, e)
})
this.group.on('mouseup', e => {
if (!this.isRoot) {
e.stopPropagation()
}
this.mindMap.emit('node_mouseup', this, e)
})
this.group.on('mouseenter', e => {
this._isMouseenter = true
// 显示展开收起按钮
this.showExpandBtn()
this.mindMap.emit('node_mouseenter', this, e)
})
this.group.on('mouseleave', e => {
this._isMouseenter = false
this.hideExpandBtn()
this.mindMap.emit('node_mouseleave', this, e)
})
// 双击事件
this.group.on('dblclick', e => {
if (this.mindMap.opt.readonly) {
return
}
e.stopPropagation()
this.mindMap.emit('node_dblclick', this, e)
})
// 右键菜单事件
this.group.on('contextmenu', e => {
// 按住ctrl键点击鼠标左键不知为何触发的是contextmenu事件
if (this.mindMap.opt.readonly || e.ctrlKey) {
// || this.isGeneralization
return
}
e.stopPropagation()
e.preventDefault()
if (this.nodeData.data.isActive) {
this.renderer.clearActive()
}
this.active(e)
this.mindMap.emit('node_contextmenu', e, this)
})
}
// 激活节点
active(e) {
if (this.mindMap.opt.readonly) {
return
}
e && e.stopPropagation()
if (this.nodeData.data.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)
}
// 更新节点
update(isLayout = false) {
if (!this.group) {
return
}
let {
alwaysShowExpandBtn
} = this.mindMap.opt
if (alwaysShowExpandBtn) {
// 需要移除展开收缩按钮
if (this._expandBtn && this.nodeData.children.length <= 0) {
this.removeExpandBtn()
} else {
// 更新展开收起按钮
this.renderExpandBtn()
}
} else {
let { isActive, expand } = this.nodeData.data
// 展开状态且非激活状态,且当前鼠标不在它上面,才隐藏
if (expand && !isActive && !this._isMouseenter) {
this.hideExpandBtn()
} else {
this.showExpandBtn()
}
}
// 更新概要
this.renderGeneralization()
// 更新节点位置
let t = this.group.transform()
// 如果节点位置没有变化,则返回
if (this.left === t.translateX && this.top === t.translateY) return
this.group.translate(this.left - t.translateX, this.top - t.translateY)
}
// 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置
reRender() {
let sizeChange = this.getSize()
this.layout()
this.update()
return sizeChange
}
// 更新节点形状样式
updateNodeShape() {
if (!this.shapeNode) return
const shape = this.getShape()
this.style[shape === CONSTANTS.SHAPE.RECTANGLE ? 'rect' : 'shape'](
this.shapeNode
)
}
// 递归渲染
render(callback = () => {}) {
// 节点
// 重新渲染连线
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.layout()
this.update(isLayout)
} else {
this.draw.add(this.group)
if (this.needLayout) {
this.needLayout = false
this.layout()
}
if (this.needRerenderExpandBtnPlaceholderRect) {
this.needRerenderExpandBtnPlaceholderRect = false
this.renderExpandBtnPlaceholderRect()
}
this.update()
}
// 子节点
if (
this.children &&
this.children.length &&
this.nodeData.data.expand !== false
) {
let index = 0
asyncRun(
this.children.map(item => {
return () => {
item.render(() => {
index++
if (index >= this.children.length) {
callback()
}
})
}
})
)
} else {
callback()
}
// 手动插入的节点立即获得焦点并且开启编辑模式
if (this.nodeData.inserting) {
delete this.nodeData.inserting
this.active()
setTimeout(() => {
this.mindMap.emit('node_dblclick', this, null, true)
}, 0)
}
}
// 递归删除,只是从画布删除,节点容器还在,后续还可以重新插回画布
remove() {
if (!this.group) return
this.group.remove()
this.removeGeneralization()
this.removeLine()
// 子节点
if (this.children && this.children.length) {
asyncRun(
this.children.map(item => {
return () => {
item.remove()
}
})
)
}
}
// 销毁节点,不但会从画布删除,而且原节点直接置空,后续无法再插回画布
destroy() {
if (!this.group) return
this.group.remove()
this.removeGeneralization()
this.removeLine()
this.group = null
}
// 隐藏节点
hide() {
this.group.hide()
this.hideGeneralization()
if (this.parent) {
let index = this.parent.children.indexOf(this)
this.parent._lines[index] && this.parent._lines[index].hide()
this._lines.forEach(item => {
item.hide()
})
}
// 子节点
if (this.children && this.children.length) {
asyncRun(
this.children.map(item => {
return () => {
item.hide()
}
})
)
}
}
// 显示节点
show() {
if (!this.group) {
return
}
this.group.show()
this.showGeneralization()
if (this.parent) {
let index = this.parent.children.indexOf(this)
this.parent._lines[index] && this.parent._lines[index].show()
this._lines.forEach(item => {
item.show()
})
}
// 子节点
if (this.children && this.children.length) {
asyncRun(
this.children.map(item => {
return () => {
item.show()
}
})
)
}
}
// 连线
renderLine(deep = false) {
if (this.nodeData.data.expand === false) {
return
}
let childrenLen = this.nodeData.children.length
// 切换为鱼骨结构时,清空根节点和二级节点的连线
if (
this.mindMap.opt.layout === CONSTANTS.LAYOUT.FISHBONE &&
(this.isRoot || this.layerIndex === 1)
) {
childrenLen = 0
}
if (childrenLen > this._lines.length) {
// 创建缺少的线
new Array(childrenLen - this._lines.length).fill(0).forEach(() => {
this._lines.push(this.draw.path())
})
} else if (childrenLen < this._lines.length) {
// 删除多余的线
this._lines.slice(childrenLen).forEach(line => {
line.remove()
})
this._lines = this._lines.slice(0, childrenLen)
}
// 画线
this.renderer.layout.renderLine(
this,
this._lines,
(line, node) => {
// 添加样式
this.styleLine(line, node)
},
this.style.getStyle('lineStyle', true)
)
// 子级的连线也需要更新
if (deep && this.children && this.children.length > 0) {
this.children.forEach(item => {
item.renderLine(deep)
})
}
}
// 获取节点形状
getShape() {
// 节点使用功能横线风格的话不支持设置形状,直接使用默认的矩形
return this.mindMap.themeConfig.nodeUseLineStyle
? CONSTANTS.SHAPE.RECTANGLE
: this.style.getStyle('shape', false, false)
}
// 检查节点是否存在自定义数据
hasCustomPosition() {
return this.customLeft !== undefined && this.customTop !== undefined
}
// 检查节点是否存在自定义位置的祖先节点
ancestorHasCustomPosition() {
let node = this
while (node) {
if (node.hasCustomPosition()) {
return true
}
node = node.parent
}
return false
}
// 添加子节点
addChildren(node) {
this.children.push(node)
}
// 设置连线样式
styleLine(line, node) {
let width =
node.getSelfInhertStyle('lineWidth') || node.getStyle('lineWidth', true)
let color =
node.getSelfInhertStyle('lineColor') || node.getStyle('lineColor', true)
let dasharray =
node.getSelfInhertStyle('lineDasharray') ||
node.getStyle('lineDasharray', true)
this.style.line(line, {
width,
color,
dasharray
})
}
// 移除连线
removeLine() {
this._lines.forEach(line => {
line.remove()
})
this._lines = []
}
// 检测当前节点是否是某个节点的祖先节点
isParent(node) {
if (this === node) {
return false
}
let parent = node.parent
while (parent) {
if (this === parent) {
return true
}
parent = parent.parent
}
return false
}
// 检测当前节点是否是某个节点的兄弟节点
isBrother(node) {
if (!this.parent || this === node) {
return false
}
return this.parent.children.find(item => {
return item === node
})
}
// 获取padding值
getPaddingVale() {
let { isActive } = this.nodeData.data
return {
paddingX: this.getStyle('paddingX', true, isActive),
paddingY: this.getStyle('paddingY', true, isActive)
}
}
// 获取某个样式
getStyle(prop, root, isActive) {
let v = this.style.merge(prop, root, isActive)
return v === undefined ? '' : v
}
// 获取自定义样式
getSelfStyle(prop) {
return this.style.getSelfStyle(prop)
}
// 获取最近一个存在自身自定义样式的祖先节点的自定义样式
getParentSelfStyle(prop) {
if (this.parent) {
return (
this.parent.getSelfStyle(prop) || this.parent.getParentSelfStyle(prop)
)
}
return null
}
// 获取自身可继承的自定义样式
getSelfInhertStyle(prop) {
return (
this.getSelfStyle(prop) || // 自身
this.getParentSelfStyle(prop)
) // 父级
}
// 获取数据
getData(key) {
return key ? this.nodeData.data[key] || '' : this.nodeData.data
}
// 是否存在自定义样式
hasCustomStyle() {
return this.style.hasCustomStyle()
}
}
export default Node

View File

@@ -1,3 +1,6 @@
import { Rect, Polygon, Path } from '@svgdotjs/svg.js'
import { CONSTANTS } from '../../../constants/constant'
// 节点形状类
export default class Shape {
constructor(node) {
@@ -13,37 +16,37 @@ export default class Shape {
const actHeight = height + paddingY * 2
const actOffset = Math.abs(actWidth - actHeight)
switch (shape) {
case 'roundedRectangle':
case CONSTANTS.SHAPE.ROUNDED_RECTANGLE:
return {
paddingX: height > width ? (height - width) / 2 : 0,
paddingY: 0
}
case 'diamond':
case CONSTANTS.SHAPE.DIAMOND:
return {
paddingX: width / 2,
paddingY: height / 2
}
case 'parallelogram':
case CONSTANTS.SHAPE.PARALLELOGRAM:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'outerTriangularRectangle':
case CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'innerTriangularRectangle':
case CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'ellipse':
case CONSTANTS.SHAPE.ELLIPSE:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: paddingY <= 0 ? defaultPaddingY : 0
}
case 'circle':
case CONSTANTS.SHAPE.CIRCLE:
return {
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
paddingY: actHeight < actWidth ? actOffset / 2 : 0
@@ -62,30 +65,30 @@ export default class Shape {
let { width, height } = this.node
let node = null
// 矩形
if (shape === 'rectangle') {
node = this.node.group.rect(width, height)
} else if (shape === 'diamond') {
if (shape === CONSTANTS.SHAPE.RECTANGLE) {
node = new Rect().size(width, height)
} else if (shape === CONSTANTS.SHAPE.DIAMOND) {
// 菱形
node = this.createDiamond()
} else if (shape === 'parallelogram') {
} else if (shape === CONSTANTS.SHAPE.PARALLELOGRAM) {
// 平行四边形
node = this.createParallelogram()
} else if (shape === 'roundedRectangle') {
} else if (shape === CONSTANTS.SHAPE.ROUNDED_RECTANGLE) {
// 圆角矩形
node = this.createRoundedRectangle()
} else if (shape === 'octagonalRectangle') {
} else if (shape === CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE) {
// 八角矩形
node = this.createOctagonalRectangle()
} else if (shape === 'outerTriangularRectangle') {
} else if (shape === CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE) {
// 外三角矩形
node = this.createOuterTriangularRectangle()
} else if (shape === 'innerTriangularRectangle') {
} else if (shape === CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE) {
// 内三角矩形
node = this.createInnerTriangularRectangle()
} else if (shape === 'ellipse') {
} else if (shape === CONSTANTS.SHAPE.ELLIPSE) {
// 椭圆
node = this.createEllipse()
} else if (shape === 'circle') {
} else if (shape === CONSTANTS.SHAPE.CIRCLE) {
// 圆
node = this.createCircle()
}
@@ -105,12 +108,12 @@ export default class Shape {
let bottomY = height
let leftX = 0
let leftY = halfHeight
return this.node.group.polygon(`
${topX}, ${topY}
${rightX}, ${rightY}
${bottomX}, ${bottomY}
${leftX}, ${leftY}
`)
return new Polygon().plot([
[topX, topY],
[rightX, rightY],
[bottomX, bottomY],
[leftX, leftY]
])
}
// 创建平行四边形
@@ -118,41 +121,41 @@ export default class Shape {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${paddingX}, ${0}
${width}, ${0}
${width - paddingX}, ${height}
${0}, ${height}
`)
return new Polygon().plot([
[paddingX, 0],
[width, 0],
[width - paddingX, height],
[0, height]
])
}
// 创建圆角矩形
createRoundedRectangle() {
let { width, height } = this.node
let halfHeight = height / 2
return this.node.group.path(`
M${halfHeight},0
L${width - halfHeight},0
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
L${halfHeight},${height}
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
`)
return new Path().plot(`
M${halfHeight},0
L${width - halfHeight},0
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
L${halfHeight},${height}
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
`)
}
// 创建八角矩形
createOctagonalRectangle() {
let w = 5
let { width, height } = this.node
return this.node.group.polygon(`
${0}, ${w}
${w}, ${0}
${width - w}, ${0}
${width}, ${w}
${width}, ${height - w}
${width - w}, ${height}
${w}, ${height}
${0}, ${height - w}
`)
return new Polygon().plot([
[0, w],
[w, 0],
[width - w, 0],
[width, w],
[width, height - w],
[width - w, height],
[w, height],
[0, height - w]
])
}
// 创建外三角矩形
@@ -160,14 +163,14 @@ export default class Shape {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${paddingX}, ${0}
${width - paddingX}, ${0}
${width}, ${height / 2}
${width - paddingX}, ${height}
${paddingX}, ${height}
${0}, ${height / 2}
`)
return new Polygon().plot([
[paddingX, 0],
[width - paddingX, 0],
[width, height / 2],
[width - paddingX, height],
[paddingX, height],
[0, height / 2]
])
}
// 创建内三角矩形
@@ -175,14 +178,14 @@ export default class Shape {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${0}, ${0}
${width}, ${0}
${width - paddingX / 2}, ${height / 2}
${width}, ${height}
${0}, ${height}
${paddingX / 2}, ${height / 2}
`)
return new Polygon().plot([
[0, 0],
[width, 0],
[width - paddingX / 2, height / 2],
[width, height],
[0, height],
[paddingX / 2, height / 2]
])
}
// 创建椭圆
@@ -190,12 +193,12 @@ export default class Shape {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return this.node.group.path(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
return new Path().plot(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
// 创建圆
@@ -203,24 +206,24 @@ export default class Shape {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return this.node.group.path(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
return new Path().plot(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
}
// 形状列表
export const shapeList = [
'rectangle',
'diamond',
'parallelogram',
'roundedRectangle',
'octagonalRectangle',
'outerTriangularRectangle',
'innerTriangularRectangle',
'ellipse',
'circle'
CONSTANTS.SHAPE.RECTANGLE,
CONSTANTS.SHAPE.DIAMOND,
CONSTANTS.SHAPE.PARALLELOGRAM,
CONSTANTS.SHAPE.ROUNDED_RECTANGLE,
CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE,
CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE,
CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE,
CONSTANTS.SHAPE.ELLIPSE,
CONSTANTS.SHAPE.CIRCLE
]

View File

@@ -0,0 +1,227 @@
import { tagColorList, nodeDataNoStylePropList } from '../../../constants/constant'
const rootProp = ['paddingX', 'paddingY']
const backgroundStyleProps = ['backgroundColor', 'backgroundImage', 'backgroundRepeat', 'backgroundPosition', 'backgroundSize']
// 样式类
class Style {
// 设置背景样式
static setBackgroundStyle(el, themeConfig) {
// 缓存容器元素原本的样式
if (!Style.cacheStyle) {
Style.cacheStyle = {}
let style = window.getComputedStyle(el)
backgroundStyleProps.forEach((prop) => {
Style.cacheStyle[prop] = style[prop]
})
}
// 设置新样式
let { backgroundColor, backgroundImage, backgroundRepeat, backgroundPosition, backgroundSize } = themeConfig
el.style.backgroundColor = backgroundColor
if (backgroundImage) {
el.style.backgroundImage = `url(${backgroundImage})`
el.style.backgroundRepeat = backgroundRepeat
el.style.backgroundPosition = backgroundPosition
el.style.backgroundSize = backgroundSize
} else {
el.style.backgroundImage = 'none'
}
}
// 移除背景样式
static removeBackgroundStyle(el) {
if (!Style.cacheStyle) return
backgroundStyleProps.forEach((prop) => {
el.style[prop] = Style.cacheStyle[prop]
})
Style.cacheStyle = null
}
// 构造函数
constructor(ctx) {
this.ctx = ctx
}
// 合并样式
merge(prop, root, isActive) {
let themeConfig = this.ctx.mindMap.themeConfig
// 三级及以下节点
let defaultConfig = themeConfig.node
if (root || rootProp.includes(prop)) {
// 直接使用最外层样式
defaultConfig = themeConfig
} else if (this.ctx.isGeneralization) {
// 概要节点
defaultConfig = themeConfig.generalization
} else if (this.ctx.layerIndex === 0) {
// 根节点
defaultConfig = themeConfig.root
} else if (this.ctx.layerIndex === 1) {
// 二级节点
defaultConfig = themeConfig.second
}
// 激活状态
if (isActive !== undefined ? isActive : this.ctx.nodeData.data.isActive) {
if (
this.ctx.nodeData.data.activeStyle &&
this.ctx.nodeData.data.activeStyle[prop] !== undefined
) {
return this.ctx.nodeData.data.activeStyle[prop]
} else if (defaultConfig.active && defaultConfig.active[prop]) {
return defaultConfig.active[prop]
}
}
// 优先使用节点本身的样式
return this.getSelfStyle(prop) !== undefined
? this.getSelfStyle(prop)
: defaultConfig[prop]
}
// 获取某个样式值
getStyle(prop, root, isActive) {
return this.merge(prop, root, isActive)
}
// 获取自身自定义样式
getSelfStyle(prop) {
return this.ctx.nodeData.data[prop]
}
// 矩形
rect(node) {
this.shape(node)
node.radius(this.merge('borderRadius'))
}
// 矩形外的其他形状
shape(node) {
node.fill({
color: this.merge('fillColor')
})
// 节点使用横线样式,不需要渲染非激活状态的边框样式
// if (
// !this.ctx.isRoot &&
// !this.ctx.isGeneralization &&
// this.ctx.mindMap.themeConfig.nodeUseLineStyle &&
// !this.ctx.nodeData.data.isActive
// ) {
// return
// }
node.stroke({
color: this.merge('borderColor'),
width: this.merge('borderWidth'),
dasharray: this.merge('borderDasharray')
})
}
// 文字
text(node) {
node
.fill({
color: this.merge('color')
})
.css({
'font-family': this.merge('fontFamily'),
'font-size': this.merge('fontSize'),
'font-weight': this.merge('fontWeight'),
'font-style': this.merge('fontStyle'),
'text-decoration': this.merge('textDecoration')
})
}
// 生成内联样式
createStyleText() {
return `
color: ${this.merge('color')};
font-family: ${this.merge('fontFamily')};
font-size: ${this.merge('fontSize') + 'px'};
font-weight: ${this.merge('fontWeight')};
font-style: ${this.merge('fontStyle')};
text-decoration: ${this.merge('textDecoration')}
`
}
// 获取文本样式
getTextFontStyle() {
return {
italic: this.merge('fontStyle') === 'italic',
bold: this.merge('fontWeight'),
fontSize: this.merge('fontSize'),
fontFamily: this.merge('fontFamily')
}
}
// html文字节点
domText(node, fontSizeScale = 1, isMultiLine) {
node.style.fontFamily = this.merge('fontFamily')
node.style.fontSize = this.merge('fontSize') * fontSizeScale + 'px'
node.style.fontWeight = this.merge('fontWeight') || 'normal'
node.style.lineHeight = !isMultiLine ? 'normal' : this.merge('lineHeight')
node.style.fontStyle = this.merge('fontStyle')
}
// 标签文字
tagText(node, index) {
node
.fill({
color: tagColorList[index].color
})
.css({
'font-size': '12px'
})
}
// 标签矩形
tagRect(node, index) {
node.fill({
color: tagColorList[index].background
})
}
// 内置图标
iconNode(node) {
node.attr({
fill: this.merge('color')
})
}
// 连线
line(node, { width, color, dasharray } = {}) {
node.stroke({ width, color, dasharray }).fill({ color: 'none' })
}
// 概要连线
generalizationLine(node) {
node
.stroke({
width: this.merge('generalizationLineWidth', true),
color: this.merge('generalizationLineColor', true)
})
.fill({ color: 'none' })
}
// 展开收起按钮
iconBtn(node, node2, fillNode) {
let { color, fill } = this.ctx.mindMap.opt.expandBtnStyle || {
color: '#808080',
fill: '#fff'
}
node.fill({ color: color })
node2.fill({ color: color })
fillNode.fill({ color: fill })
}
// 是否设置了自定义的样式
hasCustomStyle() {
let res = false
Object.keys(this.ctx.nodeData.data).forEach((item) => {
if (!nodeDataNoStylePropList.includes(item)) {
res = true
}
})
return res
}
}
Style.cacheStyle = null
export default Style

View File

@@ -0,0 +1,56 @@
// 设置数据
function setData(data = {}) {
this.mindMap.execCommand('SET_NODE_DATA', this, data)
}
// 设置文本
function setText(text, richText, resetRichText) {
this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText, resetRichText)
}
// 设置图片
function setImage(imgData) {
this.mindMap.execCommand('SET_NODE_IMAGE', this, imgData)
}
// 设置图标
function setIcon(icons) {
this.mindMap.execCommand('SET_NODE_ICON', this, icons)
}
// 设置超链接
function setHyperlink(link, title) {
this.mindMap.execCommand('SET_NODE_HYPERLINK', this, link, title)
}
// 设置备注
function setNote(note) {
this.mindMap.execCommand('SET_NODE_NOTE', this, note)
}
// 设置标签
function setTag(tag) {
this.mindMap.execCommand('SET_NODE_TAG', this, tag)
}
// 设置形状
function setShape(shape) {
this.mindMap.execCommand('SET_NODE_SHAPE', this, shape)
}
// 修改某个样式
function setStyle(prop, value, isActive) {
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value, isActive)
}
export default {
setData,
setText,
setImage,
setIcon,
setHyperlink,
setNote,
setTag,
setShape,
setStyle
}

View File

@@ -0,0 +1,347 @@
import { measureText, resizeImgSize, removeHtmlStyle, addHtmlStyle, checkIsRichText } from '../../../utils'
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
import iconsSvg from '../../../svg/icons'
import { CONSTANTS, commonCaches } from '../../../constants/constant'
// 创建图片节点
function createImgNode() {
let img = this.nodeData.data.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)
}
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],
height: imgSize[1]
}
}
// 获取图片显示宽高
function getImgShowSize() {
const { custom, width, height } = this.nodeData.data.imageSize
// 如果是自定义了图片的宽高,那么不受最大宽高限制
if (custom) return [width, height]
return resizeImgSize(
width,
height,
this.mindMap.themeConfig.imgMaxWidth,
this.mindMap.themeConfig.imgMaxHeight
)
}
// 创建icon节点
function createIconNode() {
let _data = this.nodeData.data
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 node = null
// svg图标
if (/^<svg/.test(src)) {
node = SVG(src)
} else {
// 图片图标
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,
height: iconSize
}
})
}
// 创建富文本节点
function createRichTextNode() {
let g = new G()
// 重新设置富文本节点内容
let recoverText = false
if (this.nodeData.data.resetRichText) {
delete this.nodeData.data.resetRichText
recoverText = true
}
if ([CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
// 如果自定义过样式则不允许覆盖
if (!this.hasCustomStyle()) {
recoverText = true
}
}
if (recoverText) {
let text = this.nodeData.data.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.nodeData.data.text = text
}
let html = `<div>${this.nodeData.data.text}</div>`
let div = document.createElement('div')
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)
let { width, height } = el.getBoundingClientRect()
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))
g.add(foreignObject)
return {
node: g,
width,
height
}
}
// 创建文本节点
function createTextNode() {
if (this.nodeData.data.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 textStyle = this.style.getTextFontStyle()
let textArr = this.nodeData.data.text.split(/\n/gim)
let maxWidth = this.mindMap.opt.textAutoWrapWidth
let isMultiLine = false
textArr.forEach((item, index) => {
let arr = item.split('')
let lines = []
let line = []
while (arr.length) {
let str = arr.shift()
let text = [...line, str].join('')
if (measureText(text, textStyle).width <= maxWidth) {
line.push(str)
} else {
lines.push(line.join(''))
line = [str]
}
}
if (line.length > 0) {
lines.push(line.join(''))
}
if (lines.length > 1) {
isMultiLine = true
}
textArr[index] = lines.join('\n')
})
textArr = textArr.join('\n').split(/\n/gim)
textArr.forEach((item, index) => {
let node = new Text().text(item)
this.style.text(node)
node.y(fontSize * lineHeight * index)
g.add(node)
})
let { width, height } = g.bbox()
width = Math.ceil(width)
height = Math.ceil(height)
g.attr('data-width', width)
g.attr('data-height', height)
g.attr('data-ismultiLine', isMultiLine || textArr.length > 1)
return {
node: g,
width,
height
}
}
// 创建超链接节点
function createHyperlinkNode() {
let { hyperlink, hyperlinkTitle } = this.nodeData.data
if (!hyperlink) {
return
}
let iconSize = this.mindMap.themeConfig.iconSize
let node = new SVG()
// 超链接节点
let a = new A().to(hyperlink).target('_blank')
a.node.addEventListener('click', e => {
e.stopPropagation()
})
if (hyperlinkTitle) {
a.attr('title', hyperlinkTitle)
}
// 添加一个透明的层,作为鼠标区域
a.rect(iconSize, iconSize).fill({ color: 'transparent' })
// 超链接图标
let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize)
this.style.iconNode(iconNode)
a.add(iconNode)
node.add(a)
return {
node,
width: iconSize,
height: iconSize
}
}
// 创建标签节点
function createTagNode() {
let tagData = this.nodeData.data.tag
if (!tagData || tagData.length <= 0) {
return []
}
let nodes = []
tagData.slice(0, this.mindMap.opt.maxTag).forEach((item, index) => {
let tag = new G()
// 标签文本
let text = new Text().text(item).x(8).cy(10)
this.style.tagText(text, index)
let { width } = text.bbox()
// 标签矩形
let rect = new Rect().size(width + 16, 20)
this.style.tagRect(rect, index)
tag.add(rect).add(text)
nodes.push({
node: tag,
width: width + 16,
height: 20
})
})
return nodes
}
// 创建备注节点
function createNoteNode() {
if (!this.nodeData.data.note) {
return null
}
let iconSize = this.mindMap.themeConfig.iconSize
let node = new SVG().attr('cursor', 'pointer')
// 透明的层,用来作为鼠标区域
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
// 备注图标
let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize)
this.style.iconNode(iconNode)
node.add(iconNode)
// 备注tooltip
if (!this.mindMap.opt.customNoteContentShow) {
if (!this.noteEl) {
this.noteEl = document.createElement('div')
this.noteEl.style.cssText = `
position: absolute;
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 }
`
document.body.appendChild(this.noteEl)
}
this.noteEl.innerText = this.nodeData.data.note
}
node.on('mouseover', () => {
let { left, top } = node.node.getBoundingClientRect()
if (!this.mindMap.opt.customNoteContentShow) {
this.noteEl.style.left = left + 'px'
this.noteEl.style.top = top + iconSize + 'px'
this.noteEl.style.display = 'block'
} else {
this.mindMap.opt.customNoteContentShow.show(
this.nodeData.data.note,
left,
top + iconSize
)
}
})
node.on('mouseout', () => {
if (!this.mindMap.opt.customNoteContentShow) {
this.noteEl.style.display = 'none'
} else {
this.mindMap.opt.customNoteContentShow.hide()
}
})
return {
node,
width: iconSize,
height: iconSize
}
}
// 测量自定义节点内容元素的宽高
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,
measureCustomNodeContentSize,
isUseCustomNodeContent
}

View File

@@ -0,0 +1,142 @@
import btnsSvg from '../../../svg/btns'
import { SVG, Circle, G } from '@svgdotjs/svg.js'
// 创建展开收起按钮的内容节点
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)
// 收起的节点
this._closeExpandNode = SVG(close || btnsSvg.close).size(
this.expandBtnSize,
this.expandBtnSize
)
this._closeExpandNode.x(0).y(-this.expandBtnSize / 2)
// 填充节点
this._fillExpandNode = new Circle().size(this.expandBtnSize)
this._fillExpandNode.x(0).y(-this.expandBtnSize / 2)
// 设置样式
this.style.iconBtn(
this._openExpandNode,
this._closeExpandNode,
this._fillExpandNode
)
}
// 创建或更新展开收缩按钮内容
function updateExpandBtnNode() {
let { expand } = this.nodeData.data
// 如果本次和上次的展开状态一样则返回
if (expand === this._lastExpandBtnType) return
if (this._expandBtn) {
this._expandBtn.clear()
}
this.createExpandNodeContent()
let node
if (expand === false) {
node = this._openExpandNode
this._lastExpandBtnType = false
} else {
node = this._closeExpandNode
this._lastExpandBtnType = true
}
if (this._expandBtn) this._expandBtn.add(this._fillExpandNode).add(node)
}
// 更新展开收缩按钮位置
function updateExpandBtnPos() {
if (!this._expandBtn) {
return
}
this.renderer.layout.renderExpandBtn(this, this._expandBtn)
}
// 创建展开收缩按钮
function renderExpandBtn() {
if (
!this.nodeData.children ||
this.nodeData.children.length <= 0 ||
this.isRoot
) {
return
}
if (this._expandBtn) {
this.group.add(this._expandBtn)
} else {
this._expandBtn = new G()
this._expandBtn.on('mouseover', e => {
e.stopPropagation()
this._expandBtn.css({
cursor: 'pointer'
})
})
this._expandBtn.on('mouseout', e => {
e.stopPropagation()
this._expandBtn.css({
cursor: 'auto'
})
})
this._expandBtn.on('click', e => {
e.stopPropagation()
// 展开收缩
this.mindMap.execCommand(
'SET_NODE_EXPAND',
this,
!this.nodeData.data.expand
)
this.mindMap.emit('expand_btn_click', this)
})
this._expandBtn.on('dblclick', e => {
e.stopPropagation()
})
this.group.add(this._expandBtn)
}
this._showExpandBtn = true
this.updateExpandBtnNode()
this.updateExpandBtnPos()
}
// 移除展开收缩按钮
function removeExpandBtn() {
if (this._expandBtn && this._showExpandBtn) {
this._expandBtn.remove()
this._showExpandBtn = false
}
}
// 显示展开收起按钮
function showExpandBtn() {
if (this.mindMap.opt.alwaysShowExpandBtn) return
setTimeout(() => {
this.renderExpandBtn()
}, 0)
}
// 隐藏展开收起按钮
function hideExpandBtn() {
if (this.mindMap.opt.alwaysShowExpandBtn || this._isMouseenter) return
// 非激活状态且展开状态鼠标移出才隐藏按钮
let { isActive, expand } = this.nodeData.data
if (!isActive && expand) {
setTimeout(() => {
this.removeExpandBtn()
}, 0)
}
}
export default {
createExpandNodeContent,
updateExpandBtnNode,
updateExpandBtnPos,
renderExpandBtn,
removeExpandBtn,
showExpandBtn,
hideExpandBtn
}

View File

@@ -0,0 +1,118 @@
import Node from './Node'
import { createUid } from '../../../utils/index'
// 检查是否存在概要
function checkHasGeneralization () {
return !!this.nodeData.data.generalization
}
// 创建概要节点
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: createUid(),
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)
}
}
}
// 更新概要节点
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) {
this.removeGeneralization()
return
}
this.createGeneralizationNode()
this.renderer.layout.renderGeneralization(
this,
this._generalizationLine,
this._generalizationNode
)
this.style.generalizationLine(this._generalizationLine)
this._generalizationNode.render()
}
// 删除概要节点
function removeGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.remove()
this._generalizationLine = null
}
if (this._generalizationNode) {
// 删除概要节点时要同步从激活节点里删除
this.renderer.removeActiveNode(this._generalizationNode)
this._generalizationNode.remove()
this._generalizationNode = null
}
// hack修复当激活一个节点时创建概要然后立即激活创建的概要节点后会重复创建概要节点并且无法删除的问题
if (this.generalizationBelongNode) {
this.draw
.find('.generalization_' + this.generalizationBelongNode.uid)
.remove()
}
}
// 隐藏概要节点
function hideGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.hide()
}
if (this._generalizationNode) {
this._generalizationNode.hide()
}
}
// 显示概要节点
function showGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.show()
}
if (this._generalizationNode) {
this._generalizationNode.show()
}
}
export default {
checkHasGeneralization,
createGeneralizationNode,
updateGeneralization,
renderGeneralization,
removeGeneralization,
hideGeneralization,
showGeneralization
}

View File

@@ -0,0 +1,296 @@
import { CONSTANTS } from '../../constants/constant'
// 视图操作类
class View {
// 构造函数
constructor(opt = {}) {
this.opt = opt
this.mindMap = this.opt.mindMap
this.scale = 1
this.sx = 0
this.sy = 0
this.x = 0
this.y = 0
this.firstDrag = true
this.setTransformData(this.mindMap.opt.viewData)
this.bind()
}
// 绑定
bind() {
// 快捷键
this.mindMap.keyCommand.addShortcut('Control+=', () => {
this.enlarge()
})
this.mindMap.keyCommand.addShortcut('Control+-', () => {
this.narrow()
})
this.mindMap.keyCommand.addShortcut('Control+Enter', () => {
this.reset()
})
this.mindMap.keyCommand.addShortcut('Control+i', () => {
this.fit()
})
this.mindMap.svg.on('dblclick', () => {
this.reset()
})
// 拖动视图
this.mindMap.event.on('mousedown', () => {
this.sx = this.x
this.sy = this.y
})
this.mindMap.event.on('drag', (e, event) => {
if (e.ctrlKey) {
// 按住ctrl键拖动为多选
return
}
if (this.firstDrag) {
this.firstDrag = false
// 清除激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
}
this.x = this.sx + event.mousemoveOffset.x
this.y = this.sy + event.mousemoveOffset.y
this.transform()
})
this.mindMap.event.on('mouseup', () => {
this.firstDrag = true
})
// 放大缩小视图
this.mindMap.event.on('mousewheel', (e, dir, event, isTouchPad) => {
let {
customHandleMousewheel,
mousewheelAction,
mouseScaleCenterUseMousePosition,
mousewheelMoveStep,
mousewheelZoomActionReverse
} = this.mindMap.opt
// 是否自定义鼠标滚轮事件
if (
customHandleMousewheel &&
typeof customHandleMousewheel === 'function'
) {
return customHandleMousewheel(e)
}
// 鼠标滚轮事件控制缩放
if (mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
let cx = mouseScaleCenterUseMousePosition ? e.clientX : undefined
let cy = mouseScaleCenterUseMousePosition ? e.clientY : undefined
switch (dir) {
// 鼠标滚轮,向上和向左,都是缩小
case CONSTANTS.DIR.UP:
case CONSTANTS.DIR.LEFT:
mousewheelZoomActionReverse ? this.enlarge(cx, cy, isTouchPad) : this.narrow(cx, cy, isTouchPad)
break
// 鼠标滚轮,向下和向右,都是放大
case CONSTANTS.DIR.DOWN:
case CONSTANTS.DIR.RIGHT:
mousewheelZoomActionReverse ? this.narrow(cx, cy, isTouchPad) : this.enlarge(cx, cy, isTouchPad)
break
}
} else {// 鼠标滚轮事件控制画布移动
let step = mousewheelMoveStep
if (isTouchPad) {
step = 5
}
switch (dir) {
// 上移
case CONSTANTS.DIR.DOWN:
this.translateY(-step)
break
// 下移
case CONSTANTS.DIR.UP:
this.translateY(step)
break
// 右移
case CONSTANTS.DIR.LEFT:
this.translateX(-step)
break
// 左移
case CONSTANTS.DIR.RIGHT:
this.translateX(step)
break
}
}
})
}
// 获取当前变换状态数据
getTransformData() {
return {
transform: this.mindMap.draw.transform(),
state: {
scale: this.scale,
x: this.x,
y: this.y,
sx: this.sx,
sy: this.sy
}
}
}
// 动态设置变换状态数据
setTransformData(viewData) {
if (viewData) {
Object.keys(viewData.state).forEach(prop => {
this[prop] = viewData.state[prop]
})
this.mindMap.draw.transform({
...viewData.transform
})
this.mindMap.emit('view_data_change', this.getTransformData())
this.mindMap.emit('scale', this.scale)
}
}
// 平移x,y方向
translateXY(x, y) {
this.x += x
this.y += y
this.transform()
}
// 平移x方向
translateX(step) {
this.x += step
this.transform()
}
// 平移x方式到
translateXTo(x) {
this.x = x
this.transform()
}
// 平移y方向
translateY(step) {
this.y += step
this.transform()
}
// 平移y方向到
translateYTo(y) {
this.y = y
this.transform()
}
// 应用变换
transform() {
this.mindMap.draw.transform({
origin: [0, 0],
scale: this.scale,
translate: [this.x, this.y]
})
this.mindMap.emit('view_data_change', this.getTransformData())
}
// 恢复
reset() {
let scaleChange = this.scale !== 1
this.scale = 1
this.x = 0
this.y = 0
this.transform()
if (scaleChange) {
this.mindMap.emit('scale', this.scale)
}
}
// 缩小
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(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)
}
// 基于指定中心进行缩放cxcy 可不指定,此时会使用画布中心点
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)
}
// 适应画布大小
fit() {
let { fitPadding } = this.mindMap.opt
let draw = this.mindMap.draw
let origTransform = draw.transform()
let rect = draw.rbox()
let drawWidth = rect.width / origTransform.scaleX
let drawHeight = rect.height / origTransform.scaleY
let drawRatio = drawWidth / drawHeight
let { width: elWidth, height: elHeight } =
this.mindMap.el.getBoundingClientRect()
elWidth = elWidth - fitPadding * 2
elHeight = elHeight - fitPadding * 2
let elRatio = elWidth / elHeight
let newScale = 0
let flag = ''
if (drawWidth <= elWidth && drawHeight <= elHeight) {
newScale = 1
flag = 1
} else {
let newWidth = 0
let newHeight = 0
if (drawRatio > elRatio) {
newWidth = elWidth
newHeight = elWidth / drawRatio
flag = 2
} else {
newHeight = elHeight
newWidth = elHeight * drawRatio
flag = 3
}
newScale = newWidth / drawWidth
}
this.setScale(newScale)
let newRect = draw.rbox()
let newX = 0
let newY = 0
if (flag === 1) {
newX = -newRect.x + fitPadding + (elWidth - newRect.width) / 2
newY = -newRect.y + fitPadding + (elHeight - newRect.height) / 2
} else if (flag === 2) {
newX = -newRect.x + fitPadding
newY = -newRect.y + fitPadding + (elHeight - newRect.height) / 2
} else if (flag === 3) {
newX = -newRect.x + fitPadding + (elWidth - newRect.width) / 2
newY = -newRect.y + fitPadding
}
this.translateXY(newX, newY)
}
}
export default View

View File

@@ -1,4 +1,7 @@
import Node from '../Node'
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 +15,7 @@ class Base {
this.draw = this.mindMap.draw
// 根节点
this.root = null
this.lru = new Lru(this.mindMap.opt.maxNodeCacheCount)
}
// 计算节点位置
@@ -32,26 +36,84 @@ class Base {
// 概要节点
renderGeneralization() {}
// 通过uid缓存节点
cacheNode(uid, node) {
// 记录本次渲染时的节点
this.renderer.nodeCache[uid] = node
// 记录所有渲染时的节点
this.lru.add(uid, node)
}
// 检查当前来源是否需要重新计算节点大小
checkIsNeedResizeSources() {
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
}
}
// 创建节点实例
createNode(data, parent, isRoot, layerIndex) {
// 创建节点
let newNode = null
// 复用节点
// 数据上保存了节点引用,那么直接复用节点
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() || isLayerTypeChange) {
newNode.getSize()
newNode.needLayout = true
}
} else if (this.lru.has(data.data.uid) && !this.renderer.reRender) {
// 数据上没有保存节点引用但是通过uid找到了缓存的节点也可以复用
newNode = this.lru.get(data.data.uid)
// 保存该节点上一次的数据
let lastData = JSON.stringify(newNode.nodeData.data)
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 || isLayerTypeChange) {
newNode.getSize()
newNode.needLayout = true
}
} else {
// 创建新节点
let uid = data.data.uid || createUid()
newNode = new Node({
data,
uid: this.mindMap.uid++,
uid,
renderer: this.renderer,
mindMap: this.mindMap,
draw: this.draw,
layerIndex
})
newNode.getSize()
// uid保存到数据上为了节点复用
data.data.uid = uid
this.cacheNode(uid, newNode)
// 数据关联实际节点
data._node = newNode
if (data.data.isActive) {
@@ -70,10 +132,28 @@ class Base {
return newNode
}
// 格式化节点位置
formatPosition(value, size, nodeSize) {
if (typeof value === 'number') {
return value
} else if (initRootNodePositionMap[value] !== undefined) {
return size * initRootNodePositionMap[value]
} else if (/^\d\d*%$/.test(value)) {
return Number.parseFloat(value) / 100 * size
} else {
return (size - nodeSize) / 2
}
}
// 定位节点到画布中间
setNodeCenter(node) {
node.left = (this.mindMap.width - node.width) / 2
node.top = (this.mindMap.height - node.height) / 2
let { initRootNodePosition } = this.mindMap.opt
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)
}
// 更新子节点属性
@@ -87,6 +167,37 @@ class Base {
})
}
// 更新子节点多个属性
updateChildrenPro(children, props) {
children.forEach(item => {
Object.keys(props).forEach((prop) => {
item[prop] += props[prop]
})
if (item.children && item.children.length && !item.hasCustomPosition()) {
// 适配自定义位置
this.updateChildrenPro(item.children, props)
}
})
}
// 递归计算节点的宽度
getNodeAreaWidth(node) {
let widthArr = []
let loop = (node, width) => {
if (node.children.length) {
width += node.width / 2
node.children.forEach(item => {
loop(item, width)
})
} else {
width += node.width
widthArr.push(width)
}
}
loop(node, 0)
return Math.max(...widthArr)
}
// 二次贝塞尔曲线
quadraticCurvePath(x1, y1, x2, y2) {
let cx = x1 + (x2 - x1) * 0.2
@@ -196,6 +307,11 @@ class Base {
generalizationNodeMargin
}
}
// 获取节点实际存在几个子节点
getNodeActChildrenLength(node) {
return node.nodeData.children && node.nodeData.children.length
}
}
export default Base

View File

@@ -1,386 +1,359 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
// 目录组织图
class CatalogOrganization extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedLeftTopValue()
},
() => {
this.adjustLeftTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(cur, parent, isRoot, layerIndex) => {
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
// 根节点定位在画布中心位置
if (isRoot) {
this.setNodeCenter(newNode)
} else {
// 非根节点
if (parent._node.isRoot) {
newNode.top =
parent._node.top +
parent._node.height +
this.getMarginX(layerIndex)
}
}
if (!cur.data.expand) {
return true
}
},
(cur, parent, isRoot, layerIndex) => {
if (isRoot) {
let len = cur.data.expand === false ? 0 : cur._node.children.length
cur._node.childrenAreaWidth = len
? cur._node.children.reduce((h, item) => {
return h + item.width
}, 0) +
(len + 1) * this.getMarginX(layerIndex + 1)
: 0
}
},
true,
0
)
}
// 遍历节点树计算节点的left、top
computedLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (
node.nodeData.data.expand &&
node.children &&
node.children.length
) {
let marginX = this.getMarginX(layerIndex + 1)
let marginY = this.getMarginY(layerIndex + 1)
if (isRoot) {
let left = node.left + node.width / 2 - node.childrenAreaWidth / 2
let totalLeft = left + marginX
node.children.forEach(cur => {
cur.left = totalLeft
totalLeft += cur.width + marginX
})
} else {
let totalTop = node.top + node.height + marginY + node.expandBtnSize
node.children.forEach(cur => {
cur.left = node.left + node.width * 0.5
cur.top = totalTop
totalTop += cur.height + marginY + node.expandBtnSize
})
}
}
},
null,
true
)
}
// 调整节点left、top
adjustLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
return
}
// 调整left
if (parent && parent.isRoot) {
let areaWidth = this.getNodeAreaWidth(node)
let difference = areaWidth - node.width
if (difference > 0) {
this.updateBrothersLeft(node, difference / 2)
}
}
// 调整top
let len = node.children.length
if (parent && !parent.isRoot && len > 0) {
let marginY = this.getMarginY(layerIndex + 1)
let totalHeight =
node.children.reduce((h, item) => {
return h + item.height
}, 0) +
(len + 1) * marginY +
len * node.expandBtnSize
this.updateBrothersTop(node, totalHeight)
}
},
null,
true
)
}
// 递归计算节点的宽度
getNodeAreaWidth(node) {
let widthArr = []
let loop = (node, width) => {
if (node.children.length) {
width += node.width / 2
node.children.forEach(item => {
loop(item, width)
})
} else {
width += node.width
widthArr.push(width)
}
}
loop(node, 0)
return Math.max(...widthArr)
}
// 调整兄弟节点的left
updateBrothersLeft(node, addWidth) {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
// 存在大于一个节点时,第一个或最后一个节点自身也需要移动,否则两边不对称
if (
(index === 0 || index === childrenList.length - 1) &&
childrenList.length > 1
) {
let _offset = index === 0 ? -addWidth : addWidth
node.left += _offset
if (
node.children &&
node.children.length &&
!node.hasCustomPosition()
) {
this.updateChildren(node.children, 'left', _offset)
}
}
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
return
}
let _offset = 0
if (_index < index) {
// 左边的节点往左移
_offset = -addWidth
} else if (_index > index) {
// 右边的节点往右移
_offset = addWidth
}
item.left += _offset
// 同步更新子节点的位置
if (item.children && item.children.length) {
this.updateChildren(item.children, 'left', _offset)
}
})
// 更新父节点的位置
this.updateBrothersLeft(node.parent, addWidth)
}
}
// 调整兄弟节点的top
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
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) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
let len = node.children.length
let marginX = this.getMarginX(node.layerIndex + 1)
if (node.isRoot) {
// 根节点
let x1 = left + width / 2
let y1 = top + height
let s1 = marginX * 0.7
let minx = Infinity
let maxx = -Infinity
node.children.forEach((item, index) => {
let x2 = item.left + item.width / 2
let y2 = item.top
if (x2 < minx) {
minx = x2
}
if (x2 > maxx) {
maxx = x2
}
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
? ` L ${item.left},${y2} L ${item.left + item.width},${y2}`
: ''
let path =
`M ${x2},${y1 + s1} L ${x2},${y1 + s1 > y2 ? y2 + item.height : y2}` +
nodeUseLineStylePath
// 竖线
lines[index].plot(path)
style && style(lines[index], item)
})
minx = Math.min(minx, x1)
maxx = Math.max(maxx, x1)
// 父节点的竖线
let line1 = this.draw.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()
node.style.line(lin2)
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
node._lines.push(lin2)
style && style(lin2, node)
}
} else {
// 非根节点
let y1 = top + height
let maxy = -Infinity
let x2 = node.left + node.width * 0.3
node.children.forEach((item, index) => {
// 为了适配自定义位置,下面做了各种位置的兼容
let y2 = item.top + item.height / 2
if (y2 > maxy) {
maxy = y2
}
// 水平线
let path = ''
let _left = item.left
let _isLeft = item.left + item.width < x2
let _isXCenter = false
if (_isLeft) {
// 水平位置在父节点左边
_left = item.left + item.width
} else if (item.left < x2 && item.left + item.width > x2) {
// 水平位置在父节点之间
_isXCenter = true
y2 = item.top
maxy = y2
}
if (y2 > top && y2 < y1) {
// 自定义位置的情况:垂直位置节点在父节点之间
path = `M ${
_isLeft ? node.left : node.left + node.width
},${y2} L ${_left},${y2}`
} else if (y2 < y1) {
// 自定义位置的情况:垂直位置节点在父节点上面
if (_isXCenter) {
y2 = item.top + item.height
_left = x2
}
path = `M ${x2},${top} L ${x2},${y2} L ${_left},${y2}`
} else {
if (_isXCenter) {
_left = x2
}
path = `M ${x2},${y2} L ${_left},${y2}`
}
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
? ` L ${_left},${y2 - item.height / 2} L ${_left},${
y2 + item.height / 2
}`
: ''
path += nodeUseLineStylePath
lines[index].plot(path)
style && style(lines[index], item)
})
// 竖线
if (len > 0) {
let lin2 = this.draw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
node.style.line(lin2)
if (maxy < y1 + expandBtnSize) {
lin2.hide()
} else {
lin2.plot(`M ${x2},${y1 + expandBtnSize} L ${x2},${maxy}`)
lin2.show()
}
node._lines.push(lin2)
style && style(lin2, node)
}
}
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
height + expandBtnSize / 2 - translateY
)
}
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeBoundaries(node, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
import Base from './Base'
import { walk, asyncRun } from '../utils'
// 目录组织图
class CatalogOrganization extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedLeftTopValue()
},
() => {
this.adjustLeftTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(cur, parent, isRoot, layerIndex) => {
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
// 根节点定位在画布中心位置
if (isRoot) {
this.setNodeCenter(newNode)
} else {
// 根节点
if (parent._node.isRoot) {
newNode.top =
parent._node.top +
parent._node.height +
this.getMarginX(layerIndex)
}
}
if (!cur.data.expand) {
return true
}
},
(cur, parent, isRoot, layerIndex) => {
if (isRoot) {
let len = cur.data.expand === false ? 0 : cur._node.children.length
cur._node.childrenAreaWidth = len
? cur._node.children.reduce((h, item) => {
return h + item.width
}, 0) +
(len + 1) * this.getMarginX(layerIndex + 1)
: 0
}
},
true,
0
)
}
// 遍历节点树计算节点的left、top
computedLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (
node.nodeData.data.expand &&
node.children &&
node.children.length
) {
let marginX = this.getMarginX(layerIndex + 1)
let marginY = this.getMarginY(layerIndex + 1)
if (isRoot) {
let left = node.left + node.width / 2 - node.childrenAreaWidth / 2
let totalLeft = left + marginX
node.children.forEach(cur => {
cur.left = totalLeft
totalLeft += cur.width + marginX
})
} else {
let totalTop = node.top + node.height + 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)
})
}
}
},
null,
true
)
}
// 调整节点left、top
adjustLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
return
}
// 调整left
if (parent && parent.isRoot) {
let areaWidth = this.getNodeAreaWidth(node)
let difference = areaWidth - node.width
if (difference > 0) {
this.updateBrothersLeft(node, difference)
}
}
// 调整top
let len = node.children.length
if (parent && !parent.isRoot && len > 0) {
let marginY = this.getMarginY(layerIndex + 1)
let totalHeight =
node.children.reduce((h, item) => {
return h + item.height + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
}, 0) +
len * marginY
this.updateBrothersTop(node, totalHeight)
}
},
(node, parent, isRoot) => {
if (isRoot) {
let { right, left } = this.getNodeBoundaries(node, 'h')
let childrenWidth = right - left
let offset = (node.left - left) - (childrenWidth - node.width) / 2
this.updateChildren(node.children, 'left', offset)
}
},
true
)
}
// 调整兄弟节点的left
updateBrothersLeft(node, addWidth) {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition() || _index <= index) {
// 适配自定义位置
return
}
item.left += addWidth
// 同步更新子节点的位置
if (item.children && item.children.length) {
this.updateChildren(item.children, 'left', addWidth)
}
})
// 更新父节点的位置
this.updateBrothersLeft(node.parent, addWidth)
}
}
// 调整兄弟节点的top
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
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) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
let len = node.children.length
let marginX = this.getMarginX(node.layerIndex + 1)
if (node.isRoot) {
// 根节点
let x1 = left + width / 2
let y1 = top + height
let s1 = marginX * 0.7
let minx = Infinity
let maxx = -Infinity
node.children.forEach((item, index) => {
let x2 = item.left + item.width / 2
let y2 = item.top
if (x2 < minx) {
minx = x2
}
if (x2 > maxx) {
maxx = x2
}
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
? ` L ${item.left},${y2} L ${item.left + item.width},${y2}`
: ''
let path =
`M ${x2},${y1 + s1} L ${x2},${y1 + s1 > y2 ? y2 + item.height : y2}` +
nodeUseLineStylePath
// 竖线
lines[index].plot(path)
style && style(lines[index], item)
})
minx = Math.min(minx, x1)
maxx = Math.max(maxx, x1)
// 父节点的竖线
let line1 = this.draw.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()
node.style.line(lin2)
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
node._lines.push(lin2)
style && style(lin2, node)
}
} else {
// 非根节点
let y1 = top + height
let maxy = -Infinity
let x2 = node.left + node.width * 0.3
node.children.forEach((item, index) => {
// 为了适配自定义位置,下面做了各种位置的兼容
let y2 = item.top + item.height / 2
if (y2 > maxy) {
maxy = y2
}
// 水平线
let path = ''
let _left = item.left
let _isLeft = item.left + item.width < x2
let _isXCenter = false
if (_isLeft) {
// 水平位置在父节点左边
_left = item.left + item.width
} else if (item.left < x2 && item.left + item.width > x2) {
// 水平位置在父节点之间
_isXCenter = true
y2 = item.top
maxy = y2
}
if (y2 > top && y2 < y1) {
// 自定义位置的情况:垂直位置节点在父节点之间
path = `M ${
_isLeft ? node.left : node.left + node.width
},${y2} L ${_left},${y2}`
} else if (y2 < y1) {
// 自定义位置的情况:垂直位置节点在父节点上面
if (_isXCenter) {
y2 = item.top + item.height
_left = x2
}
path = `M ${x2},${top} L ${x2},${y2} L ${_left},${y2}`
} else {
if (_isXCenter) {
_left = x2
}
path = `M ${x2},${y2} L ${_left},${y2}`
}
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
? ` L ${_left},${y2 - item.height / 2} L ${_left},${
y2 + item.height / 2
}`
: ''
path += nodeUseLineStylePath
lines[index].plot(path)
style && style(lines[index], item)
})
// 线
if (len > 0) {
let lin2 = this.draw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
node.style.line(lin2)
if (maxy < y1 + expandBtnSize) {
lin2.hide()
} else {
lin2.plot(`M ${x2},${y1 + expandBtnSize} L ${x2},${maxy}`)
lin2.show()
}
node._lines.push(lin2)
style && style(lin2, node)
}
}
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
height + expandBtnSize / 2 - 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
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
rect.size(width, expandBtnSize).x(0).y(height)
}
}
export default CatalogOrganization

View File

@@ -0,0 +1,399 @@
import Base from './Base'
import { walk, asyncRun, degToRad } from '../utils'
import { CONSTANTS } from '../constants/constant'
import utils from './fishboneUtils'
// 鱼骨图
class Fishbone extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
this.indent = 0.3
this.childIndent = 0.5
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedLeftTopValue()
},
() => {
this.adjustLeftTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据创建节点、计算根节点的位置计算根节点的子节点的top值
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(node, parent, isRoot, layerIndex, index) => {
// 创建节点
let newNode = this.createNode(node, 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.TOP
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
}
// 计算二级节点的top值
if (parent._node.isRoot) {
if (this.checkIsTop(newNode)) {
newNode.top = parent._node.top - newNode.height
} else {
newNode.top = parent._node.top + parent._node.height
}
}
}
if (!node.data.expand) {
return true
}
},
null,
true,
0
)
}
// 遍历节点树计算节点的left、top
computedLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (node.isRoot) {
let topTotalLeft = node.left + node.width + node.height
let bottomTotalLeft = node.left + node.width + node.height
node.children.forEach(item => {
if (this.checkIsTop(item)) {
item.left = topTotalLeft
topTotalLeft += item.width
} else {
item.left = bottomTotalLeft + 20
bottomTotalLeft += item.width
}
})
}
let params = { layerIndex, node, ctx: this }
if (this.checkIsTop(node)) {
utils.top.computedLeftTopValue(params)
} else {
utils.bottom.computedLeftTopValue(params)
}
},
null,
true
)
}
// 调整节点left、top
adjustLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
return
}
let params = { node, parent, layerIndex, ctx: this }
if (this.checkIsTop(node)) {
utils.top.adjustLeftTopValueBefore(params)
} else {
utils.bottom.adjustLeftTopValueBefore(params)
}
},
(node, parent) => {
let params = { parent, node, ctx: this }
if (this.checkIsTop(node)) {
utils.top.adjustLeftTopValueAfter(params)
} else {
utils.bottom.adjustLeftTopValueAfter(params)
}
// 调整二级节点的子节点的left值
if (node.isRoot) {
let topTotalLeft = 0
let bottomTotalLeft = 0
node.children.forEach(item => {
if (this.checkIsTop(item)) {
item.left += topTotalLeft
this.updateChildren(item.children, 'left', topTotalLeft)
let { left, right } = this.getNodeBoundaries(item, 'h')
topTotalLeft += right - left
} else {
item.left += bottomTotalLeft
this.updateChildren(item.children, 'left', bottomTotalLeft)
let { left, right } = this.getNodeBoundaries(item, 'h')
bottomTotalLeft += right - left
}
})
}
},
true
)
}
// 递归计算节点的宽度
getNodeAreaHeight(node) {
let totalHeight = 0
let loop = node => {
totalHeight +=
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
if (node.children.length) {
node.children.forEach(item => {
loop(item)
})
}
}
loop(node)
return totalHeight
}
// 调整兄弟节点的left
updateBrothersLeft(node) {
let childrenList = node.children
let totalAddWidth = 0
childrenList.forEach(item => {
item.left += totalAddWidth
if (item.children && item.children.length) {
this.updateChildren(item.children, 'left', totalAddWidth)
}
let { left, right } = this.getNodeBoundaries(item, 'h')
let areaWidth = right - left
let difference = areaWidth - item.width
if (difference > 0) {
totalAddWidth += difference
}
})
}
// 调整兄弟节点的top
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
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)
}
})
// 更新父节点的位置
if (this.checkIsTop(node)) {
this.updateBrothersTop(node.parent, addHeight)
} else {
this.updateBrothersTop(
node.parent,
node.layerIndex === 3 ? 0 : addHeight
)
}
}
}
// 检查节点是否是上方节点
checkIsTop(node) {
return node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
}
// 绘制连线,连接该节点到其子节点
renderLine(node, lines, style) {
if (node.layerIndex !== 1 && node.children.length <= 0) {
return []
}
let { top, height, expandBtnSize } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
let len = node.children.length
if (node.isRoot) {
// 当前节点是根节点
// 根节点的子节点是和根节点同一水平线排列
let maxx = -Infinity
node.children.forEach(item => {
if (item.left > maxx) {
maxx = item.left
}
// 水平线段到二级节点的连线
let nodeLineX = item.left
let offset = node.height / 2
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
let line = this.draw.path()
if (this.checkIsTop(item)) {
line.plot(
`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
}`
)
}
node.style.line(line)
node._lines.push(line)
style && style(line, node)
})
// 从根节点出发的水平线
let nodeHalfTop = node.top + node.height / 2
let offset = node.height / 2
let line = this.draw.path()
line.plot(
`M ${node.left + node.width},${nodeHalfTop} L ${
maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
},${nodeHalfTop}`
)
node.style.line(line)
node._lines.push(line)
style && style(line, node)
} else {
// 当前节点为非根节点
let maxy = -Infinity
let miny = Infinity
let maxx = -Infinity
let x = node.left + node.width * this.indent
node.children.forEach((item, index) => {
if (item.left > maxx) {
maxx = item.left
}
let y = item.top + item.height / 2
if (y > maxy) {
maxy = y
}
if (y < miny) {
miny = y
}
// 水平线
if (node.layerIndex > 1) {
let path = `M ${x},${y} L ${item.left},${y}`
lines[index].plot(path)
style && style(lines[index], item)
}
})
// 斜线
if (len >= 0) {
let line = this.draw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
let lineLength = maxx - node.left - node.width * this.indent
lineLength = Math.max(lineLength, 0)
let params = {
node,
line,
top,
x,
lineLength,
height,
expandBtnSize,
maxy,
miny,
ctx: this
}
if (this.checkIsTop(node)) {
utils.top.renderLine(params)
} else {
utils.bottom.renderLine(params)
}
node.style.line(line)
node._lines.push(line)
style && style(line, node)
}
}
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
let params = {
node,
btn,
expandBtnSize,
translateX,
translateY,
width,
height
}
if (this.checkIsTop(node)) {
utils.top.renderExpandBtn(params)
} else {
utils.bottom.renderExpandBtn(params)
}
}
}
// 创建概要节点
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
}
// 渲染展开收起按钮的隐藏占位元素
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)
}
}
}
export default Fishbone

View File

@@ -0,0 +1,369 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { CONSTANTS } from '../utils/constant'
const degToRad = deg => {
return (Math.PI / 180) * deg
}
// 下方鱼骨图
class Fishbone extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedLeftTopValue()
},
() => {
this.adjustLeftTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据创建节点、计算根节点的位置计算根节点的子节点的top值
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(node, parent, isRoot, layerIndex, index) => {
// 创建节点
let newNode = this.createNode(node, 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.TOP
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
}
// 计算二级节点的top值
if (parent._node.isRoot) {
newNode.top = parent._node.top + parent._node.height
}
}
if (!node.data.expand) {
return true
}
},
null,
true,
0
)
}
// 遍历节点树计算节点的left、top
computedLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex, index) => {
if (node.isRoot) {
let totalLeft = node.left + node.width
node.children.forEach(item => {
item.left = totalLeft
totalLeft += item.width
})
}
if (layerIndex === 1 && node.children) {
// 遍历二级节点的子节点
let startLeft = node.left + node.width * 0.5
let totalTop =
node.top +
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(item => {
item.left = startLeft
item.top =
totalTop +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
totalTop +=
item.height +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
if (layerIndex > 1 && node.children) {
// 遍历三级及以下节点的子节点
let startLeft = node.left + node.width * 0.5
let totalTop =
node.top -
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(item => {
item.left = startLeft
item.top = totalTop - item.height
totalTop -=
item.height +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
},
null,
true
)
}
// 调整节点left、top
adjustLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
return
}
// 调整top
let len = node.children.length
// 调整三级节点的top
// if (layerIndex === 2 && len > 0) {
// let totalHeight = node.children.reduce((h, item) => {
// return h + item.height
// }, 0)
// this.updateBrothersTop(node, totalHeight)
// }
if (layerIndex > 2 && len > 0) {
let totalHeight = node.children.reduce((h, item) => {
return (
h +
item.height +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
)
}, 0)
this.updateBrothersTop(node, -totalHeight)
}
},
(node, parent) => {
// 将二级节点的子节点移到上方
if (parent && parent.isRoot) {
// 遍历二级节点的子节点
let totalHeight = 0
let totalHeight2 = 0
node.children.forEach(item => {
// 调整top
let hasChildren = this.getNodeActChildrenLength(item) > 0
let nodeTotalHeight = this.getNodeAreaHeight(item)
let offset =
hasChildren > 0
? nodeTotalHeight -
item.height -
(hasChildren ? item.expandBtnSize : 0)
: 0
let _top = totalHeight + offset
item.top += _top
// 调整left
let offsetLeft =
(totalHeight2 + nodeTotalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
item.left += offsetLeft
totalHeight += offset
totalHeight2 += nodeTotalHeight
// 同步更新后代节点
this.updateChildrenPro(item.children, {
top: _top,
left: offsetLeft
})
})
}
// 调整二级节点的子节点的left值
if (node.isRoot) {
let totalLeft = 0
node.children.forEach(item => {
item.left += totalLeft
this.updateChildren(item.children, 'left', totalLeft)
let { left, right } = this.getNodeBoundaries(item, 'h')
totalLeft += right - left
})
}
},
true
)
}
// 递归计算节点的宽度
getNodeAreaHeight(node) {
let totalHeight = 0
let loop = node => {
totalHeight +=
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
if (node.children.length) {
node.children.forEach(item => {
loop(item)
})
}
}
loop(node)
return totalHeight
}
// 调整兄弟节点的left
updateBrothersLeft(node) {
let childrenList = node.children
let totalAddWidth = 0
childrenList.forEach(item => {
item.left += totalAddWidth
if (item.children && item.children.length) {
this.updateChildren(item.children, 'left', totalAddWidth)
}
// let areaWidth = this.getNodeAreaWidth(item)
let { left, right } = this.getNodeBoundaries(item, 'h')
let areaWidth = right - left
let difference = areaWidth - item.width
if (difference > 0) {
totalAddWidth += difference
}
})
}
// 调整兄弟节点的top
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
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, node.layerIndex === 3 ? 0 : addHeight)
}
}
// 绘制连线,连接该节点到其子节点
renderLine(node, lines, style) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
let len = node.children.length
if (node.isRoot) {
// 当前节点是根节点
let prevBother = node
// 根节点的子节点是和根节点同一水平线排列
node.children.forEach((item, index) => {
let x1 = prevBother.left + prevBother.width
let x2 = item.left
let y = node.top + node.height / 2
let path = `M ${x1},${y} L ${x2},${y}`
lines[index].plot(path)
style && style(lines[index], item)
prevBother = item
})
} else {
// 当前节点为非根节点
let maxy = -Infinity
let miny = Infinity
let maxx = -Infinity
let x = node.left + node.width * 0.3
node.children.forEach((item, index) => {
if (item.left > maxx) {
maxx = item.left
}
let y = item.top + item.height / 2
if (y > maxy) {
maxy = y
}
if (y < miny) {
miny = y
}
// 水平线
if (node.layerIndex > 1) {
let path = `M ${x},${y} L ${item.left},${y}`
lines[index].plot(path)
style && style(lines[index], item)
}
})
// 竖线
if (len > 0) {
let line = this.draw.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
}`
)
} else {
line.plot(`M ${x},${top} L ${x},${miny}`)
}
node.style.line(line)
node._lines.push(line)
style && style(line, node)
}
}
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
if (node.parent && node.parent.isRoot) {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
height + expandBtnSize / 2 - translateY
)
} else {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
-expandBtnSize / 2 - 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
}
}
export default Fishbone

View File

@@ -0,0 +1,350 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { CONSTANTS } from '../utils/constant'
const degToRad = deg => {
return (Math.PI / 180) * deg
}
// 上方鱼骨图
class Fishbone extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedLeftTopValue()
},
() => {
this.adjustLeftTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据创建节点、计算根节点的位置计算根节点的子节点的top值
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(node, parent, isRoot, layerIndex, index) => {
// 创建节点
let newNode = this.createNode(node, 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.TOP
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
}
// 计算二级节点的top值
if (parent._node.isRoot) {
newNode.top = parent._node.top - newNode.height
}
}
if (!node.data.expand) {
return true
}
},
null,
true,
0
)
}
// 遍历节点树计算节点的left、top
computedLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex, index) => {
if (node.isRoot) {
let totalLeft = node.left + node.width
node.children.forEach(item => {
item.left = totalLeft
totalLeft += item.width
})
}
if (layerIndex >= 1 && node.children) {
// 遍历三级及以下节点的子节点
let startLeft = node.left + node.width * 0.5
let totalTop =
node.top +
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(item => {
item.left = startLeft
item.top += totalTop
totalTop +=
item.height +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
},
null,
true
)
}
// 调整节点left、top
adjustLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
return
}
// 调整top
let len = node.children.length
// 调整三级及以下节点的top
if (parent && !parent.isRoot && len > 0) {
let totalHeight = node.children.reduce((h, item) => {
return (
h +
item.height +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
)
}, 0)
this.updateBrothersTop(node, totalHeight)
}
},
(node, parent) => {
// 将二级节点的子节点移到上方
if (parent && parent.isRoot) {
// 遍历二级节点的子节点
let totalHeight = 0
node.children.forEach(item => {
// 调整top
let nodeTotalHeight = this.getNodeAreaHeight(item)
let _top = item.top
item.top =
node.top - (item.top - node.top) - nodeTotalHeight + node.height
// 调整left
let offsetLeft =
(nodeTotalHeight + totalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
item.left += offsetLeft
totalHeight += nodeTotalHeight
// 同步更新后代节点
this.updateChildrenPro(item.children, {
top: item.top - _top,
left: offsetLeft
})
})
}
// 调整二级节点的子节点的left值
if (node.isRoot) {
let totalLeft = 0
node.children.forEach(item => {
item.left += totalLeft
this.updateChildren(item.children, 'left', totalLeft)
let { left, right } = this.getNodeBoundaries(item, 'h')
totalLeft += right - left
})
}
},
true
)
}
// 递归计算节点的宽度
getNodeAreaHeight(node) {
let totalHeight = 0
let loop = node => {
totalHeight +=
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
if (node.children.length) {
node.children.forEach(item => {
loop(item)
})
}
}
loop(node)
return totalHeight
}
// 调整兄弟节点的left
updateBrothersLeft(node) {
let childrenList = node.children
let totalAddWidth = 0
childrenList.forEach(item => {
item.left += totalAddWidth
if (item.children && item.children.length) {
this.updateChildren(item.children, 'left', totalAddWidth)
}
// let areaWidth = this.getNodeAreaWidth(item)
let { left, right } = this.getNodeBoundaries(item, 'h')
let areaWidth = right - left
let difference = areaWidth - item.width
if (difference > 0) {
totalAddWidth += difference
}
})
}
// 调整兄弟节点的top
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
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) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
let len = node.children.length
if (node.isRoot) {
// 当前节点是根节点
let prevBother = node
// 根节点的子节点是和根节点同一水平线排列
node.children.forEach((item, index) => {
let x1 = prevBother.left + prevBother.width
let x2 = item.left
let y = node.top + node.height / 2
let path = `M ${x1},${y} L ${x2},${y}`
lines[index].plot(path)
style && style(lines[index], item)
prevBother = item
})
} else {
// 当前节点为非根节点
let maxy = -Infinity
let miny = Infinity
let maxx = -Infinity
let x = node.left + node.width * 0.3
node.children.forEach((item, index) => {
if (item.left > maxx) {
maxx = item.left
}
let y = item.top + item.height / 2
if (y > maxy) {
maxy = y
}
if (y < miny) {
miny = y
}
// 水平线
if (node.layerIndex > 1) {
let path = `M ${x},${y} L ${item.left},${y}`
lines[index].plot(path)
style && style(lines[index], item)
}
})
// 竖线
if (len > 0) {
let line = this.draw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
let lineLength = maxx - node.left - node.width * 0.3
if (
node.parent &&
node.parent.isRoot &&
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
) {
line.plot(
`M ${x},${top} L ${x + 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
}`
)
} else {
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
}
}
node.style.line(line)
node._lines.push(line)
style && style(line, node)
}
}
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
if (node.parent && node.parent.isRoot) {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
-expandBtnSize / 2 - translateY
)
} else {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
height + expandBtnSize / 2 - 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
}
}
export default Fishbone

View File

@@ -1,267 +1,291 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
// 逻辑结构图
class LogicalStructure extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedTopValue()
},
() => {
this.adjustTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(cur, parent, isRoot, layerIndex) => {
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
// 根节点定位在画布中心位置
if (isRoot) {
this.setNodeCenter(newNode)
} else {
// 非根节点
// 定位到父节点右侧
newNode.left =
parent._node.left + parent._node.width + this.getMarginX(layerIndex)
}
if (!cur.data.expand) {
return true
}
},
(cur, parent, isRoot, layerIndex) => {
// 返回时计算节点的areaHeight也就是子节点所占的高度之和包括外边距
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) => {
if (
node.nodeData.data.expand &&
node.children &&
node.children.length
) {
let marginY = this.getMarginY(layerIndex + 1)
// 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半
let top = node.top + node.height / 2 - node.childrenAreaHeight / 2
let totalTop = top + marginY
node.children.forEach(cur => {
cur.top = totalTop
totalTop += cur.height + marginY
})
}
},
null,
true
)
}
// 调整节点top
adjustTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
return
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaHeight -
this.getMarginY(layerIndex + 1) * 2 -
node.height
if (difference > 0) {
this.updateBrothers(node, difference / 2)
}
},
null,
true
)
}
// 更新兄弟节点的top
updateBrothers(node, addHeight) {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
childrenList.forEach((item, _index) => {
if (item === node || item.hasCustomPosition()) {
// 适配自定义位置
return
}
let _offset = 0
// 上面的节点往上移
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)
}
}
// 绘制连线,连接该节点到其子节点
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 { left, top, width, height, expandBtnSize } = node
let marginX = this.getMarginX(node.layerIndex + 1)
let s1 = (marginX - expandBtnSize) * 0.6
node.children.forEach((item, index) => {
let x1 =
node.layerIndex === 0 ? left + width : left + width + expandBtnSize
let y1 = top + height / 2
let x2 = item.left
let y2 = item.top + item.height / 2
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
? item.width
: 0
let path = `M ${x1},${y1} L ${x1 + s1},${y1} L ${x1 + s1},${y2} L ${
x2 + nodeUseLineStyleOffset
},${y2}`
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
node.children.forEach((item, index) => {
let x1 =
node.layerIndex === 0 ? left + width / 2 : left + width + expandBtnSize
let y1 = top + height / 2
let x2 = item.left
let y2 = item.top + item.height / 2
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
? ` L ${item.left + item.width},${y2}`
: ''
let path = `M ${x1},${y1} L ${x2},${y2}` + nodeUseLineStylePath
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
node.children.forEach((item, index) => {
let x1 =
node.layerIndex === 0 ? left + width / 2 : left + width + expandBtnSize
let y1 = top + height / 2
let x2 = item.left
let y2 = item.top + item.height / 2
let path = ''
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
? ` L ${item.left + item.width},${y2}`
: ''
if (node.isRoot) {
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
} else {
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath
}
lines[index].plot(path)
style && style(lines[index], item)
})
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height } = node
let { translateX, translateY } = btn.transform()
// 节点使用横线风格,需要调整展开收起按钮位置
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
? height / 2
: 0
btn.translate(
width - translateX,
height / 2 - translateY + nodeUseLineStyleOffset
)
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeBoundaries(node, 'h')
let x1 = right + generalizationLineMargin
import Base from './Base'
import { walk, asyncRun } from '../utils'
// 逻辑结构图
class LogicalStructure extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedTopValue()
},
() => {
this.adjustTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(cur, parent, isRoot, layerIndex) => {
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
// 根节点定位在画布中心位置
if (isRoot) {
this.setNodeCenter(newNode)
} else {
// 根节点
// 定位到父节点右侧
newNode.left =
parent._node.left + parent._node.width + this.getMarginX(layerIndex)
}
if (!cur.data.expand) {
return true
}
},
(cur, parent, isRoot, layerIndex) => {
// 返回时计算节点的areaHeight也就是子节点所占的高度之和包括外边距
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) => {
if (
node.nodeData.data.expand &&
node.children &&
node.children.length
) {
let marginY = this.getMarginY(layerIndex + 1)
// 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半
let top = node.top + node.height / 2 - node.childrenAreaHeight / 2
let totalTop = top + marginY
node.children.forEach(cur => {
cur.top = totalTop
totalTop += cur.height + marginY
})
}
},
null,
true
)
}
// 调整节点top
adjustTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
return
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaHeight -
this.getMarginY(layerIndex + 1) * 2 -
node.height
if (difference > 0) {
this.updateBrothers(node, difference / 2)
}
},
null,
true
)
}
// 更新兄弟节点的top
updateBrothers(node, addHeight) {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
childrenList.forEach((item, _index) => {
if (item === node || item.hasCustomPosition()) {
// 适配自定义位置
return
}
let _offset = 0
// 上面的节点往上移
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)
}
}
// 绘制连线,连接该节点到其子节点
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 { left, top, width, height, expandBtnSize } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
let marginX = this.getMarginX(node.layerIndex + 1)
let s1 = (marginX - expandBtnSize) * 0.6
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
node.children.forEach((item, index) => {
let x1 =
node.layerIndex === 0 ? left + width : left + width + expandBtnSize
let y1 = top + height / 2
let x2 = item.left
let y2 = item.top + item.height / 2
// 节点使用横线风格,需要额外渲染横线
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 ${
x2 + nodeUseLineStyleOffset
},${y2}`
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
}
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
node.children.forEach((item, index) => {
let x1 =
node.layerIndex === 0 ? left + width / 2 : left + width + expandBtnSize
let y1 = top + height / 2
let x2 = 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 = nodeUseLineStyle
? ` L ${item.left + item.width},${y2}`
: ''
let path = `M ${x1},${y1} L ${x2},${y2}` + nodeUseLineStylePath
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
}
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
node.children.forEach((item, index) => {
let x1 =
node.layerIndex === 0 ? left + width / 2 : left + width + expandBtnSize
let y1 = top + height / 2
let x2 = item.left
let y2 = item.top + item.height / 2
let path = ''
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = nodeUseLineStyle
? ` L ${item.left + item.width},${y2}`
: ''
if (node.isRoot) {
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
} else {
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath
}
lines[index].plot(path)
style && style(lines[index], item)
})
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height } = node
let { translateX, translateY } = btn.transform()
// 节点使用横线风格,需要调整展开收起按钮位置
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
? height / 2
: 0
// 位置没有变化则返回
let _x = width
let _y = height / 2 + nodeUseLineStyleOffset
if (_x === translateX && _y === translateY) {
return
}
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
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
rect.size(expandBtnSize, height).x(width).y(0)
}
}
export default LogicalStructure

View File

@@ -1,8 +1,8 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { CONSTANTS } from '../constants/constant'
// 思维导图
class MindMap extends Base {
// 构造函数
// 在逻辑结构图的基础上增加一个变量来记录生长方向向左还是向右同时在计算left的时候根据方向来计算、调整top时只考虑同方向的节点即可
@@ -11,7 +11,6 @@ class MindMap extends Base {
}
// 布局
doLayout(callback) {
let task = [
() => {
@@ -31,7 +30,6 @@ class MindMap extends Base {
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
@@ -48,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)
@@ -75,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 {
@@ -96,7 +97,6 @@ class MindMap extends Base {
}
// 遍历节点树计算节点的top
computedTopValue() {
walk(
this.root,
@@ -113,7 +113,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 {
@@ -129,7 +129,6 @@ class MindMap extends Base {
}
// 调整节点top
adjustTopValue() {
walk(
this.root,
@@ -152,7 +151,6 @@ class MindMap extends Base {
}
// 更新兄弟节点的top
updateBrothers(node, leftAddHeight, rightAddHeight) {
if (node.parent) {
// 过滤出和自己同方向的节点
@@ -168,7 +166,10 @@ class MindMap extends Base {
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
@@ -188,7 +189,6 @@ class MindMap extends Base {
}
// 绘制连线,连接该节点到其子节点
renderLine(node, lines, style, lineStyle) {
if (lineStyle === 'curve') {
this.renderLineCurve(node, lines, style)
@@ -200,22 +200,23 @@ class MindMap extends Base {
}
// 直线风格连线
renderLineStraight(node, lines, style) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
let marginX = this.getMarginX(node.layerIndex + 1)
let s1 = (marginX - expandBtnSize) * 0.6
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
node.children.forEach((item, index) => {
let x1 = 0
let _s = 0
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStyleOffset = this.mindMap.themeConfig.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
@@ -224,8 +225,13 @@ 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
let path = `M ${x1},${y1} L ${x1 + _s},${y1} L ${x1 + _s},${y2} L ${
x2 + nodeUseLineStyleOffset
},${y2}`
@@ -235,26 +241,34 @@ class MindMap extends Base {
}
// 直连风格
renderLineDirect(node, lines, style) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
node.children.forEach((item, index) => {
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 (this.mindMap.themeConfig.nodeUseLineStyle) {
if (item.dir === 'left') {
if (nodeUseLineStyle) {
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
nodeUseLineStylePath = ` L ${item.left},${y2}`
} else {
nodeUseLineStylePath = ` L ${item.left + item.width},${y2}`
@@ -267,27 +281,35 @@ class MindMap extends Base {
}
// 曲线风格连线
renderLineCurve(node, lines, style) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
node.children.forEach((item, index) => {
let x1 =
node.layerIndex === 0
? left + width / 2
: item.dir === 'left'
: item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
? left - expandBtnSize
: left + width + 20
: 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
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
// 节点使用横线风格,需要额外渲染横线
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}`
@@ -304,7 +326,6 @@ class MindMap extends Base {
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize } = node
let { translateX, translateY } = btn.transform()
@@ -312,15 +333,21 @@ class MindMap extends Base {
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
? height / 2
: 0
let x = (node.dir === 'left' ? 0 - expandBtnSize : width) - translateX
let y = height / 2 - translateY + nodeUseLineStyleOffset
// 位置没有变化则返回
let _x =
node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT ? 0 - expandBtnSize : width
let _y = height / 2 + nodeUseLineStyleOffset
if (_x === translateX && _y === translateY) {
return
}
let x = _x - translateX
let y = _y - translateY
btn.translate(x, y)
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let isLeft = node.dir === 'left'
let isLeft = node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
let {
top,
bottom,
@@ -346,6 +373,15 @@ class MindMap extends Base {
(isLeft ? gNode.width : 0)
gNode.top = top + (bottom - top - gNode.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 MindMap

View File

@@ -5,13 +5,11 @@ import { walk, asyncRun } from '../utils'
// 和逻辑结构图基本一样只是方向变成向下生长所以先计算节点的top后计算节点的left、最后调整节点的left即可
class OrganizationStructure extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
@@ -31,7 +29,6 @@ class OrganizationStructure extends Base {
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
walk(
this.renderer.renderTree,
@@ -67,7 +64,6 @@ class OrganizationStructure extends Base {
}
// 遍历节点树计算节点的left
computedLeftValue() {
walk(
this.root,
@@ -94,7 +90,6 @@ class OrganizationStructure extends Base {
}
// 调整节点left
adjustLeftValue() {
walk(
this.root,
@@ -118,7 +113,6 @@ class OrganizationStructure extends Base {
}
// 更新兄弟节点的left
updateBrothers(node, addWidth) {
if (node.parent) {
let childrenList = node.parent.children
@@ -150,7 +144,6 @@ class OrganizationStructure extends Base {
}
// 绘制连线,连接该节点到其子节点
renderLine(node, lines, style, lineStyle) {
if (lineStyle === 'direct') {
this.renderLineDirect(node, lines, style)
@@ -160,7 +153,6 @@ class OrganizationStructure extends Base {
}
// 直连风格
renderLineDirect(node, lines, style) {
if (node.children.length <= 0) {
return []
@@ -182,12 +174,14 @@ class OrganizationStructure extends Base {
}
// 直线风格连线
renderLineStraight(node, lines, style) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize, isRoot } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
let x1 = left + width / 2
let y1 = top + height
let marginX = this.getMarginX(node.layerIndex + 1)
@@ -232,7 +226,6 @@ class OrganizationStructure extends Base {
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize } = node
let { translateX, translateY } = btn.transform()
@@ -243,7 +236,6 @@ class OrganizationStructure extends Base {
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
bottom,
@@ -263,6 +255,11 @@ class OrganizationStructure extends Base {
gNode.top = bottom + generalizationNodeMargin
gNode.left = left + (right - left - gNode.width) / 2
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
rect.size(width, expandBtnSize).x(0).y(height)
}
}
export default OrganizationStructure

View File

@@ -0,0 +1,363 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { CONSTANTS } from '../constants/constant'
// 时间轴
class Timeline extends Base {
// 构造函数
constructor(opt = {}, layout) {
super(opt)
this.layout = layout
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedLeftTopValue()
},
() => {
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 {
// 非根节点
// 时间轴2类型需要交替显示
if (this.layout === CONSTANTS.LAYOUT.TIMELINE2) {
// 三级及以下节点以上级为准
if (parent._node.dir) {
newNode.dir = parent._node.dir
} else {
// 节点生长方向
newNode.dir =
index % 2 === 0
? CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
: CONSTANTS.LAYOUT_GROW_DIR.TOP
}
} else {
newNode.dir = ''
}
if (parent._node.isRoot) {
newNode.top =
parent._node.top +
(cur._node.height > parent._node.height
? -(cur._node.height - parent._node.height) / 2
: (parent._node.height - cur._node.height) / 2)
}
}
if (!cur.data.expand) {
return true
}
},
null,
true,
0
)
}
// 遍历节点树计算节点的left、top
computedLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex, index) => {
if (
node.nodeData.data.expand &&
node.children &&
node.children.length
) {
let marginX = this.getMarginX(layerIndex + 1)
let marginY = this.getMarginY(layerIndex + 1)
if (isRoot) {
let left = node.left + node.width
let totalLeft = left + marginX
node.children.forEach(cur => {
cur.left = totalLeft
totalLeft += cur.width + marginX
})
} else {
let totalTop =
node.top +
node.height +
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)
})
}
}
},
null,
true
)
}
// 调整节点left、top
adjustLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
return
}
// 调整left
if (node.isRoot) {
this.updateBrothersLeft(node)
}
// 调整top
let len = node.children.length
if (parent && !parent.isRoot && len > 0) {
let marginY = this.getMarginY(layerIndex + 1)
let totalHeight =
node.children.reduce((h, item) => {
return (
h +
item.height +
(this.getNodeActChildrenLength(item) > 0
? item.expandBtnSize
: 0)
)
}, 0) +
len * marginY
this.updateBrothersTop(node, totalHeight)
}
},
(node, parent, isRoot, layerIndex) => {
if (
parent &&
parent.isRoot &&
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
) {
// 遍历二级节点的子节点
node.children.forEach(item => {
let totalHeight = this.getNodeAreaHeight(item)
let _top = item.top
item.top =
node.top - (item.top - node.top) - totalHeight + node.height
this.updateChildren(item.children, 'top', item.top - _top)
})
}
},
true
)
}
// 递归计算节点的宽度
getNodeAreaHeight(node) {
let totalHeight = 0
let loop = node => {
totalHeight +=
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
this.getMarginY(node.layerIndex)
if (node.children.length) {
node.children.forEach(item => {
loop(item)
})
}
}
loop(node)
return totalHeight
}
// 调整兄弟节点的left
updateBrothersLeft(node) {
let childrenList = node.children
let totalAddWidth = 0
childrenList.forEach(item => {
item.left += totalAddWidth
if (item.children && item.children.length) {
this.updateChildren(item.children, 'left', totalAddWidth)
}
// let areaWidth = this.getNodeAreaWidth(item)
let { left, right } = this.getNodeBoundaries(item, 'h')
let areaWidth = right - left
let difference = areaWidth - item.width
if (difference > 0) {
totalAddWidth += difference
}
})
}
// 调整兄弟节点的top
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
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) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
let len = node.children.length
if (node.isRoot) {
// 当前节点是根节点
let prevBother = node
// 根节点的子节点是和根节点同一水平线排列
node.children.forEach((item, index) => {
let x1 = prevBother.left + prevBother.width
let x2 = item.left
let y = node.top + node.height / 2
let path = `M ${x1},${y} L ${x2},${y}`
lines[index].plot(path)
style && style(lines[index], item)
prevBother = item
})
} else {
// 当前节点为非根节点
let maxy = -Infinity
let miny = Infinity
let x = node.left + node.width * 0.3
node.children.forEach((item, index) => {
let y = item.top + item.height / 2
if (y > maxy) {
maxy = y
}
if (y < miny) {
miny = y
}
// 水平线
let path = `M ${x},${y} L ${item.left},${y}`
lines[index].plot(path)
style && style(lines[index], item)
})
// 竖线
if (len > 0) {
let line = this.draw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
if (
node.parent &&
node.parent.isRoot &&
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
) {
line.plot(`M ${x},${top} L ${x},${miny}`)
} else {
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
}
node.style.line(line)
node._lines.push(line)
style && style(line, node)
}
}
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
if (
node.parent &&
node.parent.isRoot &&
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
) {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
-expandBtnSize / 2 - translateY
)
} else {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
height + expandBtnSize / 2 - 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
}
// 渲染展开收起按钮的隐藏占位元素
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)
}
}
}
}
export default Timeline

View File

@@ -0,0 +1,431 @@
import Base from './Base'
import { walk, asyncRun } 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.nodeData.data.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.nodeData.data.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 = childrenList.findIndex(item => {
return item === node
})
childrenList.forEach((item, _index) => {
// 自定义节点位置
if (item.hasCustomPosition()) return
// 三级或三级以下节点自身位置不需要动
if (!node.parent.isRoot && item === node) 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 = childrenList.findIndex(item => {
return item === node
})
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(node, gLine, gNode) {
let isLeft = node.dir === CONSTANTS.LAYOUT_GROW_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
}
// 渲染展开收起按钮的隐藏占位元素
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

View File

@@ -0,0 +1,216 @@
import { degToRad } from '../utils/'
export default {
top: {
renderExpandBtn({
node,
btn,
expandBtnSize,
translateX,
translateY,
width,
height
}) {
if (node.parent && node.parent.isRoot) {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
-expandBtnSize / 2 - translateY
)
} else {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
height + expandBtnSize / 2 - translateY
)
}
},
renderLine({
node,
line,
top,
x,
lineLength,
height,
expandBtnSize,
maxy,
ctx
}) {
if (node.parent && node.parent.isRoot) {
line.plot(
`M ${x},${top} L ${x + lineLength},${
top - Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
}`
)
} else {
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
}
},
computedLeftTopValue({ layerIndex, node, ctx }) {
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)
node.children.forEach(item => {
item.left = startLeft
item.top += totalTop
totalTop +=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
},
adjustLeftTopValueBefore({ node, parent, ctx }) {
// 调整top
let len = node.children.length
// 调整三级及以下节点的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)
)
}, 0)
ctx.updateBrothersTop(node, totalHeight)
}
},
adjustLeftTopValueAfter({ parent, node, ctx }) {
// 将二级节点的子节点移到上方
if (parent && parent.isRoot) {
// 遍历二级节点的子节点
let totalHeight = node.expandBtnSize
node.children.forEach(item => {
// 调整top
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
let _top = item.top
let _left = item.left
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))
totalHeight += nodeTotalHeight
// 同步更新后代节点
ctx.updateChildrenPro(item.children, {
top: item.top - _top,
left: item.left - _left
})
})
}
}
},
bottom: {
renderExpandBtn({
node,
btn,
expandBtnSize,
translateX,
translateY,
width,
height
}) {
if (node.parent && node.parent.isRoot) {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
height + expandBtnSize / 2 - translateY
)
} else {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
-expandBtnSize / 2 - translateY
)
}
},
renderLine({ node, line, top, x, lineLength, height, miny, ctx }) {
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
}`
)
} else {
line.plot(`M ${x},${top} L ${x},${miny}`)
}
},
computedLeftTopValue({ layerIndex, node, ctx }) {
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)
node.children.forEach(item => {
item.left = startLeft
item.top =
totalTop +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
totalTop +=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
if (layerIndex > 1 && node.children) {
// 遍历三级及以下节点的子节点
let startLeft = node.left + node.width * ctx.childIndent
let totalTop =
node.top -
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(item => {
item.left = startLeft
item.top = totalTop - item.height
totalTop -=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
},
adjustLeftTopValueBefore({ node, ctx, layerIndex }) {
// 调整top
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)
)
}, 0)
ctx.updateBrothersTop(node, -totalHeight)
}
},
adjustLeftTopValueAfter({ parent, node, ctx }) {
// 将二级节点的子节点移到上方
if (parent && parent.isRoot) {
// 遍历二级节点的子节点
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 _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))
totalHeight += offset
totalHeight2 += nodeTotalHeight
// 同步更新后代节点
ctx.updateChildrenPro(item.children, {
top: _top,
left: item.left - _left
})
})
}
}
}
}

View File

@@ -0,0 +1,7 @@
import { transformToMarkdown } from './toMarkdown'
import { transformMarkdownTo } from './markdownTo'
export default {
transformToMarkdown,
transformMarkdownTo
}

View File

@@ -0,0 +1,98 @@
import { fromMarkdown } from 'mdast-util-from-markdown'
// 处理list的情况
const handleList = node => {
let list = []
let walk = (arr, newArr) => {
for (let i = 0; i < arr.length; i++) {
let cur = arr[i]
let node = {}
node.data = {
// 节点内容
text: cur.children[0].children[0].value
}
node.children = []
newArr.push(node)
if (cur.children.length > 1) {
for (let j = 1; j < cur.children.length; j++) {
let cur2 = cur.children[j]
if (cur2.type === 'list') {
walk(cur2.children, node.children)
}
}
}
}
}
walk(node.children, list)
return list
}
// 将markdown转换成节点树
export const transformMarkdownTo = async md => {
const tree = fromMarkdown(md)
let root = {
children: []
}
let childrenQueue = [root.children]
let currentChildren = root.children
let depthQueue = [-1]
let currentDepth = -1
for (let i = 0; i < tree.children.length; i++) {
let cur = tree.children[i]
if (cur.type === 'heading') {
if (!cur.children[0]) continue
// 创建新节点
let node = {}
node.data = {
// 节点内容
text: cur.children[0].value
}
node.children = []
// 如果当前的层级大于上一个节点的层级,那么是其子节点
if (cur.depth > currentDepth) {
// 添加到上一个节点的子节点列表里
currentChildren.push(node)
// 更新当前栈和数据
childrenQueue.push(node.children)
currentChildren = node.children
depthQueue.push(cur.depth)
currentDepth = cur.depth
} else if (cur.depth === currentDepth) {
// 如果当前层级等于上一个节点的层级,说明它们是同级节点
// 将上一个节点出栈
childrenQueue.pop()
currentChildren = childrenQueue[childrenQueue.length - 1]
depthQueue.pop()
currentDepth = depthQueue[depthQueue.length - 1]
// 追加到上上个节点的子节点列表里
currentChildren.push(node)
// 更新当前栈和数据
childrenQueue.push(node.children)
currentChildren = node.children
depthQueue.push(cur.depth)
currentDepth = cur.depth
} else {
// 如果当前层级小于上一个节点的层级,那么一直出栈,直到遇到比当前层级小的节点
while (depthQueue.length) {
childrenQueue.pop()
currentChildren = childrenQueue[childrenQueue.length - 1]
depthQueue.pop()
currentDepth = depthQueue[depthQueue.length - 1]
if (currentDepth < cur.depth) {
// 追加到该节点的子节点列表里
currentChildren.push(node)
// 更新当前栈和数据
childrenQueue.push(node.children)
currentChildren = node.children
depthQueue.push(cur.depth)
currentDepth = cur.depth
break
}
}
}
} else if (cur.type === 'list') {
currentChildren.push(...handleList(cur))
}
}
return root.children[0]
}

View File

@@ -0,0 +1,53 @@
import { walk } from '../utils'
let el = null
const getText = str => {
if (!el) {
el = document.createElement('div')
}
el.innerHTML = str
return el.textContent
}
const getTitleMark = level => {
return new Array(level).fill('#').join('')
}
const getIndentMark = level => {
return new Array(level - 6).fill(' ').join('') + '*'
}
// 转换成markdown格式
export const transformToMarkdown = root => {
let content = ''
walk(
root,
null,
(node, parent, isRoot, layerIndex) => {
let level = layerIndex + 1
let text = node.data.richText ? getText(node.data.text) : node.data.text
if (level <= 6) {
content += getTitleMark(level)
} else {
content += getIndentMark(level)
}
content += ' ' + text
// 概要
let generalization = node.data.generalization
if (generalization && generalization.text) {
let generalizationText = generalization.richText
? getText(generalization.text)
: generalization.text
content += `[${generalizationText}]`
}
content += '\n\n'
// 备注
if (node.data.note) {
content += node.data.note + '\n\n'
}
},
() => {},
true
)
return content
}

View File

@@ -1,5 +1,11 @@
import JSZip from 'jszip'
import xmlConvert from 'xml-js'
import {
getTextFromHtml,
imgToDataUrl,
parseDataUrl,
getImageSize
} from '../utils/index'
// 解析.xmind文件
const parseXmindFile = file => {
@@ -10,7 +16,7 @@ const parseXmindFile = file => {
let content = ''
if (zip.files['content.json']) {
let json = await zip.files['content.json'].async('string')
content = transformXmind(json)
content = await transformXmind(json, zip.files)
} else if (zip.files['content.xml']) {
let xml = await zip.files['content.xml'].async('string')
let json = xmlConvert.xml2json(xml)
@@ -33,18 +39,20 @@ const parseXmindFile = file => {
}
// 转换xmind数据
const transformXmind = content => {
const transformXmind = async (content, files) => {
let data = JSON.parse(content)[0]
let nodeTree = data.rootTopic
let newTree = {}
let walk = (node, newNode) => {
let waitLoadImageList = []
let walk = async (node, newNode) => {
newNode.data = {
// 节点内容
text: node.title
}
// 节点备注
if (node.notes) {
newNode.data.note = (node.notes.realHTML || node.notes.plain).content
let notesData = node.notes.realHTML || node.notes.plain
newNode.data.note = notesData ? notesData.content || '' : ''
}
// 超链接
if (node.href && /^https?:\/\//.test(node.href)) {
@@ -54,6 +62,42 @@ const transformXmind = content => {
if (node.labels && node.labels.length > 0) {
newNode.data.tag = node.labels
}
// 图片
if (node.image && /\.(jpg|jpeg|png|gif|webp)$/.test(node.image.src)) {
try {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
// 读取图片
let imageType = /\.([^.]+)$/.exec(node.image.src)[1]
let imageBase64 =
`data:image/${imageType};base64,` +
(await files['resources/' + node.image.src.split('/')[1]].async(
'base64'
))
newNode.data.image = imageBase64
// 如果图片尺寸不存在
if (!node.image.width && !node.image.height) {
let imageSize = await getImageSize(imageBase64)
newNode.data.imageSize = {
width: imageSize.width,
height: imageSize.height
}
} else {
newNode.data.imageSize = {
width: node.image.width,
height: node.image.height
}
}
resolve()
} catch (error) {
console.log(error)
resolve()
}
}
// 子节点
newNode.children = []
if (
@@ -69,6 +113,7 @@ const transformXmind = content => {
}
}
walk(nodeTree, newTree)
await Promise.all(waitLoadImageList)
return newTree
}
@@ -157,8 +202,127 @@ const transformOldXmind = content => {
return newTree
}
// 数据转换为xmind文件
const transformToXmind = async (data, name) => {
const id = 'simpleMindMap_' + Date.now()
const imageList = []
// 转换核心数据
let newTree = {}
let waitLoadImageList = []
let walk = async (node, newNode, isRoot) => {
let newData = {
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 || []
}
// 图片
if (node.data.image) {
try {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
let imgName = ''
let imgData = node.data.image
// 网络图片要先转换成data:url
if (/^https?:\/\//.test(node.data.image)) {
imgData = await imgToDataUrl(node.data.image)
}
// 从data:url中解析出图片类型和base64
let dataUrlRes = parseDataUrl(imgData)
imgName = 'image_' + imageList.length + '.' + dataUrlRes.type
imageList.push({
name: imgName,
data: dataUrlRes.base64
})
newData.image = {
src: 'xap:resources/' + imgName,
width: node.data.imageSize.width,
height: node.data.imageSize.height
}
resolve()
} catch (error) {
console.log(error)
resolve()
}
}
// 样式
// 暂时不考虑样式
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]
})
}
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":"1","layoutEngineVersion":"2","activeSheetId":"${id}"}`
)
const manifestData = {
'file-entries': { 'content.json': {}, 'metadata.json': {} }
}
// 图片
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
}

View File

@@ -0,0 +1,452 @@
import { walk, bfsWalk, throttle } from '../utils'
import { v4 as uuid } from 'uuid'
import {
getAssociativeLineTargetIndex,
computeCubicBezierPathPoints,
cubicBezierPath,
getNodePoint,
computeNodePoints,
getNodeLinePath
} from './associativeLine/associativeLineUtils'
import associativeLineControlsMethods from './associativeLine/associativeLineControls'
import associativeLineTextMethods from './associativeLine/associativeLineText'
// 关联线插件
class AssociativeLine {
constructor(opt = {}) {
this.mindMap = opt.mindMap
this.draw = this.mindMap.draw
// 当前所有连接线
this.lineList = []
// 当前激活的连接线
this.activeLine = null
// 当前正在创建连接线
this.isCreatingLine = false // 是否正在创建连接线中
this.creatingStartNode = null // 起始节点
this.creatingLine = null // 创建过程中的连接线
this.overlapNode = null // 创建过程中的目标节点
// 是否有节点正在被拖拽
this.isNodeDragging = false
// 箭头图标
this.markerPath = null
this.marker = this.createMarker()
// 控制点
this.controlLine1 = null
this.controlLine2 = null
this.controlPoint1 = null
this.controlPoint2 = null
this.controlPointDiameter = 10
this.isControlPointMousedown = false
this.mousedownControlPointKey = ''
this.controlPointMousemoveState = {
pos: null,
startPoint: null,
endPoint: null,
targetIndex: ''
}
// 节流一下,不然很卡
this.checkOverlapNode = throttle(this.checkOverlapNode, 100, this)
// 控制点相关方法
Object.keys(associativeLineControlsMethods).forEach(item => {
this[item] = associativeLineControlsMethods[item].bind(this)
})
// 关联线文字相关方法
Object.keys(associativeLineTextMethods).forEach(item => {
this[item] = associativeLineTextMethods[item].bind(this)
})
this.bindEvent()
}
// 监听事件
bindEvent() {
// 节点树渲染完毕后渲染连接线
this.renderAllLines = this.renderAllLines.bind(this)
this.mindMap.on('node_tree_render_end', this.renderAllLines)
// 状态改变后重新渲染连接线
this.mindMap.on('data_change', this.renderAllLines)
// 监听画布和节点点击事件,用于清除当前激活的连接线
this.mindMap.on('draw_click', () => {
if (this.isControlPointMousedown) {
return
}
this.clearActiveLine()
})
this.mindMap.on('node_click', node => {
if (this.isCreatingLine) {
this.completeCreateLine(node)
} else {
this.clearActiveLine()
}
})
// 注册删除快捷键
this.mindMap.keyCommand.addShortcut(
'Del|Backspace',
this.removeLine.bind(this)
)
// 注册添加连接线的命令
this.mindMap.command.add('ADD_ASSOCIATIVE_LINE', this.addLine.bind(this))
// 监听鼠标移动事件
this.mindMap.on('mousemove', this.onMousemove.bind(this))
// 节点拖拽事件
this.mindMap.on('node_dragging', this.onNodeDragging.bind(this))
this.mindMap.on('node_dragend', this.onNodeDragend.bind(this))
// 拖拽控制点
this.mindMap.on('mouseup', this.onControlPointMouseup.bind(this))
// 缩放事件
this.mindMap.on('scale', this.onScale)
}
// 创建箭头
createMarker() {
return this.draw.marker(20, 20, add => {
add.ref(2, 5)
add.size(10, 10)
add.attr('orient', 'auto-start-reverse')
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
})
}
// 渲染所有连线
renderAllLines() {
// 先移除
this.removeAllLines()
this.removeControls()
this.clearActiveLine()
let tree = this.mindMap.renderer.root
if (!tree) return
let idToNode = new Map()
let nodeToIds = new Map()
walk(
tree,
null,
cur => {
if (!cur) return
let data = cur.nodeData.data
if (
data.associativeLineTargets &&
data.associativeLineTargets.length > 0
) {
nodeToIds.set(cur, data.associativeLineTargets)
}
if (data.id) {
idToNode.set(data.id, cur)
}
},
() => {},
true,
0
)
nodeToIds.forEach((ids, node) => {
ids.forEach(id => {
let toNode = idToNode.get(id)
if (!node || !toNode) return
let [startPoint, endPoint] = computeNodePoints(node, toNode)
this.drawLine(startPoint, endPoint, node, toNode)
})
})
}
// 绘制连接线
drawLine(startPoint, endPoint, node, toNode) {
let {
associativeLineWidth,
associativeLineColor,
associativeLineActiveWidth,
associativeLineActiveColor
} = this.mindMap.themeConfig
// 箭头
this.markerPath
.stroke({ color: associativeLineColor })
.fill({ color: associativeLineColor })
// 路径
let { path: pathStr, controlPoints } = getNodeLinePath(
startPoint,
endPoint,
node,
toNode
)
// 虚线
let path = this.draw.path()
path
.stroke({
width: associativeLineWidth,
color: associativeLineColor,
dasharray: [6, 4]
})
.fill({ color: 'none' })
path.plot(pathStr)
path.marker('end', this.marker)
// 不可见的点击线
let clickPath = this.draw.path()
clickPath
.stroke({ width: associativeLineActiveWidth, color: 'transparent' })
.fill({ color: 'none' })
clickPath.plot(pathStr)
// 文字
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.renderText(this.getText(node, toNode), path, text)
this.lineList.push([path, clickPath, text, node, toNode])
}
// 激活某根关联线
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)
}
}
// 移除所有连接线
removeAllLines() {
this.lineList.forEach(line => {
line[0].remove()
line[1].remove()
line[2].remove()
})
this.lineList = []
}
// 从当前激活节点开始创建连接线
createLineFromActiveNode() {
if (this.mindMap.renderer.activeNodeList.length <= 0) return
let node = this.mindMap.renderer.activeNodeList[0]
this.createLine(node)
}
// 创建连接线
createLine(fromNode) {
let { associativeLineWidth, associativeLineColor } =
this.mindMap.themeConfig
if (this.isCreatingLine || !fromNode) return
this.isCreatingLine = true
this.creatingStartNode = fromNode
this.creatingLine = this.draw.path()
this.creatingLine
.stroke({
width: associativeLineWidth,
color: associativeLineColor,
dasharray: [6, 4]
})
.fill({ color: 'none' })
this.creatingLine.marker('end', this.marker)
}
// 鼠标移动事件
onMousemove(e) {
this.onControlPointMousemove(e)
this.updateCreatingLine(e)
}
// 更新创建过程中的连接线
updateCreatingLine(e) {
if (!this.isCreatingLine) return
let { x, y } = this.getTransformedEventPos(e)
let startPoint = getNodePoint(this.creatingStartNode)
let offsetX = x > startPoint.x ? -10 : 10
let pathStr = cubicBezierPath(startPoint.x, startPoint.y, x + offsetX, y)
this.creatingLine.plot(pathStr)
this.checkOverlapNode(x, y)
}
// 获取转换后的鼠标事件对象的坐标
getTransformedEventPos(e) {
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
let { scaleX, scaleY, translateX, translateY } =
this.mindMap.draw.transform()
return {
x: (x - translateX) / scaleX,
y: (y - translateY) / scaleY
}
}
// 检测当前移动到的目标节点
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 === this.creatingStartNode || this.overlapNode) {
return
}
let { left, top, width, height } = node
let right = left + width
let bottom = top + height
if (x >= left && x <= right && y >= top && y <= bottom) {
this.overlapNode = node
}
})
if (this.overlapNode && !this.overlapNode.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
}
}
// 完成创建连接线
completeCreateLine(node) {
if (this.creatingStartNode === node) return
this.addLine(this.creatingStartNode, node)
if (this.overlapNode && this.overlapNode.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
}
this.isCreatingLine = false
this.creatingStartNode = null
this.creatingLine.remove()
this.creatingLine = null
this.overlapNode = null
}
// 添加连接线
addLine(fromNode, toNode) {
if (!fromNode || !toNode) return
// 目标节点如果没有id则生成一个id
let id = toNode.nodeData.data.id
if (!id) {
id = uuid()
this.mindMap.execCommand('SET_NODE_DATA', toNode, {
id
})
}
// 将目标节点id保存起来
let list = fromNode.nodeData.data.associativeLineTargets || []
list.push(id)
// 保存控制点
let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
let controlPoints = computeCubicBezierPathPoints(
startPoint.x,
startPoint.y,
endPoint.x,
endPoint.y
)
let offsetList =
fromNode.nodeData.data.associativeLineTargetControlOffsets || []
// 保存的实际是控制点和端点的差值,否则当节点位置改变了,控制点还是原来的位置,连线就不对了
offsetList[list.length - 1] = [
{
x: controlPoints[0].x - startPoint.x,
y: controlPoints[0].y - startPoint.y
},
{
x: controlPoints[1].x - endPoint.x,
y: controlPoints[1].y - endPoint.y
}
]
this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
associativeLineTargets: list,
associativeLineTargetControlOffsets: offsetList
})
}
// 删除连接线
removeLine() {
if (!this.activeLine) return
let [, , , node, toNode] = this.activeLine
this.removeControls()
let { associativeLineTargets, associativeLineTargetControlOffsets, associativeLineText } =
node.nodeData.data
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
// 更新关联线文本数据
let newAssociativeLineText = {}
if (associativeLineText) {
Object.keys(associativeLineText).forEach((item) => {
if (item !== toNode.nodeData.data.id) {
newAssociativeLineText[item] = associativeLineText[item]
}
})
}
this.mindMap.execCommand('SET_NODE_DATA', node, {
// 目标
associativeLineTargets: associativeLineTargets.filter((_, index) => {
return index !== targetIndex
}),
// 偏移量
associativeLineTargetControlOffsets: associativeLineTargetControlOffsets
? associativeLineTargetControlOffsets.filter((_, index) => {
return index !== targetIndex
})
: [],
// 文本
associativeLineText: newAssociativeLineText
})
}
// 清除当前激活的节点
clearActiveNodes() {
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
}
// 清除激活的线
clearActiveLine() {
if (this.activeLine) {
let [, clickPath, text, node, toNode] = this.activeLine
clickPath.stroke({
color: 'transparent'
})
// 隐藏关联线文本编辑框
this.hideEditTextBox()
// 如果当前关联线没有文字,则清空文字节点
if (!this.getText(node, toNode)) {
text.clear()
}
this.activeLine = null
this.removeControls()
}
}
// 处理节点正在拖拽事件
onNodeDragging() {
if (this.isNodeDragging) return
this.isNodeDragging = true
this.lineList.forEach(line => {
line[0].hide()
line[1].hide()
line[2].hide()
})
this.hideControls()
}
// 处理节点拖拽完成事件
onNodeDragend() {
if (!this.isNodeDragging) return
this.lineList.forEach(line => {
line[0].show()
line[1].show()
line[2].show()
})
this.showControls()
this.isNodeDragging = false
}
}
AssociativeLine.instanceName = 'associativeLine'
export default AssociativeLine

View File

@@ -1,11 +1,9 @@
import { bfsWalk, throttle } from './utils'
import Base from './layouts/Base'
// 节点拖动类
import { bfsWalk, throttle } from '../utils'
import Base from '../layouts/Base'
// 节点拖动插件
class Drag extends Base {
// 构造函数
constructor({ mindMap }) {
super(mindMap.renderer)
this.mindMap = mindMap
@@ -14,7 +12,6 @@ class Drag extends Base {
}
// 复位
reset() {
// 当前拖拽节点
this.node = null
@@ -45,10 +42,11 @@ class Drag extends Base {
this.mouseDownY = 0
this.mouseMoveX = 0
this.mouseMoveY = 0
// 鼠标移动的距离距鼠标按下的位置距离多少以上才认为是拖动事件
this.checkDragOffset = 10
}
// 绑定事件
bindEvent() {
this.checkOverlapNode = throttle(this.checkOverlapNode, 300, this)
this.mindMap.on('node_mousedown', (node, e) => {
@@ -77,13 +75,14 @@ class Drag extends Base {
if (!this.isMousedown) {
return
}
this.mindMap.emit('node_dragging', this.node)
e.preventDefault()
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseMoveX = x
this.mouseMoveY = y
if (
Math.abs(x - this.mouseDownX) <= 10 &&
Math.abs(y - this.mouseDownY) <= 10 &&
Math.abs(x - this.mouseDownX) <= this.checkDragOffset &&
Math.abs(y - this.mouseDownY) <= this.checkDragOffset &&
!this.node.isDrag
) {
return
@@ -97,7 +96,6 @@ class Drag extends Base {
}
// 鼠标松开事件
onMouseup(e) {
if (!this.isMousedown) {
return
@@ -136,10 +134,10 @@ class Drag extends Base {
this.mindMap.render()
}
this.reset()
this.mindMap.emit('node_dragend')
}
// 创建克隆节点
createCloneNode() {
if (!this.clone) {
// 节点
@@ -161,7 +159,6 @@ class Drag extends Base {
}
// 移除克隆节点
removeCloneNode() {
if (!this.clone) {
return
@@ -172,7 +169,6 @@ class Drag extends Base {
}
// 拖动中
onMove(x, y) {
if (!this.isMousedown) {
return
@@ -199,14 +195,12 @@ class Drag extends Base {
}
// 检测重叠节点
checkOverlapNode() {
if (!this.drawTransform) {
return
}
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let checkRight = this.cloneNodeLeft + this.node.width * scaleX
let checkBottom = this.cloneNodeTop + this.node.height * scaleX
let x = this.mouseMoveX
let y = this.mouseMoveY
this.overlapNode = null
this.prevNode = null
this.nextNode = null
@@ -221,35 +215,71 @@ class Drag extends Base {
if (this.overlapNode || (this.prevNode && this.nextNode)) {
return
}
let { left, top, width, height } = node
let _left = left
let _top = top
let _bottom = top + height
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
// 检测是否重叠
if (!this.overlapNode) {
if (
left <= checkRight &&
right >= this.cloneNodeLeft &&
top <= checkBottom &&
bottom >= this.cloneNodeTop
) {
this.overlapNode = node
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]
}
}
// 检测兄弟节点位置
if (!this.prevNode && !this.nextNode && !node.isRoot) {
// && this.node.isBrother(node)
if (left <= checkRight && right >= this.cloneNodeLeft) {
if (this.cloneNodeTop > bottom && this.cloneNodeTop <= bottom + 10) {
// 和前一个兄弟节点的距离
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
this.placeholder.size(node.width, 10).move(_left, _bottom)
} else if (checkBottom < top && checkBottom >= top - 10) {
let size = nextBrotherOffset > 0 ? nextBrotherOffset : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom)
} else if (checkIsNextNode) {
this.nextNode = node
this.placeholder.size(node.width, 10).move(_left, _top - 10)
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
}
}
}
@@ -258,6 +288,32 @@ class Drag extends Base {
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
}
}
// 计算节点的位置尺寸信息
getNodeRect(node) {
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let { left, top, width, height } = node
let originLeft = left
let originTop = top
let originBottom = top + height
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
return {
width,
height,
left,
top,
right,
bottom,
originLeft,
originTop,
originBottom
}
}
}
Drag.instanceName = 'drag'
export default Drag

View File

@@ -1,9 +1,9 @@
import { imgToDataUrl, downloadFile } from './utils'
import JsPDF from 'jspdf'
import { imgToDataUrl, downloadFile, readBlob, removeHTMLEntities } from '../utils'
import { SVG } from '@svgdotjs/svg.js'
const URL = window.URL || window.webkitURL || window
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
import { transformToMarkdown } from '../parse/toMarkdown'
// 导出
// 导出插件
class Export {
// 构造函数
constructor(opt) {
@@ -26,7 +26,11 @@ class Export {
// 获取svg数据
async getSvgData() {
let { svg, svgHTML } = this.mindMap.miniMap.getMiniMap()
let { exportPaddingX, exportPaddingY } = this.mindMap.opt
let { svg, svgHTML } = this.mindMap.getSvgData({
paddingX: exportPaddingX,
paddingY: exportPaddingY
})
// 把图片的url转换成data:url类型否则导出会丢失图片
let imageList = svg.find('image')
let task = imageList.map(async item => {
@@ -35,6 +39,9 @@ class Export {
item.attr('href', imgData)
})
await Promise.all(task)
if (imageList.length > 0) {
svgHTML = svg.svg()
}
return {
node: svg,
str: svgHTML
@@ -42,7 +49,7 @@ class Export {
}
// svg转png
svgToPng(svgSrc) {
svgToPng(svgSrc, transparent) {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
@@ -54,7 +61,9 @@ class Export {
canvas.height = img.height + this.exportPadding * 2
let ctx = canvas.getContext('2d')
// 绘制背景
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
if (!transparent) {
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
}
// 图片绘制到canvas里
ctx.drawImage(
img,
@@ -85,7 +94,9 @@ class Export {
let {
backgroundColor = '#fff',
backgroundImage,
backgroundRepeat = 'repeat'
backgroundRepeat = 'no-repeat',
backgroundPosition = 'center center',
backgroundSize = 'cover',
} = this.mindMap.themeConfig
// 背景颜色
ctx.save()
@@ -96,76 +107,24 @@ class Export {
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
ctx.save()
let img = new Image()
img.src = backgroundImage
img.onload = () => {
let pat = ctx.createPattern(img, backgroundRepeat)
ctx.rect(0, 0, width, height)
ctx.fillStyle = pat
ctx.fill()
drawBackgroundImageToCanvas(ctx, width, height, backgroundImage, {
backgroundRepeat,
backgroundPosition,
backgroundSize
}, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
ctx.restore()
resolve()
}
img.onerror = e => {
reject(e)
}
})
} else {
resolve()
}
})
}
// 导出为png
/**
* 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png() {
let { str } = await this.getSvgData()
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'
})
// 转换成data:url数据
let svgUrl = URL.createObjectURL(blob)
// 绘制到canvas上
let imgDataUrl = await this.svgToPng(svgUrl)
URL.revokeObjectURL(svgUrl)
return imgDataUrl
}
// 导出为pdf
async pdf(name) {
let img = await this.png()
let pdf = new JsPDF('', 'pt', 'a4')
let a4Width = 595
let a4Height = 841
let a4Ratio = a4Width / a4Height
let image = new Image()
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
let imageRatio = imageWidth / imageHeight
let w, h
if (imageWidth <= a4Width && imageHeight <= a4Height) {
// 使用图片原始宽高
w = imageWidth
h = imageHeight
} else if (a4Ratio > imageRatio) {
// 以a4Height为高度缩放图片宽度
w = imageRatio * a4Height
h = a4Height
} else {
// 以a4Width为宽度缩放图片高度
w = a4Width
h = a4Width / imageRatio
}
pdf.addImage(img, 'PNG', (a4Width - w) / 2, (a4Height - h) / 2, w, h)
pdf.save(name)
}
image.src = img
}
// 在svg上绘制思维导图背景
drawBackgroundToSvg(svg) {
return new Promise(async resolve => {
@@ -188,31 +147,101 @@ class Export {
})
}
// 导出为svg
async svg(name) {
let { node } = await this.getSvgData()
node.first().before(SVG(`<title>${name}</title>`))
await this.drawBackgroundToSvg(node)
let str = node.svg()
// 导出为png
/**
* 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png(name, transparent = false) {
let { node, str } = await this.getSvgData()
str = removeHTMLEntities(str)
// 如果开启了富文本则使用htmltocanvas转换为图片
if (this.mindMap.richText) {
let res = await this.mindMap.richText.handleExportPng(node.node)
let imgDataUrl = await this.svgToPng(res, transparent)
return imgDataUrl
}
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'
})
return URL.createObjectURL(blob)
// 转换成data:url数据
let svgUrl = await readBlob(blob)
// 绘制到canvas上
let res = await this.svgToPng(svgUrl, transparent)
return res
}
// 导出为pdf
async pdf(name) {
if (!this.mindMap.doExportPDF) {
throw new Error('请注册ExportPDF插件')
}
let img = await this.png()
this.mindMap.doExportPDF.pdf(name, img)
}
// 导出为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) {
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>`))
}
}
}
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'
})
let res = await readBlob(blob)
return res
}
// 导出为json
json(name, withConfig = true) {
async json(name, withConfig = true) {
let data = this.mindMap.getData(withConfig)
let str = JSON.stringify(data)
let blob = new Blob([str])
return URL.createObjectURL(blob)
let res = await readBlob(blob)
return res
}
// 专有文件其实就是json文件
smm(name, withConfig) {
return this.json(name, withConfig)
async smm(name, withConfig) {
let res = await this.json(name, withConfig)
return res
}
// markdown文件
async md() {
let data = this.mindMap.getData()
let content = transformToMarkdown(data)
let blob = new Blob([content])
let res = await readBlob(blob)
return res
}
}
Export.instanceName = 'doExport'
export default Export

View File

@@ -0,0 +1,44 @@
import JsPDF from 'jspdf'
// 导出PDF插件需要通过Export插件使用
class ExportPDF {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
}
// 导出为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)
}
image.src = img
}
}
ExportPDF.instanceName = 'doExportPDF'
export default ExportPDF

View File

@@ -0,0 +1,19 @@
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
}
}
ExportXMind.instanceName = 'doExportXMind'
export default ExportXMind

View File

@@ -1,29 +1,35 @@
import { isKey } from './utils/keyMap'
import { bfsWalk } from './utils'
import { bfsWalk } from '../utils'
import { CONSTANTS } from '../constants/constant'
// 键盘导航
export default class KeyboardNavigation {
// 键盘导航插件
class KeyboardNavigation {
// 构造函数
constructor(opt) {
this.opt = opt
this.mindMap = opt.mindMap
this.onKeyup = this.onKeyup.bind(this)
this.mindMap.on('keyup', this.onKeyup)
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.LEFT, () => {
this.onKeyup(CONSTANTS.KEY_DIR.LEFT)
})
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.UP, () => {
this.onKeyup(CONSTANTS.KEY_DIR.UP)
})
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.RIGHT, () => {
this.onKeyup(CONSTANTS.KEY_DIR.RIGHT)
})
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.DOWN, () => {
this.onKeyup(CONSTANTS.KEY_DIR.DOWN)
})
}
// 处理按键事件
onKeyup(e) {
;['Left', 'Up', 'Right', 'Down'].forEach(dir => {
if (isKey(e, dir)) {
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.focus(dir)
} else {
let root = this.mindMap.renderer.root
this.mindMap.renderer.moveNodeToCenter(root)
root.active()
}
}
})
onKeyup(dir) {
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.focus(dir)
} else {
let root = this.mindMap.renderer.root
this.mindMap.execCommand('GO_TARGET_NODE', root)
}
}
// 聚焦到下一个节点
@@ -74,8 +80,7 @@ export default class KeyboardNavigation {
// 找到了则让目标节点聚焦
if (targetNode) {
this.mindMap.renderer.moveNodeToCenter(targetNode)
targetNode.active()
this.mindMap.execCommand('GO_TARGET_NODE', targetNode)
}
}
@@ -95,19 +100,19 @@ export default class KeyboardNavigation {
let { left, top, right, bottom } = rect
let match = false
// 按下了左方向键
if (dir === 'Left') {
if (dir === CONSTANTS.KEY_DIR.LEFT) {
// 判断节点是否在当前节点的左侧
match = right <= currentActiveNodeRect.left
// 按下了右方向键
} else if (dir === 'Right') {
} else if (dir === CONSTANTS.KEY_DIR.RIGHT) {
// 判断节点是否在当前节点的右侧
match = left >= currentActiveNodeRect.right
// 按下了上方向键
} else if (dir === 'Up') {
} else if (dir === CONSTANTS.KEY_DIR.UP) {
// 判断节点是否在当前节点的上面
match = bottom <= currentActiveNodeRect.top
// 按下了下方向键
} else if (dir === 'Down') {
} else if (dir === CONSTANTS.KEY_DIR.DOWN) {
// 判断节点是否在当前节点的下面
match = top >= currentActiveNodeRect.bottom
}
@@ -130,22 +135,22 @@ export default class KeyboardNavigation {
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
if (dir === 'Left') {
if (dir === CONSTANTS.KEY_DIR.LEFT) {
match =
left < currentActiveNodeRect.left &&
top < currentActiveNodeRect.bottom &&
bottom > currentActiveNodeRect.top
} else if (dir === 'Right') {
} else if (dir === CONSTANTS.KEY_DIR.RIGHT) {
match =
right > currentActiveNodeRect.right &&
top < currentActiveNodeRect.bottom &&
bottom > currentActiveNodeRect.top
} else if (dir === 'Up') {
} else if (dir === CONSTANTS.KEY_DIR.UP) {
match =
top < currentActiveNodeRect.top &&
left < currentActiveNodeRect.right &&
right > currentActiveNodeRect.left
} else if (dir === 'Down') {
} else if (dir === CONSTANTS.KEY_DIR.DOWN) {
match =
bottom > currentActiveNodeRect.bottom &&
left < currentActiveNodeRect.right &&
@@ -179,13 +184,13 @@ export default class KeyboardNavigation {
let offsetY = ccY - cY
if (offsetX === 0 && offsetY === 0) return
let match = false
if (dir === 'Left') {
if (dir === CONSTANTS.KEY_DIR.LEFT) {
match = offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY
} else if (dir === 'Right') {
} else if (dir === CONSTANTS.KEY_DIR.RIGHT) {
match = offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY
} else if (dir === 'Up') {
} else if (dir === CONSTANTS.KEY_DIR.UP) {
match = offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX
} else if (dir === 'Down') {
} else if (dir === CONSTANTS.KEY_DIR.DOWN) {
match = offsetY > 0 && -offsetY < offsetX && offsetY > offsetX
}
if (match) {
@@ -224,3 +229,7 @@ export default class KeyboardNavigation {
}
}
}
KeyboardNavigation.instanceName = 'keyboardNavigation'
export default KeyboardNavigation

View File

@@ -1,4 +1,6 @@
// 小地图类
import { isWhite, isTransparent, getVisibleColorFromTheme } from '../utils/index'
// 小地图插件
class MiniMap {
// 构造函数
constructor(opt) {
@@ -14,51 +16,14 @@ class MiniMap {
}
}
// 获取小地图相关数据
getMiniMap() {
const svg = this.mindMap.svg
const draw = this.mindMap.draw
// 保存原始信息
const origWidth = svg.width()
const origHeight = svg.height()
const origTransform = draw.transform()
const elRect = this.mindMap.el.getBoundingClientRect()
// 去除放大缩小的变换效果
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
// 克隆一份数据
const clone = svg.clone()
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight)
draw.transform(origTransform)
return {
svg: clone, // 思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组
svgHTML: clone.svg(), // svg字符串
rect: {
...rect, // 思维导图图形未缩放时的位置尺寸等信息
ratio: rect.width / rect.height // 思维导图图形的宽高比
},
origWidth, // 画布宽度
origHeight, // 画布高度
scaleX: origTransform.scaleX, // 思维导图图形的水平缩放值
scaleY: origTransform.scaleY // 思维导图图形的垂直缩放值
}
}
// 计算小地图的渲染数据
/**
* boxWidth小地图容器的宽度
* boxHeight小地图容器的高度
*/
calculationMiniMap(boxWidth, boxHeight) {
let { svgHTML, rect, origWidth, origHeight, scaleX, scaleY } =
this.getMiniMap()
let { svg, rect, origWidth, origHeight, scaleX, scaleY } =
this.mindMap.getSvgData()
// 计算数据
let boxRatio = boxWidth / boxHeight
let actWidth = 0
@@ -102,8 +67,10 @@ class MiniMap {
Math.max(0, ((_rectY2 - origHeight) / _rectHeight) * actHeight) +
miniMapBoxTop +
'px'
this.removeNodeContent(svg)
return {
svgHTML, // 小地图html
svgHTML: svg.svg(), // 小地图html
viewBoxStyle, // 视图框的位置信息
miniMapBoxScale, // 视图框的缩放值
miniMapBoxLeft, // 视图框的left值
@@ -111,6 +78,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
@@ -144,4 +131,6 @@ class MiniMap {
}
}
MiniMap.instanceName = 'miniMap'
export default MiniMap

View File

@@ -0,0 +1,226 @@
// 节点图片大小调整插件
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 === node && 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()
document.body.appendChild(this.handleEl)
this.isShowHandleEl = true
}
// 隐藏自定义元素
hideHandleEl() {
if (!this.isShowHandleEl) return
this.isShowHandleEl = false
document.body.removeChild(this.handleEl)
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;
background-size: cover;
`
// 调整按钮元素
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;
`
this.handleEl.appendChild(btnEl)
// 给按钮元素绑定事件
btnEl.addEventListener('mouseenter', () => {
// 移入按钮,会触发节点图片的移出事件,所以需要再次显示按钮
this.showHandleEl()
})
btnEl.addEventListener('mouseleave', () => {
// 移除按钮,需要隐藏按钮
if (this.isMousedown) return
this.hideHandleEl()
})
btnEl.addEventListener('mousedown', e => {
this.onMousedown(e)
})
}
// 鼠标按钮按下事件
onMousedown() {
this.isMousedown = true
// 隐藏节点实际图片
this.hideNodeImage()
// 将节点图片渲染到自定义元素上
this.handleEl.style.backgroundImage = `url(${this.node.nodeData.data.image})`
}
// 鼠标移动
onMousemove(e) {
if (!this.isMousedown) return
e.preventDefault()
// 计算当前拖拽位置对应的图片的实时大小
let { width: imageOriginWidth, height: imageOriginHeight } =
this.node.nodeData.data.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.nodeData.data
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()
}
}
NodeImgAdjust.instanceName = 'nodeImgAdjust'
export default NodeImgAdjust

View File

@@ -0,0 +1,557 @@
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
import html2canvas from 'html2canvas'
import { walk, getTextFromHtml, isWhite, getVisibleColorFromTheme } from '../utils'
import { CONSTANTS } from '../constants/constant'
let extended = false
// 扩展quill的字体列表
let fontFamilyList = [
'宋体, SimSun, Songti SC',
'微软雅黑, Microsoft YaHei',
'楷体, 楷体_GB2312, SimKai, STKaiti',
'黑体, SimHei, Heiti SC',
'隶书, SimLi',
'andale mono',
'arial, helvetica, sans-serif',
'arial black, avant garde',
'comic sans ms',
'impact, chicago',
'times new roman',
'sans-serif',
'serif'
]
// 扩展quill的字号列表
let fontSizeList = new Array(100).fill(0).map((_, index) => {
return index + 'px'
})
// 富文本编辑插件
class RichText {
constructor({ mindMap, pluginOpt }) {
this.mindMap = mindMap
this.pluginOpt = pluginOpt
this.textEditNode = null
this.showTextEdit = false
this.quill = null
this.range = null
this.lastRange = null
this.node = null
this.isInserting = false
this.styleEl = null
this.cacheEditingText = ''
this.lostStyle = false
this.isCompositing = false
this.initOpt()
this.extendQuill()
this.appendCss()
this.bindEvent()
// 处理数据,转成富文本格式
if (this.mindMap.opt.data) {
this.mindMap.opt.data = this.handleSetData(this.mindMap.opt.data)
}
}
// 绑定事件
bindEvent() {
this.onCompositionStart = this.onCompositionStart.bind(this)
this.onCompositionEnd = this.onCompositionEnd.bind(this)
window.addEventListener('compositionstart', this.onCompositionStart)
window.addEventListener('compositionend', this.onCompositionEnd)
}
// 解绑事件
unbindEvent() {
window.removeEventListener('compositionstart', this.onCompositionStart)
window.removeEventListener('compositionend', this.onCompositionEnd)
}
// 插入样式
appendCss() {
let cssText = `
.ql-editor {
overflow: hidden;
padding: 0;
height: auto;
line-height: normal;
-webkit-user-select: text;
}
.ql-container {
height: auto;
font-size: inherit;
}
.ql-container.ql-snow {
border: none;
}
.smm-richtext-node-wrap p {
font-family: auto;
}
.smm-richtext-node-edit-wrap p {
font-family: auto;
}
`
this.styleEl = document.createElement('style')
this.styleEl.type = 'text/css'
this.styleEl.innerHTML = cssText
document.head.appendChild(this.styleEl)
}
// 处理选项参数
initOpt() {
if (
this.pluginOpt.fontFamilyList &&
Array.isArray(this.pluginOpt.fontFamilyList)
) {
fontFamilyList = this.pluginOpt.fontFamilyList
}
if (
this.pluginOpt.fontSizeList &&
Array.isArray(this.pluginOpt.fontSizeList)
) {
fontSizeList = this.pluginOpt.fontSizeList
}
}
// 扩展quill编辑器
extendQuill() {
if (extended) {
return
}
extended = true
// 扩展quill的字体列表
const FontAttributor = Quill.import('attributors/class/font')
FontAttributor.whitelist = fontFamilyList
Quill.register(FontAttributor, true)
const FontStyle = Quill.import('attributors/style/font')
FontStyle.whitelist = fontFamilyList
Quill.register(FontStyle, true)
// 扩展quill的字号列表
const SizeAttributor = Quill.import('attributors/class/size')
SizeAttributor.whitelist = fontSizeList
Quill.register(SizeAttributor, true)
const SizeStyle = Quill.import('attributors/style/size')
SizeStyle.whitelist = fontSizeList
Quill.register(SizeStyle, true)
}
// 显示文本编辑控件
showEditText(node, rect, isInserting) {
if (this.showTextEdit) {
return
}
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()
// 原始宽高
let g = node._textData.node
let originWidth = g.attr('data-width')
let originHeight = g.attr('data-height')
// 缩放值
let scaleX = rect.width / originWidth
let scaleY = rect.height / originHeight
// 内边距
const paddingX = 6
const paddingY = 4
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.addEventListener('click', e => {
e.stopPropagation()
})
document.body.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.backgroundColor =
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.transform = `scale(${scaleX}, ${scaleY})`
this.textEditNode.style.transformOrigin = 'left top'
if (!node.nodeData.data.richText) {
// 还不是富文本的情况
let text = node.nodeData.data.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.initQuillEditor()
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
this.showTextEdit = true
// 如果是刚创建的节点,那么默认全选,否则普通激活不全选
this.focus(isInserting ? 0 : null)
if (!node.nodeData.data.richText) {
// 如果是非富文本的情况,需要手动应用文本样式
this.setTextStyleIfNotRichText(node)
}
this.cacheEditingText = ''
}
// 如果是非富文本的情况,需要手动应用文本样式
setTextStyleIfNotRichText(node) {
let style = {
font: node.style.merge('fontFamily'),
color: node.style.merge('color'),
italic: node.style.merge('fontStyle') === 'italic',
bold: node.style.merge('fontWeight') === 'bold',
size: node.style.merge('fontSize') + 'px',
underline: node.style.merge('textDecoration') === 'underline',
strike: node.style.merge('textDecoration') === 'line-through'
}
this.pureFormatAllText(style)
}
// 获取当前正在编辑的内容
getEditText() {
let html = this.quill.container.firstChild.innerHTML
// 去除最后的空行
return html.replace(/<p><br><\/p>$/, '')
}
// 隐藏文本编辑控件,即完成编辑
hideEditText(nodes) {
if (!this.showTextEdit) {
return
}
let html = this.getEditText()
let list =
nodes && nodes.length > 0 ? nodes : this.mindMap.renderer.activeNodeList
list.forEach(node => {
this.mindMap.execCommand('SET_NODE_TEXT', node, html, true)
if (node.isGeneralization) {
// 概要节点
node.generalizationBelongNode.updateGeneralization()
}
this.mindMap.render()
})
this.mindMap.emit('hide_text_edit', this.textEditNode, list)
this.textEditNode.style.display = 'none'
this.showTextEdit = false
this.mindMap.emit('rich_text_selection_change', false)
this.node = null
this.isInserting = false
}
// 初始化Quill富文本编辑器
initQuillEditor() {
this.quill = new Quill(this.textEditNode, {
modules: {
toolbar: false,
keyboard: {
bindings: {
enter: {
key: 13,
handler: function () {
// 覆盖默认的回车键换行
}
},
tab: {
key: 9,
handler: function () {
// 覆盖默认的tab键
}
}
}
}
},
theme: 'snow'
})
this.quill.on('selection-change', range => {
// 刚创建的节点全选不需要显示操作条
if (this.isInserting) return
this.lastRange = this.range
this.range = null
if (range) {
let bounds = this.quill.getBounds(range.index, range.length)
let rect = this.textEditNode.getBoundingClientRect()
let rectInfo = {
left: bounds.left + rect.left,
top: bounds.top + rect.top,
right: bounds.right + rect.left,
bottom: bounds.bottom + rect.top,
width: bounds.width
}
let formatInfo = this.quill.getFormat(range.index, range.length)
let hasRange = false
if (range.length == 0) {
hasRange = false
} else {
this.range = range
hasRange = true
}
this.mindMap.emit(
'rich_text_selection_change',
hasRange,
rectInfo,
formatInfo
)
}
})
this.quill.on('text-change', () => {
let contents = this.quill.getContents()
let len = contents.ops.length
// 如果编辑过程中删除所有字符,那么会丢失主题的样式
if (len <= 0 || (len === 1 && contents.ops[0].insert === '\n')) {
this.lostStyle = true
// 需要删除节点的样式数据
this.syncFormatToNodeConfig(null, true)
} else if (this.lostStyle && !this.isCompositing) {
// 如果处于样式丢失状态,那么需要进行格式化加回样式
this.setTextStyleIfNotRichText(this.node)
this.lostStyle = false
}
})
}
// 正则输入中文
onCompositionStart() {
if (!this.showTextEdit) {
return
}
this.isCompositing = true
}
// 中文输入结束
onCompositionEnd() {
if (!this.showTextEdit || !this.lostStyle) {
return
}
this.isCompositing = false
this.setTextStyleIfNotRichText(this.node)
}
// 选中全部
selectAll() {
this.quill.setSelection(0, this.quill.getLength())
}
// 聚焦
focus(start) {
let len = this.quill.getLength()
this.quill.setSelection(typeof start === 'number' ? start : len, len)
}
// 格式化当前选中的文本
formatText(config = {}, clear = false) {
if (!this.range && !this.lastRange) return
this.syncFormatToNodeConfig(config, clear)
let rangeLost = !this.range
let range = rangeLost ? this.lastRange : this.range
clear
? this.quill.removeFormat(range.index, range.length)
: this.quill.formatText(range.index, range.length, config)
if (rangeLost) {
this.quill.setSelection(this.lastRange.index, this.lastRange.length)
}
}
// 清除当前选中文本的样式
removeFormat() {
this.formatText({}, true)
}
// 格式化指定范围的文本
formatRangeText(range, config = {}) {
if (!range) return
this.syncFormatToNodeConfig(config)
this.quill.formatText(range.index, range.length, config)
}
// 格式化所有文本
formatAllText(config = {}) {
this.syncFormatToNodeConfig(config)
this.pureFormatAllText(config)
}
// 纯粹的格式化所有文本
pureFormatAllText(config = {}) {
this.quill.formatText(0, this.quill.getLength(), config)
}
// 同步格式化到节点样式配置
syncFormatToNodeConfig(config, clear) {
if (!this.node) return
if (clear) {
// 清除文本样式
;[
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
].forEach(prop => {
delete this.node.nodeData.data[prop]
})
} else {
let data = this.richTextStyleToNormalStyle(config)
this.mindMap.renderer.setNodeData(this.node, data)
}
}
// 将普通节点样式对象转换成富文本样式对象
normalStyleToRichTextStyle(style) {
let config = {}
Object.keys(style).forEach(prop => {
let value = style[prop]
switch (prop) {
case 'fontFamily':
config.font = value
break
case 'fontSize':
config.size = value + 'px'
break
case 'fontWeight':
config.bold = value === 'bold'
break
case 'fontStyle':
config.italic = value === 'italic'
break
case 'textDecoration':
config.underline = value === 'underline'
config.strike = value === 'line-through'
break
case 'color':
config.color = value
break
default:
break
}
})
return config
}
// 将富文本样式对象转换成普通节点样式对象
richTextStyleToNormalStyle(config) {
let data = {}
Object.keys(config).forEach(prop => {
let value = config[prop]
switch (prop) {
case 'font':
data.fontFamily = value
break
case 'size':
data.fontSize = parseFloat(value)
break
case 'bold':
data.fontWeight = value ? 'bold' : 'normal'
break
case 'italic':
data.fontStyle = value ? 'italic' : 'normal'
break
case 'underline':
data.textDecoration = value ? 'underline' : 'none'
break
case 'strike':
data.textDecoration = value ? 'line-through' : 'none'
break
case 'color':
data.color = value
break
default:
break
}
})
return data
}
// 处理导出为图片
async handleExportPng(node) {
let el = document.createElement('div')
el.style.position = 'absolute'
el.style.left = '-9999999px'
el.appendChild(node)
this.mindMap.el.appendChild(el)
// 遍历所有节点将它们的margin和padding设为0
let walk = root => {
root.style.margin = 0
root.style.padding = 0
if (root.hasChildNodes()) {
Array.from(root.children).forEach(item => {
walk(item)
})
}
}
walk(node)
let canvas = await html2canvas(el, {
backgroundColor: null
})
this.mindMap.el.removeChild(el)
return canvas.toDataURL()
}
// 将所有节点转换成非富文本节点
transformAllNodesToNormalNode() {
walk(
this.mindMap.renderer.renderTree,
null,
node => {
if (node.data.richText) {
node.data.richText = false
node.data.text = getTextFromHtml(node.data.text)
// delete node.data.uid
}
},
null,
true,
0,
0
)
// 清空历史数据,并且触发数据变化
this.mindMap.command.clearHistory()
this.mindMap.command.addHistory()
this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE)
}
// 处理导入数据
handleSetData(data) {
let walk = root => {
if (!root.data.richText) {
root.data.richText = true
root.data.resetRichText = true
}
if (root.children && root.children.length > 0) {
Array.from(root.children).forEach(item => {
walk(item)
})
}
}
walk(data)
return data
}
// 插件被移除前做的事情
beforePluginRemove() {
this.transformAllNodesToNormalNode()
document.head.removeChild(this.styleEl)
}
}
RichText.instanceName = 'richText'
export default RichText

View File

@@ -0,0 +1,160 @@
import { bfsWalk, getTextFromHtml, isUndef } from '../utils/index'
// 搜索插件
class Search {
// 构造函数
constructor({ mindMap }) {
this.mindMap = mindMap
// 是否正在搜索
this.isSearching = false
// 搜索文本
this.searchText = ''
// 匹配的节点列表
this.matchNodeList = []
// 当前所在的节点列表索引
this.currentIndex = -1
// 不要复位搜索文本
this.notResetSearchText = false
this.onDataChange = this.onDataChange.bind(this)
this.mindMap.on('data_change', this.onDataChange)
}
// 节点数据改变了,需要重新搜索
onDataChange() {
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.nodeData.data
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) {
if (
isUndef(replaceText) ||
!this.isSearching ||
this.matchNodeList.length <= 0
)
return
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.nodeData.data.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 (
isUndef(replaceText) ||
!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.nodeData.data.richText
},
true
)
})
this.mindMap.render()
this.mindMap.command.addHistory()
this.endSearch()
}
// 获取某个节点替换后的文本
getReplacedText(node, searchText, replaceText) {
let { richText, text } = node.nodeData.data
if (richText) {
text = getTextFromHtml(text)
}
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

View File

@@ -1,7 +1,6 @@
import { bfsWalk, throttle } from './utils'
// 选择节点类
import { bfsWalk, throttle } from '../utils'
// 节点选择插件
class Select {
// 构造函数
constructor({ mindMap }) {
@@ -17,14 +16,19 @@ class Select {
// 绑定事件
bindEvent() {
this.checkInNodes = throttle(this.checkInNodes, 500, this)
this.checkInNodes = throttle(this.checkInNodes, 300, this)
this.mindMap.on('mousedown', e => {
if (this.mindMap.opt.readonly) {
return
}
if (!e.ctrlKey && e.which !== 3) {
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
if (
!e.ctrlKey &&
(useLeftKeySelectionRightKeyDrag ? e.which !== 1 : e.which !== 3)
) {
return
}
e.preventDefault()
this.isMousedown = true
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseDownX = x
@@ -146,25 +150,30 @@ class Select {
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
if (left >= minx && right <= maxx && top >= miny && bottom <= maxy) {
this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, true)
this.mindMap.renderer.addActiveNode(node)
})
if (
((left >= minx && left <= maxx) || (right >= minx && right <= maxx)) &&
((top >= miny && top <= maxy) || (bottom >= miny && bottom <= maxy))
) {
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, true)
this.mindMap.renderer.addActiveNode(node)
// })
} else if (node.nodeData.data.isActive) {
this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (!node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, false)
this.mindMap.renderer.removeActiveNode(node)
})
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (!node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, false)
this.mindMap.renderer.removeActiveNode(node)
// })
}
})
}
}
Select.instanceName = 'select'
export default Select

View File

@@ -0,0 +1,126 @@
// 手势事件支持插件
class TouchEvent {
// 构造函数
constructor({ mindMap }) {
this.mindMap = mindMap
this.touchesNum = 0
this.singleTouchstartEvent = null
this.clickNum = 0
this.doubleTouchmoveDistance = 0
this.bindEvent()
}
// 绑定事件
bindEvent() {
this.onTouchstart = this.onTouchstart.bind(this)
this.onTouchmove = this.onTouchmove.bind(this)
this.onTouchcancel = this.onTouchcancel.bind(this)
this.onTouchend = this.onTouchend.bind(this)
window.addEventListener('touchstart', this.onTouchstart)
window.addEventListener('touchmove', this.onTouchmove)
window.addEventListener('touchcancel', this.onTouchcancel)
window.addEventListener('touchend', this.onTouchend)
}
// 解绑事件
unBindEvent() {
window.removeEventListener('touchstart', this.onTouchstart)
window.removeEventListener('touchmove', this.onTouchmove)
window.removeEventListener('touchcancel', this.onTouchcancel)
window.removeEventListener('touchend', this.onTouchend)
}
// 手指按下事件
onTouchstart(e) {
this.touchesNum = e.touches.length
if (this.touchesNum === 1) {
let touch = e.touches[0]
this.singleTouchstartEvent = touch
this.dispatchMouseEvent('mousedown', touch.target, touch)
}
}
// 手指移动事件
onTouchmove(e) {
let len = e.touches.length
if (len === 1) {
let touch = e.touches[0]
this.dispatchMouseEvent('mousemove', touch.target, touch)
} else if (len === 2) {
let touch1 = e.touches[0]
let touch2 = e.touches[1]
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)
let { x: touch2ClientX, y: touch2ClientY } = this.mindMap.toPos(touch2.clientX, touch2.clientY)
let cx = (touch1ClientX + touch2ClientX) / 2
let cy = (touch1ClientY + touch2ClientY) / 2
if (distance > this.doubleTouchmoveDistance) {
// 放大
this.mindMap.view.enlarge(cx, cy)
} else {
// 缩小
this.mindMap.view.narrow(cx, cy)
}
this.doubleTouchmoveDistance = distance
}
}
// 手指取消事件
onTouchcancel(e) {}
// 手指松开事件
onTouchend(e) {
this.dispatchMouseEvent('mouseup', e.target)
if (this.touchesNum === 1) {
// 模拟双击事件
this.clickNum++
setTimeout(() => {
this.clickNum = 0
}, 300)
let ev = this.singleTouchstartEvent
if (this.clickNum > 1) {
this.clickNum = 0
this.dispatchMouseEvent('dblclick', ev.target, ev)
} else {
// 点击事件应该不用模拟
// this.dispatchMouseEvent('click', ev.target, ev)
}
}
this.touchesNum = 0
this.singleTouchstartEvent = null
this.doubleTouchmoveDistance = 0
}
// 发送鼠标事件
dispatchMouseEvent(eventName, target, e) {
let opt = {}
if (e) {
opt = {
screenX: e.screenX,
screenY: e.screenY,
clientX: e.clientX,
clientY: e.clientY,
which: 1
}
}
let event = new MouseEvent(eventName, {
view: window,
bubbles: true,
cancelable: true,
...opt
})
target.dispatchEvent(event)
}
// 插件被移除前做的事情
beforePluginRemove() {
this.unBindEvent()
}
}
TouchEvent.instanceName = 'touchEvent'
export default TouchEvent

View File

@@ -1,9 +1,9 @@
import { Text, G } from '@svgdotjs/svg.js'
import { degToRad, camelCaseToHyphen } from './utils'
import { degToRad, camelCaseToHyphen } from '../utils'
import merge from 'deepmerge'
// 水印
export default class Watermark {
// 水印插件
class Watermark {
constructor(opt = {}) {
this.mindMap = opt.mindMap
this.lineSpacing = 0 // 水印行间距
@@ -20,6 +20,11 @@ export default class Watermark {
this.updateWatermark(this.mindMap.opt.watermarkConfig || {})
}
// 获取是否存在水印
hasWatermark() {
return !!this.text.trim()
}
// 处理水印配置
handleConfig({ text, lineSpacing, textSpacing, angle, textStyle }) {
this.text = text === undefined ? '' : String(text).trim()
@@ -36,7 +41,7 @@ export default class Watermark {
// 非精确绘制,会绘制一些超出可视区域的水印
draw() {
this.watermarkDraw.clear()
if (!this.text.trim()) {
if (!this.hasWatermark()) {
return
}
let x = 0
@@ -109,3 +114,7 @@ export default class Watermark {
this.draw()
}
}
Watermark.instanceName = 'watermark'
export default Watermark

View File

@@ -0,0 +1,241 @@
import {
getAssociativeLineTargetIndex,
joinCubicBezierPath,
computeNodePoints,
getDefaultControlPointOffsets
} from './associativeLineUtils'
// 创建控制点、连线节点
function createControlNodes() {
let { associativeLineActiveColor } = this.mindMap.themeConfig
// 连线
this.controlLine1 = this.draw
.line()
.stroke({ color: associativeLineActiveColor, width: 2 })
this.controlLine2 = this.draw
.line()
.stroke({ color: associativeLineActiveColor, width: 2 })
// 控制点
this.controlPoint1 = this.createOneControlNode('controlPoint1')
this.controlPoint2 = this.createOneControlNode('controlPoint2')
}
// 创建控制点
function createOneControlNode(pointKey) {
let { associativeLineActiveColor } = this.mindMap.themeConfig
return this.draw
.circle(this.controlPointDiameter)
.stroke({ color: associativeLineActiveColor })
.fill({ color: '#fff' })
.click(e => {
e.stopPropagation()
})
.mousedown(e => {
this.onControlPointMousedown(e, pointKey)
})
}
// 控制点的鼠标按下事件
function onControlPointMousedown(e, pointKey) {
e.stopPropagation()
this.isControlPointMousedown = true
this.mousedownControlPointKey = pointKey
}
// 控制点的鼠标移动事件
function onControlPointMousemove(e) {
if (
!this.isControlPointMousedown ||
!this.mousedownControlPointKey ||
!this[this.mousedownControlPointKey]
)
return
e.stopPropagation()
e.preventDefault()
let radius = this.controlPointDiameter / 2
// 转换鼠标当前的位置
let { x, y } = this.getTransformedEventPos(e)
this.controlPointMousemoveState.pos = {
x,
y
}
// 更新当前拖拽的控制点的位置
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
let [path, clickPath, text, node, toNode] = this.activeLine
let [startPoint, endPoint] = computeNodePoints(node, toNode)
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)
} else {
offsets = associativeLineTargetControlOffsets[targetIndex]
}
let point1 = null
let point2 = null
// 拖拽的是控制点1
if (this.mousedownControlPointKey === 'controlPoint1') {
point1 = {
x,
y
}
point2 = {
x: endPoint.x + offsets[1].x,
y: endPoint.y + offsets[1].y
}
// 更新控制点1的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
} else {
// 拖拽的是控制点2
point1 = {
x: startPoint.x + offsets[0].x,
y: startPoint.y + offsets[0].y
}
point2 = {
x,
y
}
// 更新控制点2的连线
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
}
// 更新关联线
let 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()
e.preventDefault()
let { pos, startPoint, endPoint, targetIndex } =
this.controlPointMousemoveState
let [, , , node] = this.activeLine
let offsetList = []
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
if (!associativeLineTargetControlOffsets) {
// 兼容0.4.5版本没有associativeLineTargetControlOffsets的情况
offsetList[targetIndex] = getDefaultControlPointOffsets(
startPoint,
endPoint
)
} else {
offsetList = associativeLineTargetControlOffsets
}
let offset1 = null
let offset2 = null
if (this.mousedownControlPointKey === 'controlPoint1') {
// 更新控制点1数据
offset1 = {
x: pos.x - startPoint.x,
y: pos.y - startPoint.y
}
offset2 = offsetList[targetIndex][1]
} else {
// 更新控制点2数据
offset1 = offsetList[targetIndex][0]
offset2 = {
x: pos.x - endPoint.x,
y: pos.y - endPoint.y
}
}
offsetList[targetIndex] = [offset1, offset2]
this.mindMap.execCommand('SET_NODE_DATA', node, {
associativeLineTargetControlOffsets: offsetList
})
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚所以重置isControlPointMousedown需要等draw_click事件触发完以后
setTimeout(() => {
this.resetControlPoint()
}, 0)
}
// 复位控制点移动
function resetControlPoint() {
this.isControlPointMousedown = false
this.mousedownControlPointKey = ''
this.controlPointMousemoveState = {
pos: null,
startPoint: null,
endPoint: null,
targetIndex: ''
}
}
// 渲染控制点
function renderControls(startPoint, endPoint, point1, point2) {
if (!this.controlLine1) {
this.createControlNodes()
}
let radius = this.controlPointDiameter / 2
// 控制点和起终点的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
// 控制点
this.controlPoint1.x(point1.x - radius).y(point1.y - radius)
this.controlPoint2.x(point2.x - radius).y(point2.y - radius)
}
// 删除控制点
function removeControls() {
if (!this.controlLine1) return
;[
this.controlLine1,
this.controlLine2,
this.controlPoint1,
this.controlPoint2
].forEach(item => {
item.remove()
})
this.controlLine1 = null
this.controlLine2 = null
this.controlPoint1 = null
this.controlPoint2 = null
}
// 隐藏控制点
function hideControls() {
if (!this.controlLine1) return
;[
this.controlLine1,
this.controlLine2,
this.controlPoint1,
this.controlPoint2
].forEach(item => {
item.hide()
})
}
// 显示控制点
function showControls() {
if (!this.controlLine1) return
;[
this.controlLine1,
this.controlLine2,
this.controlPoint1,
this.controlPoint2
].forEach(item => {
item.show()
})
}
export default {
createControlNodes,
createOneControlNode,
onControlPointMousedown,
onControlPointMousemove,
onControlPointMouseup,
resetControlPoint,
renderControls,
removeControls,
hideControls,
showControls
}

View File

@@ -0,0 +1,167 @@
import { Text } from '@svgdotjs/svg.js'
import { getStrWithBrFromHtml } from '../../utils/index'
// 创建文字节点
function createText(data) {
let g = this.draw.group()
const setActive = () => {
if (
!this.activeLine ||
this.activeLine[3] !== data.node ||
this.activeLine[4] !== data.toNode
) {
this.setActiveLine({
...data,
text: g
})
}
}
g.click(e => {
e.stopPropagation()
setActive()
})
g.on('dblclick', e => {
e.stopPropagation()
setActive()
if (!this.activeLine) return
this.showEditTextBox(g)
})
return g
}
// 显示文本编辑框
function showEditTextBox(g) {
this.mindMap.emit('before_show_text_edit')
// 注册回车快捷键
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;`
this.textEditNode.setAttribute('contenteditable', true)
this.textEditNode.addEventListener('keyup', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
document.body.appendChild(this.textEditNode)
}
let {
associativeLineTextFontSize,
associativeLineTextFontFamily,
associativeLineTextLineHeight
} = this.mindMap.themeConfig
let scale = this.mindMap.view.scale
let [, , , node, toNode] = this.activeLine
let textLines = (
this.getText(node, toNode) || this.mindMap.opt.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.innerHTML = textLines.join('<br>')
this.textEditNode.style.display = 'block'
this.updateTextEditBoxPos(g)
this.showTextEdit = true
}
// 处理画布缩放
function onScale() {
this.hideEditTextBox()
}
// 更新文本编辑框位置
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'
}
// 隐藏文本编辑框
function hideEditTextBox() {
if (!this.showTextEdit) {
return
}
let [path, , text, node, toNode] = this.activeLine
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
this.mindMap.execCommand('SET_NODE_DATA', node, {
associativeLineText: {
...(node.nodeData.data.associativeLineText || {}),
[toNode.nodeData.data.id]: str
}
})
this.textEditNode.style.display = 'none'
this.textEditNode.innerHTML = ''
this.showTextEdit = false
this.renderText(str, path, text)
this.mindMap.emit('hide_text_edit')
}
// 获取某根关联线的文字
function getText(node, toNode) {
let obj = node.nodeData.data.associativeLineText
if (!obj) {
return ''
}
return obj[toNode.nodeData.data.id] || ''
}
// 渲染关联线文字
function renderText(str, path, text) {
if (!str) return
let { associativeLineTextFontSize, associativeLineTextLineHeight } =
this.mindMap.themeConfig
text.clear()
let textArr = str.split(/\n/gim)
textArr.forEach((item, index) => {
let node = new Text().text(item)
node.y(associativeLineTextFontSize * associativeLineTextLineHeight * index)
this.styleText(node)
text.add(node)
})
updateTextPos(path, text)
}
// 给文本设置样式
function styleText(node) {
let {
associativeLineTextColor,
associativeLineTextFontSize,
associativeLineTextFontFamily
} = this.mindMap.themeConfig
node
.fill({
color: associativeLineTextColor
})
.css({
'font-family': associativeLineTextFontFamily,
'font-size': associativeLineTextFontSize
})
}
// 更新关联线文字位置
function updateTextPos(path, text) {
let pathLength = path.length()
let centerPoint = path.pointAt(pathLength / 2)
let { width: textWidth, height: textHeight } = text.bbox()
text.x(centerPoint.x - textWidth / 2)
text.y(centerPoint.y - textHeight / 2)
}
export default {
getText,
createText,
styleText,
onScale,
showEditTextBox,
hideEditTextBox,
updateTextEditBoxPos,
renderText,
updateTextPos
}

View File

@@ -0,0 +1,182 @@
// 获取目标节点在起始节点的目标数组中的索引
export const getAssociativeLineTargetIndex = (node, toNode) => {
return node.nodeData.data.associativeLineTargets.findIndex(item => {
return item === toNode.nodeData.data.id
})
}
// 计算贝塞尔曲线的控制点
export const computeCubicBezierPathPoints = (x1, y1, x2, y2) => {
let cx1 = x1 + (x2 - x1) / 2
let cy1 = y1
let cx2 = cx1
let cy2 = y2
if (Math.abs(x1 - x2) <= 5) {
cx1 = x1 + (y2 - y1) / 2
cx2 = cx1
}
return [
{
x: cx1,
y: cy1
},
{
x: cx2,
y: cy2
}
]
}
// 拼接贝塞尔曲线路径
export const joinCubicBezierPath = (startPoint, endPoint, point1, point2) => {
return `M ${startPoint.x},${startPoint.y} C ${point1.x},${point1.y} ${point2.x},${point2.y} ${endPoint.x},${endPoint.y}`
}
// 获取节点的位置信息
const getNodeRect = node => {
let { left, top, width, height } = node
return {
right: left + width,
bottom: top + height,
left,
top
}
}
// 三次贝塞尔曲线
export const cubicBezierPath = (x1, y1, x2, y2) => {
let points = computeCubicBezierPathPoints(x1, y1, x2, y2)
return joinCubicBezierPath(
{ x: x1, y: y1 },
{ x: x2, y: y2 },
points[0],
points[1]
)
}
// 获取节点的连接点
export const getNodePoint = (node, dir = 'right') => {
let { left, top, width, height } = node
switch (dir) {
case 'left':
return {
x: left,
y: top + height / 2
}
case 'right':
return {
x: left + width,
y: top + height / 2
}
case 'top':
return {
x: left + width / 2,
y: top
}
case 'bottom':
return {
x: left + width / 2,
y: top + height
}
default:
break
}
}
// 根据两个节点的位置计算节点的连接点
export const computeNodePoints = (fromNode, toNode) => {
let fromRect = getNodeRect(fromNode)
let fromCx = (fromRect.right + fromRect.left) / 2
let fromCy = (fromRect.bottom + fromRect.top) / 2
let toRect = getNodeRect(toNode)
let toCx = (toRect.right + toRect.left) / 2
let toCy = (toRect.bottom + toRect.top) / 2
// 中心点坐标的差值
let offsetX = toCx - fromCx
let offsetY = toCy - fromCy
if (offsetX === 0 && offsetY === 0) return
let fromDir = ''
let toDir = ''
if (offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY) {
// left
fromDir = 'left'
toDir = 'right'
} else if (offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY) {
// right
fromDir = 'right'
toDir = 'left'
} else if (offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX) {
// up
fromDir = 'top'
toDir = 'bottom'
} else if (offsetY > 0 && -offsetY < offsetX && offsetY > offsetX) {
// down
fromDir = 'bottom'
toDir = 'top'
}
return [getNodePoint(fromNode, fromDir), getNodePoint(toNode, toDir)]
}
// 获取节点的关联线路径
export const getNodeLinePath = (startPoint, endPoint, node, toNode) => {
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
// 控制点
let controlPoints = []
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
if (
associativeLineTargetControlOffsets &&
associativeLineTargetControlOffsets[targetIndex]
) {
// 节点保存了控制点差值
let offsets = associativeLineTargetControlOffsets[targetIndex]
controlPoints = [
{
x: startPoint.x + offsets[0].x,
y: startPoint.y + offsets[0].y
},
{
x: endPoint.x + offsets[1].x,
y: endPoint.y + offsets[1].y
}
]
} else {
// 没有保存控制点则生成默认的
controlPoints = computeCubicBezierPathPoints(
startPoint.x,
startPoint.y,
endPoint.x,
endPoint.y
)
}
// 根据控制点拼接贝塞尔曲线路径
return {
path: joinCubicBezierPath(
startPoint,
endPoint,
controlPoints[0],
controlPoints[1]
),
controlPoints
}
}
// 获取默认的控制点差值
export const getDefaultControlPointOffsets = (startPoint, endPoint) => {
let controlPoints = computeCubicBezierPathPoints(
startPoint.x,
startPoint.y,
endPoint.x,
endPoint.y
)
return [
{
x: controlPoints[0].x - startPoint.x,
y: controlPoints[0].y - startPoint.y
},
{
x: controlPoints[1].x - endPoint.x,
y: controlPoints[1].y - endPoint.y
}
]
}

View File

@@ -4,7 +4,11 @@ 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 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,
imgAdjust
}

View File

@@ -279,9 +279,9 @@ export const nodeIconList = [
]
// 获取nodeIconList icon内容
const getNodeIconListIcon = name => {
const getNodeIconListIcon = (name, extendIconList = []) => {
let arr = name.split('_')
let typeData = nodeIconList.find(item => {
let typeData = [...nodeIconList, ...extendIconList].find(item => {
return item.type === arr[0]
})
return typeData.list.find(item => {

View File

@@ -0,0 +1,57 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 秋天
export default merge(defaultTheme, {
// 背景颜色
backgroundColor: '#fff2df',
// 连线的颜色
lineColor: '#b0bc47',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: '#b0bc47',
// 根节点样式
root: {
fillColor: '#e68112',
color: '#fff',
borderColor: '#e68112',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: '#b0bc47',
borderWidth: 3
}
},
// 二级节点样式
second: {
fillColor: '#ffd683',
color: '#8c5416',
borderColor: '#b0bc47',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: '#e68112'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#8c5416',
active: {
borderColor: '#b0bc47'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: '#ffd683',
borderColor: '#b0bc47',
borderWidth: 2,
color: '#8c5416',
active: {
borderColor: '#e68112'
}
}
})

Some files were not shown because too many files have changed in this diff Show More