Compare commits

...

105 Commits

Author SHA1 Message Date
wanglin2
ece116317b 修改文档 2022-09-13 22:30:45 +08:00
wanglin25
d24d5c8281 修复节点展开收起的bug&打包 2022-09-13 10:34:20 +08:00
wanglin25
97c01cda3a 打包 2022-09-13 09:43:18 +08:00
wanglin25
6d520ece7e 修改文档 2022-09-13 09:33:05 +08:00
wanglin2
745041deef 节点支持多种形状开发完成 2022-09-12 23:07:01 +08:00
wanglin25
ae6c10cdf9 打包 2022-09-01 11:11:33 +08:00
街角小林
f5338d62fc Merge pull request #26 from huangyuanyin/main
fix 二级节点后无子节点,展开所有/收起所有操作后的报错信息
2022-09-01 11:07:38 +08:00
liuzhanghao
19fc12ff20 fix 2022-09-01 10:28:26 +08:00
liuzhanghao
d7640bb026 fix 二级节点后无子节点,展开所有/收起所有操作后的报错信息 2022-09-01 10:22:01 +08:00
wanglin
193b293cfe 更新readme 2022-08-22 21:03:08 +08:00
wanglin25
615ff3ea25 fix:修改右键菜单快捷键提示 2022-08-17 09:25:16 +08:00
wanglin25
8b9cfd2972 fix:修复右键菜单快捷键提示错误 2022-08-16 18:48:11 +08:00
wanglin25
03f8cb9290 fix:修复编辑节点文本时快捷键冲突的问题 2022-08-16 16:47:43 +08:00
wanglin
1ea0c7e316 修复输入字符串/和快捷键/冲突问题 2022-08-14 09:43:00 +08:00
wanglin
a39c8c30e6 打包 2022-08-08 21:24:02 +08:00
街角小林
26ce08c27d Update README.md 2022-08-08 20:09:31 +08:00
wanglin25
4bfc98a96f 支持导出为pdf 2022-08-08 20:06:59 +08:00
wanglin
7560411922 打包&发布新版本 2022-08-06 09:21:18 +08:00
wanglin25
4288e44f3a 支持Ctrl+左键多选 2022-08-05 17:21:21 +08:00
wanglin25
b82c5247fa Merge branch 'dev' of https://github.com/wanglin2/mind-map into dev 2022-08-05 09:30:19 +08:00
wanglin25
c7d2082944 新增库打包&修改文档 2022-08-05 09:29:59 +08:00
wanglin
29458ade9c 增加图标 2022-08-04 20:24:57 +08:00
wanglin25
49d366628e 修改文档 2022-08-04 15:06:07 +08:00
wanglin25
b093153262 新增快捷键 2022-08-04 14:48:52 +08:00
wanglin25
02f276bc2a 支持自由拖拽、修复一些bug 2022-08-04 11:39:17 +08:00
wanglin25
69cb961cc1 修复概要的一些bug 2022-08-02 08:59:05 +08:00
wanglin25
7096391f3b 修复拖拽节点时为隐藏概要内容的问题 2022-08-01 09:59:43 +08:00
wanglin25
280ffcf01d 修复初始渲染时激活节点没有触发页面效果的问题 2022-08-01 09:51:53 +08:00
wanglin25
db3c2b71f5 修复概要展开收起的定位错误问题 2022-08-01 09:36:25 +08:00
wanglin
f11f364d00 新增方法 2022-07-31 22:29:40 +08:00
wanglin
0e8c50d430 修复概要定位问题 2022-07-31 20:46:40 +08:00
wanglin
dd8d250857 修改及新增主题 2022-07-31 14:54:32 +08:00
wanglin
693ead6b49 概要开发中 2022-07-31 10:28:13 +08:00
wanglin
dc3c91270c 更新版本 2022-07-30 08:21:43 +08:00
街角小林
71ac739964 Merge pull request #16 from harris2012/patch-2
fix typo
2022-07-29 21:59:53 +08:00
街角小林
eae5dc5854 Merge pull request #15 from harris2012/patch-1
Update Node.js
2022-07-29 21:57:05 +08:00
Harris Zhang
c45ceac7dc fix typo 2022-07-29 17:55:20 +08:00
Harris Zhang
3d8702be8a Update Node.js 2022-07-29 11:29:25 +08:00
街角小林
084dd9fd84 Merge pull request #13 from harris2012/patch-1
Update index.js
2022-07-26 21:15:41 +08:00
Harris Zhang
0c6c68820f Update index.js 2022-07-26 19:12:47 +08:00
wanglin25
f5ff479f47 fix:1.节点图标不能删除的问题;2.工具按钮置灰仍然可以点击的问题 2022-06-28 19:55:31 +08:00
wanglin25
e9722efe93 增加只读模式 2022-06-08 14:30:45 +08:00
wanglin
db1f9c04c1 修改README 2022-05-10 23:09:54 +08:00
wanglin
b3e6412dbc 发布0.1.6版本 2022-05-10 23:08:02 +08:00
wanglin25
f58828469c 打包 2022-05-09 14:31:22 +08:00
wanglin25
cf7a92d9c7 节点备注支持markdown及富文本、修复不能选中文字的问题 2022-05-09 14:24:11 +08:00
wanglin25
ec0d021e92 修复节点标注在节点激活后无法隐藏问题 2022-05-09 11:31:05 +08:00
wanglin25
25a2d919fb 修复超链接、备注、标签等文字编辑时返回键和回车键与思维导图快捷键冲突的问题 2022-05-09 11:03:56 +08:00
wanglin
c3f4e2b797 增加版本号 2021-11-25 22:25:26 +08:00
wanglin25
a0c07522f9 新增支持节点拖拽 2021-11-25 16:06:18 +08:00
wanglin25
ca0cbdf009 '状态数据支持保存激活状态、视图状态(拖动位置、缩放值)' 2021-11-22 19:59:21 +08:00
wanglin
6263a49903 修复存在激活节点时设置主题存在的问题 2021-08-05 22:29:02 +08:00
wanglin
dce133f8f0 升级版本 2021-08-05 08:09:48 +08:00
wanglin
7f4f4e2fe0 增加快捷键功能 2021-08-05 00:06:27 +08:00
wanglin
4e4ade5c31 修复回退问题 2021-08-04 23:45:05 +08:00
wanglin
5423b42e9d 新增导出为json;优化一些细节 2021-08-04 07:26:01 +08:00
wanglin
9555375907 修改版本号及package.json 2021-08-01 11:58:08 +08:00
wanglin
cc19ce168b 增加本地存储功能 2021-08-01 10:49:57 +08:00
wanglin25
e324813eae 修复删除节点和节点编辑时的删除冲突问题 2021-07-27 14:17:58 +08:00
wanglin25
08a7ba380a '修改文章与打包' 2021-07-22 19:06:51 +08:00
wanglin25
dd2bfbcf93 '打包' 2021-07-22 14:13:49 +08:00
wanglin25
4ae5c914ca '打包' 2021-07-22 09:36:50 +08:00
wanglin
6f49577794 优化 2021-07-22 08:03:00 +08:00
wanglin
43a3dd97c7 修改文章 2021-07-21 22:50:46 +08:00
wanglin25
7226e40a6c '完成文章' 2021-07-21 20:24:37 +08:00
wanglin25
9f090a4474 文章写作中 2021-07-20 20:18:03 +08:00
wanglin
bf5298ab2f 文章 2021-07-20 07:48:22 +08:00
wanglin
ffe4fedbc3 修改文章 2021-07-19 07:39:34 +08:00
wanglin
07ad92e93a 添加图片 2021-07-18 22:47:03 +08:00
wanglin
5a004e40de 优化与文档编写 2021-07-18 22:27:34 +08:00
wanglin
8664bcc00b 完善开发文档 2021-07-16 19:33:01 +08:00
wanglin
d00f56c7f1 优化代码 2021-07-16 13:58:48 +08:00
wanglin
4ddce22076 基本完成 2021-07-16 13:42:37 +08:00
wanglin25
fdf4d51a1e '右键功能开发中' 2021-07-15 17:46:36 +08:00
wanglin25
a9ebe50fff Merge branch 'main' of https://github.com/wanglin2/mind-map into main 2021-07-15 09:46:53 +08:00
wanglin25
f8638088f3 '优化' 2021-07-15 09:46:27 +08:00
wanglin
081797e83b 右键菜单开发中 2021-07-14 23:57:36 +08:00
wanglin25
8adf5a7fb8 '修改样式' 2021-07-14 09:54:45 +08:00
wanglin25
194a920efa '打包' 2021-07-14 09:34:13 +08:00
wanglin
6b3741741c 修改配置 2021-07-14 08:13:23 +08:00
wanglin
80a7e334e4 修改配置 2021-07-14 08:09:23 +08:00
wanglin
5d45d7accf 打包 2021-07-14 07:38:30 +08:00
wanglin
b893776f82 修改打包配置i 2021-07-14 07:34:06 +08:00
wanglin
115a0ac480 修改 2021-07-14 07:28:31 +08:00
wanglin
cb43292aa5 上传打包文件 2021-07-13 23:29:22 +08:00
wanglin
7b773bd9ad 修改README 2021-07-13 23:20:41 +08:00
wanglin25
38a1e25cc2 '基本完成' 2021-07-13 16:39:50 +08:00
wanglin
fd0c471f00 优化 2021-07-13 08:04:47 +08:00
wanglin
97f84933f8 新增主题 2021-07-12 22:49:22 +08:00
wanglin25
321fc69798 '完成部分结构' 2021-07-12 20:12:21 +08:00
wanglin25
af2910bec8 '优化及增加主题' 2021-07-12 14:55:50 +08:00
wanglin
9deb20f1ca 回退功能开发中 2021-07-12 07:56:38 +08:00
wanglin
0f4d614c4e 增加快捷键和全屏功能 2021-07-11 22:21:40 +08:00
wanglin
eeba3153ef 完成多选操作 2021-07-11 13:33:06 +08:00
wanglin
554dab56d3 完成基本逻辑 2021-07-10 22:06:45 +08:00
wanglin
da28f89c52 日常提交 2021-07-04 22:53:36 +08:00
wanglin
8b0c62430e 完成导出功能 2021-07-04 16:56:37 +08:00
wanglin
adcf77f60d 日常提交 2021-06-28 07:46:12 +08:00
wanglin
54ae4b3e26 提交 2021-06-26 00:03:39 +08:00
wanglin
b0929da054 完成节点内容设置及主题 2021-06-26 00:01:03 +08:00
wanglin2
1528c3d64b Delete .DS_Store 2021-06-20 23:05:11 +08:00
wanglin2
ecc3ba8784 Delete .DS_Store 2021-06-20 23:04:56 +08:00
wanglin
947c4b2b44 完成节点内容布局 2021-06-20 23:03:23 +08:00
wanglin25
08c7768b18 修改忽略文件 2021-06-19 14:24:52 +08:00
wanglin25
efebe3f094 重构 2021-06-19 14:04:05 +08:00
216 changed files with 15962 additions and 3109 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules
oss.js
.DS_Store
package-lock.json

