Compare commits

...

174 Commits

Author SHA1 Message Date
wanglin2
df32655321 文档新增changelog 2023-01-11 10:25:24 +08:00
wanglin2
1c7edf47e3 文档新增常见错误解决方案 2023-01-11 09:21:50 +08:00
wanglin2
4f5f9543f7 上传package-lock.json文件 2023-01-11 09:16:45 +08:00
wanglin2
0d3c1b7417 优化注释,去掉冗余信息 2023-01-10 11:08:55 +08:00
街角小林
e634fee753 Merge pull request #56 from emircanerkul/main
Readme Translation
2023-01-10 10:05:23 +08:00
Emircan ERKUL
81c17dc643 Merge branch 'main' of github.com:emircanerkul/mind-map 2023-01-09 13:53:40 +03:00
Emircan ERKUL
cb106c7fac translate readme documentation 2023-01-09 13:48:36 +03:00
wanglin2
5a9189b7fe 修改文档 2023-01-09 14:36:30 +08:00
wanglin2
160ed0d0e4 修改文档 2023-01-05 17:12:50 +08:00
wanglin25
ed92286178 打包 2023-01-05 16:16:39 +08:00
wanglin25
a837a6fb64 新增支持节点横线风格 2023-01-05 16:12:31 +08:00
wanglin2
47328236f7 修复画布距窗口左上角不为0时节点拖拽出现偏移的问题 2022-12-30 10:52:04 +08:00
wanglin2
566147c530 修复没有激活节点时顺便按上面键都会触发自动聚焦的问题 2022-12-13 20:21:39 +08:00
wanglin2
54fad1ee08 打包 2022-12-13 20:00:03 +08:00
wanglin2
8c8e283a88 优化键盘导航寻找焦点的算法 2022-12-13 19:57:54 +08:00
wanglin2
4553bc37f5 '打包' 2022-12-09 16:56:28 +08:00
wanglin2
41b0b21354 '新增键盘导航,即通过方向键来切换激活的节点' 2022-12-09 16:53:42 +08:00
wanglin2
39b55b0cd7 支持在大纲直接编辑节点文本内容 2022-12-08 09:19:47 +08:00
wanglin2
ede01c069e 打包 2022-11-16 08:59:13 +08:00
wanglin2
aa56e53c4d 优化侧边栏显示和隐藏方式 2022-11-15 20:00:03 +08:00
wanglin2
3a723a15bf 优化备注显示 2022-11-14 19:59:21 +08:00
wanglin2
1b2da4eb72 新增禅模式 2022-11-14 19:25:25 +08:00
wanglin2
6f351d4cee 打包 2022-11-05 14:46:52 +08:00
wanglin2
dba93d4363 demo网站增加英文翻译 2022-11-05 14:42:15 +08:00
wanglin2
b46a94bd96 多语言开发中 2022-10-28 16:26:55 +08:00
wanglin2
fadd8217e8 '增加从excel导入的功能' 2022-10-24 15:16:27 +08:00
wanglin2
b0d898054e 打包 2022-10-24 10:31:35 +08:00
wanglin2
3e84892a28 修复在小屏幕下侧边栏和工具栏重叠的问题 2022-10-24 10:28:50 +08:00
wanglin2
f663f8d60a 增加eslint校验、prettier格式化 2022-10-19 16:33:09 +08:00
wanglin2
0aeee0ff28 优化小地图、优化拖拽性能 2022-10-19 09:54:38 +08:00
wanglin2
140e9cf893 '支持双击节点内图片进行大图预览' 2022-10-18 19:13:15 +08:00
wanglin2
89e426e522 优化本地文件编辑 2022-10-18 09:40:34 +08:00
wanglin2
c77062b660 打包 2022-10-17 09:25:21 +08:00
街角小林
bca2912390 Merge pull request #43 from huangyuanyin/main
优化 地图组件卸载的时候把相关事件移除
2022-10-17 09:20:38 +08:00
liuzhanghao
ed7177e29d 优化 地图组件卸载的时候把相关事件移除 2022-10-14 18:11:57 +08:00
wanglin2
12ca3ed556 Merge branch 'dev6' into main 2022-10-11 18:54:24 +08:00
wanglin2
d7de86209c 打包 2022-10-11 18:53:30 +08:00
wanglin2
e53c6cd559 优化:插入子节点时自动展开 2022-10-11 18:48:52 +08:00
wanglin2
352711c871 修复小地图关闭时报错的问题 2022-10-11 18:36:16 +08:00
wanglin2
d304ebc544 合并 2022-10-11 11:10:00 +08:00
wanglin2
27291fe5c9 更新版本 2022-10-11 11:08:58 +08:00
wanglin2
f714e47722 修改文档 2022-10-10 22:40:44 +08:00
wanglin2
ce135dd111 修改文档 2022-10-10 22:40:12 +08:00
wanglin2
fb4bfedef4 打包 2022-10-10 16:27:49 +08:00
wanglin2
5cbe5d2906 修复子节点收起状态复制时丢失的问题 2022-10-10 16:11:00 +08:00
wanglin2
b6c15164ac 更新文档 2022-10-10 15:02:22 +08:00
wanglin2
1960e96a19 打包 2022-10-10 14:39:09 +08:00
wanglin2
7a0fd5adfb 支持小地图 2022-10-10 14:31:43 +08:00
wanglin2
9ae40cff32 修改文档 2022-09-30 22:16:27 +08:00
wanglin2
0070cab9f1 打包 2022-09-30 14:48:08 +08:00
wanglin2
79ae876eeb 逻辑结构图、思维导图新增直线连接风格、直连风格 2022-09-30 14:44:40 +08:00
wanglin2
ca35b84308 逻辑结构图和思维导图支持直线连接线开发中 2022-09-25 22:07:21 +08:00
wanglin2
3b4f386900 打包 2022-09-24 20:42:53 +08:00
wanglin2
34ef1e908e 优化:手动创建节点时立即聚焦 2022-09-24 20:38:47 +08:00
wanglin2
6a3f016920 修复连线样式深度更新问题 2022-09-24 20:13:53 +08:00
wanglin2
d1ab67cd4c 支持新建、打开、 2022-09-24 17:08:11 +08:00
wanglin2
0d81f9ff9c 修改文档 2022-09-24 10:51:09 +08:00
wanglin2
471bd3c5e5 支持展开到指定层级 2022-09-24 10:49:40 +08:00
wanglin2
5d4cf8a3c3 支持展开到指定层级 2022-09-23 17:41:38 +08:00
wanglin2
5a5062ecaf 修复xmind8版本文件导入失败的问题 2022-09-23 16:16:08 +08:00
wanglin2
13c4b51f69 修改README 2022-09-21 21:02:17 +08:00
wanglin2
a3ddcc93e5 打包 2022-09-21 20:11:12 +08:00
wanglin2
365e51e2e9 支持导入.xmind文件 2022-09-21 20:07:46 +08:00
wanglin2
fd096c4444 修复根节点添加多个节点爆栈的问题 2022-09-21 09:38:41 +08:00
wanglin2
4c1786e127 导出svg增加title标签 2022-09-19 09:36:19 +08:00
wanglin2
bfeb59d43f 打包 2022-09-17 22:00:15 +08:00
wanglin2
aa998b1829 节点支持设置自定义线条样式 2022-09-17 16:18:38 +08:00
wanglin2
c8f5c94683 优化节点自定义线条样式 2022-09-17 15:09:34 +08:00
wanglin2
2aea7a3c88 节点支持自定义线条样式 2022-09-17 15:00:12 +08:00
wanglin2
9f16691cd6 修改文档 2022-09-13 22:30:45 +08:00
wanglin2
8363819933 修复节点展开收起的bug&打包 2022-09-13 10:34:20 +08:00
wanglin2
7d6149dbdf 打包 2022-09-13 09:43:18 +08:00
wanglin2
b088c40101 修改文档 2022-09-13 09:33:05 +08:00
wanglin2
5b5aab1c9e 节点支持多种形状开发完成 2022-09-12 23:07:01 +08:00
wanglin2
13a4b12ad4 打包 2022-09-01 11:11:33 +08:00
街角小林
3e59d84e6b Merge pull request #26 from huangyuanyin/main
fix 二级节点后无子节点,展开所有/收起所有操作后的报错信息
2022-09-01 11:07:38 +08:00
liuzhanghao
cd987a382a fix 2022-09-01 10:28:26 +08:00
liuzhanghao
fdada41327 fix 二级节点后无子节点,展开所有/收起所有操作后的报错信息 2022-09-01 10:22:01 +08:00
wanglin
46d44e6753 更新readme 2022-08-22 21:03:08 +08:00
wanglin2
d63d9873ac fix:修改右键菜单快捷键提示 2022-08-17 09:25:16 +08:00
wanglin2
7eeb7ab51b fix:修复右键菜单快捷键提示错误 2022-08-16 18:48:11 +08:00
wanglin2
7a2075df51 fix:修复编辑节点文本时快捷键冲突的问题 2022-08-16 16:47:43 +08:00
wanglin
ba5ff54b9a 修复输入字符串/和快捷键/冲突问题 2022-08-14 09:43:00 +08:00
wanglin
8af44b2d45 打包 2022-08-08 21:24:02 +08:00
街角小林
72c989ae11 Update README.md 2022-08-08 20:09:31 +08:00
wanglin2
861309d517 支持导出为pdf 2022-08-08 20:06:59 +08:00
wanglin
2f0d64cf41 打包&发布新版本 2022-08-06 09:21:18 +08:00
wanglin2
5df8a28403 支持Ctrl+左键多选 2022-08-05 17:21:21 +08:00
wanglin2
3c9940e076 Merge branch 'dev' of https://github.com/wanglin2/mind-map into dev 2022-08-05 09:30:19 +08:00
wanglin2
0b11aff105 新增库打包&修改文档 2022-08-05 09:29:59 +08:00
wanglin
3e4fe06197 增加图标 2022-08-04 20:24:57 +08:00
wanglin2
8bf23f8397 修改文档 2022-08-04 15:06:07 +08:00
wanglin2
10d04549fc 新增快捷键 2022-08-04 14:48:52 +08:00
wanglin2
b4193d50a3 支持自由拖拽、修复一些bug 2022-08-04 11:39:17 +08:00
wanglin2
0620d31d0a 修复概要的一些bug 2022-08-02 08:59:05 +08:00
wanglin2
a804a5e2fa 修复拖拽节点时为隐藏概要内容的问题 2022-08-01 09:59:43 +08:00
wanglin2
06daffbaab 修复初始渲染时激活节点没有触发页面效果的问题 2022-08-01 09:51:53 +08:00
wanglin2
b901c08156 修复概要展开收起的定位错误问题 2022-08-01 09:36:25 +08:00
wanglin
e27eb63627 新增方法 2022-07-31 22:29:40 +08:00
wanglin
990a4e5c4c 修复概要定位问题 2022-07-31 20:46:40 +08:00
wanglin
7f79d4881d 修改及新增主题 2022-07-31 14:54:32 +08:00
wanglin
2d8643015f 概要开发中 2022-07-31 10:28:13 +08:00
wanglin
30f7713af1 更新版本 2022-07-30 08:21:43 +08:00
街角小林
7ab9db4bbd Merge pull request #16 from harris2012/patch-2
fix typo
2022-07-29 21:59:53 +08:00
街角小林
fa7548afaf Merge pull request #15 from harris2012/patch-1
Update Node.js
2022-07-29 21:57:05 +08:00
Harris Zhang
676efa792a fix typo 2022-07-29 17:55:20 +08:00
Harris Zhang
9ffa1e2744 Update Node.js 2022-07-29 11:29:25 +08:00
街角小林
1108b29da0 Merge pull request #13 from harris2012/patch-1
Update index.js
2022-07-26 21:15:41 +08:00
Harris Zhang
b00d5f6adb Update index.js 2022-07-26 19:12:47 +08:00
wanglin2
c6438668f2 fix:1.节点图标不能删除的问题;2.工具按钮置灰仍然可以点击的问题 2022-06-28 19:55:31 +08:00
wanglin2
608adb21e1 增加只读模式 2022-06-08 14:30:45 +08:00
wanglin
495cb4c62a 修改README 2022-05-10 23:09:54 +08:00
wanglin
3048cba1ae 发布0.1.6版本 2022-05-10 23:08:02 +08:00
wanglin2
5b1f5f803e 打包 2022-05-09 14:31:22 +08:00
wanglin2
0d79e28ec9 节点备注支持markdown及富文本、修复不能选中文字的问题 2022-05-09 14:24:11 +08:00
wanglin2
806be0b537 修复节点标注在节点激活后无法隐藏问题 2022-05-09 11:31:05 +08:00
wanglin2
c15b9be7ef 修复超链接、备注、标签等文字编辑时返回键和回车键与思维导图快捷键冲突的问题 2022-05-09 11:03:56 +08:00
wanglin
f6ac2c80ed 增加版本号 2021-11-25 22:25:26 +08:00
wanglin2
5f19509079 新增支持节点拖拽 2021-11-25 16:06:18 +08:00
wanglin2
4677b11dfb '状态数据支持保存激活状态、视图状态(拖动位置、缩放值)' 2021-11-22 19:59:21 +08:00
wanglin
b3e9797b0e 修复存在激活节点时设置主题存在的问题 2021-08-05 22:29:02 +08:00
wanglin
f72deb0478 升级版本 2021-08-05 08:09:48 +08:00
wanglin
455e97074f 增加快捷键功能 2021-08-05 00:06:27 +08:00
wanglin
3e52fa6585 修复回退问题 2021-08-04 23:45:05 +08:00
wanglin
48172bc035 新增导出为json;优化一些细节 2021-08-04 07:26:01 +08:00
wanglin
09d1f82021 修改版本号及package.json 2021-08-01 11:58:08 +08:00
wanglin
786e183091 增加本地存储功能 2021-08-01 10:49:57 +08:00
wanglin2
0ebac39716 修复删除节点和节点编辑时的删除冲突问题 2021-07-27 14:17:58 +08:00
wanglin2
10b8a33ab7 '修改文章与打包' 2021-07-22 19:06:51 +08:00
wanglin2
72b9001b56 '打包' 2021-07-22 14:13:49 +08:00
wanglin2
1efabd44ec '打包' 2021-07-22 09:36:50 +08:00
wanglin
5e66bd29ff 优化 2021-07-22 08:03:00 +08:00
wanglin
2f1114b722 修改文章 2021-07-21 22:50:46 +08:00
wanglin2
4428028146 '完成文章' 2021-07-21 20:24:37 +08:00
wanglin2
e660a74630 文章写作中 2021-07-20 20:18:03 +08:00
wanglin
8f29d63441 文章 2021-07-20 07:48:22 +08:00
wanglin
98afa6eb5b 修改文章 2021-07-19 07:39:34 +08:00
wanglin
c06971987d 添加图片 2021-07-18 22:47:03 +08:00
wanglin
e385ecf35d 优化与文档编写 2021-07-18 22:27:34 +08:00
wanglin
62fb0b15e0 完善开发文档 2021-07-16 19:33:01 +08:00
wanglin
19da1a4ff3 优化代码 2021-07-16 13:58:48 +08:00
wanglin
a798a40fab 基本完成 2021-07-16 13:42:37 +08:00
wanglin2
7982a1373f '右键功能开发中' 2021-07-15 17:46:36 +08:00
wanglin2
a812637cf0 Merge branch 'main' of https://github.com/wanglin2/mind-map into main 2021-07-15 09:46:53 +08:00
wanglin2
53c4d3945a '优化' 2021-07-15 09:46:27 +08:00
wanglin
2e7519cf29 右键菜单开发中 2021-07-14 23:57:36 +08:00
wanglin2
acad210d57 '修改样式' 2021-07-14 09:54:45 +08:00
wanglin2
ba5807932e '打包' 2021-07-14 09:34:13 +08:00
wanglin
59d572ae18 修改配置 2021-07-14 08:13:23 +08:00
wanglin
14aa8659ee 修改配置 2021-07-14 08:09:23 +08:00
wanglin
59c8ed4ef8 打包 2021-07-14 07:38:30 +08:00
wanglin
b886a0572d 修改打包配置i 2021-07-14 07:34:06 +08:00
wanglin
42755cac8a 修改 2021-07-14 07:28:31 +08:00
wanglin
ab1306357e 上传打包文件 2021-07-13 23:29:22 +08:00
wanglin
fa506f4212 修改README 2021-07-13 23:20:41 +08:00
wanglin2
364aed858f '基本完成' 2021-07-13 16:39:50 +08:00
wanglin
58703597e3 优化 2021-07-13 08:04:47 +08:00
wanglin
5d4b42d519 新增主题 2021-07-12 22:49:22 +08:00
wanglin2
cd55db7ed7 '完成部分结构' 2021-07-12 20:12:21 +08:00
wanglin2
ca9b3678dc '优化及增加主题' 2021-07-12 14:55:50 +08:00
wanglin
395e802b6b 回退功能开发中 2021-07-12 07:56:38 +08:00
wanglin
f38b92a2e2 增加快捷键和全屏功能 2021-07-11 22:21:40 +08:00
wanglin
5c6fef71bd 完成多选操作 2021-07-11 13:33:06 +08:00
wanglin
f3780baedc 完成基本逻辑 2021-07-10 22:06:45 +08:00
wanglin
388656f6e0 日常提交 2021-07-04 22:53:36 +08:00
wanglin
df60f103cc 完成导出功能 2021-07-04 16:56:37 +08:00
wanglin
7a977d74dc 日常提交 2021-06-28 07:46:12 +08:00
wanglin
7336c57e80 提交 2021-06-26 00:03:39 +08:00
wanglin
66963ca3ac 完成节点内容设置及主题 2021-06-26 00:01:03 +08:00
wanglin2
1bb1afd1b7 Delete .DS_Store 2021-06-20 23:05:11 +08:00
wanglin2
52cfc40c24 Delete .DS_Store 2021-06-20 23:04:56 +08:00
wanglin
debb058889 完成节点内容布局 2021-06-20 23:03:23 +08:00
wanglin2
3d5e3ac9a0 修改忽略文件 2021-06-19 14:24:52 +08:00
wanglin2
a06cb2e031 重构 2021-06-19 14:04:05 +08:00
261 changed files with 53092 additions and 4694 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
node_modules
oss.js
.DS_Store

