Compare commits

...

215 Commits

Author SHA1 Message Date
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
wanglin2
e9de1e675f 打包0.7.1 2023-09-15 09:19:52 +08:00
wanglin2
b3e254c0ee Doc: update 2023-09-15 09:13:41 +08:00
wanglin2
bd3d470e40 Fix:修复画布左上角距浏览器窗口左上角不为0时拖拽节点画布自动移动的问题 2023-09-14 19:02:06 +08:00
wanglin2
21564445d6 Feat:拖拽节点时鼠标移到画布边缘时画布自动移动 2023-09-14 18:24:10 +08:00
街角小林
e53d1b1948 Merge pull request #331 from esky-wh/feature
Feature
2023-09-14 15:46:55 +08:00
esky-wh
c428f166a6 Feat: 1.添加开启删除节点后激活节点配置2.增加功能删除节点后激活相邻节点 2023-09-14 14:11:13 +08:00
esky-wh
f19704ec41 Merge pull request #1 from wanglin2/feature
Feature
2023-09-14 10:59:57 +08:00
wanglin2
d47f013f84 Demo:编辑页面改为异步路由 2023-09-14 09:17:30 +08:00
wanglin2
612d675a19 Fix:修复注册插件的方法没有去重的问题 2023-09-14 09:16:50 +08:00
wanglin2
13f571e0b5 Fix:重构节点拖拽逻辑,修复节点拖拽没有适配各种结构的问题 2023-09-13 16:48:41 +08:00
wanglin2
b09d408ab3 Fix:修复没有注册select插件时节点右键事件报错的问题 2023-09-11 20:07:14 +08:00
wanglin2
ea95ae2b5d Feat:重构滚动条,优化使用体验 2023-09-11 19:57:20 +08:00
wanglin2
2590e21807 Fix:不完美修复组织结构图概要和节点冲突的问题 2023-09-10 09:05:13 +08:00
wanglin2
fb41653d79 Fix:不完美修复目录组织图概要和节点冲突的问题 2023-09-09 15:42:21 +08:00
wanglin2
4c9c34a0ea Fix:不完美的解决思维导图结构概要和节点的冲突问题 2023-09-09 14:43:02 +08:00
wanglin2
0d5602b832 Fix:不完美的解决逻辑结构图概要和节点的冲突问题 2023-09-09 11:35:13 +08:00
wanglin2
af87f84ce8 Doc: update 2023-09-08 23:18:07 +08:00
wanglin2
a96c16aa45 Fix:修复导出带有贴纸的数据为xmind格式时贴纸无法显示的问题 2023-09-08 23:04:30 +08:00
wanglin2
e29e1c26e5 Fix:修复导出的xmind文件在最新版xmind软件上打开时提示已损坏的问题 2023-09-08 22:58:18 +08:00
wanglin2
fee38cbe8a Doc: update 2023-09-08 21:47:01 +08:00
wanglin2
58ca173234 Fix:修复导入存在为标题为空的节点的xmind文件报错的问题 2023-09-08 21:25:39 +08:00
wanglin2
aee10c6810 Feat:节点数据data中以_开头的字段被认为是自定义字段 2023-09-08 17:54:15 +08:00
wanglin2
fe43be3451 Fix:修复画布左上角距浏览器窗口不为0时多选节点鼠标移动到边缘时画布滚动异常的问题 2023-09-08 17:18:01 +08:00
wanglin2
21868c7f44 Feat:优化一些情况下的节点拖拽 2023-09-08 09:21:32 +08:00
wanglin2
5585d2a4f7 Feat:节点比对全部改为通过uid对比 2023-09-07 18:45:02 +08:00
wanglin2
1d04038609 Fix:修复关联线端点改变后刷新页面未保存的问题 2023-09-07 18:21:12 +08:00
wanglin2
305c5e9f70 Doc: update 2023-09-07 17:36:36 +08:00
wanglin2
4ac91003bb Demo:页面增加显示当前核心库版本号 2023-09-07 17:17:32 +08:00
wanglin2
44410900aa Doc: update 2023-09-06 09:18:29 +08:00
wanglin2
239b369391 打包 2023-09-04 22:04:14 +08:00
wanglin2
d9638fef98 Demo: update 2023-09-04 21:49:03 +08:00
wanglin2
ecb9482b82 Demo:新增主题,更新文档 2023-09-04 21:47:55 +08:00
wanglin2
cd5556aad5 合并 2023-09-04 17:00:39 +08:00
wanglin2
063af62cf6 打包0.7.0 2023-09-04 16:58:57 +08:00
wanglin2
a4611d11a8 Doc: update 2023-09-04 16:49:24 +08:00
wanglin2
5d1f460a94 Feat:鱼骨图支持设置节点margin 2023-09-02 20:58:20 +08:00
wanglin2
075e578bb4 Feat:支持在url中通过fileURL查询参数打开指定的在线文件 2023-09-02 09:53:05 +08:00
wanglin2
32c17921ca Demo:支持配置是否显示滚动条 2023-09-02 08:31:08 +08:00
wanglin2
7c470c9b88 Doc: update 2023-09-01 16:44:02 +08:00
wanglin2
e472b840f1 update README 2023-09-01 16:41:24 +08:00
wanglin2
726460c812 Fix:修复全选没有触发node_active事件的问题 2023-09-01 16:36:25 +08:00
wanglin2
7ccf796c34 Feat:新增滚动条插件 2023-09-01 16:33:07 +08:00
wanglin2
c183306ab2 update 2023-09-01 10:43:48 +08:00
wanglin2
29bb27aa23 Feat:优化鼠标按下节点事件逻辑,在右键拖拽画布模式下支持右键按住根节点拖拽画布 2023-09-01 10:41:36 +08:00
wanglin2
7f9d03c8af update 2023-09-01 10:17:39 +08:00
wanglin2
c3a0e09f6d Feat:支持双击节点进入编辑状态时全选文本的配置项 2023-09-01 10:16:21 +08:00
wanglin2
1d497b2f13 Feat:默认关闭双击复位画布 2023-09-01 09:53:45 +08:00
wanglin2
f43a2ebdef Fix:修复切换主题时存在关联线的节点样式不会更改的问题 2023-09-01 09:50:33 +08:00
wanglin2
4c3f3cb1ab Feat:node_active事件抛出的激活节点列表不再直接引用内部激活列表 2023-09-01 09:45:17 +08:00
wanglin2
a404a71ba2 Demo:不直接引用内部激活节点列表,优化性能 2023-09-01 09:36:39 +08:00
wanglin2
6b4c118a2b Fix:优化Select插件,如果多选节点没有变化,那么不触发激活激活事件 2023-09-01 09:34:22 +08:00
wanglin2
1157257911 Fix:1.优化展开收起代码逻辑;2.修复关联线不兼容老版的问题,修复端点偏移量未保存的问题 2023-08-31 19:43:59 +08:00
街角小林
f2642c9d63 Merge pull request #294 from eSeaSky-WuHan/release
Feat:
1、折叠节点添加配置项;
2、关联线效果优化。
2023-08-31 15:43:13 +08:00
eseasky
91bc0351b8 Feat: 关联线拖动效果优化 2023-08-30 20:11:44 +08:00
eseasky
df2dc96ba5 Feat: 1.配置开启收起显示节点数量;2.支持配置传入一个函数,允许控制收起时显示的内容;3.支持配置expandBtnSize展开收起按钮的尺寸;4.收起时的样式可以使用expandBtnStyle配置颜色,字体,边框颜色。 2023-08-30 20:09:16 +08:00
eseasky
3d86650f22 Feat: 实现关联线端点位置随鼠标拖拽变化 2023-08-29 20:09:57 +08:00
eseasky
a31e93bacf Feat: 收起节点时,显示折叠的节点数量 2023-08-29 19:46:22 +08:00
wanglin2
a3362e44fe Feat:右键多选节点结束时禁止触发节点右键菜单事件,避免触发右键菜单显示 2023-08-29 09:46:36 +08:00
wanglin2
f7f234b4cb Feat:优化基础样式的设置,修改不影响大小的主题属性时不触发全量渲染 2023-08-28 20:23:23 +08:00
wanglin2
48f1de5c25 Fix:修复节点hover和激活节点时渲染的矩形会重叠的问题 2023-08-28 15:40:50 +08:00
wanglin2
c533459da1 Fix:修复自定义节点内容时hover和激活效果不显示的问题 2023-08-28 13:54:37 +08:00
wanglin2
d99895b845 Fix:修改节点hover和激活矩形逻辑,修复连线和节点不重合的问题 2023-08-28 13:47:58 +08:00
wanglin2
c65393d1bc Fix:修复导出图片和svg时节点hover矩形默认会显示的问题 2023-08-28 09:52:46 +08:00
wanglin2
5d133f74cf Feat:删除设置节点激活样式的逻辑 2023-08-28 09:41:08 +08:00
wanglin2
5997c98b8f Demo:删除侧边栏节点样式配置部分的激活节点配置 2023-08-28 09:33:29 +08:00
wanglin2
217d66f692 Feat:删除主题文件中节点激活样式的部分 2023-08-28 09:21:18 +08:00
wanglin2
d14d887c1a Feat:优化鼠标hover和激活节点矩形,增加和内容的距离 2023-08-28 09:11:39 +08:00
wanglin2
e36e238c2f Fix:修复大边框尺寸的渲染问题,包括重叠,内容不居中 2023-08-28 08:49:45 +08:00
wanglin2
1b0646af6d Feat:节点矩形形状改为使用path渲染 2023-08-27 22:51:18 +08:00
wanglin2
a24f7a73c8 Feat:节点增加鼠标滑过效果、节点激活效果重构、去除激活样式改变功能 2023-08-27 22:10:49 +08:00
wanglin2
b35dd282ec Feat:插件新增销毁前生命周期函数,解决销毁思维导图时插件的一些副作用没有清除的问题 2023-08-27 10:34:15 +08:00
wanglin2
8c0c2c5bc4 Feat:提升导出的图片和pdf在高清屏的清晰度 2023-08-27 09:40:45 +08:00
wanglin2
3763cd0efc Feat:修改导出图片方法的参数,导出pdf时如果思维导图尺寸小于a4纸那么不旋转方向 2023-08-26 21:54:00 +08:00
wanglin2
4ce9533763 打包0.6.17 2023-08-25 17:29:33 +08:00
wanglin2
43ecdafdd1 Doc: update 2023-08-25 17:25:56 +08:00
wanglin2
5ff8704778 Feat:拦截富文本编辑时的粘贴操作,去掉格式,只允许粘贴纯文本 2023-08-25 09:20:12 +08:00
wanglin2
d206d6dd99 Fix:修复mindMap.export方法代码错误 2023-08-24 18:02:58 +08:00
wanglin2
850b9ed936 Fix:修复导入百度脑图导出的xmind文件报错的问题 2023-08-24 17:57:12 +08:00
wanglin2
26508b5a62 Feat:支持配置是否开启双击复位思维导图 2023-08-24 17:40:35 +08:00
街角小林
061c82459e Merge pull request #282 from hyfree/main-1
Create index.d.ts
2023-08-24 17:23:53 +08:00
wanglin2
d2bf5be7c7 更新群二维码 2023-08-24 16:55:35 +08:00
hyfree
b726aa30e5 Create index.d.ts 2023-08-24 15:50:39 +08:00
wanglin2
6546ec090e Merge branch 'feature' into main 2023-08-22 09:58:18 +08:00
wanglin2
4dc5754f1d 打包0.6.16 2023-08-22 09:57:47 +08:00
wanglin2
7f199e6c2f Doc: update 2023-08-22 09:54:41 +08:00
wanglin2
2548ac4eb4 Feat:去除dom-to-image-more库 2023-08-21 15:12:40 +08:00
wanglin2
60e503ab1f Feat:1.去除导出svg方法的第二个参数,改为通过实例化配置;2.导出图片不再使用外部库 2023-08-21 15:10:03 +08:00
wanglin2
c718cbc030 Fix:优化导出图片逻辑,遍历节点转换图片的url时,如果已经是data:URL形式不用再处理 2023-08-21 13:48:45 +08:00
wanglin2
beb2b550f0 Fix:优化富文本测量元素的逻辑,删除样式的重复设置和重复添加 2023-08-21 09:58:33 +08:00
wanglin2
d3e1389f10 Fix:降级dom-to-image-more库版本,解决在firefox浏览器导出图片为空的问题 2023-08-21 09:14:19 +08:00
wanglin2
68cb24c0b6 Merge branch 'feature' into main 2023-08-19 08:04:45 +08:00
wanglin2
e3959107bd Demo:修复单独编辑大纲时能给根节点添加兄弟节点的bug 2023-08-19 08:04:28 +08:00
wanglin2
4b3c81ab91 Merge branch 'feature' into main 2023-08-18 23:11:03 +08:00
wanglin2
76b5d9d11d 打包0.6.15-fix.2 2023-08-18 23:10:29 +08:00
wanglin2
22d0fe5ac4 打包0.6.15-fix.2 2023-08-18 23:10:01 +08:00
wanglin2
8b8d549abd Fix:修复在Firefox浏览器中富文本节点无法显示的问题 2023-08-18 23:06:30 +08:00
wanglin2
731a5a504d Merge branch 'feature' into main 2023-08-18 15:13:42 +08:00
wanglin2
008e697b74 打包0.6.15-fix.1 2023-08-18 15:13:24 +08:00
wanglin2
fe1745e779 Merge branch 'feature' into main 2023-08-18 14:55:15 +08:00
wanglin2
e6ac9be402 打包0.6.15 2023-08-18 14:54:49 +08:00
wanglin2
b427a9ed1b Doc: update 2023-08-18 14:51:42 +08:00
wanglin2
a4dc9210b3 Fix:修复节点边框会重合的问题 2023-08-18 14:25:24 +08:00
wanglin2
fb681de1f5 Feat:增加错误处理 2023-08-18 11:02:24 +08:00
wanglin2
3757622521 Fix:修复画布距浏览器窗口左上角不为0时鼠标缩放时不以鼠标为中心的问题 2023-08-18 10:33:52 +08:00
wanglin2
12265be7d4 Feat:新增禁止鼠标滚轮缩放的配置 2023-08-18 10:16:19 +08:00
wanglin2
df1aed7e04 Fix:优化展开收起按钮的占位元素:1.没有子节点的节点不渲染该元素;2.根据是否存在子节点动态更新该元素 2023-08-18 10:09:23 +08:00
wanglin2
c32c9d1ba1 更新群二维码 2023-08-17 10:06:16 +08:00
wanglin2
26d75c9203 打包 2023-08-17 10:04:49 +08:00
wanglin2
8d1e9fa8e9 Demo:侧边栏涉及图形的选项增加可视化效果 2023-08-17 10:00:33 +08:00
wanglin2
ebc99e97af Feat:导出pdf支持根据长宽比自动调整方向 2023-08-17 08:49:40 +08:00
wanglin2
efe4aa0ec2 Feat:导出pdf支持根据图片大小分页导出 2023-08-16 17:45:34 +08:00
wanglin2
3f659af1e1 Featc:删除重复的exportPadding配置,改为使用exportPaddingX和exportPaddingY配置;去除导出图片时的双重padding设置 2023-08-16 17:20:05 +08:00
wanglin2
64209a6392 Featc:删除重复的exportPadding配置,改为使用exportPaddingX和exportPaddingY配置;去除导出图片时的双重padding设置 2023-08-16 17:19:35 +08:00
wanglin2
8d1e5dd2e9 Feat:导出svg的图形的paddingX和paddingY改为单侧padding 2023-08-16 17:17:18 +08:00
wanglin2
d218122752 '打包' 2023-08-16 09:31:03 +08:00
wanglin2
9af8afca22 Demo:单独编辑大纲不再和画布联动,优化大数据量下的编辑体验 2023-08-16 09:25:33 +08:00
wanglin2
002ec41ba8 Fix:修复节点文本为空时显示异常问题 2023-08-15 09:30:00 +08:00
wanglin2
ffff257f57 Demo:修复打开本地文件右上角的提示无法关闭的问 2023-08-15 09:03:26 +08:00
街角小林
41d0b675a0 Merge pull request #269 from suka233/main
fix: 修复尝试赋值给const常量导致的vite启动dev服务报错
2023-08-15 08:48:43 +08:00
suka233
7601d6730d Merge pull request #1 from suka233/suka233-patch-1
fix: 修复尝试赋值给const常量导致的vite启动dev服务报错
2023-08-14 17:07:29 +08:00
suka233
f0e9b76bb5 fix: 修复尝试赋值给const常量导致的vite启动dev服务报错 2023-08-14 15:13:42 +08:00
wanglin2
4ec33062a4 Merge branch 'feature' into main 2023-08-14 09:39:03 +08:00
wanglin2
edc63b1293 打包 2023-08-14 09:38:49 +08:00
wanglin2
ec83976818 Doc:调整首页文档 2023-08-14 09:36:10 +08:00
wanglin2
393410757b Demo:调整小地图位置,解决被侧边按钮遮挡的问题 2023-08-14 09:29:11 +08:00
wanglin2
f20748744a Demo:右下角支持跳转相关链接 2023-08-14 09:27:59 +08:00
wanglin2
067131f76d Merge branch 'feature' into main 2023-08-13 12:21:16 +08:00
wanglin2
f689d333f9 打包0.6.14 2023-08-13 12:20:31 +08:00
wanglin2
b3059bb6a3 Doc: update 2023-08-13 12:18:25 +08:00
wanglin2
b135f6a61c Demo:支持粘贴知犀思维导图的节点数据 2023-08-13 11:43:44 +08:00
wanglin2
2a8b71497f Feat:自定义处理剪贴板函数支持返回Promise实例 2023-08-13 09:49:29 +08:00
wanglin2
726ebc5e88 Feat:支持自定义处理剪贴板中的文本数据 2023-08-13 08:47:35 +08:00
wanglin2
2c56cd453c Demo:支持从右键菜单删除节点的超链接,备注 2023-08-12 22:45:41 +08:00
wanglin2
bf2f9b2697 Demo:修复退出禅模式后左下角的节点和字数统计未更新的问题 2023-08-12 22:17:38 +08:00
wanglin2
fbd061e8b3 Demo:修复大纲修改一个节点的文本后再点击其他节点时不聚焦的问题 2023-08-12 15:43:10 +08:00
wanglin2
680320ba76 Demo:修复基础样式侧边栏打开状态下,导入思维导图数据后侧边栏数据未更新的问题 2023-08-12 15:10:46 +08:00
wanglin2
0d992bd6b1 Demo:颜色选择器支持选择透明颜色 2023-08-12 14:58:32 +08:00
wanglin2
c80eacc5e7 Demo:修复在节点样式侧边栏里无法去除节点的文本修饰线样式的问题 2023-08-12 14:42:14 +08:00
wanglin2
6d202b4b7d Demo:修复无法在节点图片弹窗里删除节点图片的问题 2023-08-12 14:21:11 +08:00
wanglin2
94478fe9f3 Demo:修复开启输入自动进入文本编辑模式和其他输入框冲突的问题 2023-08-12 11:12:11 +08:00
wanglin2
5745e4567b Feat:修改复制粘贴实现方式,去除创建隐藏输入框,支持跨浏览器粘贴思维导图数据 2023-08-12 10:57:51 +08:00
wanglin2
c6a3f4ac7b Feat:去除创建隐藏输入框,通过navigator.clipboard实现复制粘贴 2023-08-11 17:39:04 +08:00
313 changed files with 12568 additions and 2355 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] 支持协同编辑
# 安装
@@ -87,17 +90,15 @@ const mindMap = new MindMap({
# 微信交流群
<img src="./qrcode.jpg" style="width: 300px" />
如果已过期,可以微信添加`wanglinguanfang`拉你入群。
群聊人数较多,无法通过二维码入群,可以微信添加`wanglinguanfang`拉你入群。
# 请作者喝杯咖啡
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡~
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡~
> 厚椰乳一盒 + 纯牛奶半盒 + 冰块 + 咖啡液 = 生椰拿铁 yyds
> 转账请备注【思维导图】。你的头像和名字将会出现在下面和[文档页面](https://wanglin2.github.io/mind-map/#/doc/zh/introduction/%E8%AF%B7%E4%BD%9C%E8%80%85%E5%96%9D%E6%9D%AF%E5%92%96%E5%95%A1)
> 推荐使用支付宝,微信获取不到头像。转账请备注【思维导图】。
<p>
<img src="./web/src/assets/img/alipay.jpg" style="width: 300px" />
@@ -153,4 +154,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>小米bbᯤ²ᴳ</span>
</span>
<span>
<img src="./web/src/assets/avatar/棐.jpg" style="width: 50px;height: 50px;" />
<span>*棐</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>Luke</span>
</span>
<span>
<img src="./web/src/assets/avatar/布林.jpg" style="width: 50px;height: 50px;" />
<span>布林</span>
</span>
<span>
<img src="./web/src/assets/avatar/南风.jpg" style="width: 50px;height: 50px;" />
<span>南风</span>
</span>
<span>
<img src="./web/src/assets/avatar/蜉蝣撼大叔.jpg" style="width: 50px;height: 50px;" />
<span>蜉蝣撼大叔</span>
</span>
<span>
<img src="./web/src/assets/avatar/乙.jpg" style="width: 50px;height: 50px;" />
<span>乙</span>
</span>
<span>
<img src="./web/src/assets/avatar/敏.jpg" style="width: 50px;height: 50px;" />
<span>敏</span>
</span>
<span>
<img src="./web/src/assets/avatar/沐风牧草.jpg" style="width: 50px;height: 50px;" />
<span>沐风牧草</span>
</span>
<span>
<img src="./web/src/assets/avatar/有希.jpg" style="width: 50px;height: 50px;" />
<span>有希</span>
</span>
<span>
<img src="./web/src/assets/avatar/樊笼.jpg" style="width: 50px;height: 50px;" />
<span>樊笼</span>
</span>
<span>
<img src="./web/src/assets/avatar/达仁科技.jpg" style="width: 50px;height: 50px;" />
<span>达仁科技</span>
</span>
</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?5afd1a8b2213b3355dce" rel="stylesheet"><link href="dist/css/app.css?5afd1a8b2213b3355dce" 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?ceeef97326ffc3ea9cba" rel="stylesheet"><link href="dist/css/app.css?ceeef97326ffc3ea9cba" 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?5afd1a8b2213b3355dce"></script><script src="dist/js/app.js?5afd1a8b2213b3355dce"></script></body></html>
}</script><script src="dist/js/chunk-vendors.js?ceeef97326ffc3ea9cba"></script><script src="dist/js/app.js?ceeef97326ffc3ea9cba"></script></body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

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

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

View File

@@ -7,26 +7,45 @@ import Style from './src/core/render/node/Style'
import KeyCommand from './src/core/command/KeyCommand'
import Command from './src/core/command/Command'
import BatchExecution from './src/utils/BatchExecution'
import { layoutValueList, CONSTANTS, commonCaches } from './src/constants/constant'
import {
layoutValueList,
CONSTANTS,
commonCaches,
ERROR_TYPES,
cssContent
} from './src/constants/constant'
import { SVG } from '@svgdotjs/svg.js'
import { simpleDeepClone, getType } from './src/utils'
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
import { simpleDeepClone, getType, getObjectChangedProps } from './src/utils'
import defaultTheme, {
checkIsNodeSizeIndependenceConfig
} from './src/themes/default'
import { defaultOpt } from './src/constants/defaultOptions'
// 思维导图
class MindMap {
// 构造函数
/**
*
* @param {defaultOpt} opt
*/
constructor(opt = {}) {
// 合并选项
this.opt = this.handleOpt(merge(defaultOpt, opt))
// 容器元素
this.el = this.opt.el
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')
// 添加css
this.cssEl = null
this.addCss()
// 画布
this.svg = SVG().addTo(this.el).size(this.width, this.height)
@@ -68,12 +87,12 @@ class MindMap {
this.batchExecution = new BatchExecution()
// 注册插件
MindMap.pluginList.forEach((plugin) => {
MindMap.pluginList.forEach(plugin => {
this.initPlugin(plugin)
})
// 初始渲染
this.render()
this.render(this.opt.fit ? () => this.view.fit() : () => {})
setTimeout(() => {
this.command.addHistory()
}, 0)
@@ -92,6 +111,19 @@ class MindMap {
return opt
}
// 添加必要的css样式到页面
addCss() {
this.cssEl = document.createElement('style')
this.cssEl.type = 'text/css'
this.cssEl.innerHTML = cssContent
document.head.appendChild(this.cssEl)
}
// 移除css
removeCss() {
document.head.removeChild(this.cssEl)
}
// 渲染,部分渲染
render(callback, source = '') {
this.batchExecution.push('render', () => {
@@ -136,10 +168,10 @@ class MindMap {
// 初始化缓存数据
initCache() {
Object.keys(commonCaches).forEach((key) => {
Object.keys(commonCaches).forEach(key => {
let type = getType(commonCaches[key])
let value = ''
switch(type) {
switch (type) {
case 'Boolean':
value = false
break
@@ -164,7 +196,7 @@ class MindMap {
this.renderer.clearAllActive()
this.opt.theme = theme
this.render(null, CONSTANTS.CHANGE_THEME)
this.emit('view_theme_change', theme)
this.emit('view_theme_change', theme)
}
// 获取当前主题
@@ -174,9 +206,11 @@ class MindMap {
// 设置主题配置
setThemeConfig(config) {
// 计算改变了的配置
const changedConfig = getObjectChangedProps(this.themeConfig, config)
this.opt.themeConfig = config
// 检查改变的是否是节点大小无关的主题属性
let res = checkIsNodeSizeIndependenceConfig(config)
let res = checkIsNodeSizeIndependenceConfig(changedConfig)
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
}
@@ -224,15 +258,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)
}
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
@@ -278,8 +310,12 @@ class MindMap {
// 导出
async export(...args) {
let result = await this.doExport.export(...args)
return result
try {
let result = await this.doExport.export(...args)
return result
} catch (error) {
this.opt.errorHandler(ERROR_TYPES.EXPORT_ERROR, error)
}
}
// 转换位置
@@ -317,17 +353,23 @@ class MindMap {
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 内边距
rect.width += paddingX
rect.height += paddingY
draw.translate(paddingX / 2, paddingY / 2)
rect.width += paddingX * 2
rect.height += paddingY * 2
draw.translate(paddingX, paddingY)
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
// 克隆一份数据
let clone = svg.clone()
// 添加必要的样式
clone.add(SVG(`<style>${cssContent}</style>`))
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
if ((rect.width > origWidth || rect.height > origHeight) && this.watermark && this.watermark.hasWatermark()) {
if (
(rect.width > origWidth || rect.height > origHeight) &&
this.watermark &&
this.watermark.hasWatermark()
) {
this.width = rect.width
this.height = rect.height
this.watermark.draw()
@@ -388,7 +430,10 @@ class MindMap {
// 销毁
destroy() {
// 移除插件
[...MindMap.pluginList].forEach((plugin) => {
;[...MindMap.pluginList].forEach(plugin => {
if (this[plugin.instanceName].beforePluginDestroy) {
this[plugin.instanceName].beforePluginDestroy()
}
this[plugin.instanceName] = null
})
// 解绑事件
@@ -397,19 +442,22 @@ class MindMap {
this.svg.remove()
// 去除给容器元素设置的背景样式
Style.removeBackgroundStyle(this.el)
this.el.innerHTML = ''
this.el = null
this.removeCss()
}
}
// 插件列表
MindMap.pluginList = []
MindMap.usePlugin = (plugin, opt = {}) => {
if (MindMap.hasPlugin(plugin) !== -1) return MindMap
plugin.pluginOpt = opt
MindMap.pluginList.push(plugin)
return MindMap
}
MindMap.hasPlugin = (plugin) => {
return MindMap.pluginList.findIndex((item) => {
MindMap.hasPlugin = plugin => {
return MindMap.pluginList.findIndex(item => {
return item === plugin
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.6.13",
"version": "0.7.3-fix.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,21 +21,26 @@
},
"scripts": {
"lint": "eslint src/",
"format": "prettier --write ."
"format": "prettier --write .",
"types": "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir types --target es2017",
"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",
"dom-to-image-more": "^3.1.6",
"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 = [
{
@@ -249,6 +225,10 @@ export const CONSTANTS = {
PASTE_TYPE: {
CLIP_BOARD: 'clipBoard',
CANVAS: 'canvas'
},
SCROLL_BAR_DIR: {
VERTICAL: 'vertical',
HORIZONTAL: 'horizontal'
}
}
@@ -257,42 +237,42 @@ export const initRootNodePositionMap = {
[CONSTANTS.INIT_ROOT_NODE_POSITION.TOP]: 0,
[CONSTANTS.INIT_ROOT_NODE_POSITION.RIGHT]: 1,
[CONSTANTS.INIT_ROOT_NODE_POSITION.BOTTOM]: 1,
[CONSTANTS.INIT_ROOT_NODE_POSITION.CENTER]: 0.5,
[CONSTANTS.INIT_ROOT_NODE_POSITION.CENTER]: 0.5
}
// 布局结构列表
export const layoutList = [
{
name: '逻辑结构图',
value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
},
{
name: '思维导图',
value: CONSTANTS.LAYOUT.MIND_MAP,
value: CONSTANTS.LAYOUT.MIND_MAP
},
{
name: '组织结构图',
value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE
},
{
name: '目录组织图',
value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION,
value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION
},
{
name: '时间轴',
value: CONSTANTS.LAYOUT.TIMELINE,
value: CONSTANTS.LAYOUT.TIMELINE
},
{
name: '时间轴2',
value: CONSTANTS.LAYOUT.TIMELINE2,
value: CONSTANTS.LAYOUT.TIMELINE2
},
{
name: '竖向时间轴',
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE
},
{
name: '鱼骨图',
value: CONSTANTS.LAYOUT.FISHBONE,
value: CONSTANTS.LAYOUT.FISHBONE
}
]
export const layoutValueList = [
@@ -325,10 +305,49 @@ export const nodeDataNoStylePropList = [
'uid',
'activeStyle',
'associativeLineTargets',
'associativeLineTargetControlOffsets'
'associativeLineTargetControlOffsets',
'associativeLinePoint',
'associativeLineText'
]
// 数据缓存
export const commonCaches = {
measureCustomNodeContentSizeEl: null
}
measureCustomNodeContentSizeEl: null,
measureRichtextNodeTextSizeEl: null
}
// 错误类型
export const ERROR_TYPES = {
READ_CLIPBOARD_ERROR: 'read_clipboard_error',
PARSE_PASTE_DATA_ERROR: 'parse_paste_data_error',
CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR: 'custom_handle_clipboard_text_error',
LOAD_CLIPBOARD_IMAGE_ERROR: 'load_clipboard_image_error',
BEFORE_TEXT_EDIT_ERROR: 'before_text_edit_error',
EXPORT_ERROR: 'export_error'
}
// a4纸的宽高
export const a4Size = {
width: 592.28,
height: 841.89
}
// css
export const cssContent = `
/* 鼠标hover和激活时渲染的矩形 */
.smm-hover-node{
display: none;
opacity: 0.6;
stroke-width: 1;
}
.smm-node:not(.smm-node-dragging):hover .smm-hover-node{
display: block;
}
.smm-node.active .smm-hover-node{
display: block;
opacity: 1;
stroke-width: 2;
}
`

View File

@@ -18,8 +18,6 @@ export const defaultOpt = {
mouseScaleCenterUseMousePosition: true,
// 最多显示几个标签
maxTag: 5,
// 导出图片时的内边距
exportPadding: 20,
// 展开收缩按钮尺寸
expandBtnSize: 20,
// 节点里图片和文字的间距
@@ -70,18 +68,26 @@ export const defaultOpt = {
// 展开收起按钮的颜色
expandBtnStyle: {
color: '#808080',
fill: '#fff'
fill: '#fff',
fontSize: 13,
strokeColor: '#333333'
},
// 自定义展开收起按钮的图标
expandBtnIcon: {
open: '', // svg字符串
close: ''
},
// 处理收起节点数量
expandBtnNumHandler: num => {
return num
},
// 是否显示带数量的收起按钮
isShowExpandNum: true,
// 是否只有当鼠标在画布内才响应快捷键事件
enableShortcutOnlyWhenMouseInSvg: true,
// 初始根节点的位置
initRootNodePosition: null,
// 导出png、svg、pdf时的图形内边距
// 导出png、svg、pdf时的图形内边距,注意是单侧内边距
exportPaddingX: 10,
exportPaddingY: 10,
// 节点文本编辑框的z-index
@@ -127,12 +133,81 @@ export const defaultOpt = {
customInnerElsAppendTo: null,
// 拖拽元素时,指示元素新位置的块的最大高度
nodeDragPlaceholderMaxSize: 20,
// 是否允许创建一个隐藏的输入框,该输入框会在节点激活时聚焦,用于粘贴数据和自动进入文本编辑状态
enableCreateHiddenInput: true,
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
// 该配置在enableCreateHiddenInput设为true时生效
enableAutoEnterTextEditWhenKeydown: true,
// 开启该特性后需要给你的输入框绑定keydown事件并禁止冒泡
enableAutoEnterTextEditWhenKeydown: false,
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
richTextEditFakeInPlace: false
richTextEditFakeInPlace: false,
// 自定义对剪贴板文本的处理。当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据如果你想处理其他思维导图的数据比如processon、zhixi等那么可以传递一个函数接受当前剪贴板中的文本为参数返回处理后的数据可以返回两种类型
/*
1.返回一个纯文本,那么会直接以该文本创建一个子节点
2.返回一个节点对象,格式如下:
{
// 代表是simple-mind-map格式的数据
simpleMindMap: true,
// 节点数据同simple-mind-map节点数据格式
data: {
data: {
text: ''
},
children: []
}
}
*/
// 如果你的处理逻辑存在异步逻辑也可以返回一个promise
customHandleClipboardText: null,
// 禁止鼠标滚轮缩放你仍旧可以使用api进行缩放
disableMouseWheelZoom: false,
// 错误处理函数
errorHandler: (code, error) => {
console.error(code, error)
},
// 设置导出图片和svg时针对富文本节点内容也就是嵌入到svg中的html节点的默认样式覆盖
// 如果不覆盖,会发生偏移问题
resetCss: `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
`,
// 开启鼠标双击复位思维导图位置及缩放
enableDblclickReset: false,
// 导出图片时canvas的缩放倍数该配置会和window.devicePixelRatio值取最大值
minExportImgCanvasScale: 2,
// 节点鼠标hover和激活时显示的矩形边框的颜色
hoverRectColor: 'rgb(94, 200, 248)',
// 节点鼠标hover和激活时显示的矩形边框距节点内容的距离
hoverRectPadding: 2,
// 双击节点进入节点文本编辑时是否默认选中文本,默认只在创建新节点时会选中
selectTextOnEnterEditText: false,
// 删除节点后激活相邻节点
deleteNodeActive: true,
// 拖拽节点时鼠标移动到画布边缘是否开启画布自动移动
autoMoveWhenMouseInEdgeOnDrag: true,
// 是否首次加载fit view
fit: false,
// 拖拽多个节点时随鼠标移动的示意矩形的样式配置
dragMultiNodeRectConfig: {
width: 40,
height: 20,
fill: '' // 填充颜色,如果不传默认使用连线的颜色
},
// 节点拖拽时新位置的示意矩形的填充颜色,如果不传默认使用连线的颜色
dragPlaceholderRectFill: '',
// 节点拖拽时的透明度配置
dragOpacityConfig: {
cloneNodeOpacity: 0.5, // 跟随鼠标移动的克隆节点或矩形的透明度
beingDragNodeOpacity: 0.3 // 被拖拽节点的透明度
},
// 自定义标签的颜色
// {pass: 'green, unpass: 'red'}
tagsColorMap: {},
// 节点协作样式配置
cooperateStyle: {
avatarSize: 22,// 头像大小
fontSize: 12,// 如果是文字头像,那么文字的大小
}
}

View File

@@ -37,7 +37,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 +82,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 +131,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 +150,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

@@ -46,13 +46,20 @@ 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)) {
if (
this.isPause ||
(this.mindMap.opt.enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)
) {
return
}
Object.keys(this.shortcutMap).forEach(key => {

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)

View File

@@ -12,11 +12,18 @@ import {
simpleDeepClone,
walk,
bfsWalk,
loadImage
loadImage,
isUndef,
getTopAncestorsFomNodeList,
addDataToAppointNodes,
createUidForAppointNodes,
formatDataToArray,
getNodeIndex,
createUid
} from '../../utils'
import { shapeList } from './node/Shape'
import { lineStyleProps } from '../../themes/default'
import { CONSTANTS } from '../../constants/constant'
import { CONSTANTS, ERROR_TYPES } from '../../constants/constant'
// 布局列表
const layouts = {
@@ -39,7 +46,6 @@ const layouts = {
}
// 渲染
class Render {
// 构造函数
constructor(opt = {}) {
@@ -91,6 +97,15 @@ class Render {
)(this, this.mindMap.opt.layout)
}
// 重新设置思维导图数据
setData(data) {
if (this.mindMap.richText) {
this.renderTree = this.mindMap.richText.handleSetData(data)
} else {
this.renderTree = data
}
}
// 绑定事件
bindEvent() {
// 点击事件
@@ -108,10 +123,6 @@ class Render {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
})
// 粘贴事件
this.mindMap.on('paste', data => {
this.onPaste(data)
})
// let timer = null
// this.mindMap.on('view_data_change', () => {
// clearTimeout(timer)
@@ -135,9 +146,18 @@ class Render {
// 插入同级节点
this.insertNode = this.insertNode.bind(this)
this.mindMap.command.add('INSERT_NODE', this.insertNode)
// 插入多个同级节点
this.insertMultiNode = this.insertMultiNode.bind(this)
this.mindMap.command.add('INSERT_MULTI_NODE', this.insertMultiNode)
// 插入子节点
this.insertChildNode = this.insertChildNode.bind(this)
this.mindMap.command.add('INSERT_CHILD_NODE', this.insertChildNode)
// 插入多个子节点
this.insertMultiChildNode = this.insertMultiChildNode.bind(this)
this.mindMap.command.add(
'INSERT_MULTI_CHILD_NODE',
this.insertMultiChildNode
)
// 上移节点
this.upNode = this.upNode.bind(this)
this.mindMap.command.add('UP_NODE', this.upNode)
@@ -205,6 +225,9 @@ class Render {
// 设置节点标签
this.setNodeTag = this.setNodeTag.bind(this)
this.mindMap.command.add('SET_NODE_TAG', this.setNodeTag)
// 设置节点公式
this.insertFormula = this.insertFormula.bind(this)
this.mindMap.command.add('INSERT_FORMULA', this.insertFormula)
// 添加节点概要
this.addGeneralization = this.addGeneralization.bind(this)
this.mindMap.command.add('ADD_GENERALIZATION', this.addGeneralization)
@@ -273,8 +296,7 @@ class Render {
this.copy = this.copy.bind(this)
this.mindMap.keyCommand.addShortcut('Control+c', this.copy)
this.mindMap.keyCommand.addShortcut('Control+v', () => {
// 隐藏输入框可能会失去焦点,所以要重新聚焦
this.textEdit.focusHiddenInput()
this.onPaste()
})
this.cut = this.cut.bind(this)
this.mindMap.keyCommand.addShortcut('Control+x', this.cut)
@@ -346,7 +368,7 @@ class Render {
}
})
})
this.mindMap.emit('node_active', null, this.activeNodeList)
this.mindMap.emit('node_active', null, [...this.activeNodeList])
}
// 清除当前激活的节点
@@ -386,19 +408,10 @@ class Render {
// 检索某个节点在激活列表里的索引
findActiveNodeIndex(node) {
return this.activeNodeList.findIndex(item => {
return item === node
return item.uid === node.uid
})
}
// 获取节点在同级里的索引位置
getNodeIndex(node) {
return node.parent
? node.parent.children.findIndex(item => {
return item === node
})
: 0
}
// 全选
selectAll() {
walk(
@@ -411,7 +424,7 @@ class Render {
// 激活节点需要显示展开收起按钮
node.showExpandBtn()
setTimeout(() => {
node.updateNodeShape()
node.updateNodeActive()
}, 0)
}
},
@@ -420,6 +433,7 @@ class Render {
0,
0
)
this.mindMap.emit('node_active', null, [...this.activeNodeList])
}
// 回退
@@ -442,67 +456,151 @@ class Render {
}
}
// 规范指定节点数据
formatAppointNodes(appointNodes) {
if (!appointNodes) return []
return Array.isArray(appointNodes) ? appointNodes : [appointNodes]
}
// 插入同级节点,多个节点只会操作第一个节点
insertNode(openEdit = true, appointNodes = [], appointData = null) {
appointNodes = this.formatAppointNodes(appointNodes)
// 插入同级节点
insertNode(
openEdit = true,
appointNodes = [],
appointData = null,
appointChildren = []
) {
appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.textEdit.hideEditTextBox()
let {
const {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
let first = list[0]
if (first.isGeneralization) {
return
const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
const handleMultiNodes = list.length > 1
const isRichText = !!this.mindMap.richText
const params = {
expand: true,
richText: isRichText,
resetRichText: isRichText,
isActive: handleMultiNodes || !openEdit // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
}
if (first.isRoot) {
this.insertChildNode(openEdit, appointNodes, appointData)
} else {
let text =
first.layerIndex === 1
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
if (first.layerIndex === 1) {
first.parent.destroy()
// 动态指定的子节点数据也需要添加相关属性
appointChildren = addDataToAppointNodes(appointChildren, {
...params
})
const needDestroyNodeList = {}
list.forEach(node => {
if (node.isGeneralization || node.isRoot) {
return
}
let index = this.getNodeIndex(first)
let isRichText = !!this.mindMap.richText
first.parent.nodeData.children.splice(index + 1, 0, {
inserting: openEdit,
const parent = node.parent
const isOneLayer = node.layerIndex === 1
// 插入二级节点时根节点需要重新渲染
if (isOneLayer && !needDestroyNodeList[parent.uid]) {
needDestroyNodeList[parent.uid] = parent
}
// 新插入节点的默认文本
const text = isOneLayer
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
// 计算插入位置
const index = parent.nodeData.children.findIndex(item => {
return item.data.uid === node.uid
})
const newNodeData = {
inserting: handleMultiNodes ? false : openEdit, // 如果同时对多个节点插入子节点,那么无需进入编辑模式,
data: {
text: text,
expand: true,
richText: isRichText,
resetRichText: isRichText,
...params,
uid: createUid(),
...(appointData || {})
},
children: []
})
this.mindMap.render()
children: [...createUidForAppointNodes(appointChildren, true)]
}
parent.nodeData.children.splice(index + 1, 0, newNodeData)
})
Object.keys(needDestroyNodeList).forEach(key => {
needDestroyNodeList[key].destroy()
})
// 如果同时对多个节点插入子节点,需要清除原来激活的节点
if (handleMultiNodes || !openEdit) {
this.clearActive()
}
this.mindMap.render()
}
// 插入多个同级节点
insertMultiNode(appointNodes, nodeList) {
if (!nodeList || nodeList.length <= 0) return
appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.textEdit.hideEditTextBox()
const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
const isRichText = !!this.mindMap.richText
const params = {
expand: true,
richText: isRichText,
resetRichText: isRichText,
isActive: true
}
nodeList = addDataToAppointNodes(nodeList, params)
const needDestroyNodeList = {}
list.forEach(node => {
if (node.isGeneralization || node.isRoot) {
return
}
const parent = node.parent
const isOneLayer = node.layerIndex === 1
// 插入二级节点时根节点需要重新渲染
if (isOneLayer && !needDestroyNodeList[parent.uid]) {
needDestroyNodeList[parent.uid] = parent
}
// 计算插入位置
const index = parent.nodeData.children.findIndex(item => {
return item.data.uid === node.uid
})
const newNodeList = createUidForAppointNodes(simpleDeepClone(nodeList), true)
parent.nodeData.children.splice(
index + 1,
0,
...newNodeList
)
})
Object.keys(needDestroyNodeList).forEach(key => {
needDestroyNodeList[key].destroy()
})
this.clearActive()
this.mindMap.render()
}
// 插入子节点
insertChildNode(openEdit = true, appointNodes = [], appointData = null) {
appointNodes = this.formatAppointNodes(appointNodes)
insertChildNode(
openEdit = true,
appointNodes = [],
appointData = null,
appointChildren = []
) {
appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.textEdit.hideEditTextBox()
let {
const {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
const handleMultiNodes = list.length > 1
const isRichText = !!this.mindMap.richText
const params = {
expand: true,
richText: isRichText,
resetRichText: isRichText,
isActive: handleMultiNodes || !openEdit // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
}
// 动态指定的子节点数据也需要添加相关属性
appointChildren = addDataToAppointNodes(appointChildren, {
...params
})
list.forEach(node => {
if (node.isGeneralization) {
return
@@ -510,27 +608,66 @@ class Render {
if (!node.nodeData.children) {
node.nodeData.children = []
}
let text = node.isRoot
const text = node.isRoot
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
let isRichText = !!this.mindMap.richText
node.nodeData.children.push({
inserting: openEdit,
const newNode = {
inserting: handleMultiNodes ? false : openEdit, // 如果同时对多个节点插入子节点,那么无需进入编辑模式
data: {
text: text,
expand: true,
richText: isRichText,
resetRichText: isRichText,
uid: createUid(),
...params,
...(appointData || {})
},
children: []
})
children: [...createUidForAppointNodes(appointChildren, true)]
}
node.nodeData.children.push(newNode)
// 插入子节点时自动展开子节点
node.nodeData.data.expand = true
if (node.isRoot) {
node.destroy()
}
})
// 如果同时对多个节点插入子节点,需要清除原来激活的节点
if (handleMultiNodes || !openEdit) {
this.clearActive()
}
this.mindMap.render()
}
// 插入多个子节点
insertMultiChildNode(appointNodes, childList) {
if (!childList || childList.length <= 0) return
appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.textEdit.hideEditTextBox()
const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
const isRichText = !!this.mindMap.richText
const params = {
expand: true,
richText: isRichText,
resetRichText: isRichText,
isActive: true
}
childList = addDataToAppointNodes(childList, params)
list.forEach(node => {
if (node.isGeneralization) {
return
}
if (!node.nodeData.children) {
node.nodeData.children = []
}
childList = createUidForAppointNodes(childList, true)
node.nodeData.children.push(...childList)
// 插入子节点时自动展开子节点
node.nodeData.data.expand = true
if (node.isRoot) {
node.destroy()
}
})
this.clearActive()
this.mindMap.render()
}
@@ -546,7 +683,7 @@ class Render {
let parent = node.parent
let childList = parent.children
let index = childList.findIndex(item => {
return item === node
return item.uid === node.uid
})
if (index === -1 || index === 0) {
return
@@ -573,7 +710,7 @@ class Render {
let parent = node.parent
let childList = parent.children
let index = childList.findIndex(item => {
return item === node
return item.uid === node.uid
})
if (index === -1 || index === childList.length - 1) {
return
@@ -591,15 +728,29 @@ class Render {
// 复制节点
copy() {
this.beingCopyData = this.copyNode()
this.setCopyDataToClipboard(this.beingCopyData)
}
// 剪切节点
cut() {
this.mindMap.execCommand('CUT_NODE', copyData => {
this.beingCopyData = copyData
this.setCopyDataToClipboard(copyData)
})
}
// 将粘贴或剪切的数据设置到用户剪切板中
setCopyDataToClipboard(data) {
if (navigator.clipboard) {
navigator.clipboard.writeText(
JSON.stringify({
simpleMindMap: true,
data
})
)
}
}
// 粘贴节点
paste() {
if (this.beingCopyData) {
@@ -608,7 +759,29 @@ class Render {
}
// 粘贴事件
async onPaste({ text, img }) {
async onPaste() {
const { errorHandler } = this.mindMap.opt
// 读取剪贴板的文字和图片
let text = null
let img = null
if (navigator.clipboard) {
try {
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
}
}
}
}
} catch (error) {
errorHandler(ERROR_TYPES.READ_CLIPBOARD_ERROR, error)
}
}
// 检查剪切板数据是否有变化
// 通过图片大小来判断图片是否发生变化,可能是不准确的,但是目前没有其他好方法
const imgSize = img ? img.size : 0
@@ -626,9 +799,47 @@ class Render {
if (this.currentBeingPasteType === CONSTANTS.PASTE_TYPE.CLIP_BOARD) {
// 存在文本,则创建子节点
if (text) {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
// 判断粘贴的是否是simple-mind-map的数据
let smmData = null
let useDefault = true
// 用户自定义处理
if (this.mindMap.opt.customHandleClipboardText) {
try {
const res = await this.mindMap.opt.customHandleClipboardText(text)
if (!isUndef(res)) {
useDefault = false
if (typeof res === 'object' && res.simpleMindMap) {
smmData = res.data
} else {
text = String(res)
}
}
} catch (error) {
errorHandler(ERROR_TYPES.CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR, error)
}
}
// 默认处理
if (useDefault) {
try {
const parsedData = JSON.parse(text)
if (parsedData && parsedData.simpleMindMap) {
smmData = parsedData.data
}
} catch (error) {
errorHandler(ERROR_TYPES.PARSE_PASTE_DATA_ERROR, error)
}
}
if (smmData) {
this.mindMap.execCommand(
'INSERT_MULTI_CHILD_NODE',
[],
Array.isArray(smmData) ? smmData : [smmData]
)
} else {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
}
}
// 存在图片,则添加到当前激活节点
if (img) {
@@ -645,7 +856,7 @@ class Render {
})
}
} catch (error) {
console.log(error)
errorHandler(ERROR_TYPES.LOAD_CLIPBOARD_IMAGE_ERROR, error)
}
}
} else {
@@ -656,89 +867,75 @@ class Render {
// 将节点移动到另一个节点的前面
insertBefore(node, exist) {
if (node.isRoot) {
return
}
// 如果是二级节点变成了下级节点,或是下级节点变成了二级节点,节点样式需要更新
let nodeLayerChanged =
(node.layerIndex === 1 && exist.layerIndex !== 1) ||
(node.layerIndex !== 1 && exist.layerIndex === 1)
// 移动节点
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
let nodeIndex = nodeBorthers.findIndex(item => {
return item === node
})
if (nodeIndex === -1) {
return
}
nodeBorthers.splice(nodeIndex, 1)
nodeParent.nodeData.children.splice(nodeIndex, 1)
// 目标节点
let existParent = exist.parent
let existBorthers = existParent.children
let existIndex = existBorthers.findIndex(item => {
return item === exist
})
if (existIndex === -1) {
return
}
existBorthers.splice(existIndex, 0, node)
existParent.nodeData.children.splice(existIndex, 0, node.nodeData)
this.mindMap.render(() => {
if (nodeLayerChanged) {
node.reRender()
}
})
this.insertTo(node, exist, 'before')
}
// 将节点移动到另一个节点的后面
insertAfter(node, exist) {
if (node.isRoot) {
return
}
// 如果是二级节点变成了下级节点,或是下级节点变成了二级节点,节点样式需要更新
let nodeLayerChanged =
(node.layerIndex === 1 && exist.layerIndex !== 1) ||
(node.layerIndex !== 1 && exist.layerIndex === 1)
// 移动节点
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
let nodeIndex = nodeBorthers.findIndex(item => {
return item === node
})
if (nodeIndex === -1) {
return
}
nodeBorthers.splice(nodeIndex, 1)
nodeParent.nodeData.children.splice(nodeIndex, 1)
this.insertTo(node, exist, 'after')
}
// 目标节点
let existParent = exist.parent
let existBorthers = existParent.children
let existIndex = existBorthers.findIndex(item => {
return item === exist
// 将节点移动到另一个节点的前面或后面
insertTo(node, exist, dir = 'before') {
let nodeList = formatDataToArray(node)
nodeList = nodeList.filter(item => {
return !item.isRoot
})
if (existIndex === -1) {
return
if (dir === 'after') {
nodeList.reverse()
}
existIndex++
existBorthers.splice(existIndex, 0, node)
existParent.nodeData.children.splice(existIndex, 0, node.nodeData)
this.mindMap.render(() => {
if (nodeLayerChanged) {
node.reRender()
nodeList.forEach(item => {
this.checkNodeLayerChange(item, exist)
// 移动节点
let nodeParent = item.parent
let nodeBorthers = nodeParent.children
let nodeIndex = nodeBorthers.findIndex(item2 => {
return item.uid === item2.uid
})
if (nodeIndex === -1) {
return
}
nodeBorthers.splice(nodeIndex, 1)
nodeParent.nodeData.children.splice(nodeIndex, 1)
// 目标节点
let existParent = exist.parent
let existBorthers = existParent.children
let existIndex = existBorthers.findIndex(item2 => {
return item2.uid === exist.uid
})
if (existIndex === -1) {
return
}
if (dir === 'after') {
existIndex++
}
existBorthers.splice(existIndex, 0, item)
existParent.nodeData.children.splice(existIndex, 0, item.nodeData)
})
this.mindMap.render()
}
// 如果是富文本模式,那么某些层级变化需要更新样式
checkNodeLayerChange(node, toNode) {
if (this.mindMap.richText) {
let nodeLayerChanged =
(node.layerIndex === 1 && toNode.layerIndex !== 1) ||
(node.layerIndex !== 1 && toNode.layerIndex === 1)
if (nodeLayerChanged) {
node.nodeData.data.resetRichText = true
}
}
}
// 移除节点
removeNode(appointNodes = []) {
appointNodes = this.formatAppointNodes(appointNodes)
appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
// 删除节点后需要激活的节点
let needActiveNode = null
let isAppointNodes = appointNodes.length > 0
let list = isAppointNodes ? appointNodes : this.activeNodeList
let root = list.find(node => {
@@ -752,6 +949,28 @@ class Render {
root.children = []
root.nodeData.children = []
} else {
// 如果只选中了一个节点,删除后激活其兄弟节点或者父节点
if (
this.activeNodeList.length === 1 &&
!this.activeNodeList[0].isGeneralization &&
this.mindMap.opt.deleteNodeActive
) {
const node = this.activeNodeList[0]
const broList = node.parent.children
const nodeIndex = broList.findIndex(item => item.uid === node.uid)
// 如果后面有兄弟节点
if (nodeIndex < broList.length - 1) {
needActiveNode = broList[nodeIndex + 1]
} else {
// 如果前面有兄弟节点
if (nodeIndex > 0) {
needActiveNode = broList[nodeIndex - 1]
} else {
// 没有兄弟节点
needActiveNode = node.parent
}
}
}
for (let i = 0; i < list.length; i++) {
let node = list[i]
if (isAppointNodes) list.splice(i, 1)
@@ -770,55 +989,73 @@ class Render {
}
}
}
this.mindMap.emit('node_active', null, this.activeNodeList)
this.activeNodeList = []
// 激活被删除节点的兄弟节点或父节点
if (needActiveNode) {
this.activeNodeList.push(needActiveNode)
this.setNodeActive(needActiveNode, true)
needActiveNode = null
}
this.mindMap.emit('node_active', null, [...this.activeNodeList])
this.mindMap.render()
}
// 移除某个指定节点
removeOneNode(node) {
let index = this.getNodeIndex(node)
let index = getNodeIndex(node)
node.remove()
node.parent.children.splice(index, 1)
node.parent.nodeData.children.splice(index, 1)
}
// 复制节点,多个节点只会操作第一个节点
// 复制节点
copyNode() {
if (this.activeNodeList.length <= 0) {
return
}
return copyNodeTree({}, this.activeNodeList[0], true)
const nodeList = getTopAncestorsFomNodeList(this.activeNodeList)
return nodeList.map(node => {
return copyNodeTree({}, node, true)
})
}
// 剪切节点,多个节点只会操作第一个节点
// 剪切节点
cutNode(callback) {
if (this.activeNodeList.length <= 0) {
return
}
let node = this.activeNodeList[0]
if (node.isRoot) {
return null
}
let copyData = copyNodeTree({}, node, true)
this.removeActiveNode(node)
this.removeOneNode(node)
this.mindMap.emit('node_active', null, this.activeNodeList)
const nodeList = getTopAncestorsFomNodeList(this.activeNodeList).filter(
node => {
return !node.isRoot
}
)
const copyData = nodeList.map(node => {
return copyNodeTree({}, node, true)
})
nodeList.forEach(node => {
this.removeActiveNode(node)
this.removeOneNode(node)
})
this.mindMap.emit('node_active', null, [...this.activeNodeList])
this.mindMap.render()
if (callback && typeof callback === 'function') {
callback(copyData)
}
}
// 移动一个节点作为另一个节点的子节点
// 移动节点作为另一个节点的子节点
moveNodeTo(node, toNode) {
if (node.isRoot) {
return
}
// let copyData = copyNodeTree({}, node, false, true)
this.removeActiveNode(node)
this.removeOneNode(node)
this.mindMap.emit('node_active', null, this.activeNodeList)
toNode.nodeData.children.push(node.nodeData)
let nodeList = formatDataToArray(node)
nodeList = nodeList.filter(item => {
return !item.isRoot
})
nodeList.forEach(item => {
this.checkNodeLayerChange(item, toNode)
this.removeActiveNode(item)
this.removeOneNode(item)
toNode.nodeData.children.push(item.nodeData)
})
this.mindMap.emit('node_active', null, [...this.activeNodeList])
this.mindMap.render()
if (toNode.isRoot) {
toNode.destroy()
@@ -827,29 +1064,26 @@ class Render {
// 粘贴节点到节点
pasteNode(data) {
if (this.activeNodeList.length <= 0 || !data) {
data = formatDataToArray(data)
if (this.activeNodeList.length <= 0 || data.length <= 0) {
return
}
this.activeNodeList.forEach(item => {
item.nodeData.children.push(simpleDeepClone(data))
this.activeNodeList.forEach(node => {
node.nodeData.children.push(
...data.map(item => {
const newData = simpleDeepClone(item)
createUidForAppointNodes([newData], true)
return newData
})
)
})
this.mindMap.render()
}
// 设置节点样式
setNodeStyle(node, prop, value, isActive) {
let data = {}
if (isActive) {
data = {
activeStyle: {
...(node.nodeData.data.activeStyle || {}),
[prop]: value
}
}
} else {
data = {
[prop]: value
}
setNodeStyle(node, prop, value) {
let data = {
[prop]: value
}
// 如果开启了富文本,则需要应用到富文本上
if (this.mindMap.richText) {
@@ -870,18 +1104,8 @@ class Render {
}
// 设置节点多个样式
setNodeStyles(node, style, isActive) {
let data = {}
if (isActive) {
data = {
activeStyle: {
...(node.nodeData.data.activeStyle || {}),
...style
}
}
} else {
data = style
}
setNodeStyles(node, style) {
let data = { ...style }
// 如果开启了富文本,则需要应用到富文本上
if (this.mindMap.richText) {
let config = this.mindMap.richText.normalStyleToRichTextStyle(style)
@@ -916,7 +1140,7 @@ class Render {
} else {
node.hideExpandBtn()
}
node.updateNodeShape()
node.updateNodeActive()
}
// 设置节点是否展开
@@ -1027,7 +1251,14 @@ class Render {
}
// 设置节点图片
setNodeImage(node, { url, title, width, height, custom = false }) {
setNodeImage(node, data) {
const {
url,
title,
width,
height,
custom = false
} = data || { url: '', title: '', width: 0, height: 0, custom: false }
this.setNodeDataRender(node, {
image: url,
imageTitle: title || '',
@@ -1068,6 +1299,17 @@ class Render {
})
}
// 设置节点公式
insertFormula(formula, appointNodes = []) {
// 只在富文本模式下可用并且需要注册Formula插件
if (!this.mindMap.richText || !this.mindMap.formula) return
appointNodes = formatDataToArray(appointNodes)
const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
list.forEach(node => {
this.mindMap.formula.insertFormulaToNode(node, formula)
})
}
// 添加节点概要
addGeneralization(data) {
if (this.activeNodeList.length <= 0) {

View File

@@ -1,4 +1,11 @@
import { getStrWithBrFromHtml, checkNodeOuter, isMobile } from '../../utils'
import {
getStrWithBrFromHtml,
checkNodeOuter,
focusInput,
selectAllInput,
htmlEscape
} from '../../utils'
import { ERROR_TYPES } from '../../constants/constant'
// 节点文字编辑类
export default class TextEdit {
@@ -10,16 +17,11 @@ export default class TextEdit {
this.currentNode = null
// 文本编辑框
this.textEditNode = null
// 隐藏的文本输入框
this.hiddenInputEl = null
// 节点激活时默认聚焦到隐藏输入框
this.enableFocus = true
// 文本编辑框是否显示
this.showTextEdit = false
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
this.cacheEditingText = ''
this.bindEvent()
this.createHiddenInput()
}
// 事件
@@ -51,10 +53,6 @@ export default class TextEdit {
this.mindMap.on('before_node_active', () => {
this.hideEditTextBox()
})
// 节点激活事件
this.mindMap.on('node_active', () => {
this.focusHiddenInput()
})
// 注册编辑快捷键
this.mindMap.keyCommand.addShortcut('F2', () => {
if (this.renderer.activeNodeList.length <= 0) {
@@ -63,74 +61,29 @@ export default class TextEdit {
this.show(this.renderer.activeNodeList[0])
})
this.mindMap.on('scale', this.onScale)
}
// 创建一个隐藏的文本输入框
createHiddenInput() {
const { enableCreateHiddenInput, enableAutoEnterTextEditWhenKeydown } =
this.mindMap.opt
if (this.hiddenInputEl || isMobile() || !enableCreateHiddenInput) return
this.hiddenInputEl = document.createElement('input')
this.hiddenInputEl.type = 'text'
this.hiddenInputEl.style.cssText = `
position: fixed;
left: -99999px;
top: -99999px;
`
// 监听按键事件
if (enableAutoEnterTextEditWhenKeydown) {
this.hiddenInputEl.addEventListener('keydown', e => {
// // 监听按键事件,判断是否自动进入文本编辑模式
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]
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
const keyCode = e.keyCode
if (
node &&
(keyCode === 229 ||
(keyCode >= 65 && keyCode <= 90) ||
(keyCode >= 48 && keyCode <= 57)) &&
!this.mindMap.keyCommand.hasCombinationKey(e)
) {
this.show(node)
if (node && this.checkIsAutoEnterTextEditKey(e)) {
this.show(node, e, false, true)
}
})
}
// 监听粘贴事件
this.hiddenInputEl.addEventListener('paste', async event => {
event.preventDefault()
const text = (event.clipboardData || window.clipboardData).getData('text')
const files = event.clipboardData.files
let img = null
if (files.length > 0) {
for (let i = 0; i < files.length; i++) {
if (/^image\//.test(files[i].type)) {
img = files[i]
break
}
}
}
this.mindMap.emit('paste', {
text,
img
})
})
document.body.appendChild(this.hiddenInputEl)
}
// 让隐藏的文本输入框聚焦
focusHiddenInput() {
if (this.hiddenInputEl && this.enableFocus) this.hiddenInputEl.focus()
}
// 关闭默认聚焦
stopFocusOnNodeActive() {
this.enableFocus = false
}
// 开启默认聚焦
openFocusOnNodeActive() {
this.enableFocus = true
// 判断是否是自动进入文本编模式的按钮
checkIsAutoEnterTextEditKey(e) {
const keyCode = e.keyCode
return (
(keyCode === 229 ||
(keyCode >= 65 && keyCode <= 90) ||
(keyCode >= 48 && keyCode <= 57)) &&
!this.mindMap.keyCommand.hasCombinationKey(e)
)
}
// 注册临时快捷键
@@ -146,7 +99,8 @@ export default class TextEdit {
// 显示文本编辑框
// isInserting是否是刚创建的节点
async show(node, e, isInserting = false) {
// isFromKeyDown是否是在按键事件进入的编辑
async show(node, e, isInserting = false, isFromKeyDown = false) {
// 使用了自定义节点内容那么不响应编辑事件
if (node.isUseCustomNodeContent()) {
return
@@ -158,6 +112,7 @@ export default class TextEdit {
isShow = await beforeTextEdit(node, isInserting)
} catch (error) {
isShow = false
this.mindMap.opt.errorHandler(ERROR_TYPES.BEFORE_TEXT_EDIT_ERROR, error)
}
if (!isShow) return
}
@@ -166,10 +121,10 @@ export default class TextEdit {
this.mindMap.view.translateXY(offsetLeft, offsetTop)
let rect = node._textData.node.node.getBoundingClientRect()
if (this.mindMap.richText) {
this.mindMap.richText.showEditText(node, rect, isInserting)
this.mindMap.richText.showEditText(node, rect, isInserting, isFromKeyDown)
return
}
this.showEditTextBox(node, rect, isInserting)
this.showEditTextBox(node, rect, isInserting, isFromKeyDown)
}
// 处理画布缩放
@@ -187,7 +142,10 @@ export default class TextEdit {
}
// 显示文本编辑框
showEditTextBox(node, rect, isInserting) {
showEditTextBox(node, rect, isInserting, isFromKeyDown) {
if (this.showTextEdit) return
const { nodeTextEditZIndex, textAutoWrapWidth, selectTextOnEnterEditText } =
this.mindMap.opt
this.mindMap.emit('before_show_text_edit')
this.registerTmpShortcut()
if (!this.textEditNode) {
@@ -203,6 +161,11 @@ export default class TextEdit {
this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('keydown', e => {
if (this.checkIsAutoEnterTextEditKey(e)) {
e.stopPropagation()
}
})
const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
@@ -210,20 +173,21 @@ 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.nodeData.data.text)
.split(/\n/gim)
.map(item => {
return htmlEscape(item)
})
let isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
node.style.domText(this.textEditNode, scale, isMultiLine)
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.style.zIndex = nodeTextEditZIndex
this.textEditNode.innerHTML = textLines.join('<br>')
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
this.textEditNode.style.display = 'block'
this.textEditNode.style.maxWidth =
this.mindMap.opt.textAutoWrapWidth * scale + 'px'
this.textEditNode.style.maxWidth = textAutoWrapWidth * scale + 'px'
if (isMultiLine && lineHeight !== 1) {
this.textEditNode.style.transform = `translateY(${
-((lineHeight * fontSize - fontSize) / 2) * scale
@@ -232,35 +196,16 @@ export default class TextEdit {
this.showTextEdit = true
// 选中文本
// if (!this.cacheEditingText) {
// this.selectNodeText()
// selectAllInput(this.textEditNode)
// }
if (isInserting) {
this.selectNodeText()
if (isInserting || (selectTextOnEnterEditText && !isFromKeyDown)) {
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,10 +1,12 @@
import Style from './Style'
import Shape from './Shape'
import { G, Rect, ForeignObject, SVG } from '@svgdotjs/svg.js'
import { G, ForeignObject, SVG, 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'
// 节点类
@@ -13,7 +15,7 @@ class Node {
constructor(opt = {}) {
// 节点数据
this.nodeData = this.handleData(opt.data || {})
// id
// uid
this.uid = opt.uid
// 控制实例
this.mindMap = opt.mindMap
@@ -54,9 +56,12 @@ class Node {
this.parent = opt.parent || null
// 子节点
this.children = opt.children || []
// 当前同时操作该节点的用户列表
this.userList = []
// 节点内容的容器
this.group = null
this.shapeNode = null // 节点形状节点
this.hoverNode = null // 节点hover和激活的节点
// 节点内容对象
this._customNodeContent = null
this._imgData = null
@@ -72,6 +77,7 @@ class Node {
this._openExpandNode = null
this._closeExpandNode = null
this._fillExpandNode = null
this._userListGroup = null
this._lines = []
this._generalizationLine = null
this._generalizationNode = null
@@ -107,6 +113,10 @@ class Node {
Object.keys(nodeExpandBtnMethods).forEach(item => {
this[item] = nodeExpandBtnMethods[item].bind(this)
})
// 展开收起按钮占位元素相关方法
Object.keys(nodeExpandBtnPlaceholderRectMethods).forEach(item => {
this[item] = nodeExpandBtnPlaceholderRectMethods[item].bind(this)
})
// 命令的相关方法
Object.keys(nodeCommandWrapsMethods).forEach(item => {
this[item] = nodeCommandWrapsMethods[item].bind(this)
@@ -115,6 +125,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()
}
@@ -252,9 +268,11 @@ class Node {
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
this.shapePadding.paddingX = shapePaddingX
this.shapePadding.paddingY = shapePaddingY
// 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点
const borderWidth = this.getBorderWidth()
return {
width: _width + paddingX * 2 + shapePaddingX * 2,
height: _height + paddingY * 2 + margin + shapePaddingY * 2
width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth,
height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth
}
}
@@ -262,27 +280,43 @@ class Node {
layout() {
// 清除之前的内容
this.group.clear()
const { hoverRectPadding } = this.mindMap.opt
let { width, height, textContentItemMargin } = this
let { paddingY } = this.getPaddingVale()
paddingY += this.shapePadding.paddingY
const halfBorderWidth = this.getBorderWidth() / 2
paddingY += this.shapePadding.paddingY + halfBorderWidth
// 节点形状
this.shapeNode = this.shapeInstance.createShape()
this.shapeNode.addClass('smm-node-shape')
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
this.style.shape(this.shapeNode)
this.group.add(this.shapeNode)
this.updateNodeShape()
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
this.renderExpandBtnPlaceholderRect()
// 创建协同头像节点
if (this.createUserListNode) this.createUserListNode()
// 概要节点添加一个带所属节点id的类名
if (this.isGeneralization && this.generalizationBelongNode) {
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
}
// 激活hover和激活边框
const addHoverNode = () => {
this.hoverNode = new Rect()
.size(width + hoverRectPadding * 2, height + hoverRectPadding * 2)
.x(-hoverRectPadding)
.y(-hoverRectPadding)
this.hoverNode.addClass('smm-hover-node')
this.style.hoverNode(this.hoverNode, width, height)
this.group.add(this.hoverNode)
}
// 如果存在自定义节点内容,那么使用自定义节点内容
if (this.isUseCustomNodeContent()) {
let foreignObject = new ForeignObject()
foreignObject.width(width)
foreignObject.height(height)
foreignObject.add(SVG(this._customNodeContent))
foreignObject.add(this._customNodeContent)
this.group.add(foreignObject)
addHoverNode()
return
}
// 图片节点
@@ -356,27 +390,7 @@ class Node {
: 0)
)
this.group.add(textContentNested)
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnPlaceholderRect() {
if (!this.mindMap.opt.alwaysShowExpandBtn) {
let { width, height } = this
if (!this._unVisibleRectRegionNode) {
this._unVisibleRectRegionNode = new Rect()
this._unVisibleRectRegionNode.fill({
color: 'transparent'
})
}
this.group.add(this._unVisibleRectRegionNode)
this.renderer.layout.renderExpandBtnRect(
this._unVisibleRectRegionNode,
this.expandBtnSize,
width,
height,
this
)
}
addHoverNode()
}
// 给节点绑定事件
@@ -392,14 +406,27 @@ class Node {
this.active(e)
})
this.group.on('mousedown', e => {
if (this.isRoot && e.which === 3 && !this.mindMap.opt.readonly) {
e.stopPropagation()
}
if (!this.isRoot && e.which !== 2 && !this.mindMap.opt.readonly) {
e.stopPropagation()
const {
readonly,
enableCtrlKeyNodeSelection,
useLeftKeySelectionRightKeyDrag
} = this.mindMap.opt
// 只读模式不需要阻止冒泡
if (!readonly) {
if (this.isRoot) {
// 根节点,右键拖拽画布模式下不需要阻止冒泡
if (e.which === 3 && !useLeftKeySelectionRightKeyDrag) {
e.stopPropagation()
}
} else {
// 非根节点,且按下的是非鼠标中键,需要阻止事件冒泡
if (e.which !== 2) {
e.stopPropagation()
}
}
}
// 多选和取消多选
if (e.ctrlKey && this.mindMap.opt.enableCtrlKeyNodeSelection) {
if (e.ctrlKey && enableCtrlKeyNodeSelection) {
this.isMultipleChoice = true
let isActive = this.nodeData.data.isActive
if (!isActive)
@@ -412,11 +439,9 @@ class Node {
this.mindMap.renderer[isActive ? 'removeActiveNode' : 'addActiveNode'](
this
)
this.mindMap.emit(
'node_active',
isActive ? null : this,
this.mindMap.renderer.activeNodeList
)
this.mindMap.emit('node_active', isActive ? null : this, [
...this.mindMap.renderer.activeNodeList
])
}
this.mindMap.emit('node_mousedown', this, e)
})
@@ -427,12 +452,14 @@ class Node {
this.mindMap.emit('node_mouseup', this, e)
})
this.group.on('mouseenter', e => {
if (this.isDrag) return
this._isMouseenter = true
// 显示展开收起按钮
this.showExpandBtn()
this.mindMap.emit('node_mouseenter', this, e)
})
this.group.on('mouseleave', e => {
if (!this._isMouseenter) return
this._isMouseenter = false
this.hideExpandBtn()
this.mindMap.emit('node_mouseleave', this, e)
@@ -447,13 +474,21 @@ class Node {
})
// 右键菜单事件
this.group.on('contextmenu', e => {
const { readonly, useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
// 按住ctrl键点击鼠标左键不知为何触发的是contextmenu事件
if (this.mindMap.opt.readonly || e.ctrlKey) {
// || this.isGeneralization
if (readonly || e.ctrlKey) {
return
}
e.stopPropagation()
e.preventDefault()
// 如果是多选节点结束,那么不要触发右键菜单事件
if (
this.mindMap.select &&
!useLeftKeySelectionRightKeyDrag &&
this.mindMap.select.hasSelectRange()
) {
return
}
if (this.nodeData.data.isActive) {
this.renderer.clearActive()
}
@@ -475,14 +510,15 @@ class Node {
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.mindMap.emit('node_active', this, [...this.renderer.activeNodeList])
}
// 更新节点
update(isLayout = false) {
update() {
if (!this.group) {
return
}
this.updateNodeActive()
let { alwaysShowExpandBtn } = this.mindMap.opt
if (alwaysShowExpandBtn) {
// 需要移除展开收缩按钮
@@ -503,6 +539,8 @@ class Node {
}
// 更新概要
this.renderGeneralization()
// 更新协同头像
if (this.updateUserListNode) this.updateUserListNode()
// 更新节点位置
let t = this.group.transform()
// // 如果上次不在可视区内,且本次也不在,那么直接返回
@@ -555,13 +593,11 @@ class Node {
return sizeChange
}
// 更新节点形状样式
updateNodeShape() {
if (!this.shapeNode) return
const shape = this.getShape()
this.style[shape === CONSTANTS.SHAPE.RECTANGLE ? 'rect' : 'shape'](
this.shapeNode
)
// 更新节点激活状态
updateNodeActive() {
if (!this.group) return
const isActive = this.nodeData.data.isActive
this.group[isActive ? 'addClass' : 'removeClass']('active')
}
// 递归渲染
@@ -569,9 +605,7 @@ class Node {
// 节点
// 重新渲染连线
this.renderLine()
let isLayout = false
if (!this.group) {
isLayout = true
// 创建组
this.group = new G()
this.group.addClass('smm-node')
@@ -581,17 +615,14 @@ class Node {
this.bindGroupEvent()
this.draw.add(this.group)
this.layout()
this.update(isLayout)
this.update()
} else {
this.draw.add(this.group)
if (this.needLayout) {
this.needLayout = false
this.layout()
}
if (this.needRerenderExpandBtnPlaceholderRect) {
this.needRerenderExpandBtnPlaceholderRect = false
this.renderExpandBtnPlaceholderRect()
}
this.updateExpandBtnPlaceholderRect()
this.update()
}
// 子节点
@@ -686,6 +717,61 @@ class Node {
}
}
// 设置节点透明度
// 包括连接线和下级节点
setOpacity(val) {
// 自身及连线
this.group.opacity(val)
this._lines.forEach(line => {
line.opacity(val)
})
// 子节点
this.children.forEach(item => {
item.setOpacity(val)
})
// 概要节点
if (this._generalizationNode) {
this._generalizationLine.opacity(val)
this._generalizationNode.group.opacity(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) {
@@ -785,12 +871,12 @@ class Node {
// 检测当前节点是否是某个节点的祖先节点
isParent(node) {
if (this === node) {
if (this.uid === node.uid) {
return false
}
let parent = node.parent
while (parent) {
if (this === parent) {
if (this.uid === parent.uid) {
return true
}
parent = parent.parent
@@ -800,11 +886,11 @@ class Node {
// 检测当前节点是否是某个节点的兄弟节点
isBrother(node) {
if (!this.parent || this === node) {
if (!this.parent || this.uid === node.uid) {
return false
}
return this.parent.children.find(item => {
return item === node
return item.uid === node.uid
})
}
@@ -818,8 +904,8 @@ class Node {
}
// 获取某个样式
getStyle(prop, root, isActive) {
let v = this.style.merge(prop, root, isActive)
getStyle(prop, root) {
let v = this.style.merge(prop, root)
return v === undefined ? '' : v
}
@@ -846,6 +932,11 @@ class Node {
) // 父级
}
// 获取节点非节点状态的边框大小
getBorderWidth() {
return this.style.merge('borderWidth', false) || 0
}
// 获取数据
getData(key) {
return key ? this.nodeData.data[key] || '' : this.nodeData.data

View File

@@ -62,11 +62,10 @@ export default class Shape {
// 创建形状节点
createShape() {
const shape = this.node.getShape()
let { width, height } = this.node
let node = null
// 矩形
if (shape === CONSTANTS.SHAPE.RECTANGLE) {
node = new Rect().size(width, height)
node = this.createRect()
} else if (shape === CONSTANTS.SHAPE.DIAMOND) {
// 菱形
node = this.createDiamond()
@@ -95,9 +94,41 @@ export default class Shape {
return node
}
// 获取节点减去节点边框宽度、hover节点边框宽度后的尺寸
getNodeSize() {
const borderWidth = this.node.getBorderWidth()
let { width, height } = this.node
width -= borderWidth
height -= borderWidth
return {
width,
height
}
}
// 创建矩形
createRect() {
let { width, height } = this.getNodeSize()
let borderRadius = this.node.style.merge('borderRadius')
return new Path().plot(`
M${borderRadius},0
L${width - borderRadius},0
C${width - borderRadius},0 ${width},${0} ${width},${borderRadius}
L${width},${height - borderRadius}
C${width},${height - borderRadius} ${width},${height} ${
width - borderRadius
},${height}
L${borderRadius},${height}
C${borderRadius},${height} ${0},${height} ${0},${height - borderRadius}
L${0},${borderRadius}
C${0},${borderRadius} ${0},${0} ${borderRadius},${0}
Z
`)
}
// 创建菱形
createDiamond() {
let { width, height } = this.node
let { width, height } = this.getNodeSize()
let halfWidth = width / 2
let halfHeight = height / 2
let topX = halfWidth
@@ -120,7 +151,7 @@ export default class Shape {
createParallelogram() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
let { width, height } = this.getNodeSize()
return new Polygon().plot([
[paddingX, 0],
[width, 0],
@@ -131,7 +162,7 @@ export default class Shape {
// 创建圆角矩形
createRoundedRectangle() {
let { width, height } = this.node
let { width, height } = this.getNodeSize()
let halfHeight = height / 2
return new Path().plot(`
M${halfHeight},0
@@ -145,7 +176,7 @@ export default class Shape {
// 创建八角矩形
createOctagonalRectangle() {
let w = 5
let { width, height } = this.node
let { width, height } = this.getNodeSize()
return new Polygon().plot([
[0, w],
[w, 0],
@@ -162,7 +193,7 @@ export default class Shape {
createOuterTriangularRectangle() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
let { width, height } = this.getNodeSize()
return new Polygon().plot([
[paddingX, 0],
[width - paddingX, 0],
@@ -177,7 +208,7 @@ export default class Shape {
createInnerTriangularRectangle() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
let { width, height } = this.getNodeSize()
return new Polygon().plot([
[0, 0],
[width, 0],
@@ -190,7 +221,7 @@ export default class Shape {
// 创建椭圆
createEllipse() {
let { width, height } = this.node
let { width, height } = this.getNodeSize()
let halfWidth = width / 2
let halfHeight = height / 2
return new Path().plot(`
@@ -203,7 +234,7 @@ export default class Shape {
// 创建圆
createCircle() {
let { width, height } = this.node
let { width, height } = this.getNodeSize()
let halfWidth = width / 2
let halfHeight = height / 2
return new Path().plot(`

View File

@@ -1,6 +1,16 @@
import { tagColorList, nodeDataNoStylePropList } from '../../../constants/constant'
import {
checkIsNodeStyleDataKey,
generateColorByContent
} from '../../../utils/index'
const rootProp = ['paddingX', 'paddingY']
const backgroundStyleProps = ['backgroundColor', 'backgroundImage', 'backgroundRepeat', 'backgroundPosition', 'backgroundSize']
const backgroundStyleProps = [
'backgroundColor',
'backgroundImage',
'backgroundRepeat',
'backgroundPosition',
'backgroundSize'
]
// 样式类
class Style {
@@ -10,12 +20,18 @@ class Style {
if (!Style.cacheStyle) {
Style.cacheStyle = {}
let style = window.getComputedStyle(el)
backgroundStyleProps.forEach((prop) => {
backgroundStyleProps.forEach(prop => {
Style.cacheStyle[prop] = style[prop]
})
}
// 设置新样式
let { backgroundColor, backgroundImage, backgroundRepeat, backgroundPosition, backgroundSize } = themeConfig
let {
backgroundColor,
backgroundImage,
backgroundRepeat,
backgroundPosition,
backgroundSize
} = themeConfig
el.style.backgroundColor = backgroundColor
if (backgroundImage && backgroundImage !== 'none') {
el.style.backgroundImage = `url(${backgroundImage})`
@@ -30,7 +46,7 @@ class Style {
// 移除背景样式
static removeBackgroundStyle(el) {
if (!Style.cacheStyle) return
backgroundStyleProps.forEach((prop) => {
backgroundStyleProps.forEach(prop => {
el.style[prop] = Style.cacheStyle[prop]
})
Style.cacheStyle = null
@@ -42,7 +58,7 @@ class Style {
}
// 合并样式
merge(prop, root, isActive) {
merge(prop, root) {
let themeConfig = this.ctx.mindMap.themeConfig
// 三级及以下节点
let defaultConfig = themeConfig.node
@@ -59,17 +75,6 @@ class Style {
// 二级节点
defaultConfig = themeConfig.second
}
// 激活状态
if (isActive !== undefined ? isActive : this.ctx.nodeData.data.isActive) {
if (
this.ctx.nodeData.data.activeStyle &&
this.ctx.nodeData.data.activeStyle[prop] !== undefined
) {
return this.ctx.nodeData.data.activeStyle[prop]
} else if (defaultConfig.active && defaultConfig.active[prop]) {
return defaultConfig.active[prop]
}
}
// 优先使用节点本身的样式
return this.getSelfStyle(prop) !== undefined
? this.getSelfStyle(prop)
@@ -77,8 +82,8 @@ class Style {
}
// 获取某个样式值
getStyle(prop, root, isActive) {
return this.merge(prop, root, isActive)
getStyle(prop, root) {
return this.merge(prop, root)
}
// 获取自身自定义样式
@@ -142,10 +147,10 @@ class Style {
// 获取文本样式
getTextFontStyle() {
return {
italic: this.merge('fontStyle') === 'italic',
bold: this.merge('fontWeight'),
fontSize: this.merge('fontSize'),
return {
italic: this.merge('fontStyle') === 'italic',
bold: this.merge('fontWeight'),
fontSize: this.merge('fontSize'),
fontFamily: this.merge('fontFamily')
}
}
@@ -160,10 +165,10 @@ class Style {
}
// 标签文字
tagText(node, index) {
tagText(node) {
node
.fill({
color: tagColorList[index].color
color: '#fff'
})
.css({
'font-size': '12px'
@@ -171,9 +176,9 @@ class Style {
}
// 标签矩形
tagRect(node, index) {
tagRect(node, text, color) {
node.fill({
color: tagColorList[index].background
color: color || generateColorByContent(text.node.textContent)
})
}
@@ -201,25 +206,40 @@ class Style {
// 展开收起按钮
iconBtn(node, node2, fillNode) {
let { color, fill } = this.ctx.mindMap.opt.expandBtnStyle || {
let { color, fill, fontSize, fontColor } = this.ctx.mindMap.opt
.expandBtnStyle || {
color: '#808080',
fill: '#fff'
fill: '#fff',
fontSize: 12,
strokeColor: '#333333',
fontColor: '#333333'
}
node.fill({ color: color })
node2.fill({ color: color })
fillNode.fill({ color: fill })
if (this.ctx.mindMap.opt.isShowExpandNum) {
node.attr({ 'font-size': fontSize, 'font-color': fontColor })
}
}
// 是否设置了自定义的样式
hasCustomStyle() {
let res = false
Object.keys(this.ctx.nodeData.data).forEach((item) => {
if (!nodeDataNoStylePropList.includes(item)) {
Object.keys(this.ctx.nodeData.data).forEach(item => {
if (checkIsNodeStyleDataKey(item)) {
res = true
}
})
return res
}
// hover和激活节点
hoverNode(node) {
const { hoverRectColor } = this.ctx.mindMap.opt
node.radius(5).fill('none').stroke({
color: hoverRectColor
})
}
}
Style.cacheStyle = null

View File

@@ -39,13 +39,13 @@ function setShape(shape) {
}
// 修改某个样式
function setStyle(prop, value, isActive) {
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value, isActive)
function setStyle(prop, value) {
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value)
}
// 修改多个样式
function setStyles(style, isActive) {
this.mindMap.execCommand('SET_NODE_STYLES', this, style, isActive)
function setStyles(style) {
this.mindMap.execCommand('SET_NODE_STYLES', this, style)
}
export default {

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

@@ -1,4 +1,11 @@
import { measureText, resizeImgSize, removeHtmlStyle, addHtmlStyle, checkIsRichText } from '../../../utils'
import {
measureText,
resizeImgSize,
removeHtmlStyle,
addHtmlStyle,
checkIsRichText,
isUndef
} from '../../../utils'
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
import iconsSvg from '../../../svg/icons'
import { CONSTANTS, commonCaches } from '../../../constants/constant'
@@ -54,7 +61,10 @@ function createIconNode() {
}
let iconSize = this.mindMap.themeConfig.iconSize
return _data.icon.map(item => {
let src = iconsSvg.getNodeIconListIcon(item, this.mindMap.opt.iconList || [])
let src = iconsSvg.getNodeIconListIcon(
item,
this.mindMap.opt.iconList || []
)
let node = null
// svg图标
if (/^<svg/.test(src)) {
@@ -77,6 +87,7 @@ function createIconNode() {
// 创建富文本节点
function createRichTextNode() {
const { textAutoWrapWidth } = this.mindMap.opt
let g = new G()
// 重新设置富文本节点内容
let recoverText = false
@@ -108,25 +119,34 @@ function createRichTextNode() {
this.nodeData.data.text = text
}
let html = `<div>${this.nodeData.data.text}</div>`
let div = document.createElement('div')
if (!commonCaches.measureRichtextNodeTextSizeEl) {
commonCaches.measureRichtextNodeTextSizeEl = document.createElement('div')
commonCaches.measureRichtextNodeTextSizeEl.style.position = 'fixed'
commonCaches.measureRichtextNodeTextSizeEl.style.left = '-999999px'
this.mindMap.el.appendChild(commonCaches.measureRichtextNodeTextSizeEl)
}
let div = commonCaches.measureRichtextNodeTextSizeEl
div.innerHTML = html
div.style.cssText = `position: fixed; left: -999999px;`
let el = div.children[0]
el.classList.add('smm-richtext-node-wrap')
el.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px'
this.mindMap.el.appendChild(div)
el.style.maxWidth = textAutoWrapWidth + 'px'
let { width, height } = el.getBoundingClientRect()
width = Math.ceil(width) + 1// 修复getBoundingClientRect方法对实际宽度是小数的元素获取到的值是整数导致宽度不够文本发生换行的问题
// 如果文本为空,那么需要计算一个默认高度
if (height <= 0) {
div.innerHTML = '<p>abc123我和你</p>'
let elTmp = div.children[0]
elTmp.classList.add('smm-richtext-node-wrap')
height = elTmp.getBoundingClientRect().height
}
width = Math.ceil(width) + 1 // 修复getBoundingClientRect方法对实际宽度是小数的元素获取到的值是整数导致宽度不够文本发生换行的问题
height = Math.ceil(height)
g.attr('data-width', width)
g.attr('data-height', height)
html = div.innerHTML
this.mindMap.el.removeChild(div)
let foreignObject = new ForeignObject()
foreignObject.width(width)
foreignObject.height(height)
foreignObject.add(SVG(html))
foreignObject.add(div.children[0])
g.add(foreignObject)
return {
node: g,
@@ -141,15 +161,14 @@ function createTextNode() {
return this.createRichTextNode()
}
let g = new G()
let fontSize = this.getStyle('fontSize', false, this.nodeData.data.isActive)
let lineHeight = this.getStyle(
'lineHeight',
false,
this.nodeData.data.isActive
)
let fontSize = this.getStyle('fontSize', false)
let lineHeight = this.getStyle('lineHeight', false)
// 文本超长自动换行
let textStyle = this.style.getTextFontStyle()
let textArr = this.nodeData.data.text.split(/\n/gim)
let textArr = []
if (!isUndef(this.nodeData.data.text)) {
textArr = String(this.nodeData.data.text).split(/\n/gim)
}
let maxWidth = this.mindMap.opt.textAutoWrapWidth
let isMultiLine = false
textArr.forEach((item, index) => {
@@ -234,12 +253,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,
@@ -274,9 +296,10 @@ function createNoteNode() {
box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
display: none;
background-color: #fff;
z-index: ${ this.mindMap.opt.nodeNoteTooltipZIndex }
z-index: ${this.mindMap.opt.nodeNoteTooltipZIndex}
`
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.noteEl)
}
this.noteEl.innerText = this.nodeData.data.note
@@ -310,7 +333,7 @@ function createNoteNode() {
}
// 测量自定义节点内容元素的宽高
function measureCustomNodeContentSize (content) {
function measureCustomNodeContentSize(content) {
if (!commonCaches.measureCustomNodeContentSizeEl) {
commonCaches.measureCustomNodeContentSizeEl = document.createElement('div')
commonCaches.measureCustomNodeContentSizeEl.style.cssText = `
@@ -330,19 +353,19 @@ function measureCustomNodeContentSize (content) {
}
// 是否使用的是自定义节点内容
function isUseCustomNodeContent() {
function isUseCustomNodeContent() {
return !!this._customNodeContent
}
export default {
createImgNode,
getImgShowSize,
createIconNode,
createRichTextNode,
createTextNode,
createHyperlinkNode,
createTagNode,
createNoteNode,
measureCustomNodeContentSize,
isUseCustomNodeContent
}
createImgNode,
getImgShowSize,
createIconNode,
createRichTextNode,
createTextNode,
createHyperlinkNode,
createTagNode,
createNoteNode,
measureCustomNodeContentSize,
isUseCustomNodeContent
}

View File

@@ -6,13 +6,27 @@ function createExpandNodeContent() {
if (this._openExpandNode) {
return
}
let { open, close } = this.mindMap.opt.expandBtnIcon || {}
// 展开的节点
this._openExpandNode = SVG(open || btnsSvg.open).size(
this.expandBtnSize,
this.expandBtnSize
)
this._openExpandNode.x(0).y(-this.expandBtnSize / 2)
let { close, open } = this.mindMap.opt.expandBtnIcon || {}
// 根据配置判断是否显示数量按钮
if (this.mindMap.opt.isShowExpandNum) {
// 展开的节点
this._openExpandNode = SVG()
.text()
.size(this.expandBtnSize, this.expandBtnSize)
// 文本垂直居中
this._openExpandNode.attr({
'text-anchor': 'middle',
'dominant-baseline': 'middle',
x: this.expandBtnSize / 2,
y: 2
})
} else {
this._openExpandNode = SVG(open || btnsSvg.open).size(
this.expandBtnSize,
this.expandBtnSize
)
this._openExpandNode.x(0).y(-this.expandBtnSize / 2)
}
// 收起的节点
this._closeExpandNode = SVG(close || btnsSvg.close).size(
this.expandBtnSize,
@@ -22,6 +36,7 @@ function createExpandNodeContent() {
// 填充节点
this._fillExpandNode = new Circle().size(this.expandBtnSize)
this._fillExpandNode.x(0).y(-this.expandBtnSize / 2)
// 设置样式
this.style.iconBtn(
this._openExpandNode,
@@ -29,7 +44,12 @@ function createExpandNodeContent() {
this._fillExpandNode
)
}
function sumNode(data = []) {
return data.reduce(
(total, cur) => total + this.sumNode(cur.children || []),
data.length
)
}
// 创建或更新展开收缩按钮内容
function updateExpandBtnNode() {
let { expand } = this.nodeData.data
@@ -47,7 +67,27 @@ function updateExpandBtnNode() {
node = this._closeExpandNode
this._lastExpandBtnType = true
}
if (this._expandBtn) this._expandBtn.add(this._fillExpandNode).add(node)
if (this._expandBtn) {
// 如果是收起按钮加上边框
let { isShowExpandNum, expandBtnStyle, expandBtnNumHandler } =
this.mindMap.opt
if (isShowExpandNum) {
if (!expand) {
// 数字按钮添加边框
this._fillExpandNode.stroke({
color: expandBtnStyle.strokeColor
})
// 计算子节点数量
let count = this.sumNode(this.nodeData.children)
count = expandBtnNumHandler(count)
node.text(count)
} else {
this._fillExpandNode.stroke('none')
}
}
this._expandBtn.add(this._fillExpandNode).add(node)
}
}
// 更新展开收缩按钮位置
@@ -96,6 +136,7 @@ function renderExpandBtn() {
this._expandBtn.on('dblclick', e => {
e.stopPropagation()
})
this._expandBtn.addClass('smm-expand-btn')
this.group.add(this._expandBtn)
}
this._showExpandBtn = true
@@ -138,5 +179,6 @@ export default {
renderExpandBtn,
removeExpandBtn,
showExpandBtn,
hideExpandBtn
hideExpandBtn,
sumNode
}

View File

@@ -0,0 +1,66 @@
import { Rect } from '@svgdotjs/svg.js'
// 渲染展开收起按钮的隐藏占位元素
function renderExpandBtnPlaceholderRect() {
// 根节点或没有子节点不需要渲染
if (
!this.nodeData.children ||
this.nodeData.children.length <= 0 ||
this.isRoot
) {
return
}
// 默认显示展开按钮的情况下也不需要渲染
if (!this.mindMap.opt.alwaysShowExpandBtn) {
let { width, height } = this
if (!this._unVisibleRectRegionNode) {
this._unVisibleRectRegionNode = new Rect()
this._unVisibleRectRegionNode.fill({
color: 'transparent'
})
}
this.group.add(this._unVisibleRectRegionNode)
this.renderer.layout.renderExpandBtnRect(
this._unVisibleRectRegionNode,
this.expandBtnSize,
width,
height,
this
)
}
}
// 删除展开收起按钮的隐藏占位元素
function clearExpandBtnPlaceholderRect() {
if (!this._unVisibleRectRegionNode) {
return
}
this._unVisibleRectRegionNode.remove()
this._unVisibleRectRegionNode = null
}
// 更新展开收起按钮的隐藏占位元素
function updateExpandBtnPlaceholderRect() {
// 布局改变需要重新渲染
if (this.needRerenderExpandBtnPlaceholderRect) {
this.needRerenderExpandBtnPlaceholderRect = false
this.renderExpandBtnPlaceholderRect()
}
// 没有子节点到有子节点需要渲染
if (this.nodeData.children && this.nodeData.children.length > 0) {
if (!this._unVisibleRectRegionNode) {
this.renderExpandBtnPlaceholderRect()
}
} else {
// 有子节点到没子节点,需要删除
if (this._unVisibleRectRegionNode) {
this.clearExpandBtnPlaceholderRect()
}
}
}
export default {
renderExpandBtnPlaceholderRect,
clearExpandBtnPlaceholderRect,
updateExpandBtnPlaceholderRect
}

View File

@@ -2,12 +2,12 @@ import Node from './Node'
import { createUid } from '../../../utils/index'
// 检查是否存在概要
function checkHasGeneralization () {
function checkHasGeneralization() {
return !!this.nodeData.data.generalization
}
// 创建概要节点
function createGeneralizationNode () {
function createGeneralizationNode() {
if (this.isGeneralization || !this.checkHasGeneralization()) {
return
}
@@ -35,14 +35,14 @@ function createGeneralizationNode () {
}
// 更新概要节点
function updateGeneralization () {
function updateGeneralization() {
if (this.isGeneralization) return
this.removeGeneralization()
this.createGeneralizationNode()
}
// 渲染概要节点
function renderGeneralization () {
function renderGeneralization() {
if (this.isGeneralization) return
if (!this.checkHasGeneralization()) {
this.removeGeneralization()
@@ -65,7 +65,7 @@ function renderGeneralization () {
}
// 删除概要节点
function removeGeneralization () {
function removeGeneralization() {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.remove()
@@ -86,7 +86,7 @@ function removeGeneralization () {
}
// 隐藏概要节点
function hideGeneralization () {
function hideGeneralization() {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.hide()
@@ -97,7 +97,7 @@ function hideGeneralization () {
}
// 显示概要节点
function showGeneralization () {
function showGeneralization() {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.show()
@@ -108,11 +108,11 @@ function showGeneralization () {
}
export default {
checkHasGeneralization,
createGeneralizationNode,
updateGeneralization,
renderGeneralization,
removeGeneralization,
hideGeneralization,
showGeneralization
}
checkHasGeneralization,
createGeneralizationNode,
updateGeneralization,
renderGeneralization,
removeGeneralization,
hideGeneralization,
showGeneralization
}

View File

@@ -32,6 +32,7 @@ class View {
this.fit()
})
this.mindMap.svg.on('dblclick', () => {
if (!this.mindMap.opt.enableDblclickReset) return
this.reset()
})
// 拖动视图
@@ -65,7 +66,8 @@ class View {
mousewheelAction,
mouseScaleCenterUseMousePosition,
mousewheelMoveStep,
mousewheelZoomActionReverse
mousewheelZoomActionReverse,
disableMouseWheelZoom
} = this.mindMap.opt
// 是否自定义鼠标滚轮事件
if (
@@ -76,8 +78,13 @@ class View {
}
// 鼠标滚轮事件控制缩放
if (mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
let cx = mouseScaleCenterUseMousePosition ? e.clientX : undefined
let cy = mouseScaleCenterUseMousePosition ? e.clientY : undefined
if (disableMouseWheelZoom) return
const { x: clientX, y: clientY } = this.mindMap.toPos(
e.clientX,
e.clientY
)
let cx = mouseScaleCenterUseMousePosition ? clientX : undefined
let cy = mouseScaleCenterUseMousePosition ? clientY : undefined
switch (dir) {
// 鼠标滚轮,向上和向左,都是缩小
case CONSTANTS.DIR.UP:
@@ -167,7 +174,6 @@ class View {
// 平移x方式到
translateXTo(x) {
if (x === 0) return
this.x = x
this.transform()
}
@@ -181,7 +187,6 @@ class View {
// 平移y方向到
translateYTo(y) {
if (y === 0) return
this.y = y
this.transform()
}

View File

@@ -46,7 +46,10 @@ class Base {
// 检查当前来源是否需要重新计算节点大小
checkIsNeedResizeSources() {
return [CONSTANTS.CHANGE_THEME, CONSTANTS.TRANSFORM_TO_NORMAL_NODE].includes(this.renderer.renderSource)
return [
CONSTANTS.CHANGE_THEME,
CONSTANTS.TRANSFORM_TO_NORMAL_NODE
].includes(this.renderer.renderSource)
}
// 层级类型改变
@@ -70,7 +73,10 @@ class Base {
// 数据上保存了节点引用,那么直接复用节点
if (data && data._node && !this.renderer.reRender) {
newNode = data._node
let isLayerTypeChange = this.checkIsLayerTypeChange(newNode.layerIndex, layerIndex)
let isLayerTypeChange = this.checkIsLayerTypeChange(
newNode.layerIndex,
layerIndex
)
newNode.reset()
newNode.layerIndex = layerIndex
this.cacheNode(data._node.uid, newNode)
@@ -85,7 +91,10 @@ class Base {
newNode = this.lru.get(data.data.uid)
// 保存该节点上一次的数据
let lastData = JSON.stringify(newNode.nodeData.data)
let isLayerTypeChange = this.checkIsLayerTypeChange(newNode.layerIndex, layerIndex)
let isLayerTypeChange = this.checkIsLayerTypeChange(
newNode.layerIndex,
layerIndex
)
newNode.reset()
newNode.nodeData = newNode.handleData(data || {})
newNode.layerIndex = layerIndex
@@ -120,6 +129,10 @@ class Base {
this.renderer.addActiveNode(newNode)
}
}
// 如果当前节点在激活节点列表里,那么添加上激活的状态
if (this.mindMap.renderer.findActiveNodeIndex(newNode) !== -1) {
newNode.nodeData.data.isActive = true
}
// 根节点
if (isRoot) {
newNode.isRoot = true
@@ -139,7 +152,7 @@ class Base {
} else if (initRootNodePositionMap[value] !== undefined) {
return size * initRootNodePositionMap[value]
} else if (/^\d\d*%$/.test(value)) {
return Number.parseFloat(value) / 100 * size
return (Number.parseFloat(value) / 100) * size
} else {
return (size - nodeSize) / 2
}
@@ -148,12 +161,24 @@ class Base {
// 定位节点到画布中间
setNodeCenter(node) {
let { initRootNodePosition } = this.mindMap.opt
let { CENTER }= CONSTANTS.INIT_ROOT_NODE_POSITION
if (!initRootNodePosition || !Array.isArray(initRootNodePosition) || initRootNodePosition.length < 2) {
let { CENTER } = CONSTANTS.INIT_ROOT_NODE_POSITION
if (
!initRootNodePosition ||
!Array.isArray(initRootNodePosition) ||
initRootNodePosition.length < 2
) {
initRootNodePosition = [CENTER, CENTER]
}
node.left = this.formatPosition(initRootNodePosition[0], this.mindMap.width, node.width)
node.top = this.formatPosition(initRootNodePosition[1], this.mindMap.height, node.height)
node.left = this.formatPosition(
initRootNodePosition[0],
this.mindMap.width,
node.width
)
node.top = this.formatPosition(
initRootNodePosition[1],
this.mindMap.height,
node.height
)
}
// 更新子节点属性
@@ -170,7 +195,7 @@ class Base {
// 更新子节点多个属性
updateChildrenPro(children, props) {
children.forEach(item => {
Object.keys(props).forEach((prop) => {
Object.keys(props).forEach(prop => {
item[prop] += props[prop]
})
if (item.children && item.children.length && !item.hasCustomPosition()) {
@@ -181,9 +206,13 @@ class Base {
}
// 递归计算节点的宽度
getNodeAreaWidth(node) {
getNodeAreaWidth(node, withGeneralization = false) {
let widthArr = []
let totalGeneralizationNodeWidth = 0
let loop = (node, width) => {
if (withGeneralization && node.checkHasGeneralization()) {
totalGeneralizationNodeWidth += node._generalizationNodeWidth
}
if (node.children.length) {
width += node.width / 2
node.children.forEach(item => {
@@ -195,7 +224,7 @@ class Base {
}
}
loop(node, 0)
return Math.max(...widthArr)
return Math.max(...widthArr) + totalGeneralizationNodeWidth
}
// 二次贝塞尔曲线
@@ -216,16 +245,22 @@ class Base {
// 获取节点的marginX
getMarginX(layerIndex) {
const { themeConfig, opt } = this.mindMap
const { second, node } = themeConfig
const hoverRectPadding = opt.hoverRectPadding * 2
return layerIndex === 1
? this.mindMap.themeConfig.second.marginX
: this.mindMap.themeConfig.node.marginX
? second.marginX + hoverRectPadding
: node.marginX + hoverRectPadding
}
// 获取节点的marginY
getMarginY(layerIndex) {
const { themeConfig, opt } = this.mindMap
const { second, node } = themeConfig
const hoverRectPadding = opt.hoverRectPadding * 2
return layerIndex === 1
? this.mindMap.themeConfig.second.marginY
: this.mindMap.themeConfig.node.marginY
? second.marginY + hoverRectPadding
: node.marginY + hoverRectPadding
}
// 获取节点包括概要在内的宽度

View File

@@ -87,11 +87,18 @@ class CatalogOrganization extends Base {
totalLeft += cur.width + marginX
})
} else {
let totalTop = node.top + node.height + marginY + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
let totalTop =
node.top +
this.getNodeHeightWithGeneralization(node) +
marginY +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(cur => {
cur.left = node.left + node.width * 0.5
cur.top = totalTop
totalTop += cur.height + marginY + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
totalTop +=
this.getNodeHeightWithGeneralization(cur) +
marginY +
(this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
})
}
}
@@ -112,7 +119,7 @@ class CatalogOrganization extends Base {
}
// 调整left
if (parent && parent.isRoot) {
let areaWidth = this.getNodeAreaWidth(node)
let areaWidth = this.getNodeAreaWidth(node, true)
let difference = areaWidth - node.width
if (difference > 0) {
this.updateBrothersLeft(node, difference)
@@ -124,7 +131,13 @@ class CatalogOrganization extends Base {
let marginY = this.getMarginY(layerIndex + 1)
let totalHeight =
node.children.reduce((h, item) => {
return h + item.height + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
return (
h +
this.getNodeHeightWithGeneralization(item) +
(this.getNodeActChildrenLength(item) > 0
? item.expandBtnSize
: 0)
)
}, 0) +
len * marginY
this.updateBrothersTop(node, totalHeight)
@@ -134,7 +147,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)
}
},
@@ -147,7 +160,7 @@ class CatalogOrganization extends Base {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition() || _index <= index) {
@@ -170,7 +183,7 @@ class CatalogOrganization extends Base {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {

View File

@@ -56,10 +56,11 @@ class Fishbone extends Base {
}
// 计算二级节点的top值
if (parent._node.isRoot) {
let marginY = this.getMarginY(layerIndex)
if (this.checkIsTop(newNode)) {
newNode.top = parent._node.top - newNode.height
newNode.top = parent._node.top - newNode.height - marginY
} else {
newNode.top = parent._node.top + parent._node.height
newNode.top = parent._node.top + parent._node.height + marginY
}
}
}
@@ -80,15 +81,16 @@ class Fishbone extends Base {
null,
(node, parent, isRoot, layerIndex) => {
if (node.isRoot) {
let topTotalLeft = node.left + node.width + node.height
let bottomTotalLeft = node.left + node.width + node.height
let marginX = this.getMarginX(layerIndex + 1)
let topTotalLeft = node.left + node.width + node.height + marginX
let bottomTotalLeft = node.left + node.width + node.height + marginX
node.children.forEach(item => {
if (this.checkIsTop(item)) {
item.left = topTotalLeft
topTotalLeft += item.width
topTotalLeft += item.width + marginX
} else {
item.left = bottomTotalLeft + 20
bottomTotalLeft += item.width
bottomTotalLeft += item.width + marginX
}
})
}
@@ -154,9 +156,11 @@ class Fishbone extends Base {
getNodeAreaHeight(node) {
let totalHeight = 0
let loop = node => {
let marginY = this.getMarginY(node.layerIndex)
totalHeight +=
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
marginY
if (node.children.length) {
node.children.forEach(item => {
loop(item)
@@ -190,7 +194,7 @@ class Fishbone extends Base {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
@@ -244,8 +248,9 @@ class Fishbone extends Base {
maxx = item.left
}
// 水平线段到二级节点的连线
let marginY = this.getMarginY(item.layerIndex)
let nodeLineX = item.left
let offset = node.height / 2
let offset = node.height / 2 + marginY
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
let line = this.draw.path()
if (this.checkIsTop(item)) {
@@ -267,7 +272,7 @@ class Fishbone extends Base {
})
// 从根节点出发的水平线
let nodeHalfTop = node.top + node.height / 2
let offset = node.height / 2
let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1)
let line = this.draw.path()
line.plot(
`M ${node.left + node.width},${nodeHalfTop} L ${

View File

@@ -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
@@ -237,7 +238,7 @@ class Fishbone extends Base {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
@@ -312,7 +313,9 @@ class Fishbone extends Base {
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

@@ -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
// 同步更新后代节点
@@ -206,7 +207,7 @@ class Fishbone extends Base {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
@@ -285,14 +286,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

@@ -56,6 +56,15 @@ class LogicalStructure extends Base {
}, 0) +
(len + 1) * this.getMarginY(layerIndex + 1)
: 0
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeHeight = cur._node.checkHasGeneralization()
? cur._node._generalizationNodeHeight +
this.getMarginY(layerIndex + 1)
: 0
cur._node.childrenAreaHeight2 = Math.max(
cur._node.childrenAreaHeight,
generalizationNodeHeight
)
},
true,
0
@@ -99,7 +108,7 @@ class LogicalStructure extends Base {
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaHeight -
node.childrenAreaHeight2 -
this.getMarginY(layerIndex + 1) * 2 -
node.height
if (difference > 0) {
@@ -116,10 +125,10 @@ class LogicalStructure extends Base {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
childrenList.forEach((item, _index) => {
if (item === node || item.hasCustomPosition()) {
if (item.uid === node.uid || item.hasCustomPosition()) {
// 适配自定义位置
return
}

View File

@@ -90,6 +90,20 @@ class MindMap extends Base {
cur._node.rightChildrenAreaHeight =
rightChildrenAreaHeight +
(rightLen + 1) * this.getMarginY(layerIndex + 1)
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeHeight = cur._node.checkHasGeneralization()
? cur._node._generalizationNodeHeight +
this.getMarginY(layerIndex + 1)
: 0
cur._node.leftChildrenAreaHeight2 = Math.max(
cur._node.leftChildrenAreaHeight,
generalizationNodeHeight
)
cur._node.rightChildrenAreaHeight2 = Math.max(
cur._node.rightChildrenAreaHeight,
generalizationNodeHeight
)
},
true,
0
@@ -139,8 +153,8 @@ class MindMap extends Base {
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let base = this.getMarginY(layerIndex + 1) * 2 + node.height
let leftDifference = node.leftChildrenAreaHeight - base
let rightDifference = node.rightChildrenAreaHeight - base
let leftDifference = node.leftChildrenAreaHeight2 - base
let rightDifference = node.rightChildrenAreaHeight2 - base
if (leftDifference > 0 || rightDifference > 0) {
this.updateBrothers(node, leftDifference / 2, rightDifference / 2)
}
@@ -158,7 +172,7 @@ class MindMap extends Base {
return item.dir === node.dir
})
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {

View File

@@ -57,6 +57,15 @@ class OrganizationStructure extends Base {
}, 0) +
(len + 1) * this.getMarginY(layerIndex + 1)
: 0
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeWidth = cur._node.checkHasGeneralization()
? cur._node._generalizationNodeWidth + this.getMarginY(layerIndex + 1)
: 0
cur._node.childrenAreaWidth2 = Math.max(
cur._node.childrenAreaWidth,
generalizationNodeWidth
)
},
true,
0
@@ -100,7 +109,7 @@ class OrganizationStructure extends Base {
}
// 判断子节点所占的宽度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaWidth -
node.childrenAreaWidth2 -
this.getMarginY(layerIndex + 1) * 2 -
node.width
if (difference > 0) {
@@ -117,7 +126,7 @@ class OrganizationStructure extends Base {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {

View File

@@ -209,7 +209,7 @@ class Timeline extends Base {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {

View File

@@ -156,13 +156,13 @@ class VerticalTimeline extends Base {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
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) {
@@ -202,7 +202,7 @@ class VerticalTimeline extends Base {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
return item.uid === node.uid
})
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {

View File

@@ -47,30 +47,35 @@ export default {
computedLeftTopValue({ layerIndex, node, ctx }) {
if (layerIndex >= 1 && node.children) {
// 遍历三级及以下节点的子节点
let marginY = ctx.getMarginY(layerIndex + 1)
let startLeft = node.left + node.width * ctx.childIndent
let totalTop =
node.top +
node.height +
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
marginY
node.children.forEach(item => {
item.left = startLeft
item.top += totalTop
totalTop +=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
marginY
})
}
},
adjustLeftTopValueBefore({ node, parent, ctx }) {
adjustLeftTopValueBefore({ node, parent, ctx, layerIndex }) {
// 调整top
let len = node.children.length
let marginY = ctx.getMarginY(layerIndex + 1)
// 调整三级及以下节点的top
if (parent && !parent.isRoot && len > 0) {
let totalHeight = node.children.reduce((h, item) => {
return (
h +
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
marginY
)
}, 0)
ctx.updateBrothersTop(node, totalHeight)
@@ -80,7 +85,8 @@ export default {
// 将二级节点的子节点移到上方
if (parent && parent.isRoot) {
// 遍历二级节点的子节点
let totalHeight = node.expandBtnSize
let marginY = ctx.getMarginY(node.layerIndex + 1)
let totalHeight = node.expandBtnSize + marginY
node.children.forEach(item => {
// 调整top
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
@@ -89,7 +95,11 @@ export default {
item.top =
node.top - (item.top - node.top) - nodeTotalHeight + node.height
// 调整left
item.left = node.left + node.width * ctx.indent + (nodeTotalHeight + totalHeight) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
item.left =
node.left +
node.width * ctx.indent +
(nodeTotalHeight + totalHeight) /
Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
totalHeight += nodeTotalHeight
// 同步更新后代节点
ctx.updateChildrenPro(item.children, {
@@ -126,7 +136,9 @@ export default {
if (node.parent && node.parent.isRoot) {
line.plot(
`M ${x},${top + height} L ${x + lineLength},${
top + height + Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
top +
height +
Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
}`
)
} else {
@@ -134,13 +146,15 @@ export default {
}
},
computedLeftTopValue({ layerIndex, node, ctx }) {
let marginY = ctx.getMarginY(layerIndex + 1)
if (layerIndex === 1 && node.children) {
// 遍历二级节点的子节点
let startLeft = node.left + node.width * ctx.childIndent
let totalTop =
node.top +
node.height +
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
marginY
node.children.forEach(item => {
item.left = startLeft
@@ -149,7 +163,8 @@ export default {
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
totalTop +=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
marginY
})
}
if (layerIndex > 1 && node.children) {
@@ -157,25 +172,29 @@ export default {
let startLeft = node.left + node.width * ctx.childIndent
let totalTop =
node.top -
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) -
marginY
node.children.forEach(item => {
item.left = startLeft
item.top = totalTop - item.height
totalTop -=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
marginY
})
}
},
adjustLeftTopValueBefore({ node, ctx, layerIndex }) {
// 调整top
let marginY = ctx.getMarginY(layerIndex + 1)
let len = node.children.length
if (layerIndex > 2 && len > 0) {
let totalHeight = node.children.reduce((h, item) => {
return (
h +
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) +
marginY
)
}, 0)
ctx.updateBrothersTop(node, -totalHeight)
@@ -185,23 +204,28 @@ export default {
// 将二级节点的子节点移到上方
if (parent && parent.isRoot) {
// 遍历二级节点的子节点
let marginY = ctx.getMarginY(node.layerIndex + 1)
let totalHeight = 0
let totalHeight2 = node.expandBtnSize
node.children.forEach(item => {
// 调整top
let hasChildren = ctx.getNodeActChildrenLength(item) > 0
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
let offset =
hasChildren > 0
? nodeTotalHeight -
item.height -
(hasChildren ? item.expandBtnSize : 0)
: 0
let offset = hasChildren
? nodeTotalHeight -
item.height -
(hasChildren ? item.expandBtnSize : 0)
: 0
offset -= hasChildren ? marginY : 0
let _top = totalHeight + offset
let _left = item.left
item.top += _top
// 调整left
item.left = node.left + node.width * ctx.indent + (nodeTotalHeight + totalHeight2) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
item.left =
node.left +
node.width * ctx.indent +
(nodeTotalHeight + totalHeight2) /
Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
totalHeight += offset
totalHeight2 += nodeTotalHeight
// 同步更新后代节点

View File

@@ -28,7 +28,7 @@ const handleList = node => {
}
// 将markdown转换成节点树
export const transformMarkdownTo = async md => {
export const transformMarkdownTo = md => {
const tree = fromMarkdown(md)
let root = {
children: []

View File

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

View File

@@ -4,7 +4,8 @@ import {
getTextFromHtml,
imgToDataUrl,
parseDataUrl,
getImageSize
getImageSize,
isUndef
} from '../utils/index'
// 解析.xmind文件
@@ -14,11 +15,13 @@ const parseXmindFile = file => {
async zip => {
try {
let content = ''
if (zip.files['content.json']) {
let json = await zip.files['content.json'].async('string')
let jsonFile = zip.files['content.json']
let xmlFile = zip.files['content.xml'] || zip.files['/content.xml']
if (jsonFile) {
let json = await jsonFile.async('string')
content = await transformXmind(json, zip.files)
} else if (zip.files['content.xml']) {
let xml = await zip.files['content.xml'].async('string')
} else if (xmlFile) {
let xml = await xmlFile.async('string')
let json = xmlConvert.xml2json(xml)
content = transformOldXmind(json)
}
@@ -47,7 +50,7 @@ const transformXmind = async (content, files) => {
let walk = async (node, newNode) => {
newNode.data = {
// 节点内容
text: node.title
text: isUndef(node.title) ? '' : node.title
}
// 节点备注
if (node.notes) {
@@ -64,13 +67,13 @@ const transformXmind = async (content, files) => {
}
// 图片
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 resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
// 读取图片
let imageType = /\.([^.]+)$/.exec(node.image.src)[1]
let imageBase64 =
@@ -144,9 +147,10 @@ const transformOldXmind = content => {
let walk = (node, newNode) => {
let nodeElements = node.elements
let nodeTitle = getItemByName(nodeElements, 'title')
nodeTitle = nodeTitle && nodeTitle.elements && nodeTitle.elements[0].text
newNode.data = {
// 节点内容
text: nodeTitle && nodeTitle.elements && nodeTitle.elements[0].text
text: isUndef(nodeTitle) ? '' : nodeTitle
}
try {
// 节点备注
@@ -187,7 +191,7 @@ const transformOldXmind = content => {
if (_children && _children.elements && _children.elements.length > 0) {
_children.elements.forEach(item => {
if (item.name === 'topics') {
item.elements.forEach(item2 => {
;(item.elements || []).forEach(item2 => {
let newChild = {}
newNode.children.push(newChild)
walk(item2, newChild)
@@ -213,6 +217,7 @@ const transformToXmind = async (data, name) => {
let waitLoadImageList = []
let walk = async (node, newNode, isRoot) => {
let newData = {
id: node.data.uid,
structureClass: 'org.xmind.ui.logic.right',
title: getTextFromHtml(node.data.text), // 节点文本
children: {
@@ -240,20 +245,20 @@ const transformToXmind = async (data, name) => {
}
// 图片
if (node.data.image) {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
try {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
let imgName = ''
let imgData = node.data.image
// 网络图片要先转换成data:url
if (/^https?:\/\//.test(node.data.image)) {
// base64之外的其他图片要先转换成data:url
if (!/^data:/.test(node.data.image)) {
imgData = await imgToDataUrl(node.data.image)
}
// 从data:url中解析出图片类型和base64
// 从data:url中解析出图片类型和ase64
let dataUrlRes = parseDataUrl(imgData)
imgName = 'image_' + imageList.length + '.' + dataUrlRes.type
imageList.push({

View File

@@ -99,13 +99,34 @@ class AssociativeLine {
// 创建箭头
createMarker() {
return this.draw.marker(20, 20, add => {
add.ref(2, 5)
add.ref(12, 5)
add.size(10, 10)
add.attr('orient', 'auto-start-reverse')
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
})
}
// 判断关联线坐标是否变更,有变更则使用变化后的坐标,无则默认坐标
updateAllLinesPos(node, toNode, associativeLinePoint) {
associativeLinePoint = associativeLinePoint || {}
let [startPoint, endPoint] = computeNodePoints(node, toNode)
let nodeRange = 0
let nodeDir = ''
let toNodeRange = 0
let toNodeDir = ''
if (associativeLinePoint.startPoint) {
nodeRange = associativeLinePoint.startPoint.range || 0
nodeDir = associativeLinePoint.startPoint.dir || 'right'
startPoint = getNodePoint(node, nodeDir, nodeRange)
}
if (associativeLinePoint.endPoint) {
toNodeRange = associativeLinePoint.endPoint.range || 0
toNodeDir = associativeLinePoint.endPoint.dir || 'right'
endPoint = getNodePoint(toNode, toNodeDir, toNodeRange)
}
return [startPoint, endPoint]
}
// 渲染所有连线
renderAllLines() {
// 先移除
@@ -128,8 +149,8 @@ class AssociativeLine {
) {
nodeToIds.set(cur, data.associativeLineTargets)
}
if (data.id) {
idToNode.set(data.id, cur)
if (data.uid) {
idToNode.set(data.uid, cur)
}
},
() => {},
@@ -137,10 +158,17 @@ class AssociativeLine {
0
)
nodeToIds.forEach((ids, node) => {
ids.forEach(id => {
let toNode = idToNode.get(id)
ids.forEach((uid, index) => {
let toNode = idToNode.get(uid)
if (!node || !toNode) return
let [startPoint, endPoint] = computeNodePoints(node, toNode)
const associativeLinePoint = (node.nodeData.data.associativeLinePoint ||
[])[index]
// 切换结构和布局,都会更新坐标
const [startPoint, endPoint] = this.updateAllLinesPos(
node,
toNode,
associativeLinePoint
)
this.drawLine(startPoint, endPoint, node, toNode)
})
})
@@ -183,11 +211,33 @@ class AssociativeLine {
.fill({ color: 'none' })
clickPath.plot(pathStr)
// 文字
let text = this.createText({ path, clickPath, node, toNode, startPoint, endPoint, controlPoints })
let text = this.createText({
path,
clickPath,
node,
toNode,
startPoint,
endPoint,
controlPoints
})
// 点击事件
clickPath.click(e => {
e.stopPropagation()
this.setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints })
this.setActiveLine({
path,
clickPath,
text,
node,
toNode,
startPoint,
endPoint,
controlPoints
})
})
// 双击进入关联线文本编辑状态
clickPath.dblclick(() => {
if (!this.activeLine) return
this.showEditTextBox(text)
})
// 渲染关联线文字
this.renderText(this.getText(node, toNode), path, text)
@@ -195,33 +245,37 @@ class AssociativeLine {
}
// 激活某根关联线
setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints }) {
let {
associativeLineActiveColor
} = this.mindMap.themeConfig
setActiveLine({
path,
clickPath,
text,
node,
toNode,
startPoint,
endPoint,
controlPoints
}) {
let { associativeLineActiveColor } = this.mindMap.themeConfig
// 如果当前存在激活节点,那么取消激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.clearActiveNodes()
} else {
// 否则清除当前的关联线的激活状态,如果有的话
this.clearActiveLine()
// 保存当前激活的关联线信息
this.activeLine = [path, clickPath, text, node, toNode]
// 让不可见的点击线显示
clickPath.stroke({ color: associativeLineActiveColor })
// 如果没有输入过关联线文字,那么显示默认文字
if (!this.getText(node, toNode)) {
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text)
}
// 渲染控制点和连线
this.renderControls(
startPoint,
endPoint,
controlPoints[0],
controlPoints[1]
)
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
// 否则清除当前的关联线的激活状态,如果有的话
this.clearActiveLine()
// 保存当前激活的关联线信息
this.activeLine = [path, clickPath, text, node, toNode]
// 让不可见的点击线显示
clickPath.stroke({ color: associativeLineActiveColor })
// 如果没有输入过关联线文字,那么显示默认文字
if (!this.getText(node, toNode)) {
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text)
}
// 渲染控制点和连线
this.renderControls(
startPoint,
endPoint,
controlPoints[0],
controlPoints[1]
)
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
}
// 移除所有连接线
@@ -287,6 +341,22 @@ class AssociativeLine {
}
}
// 计算节点偏移位置
getNodePos(node) {
const { scaleX, scaleY, translateX, translateY } =
this.mindMap.draw.transform()
const { left, top, width, height } = node
let translateLeft = left * scaleX + translateX
let translateTop = top * scaleY + translateY
return {
left,
top,
translateLeft,
translateTop,
width,
height
}
}
// 检测当前移动到的目标节点
checkOverlapNode(x, y) {
this.overlapNode = null
@@ -294,7 +364,7 @@ class AssociativeLine {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
}
if (node === this.creatingStartNode || this.overlapNode) {
if (node.uid === this.creatingStartNode.uid || this.overlapNode) {
return
}
let { left, top, width, height } = node
@@ -311,7 +381,7 @@ class AssociativeLine {
// 完成创建连接线
completeCreateLine(node) {
if (this.creatingStartNode === node) return
if (this.creatingStartNode.uid === node.uid) return
this.addLine(this.creatingStartNode, node)
if (this.overlapNode && this.overlapNode.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
@@ -327,16 +397,21 @@ class AssociativeLine {
addLine(fromNode, toNode) {
if (!fromNode || !toNode) return
// 目标节点如果没有id则生成一个id
let id = toNode.nodeData.data.id
if (!id) {
id = uuid()
let uid = toNode.nodeData.data.uid
if (!uid) {
uid = uuid()
this.mindMap.execCommand('SET_NODE_DATA', toNode, {
id
uid
})
}
// 将目标节点id保存起来
let list = fromNode.nodeData.data.associativeLineTargets || []
list.push(id)
// 连线节点是否存在相同的id,存在则阻止添加关联线
const sameLine = list.some(item => item === uid)
if (sameLine) {
return
}
list.push(uid)
// 保存控制点
let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
let controlPoints = computeCubicBezierPathPoints(
@@ -358,9 +433,13 @@ class AssociativeLine {
y: controlPoints[1].y - endPoint.y
}
]
let associativeLinePoint = fromNode.nodeData.data.associativeLinePoint || []
// 记录关联的起始|结束坐标
associativeLinePoint[list.length - 1] = { startPoint, endPoint }
this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
associativeLineTargets: list,
associativeLineTargetControlOffsets: offsetList
associativeLineTargetControlOffsets: offsetList,
associativeLinePoint
})
}
@@ -369,14 +448,19 @@ class AssociativeLine {
if (!this.activeLine) return
let [, , , node, toNode] = this.activeLine
this.removeControls()
let { associativeLineTargets, associativeLineTargetControlOffsets, associativeLineText } =
node.nodeData.data
let {
associativeLineTargets,
associativeLinePoint,
associativeLineTargetControlOffsets,
associativeLineText
} = node.nodeData.data
associativeLinePoint = associativeLinePoint || []
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
// 更新关联线文本数据
let newAssociativeLineText = {}
if (associativeLineText) {
Object.keys(associativeLineText).forEach((item) => {
if (item !== toNode.nodeData.data.id) {
Object.keys(associativeLineText).forEach(item => {
if (item !== toNode.nodeData.data.uid) {
newAssociativeLineText[item] = associativeLineText[item]
}
})
@@ -386,6 +470,10 @@ class AssociativeLine {
associativeLineTargets: associativeLineTargets.filter((_, index) => {
return index !== targetIndex
}),
// 连接线坐标
associativeLinePoint: associativeLinePoint.filter((_, index) => {
return index !== targetIndex
}),
// 偏移量
associativeLineTargetControlOffsets: associativeLineTargetControlOffsets
? associativeLineTargetControlOffsets.filter((_, index) => {
@@ -397,13 +485,6 @@ class AssociativeLine {
})
}
// 清除当前激活的节点
clearActiveNodes() {
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
}
// 清除激活的线
clearActiveLine() {
if (this.activeLine) {

View File

@@ -0,0 +1,322 @@
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.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.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('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
})
})
}
}
// 设置用户信息
/**
* {
* 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)
if (node) {
callback(node, userInfo)
}
})
})
}
// 清除之前的数据
walk(this.currentAwarenessData, (node, userInfo) => {
node.removeUser(userInfo)
})
// 设置当前数据
const data = Array.from(this.awareness.getStates().values())
this.currentAwarenessData = data
walk(data, (node, userInfo) => {
// 不显示自己
if (userInfo.id === this.userInfo.id) return
node.addUser(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) {
if (map[parentUid]) {
map[parentUid].children.push(node)
} else {
map[parentUid] = {
data: simpleDeepClone(data[parentUid].data),
children: [node]
}
}
}
})
return res
}
// 插件被移除前做的事情
beforePluginRemove() {
this.unBindEvent()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
Cooperate.instanceName = 'cooperate'
export default Cooperate

View File

@@ -1,4 +1,4 @@
import { bfsWalk, throttle } from '../utils'
import { bfsWalk, throttle, getTopAncestorsFomNodeList } from '../utils'
import Base from '../layouts/Base'
// 节点拖动插件
@@ -13,8 +13,14 @@ class Drag extends Base {
// 复位
reset() {
// 当前拖拽节点
this.node = null
// 是否正在跳转中
this.isDragging = false
// 鼠标按下的节点
this.mousedownNode = null
// 被拖拽中的节点列表
this.beingDragNodeList = []
// 当前画布节点列表
this.nodeList = []
// 当前重叠节点
this.overlapNode = null
// 当前上一个同级节点
@@ -25,16 +31,11 @@ class Drag extends Base {
this.drawTransform = null
// 克隆节点
this.clone = null
// 连接线
this.line = null
// 同级位置占位符
this.placeholder = null
// 鼠标按下位置和节点左上角的偏移量
this.offsetX = 0
this.offsetY = 0
// 克隆节点左上角的坐标
this.cloneNodeLeft = 0
this.cloneNodeTop = 0
// 当前鼠标是否按下
this.isMousedown = false
// 拖拽的鼠标位置变量
@@ -51,45 +52,43 @@ 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) {
// 只读模式、不是鼠标左键按下、按下的是概要节点或根节点直接返回
if (
this.mindMap.opt.readonly ||
e.which !== 1 ||
node.isGeneralization ||
node.isRoot
) {
return
}
e.preventDefault()
// 计算鼠标按下的位置距离节点左上角的距离
this.drawTransform = this.mindMap.draw.transform()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.offsetX = x - (node.left * scaleX + translateX)
this.offsetY = y - (node.top * scaleY + translateY)
this.node = node
this.isMousedown = true
// 记录鼠标按下时的节点
this.mousedownNode = node
// 记录鼠标按下的坐标
const { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseDownX = x
this.mouseDownY = y
})
this.mindMap.on('mousemove', e => {
if (this.mindMap.opt.readonly) {
if (this.mindMap.opt.readonly || !this.isMousedown) {
return
}
if (!this.isMousedown) {
return
}
this.mindMap.emit('node_dragging', this.node)
e.preventDefault()
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
const { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseMoveX = x
this.mouseMoveY = y
// 还没开始移动时鼠标位移过小不认为是拖拽
if (
!this.isDragging &&
Math.abs(x - this.mouseDownX) <= this.checkDragOffset &&
Math.abs(y - this.mouseDownY) <= this.checkDragOffset &&
!this.node.isDrag
Math.abs(y - this.mouseDownY) <= this.checkDragOffset
) {
return
}
this.mindMap.renderer.clearAllActive()
this.onMove(x, y)
this.mindMap.emit('node_dragging')
this.handleStartMove()
this.onMove(x, y, e)
})
this.onMouseup = this.onMouseup.bind(this)
this.mindMap.on('node_mouseup', this.onMouseup)
@@ -102,27 +101,48 @@ class Drag extends Base {
return
}
this.isMousedown = false
let _nodeIsDrag = this.node.isDrag
this.node.isDrag = false
this.node.show()
// 恢复被拖拽节点的临时设置
this.beingDragNodeList.forEach(node => {
node.setOpacity(1)
node.showChildren()
node.endDrag()
})
this.removeCloneNode()
let overlapNodeUid = this.overlapNode ? this.overlapNode.nodeData.data.uid : ''
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 : ''
// 存在重叠子节点,则移动作为其子节点
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
this.mindMap.execCommand('MOVE_NODE_TO', this.node, this.overlapNode)
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(
'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(
'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
@@ -130,11 +150,16 @@ class Drag extends Base {
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
x = (x - translateX) / scaleX
y = (y - translateY) / scaleY
this.node.left = x
this.node.top = y
this.node.customLeft = x
this.node.customTop = y
this.mindMap.execCommand('SET_NODE_CUSTOM_POSITION', this.node, x, y)
this.mousedownNode.left = x
this.mousedownNode.top = y
this.mousedownNode.customLeft = x
this.mousedownNode.customTop = y
this.mindMap.execCommand(
'SET_NODE_CUSTOM_POSITION',
this.mousedownNode,
x,
y
)
this.mindMap.render()
}
this.reset()
@@ -145,24 +170,131 @@ class Drag extends Base {
})
}
// 拖动中
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.nodeData.data.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.renderer.clearAllActive()
}
}
// 节点由树转换成数组,从子节点到根节点
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.draw
.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.draw.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)
color: dragPlaceholderRectFill || lineColor
})
// 当前被拖拽的节点的临时设置
this.beingDragNodeList.forEach(node => {
// 降低透明度
node.setOpacity(dragOpacityConfig.beingDragNodeOpacity)
// 隐藏连线及下级节点
node.hideChildren()
// 设置拖拽状态
node.startDrag()
})
this.mindMap.draw.add(this.clone)
}
}
@@ -172,125 +304,52 @@ class Drag extends Base {
return
}
this.clone.remove()
this.line.remove()
this.placeholder.remove()
}
// 拖动中
onMove(x, y) {
if (!this.isMousedown) {
return
}
this.createCloneNode()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
this.cloneNodeLeft = x - this.offsetX
this.cloneNodeTop = y - this.offsetY
x = (this.cloneNodeLeft - translateX) / scaleX
y = (this.cloneNodeTop - translateY) / scaleY
let t = this.clone.transform()
this.clone.translate(x - t.translateX, y - t.translateY)
// 连接线
let parent = this.node.parent
this.line.plot(
this.quadraticCurvePath(
parent.left + parent.width / 2,
parent.top + parent.height / 2,
x + this.node.width / 2,
y + this.node.height / 2
)
)
this.checkOverlapNode()
}
// 检测重叠节点
checkOverlapNode() {
if (!this.drawTransform || !this.placeholder) {
return
}
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt
let x = this.mouseMoveX
let y = this.mouseMoveY
this.overlapNode = null
this.prevNode = null
this.nextNode = null
this.placeholder.size(0, 0)
bfsWalk(this.mindMap.renderer.root, node => {
this.nodeList.forEach(node => {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
}
if (node === this.node || this.node.isParent(node)) {
return
}
if (this.overlapNode || (this.prevNode && this.nextNode)) {
return
}
let nodeRect = this.getNodeRect(node)
let oneFourthHeight = nodeRect.height / 4
// 前一个和后一个节点
let checkList = node.parent ? node.parent.children.filter((item) => {
return item !== this.node
}) : []
let index = checkList.findIndex((item) => {
return item === node
})
let prevBrother = null
let nextBrother = null
if (index !== -1) {
if (index - 1 >= 0) {
prevBrother = checkList[index - 1]
}
if (index + 1 <= checkList.length - 1) {
nextBrother = checkList[index + 1]
}
}
// 和前一个兄弟节点的距离
let prevBrotherOffset = 0
if (prevBrother) {
let prevNodeRect = this.getNodeRect(prevBrother)
prevBrotherOffset = nodeRect.top - prevNodeRect.bottom
// 间距小于10就当它不存在
prevBrotherOffset = prevBrotherOffset >= this.minOffset ? prevBrotherOffset / 2 : 0
} else {
// 没有前一个兄弟节点那么假设和前一个节点的距离为20
prevBrotherOffset = this.minOffset
}
// 和后一个兄弟节点的距离
let nextBrotherOffset = 0
if (nextBrother) {
let nextNodeRect = this.getNodeRect(nextBrother)
nextBrotherOffset = nextNodeRect.top - nodeRect.bottom
nextBrotherOffset = nextBrotherOffset >= this.minOffset ? nextBrotherOffset / 2 : 0
} else {
nextBrotherOffset = this.minOffset
}
if (nodeRect.left <= x && nodeRect.right >= x) {
// 检测兄弟节点位置
if (!this.overlapNode && !this.prevNode && !this.nextNode && !node.isRoot) {
let checkIsPrevNode = nextBrotherOffset > 0 ? // 距离下一个兄弟节点的距离大于0
y > nodeRect.bottom && y <= (nodeRect.bottom + nextBrotherOffset) : // 那么在当前节点外底部判断
y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断
let checkIsNextNode = prevBrotherOffset > 0 ? // 距离上一个兄弟节点的距离大于0
y < nodeRect.top && y >= (nodeRect.top - prevBrotherOffset) : // 那么在当前节点外底部判断
y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
if (checkIsPrevNode) {
this.prevNode = node
let size = nextBrotherOffset > 0 ? Math.min(nextBrotherOffset, nodeDragPlaceholderMaxSize) : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom)
} else if (checkIsNextNode) {
this.nextNode = node
let size = prevBrotherOffset > 0 ? Math.min(prevBrotherOffset, nodeDragPlaceholderMaxSize) : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originTop - size)
}
}
// 检测是否重叠
if (!this.overlapNode && !this.prevNode && !this.nextNode) {
if (
nodeRect.top + (prevBrotherOffset > 0 ? 0 : oneFourthHeight) <= y &&
nodeRect.bottom - (nextBrotherOffset > 0 ? 0 : oneFourthHeight) >= y
) {
this.overlapNode = node
}
}
switch (this.mindMap.opt.layout) {
case 'logicalStructure':
this.handleLogicalStructure(node)
break
case 'mindMap':
this.handleMindMap(node)
break
case 'organizationStructure':
this.handleOrganizationStructure(node)
break
case 'catalogOrganization':
this.handleCatalogOrganization(node)
break
case 'timeline':
this.handleTimeLine(node)
break
case 'timeline2':
this.handleTimeLine2(node)
break
case 'verticalTimeline':
this.handleLogicalStructure(node)
break
case 'fishbone':
this.handleFishbone(node)
break
default:
this.handleLogicalStructure(node)
}
})
if (this.overlapNode) {
@@ -298,6 +357,299 @@ class Drag extends Base {
}
}
// 垂直方向比较
// isReverse是否反向
handleVerticalCheck(node, checkList, isReverse = false) {
let x = this.mouseMoveX
let y = this.mouseMoveY
let nodeRect = this.getNodeRect(node)
if (isReverse) {
checkList = checkList.reverse()
}
let oneFourthHeight = nodeRect.height / 4
let { prevBrotherOffset, nextBrotherOffset } =
this.getNodeDistanceToSiblingNode(checkList, node, nodeRect, 'v')
if (nodeRect.left <= x && nodeRect.right >= x) {
// 检测兄弟节点位置
if (
!this.overlapNode &&
!this.prevNode &&
!this.nextNode &&
!node.isRoot
) {
let checkIsPrevNode =
nextBrotherOffset > 0 // 距离下一个兄弟节点的距离大于0
? y > nodeRect.bottom && y <= nodeRect.bottom + nextBrotherOffset // 那么在当前节点外底部判断
: y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断
let checkIsNextNode =
prevBrotherOffset > 0 // 距离上一个兄弟节点的距离大于0
? y < nodeRect.top && y >= nodeRect.top - prevBrotherOffset // 那么在当前节点外底部判断
: y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
if (checkIsPrevNode) {
if (isReverse) {
this.nextNode = node
} else {
this.prevNode = node
}
let size = this.formatPlaceholderSize(nextBrotherOffset)
this.setPlaceholderRect(
node.width,
size,
nodeRect.originLeft,
nodeRect.originBottom
)
} else if (checkIsNextNode) {
if (isReverse) {
this.prevNode = node
} else {
this.nextNode = node
}
let size = this.formatPlaceholderSize(prevBrotherOffset)
this.setPlaceholderRect(
node.width,
size,
nodeRect.originLeft,
nodeRect.originTop - size
)
}
}
// 检测是否重叠
this.checkIsOverlap({
node,
dir: 'v',
prevBrotherOffset,
nextBrotherOffset,
size: oneFourthHeight,
pos: y,
nodeRect
})
}
}
// 水平方向比较
handleHorizontalCheck(node, checkList) {
let x = this.mouseMoveX
let y = this.mouseMoveY
let nodeRect = this.getNodeRect(node)
let oneFourthWidth = nodeRect.width / 4
let { prevBrotherOffset, nextBrotherOffset } =
this.getNodeDistanceToSiblingNode(checkList, node, nodeRect, 'h')
if (nodeRect.top <= y && nodeRect.bottom >= y) {
// 检测兄弟节点位置
if (
!this.overlapNode &&
!this.prevNode &&
!this.nextNode &&
!node.isRoot
) {
let checkIsPrevNode =
nextBrotherOffset > 0 // 距离下一个兄弟节点的距离大于0
? x < nodeRect.right + nextBrotherOffset && x >= nodeRect.right // 那么在当前节点外底部判断
: x <= nodeRect.right && x >= nodeRect.right - oneFourthWidth // 否则在当前节点内底部1/4区间判断
let checkIsNextNode =
prevBrotherOffset > 0 // 距离上一个兄弟节点的距离大于0
? x > nodeRect.left - prevBrotherOffset && x <= nodeRect.left // 那么在当前节点外底部判断
: x <= nodeRect.left + oneFourthWidth && x >= nodeRect.left
if (checkIsPrevNode) {
this.prevNode = node
let size = this.formatPlaceholderSize(nextBrotherOffset)
this.setPlaceholderRect(
size,
node.height,
nodeRect.originRight,
nodeRect.originTop
)
} else if (checkIsNextNode) {
this.nextNode = node
let size = this.formatPlaceholderSize(prevBrotherOffset)
this.setPlaceholderRect(
size,
node.height,
nodeRect.originLeft - size,
nodeRect.originTop
)
}
}
// 检测是否重叠
this.checkIsOverlap({
node,
dir: 'h',
prevBrotherOffset,
nextBrotherOffset,
size: oneFourthWidth,
pos: x,
nodeRect
})
}
}
// 获取节点距前一个和后一个节点的距离
getNodeDistanceToSiblingNode(checkList, node, nodeRect, dir) {
let dir1 = dir === 'v' ? 'top' : 'left'
let dir2 = dir === 'v' ? 'bottom' : 'right'
let index = checkList.findIndex(item => {
return item.uid === node.uid
})
let prevBrother = null
let nextBrother = null
if (index !== -1) {
if (index - 1 >= 0) {
prevBrother = checkList[index - 1]
}
if (index + 1 <= checkList.length - 1) {
nextBrother = checkList[index + 1]
}
}
// 和前一个兄弟节点的距离
let prevBrotherOffset = 0
if (prevBrother) {
let prevNodeRect = this.getNodeRect(prevBrother)
prevBrotherOffset = nodeRect[dir1] - prevNodeRect[dir2]
// 间距小于10就当它不存在
prevBrotherOffset =
prevBrotherOffset >= this.minOffset ? prevBrotherOffset / 2 : 0
} else {
// 没有前一个兄弟节点那么假设和前一个节点的距离为20
prevBrotherOffset = this.minOffset
}
// 和后一个兄弟节点的距离
let nextBrotherOffset = 0
if (nextBrother) {
let nextNodeRect = this.getNodeRect(nextBrother)
nextBrotherOffset = nextNodeRect[dir1] - nodeRect[dir2]
nextBrotherOffset =
nextBrotherOffset >= this.minOffset ? nextBrotherOffset / 2 : 0
} else {
nextBrotherOffset = this.minOffset
}
return {
prevBrotherOffset,
nextBrotherOffset
}
}
// 处理提示元素的大小
formatPlaceholderSize(size) {
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt
return size > 0 ? Math.min(size, nodeDragPlaceholderMaxSize) : 5
}
// 设置提示元素的大小和位置
setPlaceholderRect(w, h, x, y) {
this.placeholder.size(w, h).move(x, y)
}
// 检测是否重叠
checkIsOverlap({
node,
dir,
prevBrotherOffset,
nextBrotherOffset,
size,
pos,
nodeRect
}) {
let dir1 = dir === 'v' ? 'top' : 'left'
let dir2 = dir === 'v' ? 'bottom' : 'right'
if (!this.overlapNode && !this.prevNode && !this.nextNode) {
if (
nodeRect[dir1] + (prevBrotherOffset > 0 ? 0 : size) <= pos &&
nodeRect[dir2] - (nextBrotherOffset > 0 ? 0 : size) >= pos
) {
this.overlapNode = node
}
}
}
// 处理逻辑结构图
handleLogicalStructure(node) {
const checkList = this.commonGetNodeCheckList(node)
this.handleVerticalCheck(node, checkList)
}
// 处理思维导图
handleMindMap(node) {
const checkList = node.parent
? node.parent.children.filter(item => {
let sameDir = true
if (node.layerIndex === 1) {
sameDir = item.dir === node.dir
}
return sameDir && !this.checkIsInBeingDragNodeList(item)
})
: []
this.handleVerticalCheck(node, checkList)
}
// 处理组织结构图
handleOrganizationStructure(node) {
const checkList = this.commonGetNodeCheckList(node)
this.handleHorizontalCheck(node, checkList)
}
// 处理目录组织图
handleCatalogOrganization(node) {
const checkList = this.commonGetNodeCheckList(node)
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
this.handleVerticalCheck(node, checkList)
}
}
// 处理时间轴
handleTimeLine(node) {
let checkList = this.commonGetNodeCheckList(node)
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
this.handleVerticalCheck(node, checkList)
}
}
// 处理时间轴2
handleTimeLine2(node) {
let checkList = this.commonGetNodeCheckList(node)
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
if (node.dir === 'top' && node.layerIndex === 2) {
this.handleVerticalCheck(node, checkList, true)
} else {
this.handleVerticalCheck(node, checkList)
}
}
}
// 处理鱼骨图
handleFishbone(node) {
let checkList = node.parent
? node.parent.children.filter(item => {
return item.layerIndex > 1 && !this.checkIsInBeingDragNodeList(item)
})
: []
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
if (node.dir === 'top' && node.layerIndex === 2) {
this.handleVerticalCheck(node, checkList, true)
} else {
this.handleVerticalCheck(node, checkList)
}
}
}
// 获取节点的兄弟节点列表通用方法
commonGetNodeCheckList(node) {
return node.parent
? [...node.parent.children].filter(item => {
return !this.checkIsInBeingDragNodeList(item)
})
: []
}
// 计算节点的位置尺寸信息
getNodeRect(node) {
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
@@ -305,6 +657,7 @@ class Drag extends Base {
let originLeft = left
let originTop = top
let originBottom = top + height
let originRight = left + width
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
@@ -318,9 +671,17 @@ class Drag extends Base {
bottom,
originLeft,
originTop,
originBottom
originBottom,
originRight
}
}
// 检查某个节点是否在被拖拽节点内
checkIsInBeingDragNodeList(node) {
return !!this.beingDragNodeList.find(item => {
return item.uid === node.uid || item.isParent(node)
})
}
}
Drag.instanceName = 'drag'

View File

@@ -1,14 +1,19 @@
import { imgToDataUrl, downloadFile, readBlob, removeHTMLEntities } from '../utils'
import {
imgToDataUrl,
downloadFile,
readBlob,
removeHTMLEntities
} from '../utils'
import { SVG } from '@svgdotjs/svg.js'
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
import { transformToMarkdown } from '../parse/toMarkdown'
import { a4Size } from '../constants/constant'
// 导出插件
class Export {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
this.exportPadding = this.mindMap.opt.exportPadding
}
// 导出
@@ -35,6 +40,10 @@ class Export {
let imageList = svg.find('image')
let task = imageList.map(async item => {
let imgUlr = item.attr('href') || item.attr('xlink:href')
// 已经是data:URL形式不用转换
if (/^data:/.test(imgUlr) || imgUlr === 'none') {
return
}
let imgData = await imgToDataUrl(imgUlr)
item.attr('href', imgData)
})
@@ -49,33 +58,51 @@ class Export {
}
// svg转png
svgToPng(svgSrc, transparent) {
svgToPng(
svgSrc,
transparent,
checkRotate = () => {
return false
}
) {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
img.setAttribute('crossOrigin', 'anonymous')
img.onload = async () => {
try {
let canvas = document.createElement('canvas')
canvas.width = img.width + this.exportPadding * 2
canvas.height = img.height + this.exportPadding * 2
let ctx = canvas.getContext('2d')
const canvas = document.createElement('canvas')
const dpr = Math.max(
window.devicePixelRatio,
this.mindMap.opt.minExportImgCanvasScale
)
const imgWidth = img.width
const imgHeight = img.height
// 如果宽比高长那么旋转90度
const needRotate = checkRotate(imgWidth, imgHeight)
if (needRotate) {
canvas.width = imgHeight * dpr
canvas.height = imgWidth * dpr
canvas.style.width = imgHeight + 'px'
canvas.style.height = imgWidth + 'px'
} else {
canvas.width = imgWidth * dpr
canvas.height = imgHeight * dpr
canvas.style.width = imgWidth + 'px'
canvas.style.height = imgHeight + 'px'
}
const ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr)
if (needRotate) {
ctx.rotate(0.5 * Math.PI)
ctx.translate(0, -imgHeight)
}
// 绘制背景
if (!transparent) {
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
await this.drawBackgroundToCanvas(ctx, imgWidth, imgHeight)
}
// 图片绘制到canvas里
ctx.drawImage(
img,
0,
0,
img.width,
img.height,
this.exportPadding,
this.exportPadding,
img.width,
img.height
)
ctx.drawImage(img, 0, 0, imgWidth, imgHeight)
resolve(canvas.toDataURL())
} catch (error) {
reject(error)
@@ -96,7 +123,7 @@ class Export {
backgroundImage,
backgroundRepeat = 'no-repeat',
backgroundPosition = 'center center',
backgroundSize = 'cover',
backgroundSize = 'cover'
} = this.mindMap.themeConfig
// 背景颜色
ctx.save()
@@ -107,18 +134,25 @@ class Export {
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
ctx.save()
drawBackgroundImageToCanvas(ctx, width, height, backgroundImage, {
backgroundRepeat,
backgroundPosition,
backgroundSize
}, (err) => {
if (err) {
reject(err)
} else {
resolve()
drawBackgroundImageToCanvas(
ctx,
width,
height,
backgroundImage,
{
backgroundRepeat,
backgroundPosition,
backgroundSize
},
err => {
if (err) {
reject(err)
} else {
resolve()
}
ctx.restore()
}
ctx.restore()
})
)
} else {
resolve()
}
@@ -152,14 +186,27 @@ class Export {
* 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png(name, transparent = false) {
async png(name, transparent = false, checkRotate) {
let { node, str } = await this.getSvgData()
str = removeHTMLEntities(str)
// 如果开启了富文本则使用htmltocanvas转换为图片
if (this.mindMap.richText) {
let res = await this.mindMap.richText.handleExportPng(node.node)
let imgDataUrl = await this.svgToPng(res, transparent)
return imgDataUrl
// 覆盖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], {
@@ -168,17 +215,20 @@ class Export {
// 转换成data:url数据
let svgUrl = await readBlob(blob)
// 绘制到canvas上
let res = await this.svgToPng(svgUrl, transparent)
let res = await this.svgToPng(svgUrl, transparent, checkRotate)
return res
}
// 导出为pdf
async pdf(name) {
async pdf(name, useMultiPageExport) {
if (!this.mindMap.doExportPDF) {
throw new Error('请注册ExportPDF插件')
}
let img = await this.png()
this.mindMap.doExportPDF.pdf(name, img)
let img = await this.png('', false, (width, height) => {
if (width <= a4Size.width && height && a4Size.height) return false
return width / height > 1
})
this.mindMap.doExportPDF.pdf(name, img, useMultiPageExport)
}
// 导出为xmind
@@ -194,15 +244,15 @@ class Export {
// 导出为svg
// plusCssText附加的css样式如果svg中存在dom节点想要设置一些针对节点的样式可以通过这个参数传入
async svg(name, plusCssText) {
async svg(name) {
let { node } = await this.getSvgData()
// 开启了节点富文本编辑
if (this.mindMap.richText) {
if (plusCssText) {
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>${plusCssText}</style>`))
}
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(
SVG(`<style>${this.mindMap.opt.resetCss}</style>`)
)
}
}
node.first().before(SVG(`<title>${name}</title>`))

View File

@@ -1,4 +1,5 @@
import JsPDF from 'jspdf'
import { a4Size } from '../constants/constant'
// 导出PDF插件需要通过Export插件使用
class ExportPDF {
@@ -8,31 +9,89 @@ class ExportPDF {
}
// 导出为pdf
pdf(name, img) {
pdf(name, img, useMultiPageExport = false) {
if (useMultiPageExport) {
this.multiPageExport(name, img)
} else {
this.onePageExport(name, img)
}
}
// 单页导出
onePageExport(name, img) {
let pdf = new JsPDF('', 'pt', 'a4')
let a4Width = 595
let a4Height = 841
let a4Ratio = a4Width / a4Height
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 <= a4Width && imageHeight <= a4Height) {
if (imageWidth <= a4Size.width && imageHeight <= a4Size.height) {
// 使用图片原始宽高
w = imageWidth
h = imageHeight
} else if (a4Ratio > imageRatio) {
// 以a4Height为高度缩放图片宽度
w = imageRatio * a4Height
h = a4Height
w = imageRatio * a4Size.height
h = a4Size.height
} else {
// 以a4Width为宽度缩放图片高度
w = a4Width
h = a4Width / imageRatio
w = a4Size.width
h = a4Size.width / imageRatio
}
pdf.addImage(
img,
'PNG',
(a4Size.width - w) / 2,
(a4Size.height - h) / 2,
w,
h
)
pdf.save(name)
}
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()
}
}
}
pdf.addImage(img, 'PNG', (a4Width - w) / 2, (a4Height - h) / 2, w, h)
pdf.save(name)
}
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

@@ -94,7 +94,7 @@ class KeyboardNavigation {
// 遍历节点树
bfsWalk(this.mindMap.renderer.root, node => {
// 跳过当前聚焦的节点
if (node === currentActiveNode) return
if (node.uid === currentActiveNode.uid) return
// 当前遍历到的节点的位置信息
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
@@ -131,7 +131,7 @@ class KeyboardNavigation {
checkNodeDis
}) {
bfsWalk(this.mindMap.renderer.root, node => {
if (node === currentActiveNode) return
if (node.uid === currentActiveNode.uid) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
@@ -173,7 +173,7 @@ class KeyboardNavigation {
let cX = (currentActiveNodeRect.right + currentActiveNodeRect.left) / 2
let cY = (currentActiveNodeRect.bottom + currentActiveNodeRect.top) / 2
bfsWalk(this.mindMap.renderer.root, node => {
if (node === currentActiveNode) return
if (node.uid === currentActiveNode.uid) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
// 遍历到的节点的中心点
@@ -232,4 +232,4 @@ class KeyboardNavigation {
KeyboardNavigation.instanceName = 'keyboardNavigation'
export default KeyboardNavigation
export default KeyboardNavigation

View File

@@ -1,4 +1,8 @@
import { isWhite, isTransparent, getVisibleColorFromTheme } from '../utils/index'
import {
isWhite,
isTransparent,
getVisibleColorFromTheme
} from '../utils/index'
// 小地图插件
class MiniMap {
@@ -78,7 +82,7 @@ class MiniMap {
viewBoxStyle.left = miniMapBoxLeft + actWidth
}
Object.keys(viewBoxStyle).forEach((key) => {
Object.keys(viewBoxStyle).forEach(key => {
viewBoxStyle[key] = viewBoxStyle[key] + 'px'
})
@@ -106,7 +110,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 === node && this.isShowHandleEl) return
if (this.node && this.node.uid === node.uid && this.isShowHandleEl) return
// 更新当前节点信息
this.node = node
this.img = img
@@ -263,6 +263,11 @@ class NodeImgAdjust {
beforePluginRemove() {
this.unBindEvent()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
NodeImgAdjust.instanceName = 'nodeImgAdjust'

View File

@@ -1,4 +1,4 @@
import { nodeDataNoStylePropList } from '../constants/constant'
import { checkIsNodeStyleDataKey } from '../utils/index'
// 格式刷插件
class Painter {
@@ -49,13 +49,13 @@ class Painter {
!this.isInPainter ||
!this.painterNode ||
!node ||
node === this.painterNode
node.uid === this.painterNode.uid
)
return
const style = {}
const painterNodeData = this.painterNode.nodeData.data
Object.keys(painterNodeData).forEach(key => {
if (!nodeDataNoStylePropList.includes(key)) {
if (checkIsNodeStyleDataKey(key)) {
style[key] = painterNodeData[key]
}
})
@@ -69,6 +69,11 @@ class Painter {
beforePluginRemove() {
this.unBindEvent()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
Painter.instanceName = 'painter'

View File

@@ -1,11 +1,12 @@
import Quill from 'quill'
import Delta from 'quill-delta'
import 'quill/dist/quill.snow.css'
import domtoimage from 'dom-to-image-more'
import {
walk,
getTextFromHtml,
isWhite,
getVisibleColorFromTheme
getVisibleColorFromTheme,
isUndef
} from '../utils'
import { CONSTANTS } from '../constants/constant'
@@ -43,6 +44,7 @@ class RichText {
this.quill = null
this.range = null
this.lastRange = null
this.pasteUseRange = null
this.node = null
this.isInserting = false
this.styleEl = null
@@ -151,7 +153,7 @@ class RichText {
}
// 显示文本编辑控件
showEditText(node, rect, isInserting) {
showEditText(node, rect, isInserting, isFromKeyDown) {
if (this.showTextEdit) {
return
}
@@ -159,7 +161,8 @@ class RichText {
richTextEditFakeInPlace,
customInnerElsAppendTo,
nodeTextEditZIndex,
textAutoWrapWidth
textAutoWrapWidth,
selectTextOnEnterEditText
} = this.mindMap.opt
this.node = node
this.isInserting = isInserting
@@ -198,6 +201,11 @@ class RichText {
this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('keydown', e => {
if (this.mindMap.renderer.textEdit.checkIsAutoEnterTextEditKey(e)) {
e.stopPropagation()
}
})
const targetNode = customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
@@ -230,7 +238,10 @@ class RichText {
}
if (!node.nodeData.data.richText) {
// 还不是富文本的情况
let text = node.nodeData.data.text.split(/\n/gim).join('<br>')
let text = ''
if (!isUndef(node.nodeData.data.text)) {
text = String(node.nodeData.data.text).split(/\n/gim).join('<br>')
}
let html = `<p>${text}</p>`
this.textEditNode.innerHTML = this.cacheEditingText || html
} else {
@@ -240,8 +251,11 @@ class RichText {
this.initQuillEditor()
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
this.showTextEdit = true
// 如果是刚创建的节点,那么默认全选,否则普通激活不全选
this.focus(isInserting ? 0 : null)
// 如果是刚创建的节点,那么默认全选,否则普通激活不全选除非selectTextOnEnterEditText配置为true
// 在selectTextOnEnterEditText时如果是在keydown事件进入的节点编辑也不需要全选
this.focus(
isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null
)
if (!node.nodeData.data.richText) {
// 如果是非富文本的情况,需要手动应用文本样式
this.setTextStyleIfNotRichText(node)
@@ -324,6 +338,7 @@ class RichText {
this.lastRange = this.range
this.range = null
if (range) {
this.pasteUseRange = range
let bounds = this.quill.getBounds(range.index, range.length)
let rect = this.textEditNode.getBoundingClientRect()
let rectInfo = {
@@ -363,6 +378,38 @@ class RichText {
this.lostStyle = false
}
})
// 拦截粘贴,只允许粘贴纯文本
this.quill.clipboard.addMatcher(Node.TEXT_NODE, node => {
let style = this.getPasteTextStyle()
return new Delta().insert(node.data, style)
})
this.quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
let ops = []
let style = this.getPasteTextStyle()
delta.ops.forEach(op => {
// 过滤出文本内容,过滤掉换行
if (op.insert && typeof op.insert === 'string' && op.insert !== '\n') {
ops.push({
attributes: { ...style },
insert: op.insert
})
}
})
delta.ops = ops
return delta
})
}
// 获取粘贴的文本的样式
getPasteTextStyle() {
// 粘贴的数据使用当前光标位置处的文本样式
if (this.pasteUseRange) {
return this.quill.getFormat(
this.pasteUseRange.index,
this.pasteUseRange.length
)
}
return {}
}
// 正则输入中文
@@ -573,7 +620,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
}
@@ -592,6 +639,11 @@ class RichText {
this.transformAllNodesToNormalNode()
document.head.removeChild(this.styleEl)
}
// 插件被卸载前做的事情
beforePluginDestroy() {
document.head.removeChild(this.styleEl)
}
}
RichText.instanceName = 'richText'

View File

@@ -0,0 +1,258 @@
import { throttle } from '../utils/index'
import { CONSTANTS } from '../constants/constant'
// 滚动条插件
class Scrollbar {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
this.scrollbarWrapSize = {
width: 0, // 水平滚动条的容器宽度
height: 0 // 垂直滚动条的容器高度
}
// 思维导图实际高度
this.chartHeight = 0
this.chartWidth = 0
this.reset()
this.bindEvent()
}
// 复位数据
reset() {
// 当前拖拽的滚动条类型
this.currentScrollType = ''
this.isMousedown = false
this.mousedownPos = {
x: 0,
y: 0
}
// 鼠标按下时,滚动条位置
this.mousedownScrollbarPos = 0
}
// 绑定事件
bindEvent() {
this.onMousemove = this.onMousemove.bind(this)
this.onMouseup = this.onMouseup.bind(this)
this.updateScrollbar = this.updateScrollbar.bind(this)
this.updateScrollbar = throttle(this.updateScrollbar, 16, this) // 加个节流
this.mindMap.on('mousemove', this.onMousemove)
this.mindMap.on('mouseup', this.onMouseup)
this.mindMap.on('node_tree_render_end', this.updateScrollbar)
this.mindMap.on('view_data_change', this.updateScrollbar)
}
// 解绑事件
unBindEvent() {
this.mindMap.off('mousemove', this.onMousemove)
this.mindMap.off('mouseup', this.onMouseup)
this.mindMap.off('node_tree_render_end', this.updateScrollbar)
this.mindMap.off('view_data_change', this.updateScrollbar)
}
// 渲染后、数据改变需要更新滚动条
updateScrollbar() {
// 当前正在拖拽滚动条时不需要更新
if (this.isMousedown) return
const res = this.calculationScrollbar()
this.emitEvent(res)
}
// 发送滚动条改变事件
emitEvent(data) {
this.mindMap.emit('scrollbar_change', data)
}
// 设置滚动条容器的大小,指滚动条容器的大小,对于水平滚动条,即宽度,对于垂直滚动条,即高度
setScrollBarWrapSize(width, height) {
this.scrollbarWrapSize.width = width
this.scrollbarWrapSize.height = height
}
// 计算滚动条大小和位置
calculationScrollbar() {
const rect = this.mindMap.draw.rbox()
// 减去画布距离浏览器窗口左上角的距离
const elRect = this.mindMap.elRect
rect.x -= elRect.left
rect.y -= elRect.top
// 垂直滚动条
const canvasHeight = this.mindMap.height // 画布高度
const paddingY = canvasHeight / 2 // 首尾允许超出的距离,默认为高度的一半
const chartHeight = rect.height + paddingY * 2 // 思维导图高度
this.chartHeight = chartHeight
const chartTop = rect.y - paddingY // 思维导图顶部距画布顶部的距离
const height = Math.min((canvasHeight / chartHeight) * 100, 100) // 滚动条高度 = 画布高度 / 思维导图高度
let top = (-chartTop / chartHeight) * 100 // 滚动条距离 = 思维导图顶部距画布顶部的距离 / 思维导图高度
// 判断是否到达边界
if (top < 0) {
top = 0
}
if (top > 100 - height) {
top = 100 - height
}
// 水平滚动条
const canvasWidth = this.mindMap.width
const paddingX = canvasWidth / 2
const chartWidth = rect.width + paddingX * 2
this.chartWidth = chartWidth
const chartLeft = rect.x - paddingX
const width = Math.min((canvasWidth / chartWidth) * 100, 100)
let left = (-chartLeft / chartWidth) * 100
if (left < 0) {
left = 0
}
if (left > 100 - width) {
left = 100 - width
}
const res = {
// 垂直滚动条
vertical: {
top,
height
},
// 水平滚动条
horizontal: {
left,
width
}
}
return res
}
// 滚动条鼠标按下事件处理函数
onMousedown(e, type) {
e.preventDefault()
e.stopPropagation()
this.currentScrollType = type
this.isMousedown = true
this.mousedownPos = {
x: e.clientX,
y: e.clientY
}
// 保存滚动条当前的位置
const styles = window.getComputedStyle(e.target)
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
this.mousedownScrollbarPos = Number.parseFloat(styles.top)
} else {
this.mousedownScrollbarPos = Number.parseFloat(styles.left)
}
}
// 鼠标移动事件处理函数
onMousemove(e) {
if (!this.isMousedown) {
return
}
e.preventDefault()
e.stopPropagation()
if (this.currentScrollType === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
const oy = e.clientY - this.mousedownPos.y + this.mousedownScrollbarPos
this.updateMindMapView(CONSTANTS.SCROLL_BAR_DIR.VERTICAL, oy)
} else {
const ox = e.clientX - this.mousedownPos.x + this.mousedownScrollbarPos
this.updateMindMapView(CONSTANTS.SCROLL_BAR_DIR.HORIZONTAL, ox)
}
}
// 鼠标松开事件处理函数
onMouseup() {
this.isMousedown = false
this.reset()
}
// 更新视图
updateMindMapView(type, offset) {
const scrollbarData = this.calculationScrollbar()
const t = this.mindMap.draw.transform()
const drawRect = this.mindMap.draw.rbox()
const rootRect = this.mindMap.renderer.root.group.rbox()
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
// 滚动条新位置
let oy = offset
// 判断是否达到首尾
if (oy <= 0) {
oy = 0
}
let max =
((100 - scrollbarData.vertical.height) / 100) *
this.scrollbarWrapSize.height
if (oy >= max) {
oy = max
}
// 转换成百分比
const oyPercentage = (oy / this.scrollbarWrapSize.height) * 100
// 转换成相对于图形高度的距离
const oyPx = (-oyPercentage / 100) * this.chartHeight
// 节点中心点到图形最上方的距离
const yOffset = rootRect.y - drawRect.y
// 内边距
const paddingY = this.mindMap.height / 2
// 图形新位置
let chartTop = oyPx + yOffset - paddingY * t.scaleY + paddingY
this.mindMap.view.translateYTo(chartTop)
this.emitEvent({
horizontal: scrollbarData.horizontal,
vertical: {
top: oyPercentage,
height: scrollbarData.vertical.height
}
})
} else {
// 滚动条新位置
let ox = offset
// 判断是否达到首尾
if (ox <= 0) {
ox = 0
}
let max =
((100 - scrollbarData.horizontal.width) / 100) *
this.scrollbarWrapSize.width
if (ox >= max) {
ox = max
}
// 转换成百分比
const oxPercentage = (ox / this.scrollbarWrapSize.width) * 100
// 转换成相对于图形高度的距离
const oxPx = (-oxPercentage / 100) * this.chartWidth
// 节点中心点到图形最左边的距离
const xOffset = rootRect.x - drawRect.x
// 内边距
const paddingX = this.mindMap.width / 2
// 图形新位置
let chartLeft = oxPx + xOffset - paddingX * t.scaleX + paddingX
this.mindMap.view.translateXTo(chartLeft)
this.emitEvent({
vertical: scrollbarData.vertical,
horizontal: {
left: oxPercentage,
width: scrollbarData.horizontal.width
}
})
}
}
// 滚动条的点击事件
onClick(e, type) {
let offset = 0
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
offset = e.clientY - e.currentTarget.getBoundingClientRect().top
} else {
offset = e.clientX - e.currentTarget.getBoundingClientRect().left
}
this.updateMindMapView(type, offset)
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
Scrollbar.instanceName = 'scrollbar'
export default Scrollbar

View File

@@ -1,4 +1,9 @@
import { bfsWalk, getTextFromHtml, isUndef, replaceHtmlText } from '../utils/index'
import {
bfsWalk,
getTextFromHtml,
isUndef,
replaceHtmlText
} from '../utils/index'
// 搜索插件
class Search {

View File

@@ -1,4 +1,4 @@
import { bfsWalk, throttle } from '../utils'
import { bfsWalk, throttle, checkTwoRectIsOverlap } from '../utils'
// 节点选择插件
class Select {
@@ -11,6 +11,8 @@ class Select {
this.mouseDownY = 0
this.mouseMoveX = 0
this.mouseMoveY = 0
this.isSelecting = false
this.cacheActiveList = []
this.bindEvent()
}
@@ -30,6 +32,7 @@ class Select {
}
e.preventDefault()
this.isMousedown = true
this.cacheActiveList = [...this.mindMap.renderer.activeNodeList]
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseDownX = x
this.mouseDownY = y
@@ -51,80 +54,142 @@ class Select {
) {
return
}
clearTimeout(this.autoMoveTimer)
this.onMove(x, y)
})
this.mindMap.on('mouseup', () => {
if (this.mindMap.opt.readonly) {
return
}
if (!this.isMousedown) {
return
}
this.mindMap.emit(
'node_active',
null,
this.mindMap.renderer.activeNodeList
this.clearAutoMoveTimer()
this.onMove(
e.clientX,
e.clientY,
() => {
this.isSelecting = true
// 绘制矩形
this.rect.plot([
[this.mouseDownX, this.mouseDownY],
[this.mouseMoveX, this.mouseDownY],
[this.mouseMoveX, this.mouseMoveY],
[this.mouseDownX, this.mouseMoveY]
])
this.checkInNodes()
},
(dir, step) => {
switch (dir) {
case 'left':
this.mouseDownX += step
break
case 'top':
this.mouseDownY += step
break
case 'right':
this.mouseDownX -= step
break
case 'bottom':
this.mouseDownY -= step
break
default:
break
}
}
)
clearTimeout(this.autoMoveTimer)
this.isMousedown = false
if (this.rect) this.rect.remove()
this.rect = null
})
this.onMouseup = this.onMouseup.bind(this)
this.mindMap.on('mouseup', this.onMouseup)
this.mindMap.on('node_mouseup', this.onMouseup)
}
// 结束框选
onMouseup() {
if (this.mindMap.opt.readonly) {
return
}
if (!this.isMousedown) {
return
}
this.checkTriggerNodeActiveEvent()
clearTimeout(this.autoMoveTimer)
this.isMousedown = false
this.cacheActiveList = []
if (this.rect) this.rect.remove()
this.rect = null
setTimeout(() => {
this.isSelecting = false
}, 0)
}
// 如果激活节点改变了,那么触发事件
checkTriggerNodeActiveEvent() {
let isNumChange =
this.cacheActiveList.length !==
this.mindMap.renderer.activeNodeList.length
let isNodeChange = false
if (!isNumChange) {
for (let i = 0; i < this.cacheActiveList.length; i++) {
let cur = this.cacheActiveList[i]
if (
!this.mindMap.renderer.activeNodeList.find(item => {
return item.nodeData.data.uid === cur.nodeData.data.uid
})
) {
isNodeChange = true
break
}
}
}
if (isNumChange || isNodeChange) {
this.mindMap.emit('node_active', null, [
...this.mindMap.renderer.activeNodeList
])
}
}
// 鼠标移动事件
onMove(x, y) {
// 绘制矩形
this.rect.plot([
[this.mouseDownX, this.mouseDownY],
[this.mouseMoveX, this.mouseDownY],
[this.mouseMoveX, this.mouseMoveY],
[this.mouseDownX, this.mouseMoveY]
])
this.checkInNodes()
onMove(x, y, callback = () => {}, handle = () => {}) {
callback()
// 检测边缘移动
let step = this.mindMap.opt.selectTranslateStep
let limit = this.mindMap.opt.selectTranslateLimit
let count = 0
// 左边缘
if (x <= this.mindMap.elRect.left + limit) {
this.mouseDownX += step
handle('left', step)
this.mindMap.view.translateX(step)
count++
}
// 右边缘
if (x >= this.mindMap.elRect.right - limit) {
this.mouseDownX -= step
handle('right', step)
this.mindMap.view.translateX(-step)
count++
}
// 上边缘
if (y <= this.mindMap.elRect.top + limit) {
this.mouseDownY += step
handle('top', step)
this.mindMap.view.translateY(step)
count++
}
// 下边缘
if (y >= this.mindMap.elRect.bottom - limit) {
this.mouseDownY -= step
handle('bottom', step)
this.mindMap.view.translateY(-step)
count++
}
if (count > 0) {
this.startAutoMove(x, y)
this.startAutoMove(x, y, callback, handle)
}
}
// 开启自动移动
startAutoMove(x, y) {
startAutoMove(x, y, callback, handle) {
this.autoMoveTimer = setTimeout(() => {
this.onMove(x, y)
this.onMove(x, y, callback, handle)
}, 20)
}
// 清除自动移动定时器
clearAutoMoveTimer() {
clearTimeout(this.autoMoveTimer)
}
// 创建矩形
createRect(x, y) {
if (this.rect) this.rect.remove()
this.rect = this.mindMap.svg
.polygon()
.stroke({
@@ -151,27 +216,27 @@ 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) {
return
}
this.mindMap.renderer.setNodeActive(node, true)
this.mindMap.renderer.addActiveNode(node)
// })
} else if (node.nodeData.data.isActive) {
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (!node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, false)
this.mindMap.renderer.removeActiveNode(node)
// })
}
})
}
// 是否存在选区
hasSelectRange() {
return this.isSelecting
}
}
Select.instanceName = 'select'

View File

@@ -78,7 +78,7 @@ class TouchEvent {
return
}
const viewBefore = this.touchStartScaleView
const scale = viewBefore.scale * (distance / viewBefore.distance)
let scale = viewBefore.scale * (distance / viewBefore.distance)
if (Math.abs(distance - viewBefore.distance) <= 10) {
scale = viewBefore.scale
}
@@ -148,6 +148,11 @@ class TouchEvent {
beforePluginRemove() {
this.unBindEvent()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
TouchEvent.instanceName = 'touchEvent'

View File

@@ -109,7 +109,10 @@ 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()
}
@@ -117,4 +120,4 @@ class Watermark {
Watermark.instanceName = 'watermark'
export default Watermark
export default Watermark

View File

@@ -1,7 +1,7 @@
import {
getAssociativeLineTargetIndex,
joinCubicBezierPath,
computeNodePoints,
getNodePoint,
getDefaultControlPointOffsets
} from './associativeLineUtils'
@@ -61,15 +61,22 @@ function onControlPointMousemove(e) {
}
// 更新当前拖拽的控制点的位置
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
let [path, clickPath, text, node, toNode] = this.activeLine
let [startPoint, endPoint] = computeNodePoints(node, toNode)
let [, , , node, toNode] = this.activeLine
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
let { associativeLinePoint, associativeLineTargetControlOffsets } =
node.nodeData.data
associativeLinePoint = associativeLinePoint || []
const nodePos = this.getNodePos(node)
const toNodePos = this.getNodePos(toNode)
let [startPoint, endPoint] = this.updateAllLinesPos(
node,
toNode,
associativeLinePoint[targetIndex]
)
this.controlPointMousemoveState.startPoint = startPoint
this.controlPointMousemoveState.endPoint = endPoint
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
this.controlPointMousemoveState.targetIndex = targetIndex
let offsets = []
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
if (!associativeLineTargetControlOffsets) {
// 兼容0.4.5版本没有associativeLineTargetControlOffsets的情况
offsets = getDefaultControlPointOffsets(startPoint, endPoint)
@@ -78,8 +85,14 @@ function onControlPointMousemove(e) {
}
let point1 = null
let point2 = null
const { x: clientX, y: clientY } = this.mindMap.toPos(e.clientX, e.clientY)
const _e = {
clientX,
clientY
}
// 拖拽的是控制点1
if (this.mousedownControlPointKey === 'controlPoint1') {
startPoint = getNodePoint(nodePos, '', 0, _e)
point1 = {
x,
y
@@ -88,10 +101,15 @@ function onControlPointMousemove(e) {
x: endPoint.x + offsets[1].x,
y: endPoint.y + offsets[1].y
}
// 更新控制点1的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
if (startPoint) {
// 保存更新后的坐标
this.controlPointMousemoveState.startPoint = startPoint
// 更新控制点1的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
}
} else {
// 拖拽的是控制点2
endPoint = getNodePoint(toNodePos, '', 0, _e)
point1 = {
x: startPoint.x + offsets[0].x,
y: startPoint.y + offsets[0].y
@@ -100,18 +118,39 @@ function onControlPointMousemove(e) {
x,
y
}
// 更新控制点2的连线
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
if (endPoint) {
// 保存更新后结束节点的坐标
this.controlPointMousemoveState.endPoint = endPoint
// 更新控制点2的连线
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
}
}
this.updataAassociativeLine(
startPoint,
endPoint,
point1,
point2,
this.activeLine
)
}
function updataAassociativeLine(
startPoint,
endPoint,
point1,
point2,
activeLine
) {
const [path, clickPath, text] = activeLine
// 更新关联线
let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
const pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
path.plot(pathStr)
clickPath.plot(pathStr)
this.updateTextPos(path, text)
this.updateTextEditBoxPos(text)
}
// 控制点的鼠标移动事件
// 控制点的鼠标松开事件
function onControlPointMouseup(e) {
if (!this.isControlPointMousedown) return
e.stopPropagation()
@@ -120,8 +159,15 @@ function onControlPointMouseup(e) {
this.controlPointMousemoveState
let [, , , node] = this.activeLine
let offsetList = []
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
let { associativeLinePoint, associativeLineTargetControlOffsets } =
node.nodeData.data
if (!associativeLinePoint) {
associativeLinePoint = []
}
associativeLinePoint[targetIndex] = associativeLinePoint[targetIndex] || {
startPoint,
endPoint
}
if (!associativeLineTargetControlOffsets) {
// 兼容0.4.5版本没有associativeLineTargetControlOffsets的情况
offsetList[targetIndex] = getDefaultControlPointOffsets(
@@ -140,6 +186,7 @@ function onControlPointMouseup(e) {
y: pos.y - startPoint.y
}
offset2 = offsetList[targetIndex][1]
associativeLinePoint[targetIndex].startPoint = startPoint
} else {
// 更新控制点2数据
offset1 = offsetList[targetIndex][0]
@@ -147,10 +194,12 @@ function onControlPointMouseup(e) {
x: pos.x - endPoint.x,
y: pos.y - endPoint.y
}
associativeLinePoint[targetIndex].endPoint = endPoint
}
offsetList[targetIndex] = [offset1, offset2]
this.mindMap.execCommand('SET_NODE_DATA', node, {
associativeLineTargetControlOffsets: offsetList
associativeLineTargetControlOffsets: offsetList,
associativeLinePoint
})
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚所以重置isControlPointMousedown需要等draw_click事件触发完以后
setTimeout(() => {
@@ -237,5 +286,6 @@ export default {
renderControls,
removeControls,
hideControls,
showControls
showControls,
updataAassociativeLine
}

View File

@@ -1,5 +1,9 @@
import { Text } from '@svgdotjs/svg.js'
import { getStrWithBrFromHtml } from '../../utils/index'
import {
getStrWithBrFromHtml,
focusInput,
selectAllInput
} from '../../utils/index'
// 创建文字节点
function createText(data) {
@@ -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,19 +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.lineHeight =
textLines.length > 1 ? associativeLineTextLineHeight : 'normal'
this.textEditNode.style.zIndex = nodeTextEditZIndex
this.textEditNode.innerHTML = textLines.join('<br>')
this.textEditNode.style.display = 'block'
this.updateTextEditBoxPos(g)
this.showTextEdit = true
// 如果是默认文本要全选输入框
if (text === '' || text === defaultAssociativeLineText) {
selectAllInput(this.textEditNode)
} else {
// 否则聚焦即可
focusInput(this.textEditNode)
}
}
// 处理画布缩放
@@ -78,10 +90,12 @@ function onScale() {
// 更新文本编辑框位置
function updateTextEditBoxPos(g) {
let rect = g.node.getBoundingClientRect()
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
if (this.textEditNode) {
this.textEditNode.style.minWidth = `${rect.width + 10}px`
this.textEditNode.style.minHeight = `${rect.height + 6}px`
this.textEditNode.style.left = `${rect.left}px`
this.textEditNode.style.top = `${rect.top}px`
}
}
// 隐藏文本编辑框
@@ -91,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
[toNode.nodeData.data.uid]: str
}
})
this.textEditNode.style.display = 'none'
@@ -110,7 +127,7 @@ function getText(node, toNode) {
if (!obj) {
return ''
}
return obj[toNode.nodeData.data.id] || ''
return obj[toNode.nodeData.data.uid] || ''
}
// 渲染关联线文字

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 item === toNode.nodeData.data.uid
})
}
@@ -54,28 +54,136 @@ export const cubicBezierPath = (x1, y1, x2, y2) => {
)
}
export const calcPoint = (node, e) => {
const { left, top, translateLeft, translateTop, width, height } = node
const clientX = e.clientX
const clientY = e.clientY
// 中心点的坐标
const centerX = translateLeft + width / 2
const centerY = translateTop + height / 2
const translateCenterX = left + width / 2
const translateCenterY = top + height / 2
const theta = Math.atan(height / width)
// 矩形左上角坐标
const deltaX = clientX - centerX
const deltaY = centerY - clientY
// 方向值
const direction = Math.atan2(deltaY, deltaX)
// 默认坐标
let x = left + width
let y = top + height
if (direction < theta && direction >= -theta) {
// 右边
// 正切值 = 对边/邻边,对边 = 正切值*邻边
const range = direction * (width / 2)
if (direction < theta && direction >= 0) {
// 中心点上边
y = translateCenterY - range
} else if (direction >= -theta && direction < 0) {
// 中心点下方
y = translateCenterY - range
}
return {
x,
y,
dir: 'right',
range
}
} else if (direction >= theta && direction < Math.PI - theta) {
// 上边
y = top
let range = 0
if (direction < Math.PI / 2 - theta && direction >= theta) {
// 正切值 = 对边/邻边,邻边 = 对边/正切值
const side = height / 2 / direction
range = -side
// 中心点右侧
x = translateCenterX + side
} else if (
direction >= Math.PI / 2 - theta &&
direction < Math.PI - theta
) {
// 中心点左侧
const tanValue = (centerX - clientX) / (centerY - clientY)
const side = (height / 2) * tanValue
range = side
x = translateCenterX - side
}
return {
x,
y,
dir: 'top',
range
}
} else if (direction < -theta && direction >= theta - Math.PI) {
// 下边
let range = 0
if (direction >= theta - Math.PI / 2 && direction < -theta) {
// 中心点右侧
// 正切值 = 对边/邻边,邻边 = 对边/正切值
const side = height / 2 / direction
range = side
x = translateCenterX - side
} else if (
direction < theta - Math.PI / 2 &&
direction >= theta - Math.PI
) {
// 中心点左侧
const tanValue = (centerX - clientX) / (centerY - clientY)
const side = (height / 2) * tanValue
range = -side
x = translateCenterX + side
}
return {
x,
y,
dir: 'bottom',
range
}
}
// 左边
x = left
const tanValue = (centerY - clientY) / (centerX - clientX)
const range = tanValue * (width / 2)
if (direction >= -Math.PI && direction < theta - Math.PI) {
// 中心点右侧
y = translateCenterY - range
} else if (direction < Math.PI && direction >= Math.PI - theta) {
// 中心点左侧
y = translateCenterY - range
}
return {
x,
y,
dir: 'left',
range
}
}
// 获取节点的连接点
export const getNodePoint = (node, dir = 'right') => {
export const getNodePoint = (node, dir = 'right', range = 0, e = null) => {
let { left, top, width, height } = node
if (e) {
return calcPoint(node, e)
}
switch (dir) {
case 'left':
return {
x: left,
y: top + height / 2
y: top + height / 2 - range
}
case 'right':
return {
x: left + width,
y: top + height / 2
y: top + height / 2 - range
}
case 'top':
return {
x: left + width / 2,
x: left + width / 2 - range,
y: top
}
case 'bottom':
return {
x: left + width / 2,
x: left + width / 2 - range,
y: top + height
}
default:
@@ -111,8 +219,8 @@ export const computeNodePoints = (fromNode, toNode) => {
toDir = 'bottom'
} else if (offsetY > 0 && -offsetY < offsetX && offsetY > offsetX) {
// down
fromDir = 'bottom'
toDir = 'top'
fromDir = 'right'
toDir = 'right'
}
return [getNodePoint(fromNode, fromDir), getNodePoint(toNode, toDir)]
}
@@ -179,4 +287,4 @@ export const getDefaultControlPointOffsets = (startPoint, endPoint) => {
y: controlPoints[1].y - endPoint.y
}
]
}
}

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

@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '#e68112',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: '#b0bc47',
borderWidth: 3
}
fontSize: 24
},
// 二级节点样式
second: {
@@ -30,18 +26,12 @@ export default merge(defaultTheme, {
color: '#8c5416',
borderColor: '#b0bc47',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: '#e68112'
}
fontSize: 18
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#8c5416',
active: {
borderColor: '#b0bc47'
}
color: '#8c5416'
},
// 概要节点样式
generalization: {
@@ -49,9 +39,6 @@ export default merge(defaultTheme, {
fillColor: '#ffd683',
borderColor: '#b0bc47',
borderWidth: 2,
color: '#8c5416',
active: {
borderColor: '#e68112'
}
color: '#8c5416'
}
})

View File

@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '#94c143',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: '#749336',
borderWidth: 3
}
fontSize: 24
},
// 二级节点样式
second: {
@@ -30,18 +26,12 @@ export default merge(defaultTheme, {
color: '#749336',
borderColor: '#aec668',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: '#749336'
}
fontSize: 18
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#749336',
active: {
borderColor: '#749336'
}
color: '#749336'
},
// 概要节点样式
generalization: {
@@ -49,9 +39,6 @@ export default merge(defaultTheme, {
fillColor: '#cee498',
borderColor: '#aec668',
borderWidth: 2,
color: '#749336',
active: {
borderColor: '#749336'
}
color: '#749336'
}
})

View File

@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
color: 'rgb(111, 61, 6)',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: '#fff',
borderWidth: 3
}
fontSize: 24
},
// 二级节点样式
second: {
@@ -30,18 +26,12 @@ export default merge(defaultTheme, {
color: 'rgb(225, 201, 158)',
borderColor: 'rgb(245, 224, 191)',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: 'rgb(255, 208, 124)'
}
fontSize: 18
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(231, 203, 155)',
active: {
borderColor: 'rgb(255, 208, 124)'
}
color: 'rgb(231, 203, 155)'
},
// 概要节点样式
generalization: {
@@ -49,9 +39,6 @@ export default merge(defaultTheme, {
fillColor: 'rgb(56, 45, 34)',
borderColor: 'rgb(104, 84, 61)',
borderWidth: 2,
color: 'rgb(242, 216, 176)',
active: {
borderColor: 'rgb(255, 208, 124)'
}
color: 'rgb(242, 216, 176)'
}
})

View File

@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(254, 199, 13)',
borderWidth: 3
}
fontSize: 24
},
// 二级节点样式
second: {
@@ -30,19 +26,12 @@ export default merge(defaultTheme, {
color: 'rgb(0, 0, 0)',
borderColor: '',
borderWidth: 0,
fontSize: 18,
active: {
borderColor: 'rgb(36, 179, 96)',
borderWidth: 3
}
fontSize: 18
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(254, 199, 13)'
}
color: 'rgb(204, 204, 204)'
},
// 概要节点样式
generalization: {
@@ -50,9 +39,6 @@ export default merge(defaultTheme, {
fillColor: 'rgb(27, 31, 34)',
borderColor: 'rgb(255, 119, 34)',
borderWidth: 2,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(36, 179, 96)'
}
color: 'rgb(204, 204, 204)'
}
})

View File

@@ -13,10 +13,7 @@ export default merge(defaultTheme, {
generalizationLineColor: '#333',
// 根节点样式
root: {
fillColor: 'rgb(115, 161, 191)',
active: {
borderColor: 'rgb(57, 80, 96)'
}
fillColor: 'rgb(115, 161, 191)'
},
// 二级节点样式
second: {
@@ -24,26 +21,17 @@ export default merge(defaultTheme, {
color: '#333',
borderColor: 'rgb(115, 161, 191)',
borderWidth: 1,
fontSize: 14,
active: {
borderColor: 'rgb(57, 80, 96)'
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333',
active: {
borderColor: 'rgb(57, 80, 96)'
}
color: '#333'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333',
active: {
borderColor: 'rgb(57, 80, 96)'
}
color: '#333'
}
})

View File

@@ -13,10 +13,7 @@ export default merge(defaultTheme, {
generalizationLineColor: '#333',
// 根节点样式
root: {
fillColor: 'rgb(191, 115, 148)',
active: {
borderColor: 'rgb(96, 57, 74)'
}
fillColor: 'rgb(191, 115, 148)'
},
// 二级节点样式
second: {
@@ -24,26 +21,17 @@ export default merge(defaultTheme, {
color: '#333',
borderColor: 'rgb(191, 115, 148)',
borderWidth: 1,
fontSize: 14,
active: {
borderColor: 'rgb(96, 57, 74)'
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333',
active: {
borderColor: 'rgb(96, 57, 74)'
}
color: '#333'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333',
active: {
borderColor: 'rgb(96, 57, 74)'
}
color: '#333'
}
})

View File

@@ -18,16 +18,13 @@ export default merge(defaultTheme, {
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=',
// 背景重复
backgroundRepeat: 'repeat',
backgroundSize: 'auto',
// 根节点样式
root: {
fillColor: 'rgb(233, 223, 152)',
color: '#333',
fontSize: 24,
borderRadius: 21,
active: {
fillColor: 'rgb(254, 219, 0)',
borderColor: 'transparent'
}
borderRadius: 21
},
// 二级节点样式
second: {
@@ -35,30 +32,18 @@ export default merge(defaultTheme, {
borderColor: 'transparent',
color: '#333',
fontSize: 16,
borderRadius: 10,
active: {
fillColor: 'rgb(254, 219, 0)',
borderColor: 'transparent'
}
borderRadius: 10
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#fff',
fontWeight: 'bold',
active: {
fillColor: 'rgb(254, 219, 0)',
borderColor: 'transparent'
}
fontWeight: 'bold'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'transparent',
color: '#333',
active: {
fillColor: 'rgb(254, 219, 0)',
borderColor: 'transparent'
}
color: '#333'
}
})

View File

@@ -18,10 +18,7 @@ export default merge(defaultTheme, {
fillColor: 'rgb(18, 187, 55)',
color: '#fff',
fontSize: 24,
borderRadius: 10,
active: {
borderColor: 'rgb(51, 51, 51)'
}
borderRadius: 10
},
// 二级节点样式
second: {
@@ -29,27 +26,18 @@ export default merge(defaultTheme, {
borderColor: 'transparent',
color: '#1a1a1a',
fontSize: 18,
borderRadius: 10,
active: {
borderColor: 'rgb(51, 51, 51)'
}
borderRadius: 10
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#1a1a1a',
active: {
borderColor: 'rgb(51, 51, 51)'
}
color: '#1a1a1a'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'rgb(51, 51, 51)',
borderWidth: 2,
color: '#1a1a1a',
active: {
borderColor: 'rgb(18, 187, 55)'
}
color: '#1a1a1a'
}
})

View File

@@ -20,10 +20,7 @@ export default merge(defaultTheme, {
fontSize: 24,
borderRadius: 10,
borderColor: 'rgb(249, 199, 84)',
borderWidth: 1,
active: {
borderColor: 'rgb(94, 202, 110)'
}
borderWidth: 1
},
// 二级节点样式
second: {
@@ -32,27 +29,18 @@ export default merge(defaultTheme, {
borderWidth: 1,
color: '#1a1a1a',
fontSize: 18,
borderRadius: 10,
active: {
borderColor: 'rgb(94, 202, 110)'
}
borderRadius: 10
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#1a1a1a',
active: {
borderColor: 'rgb(94, 202, 110)'
}
color: '#1a1a1a'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#1a1a1a',
color: '#1a1a1a',
borderWidth: 2,
active: {
borderColor: 'rgb(94, 202, 110)'
}
borderWidth: 2
}
})

View File

@@ -20,10 +20,7 @@ export default merge(defaultTheme, {
fontSize: 24,
borderRadius: 10,
borderColor: 'rgb(189, 197, 201)',
borderWidth: 2,
active: {
borderColor: 'rgb(169, 218, 218)'
}
borderWidth: 2
},
// 二级节点样式
second: {
@@ -32,10 +29,7 @@ export default merge(defaultTheme, {
borderWidth: 2,
color: '#fff',
fontSize: 18,
borderRadius: 10,
active: {
borderColor: 'rgb(56, 123, 233)'
}
borderRadius: 10
},
// 三级及以下节点样式
node: {
@@ -43,19 +37,13 @@ export default merge(defaultTheme, {
color: 'rgb(30, 53, 86)',
borderColor: 'rgb(30, 53, 86)',
borderWidth: 1,
marginY: 20,
active: {
borderColor: 'rgb(169, 218, 218)'
}
marginY: 20
},
// 概要节点样式
generalization: {
fillColor: 'rgb(56, 123, 233)',
borderColor: 'rgb(56, 123, 233)',
color: '#fff',
borderWidth: 0,
active: {
borderColor: 'rgb(169, 218, 218)'
}
borderWidth: 0
}
})

View File

@@ -16,10 +16,7 @@ export default merge(defaultTheme, {
// 根节点样式
root: {
fillColor: 'rgb(255, 255, 255)',
color: '#222',
active: {
borderColor: 'rgb(94, 199, 248)'
}
color: '#222'
},
// 二级节点样式
second: {
@@ -27,26 +24,17 @@ export default merge(defaultTheme, {
color: '#222',
borderColor: 'rgb(255, 255, 255)',
borderWidth: 1,
fontSize: 14,
active: {
borderColor: 'rgb(94, 199, 248)'
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333',
active: {
borderColor: 'rgb(94, 199, 248)'
}
color: '#333'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'rgb(51, 51, 51)',
color: '#333',
active: {
borderColor: 'rgb(94, 199, 248)'
}
color: '#333'
}
})

View File

@@ -14,10 +14,7 @@ export default merge(defaultTheme, {
// 根节点样式
root: {
fillColor: 'rgb(253, 244, 217)',
color: '#222',
active: {
borderColor: 'rgb(94, 199, 248)'
}
color: '#222'
},
// 二级节点样式
second: {
@@ -25,27 +22,18 @@ export default merge(defaultTheme, {
color: '#222',
borderColor: 'rgb(242, 200, 104)',
borderWidth: 1,
fontSize: 14,
active: {
borderColor: 'rgb(94, 199, 248)'
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333',
active: {
borderColor: 'rgb(94, 199, 248)'
}
color: '#333'
},
// 概要节点样式
generalization: {
fillColor: 'rgb(123, 199, 120)',
borderColor: 'transparent',
borderWidth: 2,
color: '#fff',
active: {
borderColor: 'rgb(94, 199, 248)'
}
color: '#fff'
}
})

View File

@@ -16,11 +16,7 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(173, 123, 91)',
borderWidth: 3
}
fontSize: 24
},
// 二级节点样式
second: {
@@ -28,18 +24,12 @@ export default merge(defaultTheme, {
color: 'rgb(125, 86, 42)',
borderColor: '',
borderWidth: 0,
fontSize: 18,
active: {
borderColor: 'rgb(173, 123, 91)'
}
fontSize: 18
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(96, 71, 47)',
active: {
borderColor: 'rgb(173, 123, 91)'
}
color: 'rgb(96, 71, 47)'
},
// 概要节点样式
generalization: {
@@ -47,9 +37,6 @@ export default merge(defaultTheme, {
fillColor: 'rgb(255, 249, 239)',
borderColor: 'rgb(173, 123, 91)',
borderWidth: 2,
color: 'rgb(122, 83, 44)',
active: {
borderColor: 'rgb(202, 117, 79)'
}
color: 'rgb(122, 83, 44)'
}
})

View File

@@ -16,11 +16,7 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(173, 91, 12)',
borderWidth: 3
}
fontSize: 24
},
// 二级节点样式
second: {
@@ -28,18 +24,12 @@ export default merge(defaultTheme, {
color: 'rgb(50, 113, 96)',
borderColor: 'rgb(113, 195, 169)',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: 'rgb(173, 91, 12)'
}
fontSize: 18
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(10, 59, 43)',
active: {
borderColor: 'rgb(173, 91, 12)'
}
color: 'rgb(10, 59, 43)'
},
// 概要节点样式
generalization: {
@@ -47,9 +37,6 @@ export default merge(defaultTheme, {
fillColor: 'rgb(246, 238, 211)',
borderColor: '',
borderWidth: 0,
color: 'rgb(173, 91, 12)',
active: {
borderColor: 'rgb(113, 195, 169)'
}
color: 'rgb(173, 91, 12)'
}
})

View File

@@ -18,10 +18,7 @@ export default merge(defaultTheme, {
fillColor: 'rgb(28, 178, 43)',
color: '#fff',
fontSize: 24,
borderRadius: 10,
active: {
borderColor: 'rgb(17, 68, 23)'
}
borderRadius: 10
},
// 二级节点样式
second: {
@@ -29,26 +26,17 @@ export default merge(defaultTheme, {
color: 'rgb(147,148,149)',
fontSize: 18,
borderRadius: 10,
borderWidth: 0,
active: {
borderColor: 'rgb(17, 68, 23)'
}
borderWidth: 0
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(147, 148, 149)',
active: {
borderColor: 'rgb(17, 68, 23)'
}
color: 'rgb(147, 148, 149)'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'transparent',
color: '#333',
active: {
borderColor: 'rgb(17, 68, 23)'
}
color: '#333'
}
})

View File

@@ -17,11 +17,7 @@ export default merge(defaultTheme, {
fillColor: 'rgb(36, 179, 96)',
color: '#fff',
borderColor: '',
borderWidth: 0,
active: {
borderColor: 'rgb(254, 199, 13)',
borderWidth: 3
}
borderWidth: 0
},
// 二级节点样式
second: {
@@ -29,28 +25,18 @@ export default merge(defaultTheme, {
color: 'rgb(0, 0, 0)',
borderColor: '',
borderWidth: 0,
fontSize: 14,
active: {
borderColor: 'rgb(36, 179, 96)',
borderWidth: 2
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(254, 199, 13)'
}
color: 'rgb(204, 204, 204)'
},
// 概要节点样式
generalization: {
fillColor: 'transparent',
borderColor: 'rgb(255, 119, 34)',
borderWidth: 2,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(254, 199, 13)'
}
color: 'rgb(204, 204, 204)'
}
})

View File

@@ -70,12 +70,7 @@ export default {
borderWidth: 0,
borderDasharray: 'none',
borderRadius: 5,
textDecoration: 'none',
active: {
borderColor: 'rgb(57, 80, 96)',
borderWidth: 3,
borderDasharray: 'none'
}
textDecoration: 'none'
},
// 二级节点样式
second: {
@@ -93,12 +88,7 @@ export default {
borderWidth: 1,
borderDasharray: 'none',
borderRadius: 5,
textDecoration: 'none',
active: {
borderColor: 'rgb(57, 80, 96)',
borderWidth: 3,
borderDasharray: 'none'
}
textDecoration: 'none'
},
// 三级及以下节点样式
node: {
@@ -116,12 +106,7 @@ export default {
borderWidth: 0,
borderRadius: 5,
borderDasharray: 'none',
textDecoration: 'none',
active: {
borderColor: 'rgb(57, 80, 96)',
borderWidth: 3,
borderDasharray: 'none'
}
textDecoration: 'none'
},
// 概要节点样式
generalization: {
@@ -139,12 +124,7 @@ export default {
borderWidth: 1,
borderDasharray: 'none',
borderRadius: 5,
textDecoration: 'none',
active: {
borderColor: 'rgb(57, 80, 96)',
borderWidth: 3,
borderDasharray: 'none'
}
textDecoration: 'none'
}
}
@@ -178,14 +158,17 @@ const nodeSizeIndependenceList = [
'backgroundImage',
'backgroundRepeat',
'backgroundPosition',
'backgroundSize'
'backgroundSize',
'rootLineKeepSameInCurve'
]
export const checkIsNodeSizeIndependenceConfig = (config) => {
export const checkIsNodeSizeIndependenceConfig = config => {
let keys = Object.keys(config)
for(let i = 0; i < keys.length; i++) {
if (!nodeSizeIndependenceList.find((item) => {
return item === keys[i]
})) {
for (let i = 0; i < keys.length; i++) {
if (
!nodeSizeIndependenceList.find(item => {
return item === keys[i]
})
) {
return false
}
}

View File

@@ -13,10 +13,7 @@ export default merge(defaultTheme, {
generalizationLineColor: '#333',
// 根节点样式
root: {
fillColor: 'rgb(191, 147, 115)',
active: {
borderColor: 'rgb(96, 73, 57)'
}
fillColor: 'rgb(191, 147, 115)'
},
// 二级节点样式
second: {
@@ -24,26 +21,17 @@ export default merge(defaultTheme, {
color: '#333',
borderColor: 'rgb(191, 147, 115)',
borderWidth: 1,
fontSize: 14,
active: {
borderColor: 'rgb(96, 73, 57)'
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333',
active: {
borderColor: 'rgb(96, 73, 57)'
}
color: '#333'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333',
active: {
borderColor: 'rgb(96, 73, 57)'
}
color: '#333'
}
})

View File

@@ -26,11 +26,6 @@ export default merge(defaultTheme, {
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333',
active: {
borderColor: 'rgb(57, 80, 96)',
borderWidth: 3,
borderDasharray: 'none'
}
color: '#333'
}
})

View File

@@ -13,10 +13,7 @@ export default merge(defaultTheme, {
generalizationLineColor: '#333',
// 根节点样式
root: {
fillColor: 'rgb(191, 115, 115)',
active: {
borderColor: 'rgb(96, 57, 57)'
}
fillColor: 'rgb(191, 115, 115)'
},
// 二级节点样式
second: {
@@ -24,26 +21,17 @@ export default merge(defaultTheme, {
color: '#333',
borderColor: 'rgb(191, 115, 115)',
borderWidth: 1,
fontSize: 14,
active: {
borderColor: 'rgb(96, 57, 57)'
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333',
active: {
borderColor: 'rgb(96, 57, 57)'
}
color: '#333'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333',
active: {
borderColor: 'rgb(96, 57, 57)'
}
color: '#333'
}
})

View File

@@ -17,11 +17,7 @@ export default merge(defaultTheme, {
fillColor: 'rgb(51, 56, 62)',
color: 'rgb(247, 208, 160)',
borderColor: '',
borderWidth: 0,
active: {
borderColor: 'rgb(247, 208, 160)',
borderWidth: 3
}
borderWidth: 0
},
// 二级节点样式
second: {
@@ -29,27 +25,17 @@ export default merge(defaultTheme, {
color: 'rgb(81, 58, 42)',
borderColor: '',
borderWidth: 0,
fontSize: 14,
active: {
borderColor: 'rgb(51, 56, 62)',
borderWidth: 2
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222',
active: {
borderColor: 'rgb(0, 192, 184)'
}
color: '#222'
},
// 概要节点样式
generalization: {
fillColor: 'rgb(127, 93, 64)',
borderColor: 'transparent',
color: 'rgb(255, 214, 175)',
active: {
borderColor: 'rgb(51, 56, 62)'
}
color: 'rgb(255, 214, 175)'
}
})

View File

@@ -17,11 +17,7 @@ export default merge(defaultTheme, {
fillColor: 'rgb(25, 193, 73)',
color: '#fff',
borderColor: '',
borderWidth: 0,
active: {
borderColor: '#222',
borderWidth: 3
}
borderWidth: 0
},
// 二级节点样式
second: {
@@ -29,28 +25,18 @@ export default merge(defaultTheme, {
color: 'rgb(69, 149, 96)',
borderColor: '',
borderWidth: 0,
fontSize: 14,
active: {
borderColor: 'rgb(25, 193, 73)',
borderWidth: 2
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222',
active: {
borderColor: 'rgb(25, 193, 73)'
}
color: '#222'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'rgb(251, 158, 0)',
borderWidth: 2,
color: 'rgb(51, 51, 51)',
active: {
borderColor: 'rgb(25, 193, 73)'
}
color: 'rgb(51, 51, 51)'
}
})

View File

@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
color: 'rgb(255, 255, 255)',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(255, 119, 34)',
borderWidth: 3
}
fontSize: 24
},
// 二级节点样式
second: {
@@ -30,19 +26,12 @@ export default merge(defaultTheme, {
color: 'rgb(209, 210, 210)',
borderColor: '',
borderWidth: 0,
fontSize: 18,
active: {
borderColor: 'rgb(255, 119, 34)',
borderWidth: 3
}
fontSize: 18
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(255, 119, 34)'
}
color: 'rgb(204, 204, 204)'
},
// 概要节点样式
generalization: {
@@ -50,9 +39,6 @@ export default merge(defaultTheme, {
fillColor: 'rgb(255, 119, 34)',
borderColor: '',
borderWidth: 2,
color: '#fff',
active: {
borderColor: 'rgb(23, 153, 243)'
}
color: '#fff'
}
})

View File

@@ -16,10 +16,7 @@ export default merge(defaultTheme, {
root: {
fillColor: 'rgb(55, 165, 255)',
borderColor: 'rgb(51, 51, 51)',
borderWidth: 3,
active: {
borderColor: 'rgb(255, 160, 36)'
}
borderWidth: 3
},
// 二级节点样式
second: {
@@ -27,26 +24,17 @@ export default merge(defaultTheme, {
color: '#222',
borderColor: 'rgb(51, 51, 51)',
borderWidth: 3,
fontSize: 14,
active: {
borderColor: 'rgb(55, 165, 255)'
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222',
active: {
borderColor: 'rgb(55, 165, 255)'
}
color: '#222'
},
// 概要节点样式
generalization: {
borderColor: '#222',
borderWidth: 3,
color: '#222',
active: {
borderColor: 'rgb(55, 165, 255)'
}
color: '#222'
}
})

View File

@@ -16,11 +16,7 @@ export default merge(defaultTheme, {
root: {
fillColor: 'rgb(0, 192, 184)',
borderColor: '',
borderWidth: 0,
active: {
borderColor: 'rgb(255, 160, 36)',
borderWidth: 3
}
borderWidth: 0
},
// 二级节点样式
second: {
@@ -28,26 +24,17 @@ export default merge(defaultTheme, {
color: '#222',
borderColor: 'rgb(184, 235, 233)',
borderWidth: 2,
fontSize: 14,
active: {
borderColor: 'rgb(0, 192, 184)'
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222',
active: {
borderColor: 'rgb(0, 192, 184)'
}
color: '#222'
},
// 概要节点样式
generalization: {
fillColor: 'rgb(90, 206, 241)',
borderColor: 'transparent',
color: '#fff',
active: {
borderColor: 'rgb(0, 192, 184)'
}
color: '#fff'
}
})

View File

@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
color: '#110501',
borderColor: '#ff6811',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: '#a9a4a9',
borderWidth: 3
}
fontSize: 24
},
// 二级节点样式
second: {
@@ -30,18 +26,12 @@ export default merge(defaultTheme, {
color: '#a9a4a9',
borderColor: '#ff6811',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: '#110501'
}
fontSize: 18
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#a9a4a9',
active: {
borderColor: '#ff6811'
}
color: '#a9a4a9'
},
// 概要节点样式
generalization: {
@@ -49,9 +39,6 @@ export default merge(defaultTheme, {
fillColor: '',
borderColor: '#ff6811',
borderWidth: 2,
color: '#a9a4a9',
active: {
borderColor: '#110501'
}
color: '#a9a4a9'
}
})

View File

@@ -16,11 +16,7 @@ export default merge(defaultTheme, {
root: {
fillColor: 'rgb(139, 109, 225)',
borderColor: '',
borderWidth: 0,
active: {
borderColor: 'rgb(243, 104, 138)',
borderWidth: 2
}
borderWidth: 0
},
// 二级节点样式
second: {
@@ -28,28 +24,17 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 14,
active: {
borderColor: 'rgb(139, 109, 225)',
borderWidth: 2
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222',
active: {
borderColor: 'rgb(139, 109, 225)'
}
color: '#222'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'transparent',
color: '#222',
active: {
borderColor: 'rgb(139, 109, 225)',
borderWidth: 2
}
color: '#222'
}
})

View File

@@ -18,11 +18,7 @@ export default merge(defaultTheme, {
color: 'rgb(255, 233, 157)',
borderColor: '',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: 'rgb(255, 233, 157)',
borderWidth: 3
}
fontSize: 24
},
// 二级节点样式
second: {
@@ -30,18 +26,12 @@ export default merge(defaultTheme, {
color: 'rgb(211, 58, 21)',
borderColor: 'rgb(222, 101, 85)',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: 'rgb(255, 233, 157)'
}
fontSize: 18
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(144, 71, 43)',
active: {
borderColor: 'rgb(255, 233, 157)'
}
color: 'rgb(144, 71, 43)'
},
// 概要节点样式
generalization: {
@@ -49,9 +39,6 @@ export default merge(defaultTheme, {
fillColor: 'rgb(255, 247, 211)',
borderColor: 'rgb(255, 202, 162)',
borderWidth: 2,
color: 'rgb(187, 101, 69)',
active: {
borderColor: 'rgb(222, 101, 85)'
}
color: 'rgb(187, 101, 69)'
}
})

View File

@@ -13,10 +13,7 @@ export default merge(defaultTheme, {
generalizationLineColor: '#333',
// 根节点样式
root: {
fillColor: 'rgb(123, 115, 191)',
active: {
borderColor: 'rgb(61, 57, 96)'
}
fillColor: 'rgb(123, 115, 191)'
},
// 二级节点样式
second: {
@@ -24,26 +21,17 @@ export default merge(defaultTheme, {
color: '#333',
borderColor: 'rgb(123, 115, 191)',
borderWidth: 1,
fontSize: 14,
active: {
borderColor: 'rgb(61, 57, 96)'
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333',
active: {
borderColor: 'rgb(61, 57, 96)'
}
color: '#333'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333',
active: {
borderColor: 'rgb(61, 57, 96)'
}
color: '#333'
}
})

View File

@@ -16,10 +16,7 @@ export default merge(defaultTheme, {
color: 'rgb(34, 34, 34)',
borderColor: 'rgb(34, 34, 34)',
borderWidth: 3,
fontSize: 24,
active: {
borderColor: '#a13600'
}
fontSize: 24
},
// 二级节点样式
second: {
@@ -27,18 +24,12 @@ export default merge(defaultTheme, {
color: 'rgb(34, 34, 34)',
borderColor: 'rgb(34, 34, 34)',
borderWidth: 3,
fontSize: 18,
active: {
borderColor: '#a13600'
}
fontSize: 18
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(34, 34, 34)',
active: {
borderColor: '#a13600'
}
color: 'rgb(34, 34, 34)'
},
// 概要节点样式
generalization: {
@@ -46,9 +37,6 @@ export default merge(defaultTheme, {
fillColor: 'transparent',
borderColor: 'rgb(34, 34, 34)',
borderWidth: 2,
color: 'rgb(34, 34, 34)',
active: {
borderColor: '#a13600'
}
color: 'rgb(34, 34, 34)'
}
})

View File

@@ -17,11 +17,7 @@ export default merge(defaultTheme, {
fillColor: '#fff',
borderColor: '',
borderWidth: 0,
color: 'rgb(65, 89, 158)',
active: {
borderColor: 'rgb(251, 227, 188)',
borderWidth: 3
}
color: 'rgb(65, 89, 158)'
},
// 二级节点样式
second: {
@@ -29,27 +25,17 @@ export default merge(defaultTheme, {
color: 'rgb(65, 89, 158)',
borderColor: '',
borderWidth: 0,
fontSize: 14,
active: {
borderColor: '#fff',
borderWidth: 2
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: 'rgb(65, 89, 158)',
active: {
borderColor: 'rgb(251, 227, 188)'
}
color: 'rgb(65, 89, 158)'
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'transparent',
color: 'rgb(65, 89, 158)',
active: {
borderColor: 'rgb(251, 227, 188)'
}
color: 'rgb(65, 89, 158)'
}
})

View File

@@ -17,11 +17,7 @@ export default merge(defaultTheme, {
fillColor: 'rgb(255, 112, 52)',
color: '#fff',
borderColor: '',
borderWidth: 0,
active: {
borderColor: 'rgb(51, 51, 51)',
borderWidth: 3
}
borderWidth: 0
},
// 二级节点样式
second: {
@@ -29,27 +25,17 @@ export default merge(defaultTheme, {
color: 'rgb(51, 51, 51)',
borderColor: '',
borderWidth: 0,
fontSize: 14,
active: {
borderColor: 'rgb(255, 112, 52)',
borderWidth: 2
}
fontSize: 14
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222',
active: {
borderColor: 'rgb(255, 112, 52)'
}
color: '#222'
},
// 概要节点样式
generalization: {
fillColor: 'rgb(255, 222, 69)',
borderColor: 'transparent',
color: 'rgb(51, 51, 51)',
active: {
borderColor: 'rgb(255, 112, 52)'
}
color: 'rgb(51, 51, 51)'
}
})

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,5 +1,6 @@
import { v4 as uuidv4 } from 'uuid'
import { nodeDataNoStylePropList } from '../constants/constant'
import MersenneTwister from './mersenneTwister'
// 深度优先遍历树
export const walk = (
root,
@@ -166,19 +167,22 @@ 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 &&
@@ -186,7 +190,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
@@ -464,7 +468,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和空字符串
@@ -632,3 +636,241 @@ export const isMobile = () => {
navigator.userAgent
)
}
// 获取对象改变了的的属性
export const getObjectChangedProps = (oldObject, newObject) => {
const res = {}
Object.keys(newObject).forEach(prop => {
const oldVal = oldObject[prop]
const newVal = newObject[prop]
if (getType(oldVal) !== getType(newVal)) {
res[prop] = newVal
return
}
if (getType(oldVal) === 'Object') {
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
res[prop] = newVal
return
}
} else {
if (oldVal !== newVal) {
res[prop] = newVal
return
}
}
})
return res
}
// 判断一个字段是否是节点数据中的样式字段
export const checkIsNodeStyleDataKey = key => {
// 用户自定义字段
if (/^_/.test(key)) return false
// 不在节点非样式字段列表里,那么就是样式字段
if (!nodeDataNoStylePropList.includes(key)) {
return true
}
return false
}
// 合并图标数组
// const data = [
// { type: 'priority', name: '优先级图标', list: [{ name: '1', icon: 'a' }, { name: 2, icon: 'b' }] },
// { type: 'priority', name: '优先级图标', list: [{ name: '2', icon: 'c' }, { name: 3, icon: 'd' }] },
// ];
// mergerIconList(data) 结果
// [
// { type: 'priority', name: '优先级图标', list: [{ name: '1', icon: 'a' }, { name: 2, icon: 'c' }, { name: 3, icon: 'd' }] },
// ]
export const mergerIconList = list => {
return list.reduce((result, item) => {
const existingItem = result.find(x => x.type === item.type)
if (existingItem) {
item.list.forEach(newObj => {
const existingObj = existingItem.list.find(x => x.name === newObj.name)
if (existingObj) {
existingObj.icon = newObj.icon
} else {
existingItem.list.push(newObj)
}
})
} else {
result.push({ ...item })
}
return result
}, [])
}
// 从节点实例列表里找出顶层的节点
export const getTopAncestorsFomNodeList = list => {
let res = []
list.forEach(node => {
if (
!list.find(item => {
return item.uid !== node.uid && item.isParent(node)
})
) {
res.push(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 getNodeIndex = node => {
return node.parent
? node.parent.children.findIndex(item => {
return item.uid === node.uid
})
: 0
}
// 根据内容生成颜色
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
}
}

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

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

@@ -0,0 +1,176 @@
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;
errorHandler: (code: any, error: any) => void;
resetCss: string;
enableDblclickReset: 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;
};
});
opt: any;
el: any;
elRect: any;
width: any;
height: any;
cssEl: HTMLStyleElement;
svg: any;
draw: any;
event: Event;
keyCommand: KeyCommand;
command: Command;
renderer: Render;
view: View;
batchExecution: BatchExecution;
handleOpt(opt: any): any;
addCss(): void;
removeCss(): void;
render(callback: any, source?: string): void;
reRender(callback: any, source?: string): void;
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): void;
getTheme(): any;
setThemeConfig(config: any): void;
getCustomThemeConfig(): any;
getThemeConfig(prop: any): any;
getConfig(prop: any): any;
updateConfig(opt?: {}): void;
getLayout(): any;
setLayout(layout: any): 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 }?: {
paddingX?: number;
paddingY?: number;
}): {
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,109 @@
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;
}
}
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";

View File

@@ -0,0 +1,100 @@
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;
function errorHandler(code: any, error: any): void;
const resetCss: string;
const enableDblclickReset: 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 };
}
}

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;
}

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