Compare commits

...

204 Commits
0.7.1 ... 0.9.1

Author SHA1 Message Date
wanglin2
9fe94bfc21 打包0.9.1 2023-12-05 09:34:51 +08:00
wanglin2
6be6f6ee8a Doc 2023-12-05 09:31:13 +08:00
wanglin2
83ce090402 Fix:修复在节点文本编辑中和关联线文本编辑中时销毁思维导图文本编辑框未被销毁的问题 2023-12-05 09:02:41 +08:00
wanglin2
250ff30704 Demo:支持配置创建新节点时的行为 2023-12-04 11:25:04 +08:00
wanglin2
55d7d0a846 Feat:新增创建新节点的行为配置选项 2023-12-04 11:07:26 +08:00
wanglin2
0b3d2cedbd Fix:重新加上因新功能而丢失的旧功能:新创建的节点默认全选 2023-12-04 10:18:17 +08:00
wanglin2
de025d9bc7 Fix:修复不在格式刷时点击画布和节点也会触发painter_end事件的问题 2023-12-04 09:57:29 +08:00
wanglin2
79a39f993d Fix:修复点击节点也会触发node_dragend事件的问题 2023-12-04 09:51:56 +08:00
wanglin2
0fa731c3a2 Fix:修复在节点编辑状态中通过鼠标滚轮缩放画布再推出节点编辑后快捷键生效的问题 2023-12-01 10:23:32 +08:00
wanglin2
34eece8fcf Fix:修复节点处于编辑状态时,通过鼠标滚动移动画布后编辑框和节点脱离的问题 2023-12-01 09:59:33 +08:00
wanglin2
790662ad0c Demo: 增加快捷键提示 2023-12-01 09:52:17 +08:00
wanglin2
7d83a3635f Feat:在鼠标滚轮行为为上下移动画布时,支持按住Ctrl键改为放大缩小画布 2023-12-01 09:49:02 +08:00
wanglin2
546cf27b33 Feat:鼠标滚轮行为默认改为上下移动画布;改为默认向前滚动放大画布,向后缩小 2023-12-01 09:48:14 +08:00
wanglin2
e59d419708 Feat:只读模式下搜索时给当前匹配到的节点增加高亮效果 2023-11-30 17:40:12 +08:00
wanglin2
2d50106ce7 Feat:新增创建新节点时默认不聚焦新节点的配置选项 2023-11-29 08:50:13 +08:00
wanglin2
70c32e3c74 Fix:1.按住Ctrl键时禁用节点双击事件;2.优化节点激活事件的派发,激活节点未改变时不派发事件,短时间派发多次事件时跳过中间事件 2023-11-28 17:37:44 +08:00
wanglin2
a0f56473ee Fix:修复批量执行类添加同名任务时任务未更新为最小任务的问题 2023-11-28 17:36:13 +08:00
wanglin2
51dcb1f54f Fix:修复自定义节点内容时导出图片、svg、pdf报错的问题 2023-11-27 17:15:35 +08:00
wanglin2
9e34fd6174 Doc update 2023-11-24 13:45:03 +08:00
wanglin2
3b47d11b21 打包0.9.0 2023-11-23 14:22:43 +08:00
wanglin2
6e91df9d03 Doc: update 2023-11-23 14:16:06 +08:00
wanglin2
4c8f1bd69c Feat:新增鼠标移入概要时高亮其所属的节点 2023-11-23 11:25:37 +08:00
wanglin2
1f993518fd Merge branch 'feature3' into feature 2023-11-22 16:58:57 +08:00
wanglin2
7f9a1e9309 Fix:修复部分事件在思维导图卸载后未取消监听的问题 2023-11-22 16:23:04 +08:00
wanglin2
5df4a7edb8 Feat:导入和导出xmind文件时,支持处理区间概要 2023-11-22 14:23:21 +08:00
wanglin2
ddd578d773 Doc: update 2023-11-21 17:54:12 +08:00
wanglin2
c8e63e433c Feat:支持对同一个节点的多个子节点添加概要 2023-11-21 17:33:43 +08:00
wanglin2
cadd159a46 Feat:给节点实例增加isParent方法 2023-11-21 10:41:31 +08:00
wanglin2
d32588763c Feat:将节点实例的isParent方法改名为isAncestor 2023-11-21 10:40:00 +08:00
wanglin2
6723250266 Fix:修复同时给存在上下级关系的节点添加概要时概要重叠的问题 2023-11-21 10:32:19 +08:00
街角小林
41ca8f8e46 Merge pull request #407 from doyouhaobaby/doyouhaobaby-patch-1
Xmind 导入支持概要,其中xmind8修复将概要弄成了主题,导出改为最新版本xmind结构
2023-11-21 09:40:18 +08:00
wanglin2
a6d04ffa91 Feat:导出png的方法新增压缩参数;优化大数据量节点导出pdf时体积过大的问题 2023-11-20 18:06:14 +08:00
wanglin2
631892d785 Demo:导出操作增加loading 2023-11-20 17:49:30 +08:00
wanglin2
3d7f0fcbe7 Fix:修复导出pdf时异步控制丢失的问题 2023-11-20 17:49:05 +08:00
wanglin2
c716ec7294 Fix:修复节点数量很多的情况下导出pdf报错的问题 2023-11-20 17:48:19 +08:00
wanglin2
4db6bda193 修复先给自身添加概要,再给下级添加概要会出现概要重叠的问题 2023-11-20 17:05:00 +08:00
wanglin2
bc2a5b214f Demo:修复同时选中多个节点添加图标时,所有节点图标都会统一为第一个节点的图标的问题 2023-11-20 16:46:00 +08:00
wanglin2
879de57b49 Fix:修复富文本模式下节点内容存在 时导出为图片出错的问题 2023-11-20 16:26:35 +08:00
wanglin2
0d4cbc7344 Demo:修复节点内容为html标签时大纲无法显示和编辑的问题 2023-11-20 15:46:33 +08:00
wanglin2
217f5ee95d Fix:修复只读模式下可以全选节点的问题 2023-11-20 15:15:57 +08:00
wanglin2
ad98c0f229 Demo:修复只读模式下仍旧可以替换和编辑大纲的问题 2023-11-20 15:15:38 +08:00
wanglin2
9a7f827301 Fix:修复缩放画布时图标浮层和备注浮层和节点脱离的问题 2023-11-20 14:58:42 +08:00
wanglin2
211fa183d3 Fix:修复在safari浏览器中运行时,页面空白且控制台抛出异常的问题 2023-11-20 14:07:09 +08:00
wanglin2
c0e0fb23e4 Demo:修改本地文件操作功能不可用的提示 2023-11-20 14:00:32 +08:00
wanglin2
e0d46055a7 Feat:新增禁止双指缩放画布的配置信息 2023-11-20 11:46:46 +08:00
wanglin2
a1ec72b401 Feat:新增禁止拖动画布的配置选项 2023-11-20 11:39:08 +08:00
wanglin2
ffc7bf5b7e Merge branch 'feature' of https://github.com/wanglin2/mind-map into feature 2023-11-20 11:04:52 +08:00
wanglin2
4e6e0e2221 Fix:修复历史记录数据概要节点的激活状态未被删除的问题 2023-11-20 11:04:23 +08:00
小牛仔
a1bbe543ba 修复判断 2023-11-17 17:03:35 +08:00
小牛仔
48b5ff5f1c 修复判断 2023-11-17 16:56:15 +08:00
小牛仔
13dc61a585 旧版本xmind8支持导入概要,老的注解被搞成了一个主题了 2023-11-17 16:20:02 +08:00
小牛仔
6e476489f5 feat:Xmind导入支持概要,导入支持概要并且导出为最新版本的xmind格式 2023-11-17 13:47:04 +08:00
wanglin2
e6b82db674 Doc: update 2023-11-04 15:10:47 +08:00
wanglin2
4c60c52a8f 打包demo 2023-11-03 10:33:01 +08:00
wanglin2
29ed8f7a8d Merge branch 'main' into feature 2023-11-03 10:32:07 +08:00
街角小林
4b880e13a5 Merge pull request #373 from maxchang3/docs-typo
chore: typo
2023-11-03 10:30:54 +08:00
wanglin2
b5a08a2414 Doc: update 2023-11-03 10:27:04 +08:00
wanglin2
57e9379f49 Doc: update 2023-11-03 10:01:01 +08:00
MaxChang3
134fd69ffa chore: typo 2023-10-24 12:31:59 +08:00
wanglin2
e2830ccfb1 Doc: update 2023-10-20 11:45:00 +08:00
wanglin2
b0dd90f5b3 Feat:节点增加getPureData方法获取该节点的纯数据 2023-10-19 10:47:53 +08:00
wanglin2
32fc6937d2 Fix:修复极少数情况下输入中文时文本样式丢失的问题 2023-10-18 15:34:14 +08:00
wanglin2
23a3e26800 打包0.8.0-fix.1 2023-10-18 14:34:35 +08:00
wanglin2
c7b0cbc128 Fix:修复粘贴方式创建新节点时如果粘贴的内容带有<>等html标签符号时新创建的节点内容为空的问题 2023-10-18 14:30:00 +08:00
wanglin2
9915479354 打包0.8.0 2023-10-18 14:02:16 +08:00
wanglin2
abddafa3cf Demo:调整界面样式 2023-10-18 13:45:42 +08:00
wanglin2
5b057ff9de Doc: update 2023-10-18 11:21:43 +08:00
wanglin2
6dcbc0604d Fix:修复协同插件当创建新节点时新节点未显示创建人头像的问题 2023-10-18 11:19:55 +08:00
wanglin2
0e5fed6645 Doc: update 2023-10-18 10:49:27 +08:00
wanglin2
b6d91ec07a Feat:指定时间内只允许添加一次历史记录,避免添加没有必要的中间状态 2023-10-18 10:45:25 +08:00
wanglin2
baf36acc97 Doc: update 2023-10-17 16:56:07 +08:00
wanglin2
e0db4ad4f6 Doc: update 2023-10-17 16:14:56 +08:00
wanglin2
4fc2d79616 Fix:修复存在水印时小地图渲染非常慢的问题 2023-10-17 16:09:31 +08:00
wanglin2
9e4652b7b5 Fix:修复容器尺寸改变后没有水印没有重新绘制的问题 2023-10-17 16:01:03 +08:00
wanglin2
8466a3e99f Demo:小地图改为通过图片渲染 2023-10-17 15:35:32 +08:00
wanglin2
e85f187199 Feat:小地图插件支持返回图片类型的小地图 2023-10-17 15:35:21 +08:00
wanglin2
77167c572d Doc: update 2023-10-17 14:05:21 +08:00
wanglin2
9aae8bf55c Fix:修复存在水印时导出图片、svg、pdf时每个节点都会显示边框的问题 2023-10-17 14:02:08 +08:00
wanglin2
250fb2eb50 Fix:修复关联线插件computeNodePoints方法返回undefined时报错的问题 2023-10-17 13:45:39 +08:00
wanglin2
2a49dd9140 Doc: update 2023-10-17 11:13:06 +08:00
wanglin2
4dedaaea3b Demo:完善多语言 2023-10-16 18:43:07 +08:00
wanglin2
7bc666be36 Feat:粘贴带换行的文本支持控制是否按换行分割节点 2023-10-16 15:52:52 +08:00
wanglin2
93a56ef4ee Demo:支持手动输入缩放倍数 2023-10-16 15:08:31 +08:00
wanglin2
83b916d3c9 Demo:顶部工具栏支持根据窗口宽度自动收起到更多中 2023-10-16 14:26:10 +08:00
wanglin2
20157fcc8d Feat:被删除的节点同步从激活节点列表里删除;优化代码:1.移除父节点的连线逻辑合并到node.destroy方法内;2.提取render类中派发节点激活事件的重复代码 2023-10-16 10:29:45 +08:00
wanglin2
2c3fb4d7ea Feat:画布右键菜单事件清除当前激活的节点列表 2023-10-16 10:08:24 +08:00
wanglin2
75ad40ffbc Feat:鼠标右键单击画布时清除当前激活节点 2023-10-16 09:52:45 +08:00
wanglin2
e263eb8252 优化代码:render类中删除和更新概要时无需手动调用更新方法 2023-10-16 09:19:43 +08:00
wanglin2
8e43cd609f Feat:1.插入概要时自动展开子节点;2.删除插入和删除概要的方法中手动调用node.update方法的逻辑 2023-10-15 17:00:39 +08:00
wanglin2
1a3401fd1a 优化代码:修改unexpandAllNode,expandToLevel方法,没有子节点的节点无法收起 2023-10-15 16:46:08 +08:00
wanglin2
d1dcef2537 Feat:增加插入概要的默认文本配置选项 2023-10-15 15:15:27 +08:00
wanglin2
e732415aa3 优化代码:删除render类的unexpandAllNode,expandToLevel方法中重置_node属性的逻辑 2023-10-15 14:50:12 +08:00
wanglin2
ddbde0141a 优化代码:去除render类的setNodeExpand方法中的调用节点移除方法的逻辑 2023-10-15 11:18:44 +08:00
wanglin2
bc907f4b37 优化代码:1.将render类的setNodeActive方法的部分逻辑移到node类;2.将node类的updateNodeActive方法名称改为updateNodeActiveClass 2023-10-15 09:51:02 +08:00
wanglin2
1caf2c7f15 优化代码:将render类的setNodeStyle和setNodeStyles方法的公共逻辑提取到richText插件 2023-10-15 09:13:44 +08:00
wanglin2
22b56fb8dc 代码优化:通过addNodeToActiveList方法优化render类中的重复逻辑 2023-10-13 17:46:09 +08:00
wanglin2
87eccc298c 代码优化:删除节点时无需调用节点的删除方法,只需修改节点的nodeData.data数据即可 2023-10-13 17:30:21 +08:00
wanglin2
ca9e47183d 代码优化:将render类的onPaste方法中的读取剪贴板数据的逻辑提取为工具函数 2023-10-13 16:25:05 +08:00
wanglin2
1fbfe6f5ac 代码优化:将render类的setCopyDataToClipboard方法提取为工具方法 2023-10-13 16:18:28 +08:00
wanglin2
84d2a374d1 Demo:给节点的getData方法的返回值增加默认值 2023-10-13 16:09:35 +08:00
wanglin2
74a000723b 代码优化:读取和设置节点的nodeData.data改为通过setData和getData方法 2023-10-13 16:09:10 +08:00
wanglin2
5079ad2190 代码优化:将调用render类的setNodeData方法的地方改为调用SET_NODE_DATA命令 2023-10-13 15:26:00 +08:00
wanglin2
21053c43c9 Fix:修复同时给多个节点插入父节点时报错的问题 2023-10-13 15:17:19 +08:00
wanglin2
4c6270881a Demo:修改右键菜单的宽度 2023-10-13 14:29:51 +08:00
wanglin2
c17e5430ed 优化代码:使用getNodeDataIndex工具函数去除render类重复逻辑 2023-10-13 13:57:07 +08:00
wanglin2
0a36555343 代码优化:提取render类前进回退方法公共逻辑 2023-10-13 13:51:33 +08:00
wanglin2
bce2bb8fc4 代码优化:提取getNodeIndexInNodeList工具函数 2023-10-13 12:07:26 +08:00
wanglin2
d6ae06dbd6 代码优化:1.将render类的removeActiveNode函数名称改为removeNodeFromActiveList;2.addNodeToActiveList和removeNodeFromActiveList方法增加修改节点的激活状态数据 2023-10-13 11:46:29 +08:00
wanglin2
9221c404ee 代码优化:1.将render类的addActiveNode函数名称改为addNodeToActiveList;2.将调用render类的setNodeActive方法的地方改为调用SET_NODE_ACTIVE命令 2023-10-13 11:28:34 +08:00
wanglin2
697e53ff7d 代码优化:节点右键事件,如果有且只有当前节点被激活了,不再重复激活 2023-10-13 11:16:08 +08:00
wanglin2
9360aff6c9 代码优化:将render类的clearActive函数名称改为clearActiveNodeList 2023-10-13 11:05:34 +08:00
wanglin2
c68d629b7a 代码优化:将调用clearAllActive方法的地方改为调用CLEAR_ACTIVE_NODE命令 2023-10-13 10:55:02 +08:00
wanglin2
caedfb46a9 Demo:支持传入父节点和仅删除当前节点 2023-10-13 09:26:36 +08:00
wanglin2
2e4c6bc08e Feat:新增仅删除当前节点的命令 2023-10-13 09:26:12 +08:00
wanglin2
777eafcd2f Feat:新增插入父节点的命令;Fix:修复插入概要、上移、下移、一键整理布局的快捷键操作没有触发data_change事件的问题 2023-10-12 09:36:50 +08:00
wanglin2
20780a0c59 Doc: update 2023-10-12 09:11:41 +08:00
wanglin2
5d433cce16 Demo:修复覆盖方式切换主题时第一次切换不生效的问题 2023-10-11 17:13:35 +08:00
wanglin2
ba77fde93b Feat:setTheme、setThemeConfig、setLayout函数增加不触发重新渲染的参数 2023-10-11 17:12:37 +08:00
wanglin2
45b8850493 Fix:修复存在排队渲染时,最后一次渲染参数丢失的问题 2023-10-11 17:11:12 +08:00
wanglin2
39c2c15259 Demo:修改回到根节点的方法及文案 2023-10-11 15:49:22 +08:00
wanglin2
6d780c6c26 Feat:修复调整容器大小后回到根节点的操作异常的问题 2023-10-11 15:48:22 +08:00
wanglin2
4cf66adc18 Feat:优化代码,导出和适应画布操作时不再重新获取容器元素尺寸位置信息 2023-10-11 15:09:47 +08:00
wanglin2
7bff14e1bb Doc: update 2023-10-11 13:57:29 +08:00
wanglin2
7986e0d0cc Fix:修复导出图片和svg时关联线的箭头消失的问题 2023-10-11 13:49:28 +08:00
wanglin2
a316d0f0fe Feat:优化水印插件 2023-10-11 11:36:54 +08:00
wanglin2
88fa6225eb Feat:优化画布DOM结构,将节点、连线、关联线分层渲染 2023-10-11 11:36:34 +08:00
wanglin2
7ec720823f Demo build 2023-10-10 09:49:52 +08:00
wanglin2
c39daf72b4 Doc: update 2023-10-10 09:47:40 +08:00
wanglin2
9c4e72eb29 打包0.7.3-fix.2 2023-10-08 16:04:29 +08:00
wanglin2
88e3c1f660 Doc: update 2023-10-08 15:59:03 +08:00
wanglin2
e6c92d4a5e Fix:修复协同编辑时修改同级节点的位置时不生效的问题 2023-10-08 15:45:32 +08:00
wanglin2
745531f20f 打包0.7.3-fix.1 2023-10-08 09:45:25 +08:00
wanglin2
3acd425c09 Demo:优化超链接输入 2023-10-08 09:37:54 +08:00
wanglin2
8dcc7c985d Doc: update 2023-10-08 09:33:06 +08:00
wanglin2
253ded33bf Demo:超链接输入框增加协议选择功能 2023-10-08 09:32:07 +08:00
wanglin2
2c6b8294f4 Fix:修复多次粘贴节点时由于节点uid重复造成的渲染异常问题 2023-10-08 09:10:14 +08:00
wanglin2
83a5ef8e2e Fix:修复多选节点时在节点上松开鼠标时框选区域不会消失的问题 2023-10-06 14:08:45 +08:00
wanglin2
b959e90723 Fix:修复一些情况下多选节点时的框选区域没有消失的问题 2023-10-06 13:47:47 +08:00
wanglin2
89ebc9a1fa 打包 2023-10-06 10:20:54 +08:00
wanglin2
56d2e34fbd Doc: update 2023-10-06 09:52:03 +08:00
wanglin2
0f2aed7e8a 打包0.7.3 2023-10-05 13:50:36 +08:00
wanglin2
288ceafa92 Fix:修复运行信令服务器命令错误的问题 2023-10-05 13:40:01 +08:00
wanglin2
99dc443142 Doc: update 2023-10-05 10:27:24 +08:00
wanglin2
b6440eba1a Merge branch 'test' into feature 2023-10-04 16:11:38 +08:00
wanglin2
545e46babc Feat:没有注册协同插件时不给节点实例添加相关的方法 2023-10-04 16:01:47 +08:00
wanglin2
b95b6af1b1 完善协同插件 2023-10-04 15:39:45 +08:00
wanglin2
ccef5fc581 修改协同插件后端服务启动命令 2023-10-04 15:38:48 +08:00
wanglin2
ed82fe5a61 Feat:对setData方法传入的数据进行深拷贝;更新渲染树数据的逻辑移到Render类中 2023-10-04 15:37:57 +08:00
wanglin2
1550f032d9 协作增加状态同步 2023-09-28 15:32:41 +08:00
wanglin2
7d2758a21c update 2023-09-28 07:49:11 +08:00
wanglin2
e4fab73017 Demo打包 2023-09-27 18:25:55 +08:00
wanglin2
1beb03eaa6 Feat:尝试支持协同 2023-09-27 18:21:27 +08:00
wanglin2
20f67efd58 Demo:修复公式侧边栏组件导致的侧边栏自动关闭问题 2023-09-27 16:44:29 +08:00
wanglin2
8c3d66eb3c Feat:创建节点、复制节点时给新节点数据创建uid 2023-09-27 13:38:15 +08:00
wanglin2
a4f6006efd Fix:修复isSameObject工具方法逻辑错误的问题 2023-09-27 11:02:37 +08:00
wanglin2
1d297350cc Fix:修复工具方法getType返回错误的问题 2023-09-27 09:03:40 +08:00
wanglin2
07650f8978 Doc update 2023-09-26 09:19:58 +08:00
wanglin2
b26e5625ce 打包 2023-09-24 22:14:10 +08:00
wanglin2
1e38731ecb Doc update 2023-09-24 22:03:06 +08:00
wanglin2
ef9b9804cb 打包0.7.2 2023-09-23 16:35:13 +08:00
wanglin2
9fe321a127 Doc update 2023-09-23 16:28:17 +08:00
wanglin2
464e57d019 Doc update 2023-09-23 15:48:05 +08:00
wanglin2
50f125471e Feat:插入公式命令支持传入指定的节点 2023-09-23 15:22:26 +08:00
wanglin2
6bbee4a5cc Demo:节点标签输入适配新颜色生成逻辑 2023-09-23 10:52:04 +08:00
wanglin2
5052c0427a 优化节点标签代码 2023-09-23 10:51:30 +08:00
街角小林
9528631ed1 Merge pull request #347 from wanghao1993/tag_color
feat: 修改tag颜色,可以自定义tag颜色,没有自定义的tag颜色,那么就根据内容生成
2023-09-23 10:34:49 +08:00
街角小林
2a816f62fa Merge branch 'feature' into tag_color 2023-09-23 10:34:38 +08:00
wanglin2
8596e3356d Fix:修复非富文本模式下文本中存在<>&字符时再次编辑时部分文本会消失的问题 2023-09-23 10:22:57 +08:00
Isaac Wang1 汪浩
7422af7f3b feat: 修改tag颜色,可以自定义tag颜色,没有自定义的tag颜色,那么就根据内容生成 2023-09-22 19:36:34 +08:00
wanglin2
0f047da78b update 2023-09-22 17:54:26 +08:00
wanglin2
f866abb34c 优化图标合并相关逻辑 2023-09-22 17:13:09 +08:00
wanglin2
01d7e36990 恢复意外的合并部分 2023-09-22 16:44:57 +08:00
街角小林
a9493b9c16 Merge pull request #345 from wanghao1993/feature/icon-merge
Feature/icon merge
2023-09-22 16:29:03 +08:00
街角小林
a59a283d74 Merge branch 'feature' into feature/icon-merge 2023-09-22 16:28:47 +08:00
wanglin2
aafcba8bb7 Demo:支持数学公式 2023-09-22 16:12:40 +08:00
wanglin2
11ea7d452c Feat:新增数学公式插件 2023-09-22 16:11:33 +08:00
Isaac Wang1 汪浩
47d21d85fb feat: build 2023-09-22 14:01:02 +08:00
wanglin2
518b7642a0 Demo:修复复制知犀思维导图多个节点时无法粘贴的问题 2023-09-22 10:57:32 +08:00
wanglin2
a9ea4b8e33 Feat:1.新增同时插入多个同级节点、多个子节点的命令;2.复制、剪切操作支持同时操作多个节点;3.新增对插入同级节点、子节点的命令插入的第四个参数数据的处理 2023-09-22 10:00:44 +08:00
wanglin2
443465eb86 Feat:将节点唯一标识由id全部改为uid 2023-09-22 08:57:00 +08:00
wanghao1993
e1172c8d0d fix: text 是空的时候会报错 2023-09-21 23:33:06 +08:00
wanglin2
7a2605fdad Feat:支持同时对多个节点插入兄弟节点;对根节点调用插入兄弟节点的命令时不再创建子节点 2023-09-21 16:27:26 +08:00
wanglin2
a97d549d69 Feat:优化子节点的插入:1.同时对多个节点插入子节点时,不进入编辑状态;2.新插入的子节点自动进入激活状态 2023-09-21 15:58:22 +08:00
wanglin2
fcf48ca3dc Fix:增加一些边界判断 2023-09-21 15:10:17 +08:00
wanglin2
c0ad18cff8 Feat:存在激活节点时点击关联线可直接激活关联线 2023-09-21 09:53:35 +08:00
wanglin2
73e7855575 Feat:1.双击关联线进入关联线文本编辑模式;2.关联线文本为默认文本的话不保存 2023-09-21 09:51:46 +08:00
wanglin2
69ef7faf49 Feat:优化drag插件,支持同时拖动多个节点 2023-09-21 09:02:45 +08:00
wanglin2
036f845968 Feat:支持移动多个节点 2023-09-21 09:01:37 +08:00
wanglin2
740e2e3410 Fix:修复多选节点时选区未包含节点边界时节点不会被选中的问题 2023-09-21 09:00:25 +08:00
wanghao1993
ba44f69f9f fix: icon 合并错误 2023-09-20 00:00:48 +08:00
wanglin2
977799a3ac 打包0.7.1-fix.2 2023-09-18 16:56:12 +08:00
wanglin2
76ddecee50 Doc: update 2023-09-18 16:53:33 +08:00
wanglin2
0ab495a161 Fix:修复插件注册方法链式调用报错的问题 2023-09-18 16:52:40 +08:00
wanglin2
fefbcfbbee Feat:改为通过节点uid比对节点 2023-09-18 16:30:45 +08:00
wanglin2
c77c7403da Fix:修复lru类名错误的问题 2023-09-18 09:20:03 +08:00
wanglin2
b0044bb5fa update package.json 2023-09-18 09:16:21 +08:00
wanglin2
e61749c1b3 Demo:更新文档,去除默认适应画布大小 2023-09-18 09:13:24 +08:00
街角小林
43216ed925 Merge pull request #336 from wanghao1993/feature/add_opt_fit
Feature/add opt fit
2023-09-18 09:06:40 +08:00
街角小林
385873b6e8 Merge pull request #335 from wanghao1993/feature/types
types: 完善dts,增加一条script "types"
2023-09-18 09:03:40 +08:00
wanghao1993
5583642961 doc: 增加文档 2023-09-17 18:51:12 +08:00
wanghao1993
e063724ab6 feat: 增加默认配置fit,支持初始化的时候是否fit view 2023-09-17 18:42:29 +08:00
wanghao1993
19d4788489 feat: 增加自动生成dts声明文件,执行了一下format 2023-09-17 18:08:56 +08:00
wanglin2
1452fd2a28 打包0.7.1-fix.1 2023-09-15 09:35:38 +08:00
wanglin2
c296b99d5a Fix:修复拖拽节点时没有排除被拖拽节点的下级节点的问题 2023-09-15 09:35:24 +08:00
253 changed files with 40347 additions and 2673 deletions

View File

@@ -25,18 +25,21 @@ Github[releases](https://github.com/wanglin2/mind-map/releases)。
百度云盘:[地址](https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3)。
> 客户端版本会落后于在线版本,尝试最新功能请优先使用在线版。
# 特性
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴(横向、竖向)、鱼骨图等结构
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
- [x] 节点内容支持文本(普通文本、富文本)、图片、图标、超链接、备注、标签、概要
- [x] 节点内容支持文本(普通文本、富文本)、图片、图标、超链接、备注、标签、概要、数学公式
- [x] 节点支持拖拽(拖拽移动、自由调整)、多种节点形状,支持使用 DDM 完全自定义节点内容
- [x] 支持画布拖动、缩放
- [x] 支持鼠标按键拖动选择和Ctrl+左键两种多选节点方式
- [x] 支持导出为`json``png``svg``pdf``markdown``xmind`,支持从`json``xmind``markdown`导入
- [x] 支持快捷键、前进后退、关联线、搜索替换、小地图、水印
- [x] 支持快捷键、前进后退、关联线、搜索替换、小地图、水印、滚动条
- [x] 提供丰富的配置,满足各种场景各种使用习惯
- [x] 支持协同编辑
# 安装
@@ -91,11 +94,11 @@ const mindMap = new MindMap({
# 请作者喝杯咖啡
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡~
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡~
> 厚椰乳一盒 + 纯牛奶半盒 + 冰块 + 咖啡液 = 生椰拿铁 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" />
@@ -179,4 +182,52 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/乙.jpg" style="width: 50px;height: 50px;" />
<span>乙</span>
</span>
<span>
<img src="./web/src/assets/avatar/敏.jpg" style="width: 50px;height: 50px;" />
<span>敏</span>
</span>
<span>
<img src="./web/src/assets/avatar/沐风牧草.jpg" style="width: 50px;height: 50px;" />
<span>沐风牧草</span>
</span>
<span>
<img src="./web/src/assets/avatar/有希.jpg" style="width: 50px;height: 50px;" />
<span>有希</span>
</span>
<span>
<img src="./web/src/assets/avatar/樊笼.jpg" style="width: 50px;height: 50px;" />
<span>樊笼</span>
</span>
<span>
<img src="./web/src/assets/avatar/达仁科技.jpg" style="width: 50px;height: 50px;" />
<span>达仁科技</span>
</span>
<span>
<img src="./web/src/assets/avatar/小逗比.png" style="width: 50px;height: 50px;" />
<span>小逗比</span>
</span>
<span>
<img src="./web/src/assets/avatar/天清如愿.jpg" style="width: 50px;height: 50px;" />
<span>天清如愿</span>
</span>
<span>
<img src="./web/src/assets/avatar/敬明朗.jpg" style="width: 50px;height: 50px;" />
<span>敬明朗</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>飞箭</span>
</span>
<span>
<img src="./web/src/assets/avatar/戚永峰.png" style="width: 50px;height: 50px;" />
<span>戚永峰</span>
</span>
<span>
<img src="./web/src/assets/avatar/moom.jpg" style="width: 50px;height: 50px;" />
<span>moom</span>
</span>
<span>
<img src="./web/src/assets/avatar/张扬.png" style="width: 50px;height: 50px;" />
<span>张扬</span>
</span>
</p>

View File

@@ -1,7 +1,7 @@
<!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?8e81eea7e47ca2aeb9e0" rel="stylesheet"><link href="dist/css/app.css?8e81eea7e47ca2aeb9e0" 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 = () => {
window.takeOverApp = false</script><link href="dist/css/chunk-vendors.css?7820598bfce2a2653e5c" rel="stylesheet"><link href="dist/css/app.css?7820598bfce2a2653e5c" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
@@ -66,4 +66,4 @@
// 可以通过window.$bus.$on()来监听应用的一些事件
// 实例化页面
window.initApp()
}</script><script src="dist/js/chunk-vendors.js?8e81eea7e47ca2aeb9e0"></script><script src="dist/js/app.js?8e81eea7e47ca2aeb9e0"></script></body></html>
}</script><script src="dist/js/chunk-vendors.js?7820598bfce2a2653e5c"></script><script src="dist/js/app.js?7820598bfce2a2653e5c"></script></body></html>

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env node
import ws from 'ws'
import http from 'http'
import * as map from 'lib0/map'
const wsReadyStateConnecting = 0
const wsReadyStateOpen = 1
const wsReadyStateClosing = 2 // eslint-disable-line
const wsReadyStateClosed = 3 // eslint-disable-line
const pingTimeout = 30000
const port = process.env.PORT || 4444
// @ts-ignore
const wss = new ws.Server({ noServer: true })
const server = http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' })
response.end('okay')
})
/**
* Map froms topic-name to set of subscribed clients.
* @type {Map<string, Set<any>>}
*/
const topics = new Map()
/**
* @param {any} conn
* @param {object} message
*/
const send = (conn, message) => {
if (
conn.readyState !== wsReadyStateConnecting &&
conn.readyState !== wsReadyStateOpen
) {
conn.close()
}
try {
conn.send(JSON.stringify(message))
} catch (e) {
conn.close()
}
}
/**
* Setup a new client
* @param {any} conn
*/
const onconnection = conn => {
/**
* @type {Set<string>}
*/
const subscribedTopics = new Set()
let closed = false
// Check if connection is still alive
let pongReceived = true
const pingInterval = setInterval(() => {
if (!pongReceived) {
conn.close()
clearInterval(pingInterval)
} else {
pongReceived = false
try {
conn.ping()
} catch (e) {
conn.close()
}
}
}, pingTimeout)
conn.on('pong', () => {
pongReceived = true
})
conn.on('close', () => {
subscribedTopics.forEach(topicName => {
const subs = topics.get(topicName) || new Set()
subs.delete(conn)
if (subs.size === 0) {
topics.delete(topicName)
}
})
subscribedTopics.clear()
closed = true
})
conn.on(
'message',
/** @param {object} message */ message => {
if (typeof message === 'string') {
message = JSON.parse(message)
}
if (message && message.type && !closed) {
switch (message.type) {
case 'subscribe':
/** @type {Array<string>} */ ;(message.topics || []).forEach(
topicName => {
if (typeof topicName === 'string') {
// add conn to topic
const topic = map.setIfUndefined(
topics,
topicName,
() => new Set()
)
topic.add(conn)
// add topic to conn
subscribedTopics.add(topicName)
}
}
)
break
case 'unsubscribe':
/** @type {Array<string>} */ ;(message.topics || []).forEach(
topicName => {
const subs = topics.get(topicName)
if (subs) {
subs.delete(conn)
}
}
)
break
case 'publish':
if (message.topic) {
const receivers = topics.get(message.topic)
if (receivers) {
message.clients = receivers.size
receivers.forEach(receiver => send(receiver, message))
}
}
break
case 'ping':
send(conn, { type: 'pong' })
}
}
}
)
}
wss.on('connection', onconnection)
server.on('upgrade', (request, socket, head) => {
// You may check auth of request here..
/**
* @param {any} ws
*/
const handleAuth = ws => {
wss.emit('connection', ws, request)
}
wss.handleUpgrade(request, socket, head, handleAuth)
})
server.listen(port)
console.log('Signaling server running on localhost:', port)