1054
README.md

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.81d632f4.js" rel="prefetch"><link href="dist/js/chunk-2d216b67.228f2009.js" rel="prefetch"><link href="dist/js/chunk-e86f1494.f8dd20e2.js" rel="prefetch"><link href="dist/css/app.b771b210.css" rel="preload" as="style"><link href="dist/css/chunk-vendors.6fd71983.css" rel="preload" as="style"><link href="dist/js/app.4bae41a9.js" rel="preload" as="script"><link href="dist/js/chunk-vendors.54c023de.js" rel="preload" as="script"><link href="dist/css/chunk-vendors.6fd71983.css" rel="stylesheet"><link href="dist/css/app.b771b210.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.54c023de.js"></script><script src="dist/js/app.4bae41a9.js"></script></body></html>

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

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

@@ -0,0 +1,372 @@
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 {
layoutValueList
} from './src/utils/constant'
import {
SVG
} from '@svgdotjs/svg.js'
// 默认选项配置
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(){}
}
*/
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-06 11:18:47
* @Desc: 思维导图
*/
class MindMap {
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-06 11:19:01
* @Desc: 构造函数
*/
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.doExport = new Export({
mindMap: this
})
// 选择类
this.select = new Select({
mindMap: this
})
// 拖动类
this.drag = new Drag({
mindMap: this
})
// 批量执行类
this.batchExecution = new BatchExecution()
// 初始渲染
this.reRender()
setTimeout(() => {
this.command.addHistory()
}, 0)
}
/**
* @Author: 王林
* @Date: 2021-07-01 22:15:22
* @Desc: 配置参数处理
*/
handleOpt(opt) {
// 检查布局配置
if (!layoutValueList.includes(opt.layout)) {
opt.layout = 'logicalStructure'
}
// 检查主题配置
opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default'
return opt
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-06 18:47:29
* @Desc: 渲染,部分渲染
*/
render() {
this.batchExecution.push('render', () => {
this.initTheme()
this.renderer.reRender = false
this.renderer.render()
})
}
/**
* @Author: 王林
* @Date: 2021-07-08 22:05:11
* @Desc: 重新渲染
*/
reRender() {
this.batchExecution.push('render', () => {
this.draw.clear()
this.initTheme()
this.renderer.reRender = true
this.renderer.render()
})
}
/**
* @Author: 王林
* @Date: 2021-07-11 21:16:52
* @Desc: 容器尺寸变化,调整尺寸
*/
resize() {
this.elRect = this.el.getBoundingClientRect()
this.width = this.elRect.width
this.height = this.elRect.height
this.svg.size(this.width, this.height)
}
/**
* @Author: 王林
* @Date: 2021-04-24 13:25:50
* @Desc: 监听事件
*/
on(event, fn) {
this.event.on(event, fn)
}
/**
* @Author: 王林
* @Date: 2021-04-24 13:51:35
* @Desc: 触发事件
*/
emit(event, ...args) {
this.event.emit(event, ...args)
}
/**
* @Author: 王林
* @Date: 2021-04-24 13:53:54
* @Desc: 解绑事件
*/
off(event, fn) {
this.event.off(event, fn)
}
/**
* @Author: 王林
* @Date: 2021-05-05 13:32:43
* @Desc: 设置主题
*/
initTheme() {
// 合并主题配置
this.themeConfig = merge(theme[this.opt.theme], this.opt.themeConfig)
// 设置背景样式
Style.setBackgroundStyle(this.el, this.themeConfig)
}
/**
* @Author: 王林
* @Date: 2021-05-05 13:52:08
* @Desc: 设置主题
*/
setTheme(theme) {
this.renderer.clearAllActive()
this.opt.theme = theme
this.reRender()
}
/**
* @Author: 王林
* @Date: 2021-06-25 23:52:37
* @Desc: 获取当前主题
*/
getTheme() {
return this.opt.theme
}
/**
* @Author: 王林
* @Date: 2021-05-05 13:50:17
* @Desc: 设置主题配置
*/
setThemeConfig(config) {
this.opt.themeConfig = config
this.reRender()
}
/**
* @Author: 王林
* @Date: 2021-08-01 10:38:34
* @Desc: 获取自定义主题配置
*/
getCustomThemeConfig() {
return this.opt.themeConfig
}
/**
* @Author: 王林
* @Date: 2021-05-05 14:01:29
* @Desc: 获取某个主题配置值
*/
getThemeConfig(prop) {
return prop === undefined ? this.themeConfig : this.themeConfig[prop]
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-13 16:17:06
* @Desc: 获取当前布局结构
*/
getLayout() {
return this.opt.layout
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-13 16:17:33
* @Desc: 设置布局结构
*/
setLayout(layout) {
// 检查布局配置
if (!layoutValueList.includes(layout)) {
layout = 'logicalStructure'
}
this.opt.layout = layout
this.renderer.setLayout()
this.render()
}
/**
* @Author: 王林
* @Date: 2021-05-04 13:01:00
* @Desc: 执行命令
*/
execCommand(...args) {
this.command.exec(...args)
}
/**
* @Author: 王林
* @Date: 2021-08-03 22:58:12
* @Desc: 动态设置思维导图数据
*/
setData(data) {
this.execCommand('CLEAR_ACTIVE_NODE')
this.command.clearHistory()
this.renderer.renderTree = data
this.reRender()
}
/**
* @Author: 王林
* @Date: 2021-07-01 22:06:38
* @Desc: 导出
*/
async export(...args) {
let result = await this.doExport.export(...args)
return result
}
/**
* @Author: 王林
* @Date: 2021-07-11 09:20:03
* @Desc: 转换位置
*/
toPos(x, y) {
return {
x: x - this.elRect.left,
y: y - this.elRect.top
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-06-08 14:12:38
* @Desc: 设置只读模式、编辑模式
*/
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)
}
}
export default MindMap

View File

@@ -0,0 +1,37 @@
{
"name": "simple-mind-map",
"version": "0.2.4",
"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": {},
"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"
},
"keywords": [
"javascript",
"svg",
"mind-map",
"mindMap",
"MindMap"
]
}

View File

@@ -0,0 +1,85 @@
/**
* @Author: 王林
* @Date: 2021-06-27 13:16:23
* @Desc: 在下一个事件循环里执行任务
*/
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 (cb, ctx) {
if (pending) return
pending = true
timerFunc(handle, 0)
}
}
/**
* @Author: 王林
* @Date: 2021-06-26 22:40:52
* @Desc: 批量执行
*/
class BatchExecution {
/**
* @Author: 王林
* @Date: 2021-06-26 22:41:41
* @Desc: 构造函数
*/
constructor() {
this.has = {}
this.queue = []
this.nextTick = nextTick(this.flush, this)
}
/**
* @Author: 王林
* @Date: 2021-06-27 12:54:04
* @Desc: 添加任务
*/
push(name, fn) {
if (this.has[name]) {
return;
}
this.has[name] = true
this.queue.push({
name,
fn
})
this.nextTick()
}
/**
* @Author: 王林
* @Date: 2021-06-27 13:09:24
* @Desc: 执行队列
*/
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,152 @@
import { copyRenderTree, simpleDeepClone } from './utils';
/**
* @Author: 王林
* @Date: 2021-05-04 13:10:06
* @Desc: 命令类
*/
class Command {
/**
* @Author: 王林
* @Date: 2021-05-04 13:10:24
* @Desc: 构造函数
*/
constructor(opt = {}) {
this.opt = opt
this.mindMap = opt.mindMap
this.commands = {}
this.history = []
this.activeHistoryIndex = 0
// 注册快捷键
this.registerShortcutKeys()
}
/**
* @Author: 王林
* @Date: 2021-08-03 23:06:55
* @Desc: 清空历史数据
*/
clearHistory() {
this.history = []
this.activeHistoryIndex = 0
this.mindMap.emit('back_forward', 0, 0)
}
/**
* @Author: 王林
* @Date: 2021-08-02 23:23:19
* @Desc: 注册快捷键
*/
registerShortcutKeys() {
this.mindMap.keyCommand.addShortcut('Control+z', () => {
this.mindMap.execCommand('BACK')
})
this.mindMap.keyCommand.addShortcut('Control+y', () => {
this.mindMap.execCommand('FORWARD')
})
}
/**
* @Author: 王林
* @Date: 2021-05-04 13:12:30
* @Desc: 执行命令
*/
exec(name, ...args) {
if (this.commands[name]) {
this.commands[name].forEach((fn) => {
fn(...args)
})
if (name === 'BACK' || name === 'FORWARD') {
return;
}
this.addHistory()
}
}
/**
* @Author: 王林
* @Date: 2021-05-04 13:13:01
* @Desc: 添加命令
*/
add(name, fn) {
if (this.commands[name]) {
this.commands[name].push(fn)
} else {
this.commands[name] = [fn]
}
}
/**
* @Author: 王林
* @Date: 2021-07-15 23:02:41
* @Desc: 移除命令
*/
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)
}
}
}
/**
* @Author: 王林
* @Date: 2021-05-04 14:35:43
* @Desc: 添加回退数据
*/
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)
}
/**
* @Author: 王林
* @Date: 2021-07-11 22:34:53
* @Desc: 回退
*/
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]);
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-12 10:45:31
* @Desc: 前进
*/
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]);
}
}
/**
* @Author: 王林
* @Date: 2021-05-04 15:02:58
* @Desc: 获取渲染树数据副本
*/
getCopyData() {
return copyRenderTree({}, this.mindMap.renderer.renderTree)
}
}
export default Command

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