1304
README.md

File diff suppressed because it is too large Load Diff

1205
README.zh-Hans.md Normal file

File diff suppressed because it is too large Load Diff

15
copy.js Normal file
View File

@@ -0,0 +1,15 @@
const fs = require('fs')
const path = require('path')
const src = path.resolve(__dirname, './dist/index.html')
const dest = path.resolve(__dirname, './index.html')
if (fs.existsSync(dest)) {
fs.unlinkSync(dest)
}
if (fs.existsSync(src)) {
fs.copyFileSync(src, dest)
fs.unlinkSync(src)
}

BIN
docs/assets/1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
docs/assets/2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
docs/assets/3.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
docs/assets/swdt.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

File diff suppressed because it is too large Load Diff

1
index.html Normal file
View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>一个简单的web思维导图实现</title><link href="dist/js/chunk-2d20ec02.917aff76.js" rel="prefetch"><link href="dist/js/chunk-2d216b67.2d06497a.js" rel="prefetch"><link href="dist/js/chunk-3a2f3e67.13278516.js" rel="prefetch"><link href="dist/css/app.92b546b0.css" rel="preload" as="style"><link href="dist/css/chunk-vendors.94891485.css" rel="preload" as="style"><link href="dist/js/app.aca24f03.js" rel="preload" as="script"><link href="dist/js/chunk-vendors.6cac1a4d.js" rel="preload" as="script"><link href="dist/css/chunk-vendors.94891485.css" rel="stylesheet"><link href="dist/css/app.92b546b0.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="dist/js/chunk-vendors.6cac1a4d.js"></script><script src="dist/js/app.aca24f03.js"></script></body></html>

View File

@@ -0,0 +1,17 @@
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": "eslint:recommended",
"overrides": [
],
"parserOptions": {
"parser": 'babel-eslint',
"ecmaVersion": 12,
"sourceType": "module",
"allowImportExportEverywhere": true
},
"rules": {
}
}

View File

@@ -0,0 +1,10 @@
src/assets
*/.DS_Store
dist
example
node_modules
*.json
*.md
.eslintrc.js
.prettierignore
.prettierrc

View File

@@ -0,0 +1,5 @@
semi: false
singleQuote: true
printWidth: 80
trailingComma: 'none'
arrowParens: 'avoid'

View File