View File

@@ -14,6 +14,7 @@ import TouchEvent from './src/plugins/TouchEvent.js'
import Search from './src/plugins/Search.js'
import Painter from './src/plugins/Painter.js'
import Scrollbar from './src/plugins/Scrollbar.js'
import Formula from './src/plugins/Formula.js'
import xmind from './src/parse/xmind.js'
import markdown from './src/parse/markdown.js'
import icons from './src/svg/icons.js'
@@ -28,8 +29,7 @@ MindMap.constants = constants
MindMap.themes = themes
MindMap.defaultTheme = defaultTheme
MindMap
.usePlugin(MiniMap)
MindMap.usePlugin(MiniMap)
.usePlugin(Watermark)
.usePlugin(Drag)
.usePlugin(KeyboardNavigation)
@@ -44,5 +44,6 @@ MindMap
.usePlugin(Search)
.usePlugin(Painter)
.usePlugin(Scrollbar)
.usePlugin(Formula)
export default MindMap
export default MindMap

View File

@@ -1,14 +0,0 @@
// declare module "simple-mind-map";
declare module 'simple-mind-map'{
class MindMap {
constructor(opt:any);
handleOpt(opt:any):void;
render(callback:any, source:string):void;
reRender(callback:any, source:string):void;
resize():void;
on(event:any, fn:any):void;
setFullData(data:any):void;
getData(withConfig:any):any;
}
export default MindMap;
}

View File