@@ -0,0 +1,317 @@
import {
bfsWalk,
throttle
} from './utils'
import Base from './layouts/Base'
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-23 17:38:55
* @Desc: 节点拖动类
*/
class Drag extends Base {
/**
* @Author: 王林
* @Date: 2021-07-10 22:35:16
* @Desc: 构造函数
*/
constructor({
mindMap
}) {
super(mindMap.renderer)
this.mindMap = mindMap
this.reset()
this.bindEvent()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-23 19:33:56
* @Desc: 复位
*/
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
}
/**
* @Author: 王林
* @Date: 2021-07-10 22:36:36
* @Desc: 绑定事件
*/
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
this.offsetX = e.clientX - (node.left * scaleX + translateX)
this.offsetY = e.clientY - (node.top * scaleY + translateY)
//
this.node = node
this.isMousedown = true
let {
x,
y
} = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseDownX = x
this.mouseDownY = y
})
this.mindMap.on('mousemove', (e) => {
if (this.mindMap.opt.readonly) {
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)
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-23 19:38:02
* @Desc: 鼠标松开事件
*/
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()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-23 19:34:53
* @Desc: 创建克隆节点
*/
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.style.line(this.line)
// 同级位置占位符
this.placeholder = this.draw.rect().fill({
color: this.node.style.merge('lineColor', true)
})
this.mindMap.draw.add(this.clone)
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-23 19:35:16
* @Desc: 移除克隆节点
*/
removeCloneNode() {
if (!this.clone) {
return
}
this.clone.remove()
this.line.remove()
this.placeholder.remove()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-23 18:53:47
* @Desc: 拖动中
*/
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()
}
/**
* @Author: 王林
* @Date: 2021-07-11 10:20:43
* @Desc: 检测重叠节点
*/
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 && 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)
}
}
}
})
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
}
}
}
export default Drag