@@ -0,0 +1,3 @@
# 一个web思维导图的简单实现
详细文档见:[https://github.com/wanglin2/mind-map](https://github.com/wanglin2/mind-map)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,929 @@
const createFullData = () => {
return {
"image": "/enJFNMHnedQTYTESGfDkctCp2.jpeg",
"imageTitle": "图片名称",
"imageSize": {
"width": 1000,
"height": 563
},
"icon": ['priority_1'],
"tag": ["标签1", "标签2"],
"hyperlink": "http://lxqnsys.com/",
"hyperlinkTitle": "理想青年实验室",
"note": "理想青年实验室\n一个有意思的角落",
// 自定义位置
// "customLeft": 1318,
// "customTop": 374.5
};
}
/**
* @Author: 王林
* @Date: 2021-04-15 22:23:24
* @Desc: 节点较多示例数据
*/
const data1 = {
"root": {
"data": {
"text": "根节点"
},
"children": [
{
"data": {
"text": "二级节点1",
"expand": true,
},
"children": [{
"data": {
"text": "分支主题",
...createFullData()
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
...createFullData()
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}]
},
{
"data": {
"text": "二级节点2",
"expand": true,
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
},
{
"data": {
"text": "二级节点3",
"expand": true,
},
"children": [{
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}]
},
{
"data": {
"text": "二级节点4",
"expand": true,
},
"children": [{
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}]
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}, {
"data": {
"text": "分支主题",
},
"children": [{
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}, {
"data": {
"text": "分支主题",
},
}]
}]
}]
}]
},
]
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-12 13:49:43
* @Desc: 真实场景数据
*/
const data2 = {
"root": {
"data": {
"text": "一周安排"
},
"children": [
{
"data": {
"text": "生活"
},
"children": [
{
"data": {
"text": "锻炼"
},
"children": [
{
"data": {
"text": "晨跑"
},
"children": [
{
"data": {
"text": "7:00-8:00"
},
"children": []
}
]
},
{
"data": {
"text": "夜跑"
},
"children": [
{
"data": {
"text": "20:00-21:00"
},
"children": []
}
]
}
]
},
{
"data": {
"text": "饮食"
},
"children": [
{
"data": {
"text": "早餐"
},
"children": [
{
"data": {
"text": "8:30"
},
"children": []
}
]
},
{
"data": {
"text": "午餐"
},
"children": [
{
"data": {
"text": "11:30"
},
"children": []
}
]
},
{
"data": {
"text": "晚餐"
},
"children": [
{
"data": {
"text": "19:00"
},
"children": []
}
]
}
]
},
{
"data": {
"text": "休息"
},
"children": [
{
"data": {
"text": "午休"
},
"children": [
{
"data": {
"text": "12:30-13:00"
},
"children": []
}
]
},
{
"data": {
"text": "晚休"
},
"children": [
{
"data": {
"text": "23:00-6:30"
},
"children": []
}
]
}
]
}
]
},
{
"data": {
"text": "工作日\n周一至周五"
},
"children": [
{
"data": {
"text": "日常工作"
},
"children": [
{
"data": {
"text": "9:00-18:00"
},
"children": []
}
]
},
{
"data": {
"text": "工作总结"
},
"children": [
{
"data": {
"text": "21:00-22:00"
},
"children": []
}
]
}
]
},
{
"data": {
"text": "学习"
},
"children": [
{
"data": {
"text": "工作日"
},
"children": [
{
"data": {
"text": "早间新闻"
},
"children": [
{
"data": {
"text": "8:00-8:30"
},
"children": []
}
]
},
{
"data": {
"text": "阅读"
},
"children": [
{
"data": {
"text": "21:00-23:00"
},
"children": []
}
]
}
]
},
{
"data": {
"text": "休息日"
},
"children": [
{
"data": {
"text": "财务管理"
},
"children": [
{
"data": {
"text": "9:00-10:30"
},
"children": []
}
]
},
{
"data": {
"text": "职场技能"
},
"children": [
{
"data": {
"text": "14:00-15:30"
},
"children": []
}
]
},
{
"data": {
"text": "其他书籍"
},
"children": [
{
"data": {
"text": "16:00-18:00"
},
"children": []
}
]
}
]
}
]
},
{
"data": {
"text": "休闲娱乐"
},
"children": [
{
"data": {
"text": "看电影"
},
"children": [
{
"data": {
"text": "1~2部"
},
"children": []
}
]
},
{
"data": {
"text": "逛街"
},
"children": [
{
"data": {
"text": "1~2次"
},
"children": []
}
]
}
]
}
]
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-12 14:29:10
* @Desc: 极简数据
*/
const data3 = {
"root": {
"data": {
"text": "根节点"
},
"children": [
{
"data": {
"text": "二级节点"
},
"children": [
{
"data": {
"text": "分支主题"
},
"children": []
},
{
"data": {
"text": "分支主题"
},
"children": []
}
]
}
]
}
}
const data4 = {
"root": {
"data": {
"text": "根节点"
},
"children": [
{
"data": {
"text": "二级节点1"
},
"children": [
{
"data": {
"text": "子节点1-1"
},
"children": []
},
{
"data": {
"text": "子节点1-2"
},
"children": [
{
"data": {
"text": "子节点1-2-1"
},
"children": []
},
{
"data": {
"text": "子节点1-2-2"
},
"children": []
},
{
"data": {
"text": "子节点1-2-3"
},
"children": []
}
]
}
]
},
{
"data": {
"text": "二级节点2"
},
"children": [
{
"data": {
"text": "子节点2-1"
},
"children": [
{
"data": {
"text": "子节点2-1-1"
},
"children": [
{
"data": {
"text": "子节点2-1-1-1"
},
"children": []
},
]
}
]
},
{
"data": {
"text": "子节点2-2"
},
"children": []
}
]
}
]
}
}
// 带概要
const data5 = {
"root": {
"data": {
"text": "根节点"
},
"children": [
{
"data": {
"text": "二级节点",
"generalization": {
"text": "概要",
}
},
"children": [
{
"data": {
"text": "分支主题"
},
"children": []
},
{
"data": {
"text": "分支主题"
},
"children": []
}
]
},
]
}
}
const rootData = {
"root": {
"data": {
"text": "根节点"
},
"children": []
}
}
export default {
// ...data1,
// ...data2,
// ...data3,
// ...data4,
...data5,
// ...rootData,
"theme": {
"template": "classic4",
"config": {
// 自定义配置...
}
},
"layout": "logicalStructure",
// "layout": "mindMap",
// "layout": "catalogOrganization"
// "layout": "organizationStructure"
}

View File

@@ -0,0 +1,66 @@
{
"layout": "logicalStructure",
"root": {
"data": {
"text": "根节点",
"expand": true,
"isActive": false
},
"children": [{
"data": {
"text": "二级节点",
"generalization": {
"text": "概要",
"expand": true,
"isActive": false
},
"expand": true,
"isActive": false
},
"children": [{
"data": {
"text": "分支主题",
"expand": true,
"isActive": false
},
"children": []
}, {
"data": {
"text": "分支主题",
"expand": true,
"isActive": false
},
"children": []
}]
}]
},
"theme": {
"template": "classic4",
"config": {}
},
"view": {
"transform": {
"scaleX": 1,
"scaleY": 1,
"shear": 0,
"rotate": 0,
"translateX": 0,
"translateY": 0,
"originX": 0,
"originY": 0,
"a": 1,
"b": 0,
"c": 0,
"d": 1,
"e": 0,
"f": 0
},
"state": {
"scale": 1,
"x": 0,
"y": 0,
"sx": 0,
"sy": 0
}
}
}

331
simple-mind-map/index.js Normal file
View File

@@ -0,0 +1,331 @@
import View from './src/View'
import Event from './src/Event'
import Render from './src/Render'
import merge from 'deepmerge'
import theme from './src/themes'
import Style from './src/Style'
import KeyCommand from './src/KeyCommand'
import Command from './src/Command'
import BatchExecution from './src/BatchExecution'
import Export from './src/Export'
import Select from './src/Select'
import Drag from './src/Drag'
import MiniMap from './src/MiniMap'
import { layoutValueList } from './src/utils/constant'
import { SVG } from '@svgdotjs/svg.js'
import xmind from './src/parse/xmind'
import { simpleDeepClone } from './src/utils'
import KeyboardNavigation from './src/KeyboardNavigation'
// 默认选项配置
const defaultOpt = {
// 是否只读
readonly: false,
// 布局
layout: 'logicalStructure',
// 主题
theme: 'default', // 内置主题default默认主题
// 主题配置,会和所选择的主题进行合并
themeConfig: {},
// 放大缩小的增量比例
scaleRatio: 0.1,
// 最多显示几个标签
maxTag: 5,
// 导出图片时的内边距
exportPadding: 20,
// 展开收缩按钮尺寸
expandBtnSize: 20,
// 节点里图片和文字的间距
imgTextMargin: 5,
// 节点里各种文字信息的间距,如图标和文字的间距
textContentMargin: 2,
// 多选节点时鼠标移动到边缘时的画布移动偏移量
selectTranslateStep: 3,
// 多选节点时鼠标移动距边缘多少距离时开始偏移
selectTranslateLimit: 20,
// 自定义节点备注内容显示
customNoteContentShow: null
/*
{
show(){},
hide(){}
}
*/
}
// 思维导图
class MindMap {
// 构造函数
constructor(opt = {}) {
// 合并选项
this.opt = this.handleOpt(merge(defaultOpt, opt))
// 容器元素
this.el = this.opt.el
this.elRect = this.el.getBoundingClientRect()
// 画布宽高
this.width = this.elRect.width
this.height = this.elRect.height
// 画布
this.svg = SVG().addTo(this.el).size(this.width, this.height)
this.draw = this.svg.group()
// 节点id
this.uid = 0
// 初始化主题
this.initTheme()
// 事件类
this.event = new Event({
mindMap: this
})
// 按键类
this.keyCommand = new KeyCommand({
mindMap: this
})
// 命令类
this.command = new Command({
mindMap: this
})
// 渲染类
this.renderer = new Render({
mindMap: this
})
// 视图操作类
this.view = new View({
mindMap: this,
draw: this.draw
})
// 小地图类
this.miniMap = new MiniMap({
mindMap: this
})
// 导出类
this.doExport = new Export({
mindMap: this
})
// 选择类
this.select = new Select({
mindMap: this
})
// 拖动类
this.drag = new Drag({
mindMap: this
})
// 键盘导航类
this.keyboardNavigation = new KeyboardNavigation({
mindMap: this
})
// 批量执行类
this.batchExecution = new BatchExecution()
// 初始渲染
this.reRender()
setTimeout(() => {
this.command.addHistory()
}, 0)
}
// 配置参数处理
handleOpt(opt) {
// 检查布局配置
if (!layoutValueList.includes(opt.layout)) {
opt.layout = 'logicalStructure'
}
// 检查主题配置
opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default'
return opt
}
// 渲染,部分渲染
render() {
this.batchExecution.push('render', () => {
this.initTheme()
this.renderer.reRender = false
this.renderer.render()
})
}
// 重新渲染
reRender() {
this.batchExecution.push('render', () => {
this.draw.clear()
this.initTheme()
this.renderer.reRender = true
this.renderer.render()
})
}
// 容器尺寸变化,调整尺寸
resize() {
this.elRect = this.el.getBoundingClientRect()
this.width = this.elRect.width
this.height = this.elRect.height
this.svg.size(this.width, this.height)
}
// 监听事件
on(event, fn) {
this.event.on(event, fn)
}
// 触发事件
emit(event, ...args) {
this.event.emit(event, ...args)
}
// 解绑事件
off(event, fn) {
this.event.off(event, fn)
}
// 设置主题
initTheme() {
// 合并主题配置
this.themeConfig = merge(theme[this.opt.theme], this.opt.themeConfig)
// 设置背景样式
Style.setBackgroundStyle(this.el, this.themeConfig)
}
// 设置主题
setTheme(theme) {
this.renderer.clearAllActive()
this.opt.theme = theme
this.reRender()
}
// 获取当前主题
getTheme() {
return this.opt.theme
}
// 设置主题配置
setThemeConfig(config) {
this.opt.themeConfig = config
this.reRender()
}
// 获取自定义主题配置
getCustomThemeConfig() {
return this.opt.themeConfig
}
// 获取某个主题配置值
getThemeConfig(prop) {
return prop === undefined ? this.themeConfig : this.themeConfig[prop]
}
// 获取当前布局结构
getLayout() {
return this.opt.layout
}
// 设置布局结构
setLayout(layout) {
// 检查布局配置
if (!layoutValueList.includes(layout)) {
layout = 'logicalStructure'
}
this.opt.layout = layout
this.renderer.setLayout()
this.render()
}
// 执行命令
execCommand(...args) {
this.command.exec(...args)
}
// 动态设置思维导图数据,纯节点数据
setData(data) {
this.execCommand('CLEAR_ACTIVE_NODE')
this.command.clearHistory()
this.renderer.renderTree = data
this.reRender()
}
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
setFullData(data) {
if (data.root) {
this.setData(data.root)
}
if (data.layout) {
this.setLayout(data.layout)
}
if (data.theme) {
if (data.theme.template) {
this.setTheme(data.theme.template)
}
if (data.theme.config) {
this.setThemeConfig(data.theme.config)
}
}
if (data.view) {
this.view.setTransformData(data.view)
}
}
// 获取思维导图数据,节点树、主题、布局等
getData(withConfig) {
let nodeData = this.command.getCopyData()
let data = {}
if (withConfig) {
data = {
layout: this.getLayout(),
root: nodeData,
theme: {
template: this.getTheme(),
config: this.getCustomThemeConfig()
},
view: this.view.getTransformData()
}
} else {
data = nodeData
}
return simpleDeepClone(data)
}
// 导出
async export(...args) {
let result = await this.doExport.export(...args)
return result
}
// 转换位置
toPos(x, y) {
return {
x: x - this.elRect.left,
y: y - this.elRect.top
}
}
// 设置只读模式、编辑模式
setMode(mode) {
if (!['readonly', 'edit'].includes(mode)) {
return
}
this.opt.readonly = mode === 'readonly'
if (this.opt.readonly) {
// 取消当前激活的元素
this.renderer.clearAllActive()
}
this.emit('mode_change', mode)
}
}
MindMap.xmind = xmind
export default MindMap

2507
simple-mind-map/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
{
"name": "simple-mind-map",
"version": "0.2.21",
"description": "一个简单的web在线思维导图",
"authors": [
{
"name": "街角小林",
"email": "1013335014@qq.com"
},
{
"name": "理想青年实验室",
"url": "http://lxqnsys.com/"
}
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/wanglin2/mind-map"
},
"scripts": {
"lint": "eslint src/",
"format": "prettier --write ."
},
"module": "index.js",
"main": "./dist/simpleMindMap.umd.min.js",
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",
"canvg": "^3.0.7",
"deepmerge": "^1.5.2",
"eventemitter3": "^4.0.7",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"xml-js": "^1.6.11"
},
"keywords": [
"javascript",
"svg",
"mind-map",
"mindMap",
"MindMap"
],
"devDependencies": {
"eslint": "^8.25.0",
"prettier": "^2.7.1"
}
}

View File

@@ -0,0 +1,32 @@
// 将/** */类型的注释转换为//类型
const path = require('path')
const fs = require('fs')
const entryPath = path.resolve(__dirname, '../src')
const transform = dir => {
let dirs = fs.readdirSync(dir)
dirs.forEach(item => {
let file = path.join(dir, item)
if (fs.statSync(file).isDirectory()) {
transform(file)
} else if (/\.js$/.test(file)) {
rewriteComments(file)
}
})
}
const rewriteComments = file => {
let content = fs.readFileSync(file, 'utf-8')
console.log('当前转换文件:', file)
content = content.replace(/\/\*\*[^/]+\*\//g, str => {
let res = /@Desc:([^\n]+)\n/g.exec(str)
if (res.length > 0) {
return '// ' + res[1]
}
})
fs.writeFileSync(file, content)
}
transform(entryPath)
rewriteComments(path.join(__dirname, '../index.js'))

