Compare commits

..

72 Commits
0.6.0 ... 0.1.8

Author SHA1 Message Date
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
399 changed files with 12130 additions and 68945 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,3 @@
node_modules
.DS_Store
dist_electron
package-lock.json

21
LICENSE
View File

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

962
README.md
View File

@@ -1,45 +1,68 @@
<h1 align="center">Simple mind map</h1>
# web思维导图的简单实现
[![npm-version](https://img.shields.io/npm/v/simple-mind-map)](https://www.npmjs.com/package/simple-mind-map)
![npm download](https://img.shields.io/npm/dm/simple-mind-map)
[![GitHub stars](https://img.shields.io/github/stars/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/stargazers)
[![GitHub issues](https://img.shields.io/github/issues/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/issues)
[![GitHub forks](https://img.shields.io/github/forks/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/network/members)
![license](https://img.shields.io/npm/l/express.svg)
## 特性
> 一个简单&强大的Web思维导图
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图四种结构
本项目包含两部分:
- [x] 内置多种主题,允许高度自定义样式
1.一个js思维导图库不依赖任何框架你可以使用它来快速完成Web思维导图产品的开发。
开发文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)
2.一个Web思维导图基于思维导图库、Vue2.x、ElementUI开发可以操作电脑本地文件所以你可以直接把它当做一个在线版思维导图应用使用如果觉得github的响应速度慢你也可以部署到你的服务器上。
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
另外也提供了客户端可供下载使用,支持`Windows``Mac``Linux`,下载地址:
Github[releases](https://github.com/wanglin2/mind-map/releases)。
百度云盘:[地址](https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3)。
# 特性
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴、鱼骨图六种结构
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
- [x] 支持快捷键
- [x] 节点内容支持图片、图标、超链接、备注、标签、概要
- [x] 节点内容支持图片、图标、超链接、备注、标签
- [x] 支持前进后退
- [x] 支持拖动、缩放
- [x] 支持右键和Ctrl+左键两种多选方式
- [x] 支持节点自由拖拽、拖拽调整
- [x] 支持多种节点形状
- [x] 支持导出为`json``png``svg``pdf``markdown`,支持从`json``xmind``markdown`导入
- [x] 支持小地图、支持水印
- [x] 支持关联线
- [x] 支持右键多选
- [x] 支持节点拖拽
## 目录介绍
1.simple-mind-map
思维导图工具库。
2.web
使用`simple-mind-map`工具库基于vue2.x、ElementUI搭建的在线思维导图。
3.dist
打包后的资源文件夹。
4.docs
文档等。
## 开发
本地开发
```bash
git clone https://github.com/wanglin2/mind-map.git
cd simple-mind-map
npm i
npm link
cd ..
cd web
npm i
npm link simple-mind-map
npm run serve
```
打包
```bash
cd web
npm run build
```
会自动把`index.html`移动到根目录。
## 相关文章
[Web思维导图实现的技术点分析](https://juejin.cn/post/6987711560521089061)
# 安装
@@ -47,56 +70,863 @@ Github[releases](https://github.com/wanglin2/mind-map/releases)。
npm i simple-mind-map
```
# 使用
注意本项目为源码直接发布并未进行打包如果出现编译失败的情况Vue CLI创建的项目可以在vue.config.js文件中增加如下配置来让babel-loader编译本依赖
提供一个宽高不为0的容器元素
```js
module.exports = {
transpileDependencies: ['simple-mind-map']
}
```
其他项目请自行修改打包配置。
# API
## 实例化
```html
<div id="mindMapContainer"></div>
```
另外再设置一下`css`样式:
```css
#mindMapContainer * {
margin: 0;
padding: 0;
}
```
然后创建一个实例:
```js
import MindMap from "simple-mind-map";
const mindMap = new MindMap({
el: document.getElementById('mindMapContainer'),
data: {
"data": {
"text": "根节点"
},
"children": []
}
data: data
});
```
即可得到一个思维导图。
想要实现更多功能?可以查看[开发文档](https://wanglin2.github.io/mind-map/#/doc/zh/)。
# License
### 实例化选项:
MIT
| 字段名称 | 类型 | 默认值 | 描述 | 是否必填 |
| -------------------- | ------- | ---------------- | ------------------------------------------------------------ | -------- |
| el | Element | | 容器元素必须为DOM元素 | 是 |
| data | Object | {} | 思维导图数据,可参考:[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) | |
| layout | String | logicalStructure | 布局类型可选列表logicalStructure逻辑结构图、mindMap思维导图、catalogOrganization目录组织图、organizationStructure组织结构图 | |
| theme | String | default | 主题可选列表default默认、classic脑图经典、minions小黄人、pinkGrape粉红葡萄、mint薄荷、gold金色vip、vitalityOrange活力橙、greenLeaf绿叶、dark2暗色2、skyGreen天清绿、classic2脑图经典2、classic3脑图经典3、classicGreen经典绿、classicBlue经典蓝、blueSky天空蓝、brainImpairedPink脑残粉、dark暗色、earthYellow泥土黄、freshGreen清新绿、freshRed清新红、romanticPurple浪漫紫 | |
| themeConfig | Object | {} | 主题配置,会和所选择的主题进行合并,可用字段可参考:[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js) | |
| scaleRatio | Number | 0.1 | 放大缩小的增量比例 | |
| maxTag | Number | 5 | 节点里最多显示的标签数量,多余的会被丢弃 | |
| exportPadding | Number | 20 | 导出图片时的内边距 | |
| imgTextMargin | Number | 5 | 节点里图片和文字的间距 | |
| textContentMargin | Number | 2 | 节点里各种文字信息的间距,如图标和文字的间距 | |
| selectTranslateStep | Number | 3 | 多选节点时鼠标移动到边缘时的画布移动偏移量 | |
| selectTranslateLimit | Number | 20 | 多选节点时鼠标移动距边缘多少距离时开始偏移 | |
| customNoteContentShowv0.1.6+ | Object | null | 自定义节点备注内容显示Object类型结构为{show: (noteContent, left, top) => {// 你的显示节点备注逻辑 }, hide: () => {// 你的隐藏节点备注逻辑 }} | |
| readonlyv0.1.7+ | Boolean | false | 是否是只读模式 | |
# 微信交流群
<img src="./qrcode.jpg" style="width: 300px" />
### 实例方法:
# 请作者喝杯咖啡
#### render()
> 厚椰乳一盒 + 纯牛奶半盒 + 冰块 + 咖啡液 = 生椰拿铁 yyds
触发整体渲染,会进行节点复用,性能较`reRender`会更好一点,如果只是节点位置变化了可以调用该方法进行渲染
#### reRender()
整体重新渲染,会清空画布,节点也会重新创建,性能不好,慎重使用
#### resize()
容器尺寸变化后,需要调用该方法进行适应
#### setMode(mode)
v0.1.7+。切换模式为只读或编辑。
`mode`readonly、edit
#### on(event, fn)
监听事件,事件列表:
| 事件名称 | 描述 | 回调参数 |
| --------------------------- | ------------------------------------------ | ------------------------------------------------------------ |
| data_change | 渲染树数据变化,可以监听该方法获取最新数据 | data当前渲染树数据 |
| view_data_changev0.1.1+ | 视图变化数据,比如拖动或缩放时会触发 | data当前视图状态数据 |
| back_forward | 前进或回退 | activeHistoryIndex当前在历史数据数组里的索引、length当前历史数据数组的长度 |
| draw_click | *画布的单击事件* | e事件对象 |
| svg_mousedown | svg画布的鼠标按下事件 | e事件对象 |
| mousedown | el元素的鼠标按下事件 | e事件对象、thisEvent事件类实例 |
| mousemove | el元素的鼠标移动事件 | e事件对象、thisEvent事件类实例 |
| drag | 如果是按住左键拖动的话会触发拖动事件 | e事件对象、thisEvent事件类实例 |
| mouseup | el元素的鼠标松开事件 | e事件对象、thisEvent事件类实例 |
| mousewheel | 鼠标滚动事件 | e事件对象、dir向上up还是向下down滚动、thisEvent事件类实例 |
| contextmenu | svg画布的鼠标右键菜单事件 | e事件对象 |
| node_click | 节点的单击事件 | this节点实例、e事件对象 |
| node_mousedown | 节点的鼠标按下事件 | this节点实例、e事件对象 |
| node_mouseup | 节点的鼠标松开事件 | this节点实例、e事件对象 |
| node_dblclick | 节点的双击事件 | this节点实例、e事件对象 |
| node_contextmenu | 节点的右键菜单事件 | e事件对象、this节点实例 |
| before_node_active | 节点激活前事件 | this节点实例、activeNodeList当前激活的所有节点列表 |
| node_active | 节点激活事件 | this节点实例、activeNodeList当前激活的所有节点列表 |
| expand_btn_click | 节点展开或收缩事件 | this节点实例 |
| before_show_text_edit | 节点文本编辑框即将打开事件 | |
| hide_text_edit | 节点文本编辑框关闭事件 | textEditNode文本编辑框DOM节点、activeNodeList当前激活的所有节点列表 |
| scale | 放大缩小事件 | scale缩放比例 |
#### emit(event, ...args)
触发事件,可以是上面表格里的事件,也可以是自定义事件
#### off(event, fn)
解绑事件
#### setTheme(theme)
切换主题,可选主题见上面的选项表格
#### getTheme()
获取当前主题
#### setThemeConfig(config)
设置主题配置,`config`同上面选项表格里的选项`themeConfig`
#### getCustomThemeConfig()
获取自定义主题配置
#### getThemeConfig(prop)
获取某个主题配置属性值
#### getLayout()
获取当前的布局结构
#### setLayout(layout)
设置布局结构,可选值见上面选项表格的`layout`字段
#### execCommand(name, ...args)
执行命令,每执行一个命令就会在历史堆栈里添加一条记录用于回退或前进。所有命令如下:
| 命令名称 | 描述 | 参数 |
| ------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| SELECT_ALL | 全选 | |
| BACK | 回退指定的步数 | step要回退的步数默认为1 |
| FORWARD | 前进指定的步数 | step要前进的步数默认为1 |
| INSERT_NODE | 插入同级节点,操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效 | |
| INSERT_CHILD_NODE | 插入子节点,操作节点为当前激活的节点 | |
| UP_NODE | 上移节点,操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点或在列表里的第一个节点使用无效 | |
| DOWN_NODE | 操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点或在列表里的最后一个节点使用无效 | |
| REMOVE_NODE | 删除节点,操作节点为当前激活的节点 | |
| PASTE_NODE | 粘贴节点到节点,操作节点为当前激活的节点 | data要粘贴的节点数据一般通过`renderer.copyNode()`方法和`renderer.cutNode()`方法获取) |
| CUT_NODE | 剪切节点,操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点使用无效 | callback(回调函数,剪切的节点数据会通过调用该函数并通过参数返回) |
| SET_NODE_STYLE | 修改节点样式 | node要设置样式的节点、prop样式属性、value样式属性值、isActive布尔值是否设置的是激活状态的样式 |
| SET_NODE_ACTIVE | 设置节点是否激活 | node要设置的节点、active布尔值是否激活 |
| CLEAR_ACTIVE_NODE | 清除当前已激活节点的激活状态,操作节点为当前激活的节点 | |
| SET_NODE_EXPAND | 设置节点是否展开 | node要设置的节点、expand布尔值是否展开 |
| EXPAND_ALL | 展开所有节点 | |
| UNEXPAND_ALL | 收起所有节点 | |
| SET_NODE_DATA | 更新节点数据,即更新节点数据对象里`data`对象的数据 | node要设置的节点、data对象要更新的数据`{expand: true}` |
| SET_NODE_TEXT | 设置节点文本 | node要设置的节点、text要设置的文本字符串换行可以使用`\n` |
| SET_NODE_IMAGE | 设置节点图片 | node要设置的节点、imgData对象图片信息结构为`{url, title, width, height}`,图片的宽高必须要传) |
| SET_NODE_ICON | 设置节点图标 | node要设置的节点、icons数组预定义的图片名称组成的数组可用图标可在[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/svg/icons.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/svg/icons.js)文件里的`nodeIconList`列表里获取到,图标名称为`type_name`,如`['priority_1']` |
| SET_NODE_HYPERLINK | 设置节点超链接 | node要设置的节点、link超链接地址、title超链接名称可选 |
| SET_NODE_NOTE | 设置节点备注 | node要设置的节点、note备注文字 |
| SET_NODE_TAG | 设置节点标签 | node要设置的节点、tag字符串数组内置颜色信息可在[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/utils/constant.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/utils/constant.js)里获取到) |
| INSERT_AFTERv0.1.5+ | 将节点移动到另一个节点的后面 | node要移动的节点、 exist目标节点 |
| INSERT_BEFOREv0.1.5+ | 将节点移动到另一个节点的前面 | node要移动的节点、 exist目标节点 |
| MOVE_NODE_TOv0.1.5+ | 移动一个节点作为另一个节点的子节点 | node要移动的节点、 toNode目标节点 |
#### setData(data)
动态设置思维导图数据
`data`:思维导图结构数据
#### export(type, isDownload, fileName)
导出
`type`要导出的类型可选值png、svg
`isDownload`:是否需要直接触发下载,布尔值,默认为`false`
`fileName`v0.1.6+)导出文件的名称,默认为`思维导图`
#### toPos(x, y)
v0.1.5+
将浏览器可视窗口的坐标转换成相对于画布的坐标
## render实例
`render`实例负载整个渲染过程,可通过`mindMap.renderer`获取到
### 属性
#### activeNodeList
获取当前激活的节点列表
#### root
获取节点树的根节点
### 方法
#### clearActive()
清除当前激活的节点
#### clearAllActive()
清除当前所有激活节点,并会触发`node_active`事件
#### startTextEdit()
v0.1.6+)若有文字编辑需求可调用该方法,会禁用回车键和删除键相关快捷键防止冲突
#### endTextEdit()
v0.1.6+)结束文字编辑,会恢复回车键和删除键相关快捷键
#### addActiveNode(node)
添加节点到激活列表里
#### removeActiveNode(node)
在激活列表里移除某个节点
#### findActiveNodeIndex(node)
检索某个节点在激活列表里的索引
#### getNodeIndex(node)
获取节点在同级里的位置索引
#### removeOneNode(node)
删除某个指定节点
#### copyNode()
复制节点,操作节点为当前激活节点,有多个激活节点只会操作第一个节点
#### setNodeDataRender(node, data)
设置节点数据,即`data`字段的数据,并会根据节点大小是否变化来判断是否需要重新渲染该节点,`data`为对象,如:`{text: '我是新文本'}`
#### moveNodeTo(node, toNode)
v0.1.5+
移动一个节点作为另一个节点的子节点
#### insertBefore(node, exist)
v0.1.5+
将节点移动到另一个节点的前面
#### insertAfter(node, exist)
v0.1.5+
将节点移动到另一个节点的后面
## keyCommand实例
`keyCommand`实例负责快捷键的添加及触发,内置了一些快捷键,也可以自行添加。可通过`mindMap.keyCommand`获取到该实例。
### 方法
#### keyCommand(key, fn)
添加快捷键
`key`:快捷键按键,按键值可以通过[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/utils/keyMap.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/utils/keyMap.js)查看。示例:
```js
// 单个按键
mindMap.keyCommand..addShortcut('Enter', () => {})
// 或
mindMap.keyCommand.addShortcut('Del|Backspace', () => {})
// 组合键
mindMap.keyCommand.addShortcut('Control+Enter', () => {})
```
`fn`:要执行的方法
## command实例
`command`实例负责命令的添加及执行,内置了很多命令,也可以自行添加,命令指需要在历史堆栈数据里添加副本的操作。可通过`mindMap.command`获取到该实例
### 方法
#### add(name, fn)
添加命令。
`name`:命令名称
`fn`:命令要执行的方法
#### remove(name, fn)
移除命令。
`name`:要移除的命令名称
`fn`:要移除的方法,不传的话移除该命令所有的方法
#### getCopyData()
获取渲染树数据副本
#### clearHistory()
清空历史堆栈数据
## view实例
`view`实例负责视图操作,可通过`mindMap.view`获取到该实例
### 方法
#### translateX(step)
`x`方向进行平移,`step`:要平移的像素
#### translateY(step)
`y`方向进行平移,`step`:要平移的像素
#### reset()
恢复到默认的变换
#### narrow()
缩小
#### enlarge()
放大
#### getTransformData()
v0.1.1+
获取当前变换数据,可用于回显
#### setTransformData(data)
v0.1.1+
动态设置变换数据可以通过getTransformData方法获取变换数据
## doExport实例
`doExport`实例负责导出,可通过`mindMap.doExport`获取到该实例
### 方法
#### png()
导出为`png`,异步方法,返回图片数据,`data:url`数据,可以自行下载或显示
#### svg()
导出为`svg`,异步方法,返回`svg`数据,`data:url`数据,可以自行下载或显示
#### getSvgData()
获取`svg`数据,异步方法,返回一个对象:
```js
{
node// svg对象
str// svg字符串
}
```
## select实例
`select`实例负责鼠标右键多选节点操作,可通过`mindMap.select`获取到该实例
### 方法
#### toPos(x, y)
转换鼠标位置为相对于容器`el`的位置
## batchExecution实例
`batchExecution`用来批量异步的执行一些操作,如果某个操作同时多次调用,那么只会在下一个事件循环里执行一次。可以通过`mindMap.batchExecution`获取到该实例
### 方法
#### push(name, fn)
添加任务。
`name`:任务名称
`fn`:任务
## node实例
每个节点都会实例化一个`node`实例
### 属性
#### nodeData
该节点对应的真实数据
#### uid
该节点唯一的标识
#### isRoot
是否是根节点
#### layerIndex
节点层级
#### width
节点的宽
#### height
节点的高
#### left
节点的`left`位置
#### top
节点的`top`位置
#### parent
节点的父节点
#### children
节点的子节点列表
#### group
节点是内容容器,`svg`对象
#### isDrag
v0.1.5+
节点是否正在拖拽中
### 方法
#### addChildren(node)
添加子节点
#### getSize()
计算节点的宽高,返回一个布尔值,代表是否宽高发生了变化
#### renderNode()
渲染节点到画布,会移除旧的内容节点,创建新的
#### render()
递归渲染该节点及其所有子节点,第一次调用会创建节点内容,后续只会更新节点位置,想要重新渲染内容,可以先把`initRender`属性设为`true`
#### remove()
递归删除该节点及其所有子节点
#### renderLine()
重新渲染该节点到其子节点之间的连线
#### removeLine()
移除该节点到其子节点之间的连线
#### renderExpandBtn()
渲染展开收缩按钮的内容
#### removeExpandBtn()
移除展开收缩按钮
#### getStyle(prop, root, isActive)
获取某个最终应用到该节点的样式值
`prop`:要获取的样式属性
`root`:是否是根节点,默认`false`
`isActive`:获取的是否是激活状态的样式值,默认`false`
#### setStyle(prop, value, isActive)
修改节点的某个样式,`SET_NODE_STYLE`命令的快捷方法
#### getData(key)
获取该节点真实数据`nodeData``data`对象里的指定值,`key`不传返回这个`data`对象
#### setData(data)
设置节点数据,`SET_NODE_DATA`命令的快捷方法
#### setText(text)
设置节点文本,`SET_NODE_TEXT`命令的快捷方法
#### setImage(imgData)
设置节点图片,`SET_NODE_IMAGE`命令的快捷方法
#### setIcon(icons)
设置节点图标,`SET_NODE_ICON`命令的快捷方法
#### setHyperlink(link, title)
设置节点超链接,`SET_NODE_HYPERLINK`命令的快捷方法
#### setNote(note)
设置节点备注,`SET_NODE_NOTE`命令的快捷方法
#### setTag(tag)
设置节点标签,`SET_NODE_TAG`的快捷方法
#### hide()
v0.1.5+
隐藏节点及其下级节点
#### show()
v0.1.5+
显示节点及其下级节点
#### isParent(node)
v0.1.5+
检测当前节点是否是某个节点的祖先节点
#### isBrother(node)
v0.1.5+
检测当前节点是否是某个节点的兄弟节点
## 内置工具方法
引用:
```js
import {walk, ...} from 'simple-mind-map/src/utils'
```
### 方法
#### walk(root, parent, beforeCallback, afterCallback, isRoot, layerIndex = 0, index = 0)
深度优先遍历树
`root`:要遍历的树的根节点
`parent`:父节点
`beforeCallback`前序遍历回调函数回调参数为root, parent, isRoot, layerIndex, index
`afterCallback`后序遍历回调函数回调参数为root, parent, isRoot, layerIndex, index
`isRoot`:是否是根节点
`layerIndex`:节点层级
`index`:节点在同级节点里的索引
示例:
```js
walk(tree, null, () => {}, () => {}, false, 0, 0)
```
#### bfsWalk(root, callback)
广度优先遍历树
#### resizeImgSize(width, height, maxWidth, maxHeight)
缩放图片的尺寸
`width`:图片原本的宽
`height`:图片原本的高
`maxWidth`:要缩放到的宽
`maxHeight`:要缩放到的高
`maxWidth``maxHeight`可以同时都传,也可以只传一个
#### resizeImg(imgUrl, maxWidth, maxHeight)
缩放图片,内部先加载图片,然后调用`resizeImgSize`方法,返回一个`promise`
#### simpleDeepClone(data)
极简的深拷贝方法,只能针对全是基本数据的对象,否则会报错
#### copyRenderTree(tree, root)
复制渲染树数据,示例:
```js
copyRenderTree({}, this.mindMap.renderer.renderTree)
```
#### copyNodeTree(tree, root)
复制节点树数据,主要是剔除其中的引用`node`实例的`_node`,然后复制`data`对象的数据,示例:
```js
copyNodeTree({}, node)
```
#### imgToDataUrl(src)
图片转成dataURL
#### downloadFile(file, fileName)
下载文件
#### throttle(fn, time = 300, ctx)
节流函数
#### asyncRun(taskList, callback = () => {})
异步执行任务队列,多个任务是同步执行的,没有先后顺序
# 特别说明
本项目较粗糙,未进行完整测试,功能尚不是很完善,性能也存在一些问题,仅用于学习和参考,请勿用于实际项目。
项目内置的主题和图标来自于:
[百度脑图](https://naotu.baidu.com/)
[知犀思维导图](https://www.zhixi.com/)
<p>
<img src="./web/src/assets/img/alipay.jpg" style="width: 300px" />
<img src="./web/src/assets/img/wechat.jpg" style="width: 300px" />
</p>

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

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

View File

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

View File

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

View File

@@ -10,10 +10,7 @@ const createFullData = () => {
"tag": ["标签1", "标签2"],
"hyperlink": "http://lxqnsys.com/",
"hyperlinkTitle": "理想青年实验室",
"note": "理想青年实验室\n一个有意思的角落",
// 自定义位置
// "customLeft": 1318,
// "customTop": 374.5
"note": "理想青年实验室\n一个有意思的角落"
};
}
@@ -35,110 +32,110 @@ const data1 = {
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
...createFullData()
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
...createFullData()
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}]
@@ -150,36 +147,36 @@ const data1 = {
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
},
@@ -190,40 +187,40 @@ const data1 = {
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}]
@@ -235,225 +232,225 @@ const data1 = {
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
"children": [{
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}, {
"data": {
"text": "分支主题",
"text": "子节点",
},
}]
}]
@@ -770,13 +767,13 @@ const data3 = {
"children": [
{
"data": {
"text": "分支主题"
"text": "子节点"
},
"children": []
},
{
"data": {
"text": "分支主题"
"text": "子节点"
},
"children": []
}
@@ -867,50 +864,6 @@ const data4 = {
}
}
// 带概要
const data5 = {
"root": {
"data": {
"text": "根节点"
},
"children": [
{
"data": {
"text": "二级节点",
"generalization": {
"text": "概要",
}
},
"children": [
{
"data": {
"text": "分支主题"
},
"children": []
},
{
"data": {
"text": "分支主题"
},
"children": []
}
]
},
]
}
}
// 富文本数据v0.4.0+需要使用RichText插件才支持富文本编辑
const richTextData = {
"root": {
"data": {
"text": "<a href='http://lxqnsys.com/' target='_blank'>理想去年实验室</a>",
"richText": true
},
"children": []
}
}
const rootData = {
"root": {
"data": {
@@ -923,12 +876,11 @@ const rootData = {
export default {
// ...data1,
// ...data2,
// ...data3,
...data3,
// ...data4,
...data5,
// ...rootData,
"theme": {
"template": "classic4",
"template": "minions",
"config": {
// 自定义配置...
}
@@ -936,6 +888,5 @@ export default {
"layout": "logicalStructure",
// "layout": "mindMap",
// "layout": "catalogOrganization"
// "layout": "organizationStructure",
"config": {}
// "layout": "organizationStructure"
}

View File

@@ -1,73 +0,0 @@
{
"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": []
}, {
"data": {
"text": "<a href='http://lxqnsys.com/' target='_blank'>理想去年实验室</a>",
"richText": true
},
"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
}
},
"config": {}
}

View File

@@ -1,30 +0,0 @@
import MindMap from './index'
import MiniMap from './src/plugins/MiniMap.js'
import Watermark from './src/plugins/Watermark.js'
import KeyboardNavigation from './src/plugins/KeyboardNavigation.js'
import ExportPDF from './src/plugins/ExportPDF.js'
import Export from './src/plugins/Export.js'
import Drag from './src/plugins/Drag.js'
import Select from './src/plugins/Select.js'
import AssociativeLine from './src/plugins/AssociativeLine'
import RichText from './src/plugins/RichText'
import xmind from './src/parse/xmind.js'
import markdown from './src/parse/markdown.js'
import icons from './src/svg/icons.js'
MindMap.xmind = xmind
MindMap.markdown = markdown
MindMap.iconList = icons.nodeIconList
MindMap
.usePlugin(MiniMap)
.usePlugin(Watermark)
.usePlugin(Drag)
.usePlugin(KeyboardNavigation)
.usePlugin(ExportPDF)
.usePlugin(Export)
.usePlugin(Select)
.usePlugin(AssociativeLine)
.usePlugin(RichText)
export default MindMap

View File

@@ -1,525 +1,372 @@
import View from './src/core/view/View'
import Event from './src/core/event/Event'
import Render from './src/core/render/Render'
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/core/render/node/Style'
import KeyCommand from './src/core/command/KeyCommand'
import Command from './src/core/command/Command'
import BatchExecution from './src/utils/BatchExecution'
import { layoutValueList, CONSTANTS } from './src/constants/constant'
import { SVG } from '@svgdotjs/svg.js'
import { simpleDeepClone } from './src/utils'
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
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: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
fishboneDeg: 45,
// 主题
theme: 'default', // 内置主题default默认主题
// 主题配置,会和所选择的主题进行合并
themeConfig: {},
// 放大缩小的增量比例
scaleRatio: 0.1,
// 最多显示几个标签
maxTag: 5,
// 导出图片时的内边距
exportPadding: 20,
// 展开收缩按钮尺寸
expandBtnSize: 20,
// 节点里图片和文字的间距
imgTextMargin: 5,
// 节点里各种文字信息的间距,如图标和文字的间距
textContentMargin: 2,
// 多选节点时鼠标移动边缘时的画布移动偏移
selectTranslateStep: 3,
// 多选节点时鼠标移动距边缘多少距离时开始偏移
selectTranslateLimit: 20,
// 自定义节点备注内容显示
customNoteContentShow: null,
/*
// 是否只读
readonly: false,
// 布局
layout: 'logicalStructure',
// 主题
theme: 'default', // 内置主题default默认主题
// 主题配置,会和所选择的主题进行合并
themeConfig: {},
// 放大缩小的增量比例
scaleRatio: 0.1,
// 最多显示几个标签
maxTag: 5,
// 导出图片时的内边距
exportPadding: 20,
// 展开收缩按钮尺寸
expandBtnSize: 20,
// 节点里图片和文字的间距
imgTextMargin: 5,
// 节点里各种文字信息的间距,如图标和文字的间距
textContentMargin: 2,
// 多选节点时鼠标移动到边缘时的画布移动偏移量
selectTranslateStep: 3,
// 多选节点时鼠标移动边缘多少距离时开始偏移
selectTranslateLimit: 20,
// 自定义节点备注内容显示
customNoteContentShow: null
/*
{
show(){},
hide(){}
}
*/
// 是否开启节点自由拖拽
enableFreeDrag: false,
// 水印配置
watermarkConfig: {
text: '',
lineSpacing: 100,
textSpacing: 100,
angle: 30,
textStyle: {
color: '#999',
opacity: 0.5,
fontSize: 14
}
},
// 达到该宽度文本自动换行
textAutoWrapWidth: 500,
// 自定义鼠标滚轮事件处理
// 可以传一个函数,回调参数为事件对象
customHandleMousewheel: null,
// 鼠标滚动的行为如果customHandleMousewheel传了自定义函数这个属性不生效
mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM,// zoom放大缩小、move上下移动
// 当mousewheelAction设为move时可以通过该属性控制鼠标滚动一下视图移动的步长单位px
mousewheelMoveStep: 100,
// 默认插入的二级节点的文字
defaultInsertSecondLevelNodeText: '二级节点',
// 默认插入的二级以下节点的文字
defaultInsertBelowSecondLevelNodeText: '分支主题',
// 展开收起按钮的颜色
expandBtnStyle: {
color: '#808080',
fill: '#fff'
},
// 自定义展开收起按钮的图标
expandBtnIcon: {
open: '',// svg字符串
close: ''
},
// 是否只有当鼠标在画布内才响应快捷键事件
enableShortcutOnlyWhenMouseInSvg: true,
// 是否开启节点动画过渡
enableNodeTransitionMove: true,
// 如果开启节点动画过渡可以通过该属性设置过渡的时间单位ms
nodeTransitionMoveDuration: 300,
// 初始根节点的位置
initRootNodePosition: null,
// 导出png、svg、pdf时的图形内边距
exportPaddingX: 10,
exportPaddingY: 10,
// 节点文本编辑框的z-index
nodeTextEditZIndex: 3000,
// 节点备注浮层的z-index
nodeNoteTooltipZIndex: 3000,
// 是否在点击了画布外的区域时结束节点文本的编辑状态
isEndNodeTextEditOnClickOuter: true,
// 最大历史记录数
maxHistoryCount: 1000,
// 是否一直显示节点的展开收起按钮,默认为鼠标移上去和激活时才显示
alwaysShowExpandBtn: false,
// 扩展节点可插入的图标
iconList: [
// {
// name: '',// 分组名称
// type: '',// 分组的值
// list: [// 分组下的图标列表
// {
// name: '',// 图标名称
// icon:''// 图标可以传svg或图片
// }
// ]
// }
],
// 节点最大缓存数量
maxNodeCacheCount: 1000,
// 关联线默认文字
defaultAssociativeLineText: '关联',
// 思维导图适应画布大小时的内边距
fitPadding: 50,
// 是否开启按住ctrl键多选节点功能
enableCtrlKeyNodeSelection: true,
// 设置为左键多选节点,右键拖动画布
useLeftKeySelectionRightKeyDrag: false,
// 节点即将进入编辑前的回调方法如果该方法返回true以外的值那么将取消编辑函数可以返回一个值或一个Promise回调参数为节点实例
beforeTextEdit: null
}
// 思维导图
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-06 11:18:47
* @Desc: 思维导图
*/
class MindMap {
// 构造函数
constructor(opt = {}) {
// 合并选项
this.opt = this.handleOpt(merge(defaultOpt, opt))
/**
* 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.el = this.opt.el
this.elRect = this.el.getBoundingClientRect()
// 画布宽高
this.width = this.elRect.width
this.height = this.elRect.height
// 画布宽高
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()
// 画布
this.svg = SVG().addTo(this.el).size(this.width, this.height)
this.draw = this.svg.group()
// 节点id
this.uid = 1
// 节点id
this.uid = 0
// 初始化主题
this.initTheme()
// 初始化主题
this.initTheme()
// 事件类
this.event = new Event({
mindMap: this
})
// 事件类
this.event = new Event({
mindMap: this
})
// 按键类
this.keyCommand = new KeyCommand({
mindMap: this
})
// 按键类
this.keyCommand = new KeyCommand({
mindMap: this
})
// 命令类
this.command = new Command({
mindMap: this
})
// 命令类
this.command = new Command({
mindMap: this
})
// 渲染类
this.renderer = new Render({
mindMap: this
})
// 渲染类
this.renderer = new Render({
mindMap: this
})
// 视图操作类
this.view = new View({
mindMap: this,
draw: this.draw
})
// 视图操作类
this.view = new View({
mindMap: this,
draw: this.draw
})
// 批量执行
this.batchExecution = new BatchExecution()
// 导出
this.doExport = new Export({
mindMap: this
})
// 注册插件
MindMap.pluginList.forEach((plugin) => {
this.initPlugin(plugin)
})
// 选择类
this.select = new Select({
mindMap: this
})
// 初始渲染
this.render()
setTimeout(() => {
this.command.addHistory()
}, 0)
}
// 拖动类
this.drag = new Drag({
mindMap: this
})
// 配置参数处理
handleOpt(opt) {
// 检查布局配置
if (!layoutValueList.includes(opt.layout)) {
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
// 批量执行类
this.batchExecution = new BatchExecution()
// 初始渲染
this.reRender()
setTimeout(() => {
this.command.addHistory()
}, 0)
}
// 检查主题配置
opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default'
return opt
}
// 渲染,部分渲染
render(callback, source = '') {
this.batchExecution.push('render', () => {
this.initTheme()
this.renderer.reRender = false
this.renderer.render(callback, source)
})
}
// 重新渲染
reRender(callback, source = '') {
this.batchExecution.push('render', () => {
this.draw.clear()
this.initTheme()
this.renderer.reRender = true
this.renderer.render(callback, source)
})
}
// 容器尺寸变化,调整尺寸
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.render(null, CONSTANTS.CHANGE_THEME)
}
// 获取当前主题
getTheme() {
return this.opt.theme
}
// 设置主题配置
setThemeConfig(config) {
this.opt.themeConfig = config
// 检查改变的是否是节点大小无关的主题属性
let res = checkIsNodeSizeIndependenceConfig(config)
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
}
// 获取自定义主题配置
getCustomThemeConfig() {
return this.opt.themeConfig
}
// 获取某个主题配置值
getThemeConfig(prop) {
return prop === undefined ? this.themeConfig : this.themeConfig[prop]
}
// 获取配置
getConfig(prop) {
return prop === undefined ? this.opt : this.opt[prop]
}
// 更新配置
updateConfig(opt = {}) {
this.opt = this.handleOpt(merge.all([defaultOpt, this.opt, opt]))
}
// 获取当前布局结构
getLayout() {
return this.opt.layout
}
// 设置布局结构
setLayout(layout) {
// 检查布局配置
if (!layoutValueList.includes(layout)) {
layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
}
this.opt.layout = layout
this.view.reset()
this.renderer.setLayout()
this.render()
}
// 执行命令
execCommand(...args) {
this.command.exec(...args)
}
// 动态设置思维导图数据,纯节点数据
setData(data) {
this.execCommand('CLEAR_ACTIVE_NODE')
this.command.clearHistory()
this.command.addHistory()
if (this.richText) {
this.renderer.renderTree = this.richText.handleSetData(data)
} else {
this.renderer.renderTree = data
}
this.reRender(() => {}, CONSTANTS.SET_DATA)
}
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
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.removeDataUid(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 (![CONSTANTS.MODE.READONLY, CONSTANTS.MODE.EDIT].includes(mode)) {
return
}
this.opt.readonly = mode === CONSTANTS.MODE.READONLY
if (this.opt.readonly) {
// 取消当前激活的元素
this.renderer.clearAllActive()
}
this.emit('mode_change', mode)
}
// 获取svg数据
getSvgData({ paddingX = 0, paddingY = 0 } = {}) {
const svg = this.svg
const draw = this.draw
// 保存原始信息
const origWidth = svg.width()
const origHeight = svg.height()
const origTransform = draw.transform()
const elRect = this.el.getBoundingClientRect()
// 去除放大缩小的变换效果
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 内边距
rect.width += paddingX
rect.height += paddingY
draw.translate(paddingX / 2, paddingY / 2)
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
// 克隆一份数据
let clone = svg.clone()
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
if ((rect.width > origWidth || rect.height > origHeight) && this.watermark && this.watermark.hasWatermark()) {
this.width = rect.width
this.height = rect.height
this.watermark.draw()
clone = svg.clone()
this.width = origWidth
this.height = origHeight
this.watermark.draw()
}
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight)
draw.transform(origTransform)
return {
svg: clone, // 思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组
svgHTML: clone.svg(), // svg字符串
rect: {
...rect, // 思维导图图形未缩放时的位置尺寸等信息
ratio: rect.width / rect.height // 思维导图图形的宽高比
},
origWidth, // 画布宽度
origHeight, // 画布高度
scaleX: origTransform.scaleX, // 思维导图图形的水平缩放值
scaleY: origTransform.scaleY // 思维导图图形的垂直缩放值
}
}
// 添加插件
addPlugin(plugin, opt) {
let index = MindMap.hasPlugin(plugin)
if (index === -1) {
MindMap.usePlugin(plugin, opt)
this.initPlugin(plugin)
}
}
// 移除插件
removePlugin(plugin) {
let index = MindMap.hasPlugin(plugin)
if (index !== -1) {
MindMap.pluginList.splice(index, 1)
if (this[plugin.instanceName]) {
if (this[plugin.instanceName].beforePluginRemove) {
this[plugin.instanceName].beforePluginRemove()
/**
* @Author: 王林
* @Date: 2021-07-01 22:15:22
* @Desc: 配置参数处理
*/
handleOpt(opt) {
// 检查布局配置
if (!layoutValueList.includes(opt.layout)) {
opt.layout = 'logicalStructure'
}
delete this[plugin.instanceName]
}
// 检查主题配置
opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default'
return opt
}
}
// 实例化插件
initPlugin(plugin) {
this[plugin.instanceName] = new plugin({
mindMap: this,
pluginOpt: plugin.pluginOpt
})
}
/**
* 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()
})
}
// 销毁
destroy() {
// 移除插件
[...MindMap.pluginList].forEach((plugin) => {
this[plugin.instanceName] = null
})
// 解绑事件
this.event.unbind()
// 移除画布节点
this.svg.remove()
// 去除给容器元素设置的背景样式
Style.removeBackgroundStyle(this.el)
this.el = null
}
/**
* @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)
}
}
// 插件列表
MindMap.pluginList = []
MindMap.usePlugin = (plugin, opt = {}) => {
plugin.pluginOpt = opt
MindMap.pluginList.push(plugin)
return MindMap
}
MindMap.hasPlugin = (plugin) => {
return MindMap.pluginList.findIndex((item) => {
return item === plugin
})
}
// 定义新主题
MindMap.defineTheme = (name, config = {}) => {
if (theme[name]) {
return new Error('该主题名称已存在')
}
theme[name] = merge(defaultTheme, config)
}
export default MindMap
export default MindMap

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.6.0-fix.1",
"version": "0.1.8",
"description": "一个简单的web在线思维导图",
"authors": [
{
@@ -17,23 +17,14 @@
"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",
"scripts": {},
"main": "index.js",
"type": "module",
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",
"canvg": "^3.0.7",
"deepmerge": "^1.5.2",
"eventemitter3": "^4.0.7",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0",
"quill": "^1.3.6",
"uuid": "^9.0.0",
"xml-js": "^1.6.11"
"eventemitter3": "^4.0.7"
},
"keywords": [
"javascript",
@@ -41,9 +32,5 @@
"mind-map",
"mindMap",
"MindMap"
],
"devDependencies": {
"eslint": "^8.25.0",
"prettier": "^2.7.1"
}
]
}

View File

@@ -1,46 +0,0 @@
// 遍历所有js文件
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)) {
transformFile(file)
}
})
}
const transformFile = file => {
console.log(file);
let content = fs.readFileSync(file, 'utf-8')
countCodeLines(content)
// transformComments(file, content)
}
// 统计代码行数
let totalLines = 0
const countCodeLines = (content) => {
totalLines += content.split(/\n/).length
}
// 转换注释类型
const transformComments = (file, content) => {
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)
transformFile(path.join(__dirname, '../index.js'))
console.log(totalLines);

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

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

@@ -0,0 +1,296 @@
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) {
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() {
if (!this.isMousedown) {
return;
}
this.isMousedown = false
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)
}
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

@@ -0,0 +1,182 @@
import EventEmitter from 'eventemitter3'
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 14:53:09
* @Desc: 事件类
*/
class Event extends EventEmitter {
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 14:53:25
* @Desc: 构造函数
*/
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()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 15:52:24
* @Desc: 绑定函数上下文
*/
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)
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 14:53:43
* @Desc: 绑定事件
*/
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)
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 15:40:51
* @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)
}
/**
* @Author: 王林
* @Date: 2021-04-24 13:19:39
* @Desc: 画布的单击事件
*/
onDrawClick(e) {
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
* @Date: 2021-04-07 15:17:35
* @Desc: 鼠标按下事件
*/
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)
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 15:18:32
* @Desc: 鼠标移动事件
*/
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)
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 15:18:57
* @Desc: 鼠标松开事件
*/
onMouseup(e) {
this.isLeftMousedown = false
this.emit('mouseup', e, this)
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-07 15:46:27
* @Desc: 鼠标滚动
*/
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)
}
/**
* @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,226 @@
import { imgToDataUrl, downloadFile } from './utils'
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]()
if (isDownload) {
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
}
/**
* @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

@@ -0,0 +1,142 @@
import { keyMap } from './utils/keyMap';
/**
* @Author: 王林
* @Date: 2021-04-24 15:20:46
* @Desc: 快捷按键、命令处理类
*/
export default class KeyCommand {
/**
* @Author: 王林
* @Date: 2021-04-24 15:21:32
* @Desc: 构造函数
*/
constructor(opt) {
this.opt = opt
this.mindMap = opt.mindMap
this.shortcutMap = {
//Enter: [fn]
}
this.bindEvent()
}
/**
* @Author: 王林
* @Date: 2021-04-24 15:23:22
* @Desc: 绑定事件
*/
bindEvent() {
window.addEventListener('keydown', (e) => {
Object.keys(this.shortcutMap).forEach((key) => {
if (this.checkKey(e, key)) {
e.stopPropagation()
e.preventDefault()
this.shortcutMap[key].forEach((fn) => {
fn()
})
}
})
})
}
/**
* @Author: 王林
* @Date: 2021-04-24 19:24:53
* @Desc: 检查键值是否符合
*/
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
}
/**
* @Author: 王林
* @Date: 2021-04-24 19:15:19
* @Desc: 获取事件对象里的键值数组
*/
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
}
/**
* @Author: 王林
* @Date: 2021-04-24 19:40:11
* @Desc: 获取快捷键对应的键值数组
*/
getKeyCodeArr(key) {
let keyArr = key.split(/\s*\+\s*/)
let arr = []
keyArr.forEach((item) => {
arr.push(keyMap[item])
})
return arr
}
/**
* @Author: 王林
* @Date: 2021-04-24 15:23:00
* @Desc: 添加快捷键命令
* 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]
}
})
}
/**
* 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]
}
}
})
}
}

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,884 @@
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'
// 布局列表
const layouts = {
// 逻辑结构图
logicalStructure: LogicalStructure,
// 思维导图
mindMap: MindMap,
// 目录组织图
catalogOrganization: CatalogOrganization,
// 组织结构图
organizationStructure: OrganizationStructure
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-08 16:25:07
* @Desc: 渲染
*/
class Render {
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-08 16:25:32
* @Desc: 构造函数
*/
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()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-13 16:20:07
* @Desc: 设置布局结构
*/
setLayout() {
this.layout = new (layouts[this.mindMap.opt.layout] ? layouts[this.mindMap.opt.layout] : layouts.logicalStructure)(this)
}
/**
* @Author: 王林
* @Date: 2021-06-20 10:34:06
* @Desc: 绑定事件
*/
bindEvent() {
// 点击事件
this.mindMap.on('draw_click', () => {
// 清除激活状态
if (this.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
})
}
/**
* @Author: 王林
* @Date: 2021-05-04 13:19:06
* @Desc: 注册命令
*/
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.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)
}
/**
* @Author: 王林
* @Date: 2021-07-11 16:55:44
* @Desc: 注册快捷键
*/
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('/', () => {
this.activeNodeList.forEach((node) => {
if (node.nodeData.children.length <= 0) {
return
}
this.toggleNodeExpand(node)
})
})
// 删除节点
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')
})
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-05-09 10:43:52
* @Desc: 开启文字编辑,会禁用回车键和删除键相关快捷键防止冲突
*/
startTextEdit() {
this.mindMap.keyCommand.removeShortcut('Del|Backspace')
this.mindMap.keyCommand.removeShortcut('Enter', this.insertNodeWrap)
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-05-09 10:45:11
* @Desc: 结束文字编辑,会恢复回车键和删除键相关快捷键
*/
endTextEdit() {
this.mindMap.keyCommand.addShortcut('Del|Backspace', this.removeNodeWrap)
this.mindMap.keyCommand.addShortcut('Enter', this.insertNodeWrap)
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-04-08 16:27:55
* @Desc: 渲染
*/
render() {
if (this.reRender) {
this.clearActive()
}
this.layout.doLayout((root) => {
this.root = root
this.root.render()
})
}
/**
* @Author: 王林
* @Date: 2021-04-12 22:45:01
* @Desc: 清除当前激活的节点
*/
clearActive() {
this.activeNodeList.forEach((item) => {
this.setNodeActive(item, false)
})
this.activeNodeList = []
}
/**
* @Author: 王林
* @Date: 2021-08-03 23:14:34
* @Desc: 清除当前所有激活节点,并会触发事件
*/
clearAllActive() {
if (this.activeNodeList.length <= 0) {
return
}
this.clearActive()
this.mindMap.emit('node_active', null, [])
}
/**
* @Author: 王林
* @Date: 2021-07-11 10:54:00
* @Desc: 添加节点到激活列表里
*/
addActiveNode(node) {
let index = this.findActiveNodeIndex(node)
if (index === -1) {
this.activeNodeList.push(node)
}
}
/**
* @Author: 王林
* @Date: 2021-07-10 10:04:04
* @Desc: 在激活列表里移除某个节点
*/
removeActiveNode(node) {
let index = this.findActiveNodeIndex(node)
if (index === -1) {
return
}
this.activeNodeList.splice(index, 1)
}
/**
* @Author: 王林
* @Date: 2021-07-11 10:55:23
* @Desc: 检索某个节点在激活列表里的索引
*/
findActiveNodeIndex(node) {
return this.activeNodeList.findIndex((item) => {
return item === node
})
}
/**
* @Author: 王林
* @Date: 2021-05-04 13:46:08
* @Desc: 获取节点在同级里的索引位置
*/
getNodeIndex(node) {
return node.parent ? node.parent.children.findIndex((item) => {
return item === node
}) : 0
}
/**
* @Author: 王林
* @Date: 2021-08-04 23:54:52
* @Desc: 全选
*/
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)
}
/**
* @Author: 王林
* @Date: 2021-07-11 22:34:12
* @Desc: 回退
*/
back(step) {
this.clearAllActive()
let data = this.mindMap.command.back(step)
if (data) {
this.renderTree = data
this.mindMap.reRender()
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-12 10:44:51
* @Desc: 前进
*/
forward(step) {
this.clearAllActive()
let data = this.mindMap.command.forward(step)
if (data) {
this.renderTree = data
this.mindMap.reRender()
}
}
/**
* @Author: 王林
* @Date: 2021-05-04 13:19:54
* @Desc: 插入同级节点,多个节点只会操作第一个节点
*/
insertNode() {
if (this.activeNodeList.length <= 0) {
return
}
let first = this.activeNodeList[0]
if (first.isRoot) {
this.insertChildNode()
} else {
if (first.layerIndex === 1) {
first.parent.initRender = true
}
let index = this.getNodeIndex(first)
first.parent.nodeData.children.splice(index + 1, 0, {
"data": {
"text": "分支主题",
"expand": true
},
"children": []
})
this.mindMap.render()
}
}
/**
* @Author: 王林
* @Date: 2021-05-04 13:31:02
* @Desc: 插入子节点
*/
insertChildNode() {
if (this.activeNodeList.length <= 0) {
return
}
this.activeNodeList.forEach((node, index) => {
if (!node.nodeData.children) {
node.nodeData.children = []
}
node.nodeData.children.push({
"data": {
"text": "分支主题",
"expand": true
},
"children": []
})
if (node.isRoot) {
node.initRender = true
// this.mindMap.batchExecution.push('renderNode' + index, () => {
// node.renderNode()
// })
}
})
this.mindMap.render()
}
/**
* @Author: 王林
* @Date: 2021-07-14 23:34:14
* @Desc: 上移节点,多个节点只会操作第一个节点
*/
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()
}
/**
* @Author: 王林
* @Date: 2021-07-14 23:34:18
* @Desc: 下移节点,多个节点只会操作第一个节点
*/
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()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-25 10:51:34
* @Desc: 将节点移动到另一个节点的前面
*/
insertBefore(node, exist) {
if (node.isRoot) {
return
}
let parent = node.parent
let childList = parent.children
// 要移动节点的索引
let index = childList.findIndex((item) => {
return item === node
})
if (index === -1) {
return
}
// 目标节点的索引
let existIndex = childList.findIndex((item) => {
return item === exist
})
if (existIndex === -1) {
return
}
// 当前节点在目标节点前面
if (index < existIndex) {
existIndex = existIndex - 1
} else {
existIndex = existIndex
}
// 节点实例
childList.splice(index, 1)
childList.splice(existIndex, 0, node)
// 节点数据
parent.nodeData.children.splice(index, 1)
parent.nodeData.children.splice(existIndex, 0, node.nodeData)
this.mindMap.render()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-25 10:51:34
* @Desc: 将节点移动到另一个节点的后面
*/
insertAfter(node, exist) {
if (node.isRoot) {
return
}
let parent = node.parent
let childList = parent.children
// 要移动节点的索引
let index = childList.findIndex((item) => {
return item === node
})
if (index === -1) {
return
}
// 目标节点的索引
let existIndex = childList.findIndex((item) => {
return item === exist
})
if (existIndex === -1) {
return
}
// 当前节点在目标节点前面
if (index < existIndex) {
existIndex = existIndex
} else {
existIndex = existIndex + 1
}
// 节点实例
childList.splice(index, 1)
childList.splice(existIndex, 0, node)
// 节点数据
parent.nodeData.children.splice(index, 1)
parent.nodeData.children.splice(existIndex, 0, node.nodeData)
this.mindMap.render()
}
/**
* @Author: 王林
* @Date: 2021-05-04 13:40:39
* @Desc: 移除节点
*/
removeNode() {
if (this.activeNodeList.length <= 0) {
return
}
for (let i = 0; i < this.activeNodeList.length; i++) {
let node = this.activeNodeList[i]
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()
}
/**
* @Author: 王林
* @Date: 2021-07-15 22:46:27
* @Desc: 移除某个指定节点
*/
removeOneNode(node) {
let index = this.getNodeIndex(node)
node.remove()
node.parent.children.splice(index, 1)
node.parent.nodeData.children.splice(index, 1)
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-07-15 09:53:23
* @Desc: 复制节点,多个节点只会操作第一个节点
*/
copyNode() {
if (this.activeNodeList.length <= 0) {
return
}
return copyNodeTree({}, this.activeNodeList[0])
}
/**
* @Author: 王林
* @Date: 2021-07-15 22:36:45
* @Desc: 剪切节点,多个节点只会操作第一个节点
*/
cutNode(callback) {
if (this.activeNodeList.length <= 0) {
return
}
let node = this.activeNodeList[0]
if (node.isRoot) {
return null
}
let copyData = copyNodeTree({}, node)
this.removeActiveNode(node)
this.removeOneNode(node)
this.mindMap.emit('node_active', null, this.activeNodeList)
this.mindMap.render()
if (callback && typeof callback === 'function') {
callback(copyData)
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2021-11-24 16:54:01
* @Desc: 移动一个节点作为另一个节点的子节点
*/
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()
}
/**
* @Author: 王林
* @Date: 2021-07-15 20:09:39
* @Desc: 粘贴节点到节点
*/
pasteNode(data) {
if (this.activeNodeList.length <= 0) {
return
}
this.activeNodeList.forEach((item) => {
item.nodeData.children.push(simpleDeepClone(data))
})
this.mindMap.render()
}
/**
* @Author: 王林
* @Date: 2021-07-08 21:54:30
* @Desc: 设置节点样式
*/
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)
}
/**
* @Author: 王林
* @Date: 2021-07-08 22:13:03
* @Desc: 设置节点是否激活
*/
setNodeActive(node, active) {
this.setNodeData(node, {
isActive: active
})
node.renderNode()
}
/**
* @Author: 王林
* @Date: 2021-07-10 16:52:41
* @Desc: 设置节点是否展开
*/
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()
}
/**
* @Author: 王林
* @Date: 2021-07-15 23:23:37
* @Desc: 展开所有
*/
expandAllNode() {
walk(this.renderTree, null, (node) => {
if (!node.data.expand) {
node.data.expand = true
}
}, null, true, 0, 0)
this.mindMap.render()
this.root.children.forEach((item) => {
item.updateExpandBtnNode()
})
}
/**
* @Author: 王林
* @Date: 2021-07-15 23:27:14
* @Desc: 收起所有
*/
unexpandAllNode() {
this.root.children.forEach((item) => {
this.setNodeExpand(item, false)
})
walk(this.renderTree, null, (node, parent, isRoot) => {
if (!isRoot) {
node.data.expand = false
}
}, null, true, 0, 0)
}
/**
* @Author: 王林
* @Date: 2021-07-11 17:15:33
* @Desc: 切换节点展开状态
*/
toggleNodeExpand(node) {
this.mindMap.execCommand('SET_NODE_EXPAND', node, !node.nodeData.data.expand)
}
/**
* @Author: 王林
* @Date: 2021-07-09 22:04:19
* @Desc: 设置节点文本
*/
setNodeText(node, text) {
this.setNodeDataRender(node, {
text
})
}
/**
* @Author: 王林
* @Date: 2021-07-10 08:37:40
* @Desc: 设置节点图片
*/
setNodeImage(node, {
url,
title,
width,
height
}) {
this.setNodeDataRender(node, {
image: url,
imageTitle: title || '',
imageSize: {
width,
height,
},
})
}
/**
* @Author: 王林
* @Date: 2021-07-10 08:44:06
* @Desc: 设置节点图标
*/
setNodeIcon(node, icons) {
this.setNodeDataRender(node, {
icon: icons
})
}
/**
* @Author: 王林
* @Date: 2021-07-10 08:49:33
* @Desc: 设置节点超链接
*/
setNodeHyperlink(node, link, title = '') {
this.setNodeDataRender(node, {
hyperlink: link,
hyperlinkTitle: title,
})
}
/**
* @Author: 王林
* @Date: 2021-07-10 08:52:59
* @Desc: 设置节点备注
*/
setNodeNote(node, note) {
this.setNodeDataRender(node, {
note
})
}
/**
* @Author: 王林
* @Date: 2021-07-10 08:54:53
* @Desc: 设置节点标签
*/
setNodeTag(node, tag) {
this.setNodeDataRender(node, {
tag
})
}
/**
* @Author: 王林
* @Date: 2021-05-04 14:19:48
* @Desc: 更新节点数据
*/
setNodeData(node, data) {
Object.keys(data).forEach((key) => {
node.nodeData.data[key] = data[key]
})
}
/**
* @Author: 王林
* @Date: 2021-07-10 08:45:48
* @Desc: 设置节点数据,并判断是否渲染
*/
setNodeDataRender(node, data) {
this.setNodeData(node, data)
let changed = node.getSize()
node.renderNode()
if (changed) {
this.mindMap.render()
}
}
}
export default Render

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.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,168 @@
import { tagColorList } from './utils/constant';
const rootProp = ['paddingX', 'paddingY']
/**
* @Author: 王林
* @Date: 2021-04-11 10:09:08
* @Desc: 样式类
*/
class Style {
/**
* @Author: 王林
* @Date: 2021-04-11 16:01:53
* @Desc: 设置背景样式
*/
static setBackgroundStyle(el, themeConfig) {
let { backgroundColor, backgroundImage, backgroundRepeat } = themeConfig
el.style.backgroundColor = backgroundColor
if (backgroundImage) {
el.style.backgroundImage = `url(${backgroundImage})`
el.style.backgroundRepeat = backgroundRepeat
}
}
/**
* @Author: 王林
* @Date: 2021-04-11 10:10:11
* @Desc: 构造函数
*/
constructor(ctx, themeConfig) {
this.ctx = ctx
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
* @Desc: 合并样式
*/
merge(prop, root, isActive) {
// 三级及以下节点
let defaultConfig = this.themeConfig.node
if (root || rootProp.includes(prop)) {// 直接使用最外层样式
defaultConfig = this.themeConfig
} 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.ctx.nodeData.data[prop] !== undefined ? this.ctx.nodeData.data[prop] : defaultConfig[prop]
}
/**
* @Author: 王林
* @Date: 2021-04-11 10:12:56
* @Desc: 矩形
*/
rect(node) {
node.fill({
color: this.merge('fillColor')
}).stroke({
color: this.merge('borderColor'),
width: this.merge('borderWidth'),
dasharray: this.merge('borderDasharray')
}).radius(this.merge('borderRadius'))
}
/**
* @Author: 王林
* @Date: 2021-04-11 12:07:59
* @Desc: 文字
*/
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')
})
}
/**
* @Author: 王林
* @Date: 2021-04-13 08:14:34
* @Desc: 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'
}
/**
* @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
* @Desc: 连线
*/
line(node) {
node.stroke({ width: this.merge('lineWidth', true), color: this.merge('lineColor', true) }).fill({ color: 'none' })
}
/**
* @Author: 王林
* @Date: 2021-04-11 20:03:59
* @Desc: 按钮
*/
iconBtn(node, fillNode) {
node.fill({ color: '#808080' })
fillNode.fill({ color: '#fff' })
}
}
export default Style

View File

@@ -0,0 +1,132 @@
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('Enter', () => {
this.hideEditTextBox()
})
// 注册编辑快捷键
this.mindMap.keyCommand.addShortcut('F2', () => {
if (this.renderer.activeNodeList.length <= 0) {
return
}
this.show(this.renderer.activeNodeList[0])
})
}
/**
* @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')
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)
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
}
}

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

@@ -0,0 +1,189 @@
/**
* 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 (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

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -1,262 +0,0 @@
// 标签颜色列表
export const tagColorList = [
{
color: 'rgb(77, 65, 0)',
background: 'rgb(255, 244, 179)'
},
{
color: 'rgb(0, 50, 77)',
background: 'rgb(179, 229, 255)'
},
{
color: 'rgb(77, 0, 73)',
background: 'rgb(255, 179, 251)'
},
{
color: 'rgb(57, 77, 0)',
background: 'rgb(236, 255, 179)'
},
{
color: 'rgb(0, 77, 47)',
background: 'rgb(179, 255, 226)'
}
]
// 主题列表
export const themeList = [
{
name: '默认',
value: 'default',
},
{
name: '暗色2',
value: 'dark2',
},
{
name: '天清绿',
value: 'skyGreen',
},
{
name: '脑图经典2',
value: 'classic2',
},
{
name: '脑图经典3',
value: 'classic3',
},
{
name: '经典绿',
value: 'classicGreen',
},
{
name: '经典蓝',
value: 'classicBlue',
},
{
name: '天空蓝',
value: 'blueSky',
},
{
name: '脑残粉',
value: 'brainImpairedPink',
},
{
name: '暗色',
value: 'dark',
},
{
name: '泥土黄',
value: 'earthYellow',
},
{
name: '清新绿',
value: 'freshGreen',
},
{
name: '清新红',
value: 'freshRed',
},
{
name: '浪漫紫',
value: 'romanticPurple',
},
{
name: '粉红葡萄',
value: 'pinkGrape',
},
{
name: '薄荷',
value: 'mint',
},
{
name: '金色vip',
value: 'gold',
},
{
name: '活力橙',
value: 'vitalityOrange',
},
{
name: '绿叶',
value: 'greenLeaf',
},
{
name: '脑图经典',
value: 'classic',
},
{
name: '脑图经典4',
value: 'classic4',
},
{
name: '小黄人',
value: 'minions',
},
{
name: '简约黑',
value: 'simpleBlack',
},
{
name: '课程绿',
value: 'courseGreen',
},
{
name: '咖啡',
value: 'coffee',
},
{
name: '红色精神',
value: 'redSpirit',
},
{
name: '黑色幽默',
value: 'blackHumour',
},
{
name: '深夜办公室',
value: 'lateNightOffice',
},
{
name: '黑金',
value: 'blackGold',
},
{
name: '牛油果',
value: 'avocado',
},
{
name: '秋天',
value: 'autumn',
},
{
name: '橙汁',
value: 'orangeJuice',
}
]
// 常量
export const CONSTANTS = {
CHANGE_THEME: 'changeTheme',
SET_DATA: 'setData',
TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode',
MODE: {
READONLY: 'readonly',
EDIT: 'edit'
},
LAYOUT: {
LOGICAL_STRUCTURE: 'logicalStructure',
MIND_MAP: 'mindMap',
ORGANIZATION_STRUCTURE: 'organizationStructure',
CATALOG_ORGANIZATION: 'catalogOrganization',
TIMELINE: 'timeline',
TIMELINE2: 'timeline2',
FISHBONE: 'fishbone'
},
DIR: {
UP: 'up',
LEFT: 'left',
DOWN: 'down',
RIGHT: 'right'
},
KEY_DIR: {
LEFT: 'Left',
UP: 'Up',
RIGHT: 'Right',
DOWN: 'Down'
},
SHAPE: {
RECTANGLE: 'rectangle',
DIAMOND: 'diamond',
PARALLELOGRAM: 'parallelogram',
ROUNDED_RECTANGLE: 'roundedRectangle',
OCTAGONAL_RECTANGLE: 'octagonalRectangle',
OUTER_TRIANGULAR_RECTANGLE: 'outerTriangularRectangle',
INNER_TRIANGULAR_RECTANGLE: 'innerTriangularRectangle',
ELLIPSE: 'ellipse',
CIRCLE: 'circle'
},
MOUSE_WHEEL_ACTION: {
ZOOM: 'zoom',
MOVE: 'move'
},
INIT_ROOT_NODE_POSITION: {
LEFT: 'left',
TOP: 'top',
RIGHT: 'right',
BOTTOM: 'bottom',
CENTER: 'center'
},
TIMELINE_DIR: {
TOP: 'top',
BOTTOM: 'bottom'
}
}
export const initRootNodePositionMap = {
[CONSTANTS.INIT_ROOT_NODE_POSITION.LEFT]: 0,
[CONSTANTS.INIT_ROOT_NODE_POSITION.TOP]: 0,
[CONSTANTS.INIT_ROOT_NODE_POSITION.RIGHT]: 1,
[CONSTANTS.INIT_ROOT_NODE_POSITION.BOTTOM]: 1,
[CONSTANTS.INIT_ROOT_NODE_POSITION.CENTER]: 0.5,
}
// 布局结构列表
export const layoutList = [
{
name: '逻辑结构图',
value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
},
{
name: '思维导图',
value: CONSTANTS.LAYOUT.MIND_MAP,
},
{
name: '组织结构图',
value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
},
{
name: '目录组织图',
value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION,
},
{
name: '时间轴',
value: CONSTANTS.LAYOUT.TIMELINE,
},
{
name: '时间轴2',
value: CONSTANTS.LAYOUT.TIMELINE2,
},
{
name: '鱼骨图',
value: CONSTANTS.LAYOUT.FISHBONE,
}
]
export const layoutValueList = [
CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
CONSTANTS.LAYOUT.MIND_MAP,
CONSTANTS.LAYOUT.CATALOG_ORGANIZATION,
CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
CONSTANTS.LAYOUT.TIMELINE,
CONSTANTS.LAYOUT.TIMELINE2,
CONSTANTS.LAYOUT.FISHBONE
]

View File

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

View File

@@ -1,161 +0,0 @@
import { keyMap } from './keyMap'
// 快捷按键、命令处理类
export default class KeyCommand {
// 构造函数
constructor(opt) {
this.opt = opt
this.mindMap = opt.mindMap
this.shortcutMap = {
//Enter: [fn]
}
this.shortcutMapCache = {}
this.isPause = false
this.isInSvg = 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() {
// 只有当鼠标在画布内才响应快捷键
this.mindMap.on('svg_mouseenter', () => {
this.isInSvg = true
})
this.mindMap.on('svg_mouseleave', () => {
if (this.mindMap.richText && this.mindMap.richText.showTextEdit) {
return
}
if (this.mindMap.renderer.textEdit.showTextEdit || (this.mindMap.associativeLine && this.mindMap.associativeLine.showTextEdit)) {
return
}
this.isInSvg = false
})
window.addEventListener('keydown', e => {
if (this.isPause || (this.mindMap.opt.enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) {
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

@@ -1,69 +0,0 @@
const map = {
Backspace: 8,
Tab: 9,
Enter: 13,
Shift: 16,
Control: 17,
Alt: 18,
CapsLock: 20,
Esc: 27,
Spacebar: 32,
PageUp: 33,
PageDown: 34,
End: 35,
Home: 36,
Insert: 45,
Left: 37,
Up: 38,
Right: 39,
Down: 40,
Del: 46,
NumLock: 144,
Cmd: 91,
CmdFF: 224,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
'`': 192,
'=': 187,
'-': 189,
'/': 191,
'.': 190
}
// 数字
for (let i = 0; i <= 9; i++) {
map[i] = i + 48
}
// 字母
'abcdefghijklmnopqrstuvwxyz'.split('').forEach((n, index) => {
map[n] = index + 65
})
export const keyMap = map
export const isKey = (e, key) => {
let code = typeof e === 'object' ? e.keyCode : e
return map[key] === code
}

View File

@@ -1,168 +0,0 @@
import EventEmitter from 'eventemitter3'
import { CONSTANTS } from '../../constants/constant'
// 事件类
class Event extends EventEmitter {
// 构造函数
constructor(opt = {}) {
super()
this.opt = opt
this.mindMap = opt.mindMap
this.isLeftMousedown = false
this.isRightMousedown = 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.onBodyClick = this.onBodyClick.bind(this)
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)
this.onMouseenter = this.onMouseenter.bind(this)
this.onMouseleave = this.onMouseleave.bind(this)
}
// 绑定事件
bind() {
document.body.addEventListener('click', this.onBodyClick)
this.mindMap.svg.on('click', this.onDrawClick)
this.mindMap.el.addEventListener('mousedown', this.onMousedown)
this.mindMap.svg.on('mousedown', this.onSvgMousedown)
window.addEventListener('mousemove', this.onMousemove)
window.addEventListener('mouseup', this.onMouseup)
this.mindMap.el.addEventListener('wheel', this.onMousewheel)
this.mindMap.svg.on('contextmenu', this.onContextmenu)
this.mindMap.svg.on('mouseenter', this.onMouseenter)
this.mindMap.svg.on('mouseleave', this.onMouseleave)
window.addEventListener('keyup', this.onKeyup)
}
// 解绑事件
unbind() {
document.body.removeEventListener('click', this.onBodyClick)
this.mindMap.svg.off('click', this.onDrawClick)
this.mindMap.el.removeEventListener('mousedown', this.onMousedown)
window.removeEventListener('mousemove', this.onMousemove)
window.removeEventListener('mouseup', this.onMouseup)
this.mindMap.el.removeEventListener('wheel', this.onMousewheel)
this.mindMap.svg.off('contextmenu', this.onContextmenu)
this.mindMap.svg.off('mouseenter', this.onMouseenter)
this.mindMap.svg.off('mouseleave', this.onMouseleave)
window.removeEventListener('keyup', this.onKeyup)
}
// 画布的单击事件
onDrawClick(e) {
this.emit('draw_click', e)
}
// 页面的单击事件
onBodyClick(e) {
this.emit('body_click', e)
}
// svg画布的鼠标按下事件
onSvgMousedown(e) {
this.emit('svg_mousedown', e)
}
// 鼠标按下事件
onMousedown(e) {
// 鼠标左键
if (e.which === 1) {
this.isLeftMousedown = true
} else if (e.which === 3) {
this.isRightMousedown = true
}
this.mousedownPos.x = e.clientX
this.mousedownPos.y = e.clientY
this.emit('mousedown', e, this)
}
// 鼠标移动事件
onMousemove(e) {
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
this.mousemovePos.x = e.clientX
this.mousemovePos.y = e.clientY
this.mousemoveOffset.x = e.clientX - this.mousedownPos.x
this.mousemoveOffset.y = e.clientY - this.mousedownPos.y
this.emit('mousemove', e, this)
if (
useLeftKeySelectionRightKeyDrag
? this.isRightMousedown
: this.isLeftMousedown
) {
e.preventDefault()
this.emit('drag', e, this)
}
}
// 鼠标松开事件
onMouseup(e) {
this.isLeftMousedown = false
this.isRightMousedown = false
this.emit('mouseup', e, this)
}
// 鼠标滚动
onMousewheel(e) {
e.stopPropagation()
e.preventDefault()
let dir
// 解决mac触控板双指缩放方向相反的问题
if (e.ctrlKey) {
if (e.deltaY > 0) dir = CONSTANTS.DIR.UP
if (e.deltaY < 0) dir = CONSTANTS.DIR.DOWN
if (e.deltaX > 0) dir = CONSTANTS.DIR.LEFT
if (e.deltaX < 0) dir = CONSTANTS.DIR.RIGHT
} else {
if ((e.wheelDeltaY || e.detail) > 0) dir = CONSTANTS.DIR.UP
if ((e.wheelDeltaY || e.detail) < 0) dir = CONSTANTS.DIR.DOWN
if ((e.wheelDeltaX || e.detail) > 0) dir = CONSTANTS.DIR.LEFT
if ((e.wheelDeltaX || e.detail) < 0) dir = CONSTANTS.DIR.RIGHT
}
this.emit('mousewheel', e, dir, this)
}
// 鼠标右键菜单事件
onContextmenu(e) {
e.preventDefault()
this.emit('contextmenu', e)
}
// 按键松开事件
onKeyup(e) {
this.emit('keyup', e)
}
// 进入
onMouseenter(e) {
this.emit('svg_mouseenter', e)
}
// 离开
onMouseleave(e) {
this.emit('svg_mouseleave', e)
}
}
export default Event

File diff suppressed because it is too large Load Diff

View File

@@ -1,188 +0,0 @@
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils'
// 节点文字编辑类
export default class TextEdit {
// 构造函数
constructor(renderer) {
this.renderer = renderer
this.mindMap = renderer.mindMap
// 当前编辑的节点
this.currentNode = null
// 文本编辑框
this.textEditNode = null
// 文本编辑框是否显示
this.showTextEdit = false
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
this.cacheEditingText = ''
this.bindEvent()
}
// 事件
bindEvent() {
this.show = this.show.bind(this)
this.onScale = this.onScale.bind(this)
// 节点双击事件
this.mindMap.on('node_dblclick', this.show)
// 点击事件
this.mindMap.on('draw_click', () => {
// 隐藏文本编辑框
this.hideEditTextBox()
})
this.mindMap.on('body_click', () => {
// 隐藏文本编辑框
if (this.mindMap.opt.isEndNodeTextEditOnClickOuter) {
this.hideEditTextBox()
}
})
this.mindMap.on('svg_mousedown', () => {
// 隐藏文本编辑框
this.hideEditTextBox()
})
// 展开收缩按钮点击事件
this.mindMap.on('expand_btn_click', () => {
this.hideEditTextBox()
})
// 节点激活前事件
this.mindMap.on('before_node_active', () => {
this.hideEditTextBox()
})
// 注册编辑快捷键
this.mindMap.keyCommand.addShortcut('F2', () => {
if (this.renderer.activeNodeList.length <= 0) {
return
}
this.show(this.renderer.activeNodeList[0])
})
this.mindMap.on('scale', this.onScale)
}
// 注册临时快捷键
registerTmpShortcut() {
// 注册回车快捷键
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
}
// 显示文本编辑框
async show(node) {
if (typeof this.mindMap.opt.beforeTextEdit === 'function') {
let isShow = false
try {
isShow = await this.mindMap.opt.beforeTextEdit(node)
} catch (error) {
isShow = false
}
if (!isShow) return
}
this.currentNode = node
let { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
this.mindMap.view.translateXY(offsetLeft, offsetTop)
let rect = node._textData.node.node.getBoundingClientRect()
if (this.mindMap.richText) {
this.mindMap.richText.showEditText(node, rect)
return
}
this.showEditTextBox(node, rect)
}
// 处理画布缩放
onScale() {
if (!this.currentNode) return
if (this.mindMap.richText) {
this.mindMap.richText.cacheEditingText = this.mindMap.richText.getEditText()
this.mindMap.richText.showTextEdit = false
} else {
this.cacheEditingText = this.getEditText()
this.showTextEdit = false
}
this.show(this.currentNode)
}
// 显示文本编辑框
showEditTextBox(node, rect) {
this.mindMap.emit('before_show_text_edit')
this.registerTmpShortcut()
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
this.textEditNode.setAttribute('contenteditable', true)
this.textEditNode.addEventListener('keyup', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
document.body.appendChild(this.textEditNode)
}
let scale = this.mindMap.view.scale
let lineHeight = node.style.merge('lineHeight')
let fontSize = node.style.merge('fontSize')
let textLines = (this.cacheEditingText || node.nodeData.data.text).split(/\n/gim)
let isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
node.style.domText(this.textEditNode, scale, isMultiLine)
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.innerHTML = textLines.join('<br>')
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
this.textEditNode.style.display = 'block'
this.textEditNode.style.maxWidth = this.mindMap.opt.textAutoWrapWidth * scale + 'px'
if (isMultiLine && lineHeight !== 1) {
this.textEditNode.style.transform = `translateY(${-((lineHeight * fontSize - fontSize) / 2) * scale}px)`
}
this.showTextEdit = true
// 选中文本
if (!this.cacheEditingText) {
this.selectNodeText()
}
this.cacheEditingText = ''
}
// 选中文本
selectNodeText() {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.textEditNode)
selection.removeAllRanges()
selection.addRange(range)
}
// 获取当前正在编辑的内容
getEditText() {
return getStrWithBrFromHtml(this.textEditNode.innerHTML)
}
// 隐藏文本编辑框
hideEditTextBox() {
this.currentNode = null
if (this.mindMap.richText) {
return this.mindMap.richText.hideEditText()
}
if (!this.showTextEdit) {
return
}
this.renderer.activeNodeList.forEach(node => {
let str = this.getEditText()
this.mindMap.execCommand('SET_NODE_TEXT', node, str)
if (node.isGeneralization) {
// 概要节点
node.generalizationBelongNode.updateGeneralization()
}
this.mindMap.render()
})
this.mindMap.emit(
'hide_text_edit',
this.textEditNode,
this.renderer.activeNodeList
)
this.textEditNode.style.display = 'none'
this.textEditNode.innerHTML = ''
this.textEditNode.style.fontFamily = 'inherit'
this.textEditNode.style.fontSize = 'inherit'
this.textEditNode.style.fontWeight = 'normal'
this.textEditNode.style.transform = 'translateY(0)'
this.showTextEdit = false
}
}

View File

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

View File

@@ -1,229 +0,0 @@
import { Rect, Polygon, Path } from '@svgdotjs/svg.js'
import { CONSTANTS } from '../../../constants/constant'
// 节点形状类
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 CONSTANTS.SHAPE.ROUNDED_RECTANGLE:
return {
paddingX: height > width ? (height - width) / 2 : 0,
paddingY: 0
}
case CONSTANTS.SHAPE.DIAMOND:
return {
paddingX: width / 2,
paddingY: height / 2
}
case CONSTANTS.SHAPE.PARALLELOGRAM:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case CONSTANTS.SHAPE.ELLIPSE:
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: paddingY <= 0 ? defaultPaddingY : 0
}
case CONSTANTS.SHAPE.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 === CONSTANTS.SHAPE.RECTANGLE) {
node = new Rect().size(width, height)
} else if (shape === CONSTANTS.SHAPE.DIAMOND) {
// 菱形
node = this.createDiamond()
} else if (shape === CONSTANTS.SHAPE.PARALLELOGRAM) {
// 平行四边形
node = this.createParallelogram()
} else if (shape === CONSTANTS.SHAPE.ROUNDED_RECTANGLE) {
// 圆角矩形
node = this.createRoundedRectangle()
} else if (shape === CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE) {
// 八角矩形
node = this.createOctagonalRectangle()
} else if (shape === CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE) {
// 外三角矩形
node = this.createOuterTriangularRectangle()
} else if (shape === CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE) {
// 内三角矩形
node = this.createInnerTriangularRectangle()
} else if (shape === CONSTANTS.SHAPE.ELLIPSE) {
// 椭圆
node = this.createEllipse()
} else if (shape === CONSTANTS.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 new Polygon().plot([
[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 new Polygon().plot([
[paddingX, 0],
[width, 0],
[width - paddingX, height],
[0, height]
])
}
// 创建圆角矩形
createRoundedRectangle() {
let { width, height } = this.node
let halfHeight = height / 2
return new Path().plot(`
M${halfHeight},0
L${width - halfHeight},0
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
L${halfHeight},${height}
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
`)
}
// 创建八角矩形
createOctagonalRectangle() {
let w = 5
let { width, height } = this.node
return new Polygon().plot([
[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 new Polygon().plot([
[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 new Polygon().plot([
[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 new Path().plot(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
// 创建圆
createCircle() {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return new Path().plot(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
}
// 形状列表
export const shapeList = [
CONSTANTS.SHAPE.RECTANGLE,
CONSTANTS.SHAPE.DIAMOND,
CONSTANTS.SHAPE.PARALLELOGRAM,
CONSTANTS.SHAPE.ROUNDED_RECTANGLE,
CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE,
CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE,
CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE,
CONSTANTS.SHAPE.ELLIPSE,
CONSTANTS.SHAPE.CIRCLE
]

View File

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

View File

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

View File

@@ -1,282 +0,0 @@
import { measureText, resizeImgSize, getTextFromHtml } from '../../../utils'
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
import iconsSvg from '../../../svg/icons'
import { CONSTANTS } from '../../../constants/constant'
// 创建图片节点
function createImgNode() {
let img = this.nodeData.data.image
if (!img) {
return
}
let imgSize = this.getImgShowSize()
let node = new Image().load(img).size(...imgSize)
if (this.nodeData.data.imageTitle) {
node.attr('title', this.nodeData.data.imageTitle)
}
node.on('dblclick', e => {
this.mindMap.emit('node_img_dblclick', this, e)
})
return {
node,
width: imgSize[0],
height: imgSize[1]
}
}
// 获取图片显示宽高
function getImgShowSize() {
return resizeImgSize(
this.nodeData.data.imageSize.width,
this.nodeData.data.imageSize.height,
this.mindMap.themeConfig.imgMaxWidth,
this.mindMap.themeConfig.imgMaxHeight
)
}
// 创建icon节点
function createIconNode() {
let _data = this.nodeData.data
if (!_data.icon || _data.icon.length <= 0) {
return []
}
let iconSize = this.mindMap.themeConfig.iconSize
return _data.icon.map(item => {
let src = iconsSvg.getNodeIconListIcon(item, this.mindMap.opt.iconList || [])
let node = null
// svg图标
if (/^<svg/.test(src)) {
node = SVG(src)
} else {
// 图片图标
node = new Image().load(src)
}
node.size(iconSize, iconSize)
return {
node,
width: iconSize,
height: iconSize
}
})
}
// 创建富文本节点
function createRichTextNode() {
let g = new G()
// 重新设置富文本节点内容
if (this.nodeData.data.resetRichText || [CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
delete this.nodeData.data.resetRichText
let text = getTextFromHtml(this.nodeData.data.text)
this.nodeData.data.text = `<p><span style="${this.style.createStyleText()}">${text}</span></p>`
}
let html = `<div>${this.nodeData.data.text}</div>`
let div = document.createElement('div')
div.innerHTML = html
div.style.cssText = `position: fixed; left: -999999px;`
let el = div.children[0]
el.classList.add('smm-richtext-node-wrap')
el.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px'
this.mindMap.el.appendChild(div)
let { width, height } = el.getBoundingClientRect()
width = Math.ceil(width)
height = Math.ceil(height)
g.attr('data-width', width)
g.attr('data-height', height)
html = div.innerHTML
this.mindMap.el.removeChild(div)
let foreignObject = new ForeignObject()
foreignObject.width(width)
foreignObject.height(height)
foreignObject.add(SVG(html))
g.add(foreignObject)
return {
node: g,
width,
height
}
}
// 创建文本节点
function createTextNode() {
if (this.nodeData.data.richText) {
return this.createRichTextNode()
}
let g = new G()
let fontSize = this.getStyle('fontSize', false, this.nodeData.data.isActive)
let lineHeight = this.getStyle(
'lineHeight',
false,
this.nodeData.data.isActive
)
// 文本超长自动换行
let textStyle = this.style.getTextFontStyle()
let textArr = this.nodeData.data.text.split(/\n/gim)
let maxWidth = this.mindMap.opt.textAutoWrapWidth
let isMultiLine = false
textArr.forEach((item, index) => {
let arr = item.split('')
let lines = []
let line = []
while (arr.length) {
let str = arr.shift()
let text = [...line, str].join('')
if (measureText(text, textStyle).width <= maxWidth) {
line.push(str)
} else {
lines.push(line.join(''))
line = [str]
}
}
if (line.length > 0) {
lines.push(line.join(''))
}
if (lines.length > 1) {
isMultiLine = true
}
textArr[index] = lines.join('\n')
})
textArr = textArr.join('\n').split(/\n/gim)
textArr.forEach((item, index) => {
let node = new Text().text(item)
this.style.text(node)
node.y(fontSize * lineHeight * index)
g.add(node)
})
let { width, height } = g.bbox()
width = Math.ceil(width)
height = Math.ceil(height)
g.attr('data-width', width)
g.attr('data-height', height)
g.attr('data-ismultiLine', isMultiLine || textArr.length > 1)
return {
node: g,
width,
height
}
}
// 创建超链接节点
function createHyperlinkNode() {
let { hyperlink, hyperlinkTitle } = this.nodeData.data
if (!hyperlink) {
return
}
let iconSize = this.mindMap.themeConfig.iconSize
let node = new SVG()
// 超链接节点
let a = new A().to(hyperlink).target('_blank')
a.node.addEventListener('click', e => {
e.stopPropagation()
})
if (hyperlinkTitle) {
a.attr('title', hyperlinkTitle)
}
// 添加一个透明的层,作为鼠标区域
a.rect(iconSize, iconSize).fill({ color: 'transparent' })
// 超链接图标
let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize)
this.style.iconNode(iconNode)
a.add(iconNode)
node.add(a)
return {
node,
width: iconSize,
height: iconSize
}
}
// 创建标签节点
function createTagNode() {
let tagData = this.nodeData.data.tag
if (!tagData || tagData.length <= 0) {
return []
}
let nodes = []
tagData.slice(0, this.mindMap.opt.maxTag).forEach((item, index) => {
let tag = new G()
// 标签文本
let text = new Text().text(item).x(8).cy(10)
this.style.tagText(text, index)
let { width } = text.bbox()
// 标签矩形
let rect = new Rect().size(width + 16, 20)
this.style.tagRect(rect, index)
tag.add(rect).add(text)
nodes.push({
node: tag,
width: width + 16,
height: 20
})
})
return nodes
}
// 创建备注节点
function createNoteNode() {
if (!this.nodeData.data.note) {
return null
}
let iconSize = this.mindMap.themeConfig.iconSize
let node = new SVG().attr('cursor', 'pointer')
// 透明的层,用来作为鼠标区域
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
// 备注图标
let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize)
this.style.iconNode(iconNode)
node.add(iconNode)
// 备注tooltip
if (!this.mindMap.opt.customNoteContentShow) {
if (!this.noteEl) {
this.noteEl = document.createElement('div')
this.noteEl.style.cssText = `
position: absolute;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
display: none;
background-color: #fff;
z-index: ${ this.mindMap.opt.nodeNoteTooltipZIndex }
`
document.body.appendChild(this.noteEl)
}
this.noteEl.innerText = this.nodeData.data.note
}
node.on('mouseover', () => {
let { left, top } = node.node.getBoundingClientRect()
if (!this.mindMap.opt.customNoteContentShow) {
this.noteEl.style.left = left + 'px'
this.noteEl.style.top = top + iconSize + 'px'
this.noteEl.style.display = 'block'
} else {
this.mindMap.opt.customNoteContentShow.show(
this.nodeData.data.note,
left,
top + iconSize
)
}
})
node.on('mouseout', () => {
if (!this.mindMap.opt.customNoteContentShow) {
this.noteEl.style.display = 'none'
} else {
this.mindMap.opt.customNoteContentShow.hide()
}
})
return {
node,
width: iconSize,
height: iconSize
}
}
export default {
createImgNode,
getImgShowSize,
createIconNode,
createRichTextNode,
createTextNode,
createHyperlinkNode,
createTagNode,
createNoteNode
}

View File

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

View File

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

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