View File

@@ -17,7 +17,7 @@ class Event extends EventEmitter {
super()
this.opt = opt
this.mindMap = opt.mindMap
this.isMousedown = false
this.isLeftMousedown = false
this.mousedownPos = {
x: 0,
y: 0
@@ -46,6 +46,8 @@ class Event extends EventEmitter {
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)
}
/**
@@ -55,12 +57,19 @@ class Event extends EventEmitter {
* @Desc: 绑定事件
*/
bind() {
this.mindMap.draw.on('click', this.onDrawClick)
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)
}
/**
* javascript comment
@@ -69,10 +78,12 @@ class Event extends EventEmitter {
* @Desc: 解绑事件
*/
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)
}
/**
@@ -84,6 +95,15 @@ class Event extends EventEmitter {
this.emit('draw_click', e)
}
/**
* @Author: 王林
* @Date: 2021-07-16 13:37:30
* @Desc: svg画布的鼠标按下事件
*/
onSvgMousedown(e) {
this.emit('svg_mousedown', e)
}
/**
* javascript comment
* @Author: 王林25
@@ -91,8 +111,11 @@ class Event extends EventEmitter {
* @Desc: 鼠标按下事件
*/
onMousedown(e) {
e.preventDefault()
this.isMousedown = true
// e.preventDefault()
// 鼠标左键
if (e.which === 1) {
this.isLeftMousedown = true
}
this.mousedownPos.x = e.clientX
this.mousedownPos.y = e.clientY
this.emit('mousedown', e, this)
@@ -105,13 +128,13 @@ class Event extends EventEmitter {
* @Desc: 鼠标移动事件
*/
onMousemove(e) {
e.preventDefault()
// 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.isMousedown) {
if (this.isLeftMousedown) {
this.emit('drag', e, this)
}
}
@@ -123,7 +146,7 @@ class Event extends EventEmitter {
* @Desc: 鼠标松开事件
*/
onMouseup(e) {
this.isMousedown = false
this.isLeftMousedown = false
this.emit('mouseup', e, this)
}
@@ -137,13 +160,23 @@ class Event extends EventEmitter {
e.stopPropagation()
e.preventDefault()
let dir
if (e.wheelDeltaY > 0) {
if ((e.wheelDeltaY || e.detail) > 0) {
dir = 'up'
} else {
dir = 'down'
}
this.emit('mousewheel', e, dir, this)
}
/**
* @Author: 王林
* @Date: 2021-07-10 22:34:13
* @Desc: 鼠标右键菜单事件
*/
onContextmenu(e) {
e.preventDefault()
this.emit('contextmenu', e)
}
}
export default Event

View File

@@ -0,0 +1,264 @@
import { imgToDataUrl, downloadFile } from './utils'
import JsPDF from 'jspdf'
const URL = window.URL || window.webkitURL || window
/**
* @Author: 王林
* @Date: 2021-07-01 22:05:16
* @Desc: 导出类
*/
class Export {
/**
* @Author: 王林
* @Date: 2021-07-01 22:05:42
* @Desc: 构造函数
*/
constructor(opt) {
this.mindMap = opt.mindMap
this.exportPadding = this.mindMap.opt.exportPadding
}
/**
* @Author: 王林
* @Date: 2021-07-02 07:44:06
* @Desc: 导出
*/
async export(type, isDownload = true, name = '思维导图') {
if (this[type]) {
let result = await this[type](name)
if (isDownload && type !== 'pdf') {
downloadFile(result, name + '.' + type)
}
return result
} else {
return null
}
}
/**
* @Author: 王林
* @Date: 2021-07-04 14:57:40
* @Desc: 获取svg数据
*/
async getSvgData() {
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)
// 把图片的url转换成data:url类型否则导出会丢失图片
let imageList = clone.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: clone,
str: clone.svg()
}
}
/**
* @Author: 王林
* @Date: 2021-07-04 15:25:19
* @Desc: 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
})
}
/**
* @Author: 王林
* @Date: 2021-07-04 15:32:07
* @Desc: 在canvas上绘制思维导图背景
*/
drawBackgroundToCanvas(ctx, width, height) {
return new Promise((resolve, rejct) => {
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) => {
rejct(e)
}
} else {
resolve()
}
})
}
/**
* @Author: 王林
* @Date: 2021-07-01 22:09:51
* @Desc: 导出为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
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-08-08 19:23:08
* @Desc: 导出为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
}
/**
* @Author: 王林
* @Date: 2021-07-04 15:32:07
* @Desc: 在svg上绘制思维导图背景
*/
drawBackgroundToSvg(svg) {
return new Promise(async (resolve, rejct) => {
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()
}
})
}
/**
* @Author: 王林
* @Date: 2021-07-04 14:54:07
* @Desc: 导出为svg
*/
async svg() {
let { node } = await this.getSvgData()
await this.drawBackgroundToSvg(node)
let str = node.svg()
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'
})
return URL.createObjectURL(blob)
}
/**
* @Author: 王林
* @Date: 2021-08-03 22:19:17
* @Desc: 导出为json
*/
json () {
let data = this.mindMap.command.getCopyData()
let str = JSON.stringify(data)
let blob = new Blob([str])
return URL.createObjectURL(blob)
}
/**
* @Author: 王林
* @Date: 2021-08-03 22:24:24
* @Desc: 专有文件其实就是json文件
*/
smm () {
return this.json();
}
}
export default Export