View File

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

View File

@@ -0,0 +1,115 @@
import { copyRenderTree, simpleDeepClone } from './utils'
// 命令类
class Command {
// 构造函数
constructor(opt = {}) {
this.opt = opt
this.mindMap = opt.mindMap
this.commands = {}
this.history = []
this.activeHistoryIndex = 0
// 注册快捷键
this.registerShortcutKeys()
}
// 清空历史数据
clearHistory() {
this.history = []
this.activeHistoryIndex = 0
this.mindMap.emit('back_forward', 0, 0)
}
// 注册快捷键
registerShortcutKeys() {
this.mindMap.keyCommand.addShortcut('Control+z', () => {
this.mindMap.execCommand('BACK')
})
this.mindMap.keyCommand.addShortcut('Control+y', () => {
this.mindMap.execCommand('FORWARD')
})
}
// 执行命令
exec(name, ...args) {
if (this.commands[name]) {
this.commands[name].forEach(fn => {
fn(...args)
})
if (name === 'BACK' || name === 'FORWARD') {
return
}
this.addHistory()
}
}
// 添加命令
add(name, fn) {
if (this.commands[name]) {
this.commands[name].push(fn)
} else {
this.commands[name] = [fn]
}
}
// 移除命令
remove(name, fn) {
if (!this.commands[name]) {
return
}
if (!fn) {
this.commands[name] = []
delete this.commands[name]
} else {
let index = this.commands[name].find(item => {
return item === fn
})
if (index !== -1) {
this.commands[name].splice(index, 1)
}
}
}
// 添加回退数据
addHistory() {
let data = this.getCopyData()
this.history.push(simpleDeepClone(data))
this.activeHistoryIndex = this.history.length - 1
this.mindMap.emit('data_change', data)
this.mindMap.emit(
'back_forward',
this.activeHistoryIndex,
this.history.length
)
}
// 回退
back(step = 1) {
if (this.activeHistoryIndex - step >= 0) {
this.activeHistoryIndex -= step
this.mindMap.emit(
'back_forward',
this.activeHistoryIndex,
this.history.length
)
return simpleDeepClone(this.history[this.activeHistoryIndex])
}
}
// 前进
forward(step = 1) {
let len = this.history.length
if (this.activeHistoryIndex + step <= len - 1) {
this.activeHistoryIndex += step
this.mindMap.emit('back_forward', this.activeHistoryIndex)
return simpleDeepClone(this.history[this.activeHistoryIndex])
}
}
// 获取渲染树数据副本
getCopyData() {
return copyRenderTree({}, this.mindMap.renderer.renderTree)
}
}
export default Command

254
simple-mind-map/src/Drag.js Normal file
View File