@@ -24,6 +24,10 @@ import { defaultOpt } from './src/constants/defaultOptions'
// 思维导图
class MindMap {
// 构造函数
/**
*
* @param {defaultOpt} opt
*/
constructor(opt = {}) {
// 合并选项
this.opt = this.handleOpt(merge(defaultOpt, opt))
@@ -31,20 +35,16 @@ class MindMap {
// 容器元素
this.el = this.opt.el
if (!this.el) throw new Error('缺少容器元素el')
this.elRect = this.el.getBoundingClientRect()
// 画布宽高
this.width = this.elRect.width
this.height = this.elRect.height
if (this.width <= 0 || this.height <= 0) throw new Error('容器元素el的宽高不能为0')
// 获取容器尺寸位置信息
this.getElRectInfo()
// 添加css
this.cssEl = null
this.addCss()
// 画布
this.svg = SVG().addTo(this.el).size(this.width, this.height)
this.draw = this.svg.group()
this.initContainer()
// 初始化主题
this.initTheme()
@@ -74,8 +74,7 @@ class MindMap {
// 视图操作类
this.view = new View({
mindMap: this,
draw: this.draw
mindMap: this
})
// 批量执行类
@@ -87,7 +86,7 @@ class MindMap {
})
// 初始渲染
this.render()
this.render(this.opt.fit ? () => this.view.fit() : () => {})
setTimeout(() => {
this.command.addHistory()
}, 0)
@@ -106,6 +105,46 @@ class MindMap {
return opt
}
// 创建容器元素
initContainer() {
const { associativeLineIsAlwaysAboveNode } = this.opt
// 节点关联线容器
const createAssociativeLineDraw = () => {
this.associativeLineDraw = this.draw.group()
this.associativeLineDraw.addClass('smm-associative-line-container')
}
// 画布
this.svg = SVG().addTo(this.el).size(this.width, this.height)
// 容器
this.draw = this.svg.group()
this.draw.addClass('smm-container')
// 节点连线容器
this.lineDraw = this.draw.group()
this.lineDraw.addClass('smm-line-container')
// 默认处于节点下方
if (!associativeLineIsAlwaysAboveNode) {
createAssociativeLineDraw()
}
// 节点容器
this.nodeDraw = this.draw.group()
this.nodeDraw.addClass('smm-node-container')
// 关联线始终处于节点上方
if (associativeLineIsAlwaysAboveNode) {
createAssociativeLineDraw()
}
// 其他内容的容器
this.otherDraw = this.draw.group()
this.otherDraw.addClass('smm-other-container')
}
// 清空各容器
clearDraw() {
this.lineDraw.clear()
this.associativeLineDraw.clear()
this.nodeDraw.clear()
this.otherDraw.clear()
}
// 添加必要的css样式到页面
addCss() {
this.cssEl = document.createElement('style')
@@ -131,19 +170,27 @@ class MindMap {
// 重新渲染
reRender(callback, source = '') {
this.batchExecution.push('render', () => {
this.draw.clear()
this.clearDraw()
this.initTheme()
this.renderer.reRender = true
this.renderer.render(callback, source)
})
}
// 容器尺寸变化,调整尺寸
resize() {
// 获取或更新容器尺寸位置信息
getElRectInfo() {
this.elRect = this.el.getBoundingClientRect()
this.width = this.elRect.width
this.height = this.elRect.height
if (this.width <= 0 || this.height <= 0)
throw new Error('容器元素el的宽高不能为0')
}
// 容器尺寸变化,调整尺寸
resize() {
this.getElRectInfo()
this.svg.size(this.width, this.height)
this.emit('resize')
}
// 监听事件
@@ -187,10 +234,12 @@ class MindMap {
}
// 设置主题
setTheme(theme) {
this.renderer.clearAllActive()
setTheme(theme, notRender = false) {
this.execCommand('CLEAR_ACTIVE_NODE')
this.opt.theme = theme
this.render(null, CONSTANTS.CHANGE_THEME)
if (!notRender) {
this.render(null, CONSTANTS.CHANGE_THEME)
}
this.emit('view_theme_change', theme)
}
@@ -200,13 +249,15 @@ class MindMap {
}
// 设置主题配置
setThemeConfig(config) {
setThemeConfig(config, notRender = false) {
// 计算改变了的配置
const changedConfig = getObjectChangedProps(this.themeConfig, config)
this.opt.themeConfig = config
// 检查改变的是否是节点大小无关的主题属性
let res = checkIsNodeSizeIndependenceConfig(changedConfig)
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
if (!notRender) {
// 检查改变的是否是节点大小无关的主题属性
let res = checkIsNodeSizeIndependenceConfig(changedConfig)
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
}
}
// 获取自定义主题配置
@@ -235,7 +286,7 @@ class MindMap {
}
// 设置布局结构
setLayout(layout) {
setLayout(layout, notRender = false) {
// 检查布局配置
if (!layoutValueList.includes(layout)) {
layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
@@ -243,7 +294,9 @@ class MindMap {
this.opt.layout = layout
this.view.reset()
this.renderer.setLayout()
this.render(null, CONSTANTS.CHANGE_LAYOUT)
if (!notRender) {
this.render(null, CONSTANTS.CHANGE_LAYOUT)
}
}
// 执行命令
@@ -253,15 +306,13 @@ class MindMap {
// 动态设置思维导图数据,纯节点数据
setData(data) {
data = simpleDeepClone(data || {})
this.execCommand('CLEAR_ACTIVE_NODE')
this.command.clearHistory()
this.command.addHistory()
if (this.richText) {
this.renderer.renderTree = this.richText.handleSetData(data)
} else {
this.renderer.renderTree = data
}
this.renderer.setData(data)
this.reRender(() => {}, CONSTANTS.SET_DATA)
this.emit('set_data', data)
}
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
@@ -331,20 +382,20 @@ class MindMap {
this.opt.readonly = mode === CONSTANTS.MODE.READONLY
if (this.opt.readonly) {
// 取消当前激活的元素
this.renderer.clearAllActive()
this.execCommand('CLEAR_ACTIVE_NODE')
}
this.emit('mode_change', mode)
}
// 获取svg数据
getSvgData({ paddingX = 0, paddingY = 0 } = {}) {
getSvgData({ paddingX = 0, paddingY = 0, ignoreWatermark = false } = {}) {
const svg = this.svg
const draw = this.draw
// 保存原始信息
const origWidth = svg.width()
const origHeight = svg.height()
const origTransform = draw.transform()
const elRect = this.el.getBoundingClientRect()
const elRect = this.elRect
// 去除放大缩小的变换效果
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
@@ -359,10 +410,9 @@ class MindMap {
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
// 克隆一份数据
let clone = svg.clone()
// 添加必要的样式
clone.add(SVG(`<style>${ cssContent }</style>`))
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
if (
!ignoreWatermark &&
(rect.width > origWidth || rect.height > origHeight) &&
this.watermark &&
this.watermark.hasWatermark()
@@ -375,6 +425,16 @@ class MindMap {
this.height = origHeight
this.watermark.draw()
}
// 添加必要的样式
clone.add(SVG(`<style>${cssContent}</style>`))
// 修正关联线箭头marker的id
const markerList = svg.find('marker')
if (markerList && markerList.length > 0) {
const id = markerList[0].attr('id')
clone.find('marker').forEach(item => {
item.attr('id', id)
})
}
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight)
draw.transform(origTransform)
@@ -426,6 +486,13 @@ class MindMap {
// 销毁
destroy() {
this.emit('beforeDestroy')
// 清除节点编辑框
this.renderer.textEdit.hideEditTextBox()
// 清除关联线文字编辑框
if (this.associativeLine) {
this.associativeLine.hideEditTextBox()
}
// 移除插件
;[...MindMap.pluginList].forEach(plugin => {
if (this[plugin.instanceName].beforePluginDestroy) {
@@ -448,7 +515,7 @@ class MindMap {
// 插件列表
MindMap.pluginList = []
MindMap.usePlugin = (plugin, opt = {}) => {
if (MindMap.hasPlugin(plugin) !== -1) return
if (MindMap.hasPlugin(plugin) !== -1) return MindMap
plugin.pluginOpt = opt
MindMap.pluginList.push(plugin)
return MindMap

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.7.1",
"version": "0.9.1",
"description": "一个简单的web在线思维导图",
"authors": [
{
@@ -12,6 +12,8 @@
"url": "http://lxqnsys.com/"
}
],
"types": "./types/index.d.ts",
"typings": "./types/index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
@@ -19,20 +21,26 @@
},
"scripts": {
"lint": "eslint src/",
"format": "prettier --write ."
"format": "prettier --write .",
"types": "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir types --target es2017 --skipLibCheck",
"wsServe": "node ./bin/wsServer.mjs"
},
"module": "index.js",
"__main": "./dist/simpleMindMap.umd.min.js",
"main": "./dist/simpleMindMap.umd.min.js",
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",
"deepmerge": "^1.5.2",
"eventemitter3": "^4.0.7",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"katex": "^0.16.8",
"mdast-util-from-markdown": "^1.3.0",
"quill": "^1.3.6",
"tern": "^0.24.3",
"uuid": "^9.0.0",
"xml-js": "^1.6.11"
"xml-js": "^1.6.11",
"y-webrtc": "^10.2.5",
"yjs": "^13.6.8"
},
"keywords": [
"javascript",

View File

@@ -17,7 +17,7 @@ const transform = dir => {
}
const transformFile = file => {
console.log(file);
console.log(file)
let content = fs.readFileSync(file, 'utf-8')
countCodeLines(content)
// transformComments(file, content)
@@ -25,7 +25,7 @@ const transformFile = file => {
// 统计代码行数
let totalLines = 0
const countCodeLines = (content) => {
const countCodeLines = content => {
totalLines += content.split(/\n/).length
}
@@ -43,4 +43,4 @@ const transformComments = (file, content) => {
transform(entryPath)
transformFile(path.join(__dirname, '../index.js'))
console.log(totalLines);
console.log(totalLines)

View File

@@ -1,27 +1,3 @@
// 标签颜色列表
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 = [
{
@@ -253,6 +229,11 @@ export const CONSTANTS = {
SCROLL_BAR_DIR: {
VERTICAL: 'vertical',
HORIZONTAL: 'horizontal'
},
CREATE_NEW_NODE_BEHAVIOR: {
DEFAULT: 'default',
NOT_ACTIVE: 'notActive',
ACTIVE_ONLY: 'activeOnly'
}
}
@@ -261,42 +242,42 @@ export const initRootNodePositionMap = {
[CONSTANTS.INIT_ROOT_NODE_POSITION.TOP]: 0,
[CONSTANTS.INIT_ROOT_NODE_POSITION.RIGHT]: 1,
[CONSTANTS.INIT_ROOT_NODE_POSITION.BOTTOM]: 1,
[CONSTANTS.INIT_ROOT_NODE_POSITION.CENTER]: 0.5,
[CONSTANTS.INIT_ROOT_NODE_POSITION.CENTER]: 0.5
}
// 布局结构列表
export const layoutList = [
{
name: '逻辑结构图',
value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
},
{
name: '思维导图',
value: CONSTANTS.LAYOUT.MIND_MAP,
value: CONSTANTS.LAYOUT.MIND_MAP
},
{
name: '组织结构图',
value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE
},
{
name: '目录组织图',
value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION,
value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION
},
{
name: '时间轴',
value: CONSTANTS.LAYOUT.TIMELINE,
value: CONSTANTS.LAYOUT.TIMELINE
},
{
name: '时间轴2',
value: CONSTANTS.LAYOUT.TIMELINE2,
value: CONSTANTS.LAYOUT.TIMELINE2
},
{
name: '竖向时间轴',
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE
},
{
name: '鱼骨图',
value: CONSTANTS.LAYOUT.FISHBONE,
value: CONSTANTS.LAYOUT.FISHBONE
}
]
export const layoutValueList = [
@@ -365,7 +346,7 @@ export const cssContent = `
stroke-width: 1;
}
.smm-node:hover .smm-hover-node{
.smm-node:not(.smm-node-dragging):hover .smm-hover-node{
display: block;
}
@@ -374,4 +355,15 @@ export const cssContent = `
opacity: 1;
stroke-width: 2;
}
`
`
// html自闭合标签列表
export const selfCloseTagList = [
'img',
'br',
'hr',
'input',
'link',
'meta',
'area'
]

View File

@@ -56,11 +56,11 @@ export const defaultOpt = {
// 可以传一个函数,回调参数为事件对象
customHandleMousewheel: null,
// 鼠标滚动的行为如果customHandleMousewheel传了自定义函数这个属性不生效
mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM, // zoom放大缩小、move上下移动
mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.MOVE, // zoom放大缩小、move上下移动
// 当mousewheelAction设为move时可以通过该属性控制鼠标滚动一下视图移动的步长单位px
mousewheelMoveStep: 100,
// 当mousewheelAction设为zoom时默认向前滚动是缩小向后滚动是放大如果该属性设为true那么会反过来
mousewheelZoomActionReverse: false,
// 当mousewheelAction设为zoom时或者按住Ctrl键时默认向前滚动是缩小向后滚动是放大如果该属性设为true那么会反过来
mousewheelZoomActionReverse: true,
// 默认插入的二级节点的文字
defaultInsertSecondLevelNodeText: '二级节点',
// 默认插入的二级以下节点的文字
@@ -160,6 +160,9 @@ export const defaultOpt = {
customHandleClipboardText: null,
// 禁止鼠标滚轮缩放你仍旧可以使用api进行缩放
disableMouseWheelZoom: false,
// 禁止双指缩放你仍旧可以使用api进行缩放
// 需要注册TouchEvent插件后生效
disableTouchZoom: false,
// 错误处理函数
errorHandler: (code, error) => {
console.error(code, error)
@@ -173,8 +176,8 @@ export const defaultOpt = {
box-sizing: border-box;
}
`,
// 开启鼠标双击复位思维导图位置及缩放
enableDblclickReset: false,
// 是否在鼠标双击时回到根节点,也就是让根节点居中显示
enableDblclickBackToRootNode: false,
// 导出图片时canvas的缩放倍数该配置会和window.devicePixelRatio值取最大值
minExportImgCanvasScale: 2,
// 节点鼠标hover和激活时显示的矩形边框的颜色
@@ -186,5 +189,52 @@ export const defaultOpt = {
// 删除节点后激活相邻节点
deleteNodeActive: true,
// 拖拽节点时鼠标移动到画布边缘是否开启画布自动移动
autoMoveWhenMouseInEdgeOnDrag: true
autoMoveWhenMouseInEdgeOnDrag: true,
// 是否首次加载fit view
fit: false,
// 拖拽多个节点时随鼠标移动的示意矩形的样式配置
dragMultiNodeRectConfig: {
width: 40,
height: 20,
fill: '' // 填充颜色,如果不传默认使用连线的颜色
},
// 节点拖拽时新位置的示意矩形的填充颜色,如果不传默认使用连线的颜色
dragPlaceholderRectFill: '',
// 节点拖拽时的透明度配置
dragOpacityConfig: {
cloneNodeOpacity: 0.5, // 跟随鼠标移动的克隆节点或矩形的透明度
beingDragNodeOpacity: 0.3 // 被拖拽节点的透明度
},
// 自定义标签的颜色
// {pass: 'green, unpass: 'red'}
tagsColorMap: {},
// 节点协作样式配置
cooperateStyle: {
avatarSize: 22, // 头像大小
fontSize: 12 // 如果是文字头像,那么文字的大小
},
// 关联线是否始终显示在节点上层
// false即创建关联线和激活关联线时处于最顶层其他情况下处于节点下方
associativeLineIsAlwaysAboveNode: true,
// 插入概要的默认文本
defaultGeneralizationText: '概要',
// 粘贴文本的方式创建新节点时,控制是否按换行自动分割节点,即如果存在换行,那么会根据换行创建多个节点,否则只会创建一个节点
// 可以传递一个函数返回promiseresolve代表根据换行分割reject代表忽略换行
handleIsSplitByWrapOnPasteCreateNewNode: null,
// 多少时间内只允许添加一次历史记录避免添加没有必要的中间状态单位ms
addHistoryTime: 100,
// 是否禁止拖动画布
isDisableDrag: false,
// 鼠标移入概要高亮所属节点时的高亮框样式
highlightNodeBoxStyle: {
stroke: 'rgb(94, 200, 248)',
fill: 'transparent'
},
// 创建新节点时的行为
/*
DEFAULT :默认会激活新创建的节点,并且进入编辑模式。如果同时创建了多个新节点,那么只会激活而不会进入编辑模式
NOT_ACTIVE : 不激活新创建的节点
ACTIVE_ONLY : 只激活新创建的节点,不进入编辑模式
*/
createNewNodeBehavior: CONSTANTS.CREATE_NEW_NODE_BEHAVIOR.DEFAULT
}

View File

@@ -1,4 +1,4 @@
import { copyRenderTree, simpleDeepClone, nextTick } from '../../utils'
import { copyRenderTree, simpleDeepClone, throttle } from '../../utils'
// 命令类
class Command {
@@ -11,7 +11,11 @@ class Command {
this.activeHistoryIndex = 0
// 注册快捷键
this.registerShortcutKeys()
this.addHistory = nextTick(this.addHistory, this)
this.addHistory = throttle(
this.addHistory,
this.mindMap.opt.addHistoryTime,
this
)
}
// 清空历史数据
@@ -37,7 +41,11 @@ class Command {
this.commands[name].forEach(fn => {
fn(...args)
})
if (['BACK', 'FORWARD', 'SET_NODE_ACTIVE', 'CLEAR_ACTIVE_NODE'].includes(name)) {
if (
['BACK', 'FORWARD', 'SET_NODE_ACTIVE', 'CLEAR_ACTIVE_NODE'].includes(
name
)
) {
return
}
this.addHistory()
@@ -78,7 +86,11 @@ class Command {
}
let data = this.getCopyData()
// 此次数据和上次一样则不重复添加
if (this.history.length > 0 && JSON.stringify(this.history[this.history.length - 1]) === JSON.stringify(data)) {
if (
this.history.length > 0 &&
JSON.stringify(this.history[this.history.length - 1]) ===
JSON.stringify(data)
) {
return
}
// 删除当前历史指针后面的数据
@@ -123,7 +135,11 @@ class Command {
let len = this.history.length
if (this.activeHistoryIndex + step <= len - 1) {
this.activeHistoryIndex += step
this.mindMap.emit('back_forward', this.activeHistoryIndex, this.history.length)
this.mindMap.emit(
'back_forward',
this.activeHistoryIndex,
this.history.length
)
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
this.mindMap.emit('data_change', data)
return data
@@ -138,10 +154,10 @@ class Command {
// 移除节点数据中的uid
removeDataUid(data) {
data = simpleDeepClone(data)
let walk = (root) => {
let walk = root => {
delete root.data.uid
if (root.children && root.children.length > 0) {
root.children.forEach((item) => {
root.children.forEach(item => {
walk(item)
})
}

View File

@@ -38,6 +38,7 @@ export default class KeyCommand {
// 绑定事件
bindEvent() {
this.onKeydown = this.onKeydown.bind(this)
// 只有当鼠标在画布内才响应快捷键
this.mindMap.on('svg_mouseenter', () => {
this.isInSvg = true
@@ -46,27 +47,45 @@ export default class KeyCommand {
if (this.mindMap.richText && this.mindMap.richText.showTextEdit) {
return
}
if (this.mindMap.renderer.textEdit.showTextEdit || (this.mindMap.associativeLine && this.mindMap.associativeLine.showTextEdit)) {
if (
this.mindMap.renderer.textEdit.showTextEdit ||
(this.mindMap.associativeLine &&
this.mindMap.associativeLine.showTextEdit)
) {
return
}
this.isInSvg = false
})
window.addEventListener('keydown', e => {
if (this.isPause || (this.mindMap.opt.enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) {
return
}
Object.keys(this.shortcutMap).forEach(key => {
if (this.checkKey(e, key)) {
// 粘贴事件不组织因为要监听paste事件
if (!this.checkKey(e, 'Control+v')) {
e.stopPropagation()
e.preventDefault()
}
this.shortcutMap[key].forEach(fn => {
fn()
})
window.addEventListener('keydown', this.onKeydown)
this.mindMap.on('beforeDestroy', () => {
this.unBindEvent()
})
}
// 解绑事件
unBindEvent() {
window.removeEventListener('keydown', this.onKeydown)
}
// 按键事件
onKeydown(e) {
if (
this.isPause ||
(this.mindMap.opt.enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)
) {
return
}
Object.keys(this.shortcutMap).forEach(key => {
if (this.checkKey(e, key)) {
// 粘贴事件不组织因为要监听paste事件
if (!this.checkKey(e, 'Control+v')) {
e.stopPropagation()
e.preventDefault()
}
})
this.shortcutMap[key].forEach(fn => {
fn()
})
}
})
}

View File

@@ -113,7 +113,7 @@ class Event extends EventEmitter {
this.isMiddleMousedown ||
(useLeftKeySelectionRightKeyDrag
? this.isRightMousedown
: this.isLeftMousedown)
: this.isLeftMousedown)
) {
e.preventDefault()
this.emit('drag', e, this)
@@ -133,18 +133,10 @@ class Event extends EventEmitter {
e.stopPropagation()
e.preventDefault()
let dir
// 解决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 {
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
}
if (e.deltaY < 0) dir = CONSTANTS.DIR.UP
if (e.deltaY > 0) dir = CONSTANTS.DIR.DOWN
if (e.deltaX < 0) dir = CONSTANTS.DIR.LEFT
if (e.deltaX > 0) dir = CONSTANTS.DIR.RIGHT
// 判断是否是触控板
let isTouchPad = false
// mac、windows

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,11 @@
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils'
import { ERROR_TYPES } from '../../constants/constant'
import {
getStrWithBrFromHtml,
checkNodeOuter,
focusInput,
selectAllInput,
htmlEscape
} from '../../utils'
import { ERROR_TYPES, CONSTANTS } from '../../constants/constant'
// 节点文字编辑类
export default class TextEdit {
@@ -22,8 +28,11 @@ export default class TextEdit {
bindEvent() {
this.show = this.show.bind(this)
this.onScale = this.onScale.bind(this)
this.onKeydown = this.onKeydown.bind(this)
// 节点双击事件
this.mindMap.on('node_dblclick', this.show)
this.mindMap.on('node_dblclick', (node, e, isInserting) => {
this.show({ node, e, isInserting })
})
// 点击事件
this.mindMap.on('draw_click', () => {
// 隐藏文本编辑框
@@ -47,24 +56,50 @@ export default class TextEdit {
this.mindMap.on('before_node_active', () => {
this.hideEditTextBox()
})
// 鼠标滚动事件
this.mindMap.on('mousewheel', () => {
if (
this.mindMap.opt.mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.MOVE
) {
this.hideEditTextBox()
}
})
// 注册编辑快捷键
this.mindMap.keyCommand.addShortcut('F2', () => {
if (this.renderer.activeNodeList.length <= 0) {
return
}
this.show(this.renderer.activeNodeList[0])
this.show({
node: this.renderer.activeNodeList[0]
})
})
this.mindMap.on('scale', this.onScale)
// // 监听按键事件,判断是否自动进入文本编辑模式
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
window.addEventListener('keydown', e => {
const activeNodeList = this.mindMap.renderer.activeNodeList
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
const node = activeNodeList[0]
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
if (node && this.checkIsAutoEnterTextEditKey(e)) {
this.show(node, e, false, true)
}
window.addEventListener('keydown', this.onKeydown)
}
this.mindMap.on('beforeDestroy', () => {
this.unBindEvent()
})
}
// 解绑事件
unBindEvent() {
window.removeEventListener('keydown', this.onKeydown)
}
// 按键事件
onKeydown(e) {
const activeNodeList = this.mindMap.renderer.activeNodeList
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
const node = activeNodeList[0]
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
if (node && this.checkIsAutoEnterTextEditKey(e)) {
this.show({
node,
e,
isInserting: false,
isFromKeyDown: true
})
}
}
@@ -94,12 +129,17 @@ export default class TextEdit {
// 显示文本编辑框
// isInserting是否是刚创建的节点
// isFromKeyDown是否是在按键事件进入的编辑
async show(node, e, isInserting = false, isFromKeyDown = false) {
async show({
node,
isInserting = false,
isFromKeyDown = false,
isFromScale = false
}) {
// 使用了自定义节点内容那么不响应编辑事件
if (node.isUseCustomNodeContent()) {
return
}
let { beforeTextEdit } = this.mindMap.opt
const { beforeTextEdit } = this.mindMap.opt
if (typeof beforeTextEdit === 'function') {
let isShow = false
try {
@@ -111,14 +151,21 @@ export default class TextEdit {
if (!isShow) return
}
this.currentNode = node
let { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
const { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
this.mindMap.view.translateXY(offsetLeft, offsetTop)
let rect = node._textData.node.node.getBoundingClientRect()
const rect = node._textData.node.node.getBoundingClientRect()
const params = {
node,
rect,
isInserting,
isFromKeyDown,
isFromScale
}
if (this.mindMap.richText) {
this.mindMap.richText.showEditText(node, rect, isInserting, isFromKeyDown)
this.mindMap.richText.showEditText(params)
return
}
this.showEditTextBox(node, rect, isInserting, isFromKeyDown)
this.showEditTextBox(params)
}
// 处理画布缩放
@@ -132,15 +179,20 @@ export default class TextEdit {
this.cacheEditingText = this.getEditText()
this.showTextEdit = false
}
this.show(this.currentNode)
this.show({
node: this.currentNode,
isFromScale: true
})
}
// 显示文本编辑框
showEditTextBox(node, rect, isInserting, isFromKeyDown) {
showEditTextBox({ node, rect, isInserting, isFromKeyDown, isFromScale }) {
if (this.showTextEdit) return
const { nodeTextEditZIndex, textAutoWrapWidth, selectTextOnEnterEditText } =
this.mindMap.opt
this.mindMap.emit('before_show_text_edit')
if (!isFromScale) {
this.mindMap.emit('before_show_text_edit')
}
this.registerTmpShortcut()
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
@@ -167,9 +219,11 @@ export default class TextEdit {
let scale = this.mindMap.view.scale
let lineHeight = node.style.merge('lineHeight')
let fontSize = node.style.merge('fontSize')
let textLines = (this.cacheEditingText || node.nodeData.data.text).split(
/\n/gim
)
let textLines = (this.cacheEditingText || node.getData('text'))
.split(/\n/gim)
.map(item => {
return htmlEscape(item)
})
let isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
node.style.domText(this.textEditNode, scale, isMultiLine)
this.textEditNode.style.zIndex = nodeTextEditZIndex
@@ -188,35 +242,16 @@ export default class TextEdit {
this.showTextEdit = true
// 选中文本
// if (!this.cacheEditingText) {
// this.selectNodeText()
// selectAllInput(this.textEditNode)
// }
if (isInserting || (selectTextOnEnterEditText && !isFromKeyDown)) {
this.selectNodeText()
selectAllInput(this.textEditNode)
} else {
this.focus()
focusInput(this.textEditNode)
}
this.cacheEditingText = ''
}
// 聚焦
focus() {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.textEditNode)
range.collapse()
selection.removeAllRanges()
selection.addRange(range)
}
// 选中文本
selectNodeText() {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.textEditNode)
selection.removeAllRanges()
selection.addRange(range)
}
// 获取当前正在编辑的内容
getEditText() {
return getStrWithBrFromHtml(this.textEditNode.innerHTML)

View File

@@ -1,12 +1,14 @@
import Style from './Style'
import Shape from './Shape'
import { G, ForeignObject, SVG, Rect } from '@svgdotjs/svg.js'
import { G, ForeignObject, Rect } from '@svgdotjs/svg.js'
import nodeGeneralizationMethods from './nodeGeneralization'
import nodeExpandBtnMethods from './nodeExpandBtn'
import nodeCommandWrapsMethods from './nodeCommandWraps'
import nodeCreateContentsMethods from './nodeCreateContents'
import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect'
import nodeCooperateMethods from './nodeCooperate'
import { CONSTANTS } from '../../../constants/constant'
import { copyNodeTree } from '../../../utils/index'
// 节点类
class Node {
@@ -14,14 +16,16 @@ class Node {
constructor(opt = {}) {
// 节点数据
this.nodeData = this.handleData(opt.data || {})
// id
// uid
this.uid = opt.uid
// 控制实例
this.mindMap = opt.mindMap
// 渲染实例
this.renderer = opt.renderer
// 渲染器
this.draw = opt.draw || null
this.draw = this.mindMap.draw
this.nodeDraw = this.mindMap.nodeDraw
this.lineDraw = this.mindMap.lineDraw
// 样式实例
this.style = new Style(this)
// 形状实例
@@ -55,6 +59,8 @@ class Node {
this.parent = opt.parent || null
// 子节点
this.children = opt.children || []
// 当前同时操作该节点的用户列表
this.userList = []
// 节点内容的容器
this.group = null
this.shapeNode = null // 节点形状节点
@@ -68,15 +74,16 @@ class Node {
this._tagData = null
this._noteData = null
this.noteEl = null
this.noteContentIsShow = false
this._expandBtn = null
this._lastExpandBtnType = null
this._showExpandBtn = false
this._openExpandNode = null
this._closeExpandNode = null
this._fillExpandNode = null
this._userListGroup = null
this._lines = []
this._generalizationLine = null
this._generalizationNode = null
this._generalizationList = []
this._unVisibleRectRegionNode = null
this._isMouseenter = false
// 尺寸信息
@@ -121,6 +128,12 @@ class Node {
Object.keys(nodeCreateContentsMethods).forEach(item => {
this[item] = nodeCreateContentsMethods[item].bind(this)
})
// 协同相关
if (this.mindMap.cooperate) {
Object.keys(nodeCooperateMethods).forEach(item => {
this[item] = nodeCooperateMethods[item].bind(this)
})
}
// 初始化
this.getSize()
}
@@ -166,6 +179,7 @@ class Node {
let { isUseCustomNodeContent, customCreateNodeContent } = this.mindMap.opt
if (isUseCustomNodeContent && customCreateNodeContent) {
this._customNodeContent = customCreateNodeContent(this)
this._customNodeContent.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
}
// 如果没有返回内容,那么还是使用内置的节点内容
if (this._customNodeContent) return
@@ -283,6 +297,8 @@ class Node {
this.group.add(this.shapeNode)
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
this.renderExpandBtnPlaceholderRect()
// 创建协同头像节点
if (this.createUserListNode) this.createUserListNode()
// 概要节点添加一个带所属节点id的类名
if (this.isGeneralization && this.generalizationBelongNode) {
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
@@ -394,7 +410,11 @@ class Node {
this.active(e)
})
this.group.on('mousedown', e => {
const { readonly, enableCtrlKeyNodeSelection, useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
const {
readonly,
enableCtrlKeyNodeSelection,
useLeftKeySelectionRightKeyDrag
} = this.mindMap.opt
// 只读模式不需要阻止冒泡
if (!readonly) {
if (this.isRoot) {
@@ -412,22 +432,17 @@ class Node {
// 多选和取消多选
if (e.ctrlKey && enableCtrlKeyNodeSelection) {
this.isMultipleChoice = true
let isActive = this.nodeData.data.isActive
let isActive = this.getData('isActive')
if (!isActive)
this.mindMap.emit(
'before_node_active',
this,
this.renderer.activeNodeList
)
this.mindMap.execCommand('SET_NODE_ACTIVE', this, !isActive)
this.mindMap.renderer[isActive ? 'removeActiveNode' : 'addActiveNode'](
this
)
this.mindMap.emit(
'node_active',
isActive ? null : this,
[...this.mindMap.renderer.activeNodeList]
)
this.mindMap.renderer[
isActive ? 'removeNodeFromActiveList' : 'addNodeToActiveList'
](this)
this.renderer.emitNodeActiveEvent(isActive ? null : this)
}
this.mindMap.emit('node_mousedown', this, e)
})
@@ -438,19 +453,27 @@ class Node {
this.mindMap.emit('node_mouseup', this, e)
})
this.group.on('mouseenter', e => {
if (this.isDrag) return
this._isMouseenter = true
// 显示展开收起按钮
this.showExpandBtn()
if (this.isGeneralization) {
this.handleGeneralizationMouseenter()
}
this.mindMap.emit('node_mouseenter', this, e)
})
this.group.on('mouseleave', e => {
if (!this._isMouseenter) return
this._isMouseenter = false
this.hideExpandBtn()
if (this.isGeneralization) {
this.handleGeneralizationMouseleave()
}
this.mindMap.emit('node_mouseleave', this, e)
})
// 双击事件
this.group.on('dblclick', e => {
if (this.mindMap.opt.readonly) {
if (this.mindMap.opt.readonly || e.ctrlKey) {
return
}
e.stopPropagation()
@@ -466,13 +489,20 @@ class Node {
e.stopPropagation()
e.preventDefault()
// 如果是多选节点结束,那么不要触发右键菜单事件
if(this.mindMap.select && !useLeftKeySelectionRightKeyDrag && this.mindMap.select.hasSelectRange()) {
if (
this.mindMap.select &&
!useLeftKeySelectionRightKeyDrag &&
this.mindMap.select.hasSelectRange()
) {
return
}
if (this.nodeData.data.isActive) {
this.renderer.clearActive()
// 如果有且只有当前节点激活了,那么不需要重新激活
if (
!(this.getData('isActive') && this.renderer.activeNodeList.length === 1)
) {
this.renderer.clearActiveNodeList()
this.active(e)
}
this.active(e)
this.mindMap.emit('node_contextmenu', e, this)
})
}
@@ -483,14 +513,13 @@ class Node {
return
}
e && e.stopPropagation()
if (this.nodeData.data.isActive) {
if (this.getData('isActive')) {
return
}
this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList)
this.renderer.clearActive()
this.mindMap.execCommand('SET_NODE_ACTIVE', this, true)
this.renderer.addActiveNode(this)
this.mindMap.emit('node_active', this, [...this.renderer.activeNodeList])
this.renderer.clearActiveNodeList()
this.renderer.addNodeToActiveList(this)
this.renderer.emitNodeActiveEvent(this)
}
// 更新节点
@@ -498,7 +527,7 @@ class Node {
if (!this.group) {
return
}
this.updateNodeActive()
this.updateNodeActiveClass()
let { alwaysShowExpandBtn } = this.mindMap.opt
if (alwaysShowExpandBtn) {
// 需要移除展开收缩按钮
@@ -509,7 +538,7 @@ class Node {
this.renderExpandBtn()
}
} else {
let { isActive, expand } = this.nodeData.data
let { isActive, expand } = this.getData()
// 展开状态且非激活状态,且当前鼠标不在它上面,才隐藏
if (expand && !isActive && !this._isMouseenter) {
this.hideExpandBtn()
@@ -519,6 +548,8 @@ class Node {
}
// 更新概要
this.renderGeneralization()
// 更新协同头像
if (this.updateUserListNode) this.updateUserListNode()
// 更新节点位置
let t = this.group.transform()
// // 如果上次不在可视区内,且本次也不在,那么直接返回
@@ -572,12 +603,25 @@ class Node {
}
// 更新节点激活状态
updateNodeActive() {
updateNodeActiveClass() {
if (!this.group) return
const isActive = this.nodeData.data.isActive
const isActive = this.getData('isActive')
this.group[isActive ? 'addClass' : 'removeClass']('active')
}
// 根据是否激活更新节点
updateNodeByActive(active) {
if (this.group) {
// 切换激活状态,需要切换展开收起按钮的显隐
if (active) {
this.showExpandBtn()
} else {
this.hideExpandBtn()
}
this.updateNodeActiveClass()
}
}
// 递归渲染
render(callback = () => {}) {
// 节点
@@ -591,11 +635,11 @@ class Node {
cursor: 'default'
})
this.bindGroupEvent()
this.draw.add(this.group)
this.nodeDraw.add(this.group)
this.layout()
this.update()
} else {
this.draw.add(this.group)
this.nodeDraw.add(this.group)
if (this.needLayout) {
this.needLayout = false
this.layout()
@@ -607,7 +651,7 @@ class Node {
if (
this.children &&
this.children.length &&
this.nodeData.data.expand !== false
this.getData('expand') !== false
) {
let index = 0
this.children.forEach(item => {
@@ -652,6 +696,9 @@ class Node {
this.removeGeneralization()
this.removeLine()
this.group = null
if (this.parent) {
this.parent.removeLine()
}
}
// 隐藏节点
@@ -695,9 +742,61 @@ class Node {
}
}
// 设置节点透明度
// 包括连接线和下级节点
setOpacity(val) {
// 自身及连线
this.group.opacity(val)
this._lines.forEach(line => {
line.opacity(val)
})
// 子节点
this.children.forEach(item => {
item.setOpacity(val)
})
// 概要节点
this.setGeneralizationOpacity(val)
}
// 隐藏子节点
hideChildren() {
this._lines.forEach(item => {
item.hide()
})
if (this.children && this.children.length) {
this.children.forEach(item => {
item.hide()
})
}
}
// 显示子节点
showChildren() {
this._lines.forEach(item => {
item.show()
})
if (this.children && this.children.length) {
this.children.forEach(item => {
item.show()
})
}
}
// 被拖拽中
startDrag() {
this.isDrag = true
this.group.addClass('smm-node-dragging')
}
// 拖拽结束
endDrag() {
this.isDrag = false
this.group.removeClass('smm-node-dragging')
}
// 连线
renderLine(deep = false) {
if (this.nodeData.data.expand === false) {
if (this.getData('expand') === false) {
return
}
let childrenLen = this.nodeData.children.length
@@ -711,7 +810,7 @@ class Node {
if (childrenLen > this._lines.length) {
// 创建缺少的线
new Array(childrenLen - this._lines.length).fill(0).forEach(() => {
this._lines.push(this.draw.path())
this._lines.push(this.lineDraw.path())
})
} else if (childrenLen < this._lines.length) {
// 删除多余的线
@@ -751,7 +850,7 @@ class Node {
return this.customLeft !== undefined && this.customTop !== undefined
}
// 检查节点是否存在自定义位置的祖先节点
// 检查节点是否存在自定义位置的祖先节点,包含自身
ancestorHasCustomPosition() {
let node = this
while (node) {
@@ -763,6 +862,18 @@ class Node {
return false
}
// 检查是否存在有概要的祖先节点
ancestorHasGeneralization() {
let node = this.parent
while (node) {
if (node.checkHasGeneralization()) {
return true
}
node = node.parent
}
return false
}
// 添加子节点
addChildren(node) {
this.children.push(node)
@@ -793,7 +904,7 @@ class Node {
}
// 检测当前节点是否是某个节点的祖先节点
isParent(node) {
isAncestor(node) {
if (this.uid === node.uid) {
return false
}
@@ -807,6 +918,18 @@ class Node {
return false
}
// 检查当前节点是否是某个节点的父节点
isParent(node) {
if (this.uid === node.uid) {
return false
}
const parent = node.parent
if (parent && this.uid === parent.uid) {
return true
}
return false
}
// 检测当前节点是否是某个节点的兄弟节点
isBrother(node) {
if (!this.parent || this.uid === node.uid) {
@@ -817,9 +940,18 @@ class Node {
})
}
// 获取该节点在兄弟节点列表中的索引
getIndexInBrothers() {
return this.parent && this.parent.children
? this.parent.children.findIndex(item => {
return item.uid === this.uid
})
: -1
}
// 获取padding值
getPaddingVale() {
let { isActive } = this.nodeData.data
let { isActive } = this.getData()
return {
paddingX: this.getStyle('paddingX', true, isActive),
paddingY: this.getStyle('paddingY', true, isActive)
@@ -862,13 +994,42 @@ class Node {
// 获取数据
getData(key) {
return key ? this.nodeData.data[key] || '' : this.nodeData.data
return key ? this.nodeData.data[key] : this.nodeData.data
}
// 获取该节点的纯数据,即不包含对节点实例的引用
getPureData(removeActiveState = true, removeId = false) {
return copyNodeTree({}, this, removeActiveState, removeId)
}
// 是否存在自定义样式
hasCustomStyle() {
return this.style.hasCustomStyle()
}
// 获取节点的尺寸和位置信息,宽高是应用了缩放效果后的实际宽高,位置是相对于浏览器窗口左上角的位置
getRect() {
return this.group.rbox()
}
// 获取节点的尺寸和位置信息,宽高是应用了缩放效果后的实际宽高,位置信息相对于画布
getRectInSvg() {
let { scaleX, scaleY, translateX, translateY } =
this.mindMap.draw.transform()
let { left, top, width, height } = this
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
return {
left,
right,
top,
bottom,
width: width * scaleX,
height: height * scaleY
}
}
}
export default Node

View File

@@ -1,5 +1,7 @@
import { tagColorList } from '../../../constants/constant'
import { checkIsNodeStyleDataKey } from '../../../utils/index'
import {
checkIsNodeStyleDataKey,
generateColorByContent
} from '../../../utils/index'
const rootProp = ['paddingX', 'paddingY']
const backgroundStyleProps = [
@@ -86,7 +88,7 @@ class Style {
// 获取自身自定义样式
getSelfStyle(prop) {
return this.ctx.nodeData.data[prop]
return this.ctx.getData(prop)
}
// 矩形
@@ -105,7 +107,7 @@ class Style {
// !this.ctx.isRoot &&
// !this.ctx.isGeneralization &&
// this.ctx.mindMap.themeConfig.nodeUseLineStyle &&
// !this.ctx.nodeData.data.isActive
// !this.ctx.getData('isActive')
// ) {
// return
// }
@@ -163,10 +165,10 @@ class Style {
}
// 标签文字
tagText(node, index) {
tagText(node) {
node
.fill({
color: tagColorList[index].color
color: '#fff'
})
.css({
'font-size': '12px'
@@ -174,9 +176,9 @@ class Style {
}
// 标签矩形
tagRect(node, index) {
tagRect(node, text, color) {
node.fill({
color: tagColorList[index].background
color: color || generateColorByContent(text.node.textContent)
})
}
@@ -223,7 +225,7 @@ class Style {
// 是否设置了自定义的样式
hasCustomStyle() {
let res = false
Object.keys(this.ctx.nodeData.data).forEach(item => {
Object.keys(this.ctx.getData()).forEach(item => {
if (checkIsNodeStyleDataKey(item)) {
res = true
}
@@ -234,12 +236,9 @@ class Style {
// hover和激活节点
hoverNode(node) {
const { hoverRectColor } = this.ctx.mindMap.opt
node
.radius(5)
.fill('none')
.stroke({
color: hoverRectColor
})
node.radius(5).fill('none').stroke({
color: hoverRectColor
})
}
}

View File

@@ -0,0 +1,104 @@
import { Circle, G, Text, Image } from '@svgdotjs/svg.js'
import { generateColorByContent } from '../../../utils/index'
// 协同相关功能
// 创建容器
function createUserListNode() {
// 如果没有注册协作插件,那么需要创建
if (!this.mindMap.cooperate) return
this._userListGroup = new G()
this.group.add(this._userListGroup)
}
// 创建文本头像
function createTextAvatar(item) {
const { avatarSize, fontSize } = this.mindMap.opt.cooperateStyle
const g = new G()
const str = item.isMore ? item.name : String(item.name)[0]
// 圆
const circle = new Circle().size(avatarSize, avatarSize)
circle.fill({
color: item.color || generateColorByContent(str)
})
// 文本
const text = new Text()
.text(str)
.fill({
color: '#fff'
})
.css({
'font-size': fontSize
})
.dx(-fontSize / 2)
.dy((avatarSize - fontSize) / 2)
g.add(circle).add(text)
return g
}
// 创建图片头像
function createImageAvatar(item) {
const { avatarSize } = this.mindMap.opt.cooperateStyle
return new Image().load(item.avatar).size(avatarSize, avatarSize)
}
// 更新渲染
function updateUserListNode() {
if (!this._userListGroup) return
const { avatarSize } = this.mindMap.opt.cooperateStyle
this._userListGroup.clear()
// 根据当前节点长度计算最多能显示几个
const length = this.userList.length
const maxShowCount = Math.floor(this.width / avatarSize)
const list = []
if (length > maxShowCount) {
// 如果当前用户数量比最多能显示的多,最后需要显示一个提示信息
list.push(...this.userList.slice(0, maxShowCount - 1), {
isMore: true,
name: '+' + (length - maxShowCount + 1)
})
} else {
list.push(...this.userList)
}
list.forEach((item, index) => {
let node = null
if (item.avatar) {
node = this.createImageAvatar(item)
} else {
node = this.createTextAvatar(item)
}
node.x(index * avatarSize).cy(-avatarSize / 2)
this._userListGroup.add(node)
})
}
// 添加用户
function addUser(userInfo) {
if (
this.userList.find(item => {
return item.id == userInfo.id
})
)
return
this.userList.push(userInfo)
this.updateUserListNode()
}
// 移除用户
function removeUser(userInfo) {
const index = this.userList.findIndex(item => {
return item.id == userInfo.id
})
if (index === -1) return
this.userList.splice(index, 1)
this.updateUserListNode()
}
export default {
createUserListNode,
updateUserListNode,
createTextAvatar,
createImageAvatar,
addUser,
removeUser
}

View File

@@ -3,7 +3,8 @@ import {
resizeImgSize,
removeHtmlStyle,
addHtmlStyle,
checkIsRichText
checkIsRichText,
isUndef
} from '../../../utils'
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
import iconsSvg from '../../../svg/icons'
@@ -11,14 +12,14 @@ import { CONSTANTS, commonCaches } from '../../../constants/constant'
// 创建图片节点
function createImgNode() {
let img = this.nodeData.data.image
let img = this.getData('image')
if (!img) {
return
}
let imgSize = this.getImgShowSize()
let node = new Image().load(img).size(...imgSize)
if (this.nodeData.data.imageTitle) {
node.attr('title', this.nodeData.data.imageTitle)
if (this.getData('imageTitle')) {
node.attr('title', this.getData('imageTitle'))
}
node.on('dblclick', e => {
this.mindMap.emit('node_img_dblclick', this, e)
@@ -41,7 +42,7 @@ function createImgNode() {
// 获取图片显示宽高
function getImgShowSize() {
const { custom, width, height } = this.nodeData.data.imageSize
const { custom, width, height } = this.getData('imageSize')
// 如果是自定义了图片的宽高,那么不受最大宽高限制
if (custom) return [width, height]
return resizeImgSize(
@@ -54,7 +55,7 @@ function getImgShowSize() {
// 创建icon节点
function createIconNode() {
let _data = this.nodeData.data
let _data = this.getData()
if (!_data.icon || _data.icon.length <= 0) {
return []
}
@@ -90,7 +91,7 @@ function createRichTextNode() {
let g = new G()
// 重新设置富文本节点内容
let recoverText = false
if (this.nodeData.data.resetRichText) {
if (this.getData('resetRichText')) {
delete this.nodeData.data.resetRichText
recoverText = true
}
@@ -101,7 +102,7 @@ function createRichTextNode() {
}
}
if (recoverText) {
let text = this.nodeData.data.text
let text = this.getData('text')
// 判断节点内容是否是富文本
let isRichText = checkIsRichText(text)
// 样式字符串
@@ -115,9 +116,11 @@ function createRichTextNode() {
// 非富文本
text = `<p><span style="${style}">${text}</span></p>`
}
this.nodeData.data.text = text
this.setData({
text: text
})
}
let html = `<div>${this.nodeData.data.text}</div>`
let html = `<div>${this.getData('text')}</div>`
if (!commonCaches.measureRichtextNodeTextSizeEl) {
commonCaches.measureRichtextNodeTextSizeEl = document.createElement('div')
commonCaches.measureRichtextNodeTextSizeEl.style.position = 'fixed'
@@ -156,7 +159,7 @@ function createRichTextNode() {
// 创建文本节点
function createTextNode() {
if (this.nodeData.data.richText) {
if (this.getData('richText')) {
return this.createRichTextNode()
}
let g = new G()
@@ -164,7 +167,10 @@ function createTextNode() {
let lineHeight = this.getStyle('lineHeight', false)
// 文本超长自动换行
let textStyle = this.style.getTextFontStyle()
let textArr = this.nodeData.data.text.split(/\n/gim)
let textArr = []
if (!isUndef(this.getData('text'))) {
textArr = String(this.getData('text')).split(/\n/gim)
}
let maxWidth = this.mindMap.opt.textAutoWrapWidth
let isMultiLine = false
textArr.forEach((item, index) => {
@@ -211,7 +217,7 @@ function createTextNode() {
// 创建超链接节点
function createHyperlinkNode() {
let { hyperlink, hyperlinkTitle } = this.nodeData.data
let { hyperlink, hyperlinkTitle } = this.getData()
if (!hyperlink) {
return
}
@@ -241,7 +247,7 @@ function createHyperlinkNode() {
// 创建标签节点
function createTagNode() {
let tagData = this.nodeData.data.tag
let tagData = this.getData('tag')
if (!tagData || tagData.length <= 0) {
return []
}
@@ -249,12 +255,15 @@ function createTagNode() {
tagData.slice(0, this.mindMap.opt.maxTag).forEach((item, index) => {
let tag = new G()
// 标签文本
let text = new Text().text(item).x(8).cy(10)
let text = new Text().text(item).x(8).cy(8)
this.style.tagText(text, index)
let { width } = text.bbox()
// 标签矩形
let rect = new Rect().size(width + 16, 20)
this.style.tagRect(rect, index)
// 先从自定义的颜色中获取颜色,没有的话就按照内容生成
const tagsColorList = this.mindMap.opt.tagsColorMap || {}
const color = tagsColorList[text.node.textContent]
this.style.tagRect(rect, text, color)
tag.add(rect).add(text)
nodes.push({
node: tag,
@@ -267,7 +276,7 @@ function createTagNode() {
// 创建备注节点
function createNoteNode() {
if (!this.nodeData.data.note) {
if (!this.getData('note')) {
return null
}
let iconSize = this.mindMap.themeConfig.iconSize
@@ -295,19 +304,20 @@ function createNoteNode() {
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.noteEl)
}
this.noteEl.innerText = this.nodeData.data.note
this.noteEl.innerText = this.getData('note')
}
node.on('mouseover', () => {
let { left, top } = node.node.getBoundingClientRect()
const { left, top } = this.getNoteContentPosition()
if (!this.mindMap.opt.customNoteContentShow) {
this.noteEl.style.left = left + 'px'
this.noteEl.style.top = top + iconSize + 'px'
this.noteEl.style.top = top + 'px'
this.noteEl.style.display = 'block'
} else {
this.mindMap.opt.customNoteContentShow.show(
this.nodeData.data.note,
this.getData('note'),
left,
top + iconSize
top,
this
)
}
})
@@ -325,6 +335,19 @@ function createNoteNode() {
}
}
// 获取节点备注显示位置
function getNoteContentPosition() {
const iconSize = this.mindMap.themeConfig.iconSize
const { scaleY } = this.mindMap.view.getTransformData().transform
const iconSizeAddScale = iconSize * scaleY
let { left, top } = this._noteData.node.node.getBoundingClientRect()
top += iconSizeAddScale
return {
left,
top
}
}
// 测量自定义节点内容元素的宽高
function measureCustomNodeContentSize(content) {
if (!commonCaches.measureCustomNodeContentSizeEl) {
@@ -359,6 +382,7 @@ export default {
createHyperlinkNode,
createTagNode,
createNoteNode,
getNoteContentPosition,
measureCustomNodeContentSize,
isUseCustomNodeContent
}

View File

@@ -52,7 +52,7 @@ function sumNode(data = []) {
}
// 创建或更新展开收缩按钮内容
function updateExpandBtnNode() {
let { expand } = this.nodeData.data
let { expand } = this.getData()
// 如果本次和上次的展开状态一样则返回
if (expand === this._lastExpandBtnType) return
if (this._expandBtn) {
@@ -70,7 +70,8 @@ function updateExpandBtnNode() {
if (this._expandBtn) {
// 如果是收起按钮加上边框
let { isShowExpandNum, expandBtnStyle, expandBtnNumHandler } = this.mindMap.opt
let { isShowExpandNum, expandBtnStyle, expandBtnNumHandler } =
this.mindMap.opt
if (isShowExpandNum) {
if (!expand) {
// 数字按钮添加边框
@@ -128,13 +129,14 @@ function renderExpandBtn() {
this.mindMap.execCommand(
'SET_NODE_EXPAND',
this,
!this.nodeData.data.expand
!this.getData('expand')
)
this.mindMap.emit('expand_btn_click', this)
})
this._expandBtn.on('dblclick', e => {
e.stopPropagation()
})
this._expandBtn.addClass('smm-expand-btn')
this.group.add(this._expandBtn)
}
this._showExpandBtn = true
@@ -162,7 +164,7 @@ function showExpandBtn() {
function hideExpandBtn() {
if (this.mindMap.opt.alwaysShowExpandBtn || this._isMouseenter) return
// 非激活状态且展开状态鼠标移出才隐藏按钮
let { isActive, expand } = this.nodeData.data
let { isActive, expand } = this.getData()
if (!isActive && expand) {
setTimeout(() => {
this.removeExpandBtn()

View File

@@ -1,118 +1,218 @@
import Node from './Node'
import { createUid } from '../../../utils/index'
// 获取节点概要数据
function formatGetGeneralization() {
const data = this.getData('generalization')
return Array.isArray(data) ? data : data ? [data] : []
}
// 检查是否存在概要
function checkHasGeneralization () {
return !!this.nodeData.data.generalization
function checkHasGeneralization() {
return this.formatGetGeneralization().length > 0
}
// 检查是否存在自身的概要,非子节点区间
function checkHasSelfGeneralization() {
const list = this.formatGetGeneralization()
return !!list.find(item => {
return !item.range || item.range.length <= 0
})
}
// 获取概要节点所在的概要列表里的索引
function getGeneralizationNodeIndex(node) {
return this._generalizationList.findIndex(item => {
return item.generalizationNode.uid === node.uid
})
}
// 创建概要节点
function createGeneralizationNode () {
function createGeneralizationNode() {
if (this.isGeneralization || !this.checkHasGeneralization()) {
return
}
if (!this._generalizationLine) {
this._generalizationLine = this.draw.path()
}
if (!this._generalizationNode) {
this._generalizationNode = new Node({
data: {
data: this.nodeData.data.generalization
},
uid: 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)
let maxWidth = 0
let maxHeight = 0
const list = this.formatGetGeneralization()
list.forEach((item, index) => {
let cur = this._generalizationList[index]
if (!cur) {
cur = this._generalizationList[index] = {}
}
}
// 所属节点
cur.node = this
// 区间范围
cur.range = item.range
// 线和节点
if (!cur.generalizationLine) {
cur.generalizationLine = this.lineDraw.path()
}
if (!cur.generalizationNode) {
cur.generalizationNode = new Node({
data: {
data: item
},
uid: createUid(),
renderer: this.renderer,
mindMap: this.mindMap,
isGeneralization: true
})
}
// 关联所属节点
cur.generalizationNode.generalizationBelongNode = this
// 大小
if (cur.generalizationNode.width > maxWidth)
maxWidth = cur.generalizationNode.width
if (cur.generalizationNode.height > maxHeight)
maxHeight = cur.generalizationNode.height
// 如果该概要为激活状态,那么加入激活节点列表
if (item.isActive) {
this.renderer.addNodeToActiveList(cur.generalizationNode)
}
})
this._generalizationNodeWidth = maxWidth
this._generalizationNodeHeight = maxHeight
}
// 更新概要节点
function updateGeneralization () {
function updateGeneralization() {
if (this.isGeneralization) return
this.removeGeneralization()
this.createGeneralizationNode()
}
// 渲染概要节点
function renderGeneralization () {
function renderGeneralization() {
if (this.isGeneralization) return
if (!this.checkHasGeneralization()) {
this.updateGeneralizationData()
const list = this.formatGetGeneralization()
if (list.length <= 0 || this.getData('expand') === false) {
this.removeGeneralization()
this._generalizationNodeWidth = 0
this._generalizationNodeHeight = 0
return
}
if (this.nodeData.data.expand === false) {
if (list.length !== this._generalizationList.length) {
this.removeGeneralization()
return
}
this.createGeneralizationNode()
this.renderer.layout.renderGeneralization(
this,
this._generalizationLine,
this._generalizationNode
)
this.style.generalizationLine(this._generalizationLine)
this._generalizationNode.render()
this.renderer.layout.renderGeneralization(this._generalizationList)
this._generalizationList.forEach(item => {
this.style.generalizationLine(item.generalizationLine)
item.generalizationNode.render()
})
}
// 更新节点概要数据
function updateGeneralizationData() {
const childrenLength = this.children.length
const list = this.formatGetGeneralization()
const newList = []
list.forEach(item => {
if (!item.range) {
newList.push(item)
return
}
if (
item.range.length > 0 &&
item.range[0] <= childrenLength - 1 &&
item.range[1] <= childrenLength - 1
) {
newList.push(item)
}
})
if (newList.length !== list.length) {
this.setData({
generalization: newList
})
}
}
// 删除概要节点
function removeGeneralization () {
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
}
this._generalizationList.forEach(item => {
if (item.generalizationLine) {
item.generalizationLine.remove()
item.generalizationLine = null
}
if (item.generalizationNode) {
// 删除概要节点时要同步从激活节点里删除
this.renderer.removeNodeFromActiveList(item.generalizationNode)
item.generalizationNode.remove()
item.generalizationNode = null
}
})
this._generalizationList = []
// hack修复当激活一个节点时创建概要然后立即激活创建的概要节点后会重复创建概要节点并且无法删除的问题
if (this.generalizationBelongNode) {
this.draw
this.nodeDraw
.find('.generalization_' + this.generalizationBelongNode.uid)
.remove()
}
}
// 隐藏概要节点
function hideGeneralization () {
function hideGeneralization() {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.hide()
}
if (this._generalizationNode) {
this._generalizationNode.hide()
}
this._generalizationList.forEach(item => {
if (item.generalizationLine) item.generalizationLine.hide()
if (item.generalizationNode) item.generalizationNode.hide()
})
}
// 显示概要节点
function showGeneralization () {
function showGeneralization() {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.show()
}
if (this._generalizationNode) {
this._generalizationNode.show()
this._generalizationList.forEach(item => {
if (item.generalizationLine) item.generalizationLine.show()
if (item.generalizationNode) item.generalizationNode.show()
})
}
// 设置概要节点的透明度
function setGeneralizationOpacity(val) {
this._generalizationList.forEach(item => {
item.generalizationLine.opacity(val)
item.generalizationNode.group.opacity(val)
})
}
// 处理概要节点鼠标移入事件
function handleGeneralizationMouseenter() {
const belongNode = this.generalizationBelongNode
const list = belongNode.formatGetGeneralization()
const index = belongNode.getGeneralizationNodeIndex(this)
const generalizationData = list[index]
// 区间概要,框子节点
if (
Array.isArray(generalizationData.range) &&
generalizationData.range.length > 0
) {
this.mindMap.renderer.highlightNode(belongNode, generalizationData.range)
} else {
// 否则框自己
this.mindMap.renderer.highlightNode(belongNode)
}
}
// 处理概要节点鼠标移出事件
function handleGeneralizationMouseleave() {
this.mindMap.renderer.closeHighlightNode()
}
export default {
checkHasGeneralization,
createGeneralizationNode,
updateGeneralization,
renderGeneralization,
removeGeneralization,
hideGeneralization,
showGeneralization
}
formatGetGeneralization,
checkHasGeneralization,
checkHasSelfGeneralization,
getGeneralizationNodeIndex,
createGeneralizationNode,
updateGeneralization,
updateGeneralizationData,
renderGeneralization,
removeGeneralization,
hideGeneralization,
showGeneralization,
setGeneralizationOpacity,
handleGeneralizationMouseenter,
handleGeneralizationMouseleave
}

View File

@@ -25,24 +25,19 @@ class View {
this.mindMap.keyCommand.addShortcut('Control+-', () => {
this.narrow()
})
this.mindMap.keyCommand.addShortcut('Control+Enter', () => {
this.reset()
})
this.mindMap.keyCommand.addShortcut('Control+i', () => {
this.fit()
})
this.mindMap.svg.on('dblclick', () => {
if (!this.mindMap.opt.enableDblclickReset) return
this.reset()
})
// 拖动视图
this.mindMap.event.on('mousedown', () => {
if (this.mindMap.opt.isDisableDrag) return
this.sx = this.x
this.sy = this.y
})
this.mindMap.event.on('drag', (e, event) => {
if (e.ctrlKey) {
// 按住ctrl键拖动为多选
// 按住ctrl键拖动为多选
// 禁用拖拽
if (e.ctrlKey || this.mindMap.opt.isDisableDrag) {
return
}
if (this.firstDrag) {
@@ -77,9 +72,12 @@ class View {
return customHandleMousewheel(e)
}
// 鼠标滚轮事件控制缩放
if (mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
if (mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM || e.ctrlKey) {
if (disableMouseWheelZoom) return
const { x: clientX, y: clientY } = this.mindMap.toPos(e.clientX, e.clientY)
const { x: clientX, y: clientY } = this.mindMap.toPos(
e.clientX,
e.clientY
)
let cx = mouseScaleCenterUseMousePosition ? clientX : undefined
let cy = mouseScaleCenterUseMousePosition ? clientY : undefined
switch (dir) {
@@ -264,7 +262,7 @@ class View {
let drawHeight = rect.height / origTransform.scaleY
let drawRatio = drawWidth / drawHeight
let { width: elWidth, height: elHeight } =
this.mindMap.el.getBoundingClientRect()
this.mindMap.elRect
elWidth = elWidth - fitPadding * 2
elHeight = elHeight - fitPadding * 2
let elRatio = elWidth / elHeight

View File

@@ -13,6 +13,7 @@ class Base {
this.mindMap = renderer.mindMap
// 绘图对象
this.draw = this.mindMap.draw
this.lineDraw = this.mindMap.lineDraw
// 根节点
this.root = null
this.lru = new Lru(this.mindMap.opt.maxNodeCacheCount)
@@ -90,7 +91,7 @@ class Base {
// 数据上没有保存节点引用但是通过uid找到了缓存的节点也可以复用
newNode = this.lru.get(data.data.uid)
// 保存该节点上一次的数据
let lastData = JSON.stringify(newNode.nodeData.data)
let lastData = JSON.stringify(newNode.getData())
let isLayerTypeChange = this.checkIsLayerTypeChange(
newNode.layerIndex,
layerIndex
@@ -126,9 +127,15 @@ class Base {
// 数据关联实际节点
data._node = newNode
if (data.data.isActive) {
this.renderer.addActiveNode(newNode)
this.renderer.addNodeToActiveList(newNode)
}
}
// 如果当前节点在激活节点列表里,那么添加上激活的状态
if (this.mindMap.renderer.findActiveNodeIndex(newNode) !== -1) {
newNode.setData({
isActive: true
})
}
// 根节点
if (isRoot) {
newNode.isRoot = true
@@ -293,12 +300,12 @@ class Base {
let { left, right, top, bottom } = walk(child)
// 概要内容的宽度
let generalizationWidth =
child.checkHasGeneralization() && child.nodeData.data.expand
child.checkHasGeneralization() && child.getData('expand')
? child._generalizationNodeWidth + generalizationNodeMargin
: 0
// 概要内容的高度
let generalizationHeight =
child.checkHasGeneralization() && child.nodeData.data.expand
child.checkHasGeneralization() && child.getData('expand')
? child._generalizationNodeHeight + generalizationNodeMargin
: 0
if (left - (dir === 'h' ? generalizationWidth : 0) < _left) {
@@ -339,6 +346,50 @@ class Base {
}
}
// 获取指定索引区间的子节点的边界范围
getChildrenBoundaries(node, dir, startIndex = 0, endIndex) {
let { generalizationLineMargin, generalizationNodeMargin } =
this.mindMap.themeConfig
const children = node.children.slice(startIndex, endIndex + 1)
let left = Infinity
let right = -Infinity
let top = Infinity
let bottom = -Infinity
children.forEach(item => {
const cur = this.getNodeBoundaries(item, dir)
left = cur.left < left ? cur.left : left
right = cur.right > right ? cur.right : right
top = cur.top < top ? cur.top : top
bottom = cur.bottom > bottom ? cur.bottom : bottom
})
return {
left,
right,
top,
bottom,
generalizationLineMargin,
generalizationNodeMargin
}
}
// 获取节点概要的渲染边界
getNodeGeneralizationRenderBoundaries(item, dir) {
let res = null
// 区间
if (item.range) {
res = this.getChildrenBoundaries(
item.node,
dir,
item.range[0],
item.range[1]
)
} else {
// 整体概要
res = this.getNodeBoundaries(item.node, dir)
}
return res
}
// 获取节点实际存在几个子节点
getNodeActChildrenLength(node) {
return node.nodeData.children && node.nodeData.children.length

View File

@@ -1,5 +1,5 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
// 目录组织图
class CatalogOrganization extends Base {
@@ -72,11 +72,7 @@ class CatalogOrganization extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (
node.nodeData.data.expand &&
node.children &&
node.children.length
) {
if (node.getData('expand') && node.children && node.children.length) {
let marginX = this.getMarginX(layerIndex + 1)
let marginY = this.getMarginY(layerIndex + 1)
if (isRoot) {
@@ -87,11 +83,18 @@ class CatalogOrganization extends Base {
totalLeft += cur.width + marginX
})
} else {
let totalTop = node.top + this.getNodeHeightWithGeneralization(node) + marginY + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
let totalTop =
node.top +
this.getNodeHeightWithGeneralization(node) +
marginY +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(cur => {
cur.left = node.left + node.width * 0.5
cur.top = totalTop
totalTop += this.getNodeHeightWithGeneralization(cur) + marginY + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
totalTop +=
this.getNodeHeightWithGeneralization(cur) +
marginY +
(this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
})
}
}
@@ -107,7 +110,7 @@ class CatalogOrganization extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
if (!node.getData('expand')) {
return
}
// 调整left
@@ -124,7 +127,13 @@ class CatalogOrganization extends Base {
let marginY = this.getMarginY(layerIndex + 1)
let totalHeight =
node.children.reduce((h, item) => {
return h + this.getNodeHeightWithGeneralization(item) + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
return (
h +
this.getNodeHeightWithGeneralization(item) +
(this.getNodeActChildrenLength(item) > 0
? item.expandBtnSize
: 0)
)
}, 0) +
len * marginY
this.updateBrothersTop(node, totalHeight)
@@ -134,7 +143,7 @@ class CatalogOrganization extends Base {
if (isRoot) {
let { right, left } = this.getNodeBoundaries(node, 'h')
let childrenWidth = right - left
let offset = (node.left - left) - (childrenWidth - node.width) / 2
let offset = node.left - left - (childrenWidth - node.width) / 2
this.updateChildren(node.children, 'left', offset)
}
},
@@ -146,9 +155,7 @@ class CatalogOrganization extends Base {
updateBrothersLeft(node, addWidth) {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition() || _index <= index) {
// 适配自定义位置
@@ -169,9 +176,7 @@ class CatalogOrganization extends Base {
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
@@ -234,14 +239,14 @@ class CatalogOrganization extends Base {
minx = Math.min(minx, x1)
maxx = Math.max(maxx, x1)
// 父节点的竖线
let line1 = this.draw.path()
let line1 = this.lineDraw.path()
node.style.line(line1)
line1.plot(`M ${x1},${y1} L ${x1},${y1 + s1}`)
node._lines.push(line1)
style && style(line1, node)
// 水平线
if (len > 0) {
let lin2 = this.draw.path()
let lin2 = this.lineDraw.path()
node.style.line(lin2)
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
node._lines.push(lin2)
@@ -302,7 +307,7 @@ class CatalogOrganization extends Base {
})
// 竖线
if (len > 0) {
let lin2 = this.draw.path()
let lin2 = this.lineDraw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
node.style.line(lin2)
if (maxy < y1 + expandBtnSize) {
@@ -330,24 +335,27 @@ class CatalogOrganization extends Base {
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeBoundaries(node, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
let x2 = right + generalizationLineMargin
let y2 = bottom
let cx = x1 + 20
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
gLine.plot(path)
gNode.left = right + generalizationNodeMargin
gNode.top = top + (bottom - top - gNode.height) / 2
renderGeneralization(list) {
list.forEach(item => {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
let x2 = right + generalizationLineMargin
let y2 = bottom
let cx = x1 + 20
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
item.generalizationLine.plot(path)
item.generalizationNode.left = right + generalizationNodeMargin
item.generalizationNode.top =
top + (bottom - top - item.generalizationNode.height) / 2
})
}
// 渲染展开收起按钮的隐藏占位元素

View File

@@ -1,5 +1,5 @@
import Base from './Base'
import { walk, asyncRun, degToRad } from '../utils'
import { walk, asyncRun, degToRad, getNodeIndexInNodeList } from '../utils'
import { CONSTANTS } from '../constants/constant'
import utils from './fishboneUtils'
@@ -112,7 +112,7 @@ class Fishbone extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
if (!node.getData('expand')) {
return
}
let params = { node, parent, layerIndex, ctx: this }
@@ -159,7 +159,8 @@ class Fishbone extends Base {
let marginY = this.getMarginY(node.layerIndex)
totalHeight +=
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + marginY
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
marginY
if (node.children.length) {
node.children.forEach(item => {
loop(item)
@@ -192,9 +193,7 @@ class Fishbone extends Base {
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
@@ -251,7 +250,7 @@ class Fishbone extends Base {
let nodeLineX = item.left
let offset = node.height / 2 + marginY
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
let line = this.draw.path()
let line = this.lineDraw.path()
if (this.checkIsTop(item)) {
line.plot(
`M ${nodeLineX - offsetX},${item.top + item.height + offset} L ${
@@ -272,7 +271,7 @@ class Fishbone extends Base {
// 从根节点出发的水平线
let nodeHalfTop = node.top + node.height / 2
let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1)
let line = this.draw.path()
let line = this.lineDraw.path()
line.plot(
`M ${node.left + node.width},${nodeHalfTop} L ${
maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
@@ -307,7 +306,7 @@ class Fishbone extends Base {
})
// 斜线
if (len >= 0) {
let line = this.draw.path()
let line = this.lineDraw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
let lineLength = maxx - node.left - node.width * this.indent
lineLength = Math.max(lineLength, 0)
@@ -358,24 +357,27 @@ class Fishbone extends Base {
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeBoundaries(node, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
let x2 = right + generalizationLineMargin
let y2 = bottom
let cx = x1 + 20
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
gLine.plot(path)
gNode.left = right + generalizationNodeMargin
gNode.top = top + (bottom - top - gNode.height) / 2
renderGeneralization(list) {
list.forEach(item => {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
let x2 = right + generalizationLineMargin
let y2 = bottom
let cx = x1 + 20
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
item.generalizationLine.plot(path)
item.generalizationNode.left = right + generalizationNodeMargin
item.generalizationNode.top =
top + (bottom - top - item.generalizationNode.height) / 2
})
}
// 渲染展开收起按钮的隐藏占位元素

View File

@@ -1,5 +1,5 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
import { CONSTANTS } from '../utils/constant'
const degToRad = deg => {
@@ -127,7 +127,7 @@ class Fishbone extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
if (!node.getData('expand')) {
return
}
// 调整top
@@ -170,7 +170,8 @@ class Fishbone extends Base {
item.top += _top
// 调整left
let offsetLeft =
(totalHeight2 + nodeTotalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
(totalHeight2 + nodeTotalHeight) /
Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
item.left += offsetLeft
totalHeight += offset
totalHeight2 += nodeTotalHeight
@@ -236,9 +237,7 @@ class Fishbone extends Base {
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
@@ -306,13 +305,15 @@ class Fishbone extends Base {
})
// 竖线
if (len > 0) {
let line = this.draw.path()
let line = this.lineDraw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
let lineLength = maxx - node.left - node.width * 0.3
if (node.parent && node.parent.isRoot) {
line.plot(
`M ${x},${top + height} L ${x + lineLength},${
top + height + Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
top +
height +
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
}`
)
} else {

View File

@@ -1,5 +1,5 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
import { CONSTANTS } from '../utils/constant'
const degToRad = deg => {
@@ -110,7 +110,7 @@ class Fishbone extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
if (!node.getData('expand')) {
return
}
// 调整top
@@ -140,7 +140,8 @@ class Fishbone extends Base {
node.top - (item.top - node.top) - nodeTotalHeight + node.height
// 调整left
let offsetLeft =
(nodeTotalHeight + totalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
(nodeTotalHeight + totalHeight) /
Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
item.left += offsetLeft
totalHeight += nodeTotalHeight
// 同步更新后代节点
@@ -205,9 +206,7 @@ class Fishbone extends Base {
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
@@ -275,7 +274,7 @@ class Fishbone extends Base {
})
// 竖线
if (len > 0) {
let line = this.draw.path()
let line = this.lineDraw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
let lineLength = maxx - node.left - node.width * 0.3
if (
@@ -285,14 +284,16 @@ class Fishbone extends Base {
) {
line.plot(
`M ${x},${top} L ${x + lineLength},${
top - Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
top -
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
}`
)
} else {
if (node.parent && node.parent.isRoot) {
line.plot(
`M ${x},${top} L ${x + lineLength},${
top - Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
top -
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
}`
)
} else {

View File

@@ -1,5 +1,5 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
// 逻辑结构图
class LogicalStructure extends Base {
@@ -57,8 +57,14 @@ class LogicalStructure extends Base {
(len + 1) * this.getMarginY(layerIndex + 1)
: 0
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeHeight = cur._node.checkHasGeneralization() ? cur._node._generalizationNodeHeight + this.getMarginY(layerIndex + 1) : 0
cur._node.childrenAreaHeight2 = Math.max(cur._node.childrenAreaHeight, generalizationNodeHeight)
let generalizationNodeHeight = cur._node.checkHasGeneralization()
? cur._node._generalizationNodeHeight +
this.getMarginY(layerIndex + 1)
: 0
cur._node.childrenAreaHeight2 = Math.max(
cur._node.childrenAreaHeight,
generalizationNodeHeight
)
},
true,
0
@@ -71,11 +77,7 @@ class LogicalStructure extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (
node.nodeData.data.expand &&
node.children &&
node.children.length
) {
if (node.getData('expand') && node.children && node.children.length) {
let marginY = this.getMarginY(layerIndex + 1)
// 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半
let top = node.top + node.height / 2 - node.childrenAreaHeight / 2
@@ -97,13 +99,14 @@ class LogicalStructure extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
if (!node.getData('expand')) {
return
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaHeight2 -
this.getMarginY(layerIndex + 1) * 2 - node.height
this.getMarginY(layerIndex + 1) * 2 -
node.height
if (difference > 0) {
this.updateBrothers(node, difference / 2)
}
@@ -117,11 +120,9 @@ class LogicalStructure extends Base {
updateBrothers(node, addHeight) {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item === node || item.hasCustomPosition()) {
if (item.uid === node.uid || item.hasCustomPosition()) {
// 适配自定义位置
return
}
@@ -264,24 +265,27 @@ class LogicalStructure extends Base {
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeBoundaries(node, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
let x2 = right + generalizationLineMargin
let y2 = bottom
let cx = x1 + 20
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
gLine.plot(path)
gNode.left = right + generalizationNodeMargin
gNode.top = top + (bottom - top - gNode.height) / 2
renderGeneralization(list) {
list.forEach(item => {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
let x2 = right + generalizationLineMargin
let y2 = bottom
let cx = x1 + 20
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
item.generalizationLine.plot(path)
item.generalizationNode.left = right + generalizationNodeMargin
item.generalizationNode.top =
top + (bottom - top - item.generalizationNode.height) / 2
})
}
// 渲染展开收起按钮的隐藏占位元素

View File

@@ -1,5 +1,5 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
import { CONSTANTS } from '../constants/constant'
// 思维导图
@@ -92,9 +92,18 @@ class MindMap extends Base {
(rightLen + 1) * this.getMarginY(layerIndex + 1)
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeHeight = cur._node.checkHasGeneralization() ? cur._node._generalizationNodeHeight + this.getMarginY(layerIndex + 1) : 0
cur._node.leftChildrenAreaHeight2 = Math.max(cur._node.leftChildrenAreaHeight, generalizationNodeHeight)
cur._node.rightChildrenAreaHeight2 = Math.max(cur._node.rightChildrenAreaHeight, generalizationNodeHeight)
let generalizationNodeHeight = cur._node.checkHasGeneralization()
? cur._node._generalizationNodeHeight +
this.getMarginY(layerIndex + 1)
: 0
cur._node.leftChildrenAreaHeight2 = Math.max(
cur._node.leftChildrenAreaHeight,
generalizationNodeHeight
)
cur._node.rightChildrenAreaHeight2 = Math.max(
cur._node.rightChildrenAreaHeight,
generalizationNodeHeight
)
},
true,
0
@@ -108,7 +117,7 @@ class MindMap extends Base {
null,
(node, parent, isRoot, layerIndex) => {
if (
node.nodeData.data.expand &&
node.getData('expand') &&
node.children &&
node.children.length
) {
@@ -139,7 +148,7 @@ class MindMap extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
if (!node.getData('expand')) {
return
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
@@ -162,9 +171,7 @@ class MindMap extends Base {
let childrenList = node.parent.children.filter(item => {
return item.dir === node.dir
})
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
@@ -351,32 +358,34 @@ class MindMap extends Base {
}
// 创建概要节点
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
renderGeneralization(list) {
list.forEach(item => {
let isLeft = item.node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
let {
top,
bottom,
left,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
let x = isLeft
? left - generalizationLineMargin
: right + generalizationLineMargin
let x1 = x
let y1 = top
let x2 = x
let y2 = bottom
let cx = x1 + (isLeft ? -20 : 20)
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
item.generalizationLine.plot(path)
item.generalizationNode.left =
x +
(isLeft ? -generalizationNodeMargin : generalizationNodeMargin) -
(isLeft ? item.generalizationNode.width : 0)
item.generalizationNode.top = top + (bottom - top - item.generalizationNode.height) / 2
})
}
// 渲染展开收起按钮的隐藏占位元素

View File

@@ -1,5 +1,5 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
// 组织结构图
// 和逻辑结构图基本一样只是方向变成向下生长所以先计算节点的top后计算节点的left、最后调整节点的left即可
@@ -59,8 +59,13 @@ class OrganizationStructure extends Base {
: 0
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeWidth = cur._node.checkHasGeneralization() ? cur._node._generalizationNodeWidth + this.getMarginY(layerIndex + 1) : 0
cur._node.childrenAreaWidth2 = Math.max(cur._node.childrenAreaWidth, generalizationNodeWidth)
let generalizationNodeWidth = cur._node.checkHasGeneralization()
? cur._node._generalizationNodeWidth + this.getMarginY(layerIndex + 1)
: 0
cur._node.childrenAreaWidth2 = Math.max(
cur._node.childrenAreaWidth,
generalizationNodeWidth
)
},
true,
0
@@ -74,7 +79,7 @@ class OrganizationStructure extends Base {
null,
(node, parent, isRoot, layerIndex) => {
if (
node.nodeData.data.expand &&
node.getData('expand') &&
node.children &&
node.children.length
) {
@@ -99,7 +104,7 @@ class OrganizationStructure extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
if (!node.getData('expand')) {
return
}
// 判断子节点所占的宽度之和是否大于该节点自身,大于则需要调整位置
@@ -120,9 +125,7 @@ class OrganizationStructure extends Base {
updateBrothers(node, addWidth) {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
@@ -213,7 +216,7 @@ class OrganizationStructure extends Base {
minx = Math.min(x1, minx)
maxx = Math.max(x1, maxx)
// 父节点的竖线
let line1 = this.draw.path()
let line1 = this.lineDraw.path()
node.style.line(line1)
expandBtnSize = len > 0 && !isRoot ? expandBtnSize : 0
line1.plot(`M ${x1},${y1 + expandBtnSize} L ${x1},${y1 + s1}`)
@@ -221,7 +224,7 @@ class OrganizationStructure extends Base {
style && style(line1, node)
// 水平线
if (len > 0) {
let lin2 = this.draw.path()
let lin2 = this.lineDraw.path()
node.style.line(lin2)
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
node._lines.push(lin2)
@@ -240,24 +243,27 @@ class OrganizationStructure extends Base {
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
bottom,
left,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeBoundaries(node, 'v')
let x1 = left
let y1 = bottom + generalizationLineMargin
let x2 = right
let y2 = bottom + generalizationLineMargin
let cx = x1 + (x2 - x1) / 2
let cy = y1 + 20
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
gLine.plot(path)
gNode.top = bottom + generalizationNodeMargin
gNode.left = left + (right - left - gNode.width) / 2
renderGeneralization(list) {
list.forEach(item => {
let {
bottom,
left,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeGeneralizationRenderBoundaries(item, 'v')
let x1 = left
let y1 = bottom + generalizationLineMargin
let x2 = right
let y2 = bottom + generalizationLineMargin
let cx = x1 + (x2 - x1) / 2
let cy = y1 + 20
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
item.generalizationLine.plot(path)
item.generalizationNode.top = bottom + generalizationNodeMargin
item.generalizationNode.left = left + (right - left - item.generalizationNode.width) / 2
})
}
// 渲染展开收起按钮的隐藏占位元素

View File

@@ -1,5 +1,5 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
import { CONSTANTS } from '../constants/constant'
// 时间轴
@@ -80,11 +80,7 @@ class Timeline extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex, index) => {
if (
node.nodeData.data.expand &&
node.children &&
node.children.length
) {
if (node.getData('expand') && node.children && node.children.length) {
let marginX = this.getMarginX(layerIndex + 1)
let marginY = this.getMarginY(layerIndex + 1)
if (isRoot) {
@@ -122,7 +118,7 @@ class Timeline extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
if (!node.getData('expand')) {
return
}
// 调整left
@@ -208,9 +204,7 @@ class Timeline extends Base {
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
@@ -275,7 +269,7 @@ class Timeline extends Base {
})
// 竖线
if (len > 0) {
let line = this.draw.path()
let line = this.lineDraw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
if (
node.parent &&
@@ -317,24 +311,26 @@ class Timeline extends Base {
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeBoundaries(node, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
let x2 = right + generalizationLineMargin
let y2 = bottom
let cx = x1 + 20
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
gLine.plot(path)
gNode.left = right + generalizationNodeMargin
gNode.top = top + (bottom - top - gNode.height) / 2
renderGeneralization(list) {
list.forEach(item => {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
let x2 = right + generalizationLineMargin
let y2 = bottom
let cx = x1 + 20
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
item.generalizationLine.plot(path)
item.generalizationNode.left = right + generalizationNodeMargin
item.generalizationNode.top = top + (bottom - top - item.generalizationNode.height) / 2
})
}
// 渲染展开收起按钮的隐藏占位元素

View File

@@ -1,5 +1,5 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
import { CONSTANTS } from '../constants/constant'
// 竖向时间轴
@@ -97,11 +97,7 @@ class VerticalTimeline extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex, index) => {
if (
node.nodeData.data.expand &&
node.children &&
node.children.length
) {
if (node.getData('expand') && node.children && node.children.length) {
let marginY = this.getMarginY(layerIndex + 1)
// 定位二级节点的top
if (isRoot) {
@@ -135,7 +131,7 @@ class VerticalTimeline extends Base {
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
if (!node.getData('expand')) {
return
}
if (isRoot) return
@@ -155,14 +151,12 @@ class VerticalTimeline extends Base {
updateBrothers(node, addHeight) {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
// 自定义节点位置
if (item.hasCustomPosition()) return
// 三级或三级以下节点自身位置不需要动
if (!node.parent.isRoot && item === node) return
if (!node.parent.isRoot && item.uid === node.uid) return
let _offset = 0
// 二级节点上面的兄弟节点不需要移动,自身需要往下移动
if (node.parent.isRoot) {
@@ -201,9 +195,7 @@ class VerticalTimeline extends Base {
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
@@ -390,32 +382,35 @@ class VerticalTimeline extends Base {
}
// 创建概要节点
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
renderGeneralization(list) {
list.forEach(item => {
let isLeft = item.node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
let {
top,
bottom,
left,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeGeneralizationRenderBoundaries(item, 'h')
let x = isLeft
? left - generalizationLineMargin
: right + generalizationLineMargin
let x1 = x
let y1 = top
let x2 = x
let y2 = bottom
let cx = x1 + (isLeft ? -20 : 20)
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
item.generalizationLine.plot(path)
item.generalizationNode.left =
x +
(isLeft ? -generalizationNodeMargin : generalizationNodeMargin) -
(isLeft ? item.generalizationNode.width : 0)
item.generalizationNode.top =
top + (bottom - top - item.generalizationNode.height) / 2
})
}
// 渲染展开收起按钮的隐藏占位元素

View File

@@ -52,13 +52,15 @@ export default {
let totalTop =
node.top +
node.height +
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
marginY
node.children.forEach(item => {
item.left = startLeft
item.top += totalTop
totalTop +=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
marginY
})
}
},
@@ -72,7 +74,8 @@ export default {
return (
h +
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
marginY
)
}, 0)
ctx.updateBrothersTop(node, totalHeight)
@@ -92,7 +95,11 @@ export default {
item.top =
node.top - (item.top - node.top) - nodeTotalHeight + node.height
// 调整left
item.left = node.left + node.width * ctx.indent + (nodeTotalHeight + totalHeight) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
item.left =
node.left +
node.width * ctx.indent +
(nodeTotalHeight + totalHeight) /
Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
totalHeight += nodeTotalHeight
// 同步更新后代节点
ctx.updateChildrenPro(item.children, {
@@ -129,7 +136,9 @@ export default {
if (node.parent && node.parent.isRoot) {
line.plot(
`M ${x},${top + height} L ${x + lineLength},${
top + height + Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
top +
height +
Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
}`
)
} else {
@@ -144,7 +153,8 @@ export default {
let totalTop =
node.top +
node.height +
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
marginY
node.children.forEach(item => {
item.left = startLeft
@@ -153,7 +163,8 @@ export default {
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
totalTop +=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
marginY
})
}
if (layerIndex > 1 && node.children) {
@@ -161,13 +172,15 @@ export default {
let startLeft = node.left + node.width * ctx.childIndent
let totalTop =
node.top -
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) - marginY
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) -
marginY
node.children.forEach(item => {
item.left = startLeft
item.top = totalTop - item.height
totalTop -=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
marginY
})
}
},
@@ -180,7 +193,8 @@ export default {
return (
h +
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
marginY
)
}, 0)
ctx.updateBrothersTop(node, -totalHeight)
@@ -197,18 +211,21 @@ export default {
// 调整top
let hasChildren = ctx.getNodeActChildrenLength(item) > 0
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
let offset =
hasChildren
? nodeTotalHeight -
item.height -
(hasChildren ? item.expandBtnSize : 0)
: 0
offset -= (hasChildren ? marginY : 0)
let offset = hasChildren
? nodeTotalHeight -
item.height -
(hasChildren ? item.expandBtnSize : 0)
: 0
offset -= hasChildren ? marginY : 0
let _top = totalHeight + offset
let _left = item.left
item.top += _top
// 调整left
item.left = node.left + node.width * ctx.indent + (nodeTotalHeight + totalHeight2) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
item.left =
node.left +
node.width * ctx.indent +
(nodeTotalHeight + totalHeight2) /
Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
totalHeight += offset
totalHeight2 += nodeTotalHeight
// 同步更新后代节点

View File

@@ -50,4 +50,4 @@ export const transformToMarkdown = root => {
true
)
return content
}
}

View File

@@ -1,12 +1,18 @@
import JSZip from 'jszip'
import xmlConvert from 'xml-js'
import { getTextFromHtml, isUndef } from '../utils/index'
import {
getTextFromHtml,
imgToDataUrl,
parseDataUrl,
getImageSize,
isUndef
} from '../utils/index'
getSummaryText,
getSummaryText2,
getRoot,
getItemByName,
getElementsByType,
addSummaryData,
handleNodeImageFromXmind,
handleNodeImageToXmind,
getXmindContentXmlData,
parseNodeGeneralizationToXmind
} from '../utils/xmind'
// 解析.xmind文件
const parseXmindFile = file => {
@@ -43,18 +49,18 @@ const parseXmindFile = file => {
// 转换xmind数据
const transformXmind = async (content, files) => {
let data = JSON.parse(content)[0]
let nodeTree = data.rootTopic
let newTree = {}
let waitLoadImageList = []
let walk = async (node, newNode) => {
const data = JSON.parse(content)[0]
const nodeTree = data.rootTopic
const newTree = {}
const waitLoadImageList = []
const walk = async (node, newNode) => {
newNode.data = {
// 节点内容
text: isUndef(node.title) ? '' : node.title
}
// 节点备注
if (node.notes) {
let notesData = node.notes.realHTML || node.notes.plain
const notesData = node.notes.realHTML || node.notes.plain
newNode.data.note = notesData ? notesData.content || '' : ''
}
// 超链接
@@ -66,41 +72,26 @@ const transformXmind = async (content, files) => {
newNode.data.tag = node.labels
}
// 图片
if (node.image && /\.(jpg|jpeg|png|gif|webp)$/.test(node.image.src)) {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
try {
// 读取图片
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()
}
handleNodeImageFromXmind(node, newNode, waitLoadImageList, files)
// 概要
const selfSummary = []
const childrenSummary = []
if (newNode._summary) {
selfSummary.push(newNode._summary)
}
if (Array.isArray(node.summaries) && node.summaries.length > 0) {
node.summaries.forEach(item => {
addSummaryData(
selfSummary,
childrenSummary,
() => {
return getSummaryText(node, item.topicId)
},
item.range
)
})
}
newNode.data.generalization = selfSummary
// 子节点
newNode.children = []
if (
@@ -108,9 +99,12 @@ const transformXmind = async (content, files) => {
node.children.attached &&
node.children.attached.length > 0
) {
node.children.attached.forEach(item => {
let newChild = {}
node.children.attached.forEach((item, index) => {
const newChild = {}
newNode.children.push(newChild)
if (childrenSummary[index]) {
newChild._summary = childrenSummary[index]
}
walk(item, newChild)
})
}
@@ -122,39 +116,21 @@ const transformXmind = async (content, files) => {
// 转换旧版xmind数据xmind8
const transformOldXmind = content => {
let data = JSON.parse(content)
let elements = data.elements
let root = null
let getRoot = arr => {
if (!arr) return
for (let i = 0; i < arr.length; i++) {
if (!root && arr[i].name === 'topic') {
root = arr[i]
return
}
}
arr.forEach(item => {
getRoot(item.elements)
})
}
getRoot(elements)
let newTree = {}
let getItemByName = (arr, name) => {
return arr.find(item => {
return item.name === name
})
}
let walk = (node, newNode) => {
let nodeElements = node.elements
const data = JSON.parse(content)
const elements = data.elements
const root = getRoot(elements)
const newTree = {}
const walk = (node, newNode) => {
const nodeElements = node.elements
let nodeTitle = getItemByName(nodeElements, 'title')
nodeTitle = nodeTitle && nodeTitle.elements && nodeTitle.elements[0].text
// 节点内容
newNode.data = {
// 节点内容
text: isUndef(nodeTitle) ? '' : nodeTitle
}
// 节点备注
try {
// 节点备注
let notesElement = getItemByName(nodeElements, 'notes')
const notesElement = getItemByName(nodeElements, 'notes')
if (notesElement) {
newNode.data.note =
notesElement.elements[0].elements[0].elements[0].text
@@ -162,8 +138,8 @@ const transformOldXmind = content => {
} catch (error) {
console.log(error)
}
// 超链接
try {
// 超链接
if (
node.attributes &&
node.attributes['xlink:href'] &&
@@ -174,9 +150,9 @@ const transformOldXmind = content => {
} catch (error) {
console.log(error)
}
// 标签
try {
// 标签
let labelsElement = getItemByName(nodeElements, 'labels')
const labelsElement = getItemByName(nodeElements, 'labels')
if (labelsElement) {
newNode.data.tag = labelsElement.elements.map(item => {
return item.elements[0].text
@@ -185,22 +161,50 @@ const transformOldXmind = content => {
} catch (error) {
console.log(error)
}
const childrenItem = getItemByName(nodeElements, 'children')
// 概要
const selfSummary = []
const childrenSummary = []
try {
if (newNode._summary) {
selfSummary.push(newNode._summary)
}
const summariesItem = getItemByName(nodeElements, 'summaries')
if (
summariesItem &&
Array.isArray(summariesItem.elements) &&
summariesItem.elements.length > 0
) {
summariesItem.elements.forEach(item => {
addSummaryData(
selfSummary,
childrenSummary,
() => {
return getSummaryText2(childrenItem, item.attributes['topic-id'])
},
item.attributes.range
)
})
}
} catch (error) {
console.log(error)
}
newNode.data.generalization = selfSummary
// 子节点
newNode.children = []
let _children = getItemByName(nodeElements, 'children')
if (_children && _children.elements && _children.elements.length > 0) {
_children.elements.forEach(item => {
if (item.name === 'topics') {
(item.elements || []).forEach(item2 => {
let newChild = {}
newNode.children.push(newChild)
walk(item2, newChild)
})
} else {
let newChild = {}
newNode.children.push(newChild)
walk(item, newChild)
if (
childrenItem &&
childrenItem.elements &&
childrenItem.elements.length > 0
) {
const children = getElementsByType(childrenItem.elements, 'attached')
children.forEach((item, index) => {
const newChild = {}
newNode.children.push(newChild)
if (childrenSummary[index]) {
newChild._summary = childrenSummary[index]
}
walk(item, newChild)
})
}
}
@@ -209,6 +213,7 @@ const transformOldXmind = content => {
}
// 数据转换为xmind文件
// 直接转换为最新版本的xmind文件 2023.09.11172
const transformToXmind = async (data, name) => {
const id = 'simpleMindMap_' + Date.now()
const imageList = []
@@ -244,38 +249,7 @@ const transformToXmind = async (data, name) => {
newData.labels = node.data.tag || []
}
// 图片
if (node.data.image) {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
try {
let imgName = ''
let imgData = node.data.image
// base64之外的其他图片要先转换成data:url
if (!/^data:/.test(node.data.image)) {
imgData = await imgToDataUrl(node.data.image)
}
// 从data:url中解析出图片类型和ase64
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()
}
}
handleNodeImageToXmind(node, newNode, waitLoadImageList, imageList)
// 样式
// 暂时不考虑样式
if (isRoot) {
@@ -293,6 +267,20 @@ const transformToXmind = async (data, name) => {
newNode[key] = newData[key]
})
}
// 概要
const { summary, summaries } = parseNodeGeneralizationToXmind(node)
if (isRoot) {
if (summaries.length > 0) {
newNode.rootTopic.children.summary = summary
newNode.rootTopic.summaries = summaries
}
} else {
if (summaries.length > 0) {
newNode.children.summary = summary
newNode.summaries = summaries
}
}
// 子节点
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
let newChild = {}
@@ -309,10 +297,15 @@ const transformToXmind = async (data, name) => {
zip.file('content.json', JSON.stringify(contentData))
zip.file(
'metadata.json',
`{"modifier":"","dataStructureVersion":"1","layoutEngineVersion":"2","activeSheetId":"${id}"}`
`{"modifier":"","dataStructureVersion":"2","creator":{"name":"mind-map"},"layoutEngineVersion":"3","activeSheetId":"${id}"}`
)
zip.file('content.xml', getXmindContentXmlData())
const manifestData = {
'file-entries': { 'content.json': {}, 'metadata.json': {} }
'file-entries': {
'content.json': {},
'metadata.json': {},
'Thumbnails/thumbnail.png': {}
}
}
// 图片
if (imageList.length > 0) {

View File

@@ -15,7 +15,7 @@ import associativeLineTextMethods from './associativeLine/associativeLineText'
class AssociativeLine {
constructor(opt = {}) {
this.mindMap = opt.mindMap
this.draw = this.mindMap.draw
this.associativeLineDraw = this.mindMap.associativeLineDraw
// 当前所有连接线
this.lineList = []
// 当前激活的连接线
@@ -98,14 +98,14 @@ class AssociativeLine {
// 创建箭头
createMarker() {
return this.draw.marker(20, 20, add => {
return this.associativeLineDraw.marker(20, 20, add => {
add.ref(12, 5)
add.size(10, 10)
add.attr('orient', 'auto-start-reverse')
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
})
}
// 判断关联线坐标是否变更,有变更则使用变化后的坐标,无则默认坐标
updateAllLinesPos(node, toNode, associativeLinePoint) {
associativeLinePoint = associativeLinePoint || {}
@@ -142,15 +142,15 @@ class AssociativeLine {
null,
cur => {
if (!cur) return
let data = cur.nodeData.data
let data = cur.getData()
if (
data.associativeLineTargets &&
data.associativeLineTargets.length > 0
) {
nodeToIds.set(cur, data.associativeLineTargets)
}
if (data.id) {
idToNode.set(data.id, cur)
if (data.uid) {
idToNode.set(data.uid, cur)
}
},
() => {},
@@ -158,10 +158,10 @@ class AssociativeLine {
0
)
nodeToIds.forEach((ids, node) => {
ids.forEach((id, index) => {
let toNode = idToNode.get(id)
ids.forEach((uid, index) => {
let toNode = idToNode.get(uid)
if (!node || !toNode) return
const associativeLinePoint = (node.nodeData.data.associativeLinePoint ||
const associativeLinePoint = (node.getData('associativeLinePoint') ||
[])[index]
// 切换结构和布局,都会更新坐标
const [startPoint, endPoint] = this.updateAllLinesPos(
@@ -194,7 +194,7 @@ class AssociativeLine {
toNode
)
// 虚线
let path = this.draw.path()
let path = this.associativeLineDraw.path()
path
.stroke({
width: associativeLineWidth,
@@ -205,7 +205,7 @@ class AssociativeLine {
path.plot(pathStr)
path.marker('end', this.marker)
// 不可见的点击线
let clickPath = this.draw.path()
let clickPath = this.associativeLineDraw.path()
clickPath
.stroke({ width: associativeLineActiveWidth, color: 'transparent' })
.fill({ color: 'none' })
@@ -234,6 +234,11 @@ class AssociativeLine {
controlPoints
})
})
// 双击进入关联线文本编辑状态
clickPath.dblclick(() => {
if (!this.activeLine) return
this.showEditTextBox(text)
})
// 渲染关联线文字
this.renderText(this.getText(node, toNode), path, text)
this.lineList.push([path, clickPath, text, node, toNode])
@@ -252,28 +257,26 @@ class AssociativeLine {
}) {
let { associativeLineActiveColor } = this.mindMap.themeConfig
// 如果当前存在激活节点,那么取消激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.clearActiveNodes()
} else {
// 否则清除当前的关联线的激活状态,如果有的话
this.clearActiveLine()
// 保存当前激活的关联线信息
this.activeLine = [path, clickPath, text, node, toNode]
// 让不可见的点击线显示
clickPath.stroke({ color: associativeLineActiveColor })
// 如果没有输入过关联线文字,那么显示默认文字
if (!this.getText(node, toNode)) {
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text)
}
// 渲染控制点和连线
this.renderControls(
startPoint,
endPoint,
controlPoints[0],
controlPoints[1]
)
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
// 否则清除当前的关联线的激活状态,如果有的话
this.clearActiveLine()
// 保存当前激活的关联线信息
this.activeLine = [path, clickPath, text, node, toNode]
// 让不可见的点击线显示
clickPath.stroke({ color: associativeLineActiveColor })
// 如果没有输入过关联线文字,那么显示默认文字
if (!this.getText(node, toNode)) {
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text)
}
// 渲染控制点和连线
this.renderControls(
startPoint,
endPoint,
controlPoints[0],
controlPoints[1]
)
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
this.front()
}
// 移除所有连接线
@@ -298,9 +301,10 @@ class AssociativeLine {
let { associativeLineWidth, associativeLineColor } =
this.mindMap.themeConfig
if (this.isCreatingLine || !fromNode) return
this.front()
this.isCreatingLine = true
this.creatingStartNode = fromNode
this.creatingLine = this.draw.path()
this.creatingLine = this.associativeLineDraw.path()
this.creatingLine
.stroke({
width: associativeLineWidth,
@@ -355,12 +359,13 @@ class AssociativeLine {
height
}
}
// 检测当前移动到的目标节点
checkOverlapNode(x, y) {
this.overlapNode = null
bfsWalk(this.mindMap.renderer.root, node => {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
if (node.getData('isActive')) {
this.mindMap.execCommand('SET_NODE_ACTIVE', node, false)
}
if (node.uid === this.creatingStartNode.uid || this.overlapNode) {
return
@@ -372,8 +377,8 @@ class AssociativeLine {
this.overlapNode = node
}
})
if (this.overlapNode && !this.overlapNode.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
if (this.overlapNode && !this.overlapNode.getData('isActive')) {
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
}
}
@@ -381,35 +386,36 @@ class AssociativeLine {
completeCreateLine(node) {
if (this.creatingStartNode.uid === node.uid) return
this.addLine(this.creatingStartNode, node)
if (this.overlapNode && this.overlapNode.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
if (this.overlapNode && this.overlapNode.getData('isActive')) {
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, false)
}
this.isCreatingLine = false
this.creatingStartNode = null
this.creatingLine.remove()
this.creatingLine = null
this.overlapNode = null
this.back()
}
// 添加连接线
addLine(fromNode, toNode) {
if (!fromNode || !toNode) return
// 目标节点如果没有id则生成一个id
let id = toNode.nodeData.data.id
if (!id) {
id = uuid()
let uid = toNode.getData('uid')
if (!uid) {
uid = uuid()
this.mindMap.execCommand('SET_NODE_DATA', toNode, {
id
uid
})
}
// 将目标节点id保存起来
let list = fromNode.nodeData.data.associativeLineTargets || []
let list = fromNode.getData('associativeLineTargets') || []
// 连线节点是否存在相同的id,存在则阻止添加关联线
const sameLine = list.some(item => item === id)
const sameLine = list.some(item => item === uid)
if (sameLine) {
return
}
list.push(id)
list.push(uid)
// 保存控制点
let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
let controlPoints = computeCubicBezierPathPoints(
@@ -419,7 +425,7 @@ class AssociativeLine {
endPoint.y
)
let offsetList =
fromNode.nodeData.data.associativeLineTargetControlOffsets || []
fromNode.getData('associativeLineTargetControlOffsets') || []
// 保存的实际是控制点和端点的差值,否则当节点位置改变了,控制点还是原来的位置,连线就不对了
offsetList[list.length - 1] = [
{
@@ -431,7 +437,7 @@ class AssociativeLine {
y: controlPoints[1].y - endPoint.y
}
]
let associativeLinePoint = fromNode.nodeData.data.associativeLinePoint || []
let associativeLinePoint = fromNode.getData('associativeLinePoint') || []
// 记录关联的起始|结束坐标
associativeLinePoint[list.length - 1] = { startPoint, endPoint }
this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
@@ -451,14 +457,14 @@ class AssociativeLine {
associativeLinePoint,
associativeLineTargetControlOffsets,
associativeLineText
} = node.nodeData.data
} = node.getData()
associativeLinePoint = associativeLinePoint || []
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
// 更新关联线文本数据
let newAssociativeLineText = {}
if (associativeLineText) {
Object.keys(associativeLineText).forEach(item => {
if (item !== toNode.nodeData.data.id) {
if (item !== toNode.getData('uid')) {
newAssociativeLineText[item] = associativeLineText[item]
}
})
@@ -483,13 +489,6 @@ class AssociativeLine {
})
}
// 清除当前激活的节点
clearActiveNodes() {
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
}
// 清除激活的线
clearActiveLine() {
if (this.activeLine) {
@@ -505,6 +504,7 @@ class AssociativeLine {
}
this.activeLine = null
this.removeControls()
this.back()
}
}
@@ -531,6 +531,19 @@ class AssociativeLine {
this.showControls()
this.isNodeDragging = false
}
// 关联线顶层显示
front() {
if (this.mindMap.opt.associativeLineIsAlwaysAboveNode) return
this.associativeLineDraw.front()
}
// 关联线回到原有层级
back() {
if (this.mindMap.opt.associativeLineIsAlwaysAboveNode) return
this.associativeLineDraw.back() // 最底层
this.associativeLineDraw.forward() // 连线层上面
}
}
AssociativeLine.instanceName = 'associativeLine'

View File

@@ -0,0 +1,345 @@
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'
import { isSameObject, simpleDeepClone, getType, isUndef } from '../utils/index'
// 协同插件
class Cooperate {
constructor(opt) {
this.opt = opt
this.mindMap = opt.mindMap
// yjs文档
this.ydoc = new Y.Doc()
// 共享数据
this.ymap = null
// 连接提供者
this.provider = null
// 感知数据
this.awareness = null
this.currentAwarenessData = []
this.waitNodeUidMap = {} // 该列表中的uid对应的节点还未渲染完毕
// 当前的平级对象类型的思维导图数据
this.currentData = null
// 用户信息
this.userInfo = null
// 绑定事件
this.bindEvent()
// 处理实例化时传入的思维导图数据
if (this.mindMap.opt.data) {
this.initData(this.mindMap.opt.data)
}
}
// 初始化数据
initData(data) {
data = simpleDeepClone(data)
// 解绑原来的数据
if (this.ymap) {
this.ymap.unobserve(this.onObserve)
}
// 创建共享数据
this.ymap = this.ydoc.getMap()
// 思维导图树结构转平级对象结构
this.currentData = this.transformTreeDataToObject(data)
// 将思维导图数据添加到共享数据中
Object.keys(this.currentData).forEach(uid => {
this.ymap.set(uid, this.currentData[uid])
})
// 监听数据同步
this.onObserve = this.onObserve.bind(this)
this.ymap.observe(this.onObserve)
}
// 获取yjs doc实例
getDoc() {
return this.ydoc
}
// 设置连接提供者
setProvider(provider, webrtcProviderConfig = {}) {
const { roomName, signalingList, ...otherConfig } = webrtcProviderConfig
this.provider =
provider ||
new WebrtcProvider(roomName, this.ydoc, {
signaling: signalingList,
...otherConfig
})
this.awareness = this.provider.awareness
// 监听状态同步事件
this.onAwareness = this.onAwareness.bind(this)
this.awareness.on('change', this.onAwareness)
}
// 绑定事件
bindEvent() {
// 监听思维导图改变
this.onDataChange = this.onDataChange.bind(this)
this.mindMap.on('data_change', this.onDataChange)
// 监听思维导图节点激活事件
this.onNodeActive = this.onNodeActive.bind(this)
this.mindMap.on('node_active', this.onNodeActive)
// 监听思维导图渲染完毕事件
this.onNodeTreeRenderEnd = this.onNodeTreeRenderEnd.bind(this)
this.mindMap.on('node_tree_render_end', this.onNodeTreeRenderEnd)
// 监听设置思维导图数据事件
this.initData = this.initData.bind(this)
this.mindMap.on('set_data', this.initData)
}
// 解绑事件
unBindEvent() {
if (this.ymap) {
this.ymap.unobserve(this.onObserve)
}
this.mindMap.off('data_change', this.onDataChange)
this.mindMap.off('node_active', this.onNodeActive)
this.mindMap.off('node_tree_render_end', this.onNodeTreeRenderEnd)
this.mindMap.off('set_data', this.initData)
this.ydoc.destroy()
}
// 数据同步时的处理,更新当前思维导图
onObserve(event) {
const data = event.target.toJSON()
// 如果数据没有改变直接返回
if (isSameObject(data, this.currentData)) return
this.currentData = data
// 平级对象转树结构
const res = this.transformObjectToTreeData(data)
if (!res) return
// 更新思维导图画布
this.mindMap.renderer.setData(res)
this.mindMap.render()
this.mindMap.command.addHistory()
}
// 当前思维导图改变后的处理,触发同步
onDataChange(data) {
const res = this.transformTreeDataToObject(data)
this.updateChanges(res)
}
// 找出更新点
updateChanges(data) {
const oldData = this.currentData
this.currentData = data
this.ydoc.transact(() => {
// 找出新增的或修改的
Object.keys(data).forEach(uid => {
// 新增的或已经存在的,如果数据发生了改变
if (!oldData[uid] || !isSameObject(oldData[uid], data[uid])) {
this.ymap.set(uid, data[uid])
}
})
// 找出删除的
Object.keys(oldData).forEach(uid => {
if (!data[uid]) {
this.ymap.delete(uid)
}
})
})
}
// 节点激活状态改变后触发感知数据同步
onNodeActive(node, nodeList) {
if (this.userInfo) {
this.awareness.setLocalStateField(this.userInfo.name, {
// 用户信息
userInfo: {
...this.userInfo
},
// 当前激活的节点id列表
nodeIdList: nodeList.map(item => {
return item.uid
})
})
}
}
// 节点树渲染完毕事件
onNodeTreeRenderEnd() {
Object.keys(this.waitNodeUidMap).forEach(uid => {
const node = this.mindMap.renderer.findNodeByUid(uid)
if (node) {
node.addUser(this.waitNodeUidMap[uid])
}
})
this.waitNodeUidMap = {}
}
// 设置用户信息
/**
* {
* id: '', // 必传用户唯一的id
* name: '', // 用户名称。name和avatar两个只传一个即可如果都传了会显示avatar
* avatar: '', // 用户头像
* color: '' // 如果没有传头像,那么会以一个圆形来显示名称的第一个字,文字的颜色为白色,圆的颜色可以通过该字段设置
* }
**/
setUserInfo(userInfo) {
if (
getType(userInfo) !== 'Object' ||
isUndef(userInfo.id) ||
(isUndef(userInfo.name) && isUndef(userInfo.avatar))
)
return
this.userInfo = userInfo || null
}
// 监听感知数据同步事件
onAwareness() {
const walk = (list, callback) => {
list.forEach(value => {
const userName = Object.keys(value)[0]
if (!userName) return
const data = value[userName]
const userInfo = data.userInfo
const nodeIdList = data.nodeIdList
nodeIdList.forEach(uid => {
const node = this.mindMap.renderer.findNodeByUid(uid)
callback(uid, node, userInfo)
})
})
}
// 清除之前的数据
walk(this.currentAwarenessData, (uid, node, userInfo) => {
if (node) {
node.removeUser(userInfo)
}
})
// 设置当前数据
const data = Array.from(this.awareness.getStates().values())
this.currentAwarenessData = data
walk(data, (uid, node, userInfo) => {
// 不显示自己
if (userInfo.id === this.userInfo.id) return
if (node) {
node.addUser(userInfo)
} else {
this.waitNodeUidMap[uid] = userInfo
}
})
}
// 将树结构转平级对象
/*
{
data: {
uid: 'xxx'
},
children: [
{
data: {
uid: 'xxx'
},
children: []
}
]
}
转为:
{
uid: {
children: [uid1, uid2],
data: {}
}
}
*/
transformTreeDataToObject(data) {
const res = {}
const walk = (root, parent) => {
const uid = root.data.uid
if (parent) {
parent.children.push(uid)
}
res[uid] = {
isRoot: !parent,
data: {
...root.data
},
children: []
}
if (root.children && root.children.length > 0) {
root.children.forEach(item => {
walk(item, res[uid])
})
}
}
walk(data, null)
return res
}
// 找到父节点的uid
findParentUid(data, targetUid) {
const uids = Object.keys(data)
let res = ''
uids.forEach(uid => {
const children = data[uid].children
const isParent =
children.findIndex(childUid => {
return childUid === targetUid
}) !== -1
if (isParent) {
res = uid
}
})
return res
}
// 将平级对象转树结构
transformObjectToTreeData(data) {
const uids = Object.keys(data)
if (uids.length <= 0) return null
const rootKey = uids.find(uid => {
return data[uid].isRoot
})
if (!rootKey || !data[rootKey]) return null
// 根节点
const res = {
data: simpleDeepClone(data[rootKey].data),
children: []
}
const map = {}
map[rootKey] = res
uids.forEach(uid => {
const parentUid = this.findParentUid(data, uid)
const cur = data[uid]
const node = map[uid] || {
data: simpleDeepClone(cur.data),
children: []
}
if (!map[uid]) {
map[uid] = node
}
if (parentUid) {
const index = data[parentUid].children.findIndex(item => {
return item === uid
})
if (!map[parentUid]) {
map[parentUid] = {
data: simpleDeepClone(data[parentUid].data),
children: []
}
}
map[parentUid].children[index] = node
}
})
return res
}
// 插件被移除前做的事情
beforePluginRemove() {
this.unBindEvent()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
Cooperate.instanceName = 'cooperate'
export default Cooperate

View File

@@ -1,4 +1,9 @@
import { bfsWalk, throttle } from '../utils'
import {
bfsWalk,
throttle,
getTopAncestorsFomNodeList,
getNodeIndexInNodeList
} from '../utils'
import Base from '../layouts/Base'
// 节点拖动插件
@@ -13,10 +18,14 @@ class Drag extends Base {
// 复位
reset() {
// 是否正在跳转中
this.isDragging = false
// 鼠标按下的节点
this.mousedownNode = null
// 被拖拽中的节点列表
this.beingDragNodeList = []
// 当前画布节点列表
this.nodeList = []
// 当前拖拽节点
this.node = null
// 当前重叠节点
this.overlapNode = null
// 当前上一个同级节点
@@ -27,16 +36,11 @@ class Drag extends Base {
this.drawTransform = null
// 克隆节点
this.clone = null
// 连接线
this.line = null
// 同级位置占位符
this.placeholder = null
// 鼠标按下位置和节点左上角的偏移量
this.offsetX = 0
this.offsetY = 0
// 克隆节点左上角的坐标
this.cloneNodeLeft = 0
this.cloneNodeTop = 0
// 当前鼠标是否按下
this.isMousedown = false
// 拖拽的鼠标位置变量
@@ -53,45 +57,42 @@ class Drag extends Base {
bindEvent() {
this.checkOverlapNode = throttle(this.checkOverlapNode, 300, this)
this.mindMap.on('node_mousedown', (node, e) => {
if (this.mindMap.opt.readonly || node.isGeneralization) {
return
}
if (e.which !== 1 || node.isRoot) {
return
}
e.preventDefault()
// 计算鼠标按下的位置距离节点左上角的距离
this.drawTransform = this.mindMap.draw.transform()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.offsetX = x - (node.left * scaleX + translateX)
this.offsetY = y - (node.top * scaleY + translateY)
this.node = node
this.isMousedown = true
this.mouseDownX = x
this.mouseDownY = y
this.nodeTreeToList()
})
this.mindMap.on('mousemove', e => {
if (this.mindMap.opt.readonly) {
return
}
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) <= this.checkDragOffset &&
Math.abs(y - this.mouseDownY) <= this.checkDragOffset &&
!this.node.isDrag
this.mindMap.opt.readonly ||
e.which !== 1 ||
node.isGeneralization ||
node.isRoot
) {
return
}
this.mindMap.renderer.clearAllActive()
e.preventDefault()
this.isMousedown = true
// 记录鼠标按下时的节点
this.mousedownNode = node
// 记录鼠标按下的坐标
const { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseDownX = x
this.mouseDownY = y
})
this.mindMap.on('mousemove', e => {
if (this.mindMap.opt.readonly || !this.isMousedown) {
return
}
e.preventDefault()
const { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseMoveX = x
this.mouseMoveY = y
// 还没开始移动时鼠标位移过小不认为是拖拽
if (
!this.isDragging &&
Math.abs(x - this.mouseDownX) <= this.checkDragOffset &&
Math.abs(y - this.mouseDownY) <= this.checkDragOffset
) {
return
}
this.mindMap.emit('node_dragging')
this.handleStartMove()
this.onMove(x, y, e)
})
this.onMouseup = this.onMouseup.bind(this)
@@ -105,29 +106,46 @@ class Drag extends Base {
return
}
this.isMousedown = false
let _nodeIsDrag = this.node.isDrag
this.node.isDrag = false
this.node.show()
// 恢复被拖拽节点的临时设置
this.beingDragNodeList.forEach(node => {
node.setOpacity(1)
node.showChildren()
node.endDrag()
})
this.removeCloneNode()
let overlapNodeUid = this.overlapNode
? this.overlapNode.nodeData.data.uid
: ''
let prevNodeUid = this.prevNode ? this.prevNode.nodeData.data.uid : ''
let nextNodeUid = this.nextNode ? this.nextNode.nodeData.data.uid : ''
let overlapNodeUid = this.overlapNode ? this.overlapNode.getData('uid') : ''
let prevNodeUid = this.prevNode ? this.prevNode.getData('uid') : ''
let nextNodeUid = this.nextNode ? this.nextNode.getData('uid') : ''
// 存在重叠子节点,则移动作为其子节点
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
this.mindMap.execCommand('MOVE_NODE_TO', this.node, this.overlapNode)
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, false)
this.mindMap.execCommand(
'MOVE_NODE_TO',
this.beingDragNodeList,
this.overlapNode
)
} else if (this.prevNode) {
// 存在前一个相邻节点,作为其下一个兄弟节点
this.mindMap.renderer.setNodeActive(this.prevNode, false)
this.mindMap.execCommand('INSERT_AFTER', this.node, this.prevNode)
this.mindMap.execCommand('SET_NODE_ACTIVE', this.prevNode, false)
this.mindMap.execCommand(
'INSERT_AFTER',
this.beingDragNodeList,
this.prevNode
)
} else if (this.nextNode) {
// 存在下一个相邻节点,作为其前一个兄弟节点
this.mindMap.renderer.setNodeActive(this.nextNode, false)
this.mindMap.execCommand('INSERT_BEFORE', this.node, this.nextNode)
} else if (_nodeIsDrag && this.mindMap.opt.enableFreeDrag) {
// 自定义位置
this.mindMap.execCommand('SET_NODE_ACTIVE', this.nextNode, false)
this.mindMap.execCommand(
'INSERT_BEFORE',
this.beingDragNodeList,
this.nextNode
)
} else if (
this.clone &&
this.mindMap.opt.enableFreeDrag &&
this.beingDragNodeList.length === 1
) {
// 如果只拖拽了一个节点,那么设置自定义位置
let { x, y } = this.mindMap.toPos(
e.clientX - this.offsetX,
e.clientY - this.offsetY
@@ -135,39 +153,153 @@ class Drag extends Base {
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
x = (x - translateX) / scaleX
y = (y - translateY) / scaleY
this.node.left = x
this.node.top = y
this.node.customLeft = x
this.node.customTop = y
this.mindMap.execCommand('SET_NODE_CUSTOM_POSITION', this.node, x, y)
this.mousedownNode.left = x
this.mousedownNode.top = y
this.mousedownNode.customLeft = x
this.mousedownNode.customTop = y
this.mindMap.execCommand(
'SET_NODE_CUSTOM_POSITION',
this.mousedownNode,
x,
y
)
this.mindMap.render()
}
if (this.isDragging) {
this.mindMap.emit('node_dragend', {
overlapNodeUid,
prevNodeUid,
nextNodeUid
})
}
this.reset()
this.mindMap.emit('node_dragend', {
overlapNodeUid,
prevNodeUid,
nextNodeUid
}
// 拖动中
onMove(x, y, e) {
if (!this.isMousedown) {
return
}
// 更新克隆节点的位置
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let cloneNodeLeft = x - this.offsetX
let cloneNodeTop = y - this.offsetY
x = (cloneNodeLeft - translateX) / scaleX
y = (cloneNodeTop - translateY) / scaleY
let t = this.clone.transform()
this.clone.translate(x - t.translateX, y - t.translateY)
// 检测新位置
this.checkOverlapNode()
// 如果注册了多选节点插件,那么复用它的边缘自动移动画布功能
if (this.mindMap.opt.autoMoveWhenMouseInEdgeOnDrag && this.mindMap.select) {
this.drawTransform = this.mindMap.draw.transform()
this.mindMap.select.clearAutoMoveTimer()
this.mindMap.select.onMove(e.clientX, e.clientY)
}
}
// 开始拖拽时初始化一些数据
handleStartMove() {
if (!this.isDragging) {
this.isDragging = true
// 鼠标按下的节点
let node = this.mousedownNode
// 计算鼠标按下的位置距离节点左上角的距离
this.drawTransform = this.mindMap.draw.transform()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
this.offsetX = this.mouseDownX - (node.left * scaleX + translateX)
this.offsetY = this.mouseDownY - (node.top * scaleY + translateY)
// 如果鼠标按下的节点是激活节点,那么保存当前所有激活的节点
if (node.getData('isActive')) {
// 找出这些激活节点中的最顶层节点
this.beingDragNodeList = getTopAncestorsFomNodeList(
// 过滤掉根节点和概要节点
this.mindMap.renderer.activeNodeList.filter(item => {
return !item.isRoot && !item.isGeneralization
})
)
} else {
// 否则只拖拽按下的节点
this.beingDragNodeList = [node]
}
// 将节点树转为节点数组
this.nodeTreeToList()
// 创建克隆节点
this.createCloneNode()
// 清除当前所有激活的节点
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
}
// 节点由树转换成数组,从子节点到根节点
nodeTreeToList() {
const list = []
bfsWalk(this.mindMap.renderer.root, node => {
// 过滤掉当前被拖拽的节点
if (this.checkIsInBeingDragNodeList(node)) {
return
}
if (!list[node.layerIndex]) {
list[node.layerIndex] = []
}
list[node.layerIndex].push(node)
})
this.nodeList = list.reduceRight((res, cur) => {
return [...res, ...cur]
}, [])
}
// 创建克隆节点
createCloneNode() {
if (!this.clone) {
// 节点
this.clone = this.node.group.clone()
this.clone.opacity(0.5)
const {
dragMultiNodeRectConfig,
dragPlaceholderRectFill,
dragOpacityConfig
} = this.mindMap.opt
const {
width: rectWidth,
height: rectHeight,
fill: rectFill
} = dragMultiNodeRectConfig
const node = this.beingDragNodeList[0]
const lineColor = node.style.merge('lineColor', true)
// 如果当前被拖拽的节点数量大于1那么创建一个矩形示意
if (this.beingDragNodeList.length > 1) {
this.clone = this.mindMap.otherDraw
.rect()
.size(rectWidth, rectHeight)
.radius(rectHeight / 2)
.fill({
color: rectFill || lineColor
})
this.offsetX = rectWidth / 2
this.offsetY = rectHeight / 2
} else {
// 否则克隆当前的节点
this.clone = node.group.clone()
// 删除展开收起按钮元素
const expandEl = this.clone.findOne('.smm-expand-btn')
if (expandEl) {
expandEl.remove()
}
this.mindMap.otherDraw.add(this.clone)
}
this.clone.opacity(dragOpacityConfig.cloneNodeOpacity)
this.clone.css('z-index', 99999)
this.node.isDrag = true
this.node.hide()
// 连接线
this.line = this.draw.path()
this.line.opacity(0.5)
this.node.styleLine(this.line, this.node)
// 同级位置占位符
this.placeholder = this.draw.rect().fill({
color: this.node.style.merge('lineColor', true)
// 同级位置提示元素
this.placeholder = this.mindMap.otherDraw.rect().fill({
color: dragPlaceholderRectFill || lineColor
})
// 当前被拖拽的节点的临时设置
this.beingDragNodeList.forEach(node => {
// 降低透明度
node.setOpacity(dragOpacityConfig.beingDragNodeOpacity)
// 隐藏连线及下级节点
node.hideChildren()
// 设置拖拽状态
node.startDrag()
})
this.mindMap.draw.add(this.clone)
}
}
@@ -177,42 +309,9 @@ class Drag extends Base {
return
}
this.clone.remove()
this.line.remove()
this.placeholder.remove()
}
// 拖动中
onMove(x, y, e) {
if (!this.isMousedown) {
return
}
this.createCloneNode()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
this.cloneNodeLeft = x - this.offsetX
this.cloneNodeTop = y - this.offsetY
x = (this.cloneNodeLeft - translateX) / scaleX
y = (this.cloneNodeTop - translateY) / scaleY
let t = this.clone.transform()
this.clone.translate(x - t.translateX, y - t.translateY)
// 连接线
let parent = this.node.parent
this.line.plot(
this.quadraticCurvePath(
parent.left + parent.width / 2,
parent.top + parent.height / 2,
x + this.node.width / 2,
y + this.node.height / 2
)
)
this.checkOverlapNode()
// 如果注册了多选节点插件,那么复用它的边缘自动移动画布功能
if (this.mindMap.opt.autoMoveWhenMouseInEdgeOnDrag && this.mindMap.select) {
this.drawTransform = this.mindMap.draw.transform()
this.mindMap.select.clearAutoMoveTimer()
this.mindMap.select.onMove(e.clientX, e.clientY)
}
}
// 检测重叠节点
checkOverlapNode() {
if (!this.drawTransform || !this.placeholder) {
@@ -223,11 +322,8 @@ class Drag extends Base {
this.nextNode = null
this.placeholder.size(0, 0)
this.nodeList.forEach(node => {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
}
if (node.uid === this.node.uid) {
return
if (node.getData('isActive')) {
this.mindMap.execCommand('SET_NODE_ACTIVE', node, false)
}
if (this.overlapNode || (this.prevNode && this.nextNode)) {
return
@@ -262,7 +358,7 @@ class Drag extends Base {
}
})
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
}
}
@@ -396,9 +492,7 @@ class Drag extends Base {
getNodeDistanceToSiblingNode(checkList, node, nodeRect, dir) {
let dir1 = dir === 'v' ? 'top' : 'left'
let dir2 = dir === 'v' ? 'bottom' : 'right'
let index = checkList.findIndex(item => {
return item.uid === node.uid
})
let index = getNodeIndexInNodeList(node, checkList)
let prevBrother = null
let nextBrother = null
if (index !== -1) {
@@ -484,7 +578,7 @@ class Drag extends Base {
if (node.layerIndex === 1) {
sameDir = item.dir === node.dir
}
return item !== this.node && sameDir
return sameDir && !this.checkIsInBeingDragNodeList(item)
})
: []
this.handleVerticalCheck(node, checkList)
@@ -535,7 +629,7 @@ class Drag extends Base {
handleFishbone(node) {
let checkList = node.parent
? node.parent.children.filter(item => {
return item !== this.node && item.layerIndex > 1
return item.layerIndex > 1 && !this.checkIsInBeingDragNodeList(item)
})
: []
if (node.layerIndex === 1) {
@@ -553,8 +647,8 @@ class Drag extends Base {
// 获取节点的兄弟节点列表通用方法
commonGetNodeCheckList(node) {
return node.parent
? node.parent.children.filter(item => {
return item !== this.node
? [...node.parent.children].filter(item => {
return !this.checkIsInBeingDragNodeList(item)
})
: []
}
@@ -585,18 +679,11 @@ class Drag extends Base {
}
}
// 节点由树转换成数组,从子节点到根节点
nodeTreeToList() {
const list = []
bfsWalk(this.mindMap.renderer.root, node => {
if (!list[node.layerIndex]) {
list[node.layerIndex] = []
}
list[node.layerIndex].push(node)
// 检查某个节点是否在被拖拽节点内
checkIsInBeingDragNodeList(node) {
return !!this.beingDragNodeList.find(item => {
return item.uid === node.uid || item.isAncestor(node)
})
this.nodeList = list.reduceRight((res, cur) => {
return [...res, ...cur]
}, [])
}
}

View File

@@ -2,7 +2,9 @@ import {
imgToDataUrl,
downloadFile,
readBlob,
removeHTMLEntities
removeHTMLEntities,
resizeImgSize,
handleSelfCloseTags
} from '../utils'
import { SVG } from '@svgdotjs/svg.js'
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
@@ -19,7 +21,7 @@ class Export {
// 导出
async export(type, isDownload = true, name = '思维导图', ...args) {
if (this[type]) {
let result = await this[type](name, ...args)
const result = await this[type](name, ...args)
if (isDownload && type !== 'pdf') {
downloadFile(result, name + '.' + type)
}
@@ -29,6 +31,20 @@ class Export {
}
}
// 创建图片url转换任务
createTransformImgTaskList(svg, tagName, propName, getUrlFn) {
const imageList = svg.find(tagName)
return imageList.map(async item => {
const imgUlr = getUrlFn(item)
// 已经是data:URL形式不用转换
if (/^data:/.test(imgUlr) || imgUlr === 'none') {
return
}
const imgData = await imgToDataUrl(imgUlr)
item.attr(propName, imgData)
})
}
// 获取svg数据
async getSvgData() {
let { exportPaddingX, exportPaddingY } = this.mindMap.opt
@@ -36,19 +52,34 @@ class Export {
paddingX: exportPaddingX,
paddingY: exportPaddingY
})
// 把图片的url转换成data:url类型否则导出会丢失图片
let imageList = svg.find('image')
let task = imageList.map(async item => {
let imgUlr = item.attr('href') || item.attr('xlink:href')
// 已经是data:URL形式不用转换
if (/^data:/.test(imgUlr)) {
return
// svg的image标签把图片的url转换成data:url类型否则导出会丢失图片
const task1 = this.createTransformImgTaskList(
svg,
'image',
'href',
item => {
return item.attr('href') || item.attr('xlink:href')
}
let imgData = await imgToDataUrl(imgUlr)
item.attr('href', imgData)
)
// html的img标签
const task2 = this.createTransformImgTaskList(svg, 'img', 'src', item => {
return item.attr('src')
})
await Promise.all(task)
if (imageList.length > 0) {
const taskList = [...task1, ...task2]
await Promise.all(taskList)
// 开启了节点富文本编辑,需要增加一些样式
let isAddResetCss
if (this.mindMap.richText) {
const foreignObjectList = svg.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(
SVG(`<style>${this.mindMap.opt.resetCss}</style>`)
)
isAddResetCss = true
}
}
// svg节点内容有变需要重新获取html字符串
if (taskList.length > 0 || isAddResetCss) {
svgHTML = svg.svg()
}
return {
@@ -58,7 +89,14 @@ class Export {
}
// svg转png
svgToPng(svgSrc, transparent, checkRotate = () => { return false }) {
svgToPng(
svgSrc,
transparent,
checkRotate = () => {
return false
},
compress
) {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
@@ -66,9 +104,23 @@ class Export {
img.onload = async () => {
try {
const canvas = document.createElement('canvas')
const dpr = Math.max(window.devicePixelRatio, this.mindMap.opt.minExportImgCanvasScale)
const imgWidth = img.width
const imgHeight = img.height
const dpr = Math.max(
window.devicePixelRatio,
this.mindMap.opt.minExportImgCanvasScale
)
let imgWidth = img.width
let imgHeight = img.height
// 压缩图片
if (compress) {
const compressedSize = resizeImgSize(
imgWidth,
imgHeight,
compress.width,
compress.height
)
imgWidth = compressedSize[0]
imgHeight = compressedSize[1]
}
// 如果宽比高长那么旋转90度
const needRotate = checkRotate(imgWidth, imgHeight)
if (needRotate) {
@@ -109,7 +161,7 @@ class Export {
// 在canvas上绘制思维导图背景
drawBackgroundToCanvas(ctx, width, height) {
return new Promise((resolve, reject) => {
let {
const {
backgroundColor = '#fff',
backgroundImage,
backgroundRepeat = 'no-repeat',
@@ -153,7 +205,7 @@ class Export {
// 在svg上绘制思维导图背景
drawBackgroundToSvg(svg) {
return new Promise(async resolve => {
let {
const {
backgroundColor = '#fff',
backgroundImage,
backgroundRepeat = 'repeat'
@@ -162,7 +214,7 @@ class Export {
svg.css('background-color', backgroundColor)
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
let imgDataUrl = await imgToDataUrl(backgroundImage)
const imgDataUrl = await imgToDataUrl(backgroundImage)
svg.css('background-image', `url(${imgDataUrl})`)
svg.css('background-repeat', backgroundRepeat)
resolve()
@@ -177,51 +229,31 @@ class Export {
* 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png(name, transparent = false, checkRotate) {
let { node, str } = await this.getSvgData()
str = removeHTMLEntities(str)
// 如果开启了富文本则使用htmltocanvas转换为图片
if (this.mindMap.richText) {
// 覆盖html默认的样式
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>${ this.mindMap.opt.resetCss }</style>`))
}
str = node.svg()
// 使用其他库html2canvas、dom-to-image-more等来完成导出
// let res = await this.mindMap.richText.handleExportPng(node.node)
// let imgDataUrl = await this.svgToPng(
// res,
// transparent,
// checkRotate
// )
// return imgDataUrl
}
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'
})
// 转换成data:url数据
let svgUrl = await readBlob(blob)
async png(name, transparent = false, checkRotate, compress) {
const { str } = await this.getSvgData()
const svgUrl = await this.fixSvgStrAndToBlob(str)
// 绘制到canvas上
let res = await this.svgToPng(
svgUrl,
transparent,
checkRotate
)
const res = await this.svgToPng(svgUrl, transparent, checkRotate, compress)
return res
}
// 导出为pdf
async pdf(name, useMultiPageExport) {
async pdf(name, useMultiPageExport, maxImageWidth) {
if (!this.mindMap.doExportPDF) {
throw new Error('请注册ExportPDF插件')
}
let img = await this.png('', false, (width, height) => {
if (width <= a4Size.width && height && a4Size.height) return false
return (width / height) > 1
})
this.mindMap.doExportPDF.pdf(name, img, useMultiPageExport)
const img = await this.png(
'',
false,
(width, height) => {
if (width <= a4Size.width && height && a4Size.height) return false
return width / height > 1
},
{
width: maxImageWidth || a4Size.width * 2
}
)
await this.mindMap.doExportPDF.pdf(name, img, useMultiPageExport)
}
// 导出为xmind
@@ -236,49 +268,50 @@ class Export {
}
// 导出为svg
// plusCssText附加的css样式如果svg中存在dom节点想要设置一些针对节点的样式可以通过这个参数传入
async svg(name) {
let { node } = await this.getSvgData()
// 开启了节点富文本编辑
if (this.mindMap.richText) {
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>${ this.mindMap.opt.resetCss }</style>`))
}
}
const { node } = await this.getSvgData()
node.first().before(SVG(`<title>${name}</title>`))
await this.drawBackgroundToSvg(node)
let str = node.svg()
const str = node.svg()
const res = await this.fixSvgStrAndToBlob(str)
return res
}
// 修复svg字符串并且转换为blob数据
async fixSvgStrAndToBlob(str) {
// 移除字符串中的html实体
str = removeHTMLEntities(str)
// 给html自闭合标签添加闭合状态
str = handleSelfCloseTags(str)
// 转换成blob数据
let blob = new Blob([str], {
const blob = new Blob([str], {
type: 'image/svg+xml'
})
let res = await readBlob(blob)
const res = await readBlob(blob)
return res
}
// 导出为json
async json(name, withConfig = true) {
let data = this.mindMap.getData(withConfig)
let str = JSON.stringify(data)
let blob = new Blob([str])
let res = await readBlob(blob)
const data = this.mindMap.getData(withConfig)
const str = JSON.stringify(data)
const blob = new Blob([str])
const res = await readBlob(blob)
return res
}
// 专有文件其实就是json文件
async smm(name, withConfig) {
let res = await this.json(name, withConfig)
const 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)
const data = this.mindMap.getData()
const content = transformToMarkdown(data)
const blob = new Blob([content])
const res = await readBlob(blob)
return res
}
}

View File

@@ -1,4 +1,4 @@
import JsPDF from 'jspdf'
import JsPDF from '../utils/jspdf'
import { a4Size } from '../constants/constant'
// 导出PDF插件需要通过Export插件使用
@@ -9,85 +9,104 @@ class ExportPDF {
}
// 导出为pdf
pdf(name, img, useMultiPageExport = false) {
async pdf(name, img, useMultiPageExport = false) {
if (useMultiPageExport) {
this.multiPageExport(name, img)
await this.multiPageExport(name, img)
} else {
this.onePageExport(name, img)
await this.onePageExport(name, img)
}
}
// 单页导出
onePageExport(name, img) {
let pdf = new JsPDF('', 'pt', 'a4')
let a4Ratio = a4Size.width / a4Size.height
let image = new Image()
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
let imageRatio = imageWidth / imageHeight
let w, h
if (imageWidth <= a4Size.width && imageHeight <= a4Size.height) {
// 使用图片原始宽高
w = imageWidth
h = imageHeight
} else if (a4Ratio > imageRatio) {
// 以a4Height为高度缩放图片宽度
w = imageRatio * a4Size.height
h = a4Size.height
} else {
// 以a4Width为宽度缩放图片高度
w = a4Size.width
h = a4Size.width / imageRatio
return new Promise((resolve, reject) => {
let pdf = new JsPDF('', 'pt', 'a4')
let a4Ratio = a4Size.width / a4Size.height
let image = new Image()
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
let imageRatio = imageWidth / imageHeight
let w, h
if (imageWidth <= a4Size.width && imageHeight <= a4Size.height) {
// 使用图片原始宽高
w = imageWidth
h = imageHeight
} else if (a4Ratio > imageRatio) {
// 以a4Height为高度缩放图片宽度
w = imageRatio * a4Size.height
h = a4Size.height
} else {
// 以a4Width为宽度缩放图片高度
w = a4Size.width
h = a4Size.width / imageRatio
}
pdf.addImage(
img,
'PNG',
(a4Size.width - w) / 2,
(a4Size.height - h) / 2,
w,
h
)
pdf.save(name)
resolve()
}
pdf.addImage(img, 'PNG', (a4Size.width - w) / 2, (a4Size.height - h) / 2, w, h)
pdf.save(name)
}
image.src = img
image.onerror = e => {
reject(e)
}
image.src = img
})
}
// 多页导出
multiPageExport(name, img) {
let image = new Image()
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
// 一页pdf显示高度
let pageHeight = (imageWidth / a4Size.width) * a4Size.height
// 未生成pdf的高度
let leftHeight = imageHeight
// 偏移
let position = 0
// a4纸的尺寸[595.28,841.89]图片在pdf中图片的宽高
let imgWidth = a4Size.width
let imgHeight = (a4Size.width / imageWidth) * imageHeight
let pdf = new JsPDF('', 'pt', 'a4')
// 有两个高度需要区分一个是图片的实际高度和生成pdf的页面高度(841.89)
// 当内容未超过pdf一页显示的范围无需分页
if (leftHeight < pageHeight) {
pdf.addImage(
img,
'PNG',
(a4Size.width - imgWidth) / 2,
(a4Size.height - imgHeight) / 2,
imgWidth,
imgHeight
)
} else {
// 分页
while (leftHeight > 0) {
pdf.addImage(img, 'PNG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight
position -= a4Size.height
// 避免添加空白页
if (leftHeight > 0) {
pdf.addPage()
return new Promise((resolve, reject) => {
let image = new Image()
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
// 一页pdf显示高度
let pageHeight = (imageWidth / a4Size.width) * a4Size.height
// 未生成pdf的高度
let leftHeight = imageHeight
// 偏移
let position = 0
// a4纸的尺寸[595.28,841.89]图片在pdf中图片的宽高
let imgWidth = a4Size.width
let imgHeight = (a4Size.width / imageWidth) * imageHeight
let pdf = new JsPDF('', 'pt', 'a4')
// 有两个高度需要区分一个是图片的实际高度和生成pdf的页面高度(841.89)
// 当内容未超过pdf一页显示的范围无需分页
if (leftHeight < pageHeight) {
pdf.addImage(
img,
'PNG',
(a4Size.width - imgWidth) / 2,
(a4Size.height - imgHeight) / 2,
imgWidth,
imgHeight
)
} else {
// 分页
while (leftHeight > 0) {
pdf.addImage(img, 'PNG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight
position -= a4Size.height
// 避免添加空白页
if (leftHeight > 0) {
pdf.addPage()
}
}
}
pdf.save(name)
resolve()
}
pdf.save(name)
}
image.src = img
image.onerror = (e) => {
reject(e)
}
image.src = img
})
}
}

View File

@@ -0,0 +1,53 @@
import katex from 'katex'
import Quill from 'quill'
// 数学公式支持插件
// 该插件在富文本模式下可用
class Formula {
// 构造函数
constructor(opt) {
this.opt = opt
this.mindMap = opt.mindMap
window.katex = katex
this.extendQuill()
}
// 修改formula格式工具
extendQuill() {
const QuillFormula = Quill.import('formats/formula')
class CustomFormulaBlot extends QuillFormula {
static create(value) {
let node = super.create(value)
if (typeof value === 'string') {
katex.render(value, node, {
throwOnError: false,
errorColor: '#f00',
output: 'mathml' // 增加该配置,默认只输出公式
})
node.setAttribute('data-value', value)
}
return node
}
}
Quill.register('formats/formula', CustomFormulaBlot, true)
}
// 给指定的节点插入指定公式
insertFormulaToNode(node, formula) {
let richTextPlugin = this.mindMap.richText
richTextPlugin.showEditText(node)
richTextPlugin.quill.insertEmbed(
richTextPlugin.quill.getLength() - 1,
'formula',
formula
)
richTextPlugin.setTextStyleIfNotRichText(richTextPlugin.node)
richTextPlugin.hideEditText([node])
}
}
Formula.instanceName = 'formula'
export default Formula

View File

@@ -232,4 +232,4 @@ class KeyboardNavigation {
KeyboardNavigation.instanceName = 'keyboardNavigation'
export default KeyboardNavigation
export default KeyboardNavigation

View File

@@ -1,4 +1,9 @@
import { isWhite, isTransparent, getVisibleColorFromTheme } from '../utils/index'
import {
isWhite,
isTransparent,
getVisibleColorFromTheme,
readBlob
} from '../utils/index'
// 小地图插件
class MiniMap {
@@ -23,7 +28,9 @@ class MiniMap {
*/
calculationMiniMap(boxWidth, boxHeight) {
let { svg, rect, origWidth, origHeight, scaleX, scaleY } =
this.mindMap.getSvgData()
this.mindMap.getSvgData({
ignoreWatermark: true
})
// 计算数据
const elRect = this.mindMap.elRect
rect.x -= elRect.left
@@ -78,13 +85,21 @@ class MiniMap {
viewBoxStyle.left = miniMapBoxLeft + actWidth
}
Object.keys(viewBoxStyle).forEach((key) => {
Object.keys(viewBoxStyle).forEach(key => {
viewBoxStyle[key] = viewBoxStyle[key] + 'px'
})
this.removeNodeContent(svg)
const svgStr = svg.svg()
return {
svgHTML: svg.svg(), // 小地图html
getImgUrl: async callback => {
const blob = new Blob([svgStr], {
type: 'image/svg+xml'
})
const res = await readBlob(blob)
callback(res)
},
svgHTML: svgStr, // 小地图html
viewBoxStyle, // 视图框的位置信息
miniMapBoxScale, // 视图框的缩放值
miniMapBoxLeft, // 视图框的left值
@@ -106,7 +121,7 @@ class MiniMap {
}
let children = svg.children()
if (children && children.length > 0) {
children.forEach((node) => {
children.forEach(node => {
this.removeNodeContent(node)
})
}

View File

@@ -49,7 +49,7 @@ class NodeImgAdjust {
// 如果当前正在拖动调整中那么直接返回
if (this.isMousedown || this.isAdjusted || this.mindMap.opt.readonly) return
// 如果在当前节点内移动,以及自定义元素已经是显示状态,那么直接返回
if ((this.node && this.node.uid === node.uid) && this.isShowHandleEl) return
if (this.node && this.node.uid === node.uid && this.isShowHandleEl) return
// 更新当前节点信息
this.node = node
this.img = img
@@ -205,7 +205,7 @@ class NodeImgAdjust {
// 隐藏节点实际图片
this.hideNodeImage()
// 将节点图片渲染到自定义元素上
this.handleEl.style.backgroundImage = `url(${this.node.nodeData.data.image})`
this.handleEl.style.backgroundImage = `url(${this.node.getData('image')})`
}
// 鼠标移动
@@ -214,7 +214,7 @@ class NodeImgAdjust {
e.preventDefault()
// 计算当前拖拽位置对应的图片的实时大小
let { width: imageOriginWidth, height: imageOriginHeight } =
this.node.nodeData.data.imageSize
this.node.getData('imageSize')
let newWidth = e.clientX - this.rect.x
let newHeight = e.clientY - this.rect.y
if (newWidth <= 0 || newHeight <= 0) return
@@ -237,7 +237,7 @@ class NodeImgAdjust {
// 隐藏自定义元素
this.hideHandleEl()
// 更新节点图片为新的大小
let { image, imageTitle } = this.node.nodeData.data
let { image, imageTitle } = this.node.getData()
let { scaleX, scaleY } = this.mindMap.draw.transform()
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, {
url: image,

View File

@@ -38,6 +38,7 @@ class Painter {
}
onEndPainter() {
if (!this.isInPainter) return
this.endPainter()
this.mindMap.emit('painter_end')
}
@@ -53,7 +54,7 @@ class Painter {
)
return
const style = {}
const painterNodeData = this.painterNode.nodeData.data
const painterNodeData = this.painterNode.getData()
Object.keys(painterNodeData).forEach(key => {
if (checkIsNodeStyleDataKey(key)) {
style[key] = painterNodeData[key]

View File

@@ -5,7 +5,8 @@ import {
walk,
getTextFromHtml,
isWhite,
getVisibleColorFromTheme
getVisibleColorFromTheme,
isUndef
} from '../utils'
import { CONSTANTS } from '../constants/constant'
@@ -152,7 +153,7 @@ class RichText {
}
// 显示文本编辑控件
showEditText(node, rect, isInserting, isFromKeyDown) {
showEditText({ node, rect, isInserting, isFromKeyDown, isFromScale }) {
if (this.showTextEdit) {
return
}
@@ -166,7 +167,9 @@ class RichText {
this.node = node
this.isInserting = isInserting
if (!rect) rect = node._textData.node.node.getBoundingClientRect()
this.mindMap.emit('before_show_text_edit')
if (!isFromScale) {
this.mindMap.emit('before_show_text_edit')
}
this.mindMap.renderer.textEdit.registerTmpShortcut()
// 原始宽高
let g = node._textData.node
@@ -235,22 +238,27 @@ class RichText {
this.textEditNode.style.borderRadius = (node.height || 50) + 'px'
}
}
if (!node.nodeData.data.richText) {
if (!node.getData('richText')) {
// 还不是富文本的情况
let text = node.nodeData.data.text.split(/\n/gim).join('<br>')
let text = ''
if (!isUndef(node.getData('text'))) {
text = String(node.getData('text')).split(/\n/gim).join('<br>')
}
let html = `<p>${text}</p>`
this.textEditNode.innerHTML = this.cacheEditingText || html
} else {
this.textEditNode.innerHTML =
this.cacheEditingText || node.nodeData.data.text
this.cacheEditingText || node.getData('text')
}
this.initQuillEditor()
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
this.showTextEdit = true
// 如果是刚创建的节点那么默认全选否则普通激活不全选除非selectTextOnEnterEditText配置为true
// 在selectTextOnEnterEditText时如果是在keydown事件进入的节点编辑也不需要全选
this.focus(isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null)
if (!node.nodeData.data.richText) {
this.focus(
isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null
)
if (!node.getData('richText')) {
// 如果是非富文本的情况,需要手动应用文本样式
this.setTextStyleIfNotRichText(node)
}
@@ -373,7 +381,7 @@ class RichText {
}
})
// 拦截粘贴,只允许粘贴纯文本
this.quill.clipboard.addMatcher(Node.TEXT_NODE, (node) => {
this.quill.clipboard.addMatcher(Node.TEXT_NODE, node => {
let style = this.getPasteTextStyle()
return new Delta().insert(node.data, style)
})
@@ -416,10 +424,13 @@ class RichText {
// 中文输入结束
onCompositionEnd() {
if (!this.showTextEdit || !this.lostStyle) {
if (!this.showTextEdit) {
return
}
this.isCompositing = false
if (!this.lostStyle) {
return
}
this.setTextStyleIfNotRichText(this.node)
}
@@ -488,7 +499,7 @@ class RichText {
})
} else {
let data = this.richTextStyleToNormalStyle(config)
this.mindMap.renderer.setNodeData(this.node, data)
this.mindMap.execCommand('SET_NODE_DATA', this.node, data)
}
}
@@ -558,6 +569,16 @@ class RichText {
return data
}
// 给未激活的节点设置富文本样式
setNotActiveNodeStyle(node, style) {
const config = this.normalStyleToRichTextStyle(style)
if (Object.keys(config).length > 0) {
this.showEditText({ node })
this.formatAllText(config)
this.hideEditText([node])
}
}
// 处理导出为图片
async handleExportPng(node) {
let el = document.createElement('div')
@@ -614,7 +635,7 @@ class RichText {
// 处理导入数据
handleSetData(data) {
let walk = root => {
if (!root.data.richText) {
if (root.data && !root.data.richText) {
root.data.richText = true
root.data.resetRichText = true
}
@@ -632,11 +653,13 @@ class RichText {
beforePluginRemove() {
this.transformAllNodesToNormalNode()
document.head.removeChild(this.styleEl)
this.unbindEvent()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
document.head.removeChild(this.styleEl)
this.unbindEvent()
}
}

View File

@@ -1,4 +1,9 @@
import { bfsWalk, getTextFromHtml, isUndef, replaceHtmlText } from '../utils/index'
import {
bfsWalk,
getTextFromHtml,
isUndef,
replaceHtmlText
} from '../utils/index'
// 搜索插件
class Search {
@@ -60,6 +65,9 @@ class Search {
this.currentIndex = -1
this.notResetSearchText = false
this.isSearching = false
if (this.mindMap.opt.readonly) {
this.mindMap.renderer.closeHighlightNode()
}
this.emitEvent()
}
@@ -68,7 +76,7 @@ class Search {
this.matchNodeList = []
this.currentIndex = -1
bfsWalk(this.mindMap.renderer.root, node => {
let { richText, text } = node.nodeData.data
let { richText, text } = node.getData()
if (richText) {
text = getTextFromHtml(text)
}
@@ -91,6 +99,10 @@ class Search {
this.mindMap.execCommand('GO_TARGET_NODE', currentNode, () => {
this.notResetSearchText = false
callback()
// 只读模式下节点无法激活,所以通过高亮的方式
if (this.mindMap.opt.readonly) {
this.mindMap.renderer.highlightNode(currentNode)
}
})
}
@@ -110,7 +122,7 @@ class Search {
if (!currentNode) return
let text = this.getReplacedText(currentNode, this.searchText, replaceText)
this.notResetSearchText = true
currentNode.setText(text, currentNode.nodeData.data.richText, true)
currentNode.setText(text, currentNode.getData('richText'), true)
this.matchNodeList = this.matchNodeList.filter(node => {
return currentNode !== node
})
@@ -138,7 +150,7 @@ class Search {
node,
{
text,
resetRichText: !!node.nodeData.data.richText
resetRichText: !!node.getData('richText')
},
true
)
@@ -150,7 +162,7 @@ class Search {
// 获取某个节点替换后的文本
getReplacedText(node, searchText, replaceText) {
let { richText, text } = node.nodeData.data
let { richText, text } = node.getData()
if (richText) {
return replaceHtmlText(text, searchText, replaceText)
} else {

View File

@@ -1,4 +1,4 @@
import { bfsWalk, throttle } from '../utils'
import { bfsWalk, throttle, checkTwoRectIsOverlap } from '../utils'
// 节点选择插件
class Select {
@@ -89,23 +89,28 @@ class Select {
}
)
})
this.mindMap.on('mouseup', () => {
if (this.mindMap.opt.readonly) {
return
}
if (!this.isMousedown) {
return
}
this.checkTriggerNodeActiveEvent()
clearTimeout(this.autoMoveTimer)
this.isMousedown = false
this.cacheActiveList = []
if (this.rect) this.rect.remove()
this.rect = null
setTimeout(() => {
this.isSelecting = false
}, 0)
})
this.onMouseup = this.onMouseup.bind(this)
this.mindMap.on('mouseup', this.onMouseup)
this.mindMap.on('node_mouseup', this.onMouseup)
}
// 结束框选
onMouseup() {
if (this.mindMap.opt.readonly) {
return
}
if (!this.isMousedown) {
return
}
this.checkTriggerNodeActiveEvent()
clearTimeout(this.autoMoveTimer)
this.isMousedown = false
this.cacheActiveList = []
if (this.rect) this.rect.remove()
this.rect = null
setTimeout(() => {
this.isSelecting = false
}, 0)
}
// 如果激活节点改变了,那么触发事件
@@ -119,7 +124,7 @@ class Select {
let cur = this.cacheActiveList[i]
if (
!this.mindMap.renderer.activeNodeList.find(item => {
return item.nodeData.data.uid === cur.nodeData.data.uid
return item.getData('uid') === cur.getData('uid')
})
) {
isNodeChange = true
@@ -128,9 +133,7 @@ class Select {
}
}
if (isNumChange || isNodeChange) {
this.mindMap.emit('node_active', null, [
...this.mindMap.renderer.activeNodeList
])
this.mindMap.renderer.emitNodeActiveEvent()
}
}
@@ -184,6 +187,7 @@ class Select {
// 创建矩形
createRect(x, y) {
if (this.rect) this.rect.remove()
this.rect = this.mindMap.svg
.polygon()
.stroke({
@@ -210,24 +214,17 @@ class Select {
left = left * scaleX + translateX
top = top * scaleY + translateY
if (
((left >= minx && left <= maxx) || (right >= minx && right <= maxx)) &&
((top >= miny && top <= maxy) || (bottom >= miny && bottom <= maxy))
checkTwoRectIsOverlap(minx, maxx, miny, maxy, left, right, top, bottom)
) {
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (node.nodeData.data.isActive) {
if (node.getData('isActive')) {
return
}
this.mindMap.renderer.setNodeActive(node, true)
this.mindMap.renderer.addActiveNode(node)
// })
} else if (node.nodeData.data.isActive) {
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (!node.nodeData.data.isActive) {
this.mindMap.renderer.addNodeToActiveList(node)
} else if (node.getData('isActive')) {
if (!node.getData('isActive')) {
return
}
this.mindMap.renderer.setNodeActive(node, false)
this.mindMap.renderer.removeActiveNode(node)
// })
this.mindMap.renderer.removeNodeFromActiveList(node)
}
})
}

View File

@@ -48,6 +48,7 @@ class TouchEvent {
let touch = e.touches[0]
this.dispatchMouseEvent('mousemove', touch.target, touch)
} else if (len === 2) {
if (this.mindMap.opt.disableTouchZoom) return
let touch1 = e.touches[0]
let touch2 = e.touches[1]
let ox = touch1.clientX - touch2.clientX

View File

@@ -11,13 +11,48 @@ class Watermark {
this.angle = 0 // 旋转角度
this.text = '' // 水印文字
this.textStyle = {} // 水印文字样式
this.watermarkDraw = null // 容器
this.maxLong = this.getMaxLong()
this.updateWatermark(this.mindMap.opt.watermarkConfig || {})
this.bindEvent()
}
getMaxLong() {
return Math.sqrt(
Math.pow(this.mindMap.width, 2) + Math.pow(this.mindMap.height, 2)
)
}
bindEvent() {
this.onResize = this.onResize.bind(this)
this.mindMap.on('resize', this.onResize)
}
unBindEvent() {
this.mindMap.off('resize', this.onResize)
}
onResize() {
this.maxLong = this.getMaxLong()
this.draw()
}
// 创建水印容器
createContainer() {
if (this.watermarkDraw) return
this.watermarkDraw = this.mindMap.svg
.group()
.css({ 'pointer-events': 'none', 'user-select': 'none' })
this.maxLong = Math.sqrt(
Math.pow(this.mindMap.width, 2) + Math.pow(this.mindMap.height, 2)
)
this.updateWatermark(this.mindMap.opt.watermarkConfig || {})
.addClass('smm-water-mark-container')
}
// 删除水印容器
removeContainer() {
if (!this.watermarkDraw) {
return
}
this.watermarkDraw.remove()
this.watermarkDraw = null
}
// 获取是否存在水印
@@ -40,10 +75,14 @@ class Watermark {
// 绘制水印
// 非精确绘制,会绘制一些超出可视区域的水印
draw() {
this.watermarkDraw.clear()
// 清空之前的水印
if (this.watermarkDraw) this.watermarkDraw.clear()
// 如果没有水印数据,那么水印容器也删除掉
if (!this.hasWatermark()) {
this.removeContainer()
return
}
this.createContainer()
let x = 0
while (x < this.mindMap.width) {
this.drawText(x)
@@ -109,12 +148,25 @@ class Watermark {
// 更新水印
updateWatermark(config) {
this.mindMap.opt.watermarkConfig = merge(this.mindMap.opt.watermarkConfig, config)
this.mindMap.opt.watermarkConfig = merge(
this.mindMap.opt.watermarkConfig,
config
)
this.handleConfig(config)
this.draw()
}
// 插件被移除前做的事情
beforePluginRemove() {
this.unBindEvent()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
Watermark.instanceName = 'watermark'
export default Watermark
export default Watermark

View File

@@ -9,10 +9,10 @@ import {
function createControlNodes() {
let { associativeLineActiveColor } = this.mindMap.themeConfig
// 连线
this.controlLine1 = this.draw
this.controlLine1 = this.associativeLineDraw
.line()
.stroke({ color: associativeLineActiveColor, width: 2 })
this.controlLine2 = this.draw
this.controlLine2 = this.associativeLineDraw
.line()
.stroke({ color: associativeLineActiveColor, width: 2 })
// 控制点
@@ -23,7 +23,7 @@ function createControlNodes() {
// 创建控制点
function createOneControlNode(pointKey) {
let { associativeLineActiveColor } = this.mindMap.themeConfig
return this.draw
return this.associativeLineDraw
.circle(this.controlPointDiameter)
.stroke({ color: associativeLineActiveColor })
.fill({ color: '#fff' })
@@ -64,7 +64,7 @@ function onControlPointMousemove(e) {
let [, , , node, toNode] = this.activeLine
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
let { associativeLinePoint, associativeLineTargetControlOffsets } =
node.nodeData.data
node.getData()
associativeLinePoint = associativeLinePoint || []
const nodePos = this.getNodePos(node)
const toNodePos = this.getNodePos(toNode)
@@ -160,7 +160,7 @@ function onControlPointMouseup(e) {
let [, , , node] = this.activeLine
let offsetList = []
let { associativeLinePoint, associativeLineTargetControlOffsets } =
node.nodeData.data
node.getData()
if (!associativeLinePoint) {
associativeLinePoint = []
}

View File

@@ -1,9 +1,13 @@
import { Text } from '@svgdotjs/svg.js'
import { getStrWithBrFromHtml } from '../../utils/index'
import {
getStrWithBrFromHtml,
focusInput,
selectAllInput
} from '../../utils/index'
// 创建文字节点
function createText(data) {
let g = this.draw.group()
let g = this.associativeLineDraw.group()
const setActive = () => {
if (
!this.activeLine ||
@@ -36,7 +40,7 @@ function showEditTextBox(g) {
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
// 输入框元素没有创建过,则先创建
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
@@ -55,20 +59,27 @@ function showEditTextBox(g) {
associativeLineTextFontFamily,
associativeLineTextLineHeight
} = this.mindMap.themeConfig
let { defaultAssociativeLineText, nodeTextEditZIndex } = this.mindMap.opt
let scale = this.mindMap.view.scale
let [, , , node, toNode] = this.activeLine
let textLines = (
this.getText(node, toNode) || this.mindMap.opt.defaultAssociativeLineText
).split(/\n/gim)
let text = this.getText(node, toNode)
let textLines = (text || defaultAssociativeLineText).split(/\n/gim)
this.textEditNode.style.fontFamily = associativeLineTextFontFamily
this.textEditNode.style.fontSize = associativeLineTextFontSize * scale + 'px'
this.textEditNode.style.lineHeight =
textLines.length > 1 ? associativeLineTextLineHeight : 'normal'
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.style.zIndex = nodeTextEditZIndex
this.textEditNode.innerHTML = textLines.join('<br>')
this.textEditNode.style.display = 'block'
this.updateTextEditBoxPos(g)
this.showTextEdit = true
// 如果是默认文本要全选输入框
if (text === '' || text === defaultAssociativeLineText) {
selectAllInput(this.textEditNode)
} else {
// 否则聚焦即可
focusInput(this.textEditNode)
}
}
// 处理画布缩放
@@ -94,10 +105,13 @@ function hideEditTextBox() {
}
let [path, , text, node, toNode] = this.activeLine
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
// 如果是默认文本,那么不保存
let isDefaultText = str === this.mindMap.opt.defaultAssociativeLineText
str = isDefaultText ? '' : str
this.mindMap.execCommand('SET_NODE_DATA', node, {
associativeLineText: {
...(node.nodeData.data.associativeLineText || {}),
[toNode.nodeData.data.id]: str
...(node.getData('associativeLineText') || {}),
[toNode.getData('uid')]: str
}
})
this.textEditNode.style.display = 'none'
@@ -109,20 +123,18 @@ function hideEditTextBox() {
// 获取某根关联线的文字
function getText(node, toNode) {
let obj = node.nodeData.data.associativeLineText
let obj = node.getData('associativeLineText')
if (!obj) {
return ''
}
return obj[toNode.nodeData.data.id] || ''
return obj[toNode.getData('uid')] || ''
}
// 渲染关联线文字
function renderText(str, path, text) {
if (!str) return
let {
associativeLineTextFontSize,
associativeLineTextLineHeight
} = this.mindMap.themeConfig
let { associativeLineTextFontSize, associativeLineTextLineHeight } =
this.mindMap.themeConfig
text.clear()
let textArr = str.split(/\n/gim)
textArr.forEach((item, index) => {

View File

@@ -1,7 +1,7 @@
// 获取目标节点在起始节点的目标数组中的索引
export const getAssociativeLineTargetIndex = (node, toNode) => {
return node.nodeData.data.associativeLineTargets.findIndex(item => {
return item === toNode.nodeData.data.id
return node.getData('associativeLineTargets').findIndex(item => {
return item === toNode.getData('uid')
})
}
@@ -202,7 +202,7 @@ export const computeNodePoints = (fromNode, toNode) => {
// 中心点坐标的差值
let offsetX = toCx - fromCx
let offsetY = toCy - fromCy
if (offsetX === 0 && offsetY === 0) return
if (offsetX === 0 && offsetY === 0) return []
let fromDir = ''
let toDir = ''
if (offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY) {
@@ -231,7 +231,7 @@ export const getNodeLinePath = (startPoint, endPoint, node, toNode) => {
// 控制点
let controlPoints = []
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
node.getData('associativeLineTargetControlOffsets')
if (
associativeLineTargetControlOffsets &&
associativeLineTargetControlOffsets[targetIndex]

View File

@@ -1,3 +1,5 @@
import { mergerIconList } from '../utils'
// 超链接图标
const hyperlink =
'<svg t="1624174958075" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7982" ><path d="M435.484444 251.733333v68.892445L295.822222 320.682667a168.504889 168.504889 0 0 0-2.844444 336.952889h142.506666v68.892444H295.822222a237.397333 237.397333 0 0 1 0-474.794667h139.662222z m248.945778 0a237.397333 237.397333 0 0 1 0 474.851556H544.654222v-69.006222l139.776 0.056889a168.504889 168.504889 0 0 0 2.844445-336.952889H544.597333V251.676444h139.776z m-25.827555 203.946667a34.474667 34.474667 0 0 1 0 68.892444H321.649778a34.474667 34.474667 0 0 1 0-68.892444h336.952889z" p-id="7983"></path></svg>'
@@ -281,12 +283,21 @@ export const nodeIconList = [
// 获取nodeIconList icon内容
const getNodeIconListIcon = (name, extendIconList = []) => {
let arr = name.split('_')
let typeData = [...nodeIconList, ...extendIconList].find(item => {
const iconList = mergerIconList([...nodeIconList, ...extendIconList])
let typeData = iconList.find(item => {
return item.type === arr[0]
})
return typeData.list.find(item => {
return item.name === arr[1]
}).icon
if (typeData) {
let typeName = typeData.list.find(item => {
return item.name === arr[1]
})
if (typeName) {
return typeName.icon
}
return ''
} else {
return ''
}
}
export default {

View File

@@ -161,12 +161,14 @@ const nodeSizeIndependenceList = [
'backgroundSize',
'rootLineKeepSameInCurve'
]
export const checkIsNodeSizeIndependenceConfig = (config) => {
export const checkIsNodeSizeIndependenceConfig = config => {
let keys = Object.keys(config)
for(let i = 0; i < keys.length; i++) {
if (!nodeSizeIndependenceList.find((item) => {
return item === keys[i]
})) {
for (let i = 0; i < keys.length; i++) {
if (
!nodeSizeIndependenceList.find(item => {
return item === keys[i]
})
) {
return false
}
}

View File

@@ -12,6 +12,7 @@ class BatchExecution {
// 添加任务
push(name, fn) {
if (this.has[name]) {
this.replaceTask(name, fn)
return
}
this.has[name] = true
@@ -22,6 +23,19 @@ class BatchExecution {
this.nextTick()
}
// 替换任务
replaceTask(name, fn) {
const index = this.queue.findIndex(item => {
return item.name === name
})
if (index !== -1) {
this.queue[index] = {
name,
fn
}
}
}
// 执行队列
flush() {
let fns = this.queue.slice(0)

View File

@@ -1,39 +1,38 @@
// LRU缓存类
export default class CRU {
constructor(max) {
this.max = max || 1000
this.size = 0
this.pool = new Map()
}
export default class Lru {
constructor(max) {
this.max = max || 1000
this.size = 0
this.pool = new Map()
}
add(key, value) {
// 如果该key是否已经存在则先删除
this.delete(key)
this.pool.set(key, value)
this.size++
// 如果数量超出最大值,则删除最早的
if (this.size > this.max) {
let keys = this.pool.keys()
let last = keys.next()
this.delete(last.value)
}
add(key, value) {
// 如果该key是否已经存在则先删除
this.delete(key)
this.pool.set(key, value)
this.size++
// 如果数量超出最大值,则删除最早的
if (this.size > this.max) {
let keys = this.pool.keys()
let last = keys.next()
this.delete(last.value)
}
}
delete(key) {
if (this.pool.has(key)) {
this.pool.delete(key)
this.size--
}
delete(key) {
if (this.pool.has(key)) {
this.pool.delete(key)
this.size--
}
}
has(key) {
return this.pool.has(key)
}
has(key) {
return this.pool.has(key)
}
get(key) {
if (this.pool.has(key)) {
return this.pool.get(key)
}
get(key) {
if (this.pool.has(key)) {
return this.pool.get(key)
}
}
}
}

View File

@@ -1,6 +1,9 @@
import { v4 as uuidv4 } from 'uuid'
import { nodeDataNoStylePropList } from '../constants/constant'
import {
nodeDataNoStylePropList,
selfCloseTagList
} from '../constants/constant'
import MersenneTwister from './mersenneTwister'
// 深度优先遍历树
export const walk = (
root,
@@ -152,6 +155,9 @@ export const copyRenderTree = (tree, root, removeActiveState = false) => {
tree.data = simpleDeepClone(root.data)
if (removeActiveState) {
tree.data.isActive = false
if (tree.data.generalization) {
tree.data.generalization.isActive = false
}
}
tree.children = []
if (root.children && root.children.length > 0) {
@@ -167,19 +173,23 @@ export const copyNodeTree = (
tree,
root,
removeActiveState = false,
keepId = false
removeId = true
) => {
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data)
// 除节点id因为节点id不能重复
if (tree.data.id && !keepId) delete tree.data.id
if (tree.data.uid) delete tree.data.uid
// 除节点uid
if (removeId) {
delete tree.data.uid
} else if (!tree.data.uid) {
// 否则保留或生成
tree.data.uid = createUid()
}
if (removeActiveState) {
tree.data.isActive = false
}
tree.children = []
if (root.children && root.children.length > 0) {
root.children.forEach((item, index) => {
tree.children[index] = copyNodeTree({}, item, removeActiveState, keepId)
tree.children[index] = copyNodeTree({}, item, removeActiveState, removeId)
})
} else if (
root.nodeData &&
@@ -187,7 +197,7 @@ export const copyNodeTree = (
root.nodeData.children.length > 0
) {
root.nodeData.children.forEach((item, index) => {
tree.children[index] = copyNodeTree({}, item, removeActiveState, keepId)
tree.children[index] = copyNodeTree({}, item, removeActiveState, removeId)
})
}
return tree
@@ -465,7 +475,7 @@ export const removeHTMLEntities = str => {
// 获取一个数据的类型
export const getType = data => {
return Object.prototype.toString.call(data).slice(7, -1)
return Object.prototype.toString.call(data).slice(8, -1)
}
// 判断一个数据是否是null和undefined和空字符串
@@ -622,7 +632,7 @@ export const textToNodeRichTextWithWrap = html => {
}
return list
.map(item => {
return `<p><span>${item}</span></p>`
return `<p><span>${htmlEscape(item)}</span></p>`
})
.join('')
}
@@ -637,7 +647,7 @@ export const isMobile = () => {
// 获取对象改变了的的属性
export const getObjectChangedProps = (oldObject, newObject) => {
const res = {}
Object.keys(newObject).forEach((prop) => {
Object.keys(newObject).forEach(prop => {
const oldVal = oldObject[prop]
const newVal = newObject[prop]
if (getType(oldVal) !== getType(newVal)) {
@@ -660,7 +670,7 @@ export const getObjectChangedProps = (oldObject, newObject) => {
}
// 判断一个字段是否是节点数据中的样式字段
export const checkIsNodeStyleDataKey = (key) => {
export const checkIsNodeStyleDataKey = key => {
// 用户自定义字段
if (/^_/.test(key)) return false
// 不在节点非样式字段列表里,那么就是样式字段
@@ -668,4 +678,343 @@ export const checkIsNodeStyleDataKey = (key) => {
return true
}
return false
}
}
// 合并图标数组
// const data = [
// { type: 'priority', name: '优先级图标', list: [{ name: '1', icon: 'a' }, { name: 2, icon: 'b' }] },
// { type: 'priority', name: '优先级图标', list: [{ name: '2', icon: 'c' }, { name: 3, icon: 'd' }] },
// ];
// mergerIconList(data) 结果
// [
// { type: 'priority', name: '优先级图标', list: [{ name: '1', icon: 'a' }, { name: 2, icon: 'c' }, { name: 3, icon: 'd' }] },
// ]
export const mergerIconList = list => {
return list.reduce((result, item) => {
const existingItem = result.find(x => x.type === item.type)
if (existingItem) {
item.list.forEach(newObj => {
const existingObj = existingItem.list.find(x => x.name === newObj.name)
if (existingObj) {
existingObj.icon = newObj.icon
} else {
existingItem.list.push(newObj)
}
})
} else {
result.push({ ...item })
}
return result
}, [])
}
// 从节点实例列表里找出顶层的节点
export const getTopAncestorsFomNodeList = list => {
let res = []
list.forEach(node => {
if (
!list.find(item => {
return item.uid !== node.uid && item.isAncestor(node)
})
) {
res.push(node)
}
})
return res
}
// 从给定的节点实例列表里判断是否存在上下级关系
export const checkHasSupSubRelation = list => {
for (let i = 0; i < list.length; i++) {
const cur = list[i]
if (
list.find(item => {
return item.uid !== cur.uid && cur.isAncestor(item)
})
) {
return true
}
}
return false
}
// 解析要添加概要的节点实例列表
export const parseAddGeneralizationNodeList = list => {
const cache = {}
const uidToParent = {}
list.forEach(node => {
const parent = node.parent
if (parent) {
const pUid = parent.uid
uidToParent[pUid] = parent
const index = node.getIndexInBrothers()
const data = {
node,
index
}
if (cache[pUid]) {
if (
!cache[pUid].find(item => {
return item.index === data.index
})
) {
cache[pUid].push(data)
}
} else {
cache[pUid] = [data]
}
}
})
const res = []
Object.keys(cache).forEach(uid => {
if (cache[uid].length > 1) {
const rangeList = cache[uid]
.map(item => {
return item.index
})
.sort((a, b) => {
return a - b
})
res.push({
node: uidToParent[uid],
range: [rangeList[0], rangeList[rangeList.length - 1]]
})
} else {
res.push({
node: cache[uid][0].node
})
}
})
return res
}
// 判断两个矩形是否重叠
export const checkTwoRectIsOverlap = (
minx1,
maxx1,
miny1,
maxy1,
minx2,
maxx2,
miny2,
maxy2
) => {
return maxx1 > minx2 && maxx2 > minx1 && maxy1 > miny2 && maxy2 > miny1
}
// 聚焦指定输入框
export const focusInput = el => {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(el)
range.collapse()
selection.removeAllRanges()
selection.addRange(range)
}
// 聚焦全选指定输入框
export const selectAllInput = el => {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(el)
selection.removeAllRanges()
selection.addRange(range)
}
// 给指定的节点列表树数据添加附加数据,会修改原数据
export const addDataToAppointNodes = (appointNodes, data = {}) => {
const walk = list => {
list.forEach(node => {
node.data = {
...node.data,
...data
}
if (node.children && node.children.length > 0) {
walk(node.children)
}
})
}
walk(appointNodes)
return appointNodes
}
// 给指定的节点列表树数据添加uid会修改原数据
// createNewId默认为false即如果节点不存在uid的话会创建新的uid。如果传true那么无论节点数据原来是否存在uid都会创建新的uid
export const createUidForAppointNodes = (appointNodes, createNewId = false) => {
const walk = list => {
list.forEach(node => {
if (!node.data) {
node.data = {}
}
if (createNewId || isUndef(node.data.uid)) {
node.data.uid = createUid()
}
if (node.children && node.children.length > 0) {
walk(node.children)
}
})
}
walk(appointNodes)
return appointNodes
}
// 传入一个数据,如果该数据是数组,那么返回该数组,否则返回一个以该数据为成员的数组
export const formatDataToArray = data => {
if (!data) return []
return Array.isArray(data) ? data : [data]
}
// 获取节点在同级里的位置索引
export const getNodeDataIndex = node => {
return node.parent
? node.parent.nodeData.children.findIndex(item => {
return item.data.uid === node.uid
})
: 0
}
// 从一个节点列表里找出某个节点的索引
export const getNodeIndexInNodeList = (node, nodeList) => {
return nodeList.findIndex(item => {
return item.uid === node.uid
})
}
// 根据内容生成颜色
export const generateColorByContent = str => {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
// 这里使用伪随机数的原因是因为
// 1. 如果字符串的内容差不多根据hash生产的颜色就比较相近不好区分比如v1.1 v1.2,所以需要加入随机数来使得颜色能够区分开
// 2. 普通的随机数每次数值不一样,就会导致每次新增标签原来的标签颜色就会发生改变,所以加入了这个方法,使得内容不变随机数也不变
const rng = new MersenneTwister(hash)
const h = rng.genrand_int32() % 360
return 'hsla(' + h + ', 50%, 50%, 1)'
}
// html转义
export const htmlEscape = str => {
;[
['&', '&amp;'],
['<', '&lt;'],
['>', '&gt;']
].forEach(item => {
str = str.replace(new RegExp(item[0], 'g'), item[1])
})
return str
}
// 判断两个对象是否相同,只处理对象或数组
export const isSameObject = (a, b) => {
const type = getType(a)
// a、b类型不一致那么肯定不相同
if (type !== getType(b)) return false
// 如果都是对象
if (type === 'Object') {
const keysa = Object.keys(a)
const keysb = Object.keys(b)
// 对象字段数量不一样,肯定不相同
if (keysa.length !== keysb.length) return false
// 字段数量一样,那么需要遍历字段进行判断
for (let i = 0; i < keysa.length; i++) {
const key = keysa[i]
// b没有a的一个字段那么肯定不相同
if (!keysb.includes(key)) return false
// 字段名称一样,那么需要递归判断它们的值
const isSame = isSameObject(a[key], b[key])
if (!isSame) {
return false
}
}
return true
} else if (type === 'Array') {
// 如果都是数组
// 数组长度不一样,肯定不相同
if (a.length !== b.length) return false
// 长度一样,那么需要遍历进行判断
for (let i = 0; i < a.length; i++) {
const itema = a[i]
const itemb = b[i]
const typea = getType(itema)
const typeb = getType(itemb)
if (typea !== typeb) return false
const isSame = isSameObject(itema, itemb)
if (!isSame) {
return false
}
}
return true
} else {
// 其他类型,直接全等判断
return a === b
}
}
// 将数据设置到用户剪切板中
export const setDataToClipboard = data => {
if (navigator.clipboard) {
navigator.clipboard.writeText(JSON.stringify(data))
}
}
// 从用户剪贴板中读取文字和图片
export const getDataFromClipboard = async () => {
let text = null
let img = null
if (navigator.clipboard) {
text = await navigator.clipboard.readText()
const items = await navigator.clipboard.read()
if (items && items.length > 0) {
for (const clipboardItem of items) {
for (const type of clipboardItem.types) {
if (/^image\//.test(type)) {
img = await clipboardItem.getType(type)
break
}
}
}
}
}
return {
text,
img
}
}
// 从节点的父节点的nodeData.children列表中移除该节点的数据
export const removeFromParentNodeData = node => {
if (!node || !node.parent) return
const index = getNodeDataIndex(node)
if (index === -1) return
node.parent.nodeData.children.splice(index, 1)
}
// 给html自闭合标签添加闭合状态
export const handleSelfCloseTags = str => {
selfCloseTagList.forEach(tagName => {
str = str.replaceAll(
new RegExp(`<${tagName}([^>]*)>`, 'g'),
`<${tagName} $1 />`
)
})
return str
}
// 检查两个节点列表是否包含的节点是一样的
export const checkNodeListIsEqual = (list1, list2) => {
if (list1.length !== list2.length) return false
for (let i = 0; i < list1.length; i++) {
if (
!list2.find(item => {
return item.uid === list1[i].uid
})
) {
return false
}
}
return true
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,65 @@
/**
* @description 为了保证相同的内容每次生成的随机数都是一样的我们可以使用一个伪随机数生成器PRNG并使用内容的哈希值作为种子。以下是一个使用Mersenne Twister算法的PRNG的实现
*
* @param {*} seed
*/
export default function MersenneTwister(seed) {
this.N = 624
this.M = 397
this.MATRIX_A = 0x9908b0df
this.UPPER_MASK = 0x80000000
this.LOWER_MASK = 0x7fffffff
this.mt = new Array(this.N)
this.mti = this.N + 1
this.init_genrand(seed)
}
MersenneTwister.prototype.init_genrand = function (s) {
this.mt[0] = s >>> 0
for (this.mti = 1; this.mti < this.N; this.mti++) {
s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30)
this.mt[this.mti] =
((((s & 0xffff0000) >>> 16) * 1812433253) << 16) +
(s & 0x0000ffff) * 1812433253 +
this.mti
this.mt[this.mti] >>>= 0
}
}
MersenneTwister.prototype.genrand_int32 = function () {
var y
var mag01 = new Array(0x0, this.MATRIX_A)
if (this.mti >= this.N) {
var kk
if (this.mti == this.N + 1) this.init_genrand(5489)
for (kk = 0; kk < this.N - this.M; kk++) {
y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK)
this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 0x1]
}
for (; kk < this.N - 1; kk++) {
y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK)
this.mt[kk] = this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 0x1]
}
y = (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK)
this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 0x1]
this.mti = 0
}
y = this.mt[this.mti++]
y ^= y >>> 11
y ^= (y << 7) & 0x9d2c5680
y ^= (y << 15) & 0xefc60000
y ^= y >>> 18
return y >>> 0
}

View File

@@ -351,4 +351,4 @@ const drawBackgroundImageToCanvas = (
}
}
export default drawBackgroundImageToCanvas
export default drawBackgroundImageToCanvas

File diff suppressed because one or more lines are too long

195
simple-mind-map/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,195 @@
export default MindMap;
declare class MindMap {
/**
*
* @param {defaultOpt} opt
*/
constructor(opt?: {
readonly: boolean;
layout: string;
fishboneDeg: number;
theme: string;
themeConfig: {};
scaleRatio: number;
mouseScaleCenterUseMousePosition: boolean;
maxTag: number;
expandBtnSize: number;
imgTextMargin: number;
textContentMargin: number;
selectTranslateStep: number;
selectTranslateLimit: number;
customNoteContentShow: any;
enableFreeDrag: boolean;
watermarkConfig: {
text: string;
lineSpacing: number;
textSpacing: number;
angle: number;
textStyle: {
color: string;
opacity: number;
fontSize: number;
};
};
textAutoWrapWidth: number;
customHandleMousewheel: any;
mousewheelAction: string;
mousewheelMoveStep: number;
mousewheelZoomActionReverse: boolean;
defaultInsertSecondLevelNodeText: string;
defaultInsertBelowSecondLevelNodeText: string;
expandBtnStyle: {
color: string;
fill: string;
fontSize: number;
strokeColor: string;
};
expandBtnIcon: {
open: string;
close: string;
};
expandBtnNumHandler: (num: any) => any;
isShowExpandNum: boolean;
enableShortcutOnlyWhenMouseInSvg: boolean;
initRootNodePosition: any;
exportPaddingX: number;
exportPaddingY: number;
nodeTextEditZIndex: number;
nodeNoteTooltipZIndex: number;
isEndNodeTextEditOnClickOuter: boolean;
maxHistoryCount: number;
alwaysShowExpandBtn: boolean;
iconList: any[];
maxNodeCacheCount: number;
defaultAssociativeLineText: string;
fitPadding: number;
enableCtrlKeyNodeSelection: boolean;
useLeftKeySelectionRightKeyDrag: boolean;
beforeTextEdit: any;
isUseCustomNodeContent: boolean;
customCreateNodeContent: any;
customInnerElsAppendTo: any;
nodeDragPlaceholderMaxSize: number;
enableAutoEnterTextEditWhenKeydown: boolean;
richTextEditFakeInPlace: boolean;
customHandleClipboardText: any;
disableMouseWheelZoom: boolean;
disableTouchZoom: boolean;
errorHandler: (code: any, error: any) => void;
resetCss: string;
enableDblclickBackToRootNode: boolean;
minExportImgCanvasScale: number;
hoverRectColor: string;
hoverRectPadding: number;
selectTextOnEnterEditText: boolean;
deleteNodeActive: boolean;
autoMoveWhenMouseInEdgeOnDrag: boolean;
fit: boolean;
dragMultiNodeRectConfig: {
width: number;
height: number;
fill: string;
};
dragPlaceholderRectFill: string;
dragOpacityConfig: {
cloneNodeOpacity: number;
beingDragNodeOpacity: number;
};
tagsColorMap: {};
cooperateStyle: {
avatarSize: number;
fontSize: number;
};
associativeLineIsAlwaysAboveNode: boolean;
defaultGeneralizationText: string;
handleIsSplitByWrapOnPasteCreateNewNode: any;
addHistoryTime: number;
isDisableDrag: boolean;
highlightNodeBoxStyle: {
stroke: string;
fill: string;
};
createNewNodeBehavior: string;
});
opt: any;
el: any;
cssEl: HTMLStyleElement;
event: Event;
keyCommand: KeyCommand;
command: Command;
renderer: Render;
view: View;
batchExecution: BatchExecution;
handleOpt(opt: any): any;
initContainer(): void;
associativeLineDraw: any;
svg: any;
draw: any;
lineDraw: any;
nodeDraw: any;
otherDraw: any;
clearDraw(): void;
addCss(): void;
removeCss(): void;
render(callback: any, source?: string): void;
reRender(callback: any, source?: string): void;
getElRectInfo(): void;
elRect: any;
width: any;
height: any;
resize(): void;
on(event: any, fn: any): void;
emit(event: any, ...args: any[]): void;
off(event: any, fn: any): void;
initCache(): void;
initTheme(): void;
themeConfig: any;
setTheme(theme: any, notRender?: boolean): void;
getTheme(): any;
setThemeConfig(config: any, notRender?: boolean): void;
getCustomThemeConfig(): any;
getThemeConfig(prop: any): any;
getConfig(prop: any): any;
updateConfig(opt?: {}): void;
getLayout(): any;
setLayout(layout: any, notRender?: boolean): void;
execCommand(...args: any[]): void;
setData(data: any): void;
setFullData(data: any): void;
getData(withConfig: any): any;
export(...args: any[]): Promise<any>;
toPos(x: any, y: any): {
x: number;
y: number;
};
setMode(mode: any): void;
getSvgData({ paddingX, paddingY, ignoreWatermark }?: {
paddingX?: number;
paddingY?: number;
ignoreWatermark?: boolean;
}): {
svg: any;
svgHTML: any;
rect: any;
origWidth: any;
origHeight: any;
scaleX: any;
scaleY: any;
};
addPlugin(plugin: any, opt: any): void;
removePlugin(plugin: any): void;
initPlugin(plugin: any): void;
destroy(): void;
}
declare namespace MindMap {
const pluginList: any[];
function usePlugin(plugin: any, opt?: {}): typeof MindMap;
function hasPlugin(plugin: any): number;
function defineTheme(name: any, config?: {}): Error;
}
import Event from "./src/core/event/Event";
import KeyCommand from "./src/core/command/KeyCommand";
import Command from "./src/core/command/Command";
import Render from "./src/core/render/Render";
import View from "./src/core/view/View";
import BatchExecution from "./src/utils/BatchExecution";

View File

@@ -0,0 +1,115 @@
export const themeList: {
name: string;
value: string;
dark: boolean;
}[];
export namespace CONSTANTS {
const CHANGE_THEME: string;
const CHANGE_LAYOUT: string;
const SET_DATA: string;
const TRANSFORM_TO_NORMAL_NODE: string;
namespace MODE {
const READONLY: string;
const EDIT: string;
}
namespace LAYOUT {
const LOGICAL_STRUCTURE: string;
const MIND_MAP: string;
const ORGANIZATION_STRUCTURE: string;
const CATALOG_ORGANIZATION: string;
const TIMELINE: string;
const TIMELINE2: string;
const FISHBONE: string;
const VERTICAL_TIMELINE: string;
}
namespace DIR {
const UP: string;
const LEFT: string;
const DOWN: string;
const RIGHT: string;
}
namespace KEY_DIR {
const LEFT_1: string;
export { LEFT_1 as LEFT };
const UP_1: string;
export { UP_1 as UP };
const RIGHT_1: string;
export { RIGHT_1 as RIGHT };
const DOWN_1: string;
export { DOWN_1 as DOWN };
}
namespace SHAPE {
const RECTANGLE: string;
const DIAMOND: string;
const PARALLELOGRAM: string;
const ROUNDED_RECTANGLE: string;
const OCTAGONAL_RECTANGLE: string;
const OUTER_TRIANGULAR_RECTANGLE: string;
const INNER_TRIANGULAR_RECTANGLE: string;
const ELLIPSE: string;
const CIRCLE: string;
}
namespace MOUSE_WHEEL_ACTION {
const ZOOM: string;
const MOVE: string;
}
namespace INIT_ROOT_NODE_POSITION {
const LEFT_2: string;
export { LEFT_2 as LEFT };
export const TOP: string;
const RIGHT_2: string;
export { RIGHT_2 as RIGHT };
export const BOTTOM: string;
export const CENTER: string;
}
namespace LAYOUT_GROW_DIR {
const LEFT_3: string;
export { LEFT_3 as LEFT };
const TOP_1: string;
export { TOP_1 as TOP };
const RIGHT_3: string;
export { RIGHT_3 as RIGHT };
const BOTTOM_1: string;
export { BOTTOM_1 as BOTTOM };
}
namespace PASTE_TYPE {
const CLIP_BOARD: string;
const CANVAS: string;
}
namespace SCROLL_BAR_DIR {
const VERTICAL: string;
const HORIZONTAL: string;
}
namespace CREATE_NEW_NODE_BEHAVIOR {
const DEFAULT: string;
const NOT_ACTIVE: string;
const ACTIVE_ONLY: string;
}
}
export const initRootNodePositionMap: {
[x: string]: number;
};
export const layoutList: {
name: string;
value: string;
}[];
export const layoutValueList: string[];
export const nodeDataNoStylePropList: string[];
export namespace commonCaches {
const measureCustomNodeContentSizeEl: any;
const measureRichtextNodeTextSizeEl: any;
}
export namespace ERROR_TYPES {
const READ_CLIPBOARD_ERROR: string;
const PARSE_PASTE_DATA_ERROR: string;
const CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR: string;
const LOAD_CLIPBOARD_IMAGE_ERROR: string;
const BEFORE_TEXT_EDIT_ERROR: string;
const EXPORT_ERROR: string;
}
export namespace a4Size {
const width: number;
const height: number;
}
export const cssContent: "\n /* 鼠标hover和激活时渲染的矩形 */\n .smm-hover-node{\n display: none;\n opacity: 0.6;\n stroke-width: 1;\n }\n\n .smm-node:not(.smm-node-dragging):hover .smm-hover-node{\n display: block;\n }\n\n .smm-node.active .smm-hover-node{\n display: block;\n opacity: 1;\n stroke-width: 2;\n }\n";
export const selfCloseTagList: string[];

View File

@@ -0,0 +1,112 @@
export namespace defaultOpt {
const readonly: boolean;
const layout: string;
const fishboneDeg: number;
const theme: string;
const themeConfig: {};
const scaleRatio: number;
const mouseScaleCenterUseMousePosition: boolean;
const maxTag: number;
const expandBtnSize: number;
const imgTextMargin: number;
const textContentMargin: number;
const selectTranslateStep: number;
const selectTranslateLimit: number;
const customNoteContentShow: any;
const enableFreeDrag: boolean;
namespace watermarkConfig {
const text: string;
const lineSpacing: number;
const textSpacing: number;
const angle: number;
namespace textStyle {
const color: string;
const opacity: number;
const fontSize: number;
}
}
const textAutoWrapWidth: number;
const customHandleMousewheel: any;
const mousewheelAction: string;
const mousewheelMoveStep: number;
const mousewheelZoomActionReverse: boolean;
const defaultInsertSecondLevelNodeText: string;
const defaultInsertBelowSecondLevelNodeText: string;
namespace expandBtnStyle {
const color_1: string;
export { color_1 as color };
export const fill: string;
const fontSize_1: number;
export { fontSize_1 as fontSize };
export const strokeColor: string;
}
namespace expandBtnIcon {
const open: string;
const close: string;
}
function expandBtnNumHandler(num: any): any;
const isShowExpandNum: boolean;
const enableShortcutOnlyWhenMouseInSvg: boolean;
const initRootNodePosition: any;
const exportPaddingX: number;
const exportPaddingY: number;
const nodeTextEditZIndex: number;
const nodeNoteTooltipZIndex: number;
const isEndNodeTextEditOnClickOuter: boolean;
const maxHistoryCount: number;
const alwaysShowExpandBtn: boolean;
const iconList: any[];
const maxNodeCacheCount: number;
const defaultAssociativeLineText: string;
const fitPadding: number;
const enableCtrlKeyNodeSelection: boolean;
const useLeftKeySelectionRightKeyDrag: boolean;
const beforeTextEdit: any;
const isUseCustomNodeContent: boolean;
const customCreateNodeContent: any;
const customInnerElsAppendTo: any;
const nodeDragPlaceholderMaxSize: number;
const enableAutoEnterTextEditWhenKeydown: boolean;
const richTextEditFakeInPlace: boolean;
const customHandleClipboardText: any;
const disableMouseWheelZoom: boolean;
const disableTouchZoom: boolean;
function errorHandler(code: any, error: any): void;
const resetCss: string;
const enableDblclickBackToRootNode: boolean;
const minExportImgCanvasScale: number;
const hoverRectColor: string;
const hoverRectPadding: number;
const selectTextOnEnterEditText: boolean;
const deleteNodeActive: boolean;
const autoMoveWhenMouseInEdgeOnDrag: boolean;
const fit: boolean;
namespace dragMultiNodeRectConfig {
export const width: number;
export const height: number;
const fill_1: string;
export { fill_1 as fill };
}
const dragPlaceholderRectFill: string;
namespace dragOpacityConfig {
const cloneNodeOpacity: number;
const beingDragNodeOpacity: number;
}
const tagsColorMap: {};
namespace cooperateStyle {
export const avatarSize: number;
const fontSize_2: number;
export { fontSize_2 as fontSize };
}
const associativeLineIsAlwaysAboveNode: boolean;
const defaultGeneralizationText: string;
const handleIsSplitByWrapOnPasteCreateNewNode: any;
const addHistoryTime: number;
const isDisableDrag: boolean;
namespace highlightNodeBoxStyle {
export const stroke: string;
const fill_2: string;
export { fill_2 as fill };
}
const createNewNodeBehavior: string;
}

View File

@@ -0,0 +1,19 @@
export default Command;
declare class Command {
constructor(opt?: {});
opt: {};
mindMap: any;
commands: {};
history: any[];
activeHistoryIndex: number;
addHistory(): void;
clearHistory(): void;
registerShortcutKeys(): void;
exec(name: any, ...args: any[]): void;
add(name: any, fn: any): void;
remove(name: any, fn: any): void;
back(step?: number): any;
forward(step?: number): any;
getCopyData(): any;
removeDataUid(data: any): any;
}

View File

@@ -0,0 +1,28 @@
export default class KeyCommand {
constructor(opt: any);
opt: any;
mindMap: any;
shortcutMap: {};
shortcutMapCache: {};
isPause: boolean;
isInSvg: boolean;
pause(): void;
recovery(): void;
save(): void;
restore(): void;
bindEvent(): void;
onKeydown(e: any): void;
unBindEvent(): void;
checkKey(e: any, key: any): boolean;
getOriginEventCodeArr(e: any): any[];
hasCombinationKey(e: any): any;
getKeyCodeArr(key: any): any[];
/**
* Enter
* Tab | Insert
* Shift + a
*/
addShortcut(key: any, fn: any): void;
removeShortcut(key: any, fn: any): void;
getShortcutFn(key: any): any[];
}

View File

@@ -0,0 +1,42 @@
export const keyMap: {
Backspace: number;
Tab: number;
Enter: number;
Shift: number;
Control: number;
Alt: number;
CapsLock: number;
Esc: number;
Spacebar: number;
PageUp: number;
PageDown: number;
End: number;
Home: number;
Insert: number;
Left: number;
Up: number;
Right: number;
Down: number;
Del: number;
NumLock: number;
Cmd: number;
CmdFF: number;
F1: number;
F2: number;
F3: number;
F4: number;
F5: number;
F6: number;
F7: number;
F8: number;
F9: number;
F10: number;
F11: number;
F12: number;
'`': number;
'=': number;
'-': number;
'/': number;
'.': number;
};
export function isKey(e: any, key: any): boolean;

View File

@@ -0,0 +1,35 @@
export default Event;
declare class Event {
constructor(opt?: {});
opt: {};
mindMap: any;
isLeftMousedown: boolean;
isRightMousedown: boolean;
isMiddleMousedown: boolean;
mousedownPos: {
x: number;
y: number;
};
mousemovePos: {
x: number;
y: number;
};
mousemoveOffset: {
x: number;
y: number;
};
bindFn(): void;
onBodyClick(e: any): void;
onDrawClick(e: any): void;
onMousedown(e: any): void;
onMousemove(e: any): void;
onMouseup(e: any): void;
onMousewheel(e: any): void;
onContextmenu(e: any): void;
onSvgMousedown(e: any): void;
onKeyup(e: any): void;
onMouseenter(e: any): void;
onMouseleave(e: any): void;
bind(): void;
unbind(): void;
}

View File

@@ -0,0 +1,109 @@
export default Render;
declare class Render {
constructor(opt?: {});
opt: {};
mindMap: any;
themeConfig: any;
renderTree: any;
reRender: boolean;
isRendering: boolean;
hasWaitRendering: boolean;
waitRenderingParams: any[];
nodeCache: {};
lastNodeCache: {};
renderSource: string;
activeNodeList: any[];
root: any;
textEdit: TextEdit;
lastBeingCopyData: any;
beingCopyData: any;
beingPasteText: string;
beingPasteImgSize: number;
currentBeingPasteType: string;
highlightBoxNode: any;
lastActiveNode: any;
lastActiveNodeList: any[];
setLayout(): void;
layout: MindMap | CatalogOrganization | OrganizationStructure | Timeline | VerticalTimeline;
setData(data: any): void;
bindEvent(): void;
registerCommands(): void;
selectAll(): void;
back(step: any): void;
forward(step: any): void;
insertNode(openEdit?: boolean, appointNodes?: any[], appointData?: any, appointChildren?: any[]): void;
insertMultiNode(appointNodes: any, nodeList: any): void;
insertChildNode(openEdit?: boolean, appointNodes?: any[], appointData?: any, appointChildren?: any[]): void;
insertMultiChildNode(appointNodes: any, childList: any): void;
insertParentNode(openEdit: boolean, appointNodes: any, appointData: any): void;
upNode(): void;
downNode(): void;
insertAfter(node: any, exist: any): void;
insertBefore(node: any, exist: any): void;
moveNodeTo(node: any, toNode: any): void;
removeNode(appointNodes?: any[]): void;
removeCurrentNode(appointNodes?: any[]): void;
pasteNode(data: any): void;
cutNode(callback: any): void;
setNodeStyle(node: any, prop: any, value: any): void;
setNodeStyles(node: any, style: any): void;
setNodeActive(node: any, active: any): void;
clearActiveNode(): void;
setNodeExpand(node: any, expand: any): void;
expandAllNode(): void;
unexpandAllNode(): void;
expandToLevel(level: any): void;
setNodeData(node: any, data: any): void;
setNodeText(node: any, text: any, richText: any, resetRichText: any): void;
setNodeImage(node: any, data: any): void;
setNodeIcon(node: any, icons: any): void;
setNodeHyperlink(node: any, link: any, title?: string): void;
setNodeNote(node: any, note: any): void;
setNodeTag(node: any, tag: any): void;
insertFormula(formula: any, appointNodes?: any[]): void;
addGeneralization(data: any): void;
removeGeneralization(): void;
setNodeCustomPosition(node: any, left?: any, top?: any): void;
resetLayout(): void;
setNodeShape(node: any, shape: any): void;
goTargetNode(node: any, callback?: () => void): void;
registerShortcutKeys(): void;
toggleActiveExpand(): void;
emitNodeActiveEvent(node?: any, activeNodeList?: any[]): void;
clearActiveNodeListOnDrawClick(e: any, eventType: any): void;
startTextEdit(): void;
endTextEdit(): void;
render(callback: () => void, source: any): void;
clearActiveNodeList(): void;
addNodeToActiveList(node: any): void;
removeNodeFromActiveList(node: any): void;
findActiveNodeIndex(node: any): any;
backForward(type: any, step: any): void;
getNewNodeBehavior(openEdit?: boolean, handleMultiNodes?: boolean): {
focusNewNode: boolean;
inserting: boolean;
};
copy(): void;
cut(): void;
paste(): void;
onPaste(): Promise<void>;
insertTo(node: any, exist: any, dir?: string): void;
checkNodeLayerChange(node: any, toNode: any): void;
deleteNodeGeneralization(node: any): void;
getNextActiveNode(): any;
copyNode(): any;
toggleNodeExpand(node: any): void;
setNodeDataRender(node: any, data: any, notRender?: boolean): void;
moveNodeToCenter(node: any): void;
setRootNodeCenter(): void;
expandToNodeUid(uid: any, callback?: () => void): void;
findNodeByUid(uid: any): any;
highlightNode(node: any, range: any): void;
closeHighlightNode(): void;
}
import TextEdit from "./TextEdit";
import MindMap from "../../layouts/MindMap";
import CatalogOrganization from "../../layouts/CatalogOrganization";
import OrganizationStructure from "../../layouts/OrganizationStructure";
import Timeline from "../../layouts/Timeline";
import VerticalTimeline from "../../layouts/VerticalTimeline";

View File

@@ -0,0 +1,30 @@
export default class TextEdit {
constructor(renderer: any);
renderer: any;
mindMap: any;
currentNode: any;
textEditNode: HTMLDivElement;
showTextEdit: boolean;
cacheEditingText: string;
bindEvent(): void;
show({ node, isInserting, isFromKeyDown, isFromScale }: {
node: any;
isInserting?: boolean;
isFromKeyDown?: boolean;
isFromScale?: boolean;
}): Promise<void>;
onScale(): void;
onKeydown(e: any): void;
unBindEvent(): void;
checkIsAutoEnterTextEditKey(e: any): boolean;
registerTmpShortcut(): void;
showEditTextBox({ node, rect, isInserting, isFromKeyDown, isFromScale }: {
node: any;
rect: any;
isInserting: any;
isFromKeyDown: any;
isFromScale: any;
}): void;
getEditText(): any;
hideEditTextBox(): any;
}

View File

@@ -0,0 +1,136 @@
export default Node;
declare class Node {
constructor(opt?: {});
nodeData: any;
uid: any;
mindMap: any;
renderer: any;
draw: any;
nodeDraw: any;
lineDraw: any;
style: Style;
shapeInstance: Shape;
shapePadding: {
paddingX: number;
paddingY: number;
};
isRoot: any;
isGeneralization: any;
generalizationBelongNode: any;
layerIndex: any;
width: any;
height: any;
_left: any;
_top: any;
customLeft: any;
customTop: any;
isDrag: boolean;
parent: any;
children: any;
userList: any[];
group: any;
shapeNode: any;
hoverNode: any;
_customNodeContent: any;
_imgData: any;
_iconData: any;
_textData: any;
_hyperlinkData: any;
_tagData: any;
_noteData: any;
noteEl: any;
noteContentIsShow: boolean;
_expandBtn: any;
_lastExpandBtnType: any;
_showExpandBtn: boolean;
_openExpandNode: any;
_closeExpandNode: any;
_fillExpandNode: any;
_userListGroup: any;
_lines: any[];
_generalizationList: any[];
_unVisibleRectRegionNode: any;
_isMouseenter: boolean;
_rectInfo: {
imgContentWidth: number;
imgContentHeight: number;
textContentWidth: number;
textContentHeight: number;
};
_generalizationNodeWidth: number;
_generalizationNodeHeight: number;
textContentItemMargin: any;
blockContentMargin: any;
expandBtnSize: any;
isMultipleChoice: boolean;
needLayout: boolean;
isHide: boolean;
set left(arg: any);
get left(): any;
set top(arg: any);
get top(): any;
reset(): void;
handleData(data: any): any;
createNodeData(): void;
getSize(): boolean;
getNodeRect(): {
width: any;
height: any;
};
layout(): void;
bindGroupEvent(): void;
active(e: any): void;
update(): void;
getNodePosInClient(_left: any, _top: any): {
left: any;
top: any;
};
reRender(): boolean;
updateNodeActiveClass(): void;
updateNodeByActive(active: any): void;
render(callback?: () => void): void;
remove(): void;
destroy(): void;
hide(): void;
show(): void;
setOpacity(val: any): void;
hideChildren(): void;
showChildren(): void;
startDrag(): void;
endDrag(): void;
renderLine(deep?: boolean): void;
getShape(): any;
hasCustomPosition(): boolean;
ancestorHasCustomPosition(): boolean;
ancestorHasGeneralization(): boolean;
addChildren(node: any): void;
styleLine(line: any, node: any): void;
removeLine(): void;
isAncestor(node: any): boolean;
isParent(node: any): boolean;
isBrother(node: any): any;
getIndexInBrothers(): any;
getPaddingVale(): {
paddingX: any;
paddingY: any;
};
getStyle(prop: any, root: any): any;
getSelfStyle(prop: any): any;
getParentSelfStyle(prop: any): any;
getSelfInhertStyle(prop: any): any;
getBorderWidth(): any;
getData(key: any): any;
getPureData(removeActiveState?: boolean, removeId?: boolean): any;
hasCustomStyle(): boolean;
getRect(): any;
getRectInSvg(): {
left: any;
right: any;
top: any;
bottom: any;
width: number;
height: number;
};
}
import Style from "./Style";
import Shape from "./Shape";

View File

@@ -0,0 +1,23 @@
export default class Shape {
constructor(node: any);
node: any;
getShapePadding(width: any, height: any, paddingX: any, paddingY: any): {
paddingX: number;
paddingY: number;
};
createShape(): any;
getNodeSize(): {
width: any;
height: any;
};
createRect(): any;
createDiamond(): any;
createParallelogram(): any;
createRoundedRectangle(): any;
createOctagonalRectangle(): any;
createOuterTriangularRectangle(): any;
createInnerTriangularRectangle(): any;
createEllipse(): any;
createCircle(): any;
}
export const shapeList: string[];

View File

@@ -0,0 +1,36 @@
export default Style;
declare class Style {
static setBackgroundStyle(el: any, themeConfig: any): void;
static removeBackgroundStyle(el: any): void;
constructor(ctx: any);
ctx: any;
merge(prop: any, root: any): any;
getStyle(prop: any, root: any): any;
getSelfStyle(prop: any): any;
rect(node: any): void;
shape(node: any): void;
text(node: any): void;
createStyleText(): string;
getTextFontStyle(): {
italic: boolean;
bold: any;
fontSize: any;
fontFamily: any;
};
domText(node: any, fontSizeScale: number, isMultiLine: any): void;
tagText(node: any): void;
tagRect(node: any, text: any, color: any): void;
iconNode(node: any): void;
line(node: any, { width, color, dasharray }?: {
width: any;
color: any;
dasharray: any;
}): void;
generalizationLine(node: any): void;
iconBtn(node: any, node2: any, fillNode: any): void;
hasCustomStyle(): boolean;
hoverNode(node: any): void;
}
declare namespace Style {
const cacheStyle: any;
}

View File

@@ -0,0 +1,23 @@
declare namespace _default {
export { setData };
export { setText };
export { setImage };
export { setIcon };
export { setHyperlink };
export { setNote };
export { setTag };
export { setShape };
export { setStyle };
export { setStyles };
}
export default _default;
declare function setData(data?: {}): void;
declare function setText(text: any, richText: any, resetRichText: any): void;
declare function setImage(imgData: any): void;
declare function setIcon(icons: any): void;
declare function setHyperlink(link: any, title: any): void;
declare function setNote(note: any): void;
declare function setTag(tag: any): void;
declare function setShape(shape: any): void;
declare function setStyle(prop: any, value: any): void;
declare function setStyles(style: any): void;

View File

@@ -0,0 +1,18 @@
declare namespace _default {
export { createUserListNode };
export { updateUserListNode };
export { createTextAvatar };
export { createImageAvatar };
export { addUser };
export { removeUser };
}
export default _default;
declare function createUserListNode(): void;
declare class createUserListNode {
_userListGroup: any;
}
declare function updateUserListNode(): void;
declare function createTextAvatar(item: any): any;
declare function createImageAvatar(item: any): any;
declare function addUser(userInfo: any): void;
declare function removeUser(userInfo: any): void;

View File

@@ -0,0 +1,50 @@
declare namespace _default {
export { createImgNode };
export { getImgShowSize };
export { createIconNode };
export { createRichTextNode };
export { createTextNode };
export { createHyperlinkNode };
export { createTagNode };
export { createNoteNode };
export { getNoteContentPosition };
export { measureCustomNodeContentSize };
export { isUseCustomNodeContent };
}
export default _default;
declare function createImgNode(): {
node: any;
width: any;
height: any;
};
declare function getImgShowSize(): any;
declare function createIconNode(): any;
declare function createRichTextNode(): {
node: any;
width: any;
height: any;
};
declare function createTextNode(): any;
declare function createHyperlinkNode(): {
node: any;
width: any;
height: any;
};
declare function createTagNode(): any[];
declare function createNoteNode(): {
node: any;
width: any;
height: any;
};
declare class createNoteNode {
noteEl: HTMLDivElement;
}
declare function getNoteContentPosition(): {
left: any;
top: any;
};
declare function measureCustomNodeContentSize(content: any): {
width: any;
height: any;
};
declare function isUseCustomNodeContent(): boolean;

View File

@@ -0,0 +1,34 @@
declare namespace _default {
export { createExpandNodeContent };
export { updateExpandBtnNode };
export { updateExpandBtnPos };
export { renderExpandBtn };
export { removeExpandBtn };
export { showExpandBtn };
export { hideExpandBtn };
export { sumNode };
}
export default _default;
declare function createExpandNodeContent(): void;
declare class createExpandNodeContent {
_openExpandNode: any;
_closeExpandNode: any;
_fillExpandNode: any;
}
declare function updateExpandBtnNode(): void;
declare class updateExpandBtnNode {
_lastExpandBtnType: boolean;
}
declare function updateExpandBtnPos(): void;
declare function renderExpandBtn(): void;
declare class renderExpandBtn {
_expandBtn: any;
_showExpandBtn: boolean;
}
declare function removeExpandBtn(): void;
declare class removeExpandBtn {
_showExpandBtn: boolean;
}
declare function showExpandBtn(): void;
declare function hideExpandBtn(): void;
declare function sumNode(data?: any[]): any;

View File

@@ -0,0 +1,18 @@
declare namespace _default {
export { renderExpandBtnPlaceholderRect };
export { clearExpandBtnPlaceholderRect };
export { updateExpandBtnPlaceholderRect };
}
export default _default;
declare function renderExpandBtnPlaceholderRect(): void;
declare class renderExpandBtnPlaceholderRect {
_unVisibleRectRegionNode: any;
}
declare function clearExpandBtnPlaceholderRect(): void;
declare class clearExpandBtnPlaceholderRect {
_unVisibleRectRegionNode: any;
}
declare function updateExpandBtnPlaceholderRect(): void;
declare class updateExpandBtnPlaceholderRect {
needRerenderExpandBtnPlaceholderRect: boolean;
}

View File

@@ -0,0 +1,38 @@
declare namespace _default {
export { formatGetGeneralization };
export { checkHasGeneralization };
export { checkHasSelfGeneralization };
export { getGeneralizationNodeIndex };
export { createGeneralizationNode };
export { updateGeneralization };
export { updateGeneralizationData };
export { renderGeneralization };
export { removeGeneralization };
export { hideGeneralization };
export { showGeneralization };
export { setGeneralizationOpacity };
export { handleGeneralizationMouseenter };
export { handleGeneralizationMouseleave };
}
export default _default;
declare function formatGetGeneralization(): any[];
declare function checkHasGeneralization(): boolean;
declare function checkHasSelfGeneralization(): boolean;
declare function getGeneralizationNodeIndex(node: any): any;
declare function createGeneralizationNode(): void;
declare class createGeneralizationNode {
_generalizationNodeWidth: number;
_generalizationNodeHeight: number;
}
declare function updateGeneralization(): void;
declare function updateGeneralizationData(): void;
declare function renderGeneralization(): void;
declare function removeGeneralization(): void;
declare class removeGeneralization {
_generalizationList: any[];
}
declare function hideGeneralization(): void;
declare function showGeneralization(): void;
declare function setGeneralizationOpacity(val: any): void;
declare function handleGeneralizationMouseenter(): void;
declare function handleGeneralizationMouseleave(): void;

View File

@@ -0,0 +1,36 @@
export default View;
declare class View {
constructor(opt?: {});
opt: {};
mindMap: any;
scale: number;
sx: number;
sy: number;
x: number;
y: number;
firstDrag: boolean;
bind(): void;
getTransformData(): {
transform: any;
state: {
scale: number;
x: number;
y: number;
sx: number;
sy: number;
};
};
setTransformData(viewData: any): void;
translateXY(x: any, y: any): void;
translateX(step: any): void;
translateXTo(x: any): void;
translateY(step: any): void;
translateYTo(y: any): void;
transform(): void;
reset(): void;
narrow(cx: any, cy: any, isTouchPad: any): void;
enlarge(cx: any, cy: any, isTouchPad: any): void;
scaleInCenter(scale: any, cx: any, cy: any): void;
setScale(scale: any, cx: any, cy: any): void;
fit(): void;
}

View File

@@ -0,0 +1,60 @@
export default Base;
declare class Base {
constructor(renderer: any);
renderer: any;
mindMap: any;
draw: any;
lineDraw: any;
root: any;
lru: Lru;
doLayout(): void;
renderLine(): void;
renderExpandBtn(): void;
renderGeneralization(): void;
cacheNode(uid: any, node: any): void;
checkIsNeedResizeSources(): boolean;
checkIsLayerTypeChange(oldIndex: any, newIndex: any): boolean;
checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node: any): void;
createNode(data: any, parent: any, isRoot: any, layerIndex: any): any;
formatPosition(value: any, size: any, nodeSize: any): number;
setNodeCenter(node: any): void;
updateChildren(children: any, prop: any, offset: any): void;
updateChildrenPro(children: any, props: any): void;
getNodeAreaWidth(node: any, withGeneralization?: boolean): number;
quadraticCurvePath(x1: any, y1: any, x2: any, y2: any): string;
cubicBezierPath(x1: any, y1: any, x2: any, y2: any): string;
getMarginX(layerIndex: any): any;
getMarginY(layerIndex: any): any;
getNodeWidthWithGeneralization(node: any): number;
getNodeHeightWithGeneralization(node: any): number;
/**
* dir生长方向h水平、v垂直
* isLeft是否向左生长
*/
getNodeBoundaries(node: any, dir: any): {
left: any;
right: any;
top: any;
bottom: any;
generalizationLineMargin: any;
generalizationNodeMargin: any;
};
getChildrenBoundaries(node: any, dir: any, startIndex: number, endIndex: any): {
left: number;
right: number;
top: number;
bottom: number;
generalizationLineMargin: any;
generalizationNodeMargin: any;
};
getNodeGeneralizationRenderBoundaries(item: any, dir: any): {
left: any;
right: any;
top: any;
bottom: any;
generalizationLineMargin: any;
generalizationNodeMargin: any;
};
getNodeActChildrenLength(node: any): any;
}
import Lru from "../utils/Lru";

View File

@@ -0,0 +1,11 @@
export default CatalogOrganization;
declare class CatalogOrganization extends Base {
constructor(opt?: {});
computedBaseValue(): void;
computedLeftTopValue(): void;
adjustLeftTopValue(): void;
updateBrothersLeft(node: any, addWidth: any): void;
updateBrothersTop(node: any, addHeight: any): void;
renderExpandBtnRect(rect: any, expandBtnSize: any, width: any, height: any, node: any): void;
}
import Base from "./Base";

View File

@@ -0,0 +1,15 @@
export default Fishbone;
declare class Fishbone extends Base {
constructor(opt?: {});
indent: number;
childIndent: number;
computedBaseValue(): void;
computedLeftTopValue(): void;
adjustLeftTopValue(): void;
getNodeAreaHeight(node: any): number;
updateBrothersLeft(node: any): void;
updateBrothersTop(node: any, addHeight: any): void;
checkIsTop(node: any): boolean;
renderExpandBtnRect(rect: any, expandBtnSize: any, width: any, height: any, node: any): void;
}
import Base from "./Base";

View File

@@ -0,0 +1,13 @@
export default LogicalStructure;
declare class LogicalStructure extends Base {
constructor(opt?: {});
computedBaseValue(): void;
computedTopValue(): void;
adjustTopValue(): void;
updateBrothers(node: any, addHeight: any): void;
renderLineStraight(node: any, lines: any, style: any): any[];
renderLineDirect(node: any, lines: any, style: any): any[];
renderLineCurve(node: any, lines: any, style: any): any[];
renderExpandBtnRect(rect: any, expandBtnSize: any, width: any, height: any, node: any): void;
}
import Base from "./Base";

View File

@@ -0,0 +1,13 @@
export default MindMap;
declare class MindMap extends Base {
constructor(opt?: {});
computedBaseValue(): void;
computedTopValue(): void;
adjustTopValue(): void;
updateBrothers(node: any, leftAddHeight: any, rightAddHeight: any): void;
renderLineStraight(node: any, lines: any, style: any): any[];
renderLineDirect(node: any, lines: any, style: any): any[];
renderLineCurve(node: any, lines: any, style: any): any[];
renderExpandBtnRect(rect: any, expandBtnSize: any, width: any, height: any, node: any): void;
}
import Base from "./Base";

View File

@@ -0,0 +1,12 @@
export default OrganizationStructure;
declare class OrganizationStructure extends Base {
constructor(opt?: {});
computedBaseValue(): void;
computedLeftValue(): void;
adjustLeftValue(): void;
updateBrothers(node: any, addWidth: any): void;
renderLineDirect(node: any, lines: any, style: any): any[];
renderLineStraight(node: any, lines: any, style: any): any[];
renderExpandBtnRect(rect: any, expandBtnSize: any, width: any, height: any, node: any): void;
}
import Base from "./Base";

View File

@@ -0,0 +1,13 @@
export default Timeline;
declare class Timeline extends Base {
constructor(opt: {}, layout: any);
layout: any;
computedBaseValue(): void;
computedLeftTopValue(): void;
adjustLeftTopValue(): void;
getNodeAreaHeight(node: any): number;
updateBrothersLeft(node: any): void;
updateBrothersTop(node: any, addHeight: any): void;
renderExpandBtnRect(rect: any, expandBtnSize: any, width: any, height: any, node: any): void;
}
import Base from "./Base";

View File

@@ -0,0 +1,15 @@
export default VerticalTimeline;
declare class VerticalTimeline extends Base {
constructor(opt: {}, layout: any);
layout: any;
computedBaseValue(): void;
computedTopValue(): void;
adjustLeftTopValue(): void;
updateBrothers(node: any, addHeight: any): void;
updateBrothersTop(node: any, addHeight: any): void;
renderLineStraight(node: any, lines: any, style: any): any[];
renderLineDirect(node: any, lines: any, style: any): any[];
renderLineCurve(node: any, lines: any, style: any): any[];
renderExpandBtnRect(rect: any, expandBtnSize: any, width: any, height: any, node: any): void;
}
import Base from "./Base";

View File

@@ -0,0 +1,147 @@
declare namespace _default {
namespace top {
function renderExpandBtn({ node, btn, expandBtnSize, translateX, translateY, width, height }: {
node: any;
btn: any;
expandBtnSize: any;
translateX: any;
translateY: any;
width: any;
height: any;
}): void;
function renderExpandBtn({ node, btn, expandBtnSize, translateX, translateY, width, height }: {
node: any;
btn: any;
expandBtnSize: any;
translateX: any;
translateY: any;
width: any;
height: any;
}): void;
function renderLine({ node, line, top, x, lineLength, height, expandBtnSize, maxy, ctx }: {
node: any;
line: any;
top: any;
x: any;
lineLength: any;
height: any;
expandBtnSize: any;
maxy: any;
ctx: any;
}): void;
function renderLine({ node, line, top, x, lineLength, height, expandBtnSize, maxy, ctx }: {
node: any;
line: any;
top: any;
x: any;
lineLength: any;
height: any;
expandBtnSize: any;
maxy: any;
ctx: any;
}): void;
function computedLeftTopValue({ layerIndex, node, ctx }: {
layerIndex: any;
node: any;
ctx: any;
}): void;
function computedLeftTopValue({ layerIndex, node, ctx }: {
layerIndex: any;
node: any;
ctx: any;
}): void;
function adjustLeftTopValueBefore({ node, parent, ctx, layerIndex }: {
node: any;
parent: any;
ctx: any;
layerIndex: any;
}): void;
function adjustLeftTopValueBefore({ node, parent, ctx, layerIndex }: {
node: any;
parent: any;
ctx: any;
layerIndex: any;
}): void;
function adjustLeftTopValueAfter({ parent, node, ctx }: {
parent: any;
node: any;
ctx: any;
}): void;
function adjustLeftTopValueAfter({ parent, node, ctx }: {
parent: any;
node: any;
ctx: any;
}): void;
}
namespace bottom {
function renderExpandBtn({ node, btn, expandBtnSize, translateX, translateY, width, height }: {
node: any;
btn: any;
expandBtnSize: any;
translateX: any;
translateY: any;
width: any;
height: any;
}): void;
function renderExpandBtn({ node, btn, expandBtnSize, translateX, translateY, width, height }: {
node: any;
btn: any;
expandBtnSize: any;
translateX: any;
translateY: any;
width: any;
height: any;
}): void;
function renderLine({ node, line, top, x, lineLength, height, miny, ctx }: {
node: any;
line: any;
top: any;
x: any;
lineLength: any;
height: any;
miny: any;
ctx: any;
}): void;
function renderLine({ node, line, top, x, lineLength, height, miny, ctx }: {
node: any;
line: any;
top: any;
x: any;
lineLength: any;
height: any;
miny: any;
ctx: any;
}): void;
function computedLeftTopValue({ layerIndex, node, ctx }: {
layerIndex: any;
node: any;
ctx: any;
}): void;
function computedLeftTopValue({ layerIndex, node, ctx }: {
layerIndex: any;
node: any;
ctx: any;
}): void;
function adjustLeftTopValueBefore({ node, ctx, layerIndex }: {
node: any;
ctx: any;
layerIndex: any;
}): void;
function adjustLeftTopValueBefore({ node, ctx, layerIndex }: {
node: any;
ctx: any;
layerIndex: any;
}): void;
function adjustLeftTopValueAfter({ parent, node, ctx }: {
parent: any;
node: any;
ctx: any;
}): void;
function adjustLeftTopValueAfter({ parent, node, ctx }: {
parent: any;
node: any;
ctx: any;
}): void;
}
}
export default _default;

11
simple-mind-map/types/src/svg/btns.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
declare namespace _default {
export { open };
export { close };
export { remove };
export { imgAdjust };
}
export default _default;
declare const open: "<svg t=\"1618141562310\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"13476\" width=\"200\" height=\"200\"><path d=\"M475.136 327.168v147.968h-147.968v74.24h147.968v147.968h74.24v-147.968h147.968v-74.24h-147.968v-147.968h-74.24z m36.864-222.208c225.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=\"13477\"></path></svg>";
declare 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>";
declare const remove: "<svg width=\"14px\" height=\"14px\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"13611\" width=\"200\" height=\"200\"><path fill=\"#ffffff\" d=\"M512 105.472c225.28 0 407.04 181.76 407.04 407.04s-181.76 407.04-407.04 407.04-407.04-181.76-407.04-407.04 181.76-407.04 407.04-407.04z m0-74.24c-265.216 0-480.768 215.552-480.768 480.768s215.552 480.768 480.768 480.768 480.768-215.552 480.768-480.768-215.552-480.768-480.768-480.768z\" p-id=\"13612\"></path><path fill=\"#ffffff\" d=\"M252.928 474.624h518.144v74.24h-518.144z\" p-id=\"13613\"></path></svg>";
declare 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>";

View File

@@ -0,0 +1,18 @@
export const nodeIconList: {
name: string;
type: string;
list: {
name: string;
icon: string;
}[];
}[];
declare namespace _default {
export { hyperlink };
export { note };
export { nodeIconList };
export { getNodeIconListIcon };
}
export default _default;
declare const hyperlink: "<svg t=\"1624174958075\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"7982\" ><path d=\"M435.484444 251.733333v68.892445L295.822222 320.682667a168.504889 168.504889 0 0 0-2.844444 336.952889h142.506666v68.892444H295.822222a237.397333 237.397333 0 0 1 0-474.794667h139.662222z m248.945778 0a237.397333 237.397333 0 0 1 0 474.851556H544.654222v-69.006222l139.776 0.056889a168.504889 168.504889 0 0 0 2.844445-336.952889H544.597333V251.676444h139.776z m-25.827555 203.946667a34.474667 34.474667 0 0 1 0 68.892444H321.649778a34.474667 34.474667 0 0 1 0-68.892444h336.952889z\" p-id=\"7983\"></path></svg>";
declare const note: "<svg t=\"1624195132675\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"8792\" ><path d=\"M152.768 985.984 152.768 49.856l434.56 0 66.816 0 234.048 267.392 0 66.816 0 601.92L152.768 985.984 152.768 985.984zM654.144 193.088l0 124.16 108.736 0L654.144 193.088 654.144 193.088zM821.312 384.064l-167.168 0L587.328 384.064 587.328 317.312 587.328 116.736 219.584 116.736 219.584 919.04l601.728 0L821.312 384.064 821.312 384.064zM386.688 517.888 319.808 517.888 319.808 450.944l66.816 0L386.624 517.888 386.688 517.888zM386.688 651.584 319.808 651.584 319.808 584.704l66.816 0L386.624 651.584 386.688 651.584zM386.688 785.344 319.808 785.344l0-66.88 66.816 0L386.624 785.344 386.688 785.344zM721.024 517.888 453.632 517.888 453.632 450.944l267.392 0L721.024 517.888 721.024 517.888zM654.144 651.584 453.632 651.584 453.632 584.704l200.512 0L654.144 651.584 654.144 651.584zM620.672 785.344l-167.04 0 0-66.88 167.04 0L620.672 785.344 620.672 785.344z\" p-id=\"8793\"></path></svg>";
declare function getNodeIconListIcon(name: any, extendIconList?: any[]): any;

View File

@@ -0,0 +1,2 @@
declare const _default: any
export default _default

View File

@@ -0,0 +1,2 @@
declare const _default: any
export default _default

View File

@@ -0,0 +1,2 @@
declare const _default: any
export default _default

View File

@@ -0,0 +1,2 @@
declare const _default: any
export default _default

View File

@@ -0,0 +1,2 @@
declare const _default: any
export default _default

View File

@@ -0,0 +1,2 @@
declare const _default: any
export default _default

View File

@@ -0,0 +1,2 @@
declare const _default: any
export default _default

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