View File

@@ -16,9 +16,51 @@ export default class KeyCommand {
this.shortcutMap = {
//Enter: [fn]
}
this.shortcutMapCache = {}
this.isPause = false
this.bindEvent()
}
/**
* @Author: 王林
* @Date: 2022-08-14 08:57:55
* @Desc: 暂停快捷键响应
*/
pause() {
this.isPause = true
}
/**
* @Author: 王林
* @Date: 2022-08-14 08:58:43
* @Desc: 恢复快捷键响应
*/
recovery() {
this.isPause = false
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-08-16 16:29:01
* @Desc: 保存当前注册的快捷键数据然后清空快捷键数据
*/
save() {
this.shortcutMapCache = this.shortcutMap
this.shortcutMap = {}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-08-16 16:29:38
* @Desc: 恢复保存的快捷键数据然后清空缓存数据
*/
restore() {
this.shortcutMap = this.shortcutMapCache
this.shortcutMapCache = {}
}
/**
* @Author: 王林
* @Date: 2021-04-24 15:23:22
@@ -26,6 +68,9 @@ export default class KeyCommand {
*/
bindEvent() {
window.addEventListener('keydown', (e) => {
if (this.isPause) {
return
}
Object.keys(this.shortcutMap).forEach((key) => {
if (this.checkKey(e, key)) {
e.stopPropagation()
@@ -78,7 +123,9 @@ export default class KeyCommand {
if (e.shiftKey) {
arr.push(keyMap['Shift'])
}
if (!arr.includes(e.keyCode)) {
arr.push(e.keyCode)
}
return arr
}
@@ -113,4 +160,41 @@ export default class KeyCommand {
}
})
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-27 14:06:16
* @Desc: 移除快捷键命令
*/
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]
}
}
})
}
/**
* @Author: 王林
* @Date: 2022-08-14 08:49:58
* @Desc: 获取指定快捷键的处理函数
*/
getShortcutFn(key) {
let res = []
key.split(/\s*\|\s*/).forEach((item) => {
res = this.shortcutMap[item] || []
})
return res
}
}

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,191 @@
import { bfsWalk, throttle } from './utils'
/**
* @Author: 王林
* @Date: 2021-07-10 22:34:51
* @Desc: 选择节点类
*/
class Select {
/**
* @Author: 王林
* @Date: 2021-07-10 22:35:16
* @Desc: 构造函数
*/
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()
}
/**
* @Author: 王林
* @Date: 2021-07-10 22:36:36
* @Desc: 绑定事件
*/
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', (e) => {
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
})
}
/**
* @Author: 王林
* @Date: 2021-07-13 07:55:49
* @Desc: 鼠标移动事件
*/
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)
}
}
/**
* @Author: 王林
* @Date: 2021-07-22 08:02:23
* @Desc: 开启自动移动
*/
startAutoMove(x, y) {
this.autoMoveTimer = setTimeout(() => {
this.onMove(x, y)
}, 20);
}
/**
* @Author: 王林
* @Date: 2021-07-11 10:19:37
* @Desc: 创建矩形
*/
createRect(x, y) {
this.rect = this.mindMap.svg.polygon().stroke({
color: '#0984e3'
}).fill({
color: 'rgba(9,132,227,0.3)'
}).plot([[x, y]])
}
/**
* @Author: 王林
* @Date: 2021-07-11 10:20:43
* @Desc: 检测在选区里的节点
*/
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,265 @@
/**
* @Author: 王林
* @Date: 2022-08-22 21:32:50
* @Desc: 节点形状类
*/
export default class Shape {
constructor(node) {
this.node = node
}
/**
* @Author: 王林
* @Date: 2022-08-17 22:32:32
* @Desc: 形状需要的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
}
}
}
/**
* @Author: 王林
* @Date: 2022-08-17 22:22:53
* @Desc: 创建形状节点
*/
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
}
/**
* @Author: 王林
* @Date: 2022-09-04 09:08:54
* @Desc: 创建菱形
*/
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}
`)
}
/**
* @Author: 王林
* @Date: 2022-09-03 16:14:12
* @Desc: 创建平行四边形
*/
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}
`)
}
/**
* @Author: 王林
* @Date: 2022-09-03 16:50:23
* @Desc: 创建圆角矩形
*/
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}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 16:14:08
* @Desc: 创建八角矩形
*/
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}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 20:55:50
* @Desc: 创建外三角矩形
*/
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}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 20:59:37
* @Desc: 创建内三角矩形
*/
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}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:06:31
* @Desc: 创建椭圆
*/
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}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:14:04
* @Desc: 创建圆
*/
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