@@ -0,0 +1,254 @@
import { bfsWalk, throttle } from './utils'
import Base from './layouts/Base'
// 节点拖动类
class Drag extends Base {
// 构造函数
constructor({ mindMap }) {
super(mindMap.renderer)
this.mindMap = mindMap
this.reset()
this.bindEvent()
}
// 复位
reset() {
// 当前拖拽节点
this.node = null
// 当前重叠节点
this.overlapNode = null
// 当前上一个同级节点
this.prevNode = null
// 当前下一个同级节点
this.nextNode = null
// 画布的变换数据
this.drawTransform = null
// 克隆节点
this.clone = null
// 连接线
this.line = null
// 同级位置占位符
this.placeholder = null
// 鼠标按下位置和节点左上角的偏移量
this.offsetX = 0
this.offsetY = 0
// 克隆节点左上角的坐标
this.cloneNodeLeft = 0
this.cloneNodeTop = 0
// 当前鼠标是否按下
this.isMousedown = false
// 拖拽的鼠标位置变量
this.mouseDownX = 0
this.mouseDownY = 0
this.mouseMoveX = 0
this.mouseMoveY = 0
}
// 绑定事件
bindEvent() {
this.checkOverlapNode = throttle(this.checkOverlapNode, 300, this)
this.mindMap.on('node_mousedown', (node, e) => {
if (this.mindMap.opt.readonly || node.isGeneralization) {
return
}
if (e.which !== 1 || node.isRoot) {
return
}
e.preventDefault()
// 计算鼠标按下的位置距离节点左上角的距离
this.drawTransform = this.mindMap.draw.transform()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.offsetX = x - (node.left * scaleX + translateX)
this.offsetY = y - (node.top * scaleY + translateY)
this.node = node
this.isMousedown = true
this.mouseDownX = x
this.mouseDownY = y
})
this.mindMap.on('mousemove', e => {
if (this.mindMap.opt.readonly) {
return
}
if (!this.isMousedown) {
return
}
e.preventDefault()
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseMoveX = x
this.mouseMoveY = y
if (
Math.abs(x - this.mouseDownX) <= 10 &&
Math.abs(y - this.mouseDownY) <= 10 &&
!this.node.isDrag
) {
return
}
this.mindMap.renderer.clearAllActive()
this.onMove(x, y)
})
this.onMouseup = this.onMouseup.bind(this)
this.mindMap.on('node_mouseup', this.onMouseup)
this.mindMap.on('mouseup', this.onMouseup)
}
// 鼠标松开事件
onMouseup(e) {
if (!this.isMousedown) {
return
}
this.isMousedown = false
let _nodeIsDrag = this.node.isDrag
this.node.isDrag = false
this.node.show()
this.removeCloneNode()
// 存在重叠子节点,则移动作为其子节点
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
this.mindMap.execCommand('MOVE_NODE_TO', this.node, this.overlapNode)
} else if (this.prevNode) {
// 存在前一个相邻节点,作为其下一个兄弟节点
this.mindMap.renderer.setNodeActive(this.prevNode, false)
this.mindMap.execCommand('INSERT_AFTER', this.node, this.prevNode)
} else if (this.nextNode) {
// 存在下一个相邻节点,作为其前一个兄弟节点
this.mindMap.renderer.setNodeActive(this.nextNode, false)
this.mindMap.execCommand('INSERT_BEFORE', this.node, this.nextNode)
} else if (_nodeIsDrag) {
// 自定义位置
let { x, y } = this.mindMap.toPos(
e.clientX - this.offsetX,
e.clientY - this.offsetY
)
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
x = (x - translateX) / scaleX
y = (y - translateY) / scaleY
this.node.left = x
this.node.top = y
this.node.customLeft = x
this.node.customTop = y
this.mindMap.execCommand('SET_NODE_CUSTOM_POSITION', this.node, x, y)
this.mindMap.render()
}
this.reset()
}
// 创建克隆节点
createCloneNode() {
if (!this.clone) {
// 节点
this.clone = this.node.group.clone()
this.clone.opacity(0.5)
this.clone.css('z-index', 99999)
this.node.isDrag = true
this.node.hide()
// 连接线
this.line = this.draw.path()
this.line.opacity(0.5)
this.node.styleLine(this.line, this.node)
// 同级位置占位符
this.placeholder = this.draw.rect().fill({
color: this.node.style.merge('lineColor', true)
})
this.mindMap.draw.add(this.clone)
}
}
// 移除克隆节点
removeCloneNode() {
if (!this.clone) {
return
}
this.clone.remove()
this.line.remove()
this.placeholder.remove()
}
// 拖动中
onMove(x, y) {
if (!this.isMousedown) {
return
}
this.createCloneNode()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
this.cloneNodeLeft = x - this.offsetX
this.cloneNodeTop = y - this.offsetY
x = (this.cloneNodeLeft - translateX) / scaleX
y = (this.cloneNodeTop - translateY) / scaleY
let t = this.clone.transform()
this.clone.translate(x - t.translateX, y - t.translateY)
// 连接线
let parent = this.node.parent
this.line.plot(
this.quadraticCurvePath(
parent.left + parent.width / 2,
parent.top + parent.height / 2,
x + this.node.width / 2,
y + this.node.height / 2
)
)
this.checkOverlapNode()
}
// 检测重叠节点
checkOverlapNode() {
if (!this.drawTransform) {
return
}
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let checkRight = this.cloneNodeLeft + this.node.width * scaleX
let checkBottom = this.cloneNodeTop + this.node.height * scaleX
this.overlapNode = null
this.prevNode = null
this.nextNode = null
this.placeholder.size(0, 0)
bfsWalk(this.mindMap.renderer.root, node => {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
}
if (node === this.node || this.node.isParent(node)) {
return
}
if (this.overlapNode || (this.prevNode && this.nextNode)) {
return
}
let { left, top, width, height } = node
let _left = left
let _top = top
let _bottom = top + height
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
// 检测是否重叠
if (!this.overlapNode) {
if (
left <= checkRight &&
right >= this.cloneNodeLeft &&
top <= checkBottom &&
bottom >= this.cloneNodeTop
) {
this.overlapNode = node
}
}
// 检测兄弟节点位置
if (!this.prevNode && !this.nextNode && !node.isRoot) {
// && this.node.isBrother(node)
if (left <= checkRight && right >= this.cloneNodeLeft) {
if (this.cloneNodeTop > bottom && this.cloneNodeTop <= bottom + 10) {
this.prevNode = node
this.placeholder.size(node.width, 10).move(_left, _bottom)
} else if (checkBottom < top && checkBottom >= top - 10) {
this.nextNode = node
this.placeholder.size(node.width, 10).move(_left, _top - 10)
}
}

View File

@@ -0,0 +1,133 @@
import EventEmitter from 'eventemitter3'
// 事件类
class Event extends EventEmitter {
// 构造函数
constructor(opt = {}) {
super()
this.opt = opt
this.mindMap = opt.mindMap
this.isLeftMousedown = false
this.mousedownPos = {
x: 0,
y: 0
}
this.mousemovePos = {
x: 0,
y: 0
}
this.mousemoveOffset = {
x: 0,
y: 0
}
this.bindFn()
this.bind()
}
// 绑定函数上下文
bindFn() {
this.onDrawClick = this.onDrawClick.bind(this)
this.onMousedown = this.onMousedown.bind(this)
this.onMousemove = this.onMousemove.bind(this)
this.onMouseup = this.onMouseup.bind(this)
this.onMousewheel = this.onMousewheel.bind(this)
this.onContextmenu = this.onContextmenu.bind(this)
this.onSvgMousedown = this.onSvgMousedown.bind(this)
this.onKeyup = this.onKeyup.bind(this)
}
// 绑定事件
bind() {
this.mindMap.svg.on('click', this.onDrawClick)
this.mindMap.el.addEventListener('mousedown', this.onMousedown)
this.mindMap.svg.on('mousedown', this.onSvgMousedown)
window.addEventListener('mousemove', this.onMousemove)
window.addEventListener('mouseup', this.onMouseup)
// 兼容火狐浏览器
if (window.navigator.userAgent.toLowerCase().indexOf('firefox') != -1) {
this.mindMap.el.addEventListener('DOMMouseScroll', this.onMousewheel)
} else {
this.mindMap.el.addEventListener('mousewheel', this.onMousewheel)
}
this.mindMap.svg.on('contextmenu', this.onContextmenu)
window.addEventListener('keyup', this.onKeyup)
}
// 解绑事件
unbind() {
this.mindMap.svg.off('click', this.onDrawClick)
this.mindMap.el.removeEventListener('mousedown', this.onMousedown)
window.removeEventListener('mousemove', this.onMousemove)
window.removeEventListener('mouseup', this.onMouseup)
this.mindMap.el.removeEventListener('mousewheel', this.onMousewheel)
this.mindMap.svg.off('contextmenu', this.onContextmenu)
window.removeEventListener('keyup', this.onKeyup)
}
// 画布的单击事件
onDrawClick(e) {
this.emit('draw_click', e)
}
// svg画布的鼠标按下事件
onSvgMousedown(e) {
this.emit('svg_mousedown', e)
}
// 鼠标按下事件
onMousedown(e) {
// e.preventDefault()
// 鼠标左键
if (e.which === 1) {
this.isLeftMousedown = true
}
this.mousedownPos.x = e.clientX
this.mousedownPos.y = e.clientY
this.emit('mousedown', e, this)
}
// 鼠标移动事件
onMousemove(e) {
// e.preventDefault()
this.mousemovePos.x = e.clientX
this.mousemovePos.y = e.clientY
this.mousemoveOffset.x = e.clientX - this.mousedownPos.x
this.mousemoveOffset.y = e.clientY - this.mousedownPos.y
this.emit('mousemove', e, this)
if (this.isLeftMousedown) {
this.emit('drag', e, this)
}
}
// 鼠标松开事件
onMouseup(e) {
this.isLeftMousedown = false
this.emit('mouseup', e, this)
}
// 鼠标滚动
onMousewheel(e) {
e.stopPropagation()
e.preventDefault()
let dir
if ((e.wheelDeltaY || e.detail) > 0) {
dir = 'up'
} else {
dir = 'down'
}
this.emit('mousewheel', e, dir, this)
}
// 鼠标右键菜单事件
onContextmenu(e) {
e.preventDefault()
this.emit('contextmenu', e)
}
// 按键松开事件
onKeyup(e) {
this.emit('keyup', e)
}
}
export default Event

View File

@@ -0,0 +1,218 @@
import { imgToDataUrl, downloadFile } from './utils'
import JsPDF from 'jspdf'
import { SVG } from '@svgdotjs/svg.js'
const URL = window.URL || window.webkitURL || window
// 导出类
class Export {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
this.exportPadding = this.mindMap.opt.exportPadding
}
// 导出
async export(type, isDownload = true, name = '思维导图', ...args) {
if (this[type]) {
let result = await this[type](name, ...args)
if (isDownload && type !== 'pdf') {
downloadFile(result, name + '.' + type)
}
return result
} else {
return null
}
}
// 获取svg数据
async getSvgData() {
let { svg, svgHTML } = this.mindMap.miniMap.getMiniMap()
// 把图片的url转换成data:url类型否则导出会丢失图片
let imageList = svg.find('image')
let task = imageList.map(async item => {
let imgUlr = item.attr('href') || item.attr('xlink:href')
let imgData = await imgToDataUrl(imgUlr)
item.attr('href', imgData)
})
await Promise.all(task)
return {
node: svg,
str: svgHTML
}
}
// svg转png
svgToPng(svgSrc) {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
img.setAttribute('crossOrigin', 'anonymous')
img.onload = async () => {
try {
let canvas = document.createElement('canvas')
canvas.width = img.width + this.exportPadding * 2
canvas.height = img.height + this.exportPadding * 2
let ctx = canvas.getContext('2d')
// 绘制背景
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
// 图片绘制到canvas里
ctx.drawImage(
img,
0,
0,
img.width,
img.height,
this.exportPadding,
this.exportPadding,
img.width,
img.height
)
resolve(canvas.toDataURL())
} catch (error) {
reject(error)
}
}
img.onerror = e => {
reject(e)
}
img.src = svgSrc
})
}
// 在canvas上绘制思维导图背景
drawBackgroundToCanvas(ctx, width, height) {
return new Promise((resolve, reject) => {
let {
backgroundColor = '#fff',
backgroundImage,
backgroundRepeat = 'repeat'
} = this.mindMap.themeConfig
// 背景颜色
ctx.save()
ctx.rect(0, 0, width, height)
ctx.fillStyle = backgroundColor
ctx.fill()
ctx.restore()
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
ctx.save()
let img = new Image()
img.src = backgroundImage
img.onload = () => {
let pat = ctx.createPattern(img, backgroundRepeat)
ctx.rect(0, 0, width, height)
ctx.fillStyle = pat
ctx.fill()
ctx.restore()
resolve()
}
img.onerror = e => {
reject(e)
}
} else {
resolve()
}
})
}
// 导出为png
/**
* 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png() {
let { str } = await this.getSvgData()
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'
})
// 转换成data:url数据
let svgUrl = URL.createObjectURL(blob)
// 绘制到canvas上
let imgDataUrl = await this.svgToPng(svgUrl)
URL.revokeObjectURL(svgUrl)
return imgDataUrl
}
// 导出为pdf
async pdf(name) {
let img = await this.png()
let pdf = new JsPDF('', 'pt', 'a4')
let a4Width = 595
let a4Height = 841
let a4Ratio = a4Width / a4Height
let image = new Image()
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
let imageRatio = imageWidth / imageHeight
let w, h
if (imageWidth <= a4Width && imageHeight <= a4Height) {
// 使用图片原始宽高
w = imageWidth
h = imageHeight
} else if (a4Ratio > imageRatio) {
// 以a4Height为高度缩放图片宽度
w = imageRatio * a4Height
h = a4Height
} else {
// 以a4Width为宽度缩放图片高度
w = a4Width
h = a4Width / imageRatio
}
pdf.addImage(img, 'PNG', (a4Width - w) / 2, (a4Height - h) / 2, w, h)
pdf.save(name)
}
image.src = img
}
// 在svg上绘制思维导图背景
drawBackgroundToSvg(svg) {
return new Promise(async resolve => {
let {
backgroundColor = '#fff',
backgroundImage,
backgroundRepeat = 'repeat'
} = this.mindMap.themeConfig
// 背景颜色
svg.css('background-color', backgroundColor)
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
let imgDataUrl = await imgToDataUrl(backgroundImage)
svg.css('background-image', `url(${imgDataUrl})`)
svg.css('background-repeat', backgroundRepeat)
resolve()
} else {
resolve()
}
})
}
// 导出为svg
async svg(name) {
let { node } = await this.getSvgData()
node.first().before(SVG(`<title>${name}</title>`))
await this.drawBackgroundToSvg(node)
let str = node.svg()
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'
})
return URL.createObjectURL(blob)
}
// 导出为json
json(name, withConfig = true) {
let data = this.mindMap.getData(withConfig)
let str = JSON.stringify(data)
let blob = new Blob([str])
return URL.createObjectURL(blob)
}
// 专有文件其实就是json文件
smm(name, withConfig) {
return this.json(name, withConfig)
}
}
export default Export

View File

@@ -0,0 +1,147 @@
import { keyMap } from './utils/keyMap'
// 快捷按键、命令处理类
export default class KeyCommand {
// 构造函数
constructor(opt) {
this.opt = opt
this.mindMap = opt.mindMap
this.shortcutMap = {
//Enter: [fn]
}
this.shortcutMapCache = {}
this.isPause = false
this.bindEvent()
}
// 暂停快捷键响应
pause() {
this.isPause = true
}
// 恢复快捷键响应
recovery() {
this.isPause = false
}
// 保存当前注册的快捷键数据,然后清空快捷键数据
save() {
this.shortcutMapCache = this.shortcutMap
this.shortcutMap = {}
}
// 恢复保存的快捷键数据,然后清空缓存数据
restore() {
this.shortcutMap = this.shortcutMapCache
this.shortcutMapCache = {}
}
// 绑定事件
bindEvent() {
window.addEventListener('keydown', e => {
if (this.isPause) {
return
}
Object.keys(this.shortcutMap).forEach(key => {
if (this.checkKey(e, key)) {
e.stopPropagation()
e.preventDefault()
this.shortcutMap[key].forEach(fn => {
fn()
})
}
})
})
}
// 检查键值是否符合
checkKey(e, key) {
let o = this.getOriginEventCodeArr(e)
let k = this.getKeyCodeArr(key)
if (o.length !== k.length) {
return false
}
for (let i = 0; i < o.length; i++) {
let index = k.findIndex(item => {
return item === o[i]
})
if (index === -1) {
return false
} else {
k.splice(index, 1)
}
}
return true
}
// 获取事件对象里的键值数组
getOriginEventCodeArr(e) {
let arr = []
if (e.ctrlKey || e.metaKey) {
arr.push(keyMap['Control'])
}
if (e.altKey) {
arr.push(keyMap['Alt'])
}
if (e.shiftKey) {
arr.push(keyMap['Shift'])
}
if (!arr.includes(e.keyCode)) {
arr.push(e.keyCode)
}
return arr
}
// 获取快捷键对应的键值数组
getKeyCodeArr(key) {
let keyArr = key.split(/\s*\+\s*/)
let arr = []
keyArr.forEach(item => {
arr.push(keyMap[item])
})
return arr
}
// 添加快捷键命令
/**
* Enter
* Tab | Insert
* Shift + a
*/
addShortcut(key, fn) {
key.split(/\s*\|\s*/).forEach(item => {
if (this.shortcutMap[item]) {
this.shortcutMap[item].push(fn)
} else {
this.shortcutMap[item] = [fn]
}
})
}
// 移除快捷键命令
removeShortcut(key, fn) {
key.split(/\s*\|\s*/).forEach(item => {
if (this.shortcutMap[item]) {
if (fn) {
let index = this.shortcutMap[item].findIndex(f => {
return f === fn
})
if (index !== -1) {
this.shortcutMap[item].splice(index, 1)
}
} else {
this.shortcutMap[item] = []
delete this.shortcutMap[item]
}
}
})
}
// 获取指定快捷键的处理函数
getShortcutFn(key) {
let res = []
key.split(/\s*\|\s*/).forEach(item => {
res = this.shortcutMap[item] || []
})
return res
}
}

View File

@@ -0,0 +1,226 @@
import { isKey } from './utils/keyMap'
import { bfsWalk } from './utils'
// 键盘导航类
export default class KeyboardNavigation {
// 构造函数
constructor(opt) {
this.opt = opt
this.mindMap = opt.mindMap
this.onKeyup = this.onKeyup.bind(this)
this.mindMap.on('keyup', this.onKeyup)
}
// 处理按键事件
onKeyup(e) {
;['Left', 'Up', 'Right', 'Down'].forEach(dir => {
if (isKey(e, dir)) {
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.focus(dir)
} else {
let root = this.mindMap.renderer.root
this.mindMap.renderer.moveNodeToCenter(root)
root.active()
}
}
})
}
// 聚焦到下一个节点
focus(dir) {
// 当前聚焦的节点
let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
// 当前聚焦节点的位置信息
let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
// 寻找的下一个聚焦节点
let targetNode = null
let targetDis = Infinity
// 保存并维护距离最近的节点
let checkNodeDis = (rect, node) => {
let dis = this.getDistance(currentActiveNodeRect, rect)
if (dis < targetDis) {
targetNode = node
targetDis = dis
}
}
// 第一优先级:阴影算法
this.getFocusNodeByShadowAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})
// 第二优先级:区域算法
if (!targetNode) {
this.getFocusNodeByAreaAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})
}
// 第三优先级:简单算法
if (!targetNode) {
this.getFocusNodeBySimpleAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})
}
// 找到了则让目标节点聚焦
if (targetNode) {
this.mindMap.renderer.moveNodeToCenter(targetNode)
targetNode.active()
}
}
// 1.简单算法
getFocusNodeBySimpleAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
}) {
// 遍历节点树
bfsWalk(this.mindMap.renderer.root, node => {
// 跳过当前聚焦的节点
if (node === currentActiveNode) return
// 当前遍历到的节点的位置信息
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
// 按下了左方向键
if (dir === 'Left') {
// 判断节点是否在当前节点的左侧
match = right <= currentActiveNodeRect.left
// 按下了右方向键
} else if (dir === 'Right') {
// 判断节点是否在当前节点的右侧
match = left >= currentActiveNodeRect.right
// 按下了上方向键
} else if (dir === 'Up') {
// 判断节点是否在当前节点的上面
match = bottom <= currentActiveNodeRect.top
// 按下了下方向键
} else if (dir === 'Down') {
// 判断节点是否在当前节点的下面
match = top >= currentActiveNodeRect.bottom
}
// 符合要求,判断是否是最近的节点
if (match) {
checkNodeDis(rect, node)
}
})
}
// 2.阴影算法
getFocusNodeByShadowAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
}) {
bfsWalk(this.mindMap.renderer.root, node => {
if (node === currentActiveNode) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
if (dir === 'Left') {
match =
left < currentActiveNodeRect.left &&
top < currentActiveNodeRect.bottom &&
bottom > currentActiveNodeRect.top
} else if (dir === 'Right') {
match =
right > currentActiveNodeRect.right &&
top < currentActiveNodeRect.bottom &&
bottom > currentActiveNodeRect.top
} else if (dir === 'Up') {
match =
top < currentActiveNodeRect.top &&
left < currentActiveNodeRect.right &&
right > currentActiveNodeRect.left
} else if (dir === 'Down') {
match =
bottom > currentActiveNodeRect.bottom &&
left < currentActiveNodeRect.right &&
right > currentActiveNodeRect.left
}
if (match) {
checkNodeDis(rect, node)
}
})
}
// 3.区域算法
getFocusNodeByAreaAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
}) {
// 当前聚焦节点的中心点
let cX = (currentActiveNodeRect.right + currentActiveNodeRect.left) / 2
let cY = (currentActiveNodeRect.bottom + currentActiveNodeRect.top) / 2
bfsWalk(this.mindMap.renderer.root, node => {
if (node === currentActiveNode) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
// 遍历到的节点的中心点
let ccX = (right + left) / 2
let ccY = (bottom + top) / 2
// 节点的中心点坐标和当前聚焦节点的中心点坐标的差值
let offsetX = ccX - cX
let offsetY = ccY - cY
if (offsetX === 0 && offsetY === 0) return
let match = false
if (dir === 'Left') {
match = offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY
} else if (dir === 'Right') {
match = offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY
} else if (dir === 'Up') {
match = offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX
} else if (dir === 'Down') {
match = offsetY > 0 && -offsetY < offsetX && offsetY > offsetX
}
if (match) {
checkNodeDis(rect, node)
}
})
}
// 获取节点的位置信息
getNodeRect(node) {
let { scaleX, scaleY, translateX, translateY } =
this.mindMap.draw.transform()
let { left, top, width, height } = node
return {
right: (left + width) * scaleX + translateX,
bottom: (top + height) * scaleY + translateY,
left: left * scaleX + translateX,
top: top * scaleY + translateY
}
}
// 获取两个节点的距离
getDistance(node1Rect, node2Rect) {
let center1 = this.getCenter(node1Rect)
let center2 = this.getCenter(node2Rect)
return Math.sqrt(
Math.pow(center1.x - center2.x, 2) + Math.pow(center1.y - center2.y, 2)
)
}
// 获取节点的中心点
getCenter({ left, right, top, bottom }) {
return {
x: (left + right) / 2,
y: (top + bottom) / 2
}
}
}