@@ -1,3 +1,5 @@
import { tagColorList } from './utils/constant';
const rootProp = ['paddingX', 'paddingY']
/**
* @Author: 王林
@@ -29,6 +31,15 @@ class Style {
this.themeConfig = themeConfig
}
/**
* @Author: 王林
* @Date: 2021-07-12 07:40:14
* @Desc: 更新主题配置
*/
updateThemeConfig(themeConfig) {
this.themeConfig = themeConfig
}
/**
* @Author: 王林
* @Date: 2021-04-11 12:02:55
@@ -37,23 +48,35 @@ class Style {
merge(prop, root, isActive) {
// 三级及以下节点
let defaultConfig = this.themeConfig.node
if (root) {// 直接使用最外层样式
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.secondLevel
defaultConfig = this.themeConfig.second
}
// 激活状态
if (isActive !== undefined ? isActive : this.ctx.isActive) {
if (this.ctx.activeStyle && this.ctx.activeStyle[prop] !== undefined) {
return this.ctx.activeStyle[prop];
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.ctx[prop] !== undefined ? this.ctx[prop] : defaultConfig[prop]
return this.ctx.nodeData.data[prop] !== undefined ? this.ctx.nodeData.data[prop] : defaultConfig[prop]
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:55:57
* @Desc: 获取某个样式值
*/
getStyle(prop, root, isActive) {
return this.merge(prop, root, isActive)
}
/**
@@ -62,13 +85,24 @@ class Style {
* @Desc: 矩形
*/
rect(node) {
this.shape(node)
node.radius(this.merge('borderRadius'))
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 15:04:28
* @Desc: 矩形外的其他形状
*/
shape(node) {
node.fill({
color: this.merge('fillColor')
}).stroke({
color: this.merge('borderColor'),
width: this.merge('borderWidth'),
dasharray: this.merge('borderDasharray')
}).radius(this.merge('borderRadius'))
})
}
/**
@@ -93,12 +127,47 @@ class Style {
* @Date: 2021-04-13 08:14:34
* @Desc: html文字节点
*/
domText(node) {
domText(node, fontSizeScale = 1) {
node.style.fontFamily = this.merge('fontFamily')
node.style.fontSize = this.merge('fontSize') + 'px'
node.style.fontSize = this.merge('fontSize') * fontSizeScale + 'px'
node.style.fontWeight = this.merge('fontWeight') || 'normal'
}
/**
* @Author: 王林
* @Date: 2021-06-20 20:02:18
* @Desc: 标签文字
*/
tagText(node, index) {
node.fill({
color: tagColorList[index].color
}).css({
'font-size': '12px'
})
}
/**
* @Author: 王林
* @Date: 2021-06-20 21:04:11
* @Desc: 标签矩形
*/
tagRect(node, index) {
node.fill({
color: tagColorList[index].background
})
}
/**
* @Author: 王林
* @Date: 2021-07-03 22:37:19
* @Desc: 内置图标
*/
iconNode(node) {
node.attr({
fill: this.merge('color')
})
}
/**
* @Author: 王林
* @Date: 2021-04-11 14:50:49
@@ -108,6 +177,15 @@ class Style {
node.stroke({ width: this.merge('lineWidth', true), color: this.merge('lineColor', true) }).fill({ color: 'none' })
}
/**
* @Author: 王林
* @Date: 2022-07-30 16:19:03
* @Desc: 概要连线
*/
generalizationLine(node) {
node.stroke({ width: this.merge('generalizationLineWidth', true), color: this.merge('generalizationLineColor', true) }).fill({ color: 'none' })
}
/**
* @Author: 王林
* @Date: 2021-04-11 20:03:59

View File

@@ -0,0 +1,146 @@
import {
getStrWithBrFromHtml
} from './utils'
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-06-19 11:11:28
* @Desc: 节点文字编辑类
*/
export default class TextEdit {
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-06-19 11:22:57
* @Desc: 构造函数
*/
constructor(renderer) {
this.renderer = renderer
this.mindMap = renderer.mindMap
// 文本编辑框
this.textEditNode = null
// 文本编辑框是否显示
this.showTextEdit = false
this.bindEvent()
}
/**
* @Author: 王林
* @Date: 2021-04-24 13:27:04
* @Desc: 事件
*/
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])
})
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-08-16 16:27:02
* @Desc: 注册临时快捷键
*/
registerTmpShortcut() {
// 注册回车快捷键
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
}
/**
* @Author: 王林
* @Date: 2021-04-13 22:15:56
* @Desc: 显示文本编辑框
*/
show(node) {
this.showEditTextBox(node, node._textData.node.node.getBoundingClientRect())
}
/**
* @Author: 王林
* @Date: 2021-04-13 22:13:02
* @Desc: 显示文本编辑框
*/
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)
document.body.appendChild(this.textEditNode)
}
node.style.domText(this.textEditNode, this.mindMap.view.scale)
this.textEditNode.innerHTML = node.nodeData.data.text.split(/\n/img).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()
}
/**
* @Author: 王林
* @Date: 2021-08-02 23:13:50
* @Desc: 选中文本
*/
selectNodeText() {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.textEditNode)
selection.removeAllRanges()
selection.addRange(range)
}
/**
* @Author: 王林
* @Date: 2021-04-24 13:48:16
* @Desc: 隐藏文本编辑框
*/
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
}
}

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