View File

@@ -0,0 +1,147 @@
// 小地图类
class MiniMap {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
this.isMousedown = false
this.mousedownPos = {
x: 0,
y: 0
}
this.startViewPos = {
x: 0,
y: 0
}
}
// 获取小地图相关数据
getMiniMap() {
const svg = this.mindMap.svg
const draw = this.mindMap.draw
// 保存原始信息
const origWidth = svg.width()
const origHeight = svg.height()
const origTransform = draw.transform()
const elRect = this.mindMap.el.getBoundingClientRect()
// 去除放大缩小的变换效果
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
// 克隆一份数据
const clone = svg.clone()
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight)
draw.transform(origTransform)
return {
svg: clone, // 思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组
svgHTML: clone.svg(), // svg字符串
rect: {
...rect, // 思维导图图形未缩放时的位置尺寸等信息
ratio: rect.width / rect.height // 思维导图图形的宽高比
},
origWidth, // 画布宽度
origHeight, // 画布高度
scaleX: origTransform.scaleX, // 思维导图图形的水平缩放值
scaleY: origTransform.scaleY // 思维导图图形的垂直缩放值
}
}
// 计算小地图的渲染数据
/**
* boxWidth小地图容器的宽度
* boxHeight小地图容器的高度
*/
calculationMiniMap(boxWidth, boxHeight) {
let { svgHTML, rect, origWidth, origHeight, scaleX, scaleY } =
this.getMiniMap()
// 计算数据
let boxRatio = boxWidth / boxHeight
let actWidth = 0
let actHeight = 0
if (boxRatio > rect.ratio) {
// 高度以box为准缩放宽度
actHeight = boxHeight
actWidth = rect.ratio * actHeight
} else {
// 宽度以box为准缩放高度
actWidth = boxWidth
actHeight = actWidth / rect.ratio
}
// svg图形的缩放及位置
let miniMapBoxScale = actWidth / rect.width
let miniMapBoxLeft = (boxWidth - actWidth) / 2
let miniMapBoxTop = (boxHeight - actHeight) / 2
// 视口框大小及位置
let _rectX = rect.x - (rect.width * scaleX - rect.width) / 2
let _rectX2 = rect.x2 + (rect.width * scaleX - rect.width) / 2
let _rectY = rect.y - (rect.height * scaleY - rect.height) / 2
let _rectY2 = rect.y2 + (rect.height * scaleY - rect.height) / 2
let _rectWidth = rect.width * scaleX
let _rectHeight = rect.height * scaleY
let viewBoxStyle = {
left: 0,
top: 0,
right: 0,
bottom: 0
}
viewBoxStyle.left =
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft + 'px'
viewBoxStyle.right =
Math.max(0, ((_rectX2 - origWidth) / _rectWidth) * actWidth) +
miniMapBoxLeft +
'px'
viewBoxStyle.top =
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop + 'px'
viewBoxStyle.bottom =
Math.max(0, ((_rectY2 - origHeight) / _rectHeight) * actHeight) +
miniMapBoxTop +
'px'
return {
svgHTML, // 小地图html
viewBoxStyle, // 视图框的位置信息
miniMapBoxScale, // 视图框的缩放值
miniMapBoxLeft, // 视图框的left值
miniMapBoxTop // 视图框的top值
}
}
// 小地图鼠标按下事件
onMousedown(e) {
this.isMousedown = true
this.mousedownPos = {
x: e.clientX,
y: e.clientY
}
// 保存视图当前的偏移量
let transformData = this.mindMap.view.getTransformData()
this.startViewPos = {
x: transformData.state.x,
y: transformData.state.y
}
}
// 小地图鼠标移动事件
onMousemove(e, sensitivityNum = 5) {
if (!this.isMousedown) {
return
}
let ox = e.clientX - this.mousedownPos.x
let oy = e.clientY - this.mousedownPos.y
// 在视图最初偏移量上累加更新量
this.mindMap.view.translateXTo(ox * sensitivityNum + this.startViewPos.x)
this.mindMap.view.translateYTo(oy * sensitivityNum + this.startViewPos.y)
}
// 小地图鼠标松开事件
onMouseup() {
this.isMousedown = false
}
}
export default MiniMap