@@ -0,0 +1,193 @@
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 14:45:24
* @Desc: 视图操作类
*/
class View {
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 14:45:40
* @Desc: 构造函数
*/
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()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 15:38:51
* @Desc: 绑定
*/
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
// 清除激活节点
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()
}
})
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-22 18:30:24
* @Desc: 获取当前变换状态数据
*/
getTransformData() {
return {
transform: this.mindMap.draw.transform(),
state: {
scale: this.scale,
x: this.x,
y: this.y,
sx: this.sx,
sy: this.sy
}
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-22 19:54:17
* @Desc: 动态设置变换状态数据
*/
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)
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-13 15:49:06
* @Desc: 平移x方向
*/
translateX(step) {
this.x += step
this.transform()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-13 15:48:52
* @Desc: 平移y方向
*/
translateY(step) {
this.y += step
this.transform()
}
/**
* @Author: 王林
* @Date: 2021-07-04 17:13:14
* @Desc: 应用变换
*/
transform() {
this.mindMap.draw.transform({
scale: this.scale,
// origin: 'center center',
translate: [this.x, this.y],
})
this.mindMap.emit('view_data_change', this.getTransformData())
}
/**
* @Author: 王林
* @Date: 2021-07-11 17:41:35
* @Desc: 恢复
*/
reset() {
this.scale = 1
this.x = 0
this.y = 0
this.transform()
}
/**
* @Author: 王林
* @Date: 2021-07-04 17:10:34
* @Desc: 缩小
*/
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)
}
/**
* @Author: 王林
* @Date: 2021-07-04 17:10:41
* @Desc: 放大
*/
enlarge() {
this.scale += this.mindMap.opt.scaleRatio
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

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

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