1141
simple-mind-map/src/Node.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,893 @@
import merge from 'deepmerge'
import LogicalStructure from './layouts/LogicalStructure'
import MindMap from './layouts/MindMap'
import CatalogOrganization from './layouts/CatalogOrganization'
import OrganizationStructure from './layouts/OrganizationStructure'
import TextEdit from './TextEdit'
import { copyNodeTree, simpleDeepClone, walk } from './utils'
import { shapeList } from './Shape'
import { lineStyleProps } from './themes/default'
// 布局列表
const layouts = {
// 逻辑结构图
logicalStructure: LogicalStructure,
// 思维导图
mindMap: MindMap,
// 目录组织图
catalogOrganization: CatalogOrganization,
// 组织结构图
organizationStructure: OrganizationStructure
}
// 渲染
class Render {
// 构造函数
constructor(opt = {}) {
this.opt = opt
this.mindMap = opt.mindMap
this.themeConfig = this.mindMap.themeConfig
this.draw = this.mindMap.draw
// 渲染树,操作过程中修改的都是这里的数据
this.renderTree = merge({}, this.mindMap.opt.data || {})
// 是否重新渲染
this.reRender = false
// 当前激活的节点列表
this.activeNodeList = []
// 根节点
this.root = null
// 文本编辑框需要再bindEvent之前实例化否则单击事件只能触发隐藏文本编辑框而无法保存文本修改
this.textEdit = new TextEdit(this)
// 布局
this.setLayout()
// 绑定事件
this.bindEvent()
// 注册命令
this.registerCommands()
// 注册快捷键
this.registerShortcutKeys()
}
// 设置布局结构
setLayout() {
this.layout = new (
layouts[this.mindMap.opt.layout]
? layouts[this.mindMap.opt.layout]
: layouts.logicalStructure
)(this)
}
// 绑定事件
bindEvent() {
// 点击事件
this.mindMap.on('draw_click', () => {
// 清除激活状态
if (this.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
})
}
// 注册命令
registerCommands() {
// 全选
this.selectAll = this.selectAll.bind(this)
this.mindMap.command.add('SELECT_ALL', this.selectAll)
// 回退
this.back = this.back.bind(this)
this.mindMap.command.add('BACK', this.back)
// 前进
this.forward = this.forward.bind(this)
this.mindMap.command.add('FORWARD', this.forward)
// 插入同级节点
this.insertNode = this.insertNode.bind(this)
this.mindMap.command.add('INSERT_NODE', this.insertNode)
// 插入子节点
this.insertChildNode = this.insertChildNode.bind(this)
this.mindMap.command.add('INSERT_CHILD_NODE', this.insertChildNode)
// 上移节点
this.upNode = this.upNode.bind(this)
this.mindMap.command.add('UP_NODE', this.upNode)
// 下移节点
this.downNode = this.downNode.bind(this)
this.mindMap.command.add('DOWN_NODE', this.downNode)
// 移动节点
this.insertAfter = this.insertAfter.bind(this)
this.mindMap.command.add('INSERT_AFTER', this.insertAfter)
this.insertBefore = this.insertBefore.bind(this)
this.mindMap.command.add('INSERT_BEFORE', this.insertBefore)
this.moveNodeTo = this.moveNodeTo.bind(this)
this.mindMap.command.add('MOVE_NODE_TO', this.moveNodeTo)
// 删除节点
this.removeNode = this.removeNode.bind(this)
this.mindMap.command.add('REMOVE_NODE', this.removeNode)
// 粘贴节点
this.pasteNode = this.pasteNode.bind(this)
this.mindMap.command.add('PASTE_NODE', this.pasteNode)
// 剪切节点
this.cutNode = this.cutNode.bind(this)
this.mindMap.command.add('CUT_NODE', this.cutNode)
// 修改节点样式
this.setNodeStyle = this.setNodeStyle.bind(this)
this.mindMap.command.add('SET_NODE_STYLE', this.setNodeStyle)
// 切换节点是否激活
this.setNodeActive = this.setNodeActive.bind(this)
this.mindMap.command.add('SET_NODE_ACTIVE', this.setNodeActive)
// 清除所有激活节点
this.clearAllActive = this.clearAllActive.bind(this)
this.mindMap.command.add('CLEAR_ACTIVE_NODE', this.clearAllActive)
// 切换节点是否展开
this.setNodeExpand = this.setNodeExpand.bind(this)
this.mindMap.command.add('SET_NODE_EXPAND', this.setNodeExpand)
// 展开所有节点
this.expandAllNode = this.expandAllNode.bind(this)
this.mindMap.command.add('EXPAND_ALL', this.expandAllNode)
// 收起所有节点
this.unexpandAllNode = this.unexpandAllNode.bind(this)
this.mindMap.command.add('UNEXPAND_ALL', this.unexpandAllNode)
// 展开到指定层级
this.expandToLevel = this.expandToLevel.bind(this)
this.mindMap.command.add('UNEXPAND_TO_LEVEL', this.expandToLevel)
// 设置节点数据
this.setNodeData = this.setNodeData.bind(this)
this.mindMap.command.add('SET_NODE_DATA', this.setNodeData)
// 设置节点文本
this.setNodeText = this.setNodeText.bind(this)
this.mindMap.command.add('SET_NODE_TEXT', this.setNodeText)
// 设置节点图片
this.setNodeImage = this.setNodeImage.bind(this)
this.mindMap.command.add('SET_NODE_IMAGE', this.setNodeImage)
// 设置节点图标
this.setNodeIcon = this.setNodeIcon.bind(this)
this.mindMap.command.add('SET_NODE_ICON', this.setNodeIcon)
// 设置节点超链接
this.setNodeHyperlink = this.setNodeHyperlink.bind(this)
this.mindMap.command.add('SET_NODE_HYPERLINK', this.setNodeHyperlink)
// 设置节点备注
this.setNodeNote = this.setNodeNote.bind(this)
this.mindMap.command.add('SET_NODE_NOTE', this.setNodeNote)
// 设置节点标签
this.setNodeTag = this.setNodeTag.bind(this)
this.mindMap.command.add('SET_NODE_TAG', this.setNodeTag)
// 添加节点概要
this.addGeneralization = this.addGeneralization.bind(this)
this.mindMap.command.add('ADD_GENERALIZATION', this.addGeneralization)
// 删除节点概要
this.removeGeneralization = this.removeGeneralization.bind(this)
this.mindMap.command.add('REMOVE_GENERALIZATION', this.removeGeneralization)
// 设置节点自定义位置
this.setNodeCustomPosition = this.setNodeCustomPosition.bind(this)
this.mindMap.command.add(
'SET_NODE_CUSTOM_POSITION',
this.setNodeCustomPosition
)
// 一键整理布局
this.resetLayout = this.resetLayout.bind(this)
this.mindMap.command.add('RESET_LAYOUT', this.resetLayout)
// 设置节点形状
this.setNodeShape = this.setNodeShape.bind(this)
this.mindMap.command.add('SET_NODE_SHAPE', this.setNodeShape)
}
// 注册快捷键
registerShortcutKeys() {
// 插入下级节点
this.mindMap.keyCommand.addShortcut('Tab', () => {
this.mindMap.execCommand('INSERT_CHILD_NODE')
})
// 插入同级节点
this.insertNodeWrap = () => {
if (this.textEdit.showTextEdit) {
return
}
this.mindMap.execCommand('INSERT_NODE')
}
this.mindMap.keyCommand.addShortcut('Enter', this.insertNodeWrap)
// 插入概要
this.mindMap.keyCommand.addShortcut('Control+s', this.addGeneralization)
// 展开/收起节点
this.toggleActiveExpand = this.toggleActiveExpand.bind(this)
this.mindMap.keyCommand.addShortcut('/', this.toggleActiveExpand)
// 删除节点
this.removeNodeWrap = () => {
this.mindMap.execCommand('REMOVE_NODE')
}
this.mindMap.keyCommand.addShortcut('Del|Backspace', this.removeNodeWrap)
// 节点编辑时某些快捷键会存在冲突,需要暂时去除
this.mindMap.on('before_show_text_edit', () => {
this.startTextEdit()
})
this.mindMap.on('hide_text_edit', () => {
this.endTextEdit()
})
// 全选
this.mindMap.keyCommand.addShortcut('Control+a', () => {
this.mindMap.execCommand('SELECT_ALL')
})
// 一键整理布局
this.mindMap.keyCommand.addShortcut('Control+l', this.resetLayout)
// 上移节点
this.mindMap.keyCommand.addShortcut('Control+Up', this.upNode)
// 下移节点
this.mindMap.keyCommand.addShortcut('Control+Down', this.downNode)
// 复制节点、剪切节点、粘贴节点的快捷键需开发者自行注册实现可参考demo
}
// 开启文字编辑,会禁用回车键和删除键相关快捷键防止冲突
startTextEdit() {
this.mindMap.keyCommand.save()
// this.mindMap.keyCommand.removeShortcut('Del|Backspace')
// this.mindMap.keyCommand.removeShortcut('/')
// this.mindMap.keyCommand.removeShortcut('Enter', this.insertNodeWrap)
}
// 结束文字编辑,会恢复回车键和删除键相关快捷键
endTextEdit() {
this.mindMap.keyCommand.restore()
// this.mindMap.keyCommand.addShortcut('Del|Backspace', this.removeNodeWrap)
// this.mindMap.keyCommand.addShortcut('/', this.toggleActiveExpand)
// this.mindMap.keyCommand.addShortcut('Enter', this.insertNodeWrap)
}
// 渲染
render() {
if (this.reRender) {
this.clearActive()
}
this.layout.doLayout(root => {
this.root = root
this.root.render(() => {
this.mindMap.emit('node_tree_render_end')
})
})
this.mindMap.emit('node_active', null, this.activeNodeList)
}
// 清除当前激活的节点
clearActive() {
this.activeNodeList.forEach(item => {
this.setNodeActive(item, false)
})
this.activeNodeList = []
}
// 清除当前所有激活节点,并会触发事件
clearAllActive() {
if (this.activeNodeList.length <= 0) {
return
}
this.clearActive()
this.mindMap.emit('node_active', null, [])
}
// 添加节点到激活列表里
addActiveNode(node) {
let index = this.findActiveNodeIndex(node)
if (index === -1) {
this.activeNodeList.push(node)
}
}
// 在激活列表里移除某个节点
removeActiveNode(node) {
let index = this.findActiveNodeIndex(node)
if (index === -1) {
return
}
this.activeNodeList.splice(index, 1)
}
// 检索某个节点在激活列表里的索引
findActiveNodeIndex(node) {
return this.activeNodeList.findIndex(item => {
return item === node
})
}
// 获取节点在同级里的索引位置
getNodeIndex(node) {
return node.parent
? node.parent.children.findIndex(item => {
return item === node
})
: 0
}
// 全选
selectAll() {
walk(
this.root,
null,
node => {
if (!node.nodeData.data.isActive) {
node.nodeData.data.isActive = true
this.addActiveNode(node)
setTimeout(() => {
node.renderNode()
}, 0)
}
},
null,
true,
0,
0
)
}
// 回退
back(step) {
this.clearAllActive()
let data = this.mindMap.command.back(step)
if (data) {
this.renderTree = data
this.mindMap.reRender()
}
}
// 前进
forward(step) {
this.clearAllActive()
let data = this.mindMap.command.forward(step)
if (data) {
this.renderTree = data
this.mindMap.reRender()
}
}
// 插入同级节点,多个节点只会操作第一个节点
insertNode() {
if (this.activeNodeList.length <= 0) {
return
}
let first = this.activeNodeList[0]
if (first.isRoot) {
this.insertChildNode()
} else {
let text = first.layerIndex === 1 ? '二级节点' : '分支主题'
if (first.layerIndex === 1) {
first.parent.initRender = true
}
let index = this.getNodeIndex(first)
first.parent.nodeData.children.splice(index + 1, 0, {
inserting: true,
data: {
text: text,
expand: true
},
children: []
})
this.mindMap.render()
}
}
// 插入子节点
insertChildNode() {
if (this.activeNodeList.length <= 0) {
return
}
this.activeNodeList.forEach(node => {
if (!node.nodeData.children) {
node.nodeData.children = []
}
let text = node.isRoot ? '二级节点' : '分支主题'
node.nodeData.children.push({
inserting: true,
data: {
text: text,
expand: true
},
children: []
})
// 插入子节点时自动展开子节点
node.nodeData.data.expand = true
if (node.isRoot) {
node.initRender = true
// this.mindMap.batchExecution.push('renderNode' + index, () => {
// node.renderNode()
// })
}
})
this.mindMap.render()
}
// 上移节点,多个节点只会操作第一个节点
upNode() {
if (this.activeNodeList.length <= 0) {
return
}
let node = this.activeNodeList[0]
if (node.isRoot) {
return
}
let parent = node.parent
let childList = parent.children
let index = childList.findIndex(item => {
return item === node
})
if (index === -1 || index === 0) {
return
}
let insertIndex = index - 1
// 节点实例
childList.splice(index, 1)
childList.splice(insertIndex, 0, node)
// 节点数据
parent.nodeData.children.splice(index, 1)
parent.nodeData.children.splice(insertIndex, 0, node.nodeData)
this.mindMap.render()
}
// 下移节点,多个节点只会操作第一个节点
downNode() {
if (this.activeNodeList.length <= 0) {
return
}
let node = this.activeNodeList[0]
if (node.isRoot) {
return
}
let parent = node.parent
let childList = parent.children
let index = childList.findIndex(item => {
return item === node
})
if (index === -1 || index === childList.length - 1) {
return
}
let insertIndex = index + 1
// 节点实例
childList.splice(index, 1)
childList.splice(insertIndex, 0, node)
// 节点数据
parent.nodeData.children.splice(index, 1)
parent.nodeData.children.splice(insertIndex, 0, node.nodeData)
this.mindMap.render()
}
// 将节点移动到另一个节点的前面
insertBefore(node, exist) {
if (node.isRoot) {
return
}
// 移动节点
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
let nodeIndex = nodeBorthers.findIndex(item => {
return item === node
})
if (nodeIndex === -1) {
return
}
nodeBorthers.splice(nodeIndex, 1)
nodeParent.nodeData.children.splice(nodeIndex, 1)
// 目标节点
let existParent = exist.parent
let existBorthers = existParent.children
let existIndex = existBorthers.findIndex(item => {
return item === exist
})
if (existIndex === -1) {
return
}
existBorthers.splice(existIndex, 0, node)
existParent.nodeData.children.splice(existIndex, 0, node.nodeData)
this.mindMap.render()
}
// 将节点移动到另一个节点的后面
insertAfter(node, exist) {
if (node.isRoot) {
return
}
// 移动节点
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
let nodeIndex = nodeBorthers.findIndex(item => {
return item === node
})
if (nodeIndex === -1) {
return
}
nodeBorthers.splice(nodeIndex, 1)
nodeParent.nodeData.children.splice(nodeIndex, 1)
// 目标节点
let existParent = exist.parent
let existBorthers = existParent.children
let existIndex = existBorthers.findIndex(item => {
return item === exist
})
if (existIndex === -1) {
return
}
existIndex++
existBorthers.splice(existIndex, 0, node)
existParent.nodeData.children.splice(existIndex, 0, node.nodeData)
this.mindMap.render()
}
// 移除节点
removeNode() {
if (this.activeNodeList.length <= 0) {
return
}
for (let i = 0; i < this.activeNodeList.length; i++) {
let node = this.activeNodeList[i]
if (node.isGeneralization) {
// 删除概要节点
this.setNodeData(node.generalizationBelongNode, {
generalization: null
})
node.generalizationBelongNode.update()
this.removeActiveNode(node)
i--
} else if (node.isRoot) {
node.children.forEach(child => {
child.remove()
})
node.children = []
node.nodeData.children = []
break
} else {
this.removeActiveNode(node)
this.removeOneNode(node)
i--
}
}
this.mindMap.emit('node_active', null, [])
this.mindMap.render()
}
// 移除某个指定节点
removeOneNode(node) {
let index = this.getNodeIndex(node)
node.remove()
node.parent.children.splice(index, 1)
node.parent.nodeData.children.splice(index, 1)
}
// 复制节点,多个节点只会操作第一个节点
copyNode() {
if (this.activeNodeList.length <= 0) {
return
}
return copyNodeTree({}, this.activeNodeList[0], true)
}
// 剪切节点,多个节点只会操作第一个节点
cutNode(callback) {
if (this.activeNodeList.length <= 0) {
return
}
let node = this.activeNodeList[0]
if (node.isRoot) {
return null
}
let copyData = copyNodeTree({}, node, true)
this.removeActiveNode(node)
this.removeOneNode(node)
this.mindMap.emit('node_active', null, this.activeNodeList)
this.mindMap.render()
if (callback && typeof callback === 'function') {
callback(copyData)
}
}
// 移动一个节点作为另一个节点的子节点
moveNodeTo(node, toNode) {
if (node.isRoot) {
return
}
let copyData = copyNodeTree({}, node)
this.removeActiveNode(node)
this.removeOneNode(node)
this.mindMap.emit('node_active', null, this.activeNodeList)
toNode.nodeData.children.push(copyData)
this.mindMap.render()
}
// 粘贴节点到节点
pasteNode(data) {
if (this.activeNodeList.length <= 0) {
return
}
this.activeNodeList.forEach(item => {
item.nodeData.children.push(simpleDeepClone(data))
})
this.mindMap.render()
}
// 设置节点样式
setNodeStyle(node, prop, value, isActive) {
let data = {}
if (isActive) {
data = {
activeStyle: {
...(node.nodeData.data.activeStyle || {}),
[prop]: value
}
}
} else {
data = {
[prop]: value
}
}
this.setNodeDataRender(node, data)
// 更新了连线的样式
if (lineStyleProps.includes(prop)) {
;(node.parent || node).renderLine(true)
}
}
// 设置节点是否激活
setNodeActive(node, active) {
this.setNodeData(node, {
isActive: active
})
node.renderNode()
}
// 设置节点是否展开
setNodeExpand(node, expand) {
this.setNodeData(node, {
expand
})
if (expand) {
// 展开
node.children.forEach(item => {
item.render()
})
node.renderLine()
node.updateExpandBtnNode()
} else {
// 收缩
node.children.forEach(item => {
item.remove()
})
node.removeLine()
node.updateExpandBtnNode()
}
this.mindMap.render()
}
// 展开所有
expandAllNode() {
walk(
this.renderTree,
null,
node => {
if (!node.data.expand) {
node.data.expand = true
}
},
null,
true,
0,
0
)
this.mindMap.reRender()
}
// 收起所有
unexpandAllNode() {
walk(
this.renderTree,
null,
(node, parent, isRoot) => {
node._node = null
if (!isRoot) {
node.data.expand = false
}
},
null,
true,
0,
0
)
this.mindMap.reRender()
}
// 展开到指定层级
expandToLevel(level) {
walk(
this.renderTree,
null,
(node, parent, isRoot, layerIndex) => {
node._node = null
node.data.expand = layerIndex < level
},
null,
true,
0,
0
)
this.mindMap.reRender()
}
// 切换激活节点的展开状态
toggleActiveExpand() {
this.activeNodeList.forEach(node => {
if (node.nodeData.children.length <= 0) {
return
}
this.toggleNodeExpand(node)
})
}
// 切换节点展开状态
toggleNodeExpand(node) {
this.mindMap.execCommand(
'SET_NODE_EXPAND',
node,
!node.nodeData.data.expand
)
}
// 设置节点文本
setNodeText(node, text) {
this.setNodeDataRender(node, {
text
})
}
// 设置节点图片
setNodeImage(node, { url, title, width, height }) {
this.setNodeDataRender(node, {
image: url,
imageTitle: title || '',
imageSize: {
width,
height
}
})
}
// 设置节点图标
setNodeIcon(node, icons) {
this.setNodeDataRender(node, {
icon: icons
})
}
// 设置节点超链接
setNodeHyperlink(node, link, title = '') {
this.setNodeDataRender(node, {
hyperlink: link,
hyperlinkTitle: title
})
}
// 设置节点备注
setNodeNote(node, note) {
this.setNodeDataRender(node, {
note
})
}
// 设置节点标签
setNodeTag(node, tag) {
this.setNodeDataRender(node, {
tag
})
}
// 添加节点概要
addGeneralization(data) {
if (this.activeNodeList.length <= 0) {
return
}
this.activeNodeList.forEach(node => {
if (node.nodeData.data.generalization || node.isRoot) {
return
}
this.setNodeData(node, {
generalization: data || {
text: '概要'
}
})
node.update()
})
this.mindMap.render()
}
// 删除节点概要
removeGeneralization() {
if (this.activeNodeList.length <= 0) {
return
}
this.activeNodeList.forEach(node => {
if (!node.nodeData.data.generalization) {
return
}
this.setNodeData(node, {
generalization: null
})
node.update()
})
this.mindMap.render()
}
// 设置节点自定义位置
setNodeCustomPosition(node, left = undefined, top = undefined) {
let nodeList = [node] || this.activeNodeList
nodeList.forEach(item => {
this.setNodeData(item, {
customLeft: left,
customTop: top
})
})
}
// 一键整理布局,即去除自定义位置
resetLayout() {
walk(
this.root,
null,
node => {
node.customLeft = undefined
node.customTop = undefined
this.setNodeData(node, {
customLeft: undefined,
customTop: undefined
})
this.mindMap.render()
},
null,
true,
0,
0
)
}
// 设置节点形状
setNodeShape(node, shape) {

View File

@@ -0,0 +1,170 @@
import { bfsWalk, throttle } from './utils'
// 选择节点类
class Select {
// 构造函数
constructor({ mindMap }) {
this.mindMap = mindMap
this.rect = null
this.isMousedown = false
this.mouseDownX = 0
this.mouseDownY = 0
this.mouseMoveX = 0
this.mouseMoveY = 0
this.bindEvent()
}
// 绑定事件
bindEvent() {
this.checkInNodes = throttle(this.checkInNodes, 500, this)
this.mindMap.on('mousedown', e => {
if (this.mindMap.opt.readonly) {
return
}
if (!e.ctrlKey && e.which !== 3) {
return
}
this.isMousedown = true
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseDownX = x
this.mouseDownY = y
this.createRect(x, y)
})
this.mindMap.on('mousemove', e => {
if (this.mindMap.opt.readonly) {
return
}
if (!this.isMousedown) {
return
}
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseMoveX = x
this.mouseMoveY = y
if (
Math.abs(x - this.mouseDownX) <= 10 &&
Math.abs(y - this.mouseDownY) <= 10
) {
return
}
clearTimeout(this.autoMoveTimer)
this.onMove(x, y)
})
this.mindMap.on('mouseup', () => {
if (this.mindMap.opt.readonly) {
return
}
if (!this.isMousedown) {
return
}
this.mindMap.emit(
'node_active',
null,
this.mindMap.renderer.activeNodeList
)
clearTimeout(this.autoMoveTimer)
this.isMousedown = false
if (this.rect) this.rect.remove()
this.rect = null
})
}
// 鼠标移动事件
onMove(x, y) {
// 绘制矩形
this.rect.plot([
[this.mouseDownX, this.mouseDownY],
[this.mouseMoveX, this.mouseDownY],
[this.mouseMoveX, this.mouseMoveY],
[this.mouseDownX, this.mouseMoveY]
])
this.checkInNodes()
// 检测边缘移动
let step = this.mindMap.opt.selectTranslateStep
let limit = this.mindMap.opt.selectTranslateLimit
let count = 0
// 左边缘
if (x <= this.mindMap.elRect.left + limit) {
this.mouseDownX += step
this.mindMap.view.translateX(step)
count++
}
// 右边缘
if (x >= this.mindMap.elRect.right - limit) {
this.mouseDownX -= step
this.mindMap.view.translateX(-step)
count++
}
// 上边缘
if (y <= this.mindMap.elRect.top + limit) {
this.mouseDownY += step
this.mindMap.view.translateY(step)
count++
}
// 下边缘
if (y >= this.mindMap.elRect.bottom - limit) {
this.mouseDownY -= step
this.mindMap.view.translateY(-step)
count++
}
if (count > 0) {
this.startAutoMove(x, y)
}
}
// 开启自动移动
startAutoMove(x, y) {
this.autoMoveTimer = setTimeout(() => {
this.onMove(x, y)
}, 20)
}
// 创建矩形
createRect(x, y) {
this.rect = this.mindMap.svg
.polygon()
.stroke({
color: '#0984e3'
})
.fill({
color: 'rgba(9,132,227,0.3)'
})
.plot([[x, y]])
}
// 检测在选区里的节点
checkInNodes() {
let { scaleX, scaleY, translateX, translateY } =
this.mindMap.draw.transform()
let minx = Math.min(this.mouseDownX, this.mouseMoveX)
let miny = Math.min(this.mouseDownY, this.mouseMoveY)
let maxx = Math.max(this.mouseDownX, this.mouseMoveX)
let maxy = Math.max(this.mouseDownY, this.mouseMoveY)
bfsWalk(this.mindMap.renderer.root, node => {
let { left, top, width, height } = node
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
if (left >= minx && right <= maxx && top >= miny && bottom <= maxy) {
this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, true)
this.mindMap.renderer.addActiveNode(node)
})
} else if (node.nodeData.data.isActive) {
this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (!node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, false)
this.mindMap.renderer.removeActiveNode(node)
})
}
})
}
}
export default Select

View File

@@ -0,0 +1,226 @@
// 节点形状类
export default class Shape {
constructor(node) {
this.node = node
}
// 形状需要的padding
getShapePadding(width, height, paddingX, paddingY) {
const shape = this.node.getShape()
const defaultPaddingX = 15
const defaultPaddingY = 5
const actWidth = width + paddingX * 2
const actHeight = height + paddingY * 2
const actOffset = Math.abs(actWidth - actHeight)
switch (shape) {
case 'roundedRectangle':
return {
paddingX: height > width ? (height - width) / 2 : 0,
paddingY: 0
}
case 'diamond':
return {
paddingX: width / 2,
paddingY: height / 2
}
case 'parallelogram':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'outerTriangularRectangle':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'innerTriangularRectangle':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'ellipse':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: paddingY <= 0 ? defaultPaddingY : 0
}
case 'circle':
return {
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
paddingY: actHeight < actWidth ? actOffset / 2 : 0
}
default:
return {
paddingX: 0,
paddingY: 0
}
}
}
// 创建形状节点
createShape() {
const shape = this.node.getShape()
let { width, height } = this.node
let node = null
// 矩形
if (shape === 'rectangle') {
node = this.node.group.rect(width, height)
} else if (shape === 'diamond') {
// 菱形
node = this.createDiamond()
} else if (shape === 'parallelogram') {
// 平行四边形
node = this.createParallelogram()
} else if (shape === 'roundedRectangle') {
// 圆角矩形
node = this.createRoundedRectangle()
} else if (shape === 'octagonalRectangle') {
// 八角矩形
node = this.createOctagonalRectangle()
} else if (shape === 'outerTriangularRectangle') {
// 外三角矩形
node = this.createOuterTriangularRectangle()
} else if (shape === 'innerTriangularRectangle') {
// 内三角矩形
node = this.createInnerTriangularRectangle()
} else if (shape === 'ellipse') {
// 椭圆
node = this.createEllipse()
} else if (shape === 'circle') {
// 圆
node = this.createCircle()
}
return node
}
// 创建菱形
createDiamond() {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
let topX = halfWidth
let topY = 0
let rightX = width
let rightY = halfHeight
let bottomX = halfWidth
let bottomY = height
let leftX = 0
let leftY = halfHeight
return this.node.group.polygon(`
${topX}, ${topY}
${rightX}, ${rightY}
${bottomX}, ${bottomY}
${leftX}, ${leftY}
`)
}
// 创建平行四边形
createParallelogram() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${paddingX}, ${0}
${width}, ${0}
${width - paddingX}, ${height}
${0}, ${height}
`)
}
// 创建圆角矩形
createRoundedRectangle() {
let { width, height } = this.node
let halfHeight = height / 2
return this.node.group.path(`
M${halfHeight},0
L${width - halfHeight},0
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
L${halfHeight},${height}
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
`)
}
// 创建八角矩形
createOctagonalRectangle() {
let w = 5
let { width, height } = this.node
return this.node.group.polygon(`
${0}, ${w}
${w}, ${0}
${width - w}, ${0}
${width}, ${w}
${width}, ${height - w}
${width - w}, ${height}
${w}, ${height}
${0}, ${height - w}
`)
}
// 创建外三角矩形
createOuterTriangularRectangle() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${paddingX}, ${0}
${width - paddingX}, ${0}
${width}, ${height / 2}
${width - paddingX}, ${height}
${paddingX}, ${height}
${0}, ${height / 2}
`)
}
// 创建内三角矩形
createInnerTriangularRectangle() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${0}, ${0}
${width}, ${0}
${width - paddingX / 2}, ${height / 2}
${width}, ${height}
${0}, ${height}
${paddingX / 2}, ${height / 2}
`)
}
// 创建椭圆
createEllipse() {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return this.node.group.path(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
// 创建圆
createCircle() {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return this.node.group.path(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
}
// 形状列表
export const shapeList = [
'rectangle',
'diamond',
'parallelogram',
'roundedRectangle',
'octagonalRectangle',
'outerTriangularRectangle',
'innerTriangularRectangle',
'ellipse',
'circle'
]

View File

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

View File

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

164
simple-mind-map/src/View.js Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

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