Compare commits
206 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2ca5d0fba | ||
|
|
470604e567 | ||
|
|
f5f665ec0a | ||
|
|
5e865a4e33 | ||
|
|
38ad33b604 | ||
|
|
e1b4146171 | ||
|
|
65151f4b0a | ||
|
|
942706fb63 | ||
|
|
5f4492d4b7 | ||
|
|
ace1f62a40 | ||
|
|
706c88c7d5 | ||
|
|
4512fb16eb | ||
|
|
e446ff12e7 | ||
|
|
4318646abe | ||
|
|
2cbfe4f0e7 | ||
|
|
b7910c4665 | ||
|
|
fc1ba24834 | ||
|
|
cf16937160 | ||
|
|
c3393abed6 | ||
|
|
2abff3e21b | ||
|
|
e39a94c5e2 | ||
|
|
a6fff7f7a3 | ||
|
|
e584081b41 | ||
|
|
b91dde8084 | ||
|
|
077478d654 | ||
|
|
2be97cc1a0 | ||
|
|
0f7dc949a4 | ||
|
|
c7e91cc9eb | ||
|
|
6eacfab9c2 | ||
|
|
e36a408275 | ||
|
|
abda5b7d06 | ||
|
|
f815f71dd7 | ||
|
|
fa2c5b420c | ||
|
|
4c19bc76a7 | ||
|
|
d08a317920 | ||
|
|
bd805836cd | ||
|
|
e804a8f2f7 | ||
|
|
8bf876d446 | ||
|
|
f2521f663e | ||
|
|
e676bff453 | ||
|
|
8f2cc72d3c | ||
|
|
ec22656bee | ||
|
|
4acf8ba2ac | ||
|
|
d45a18904e | ||
|
|
9fc2dbabd4 | ||
|
|
b83b81f52e | ||
|
|
d1e2db993e | ||
|
|
ab931901e2 | ||
|
|
9879a25f9b | ||
|
|
16fb8eb092 | ||
|
|
de859927ed | ||
|
|
7bde59f664 | ||
|
|
be9668c7b8 | ||
|
|
95fe3189d5 | ||
|
|
9c60857c6a | ||
|
|
3b7cea9ee3 | ||
|
|
3f081e5021 | ||
|
|
d959420d6e | ||
|
|
79d81b92e6 | ||
|
|
940c60f23d | ||
|
|
965ab8151e | ||
|
|
87d55b31ca | ||
|
|
bb68575aca | ||
|
|
e561e804be | ||
|
|
5a8c3aa9d3 | ||
|
|
f84639debd | ||
|
|
de77a2b613 | ||
|
|
3825c3769f | ||
|
|
876908e922 | ||
|
|
25ecde8948 | ||
|
|
2de0334e3b | ||
|
|
1e32338d23 | ||
|
|
21f487321a | ||
|
|
750af31996 | ||
|
|
dc1aaee75d | ||
|
|
2122fa59d7 | ||
|
|
5b7c65adad | ||
|
|
fabf4535a8 | ||
|
|
04566080e0 | ||
|
|
0f9d9cdb21 | ||
|
|
11538d1789 | ||
|
|
49bae6cc6c | ||
|
|
9644ba0c8d | ||
|
|
ac2df3cb2e | ||
|
|
aad1f911c8 | ||
|
|
27b50bf4ae | ||
|
|
2d22837e32 | ||
|
|
866f2071ce | ||
|
|
9cb35eac29 | ||
|
|
6ff63d9d56 | ||
|
|
43539df25b | ||
|
|
cbafc24670 | ||
|
|
cc83a18b18 | ||
|
|
5ae931da92 | ||
|
|
853b999a7d | ||
|
|
505622f3dc | ||
|
|
0a5b41e3e0 | ||
|
|
8630c47b26 | ||
|
|
6105fd6e3d | ||
|
|
d7b1c4e4fe | ||
|
|
6ccafaa771 | ||
|
|
6b15e469a2 | ||
|
|
5675e29df3 | ||
|
|
52d094a7c7 | ||
|
|
633ed68f92 | ||
|
|
388594e29a | ||
|
|
af148fef27 | ||
|
|
983e55bd1d | ||
|
|
69dab99bf7 | ||
|
|
c94f459ff9 | ||
|
|
cd3baf411b | ||
|
|
042911371c | ||
|
|
8e2fb72309 | ||
|
|
77aacccdad | ||
|
|
5413c867e3 | ||
|
|
4fc7eb084e | ||
|
|
a9b04312d8 | ||
|
|
4cd9b66653 | ||
|
|
9b7b41597f | ||
|
|
e56ff8fdf2 | ||
|
|
8d9299aed7 | ||
|
|
f8506cb75b | ||
|
|
c90ee9e892 | ||
|
|
4d21b84882 | ||
|
|
331f0bdf97 | ||
|
|
0a4898697d | ||
|
|
79ae08fc9a | ||
|
|
1795773af9 | ||
|
|
747a781ad8 | ||
|
|
fcfcb1c3d1 | ||
|
|
d412ae8cce | ||
|
|
d834b76d42 | ||
|
|
3f9da8940f | ||
|
|
26d5f69af2 | ||
|
|
6efe4a3fd6 | ||
|
|
2b4ab4a322 | ||
|
|
cca42d1f89 | ||
|
|
724fef87b1 | ||
|
|
d50c0e4cd5 | ||
|
|
c18c037642 | ||
|
|
cda1da5fd0 | ||
|
|
eba1aa3a37 | ||
|
|
81018bb615 | ||
|
|
5fa0ff7b5c | ||
|
|
cabe286ebb | ||
|
|
e337da088b | ||
|
|
de97ea9e75 | ||
|
|
cc331065eb | ||
|
|
9a8e630654 | ||
|
|
17ab977efb | ||
|
|
6bd10d9451 | ||
|
|
5313b9b69c | ||
|
|
8dcfdcc44a | ||
|
|
67422df3ff | ||
|
|
2ad7536eb7 | ||
|
|
6f56e5c4e6 | ||
|
|
5ae8ebe590 | ||
|
|
6ecb97e4e5 | ||
|
|
c8f938dd3e | ||
|
|
0d29e29162 | ||
|
|
be1b3dffce | ||
|
|
3ba2dbe415 | ||
|
|
19a96c92a9 | ||
|
|
c265e3e437 | ||
|
|
7434ac2648 | ||
|
|
63d73a73aa | ||
|
|
00f9258a4d | ||
|
|
7087b43d39 | ||
|
|
945a78b7b1 | ||
|
|
55acc19ab8 | ||
|
|
410f0be05e | ||
|
|
2d7c091071 | ||
|
|
9bf58a54ce | ||
|
|
231adeea44 | ||
|
|
0ea618af39 | ||
|
|
f66297ff99 | ||
|
|
3113bf2e1f | ||
|
|
8c114dac02 | ||
|
|
06dba79272 | ||
|
|
8441986ca7 | ||
|
|
c8b50908e1 | ||
|
|
7e11fde892 | ||
|
|
3d9f3bd7a8 | ||
|
|
46e11649b0 | ||
|
|
161ef7590c | ||
|
|
ca660a3c74 | ||
|
|
7a24872331 | ||
|
|
eb4ea9fb3a | ||
|
|
64228c02ff | ||
|
|
f184a5d948 | ||
|
|
0bf5b2d6f7 | ||
|
|
74a52ea386 | ||
|
|
9914eef5b9 | ||
|
|
f547f741f2 | ||
|
|
c26149fa42 | ||
|
|
b2aa3648eb | ||
|
|
b997604ab2 | ||
|
|
c6929b82ad | ||
|
|
b40da53aef | ||
|
|
1e5dfd97e1 | ||
|
|
97bcc22abd | ||
|
|
bd655839cb | ||
|
|
a53a4e8e1d | ||
|
|
eb01646747 | ||
|
|
d27eee0fae | ||
|
|
92ee241ed8 |
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
dist_electron
|
||||
43
README.md
@@ -7,15 +7,22 @@
|
||||
[](https://github.com/wanglin2/mind-map/network/members)
|
||||

|
||||
|
||||
> 一个简单&强大的Web思维导图库
|
||||
> 一个简单&强大的Web思维导图
|
||||
|
||||
Demo:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
|
||||
本项目包含两部分:
|
||||
|
||||
文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)
|
||||
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/)
|
||||
|
||||
# 特性
|
||||
|
||||
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图四种结构
|
||||
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
|
||||
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴、鱼骨图六种结构
|
||||
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
|
||||
- [x] 支持快捷键
|
||||
- [x] 节点内容支持图片、图标、超链接、备注、标签、概要
|
||||
@@ -24,8 +31,9 @@ Demo:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-ma
|
||||
- [x] 支持右键和Ctrl+左键两种多选方式
|
||||
- [x] 支持节点自由拖拽、拖拽调整
|
||||
- [x] 支持多种节点形状
|
||||
- [x] 支持导出为`json`、`png`、`svg`、`pdf`,支持从`json`、`xmind`导入
|
||||
- [x] 支持小地图
|
||||
- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`,支持从`json`、`xmind`、`markdown`导入
|
||||
- [x] 支持小地图、支持水印
|
||||
- [x] 支持关联线
|
||||
|
||||
# 安装
|
||||
|
||||
@@ -35,10 +43,23 @@ npm i simple-mind-map
|
||||
|
||||
# 使用
|
||||
|
||||
提供一个宽高不为0的容器元素:
|
||||
|
||||
```html
|
||||
<div id="mindMapContainer"></div>
|
||||
```
|
||||
|
||||
另外再设置一下`css`样式:
|
||||
|
||||
```css
|
||||
#mindMapContainer * {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
```
|
||||
|
||||
然后创建一个实例:
|
||||
|
||||
```js
|
||||
import MindMap from "simple-mind-map";
|
||||
|
||||
@@ -53,6 +74,14 @@ const mindMap = new MindMap({
|
||||
});
|
||||
```
|
||||
|
||||
即可得到一个思维导图。
|
||||
|
||||
想要实现更多功能?可以查看[开发文档](https://wanglin2.github.io/mind-map/#/doc/zh/)。
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
MIT
|
||||
|
||||
# 微信交流群
|
||||
|
||||

|
||||
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 254 KiB |
BIN
qrcode.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
@@ -900,6 +900,17 @@ const data5 = {
|
||||
}
|
||||
}
|
||||
|
||||
// 富文本数据v0.4.0+,需要使用RichText插件才支持富文本编辑
|
||||
const richTextData = {
|
||||
"root": {
|
||||
"data": {
|
||||
"text": "<a href='http://lxqnsys.com/' target='_blank'>理想去年实验室</a>",
|
||||
"richText": true
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
}
|
||||
|
||||
const rootData = {
|
||||
"root": {
|
||||
"data": {
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
"isActive": false
|
||||
},
|
||||
"children": []
|
||||
}, {
|
||||
"data": {
|
||||
"text": "<a href='http://lxqnsys.com/' target='_blank'>理想去年实验室</a>",
|
||||
"richText": true
|
||||
},
|
||||
"children": []
|
||||
}]
|
||||
}]
|
||||
},
|
||||
|
||||
28
simple-mind-map/full.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import MindMap from './index'
|
||||
import MiniMap from './src/MiniMap.js'
|
||||
import Watermark from './src/Watermark.js'
|
||||
import KeyboardNavigation from './src/KeyboardNavigation.js'
|
||||
import Export from './src/Export.js'
|
||||
import Drag from './src/Drag.js'
|
||||
import Select from './src/Select.js'
|
||||
import AssociativeLine from './src/AssociativeLine'
|
||||
import RichText from './src/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(Export)
|
||||
.usePlugin(Select)
|
||||
.usePlugin(AssociativeLine)
|
||||
.usePlugin(RichText)
|
||||
|
||||
export default MindMap
|
||||
@@ -7,16 +7,9 @@ import Style from './src/Style'
|
||||
import KeyCommand from './src/KeyCommand'
|
||||
import Command from './src/Command'
|
||||
import BatchExecution from './src/BatchExecution'
|
||||
import Export from './src/Export'
|
||||
import Select from './src/Select'
|
||||
import Drag from './src/Drag'
|
||||
import MiniMap from './src/MiniMap'
|
||||
import Watermark from './src/Watermark'
|
||||
import { layoutValueList } from './src/utils/constant'
|
||||
import { layoutValueList, CONSTANTS } from './src/utils/constant'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import xmind from './src/parse/xmind'
|
||||
import { simpleDeepClone } from './src/utils'
|
||||
import KeyboardNavigation from './src/KeyboardNavigation'
|
||||
import defaultTheme from './src/themes/default'
|
||||
|
||||
// 默认选项配置
|
||||
@@ -24,7 +17,9 @@ const defaultOpt = {
|
||||
// 是否只读
|
||||
readonly: false,
|
||||
// 布局
|
||||
layout: 'logicalStructure',
|
||||
layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
|
||||
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
|
||||
fishboneDeg: 45,
|
||||
// 主题
|
||||
theme: 'default', // 内置主题:default(默认主题)
|
||||
// 主题配置,会和所选择的主题进行合并
|
||||
@@ -66,7 +61,64 @@ const defaultOpt = {
|
||||
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或图片
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
// 思维导图
|
||||
@@ -89,7 +141,7 @@ class MindMap {
|
||||
this.draw = this.svg.group()
|
||||
|
||||
// 节点id
|
||||
this.uid = 0
|
||||
this.uid = 1
|
||||
|
||||
// 初始化主题
|
||||
this.initTheme()
|
||||
@@ -120,41 +172,16 @@ class MindMap {
|
||||
draw: this.draw
|
||||
})
|
||||
|
||||
// 小地图类
|
||||
this.miniMap = new MiniMap({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 导出类
|
||||
this.doExport = new Export({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 选择类
|
||||
this.select = new Select({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 拖动类
|
||||
this.drag = new Drag({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 键盘导航类
|
||||
this.keyboardNavigation = new KeyboardNavigation({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 水印类
|
||||
this.watermark = new Watermark({
|
||||
mindMap: this
|
||||
})
|
||||
|
||||
// 批量执行类
|
||||
this.batchExecution = new BatchExecution()
|
||||
|
||||
// 注册插件
|
||||
MindMap.pluginList.forEach((plugin) => {
|
||||
this.initPlugin(plugin)
|
||||
})
|
||||
|
||||
// 初始渲染
|
||||
this.reRender()
|
||||
this.render()
|
||||
setTimeout(() => {
|
||||
this.command.addHistory()
|
||||
}, 0)
|
||||
@@ -164,7 +191,7 @@ class MindMap {
|
||||
handleOpt(opt) {
|
||||
// 检查布局配置
|
||||
if (!layoutValueList.includes(opt.layout)) {
|
||||
opt.layout = 'logicalStructure'
|
||||
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
}
|
||||
// 检查主题配置
|
||||
opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default'
|
||||
@@ -172,21 +199,21 @@ class MindMap {
|
||||
}
|
||||
|
||||
// 渲染,部分渲染
|
||||
render() {
|
||||
render(callback, source = '') {
|
||||
this.batchExecution.push('render', () => {
|
||||
this.initTheme()
|
||||
this.renderer.reRender = false
|
||||
this.renderer.render()
|
||||
this.renderer.render(callback, source)
|
||||
})
|
||||
}
|
||||
|
||||
// 重新渲染
|
||||
reRender() {
|
||||
reRender(callback) {
|
||||
this.batchExecution.push('render', () => {
|
||||
this.draw.clear()
|
||||
this.initTheme()
|
||||
this.renderer.reRender = true
|
||||
this.renderer.render()
|
||||
this.renderer.render(callback)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -225,7 +252,7 @@ class MindMap {
|
||||
setTheme(theme) {
|
||||
this.renderer.clearAllActive()
|
||||
this.opt.theme = theme
|
||||
this.reRender()
|
||||
this.render(null, CONSTANTS.CHANGE_THEME)
|
||||
}
|
||||
|
||||
// 获取当前主题
|
||||
@@ -236,7 +263,7 @@ class MindMap {
|
||||
// 设置主题配置
|
||||
setThemeConfig(config) {
|
||||
this.opt.themeConfig = config
|
||||
this.reRender()
|
||||
this.render(null, CONSTANTS.CHANGE_THEME)
|
||||
}
|
||||
|
||||
// 获取自定义主题配置
|
||||
@@ -268,9 +295,10 @@ class MindMap {
|
||||
setLayout(layout) {
|
||||
// 检查布局配置
|
||||
if (!layoutValueList.includes(layout)) {
|
||||
layout = 'logicalStructure'
|
||||
layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
}
|
||||
this.opt.layout = layout
|
||||
this.view.reset()
|
||||
this.renderer.setLayout()
|
||||
this.render()
|
||||
}
|
||||
@@ -284,7 +312,12 @@ class MindMap {
|
||||
setData(data) {
|
||||
this.execCommand('CLEAR_ACTIVE_NODE')
|
||||
this.command.clearHistory()
|
||||
this.renderer.renderTree = data
|
||||
this.command.addHistory()
|
||||
if (this.richText) {
|
||||
this.renderer.renderTree = this.richText.handleSetData(data)
|
||||
} else {
|
||||
this.renderer.renderTree = data
|
||||
}
|
||||
this.reRender()
|
||||
}
|
||||
|
||||
@@ -311,7 +344,7 @@ class MindMap {
|
||||
|
||||
// 获取思维导图数据,节点树、主题、布局等
|
||||
getData(withConfig) {
|
||||
let nodeData = this.command.getCopyData()
|
||||
let nodeData = this.command.removeDataUid(this.command.getCopyData())
|
||||
let data = {}
|
||||
if (withConfig) {
|
||||
data = {
|
||||
@@ -345,19 +378,112 @@ class MindMap {
|
||||
|
||||
// 设置只读模式、编辑模式
|
||||
setMode(mode) {
|
||||
if (!['readonly', 'edit'].includes(mode)) {
|
||||
if (![CONSTANTS.MODE.READONLY, CONSTANTS.MODE.EDIT].includes(mode)) {
|
||||
return
|
||||
}
|
||||
this.opt.readonly = mode === 'readonly'
|
||||
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()
|
||||
}
|
||||
delete this[plugin.instanceName]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 实例化插件
|
||||
initPlugin(plugin) {
|
||||
this[plugin.instanceName] = new plugin({
|
||||
mindMap: this,
|
||||
pluginOpt: plugin.pluginOpt
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
MindMap.xmind = xmind
|
||||
// 插件列表
|
||||
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 = {}) => {
|
||||
|
||||
1375
simple-mind-map/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.2.24",
|
||||
"version": "0.5.8",
|
||||
"description": "一个简单的web在线思维导图",
|
||||
"authors": [
|
||||
{
|
||||
@@ -22,14 +22,18 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"module": "index.js",
|
||||
"main": "./dist/simpleMindMap.umd.min.js",
|
||||
"__main": "./dist/simpleMindMap.umd.min.js",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.0.16",
|
||||
"canvg": "^3.0.7",
|
||||
"deepmerge": "^1.5.2",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"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"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
627
simple-mind-map/src/AssociativeLine.js
Normal file
@@ -0,0 +1,627 @@
|
||||
import { walk, bfsWalk, throttle } from './utils/'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import {
|
||||
getAssociativeLineTargetIndex,
|
||||
computeCubicBezierPathPoints,
|
||||
joinCubicBezierPath,
|
||||
cubicBezierPath,
|
||||
getNodePoint,
|
||||
computeNodePoints,
|
||||
getNodeLinePath,
|
||||
getDefaultControlPointOffsets
|
||||
} from './utils/associativeLineUtils'
|
||||
|
||||
// 关联线类
|
||||
class AssociativeLine {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
this.draw = this.mindMap.draw
|
||||
// 当前所有连接线
|
||||
this.lineList = []
|
||||
// 当前激活的连接线
|
||||
this.activeLine = null
|
||||
// 当前正在创建连接线
|
||||
this.isCreatingLine = false // 是否正在创建连接线中
|
||||
this.creatingStartNode = null // 起始节点
|
||||
this.creatingLine = null // 创建过程中的连接线
|
||||
this.overlapNode = null // 创建过程中的目标节点
|
||||
// 是否有节点正在被拖拽
|
||||
this.isNodeDragging = false
|
||||
// 箭头图标
|
||||
this.markerPath = null
|
||||
this.marker = this.createMarker()
|
||||
// 控制点
|
||||
this.controlLine1 = null
|
||||
this.controlLine2 = null
|
||||
this.controlPoint1 = null
|
||||
this.controlPoint2 = null
|
||||
this.controlPointDiameter = 10
|
||||
this.isControlPointMousedown = false
|
||||
this.mousedownControlPointKey = ''
|
||||
this.controlPointMousemoveState = {
|
||||
pos: null,
|
||||
startPoint: null,
|
||||
endPoint: null,
|
||||
targetIndex: ''
|
||||
}
|
||||
// 节流一下,不然很卡
|
||||
this.checkOverlapNode = throttle(this.checkOverlapNode, 100, this)
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
bindEvent() {
|
||||
// 节点树渲染完毕后渲染连接线
|
||||
this.renderAllLines = this.renderAllLines.bind(this)
|
||||
this.mindMap.on('node_tree_render_end', this.renderAllLines)
|
||||
// 状态改变后重新渲染连接线
|
||||
this.mindMap.on('data_change', this.renderAllLines)
|
||||
// 监听画布和节点点击事件,用于清除当前激活的连接线
|
||||
this.mindMap.on('draw_click', () => {
|
||||
if (this.isControlPointMousedown) {
|
||||
return
|
||||
}
|
||||
this.clearActiveLine()
|
||||
})
|
||||
this.mindMap.on('node_click', node => {
|
||||
if (this.isCreatingLine) {
|
||||
this.completeCreateLine(node)
|
||||
} else {
|
||||
this.clearActiveLine()
|
||||
}
|
||||
})
|
||||
// 注册删除快捷键
|
||||
this.mindMap.keyCommand.addShortcut(
|
||||
'Del|Backspace',
|
||||
this.removeLine.bind(this)
|
||||
)
|
||||
// 注册添加连接线的命令
|
||||
this.mindMap.command.add('ADD_ASSOCIATIVE_LINE', this.addLine.bind(this))
|
||||
// 监听鼠标移动事件
|
||||
this.mindMap.on('mousemove', this.onMousemove.bind(this))
|
||||
// 节点拖拽事件
|
||||
this.mindMap.on('node_dragging', this.onNodeDragging.bind(this))
|
||||
this.mindMap.on('node_dragend', this.onNodeDragend.bind(this))
|
||||
// 拖拽控制点
|
||||
window.addEventListener('mousemove', e => {
|
||||
this.onControlPointMousemove(e)
|
||||
})
|
||||
window.addEventListener('mouseup', e => {
|
||||
this.onControlPointMouseup(e)
|
||||
})
|
||||
}
|
||||
|
||||
// 创建箭头
|
||||
createMarker() {
|
||||
return this.draw.marker(20, 20, add => {
|
||||
add.ref(2, 5)
|
||||
add.size(10, 10)
|
||||
add.attr('orient', 'auto-start-reverse')
|
||||
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染所有连线
|
||||
renderAllLines() {
|
||||
// 先移除
|
||||
this.removeAllLines()
|
||||
this.removeControls()
|
||||
this.clearActiveLine()
|
||||
let tree = this.mindMap.renderer.root
|
||||
if (!tree) return
|
||||
let idToNode = new Map()
|
||||
let nodeToIds = new Map()
|
||||
walk(
|
||||
tree,
|
||||
null,
|
||||
cur => {
|
||||
if (!cur) return
|
||||
let data = cur.nodeData.data
|
||||
if (
|
||||
data.associativeLineTargets &&
|
||||
data.associativeLineTargets.length > 0
|
||||
) {
|
||||
nodeToIds.set(cur, data.associativeLineTargets)
|
||||
}
|
||||
if (data.id) {
|
||||
idToNode.set(data.id, cur)
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
true,
|
||||
0
|
||||
)
|
||||
nodeToIds.forEach((ids, node) => {
|
||||
ids.forEach(id => {
|
||||
let toNode = idToNode.get(id)
|
||||
if (!node || !toNode) return
|
||||
let [startPoint, endPoint] = computeNodePoints(node, toNode)
|
||||
this.drawLine(startPoint, endPoint, node, toNode)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 绘制连接线
|
||||
drawLine(startPoint, endPoint, node, toNode) {
|
||||
let {
|
||||
associativeLineWidth,
|
||||
associativeLineColor,
|
||||
associativeLineActiveWidth,
|
||||
associativeLineActiveColor
|
||||
} = this.mindMap.themeConfig
|
||||
// 箭头
|
||||
this.markerPath
|
||||
.stroke({ color: associativeLineColor })
|
||||
.fill({ color: associativeLineColor })
|
||||
// 路径
|
||||
let { path: pathStr, controlPoints } = getNodeLinePath(
|
||||
startPoint,
|
||||
endPoint,
|
||||
node,
|
||||
toNode
|
||||
)
|
||||
// 虚线
|
||||
let path = this.draw.path()
|
||||
path
|
||||
.stroke({
|
||||
width: associativeLineWidth,
|
||||
color: associativeLineColor,
|
||||
dasharray: [6, 4]
|
||||
})
|
||||
.fill({ color: 'none' })
|
||||
path.plot(pathStr)
|
||||
path.marker('end', this.marker)
|
||||
// 不可见的点击线
|
||||
let clickPath = this.draw.path()
|
||||
clickPath
|
||||
.stroke({ width: associativeLineActiveWidth, color: 'transparent' })
|
||||
.fill({ color: 'none' })
|
||||
clickPath.plot(pathStr)
|
||||
// 点击事件
|
||||
clickPath.click(e => {
|
||||
e.stopPropagation()
|
||||
// 如果当前存在激活节点,那么取消激活节点
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.clearActiveNodes()
|
||||
} else {
|
||||
// 否则清除当前的关联线的激活状态,如果有的话
|
||||
this.clearActiveLine()
|
||||
// 保存当前激活的关联线信息
|
||||
this.activeLine = [path, clickPath, node, toNode]
|
||||
// 让不可见的点击线显示
|
||||
clickPath.stroke({ color: associativeLineActiveColor })
|
||||
// 渲染控制点和连线
|
||||
this.renderControls(
|
||||
startPoint,
|
||||
endPoint,
|
||||
controlPoints[0],
|
||||
controlPoints[1]
|
||||
)
|
||||
this.mindMap.emit(
|
||||
'associative_line_click',
|
||||
path,
|
||||
clickPath,
|
||||
node,
|
||||
toNode
|
||||
)
|
||||
}
|
||||
})
|
||||
this.lineList.push([path, clickPath, node, toNode])
|
||||
}
|
||||
|
||||
// 移除所有连接线
|
||||
removeAllLines() {
|
||||
this.lineList.forEach(line => {
|
||||
line[0].remove()
|
||||
line[1].remove()
|
||||
})
|
||||
this.lineList = []
|
||||
}
|
||||
|
||||
// 从当前激活节点开始创建连接线
|
||||
createLineFromActiveNode() {
|
||||
if (this.mindMap.renderer.activeNodeList.length <= 0) return
|
||||
let node = this.mindMap.renderer.activeNodeList[0]
|
||||
this.createLine(node)
|
||||
}
|
||||
|
||||
// 创建连接线
|
||||
createLine(fromNode) {
|
||||
let { associativeLineWidth, associativeLineColor } =
|
||||
this.mindMap.themeConfig
|
||||
if (this.isCreatingLine || !fromNode) return
|
||||
this.isCreatingLine = true
|
||||
this.creatingStartNode = fromNode
|
||||
this.creatingLine = this.draw.path()
|
||||
this.creatingLine
|
||||
.stroke({
|
||||
width: associativeLineWidth,
|
||||
color: associativeLineColor,
|
||||
dasharray: [6, 4]
|
||||
})
|
||||
.fill({ color: 'none' })
|
||||
this.creatingLine.marker('end', this.marker)
|
||||
}
|
||||
|
||||
// 鼠标移动事件
|
||||
onMousemove(e) {
|
||||
if (!this.isCreatingLine) return
|
||||
this.updateCreatingLine(e)
|
||||
}
|
||||
|
||||
// 更新创建过程中的连接线
|
||||
updateCreatingLine(e) {
|
||||
let { x, y } = this.getTransformedEventPos(e)
|
||||
let startPoint = getNodePoint(this.creatingStartNode)
|
||||
let pathStr = cubicBezierPath(startPoint.x, startPoint.y, x, y)
|
||||
this.creatingLine.plot(pathStr)
|
||||
this.checkOverlapNode(x, y)
|
||||
}
|
||||
|
||||
// 获取转换后的鼠标事件对象的坐标
|
||||
getTransformedEventPos(e) {
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
let { scaleX, scaleY, translateX, translateY } =
|
||||
this.mindMap.draw.transform()
|
||||
return {
|
||||
x: (x - translateX) / scaleX,
|
||||
y: (y - translateY) / scaleY
|
||||
}
|
||||
}
|
||||
|
||||
// 检测当前移动到的目标节点
|
||||
checkOverlapNode(x, y) {
|
||||
this.overlapNode = null
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
if (node.nodeData.data.isActive) {
|
||||
this.mindMap.renderer.setNodeActive(node, false)
|
||||
}
|
||||
if (node === this.creatingStartNode || this.overlapNode) {
|
||||
return
|
||||
}
|
||||
let { left, top, width, height } = node
|
||||
let right = left + width
|
||||
let bottom = top + height
|
||||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||
this.overlapNode = node
|
||||
}
|
||||
})
|
||||
if (this.overlapNode && !this.overlapNode.nodeData.data.isActive) {
|
||||
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
|
||||
}
|
||||
}
|
||||
|
||||
// 完成创建连接线
|
||||
completeCreateLine(node) {
|
||||
if (this.creatingStartNode === node) return
|
||||
this.addLine(this.creatingStartNode, node)
|
||||
if (this.overlapNode && this.overlapNode.nodeData.data.isActive) {
|
||||
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
|
||||
}
|
||||
this.isCreatingLine = false
|
||||
this.creatingStartNode = null
|
||||
this.creatingLine.remove()
|
||||
this.creatingLine = null
|
||||
this.overlapNode = null
|
||||
}
|
||||
|
||||
// 添加连接线
|
||||
addLine(fromNode, toNode) {
|
||||
if (!fromNode || !toNode) return
|
||||
// 目标节点如果没有id,则生成一个id
|
||||
let id = toNode.nodeData.data.id
|
||||
if (!id) {
|
||||
id = uuid()
|
||||
this.mindMap.execCommand('SET_NODE_DATA', toNode, {
|
||||
id
|
||||
})
|
||||
}
|
||||
// 将目标节点id保存起来
|
||||
let list = fromNode.nodeData.data.associativeLineTargets || []
|
||||
list.push(id)
|
||||
// 保存控制点
|
||||
let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
|
||||
let controlPoints = computeCubicBezierPathPoints(
|
||||
startPoint.x,
|
||||
startPoint.y,
|
||||
endPoint.x,
|
||||
endPoint.y
|
||||
)
|
||||
let offsetList =
|
||||
fromNode.nodeData.data.associativeLineTargetControlOffsets || []
|
||||
// 保存的实际是控制点和端点的差值,否则当节点位置改变了,控制点还是原来的位置,连线就不对了
|
||||
offsetList[list.length - 1] = [
|
||||
{
|
||||
x: controlPoints[0].x - startPoint.x,
|
||||
y: controlPoints[0].y - startPoint.y
|
||||
},
|
||||
{
|
||||
x: controlPoints[1].x - endPoint.x,
|
||||
y: controlPoints[1].y - endPoint.y
|
||||
}
|
||||
]
|
||||
this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
|
||||
associativeLineTargets: list,
|
||||
associativeLineTargetControlOffsets: offsetList
|
||||
})
|
||||
}
|
||||
|
||||
// 删除连接线
|
||||
removeLine() {
|
||||
if (!this.activeLine) return
|
||||
let [, , node, toNode] = this.activeLine
|
||||
this.removeControls()
|
||||
let { associativeLineTargets, associativeLineTargetControlOffsets } =
|
||||
node.nodeData.data
|
||||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
associativeLineTargets: associativeLineTargets.filter((_, index) => {
|
||||
return index !== targetIndex
|
||||
}),
|
||||
associativeLineTargetControlOffsets: associativeLineTargetControlOffsets
|
||||
? associativeLineTargetControlOffsets.filter((_, index) => {
|
||||
return index !== targetIndex
|
||||
})
|
||||
: []
|
||||
})
|
||||
}
|
||||
|
||||
// 清除当前激活的节点
|
||||
clearActiveNodes() {
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
}
|
||||
}
|
||||
|
||||
// 清除激活的线
|
||||
clearActiveLine() {
|
||||
if (this.activeLine) {
|
||||
this.activeLine[1].stroke({
|
||||
color: 'transparent'
|
||||
})
|
||||
this.activeLine = null
|
||||
this.removeControls()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理节点正在拖拽事件
|
||||
onNodeDragging() {
|
||||
if (this.isNodeDragging) return
|
||||
this.isNodeDragging = true
|
||||
this.lineList.forEach(line => {
|
||||
line[0].hide()
|
||||
line[1].hide()
|
||||
})
|
||||
this.hideControls()
|
||||
}
|
||||
|
||||
// 处理节点拖拽完成事件
|
||||
onNodeDragend() {
|
||||
if (!this.isNodeDragging) return
|
||||
this.lineList.forEach(line => {
|
||||
line[0].show()
|
||||
line[1].show()
|
||||
})
|
||||
this.showControls()
|
||||
this.isNodeDragging = false
|
||||
}
|
||||
|
||||
// 创建控制点、连线节点
|
||||
createControlNodes() {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
// 连线
|
||||
this.controlLine1 = this.draw
|
||||
.line()
|
||||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||||
this.controlLine2 = this.draw
|
||||
.line()
|
||||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||||
// 控制点
|
||||
this.controlPoint1 = this.createOneControlNode('controlPoint1')
|
||||
this.controlPoint2 = this.createOneControlNode('controlPoint2')
|
||||
}
|
||||
|
||||
// 创建控制点
|
||||
createOneControlNode(pointKey) {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
return this.draw
|
||||
.circle(this.controlPointDiameter)
|
||||
.stroke({ color: associativeLineActiveColor })
|
||||
.fill({ color: '#fff' })
|
||||
.click(e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
.mousedown(e => {
|
||||
this.onControlPointMousedown(e, pointKey)
|
||||
})
|
||||
}
|
||||
|
||||
// 控制点的鼠标按下事件
|
||||
onControlPointMousedown(e, pointKey) {
|
||||
e.stopPropagation()
|
||||
this.isControlPointMousedown = true
|
||||
this.mousedownControlPointKey = pointKey
|
||||
}
|
||||
|
||||
// 控制点的鼠标移动事件
|
||||
onControlPointMousemove(e) {
|
||||
if (
|
||||
!this.isControlPointMousedown ||
|
||||
!this.mousedownControlPointKey ||
|
||||
!this[this.mousedownControlPointKey]
|
||||
)
|
||||
return
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
let radius = this.controlPointDiameter / 2
|
||||
// 转换鼠标当前的位置
|
||||
let { x, y } = this.getTransformedEventPos(e)
|
||||
this.controlPointMousemoveState.pos = {
|
||||
x,
|
||||
y
|
||||
}
|
||||
// 更新当前拖拽的控制点的位置
|
||||
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
|
||||
let [path, clickPath, node, toNode] = this.activeLine
|
||||
let [startPoint, endPoint] = computeNodePoints(node, toNode)
|
||||
this.controlPointMousemoveState.startPoint = startPoint
|
||||
this.controlPointMousemoveState.endPoint = endPoint
|
||||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||||
this.controlPointMousemoveState.targetIndex = targetIndex
|
||||
let offsets = []
|
||||
let associativeLineTargetControlOffsets = node.nodeData.data.associativeLineTargetControlOffsets
|
||||
if (!associativeLineTargetControlOffsets) {
|
||||
// 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况
|
||||
offsets = getDefaultControlPointOffsets(startPoint, endPoint)
|
||||
} else {
|
||||
offsets = associativeLineTargetControlOffsets[targetIndex]
|
||||
}
|
||||
let point1 = null
|
||||
let point2 = null
|
||||
// 拖拽的是控制点1
|
||||
if (this.mousedownControlPointKey === 'controlPoint1') {
|
||||
point1 = {
|
||||
x,
|
||||
y
|
||||
}
|
||||
point2 = {
|
||||
x: endPoint.x + offsets[1].x,
|
||||
y: endPoint.y + offsets[1].y
|
||||
}
|
||||
// 更新控制点1的连线
|
||||
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
|
||||
} else {
|
||||
// 拖拽的是控制点2
|
||||
point1 = {
|
||||
x: startPoint.x + offsets[0].x,
|
||||
y: startPoint.y + offsets[0].y
|
||||
}
|
||||
point2 = {
|
||||
x,
|
||||
y
|
||||
}
|
||||
// 更新控制点2的连线
|
||||
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
|
||||
}
|
||||
// 更新关联线
|
||||
let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
|
||||
path.plot(pathStr)
|
||||
clickPath.plot(pathStr)
|
||||
}
|
||||
|
||||
// 控制点的鼠标移动事件
|
||||
onControlPointMouseup(e) {
|
||||
if (!this.isControlPointMousedown) return
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
let { pos, startPoint, endPoint, targetIndex } =
|
||||
this.controlPointMousemoveState
|
||||
let [, , node] = this.activeLine
|
||||
let offsetList = []
|
||||
let associativeLineTargetControlOffsets = node.nodeData.data.associativeLineTargetControlOffsets
|
||||
if (!associativeLineTargetControlOffsets) {
|
||||
// 兼容0.4.5版本,没有associativeLineTargetControlOffsets的情况
|
||||
offsetList[targetIndex] = getDefaultControlPointOffsets(startPoint, endPoint)
|
||||
} else {
|
||||
offsetList = associativeLineTargetControlOffsets
|
||||
}
|
||||
let offset1 = null
|
||||
let offset2 = null
|
||||
if (this.mousedownControlPointKey === 'controlPoint1') {
|
||||
// 更新控制点1数据
|
||||
offset1 = {
|
||||
x: pos.x - startPoint.x,
|
||||
y: pos.y - startPoint.y
|
||||
}
|
||||
offset2 = offsetList[targetIndex][1]
|
||||
} else {
|
||||
// 更新控制点2数据
|
||||
offset1 = offsetList[targetIndex][0]
|
||||
offset2 = {
|
||||
x: pos.x - endPoint.x,
|
||||
y: pos.y - endPoint.y
|
||||
}
|
||||
}
|
||||
offsetList[targetIndex] = [offset1, offset2]
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
associativeLineTargetControlOffsets: offsetList
|
||||
})
|
||||
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚,所以重置isControlPointMousedown需要等draw_click事件触发完以后
|
||||
setTimeout(() => {
|
||||
this.resetControlPoint()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 复位控制点移动
|
||||
resetControlPoint() {
|
||||
this.isControlPointMousedown = false
|
||||
this.mousedownControlPointKey = ''
|
||||
this.controlPointMousemoveState = {
|
||||
pos: null,
|
||||
startPoint: null,
|
||||
endPoint: null,
|
||||
targetIndex: ''
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染控制点
|
||||
renderControls(startPoint, endPoint, point1, point2) {
|
||||
if (!this.controlLine1) {
|
||||
this.createControlNodes()
|
||||
}
|
||||
let radius = this.controlPointDiameter / 2
|
||||
// 控制点和起终点的连线
|
||||
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
|
||||
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
|
||||
// 控制点
|
||||
this.controlPoint1.x(point1.x - radius).y(point1.y - radius)
|
||||
this.controlPoint2.x(point2.x - radius).y(point2.y - radius)
|
||||
}
|
||||
|
||||
// 删除控制点
|
||||
removeControls() {
|
||||
if (!this.controlLine1) return
|
||||
;[
|
||||
this.controlLine1,
|
||||
this.controlLine2,
|
||||
this.controlPoint1,
|
||||
this.controlPoint2
|
||||
].forEach(item => {
|
||||
item.remove()
|
||||
})
|
||||
this.controlLine1 = null
|
||||
this.controlLine2 = null
|
||||
this.controlPoint1 = null
|
||||
this.controlPoint2 = null
|
||||
}
|
||||
|
||||
// 隐藏控制点
|
||||
hideControls() {
|
||||
if (!this.controlLine1) return
|
||||
;[
|
||||
this.controlLine1,
|
||||
this.controlLine2,
|
||||
this.controlPoint1,
|
||||
this.controlPoint2
|
||||
].forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示控制点
|
||||
showControls() {
|
||||
if (!this.controlLine1) return
|
||||
;[
|
||||
this.controlLine1,
|
||||
this.controlLine2,
|
||||
this.controlPoint1,
|
||||
this.controlPoint2
|
||||
].forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
AssociativeLine.instanceName = 'associativeLine'
|
||||
|
||||
export default AssociativeLine
|
||||
@@ -1,33 +1,4 @@
|
||||
// 在下一个事件循环里执行任务
|
||||
const nextTick = function (fn, ctx) {
|
||||
let pending = false
|
||||
let timerFunc = null
|
||||
let handle = () => {
|
||||
pending = false
|
||||
ctx ? fn.call(ctx) : fn()
|
||||
}
|
||||
// 支持MutationObserver接口的话使用MutationObserver
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
let counter = 1
|
||||
let observer = new MutationObserver(handle)
|
||||
let textNode = document.createTextNode(counter)
|
||||
observer.observe(textNode, {
|
||||
characterData: true // 设为 true 表示监视指定目标节点或子节点树中节点所包含的字符数据的变化
|
||||
})
|
||||
timerFunc = function () {
|
||||
counter = (counter + 1) % 2 // counter会在0和1两者循环变化
|
||||
textNode.data = counter // 节点变化会触发回调handle,
|
||||
}
|
||||
} else {
|
||||
// 否则使用定时器
|
||||
timerFunc = setTimeout
|
||||
}
|
||||
return function () {
|
||||
if (pending) return
|
||||
pending = true
|
||||
timerFunc(handle, 0)
|
||||
}
|
||||
}
|
||||
import { nextTick } from './utils'
|
||||
|
||||
// 批量执行
|
||||
class BatchExecution {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { copyRenderTree, simpleDeepClone } from './utils'
|
||||
import { copyRenderTree, simpleDeepClone, nextTick } from './utils'
|
||||
|
||||
// 命令类
|
||||
class Command {
|
||||
@@ -11,6 +11,7 @@ class Command {
|
||||
this.activeHistoryIndex = 0
|
||||
// 注册快捷键
|
||||
this.registerShortcutKeys()
|
||||
this.addHistory = nextTick(this.addHistory, this)
|
||||
}
|
||||
|
||||
// 清空历史数据
|
||||
@@ -36,7 +37,7 @@ class Command {
|
||||
this.commands[name].forEach(fn => {
|
||||
fn(...args)
|
||||
})
|
||||
if (name === 'BACK' || name === 'FORWARD') {
|
||||
if (['BACK', 'FORWARD', 'SET_NODE_ACTIVE', 'CLEAR_ACTIVE_NODE'].includes(name)) {
|
||||
return
|
||||
}
|
||||
this.addHistory()
|
||||
@@ -72,10 +73,23 @@ class Command {
|
||||
|
||||
// 添加回退数据
|
||||
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', data)
|
||||
this.mindMap.emit('data_change', this.removeDataUid(data))
|
||||
this.mindMap.emit(
|
||||
'back_forward',
|
||||
this.activeHistoryIndex,
|
||||
@@ -85,6 +99,9 @@ class Command {
|
||||
|
||||
// 回退
|
||||
back(step = 1) {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
if (this.activeHistoryIndex - step >= 0) {
|
||||
this.activeHistoryIndex -= step
|
||||
this.mindMap.emit(
|
||||
@@ -92,23 +109,45 @@ class Command {
|
||||
this.activeHistoryIndex,
|
||||
this.history.length
|
||||
)
|
||||
return simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
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)
|
||||
return simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import Base from './layouts/Base'
|
||||
|
||||
class Drag extends Base {
|
||||
// 构造函数
|
||||
|
||||
constructor({ mindMap }) {
|
||||
super(mindMap.renderer)
|
||||
this.mindMap = mindMap
|
||||
@@ -14,7 +13,6 @@ class Drag extends Base {
|
||||
}
|
||||
|
||||
// 复位
|
||||
|
||||
reset() {
|
||||
// 当前拖拽节点
|
||||
this.node = null
|
||||
@@ -45,10 +43,11 @@ class Drag extends Base {
|
||||
this.mouseDownY = 0
|
||||
this.mouseMoveX = 0
|
||||
this.mouseMoveY = 0
|
||||
// 鼠标移动的距离距鼠标按下的位置距离多少以上才认为是拖动事件
|
||||
this.checkDragOffset = 10
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
|
||||
bindEvent() {
|
||||
this.checkOverlapNode = throttle(this.checkOverlapNode, 300, this)
|
||||
this.mindMap.on('node_mousedown', (node, e) => {
|
||||
@@ -77,13 +76,14 @@ class Drag extends Base {
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
this.mindMap.emit('node_dragging', this.node)
|
||||
e.preventDefault()
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseMoveX = x
|
||||
this.mouseMoveY = y
|
||||
if (
|
||||
Math.abs(x - this.mouseDownX) <= 10 &&
|
||||
Math.abs(y - this.mouseDownY) <= 10 &&
|
||||
Math.abs(x - this.mouseDownX) <= this.checkDragOffset &&
|
||||
Math.abs(y - this.mouseDownY) <= this.checkDragOffset &&
|
||||
!this.node.isDrag
|
||||
) {
|
||||
return
|
||||
@@ -97,7 +97,6 @@ class Drag extends Base {
|
||||
}
|
||||
|
||||
// 鼠标松开事件
|
||||
|
||||
onMouseup(e) {
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
@@ -136,10 +135,10 @@ class Drag extends Base {
|
||||
this.mindMap.render()
|
||||
}
|
||||
this.reset()
|
||||
this.mindMap.emit('node_dragend')
|
||||
}
|
||||
|
||||
// 创建克隆节点
|
||||
|
||||
createCloneNode() {
|
||||
if (!this.clone) {
|
||||
// 节点
|
||||
@@ -161,7 +160,6 @@ class Drag extends Base {
|
||||
}
|
||||
|
||||
// 移除克隆节点
|
||||
|
||||
removeCloneNode() {
|
||||
if (!this.clone) {
|
||||
return
|
||||
@@ -172,7 +170,6 @@ class Drag extends Base {
|
||||
}
|
||||
|
||||
// 拖动中
|
||||
|
||||
onMove(x, y) {
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
@@ -199,14 +196,12 @@ class Drag extends Base {
|
||||
}
|
||||
|
||||
// 检测重叠节点
|
||||
|
||||
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
|
||||
let x = this.mouseMoveX
|
||||
let y = this.mouseMoveY
|
||||
this.overlapNode = null
|
||||
this.prevNode = null
|
||||
this.nextNode = null
|
||||
@@ -221,35 +216,71 @@ class Drag extends Base {
|
||||
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
|
||||
let nodeRect = this.getNodeRect(node)
|
||||
let oneFourthHeight = nodeRect.height / 4
|
||||
// 前一个和后一个节点
|
||||
let checkList = node.parent ? node.parent.children.filter((item) => {
|
||||
return item !== this.node
|
||||
}) : []
|
||||
let index = checkList.findIndex((item) => {
|
||||
return item === node
|
||||
})
|
||||
let prevBrother = null
|
||||
let nextBrother = null
|
||||
if (index !== -1) {
|
||||
if (index - 1 >= 0) {
|
||||
prevBrother = checkList[index - 1]
|
||||
}
|
||||
if (index + 1 <= checkList.length - 1) {
|
||||
nextBrother = checkList[index + 1]
|
||||
}
|
||||
}
|
||||
// 检测兄弟节点位置
|
||||
if (!this.prevNode && !this.nextNode && !node.isRoot) {
|
||||
// && this.node.isBrother(node)
|
||||
if (left <= checkRight && right >= this.cloneNodeLeft) {
|
||||
if (this.cloneNodeTop > bottom && this.cloneNodeTop <= bottom + 10) {
|
||||
// 和前一个兄弟节点的距离
|
||||
let prevBrotherOffset = 0
|
||||
if (prevBrother) {
|
||||
let prevNodeRect = this.getNodeRect(prevBrother)
|
||||
prevBrotherOffset = nodeRect.top - prevNodeRect.bottom
|
||||
// 间距小于10就当它不存在
|
||||
prevBrotherOffset = prevBrotherOffset >= 10 ? prevBrotherOffset / 2 : 0
|
||||
} else {
|
||||
// 没有前一个兄弟节点,那么假设和前一个节点的距离为20
|
||||
prevBrotherOffset = 10
|
||||
}
|
||||
// 和后一个兄弟节点的距离
|
||||
let nextBrotherOffset = 0
|
||||
if (nextBrother) {
|
||||
let nextNodeRect = this.getNodeRect(nextBrother)
|
||||
nextBrotherOffset = nextNodeRect.top - nodeRect.bottom
|
||||
nextBrotherOffset = nextBrotherOffset >= 10 ? nextBrotherOffset / 2 : 0
|
||||
} else {
|
||||
nextBrotherOffset = 10
|
||||
}
|
||||
if (nodeRect.left <= x && nodeRect.right >= x) {
|
||||
// 检测兄弟节点位置
|
||||
if (!this.overlapNode && !this.prevNode && !this.nextNode && !node.isRoot) {
|
||||
let checkIsPrevNode = nextBrotherOffset > 0 ? // 距离下一个兄弟节点的距离大于0
|
||||
y > nodeRect.bottom && y <= (nodeRect.bottom + nextBrotherOffset) : // 那么在当前节点外底部判断
|
||||
y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断
|
||||
let checkIsNextNode = prevBrotherOffset > 0 ? // 距离上一个兄弟节点的距离大于0
|
||||
y < nodeRect.top && y >= (nodeRect.top - prevBrotherOffset) : // 那么在当前节点外底部判断
|
||||
y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
|
||||
if (checkIsPrevNode) {
|
||||
this.prevNode = node
|
||||
this.placeholder.size(node.width, 10).move(_left, _bottom)
|
||||
} else if (checkBottom < top && checkBottom >= top - 10) {
|
||||
let size = nextBrotherOffset > 0 ? nextBrotherOffset : 5
|
||||
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom)
|
||||
} else if (checkIsNextNode) {
|
||||
this.nextNode = node
|
||||
this.placeholder.size(node.width, 10).move(_left, _top - 10)
|
||||
let size = prevBrotherOffset > 0 ? prevBrotherOffset : 5
|
||||
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originTop - size)
|
||||
}
|
||||
}
|
||||
// 检测是否重叠
|
||||
if (!this.overlapNode && !this.prevNode && !this.nextNode) {
|
||||
if (
|
||||
nodeRect.top + (prevBrotherOffset > 0 ? 0 : oneFourthHeight) <= y &&
|
||||
nodeRect.bottom - (nextBrotherOffset > 0 ? 0 : oneFourthHeight) >= y
|
||||
) {
|
||||
this.overlapNode = node
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,6 +289,32 @@ class Drag extends Base {
|
||||
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算节点的位置尺寸信息
|
||||
getNodeRect(node) {
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
let { left, top, width, height } = node
|
||||
let originLeft = left
|
||||
let originTop = top
|
||||
let originBottom = top + height
|
||||
let right = (left + width) * scaleX + translateX
|
||||
let bottom = (top + height) * scaleY + translateY
|
||||
left = left * scaleX + translateX
|
||||
top = top * scaleY + translateY
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
originLeft,
|
||||
originTop,
|
||||
originBottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Drag.instanceName = 'drag'
|
||||
|
||||
export default Drag
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
|
||||
// 事件类
|
||||
class Event extends EventEmitter {
|
||||
@@ -26,6 +27,7 @@ class Event extends EventEmitter {
|
||||
|
||||
// 绑定函数上下文
|
||||
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)
|
||||
@@ -34,33 +36,36 @@ class Event extends EventEmitter {
|
||||
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)
|
||||
// 兼容火狐浏览器
|
||||
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.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('mousewheel', this.onMousewheel)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -69,6 +74,11 @@ class Event extends EventEmitter {
|
||||
this.emit('draw_click', e)
|
||||
}
|
||||
|
||||
// 页面的单击事件
|
||||
onBodyClick(e) {
|
||||
this.emit('body_click', e)
|
||||
}
|
||||
|
||||
// svg画布的鼠标按下事件
|
||||
onSvgMousedown(e) {
|
||||
this.emit('svg_mousedown', e)
|
||||
@@ -76,7 +86,6 @@ class Event extends EventEmitter {
|
||||
|
||||
// 鼠标按下事件
|
||||
onMousedown(e) {
|
||||
// e.preventDefault()
|
||||
// 鼠标左键
|
||||
if (e.which === 1) {
|
||||
this.isLeftMousedown = true
|
||||
@@ -88,13 +97,13 @@ class Event extends EventEmitter {
|
||||
|
||||
// 鼠标移动事件
|
||||
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) {
|
||||
e.preventDefault()
|
||||
this.emit('drag', e, this)
|
||||
}
|
||||
}
|
||||
@@ -110,10 +119,17 @@ class Event extends EventEmitter {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
let dir
|
||||
if ((e.wheelDeltaY || e.detail) > 0) {
|
||||
dir = 'up'
|
||||
// 解决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 {
|
||||
dir = 'down'
|
||||
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)
|
||||
}
|
||||
@@ -128,6 +144,16 @@ class Event extends EventEmitter {
|
||||
onKeyup(e) {
|
||||
this.emit('keyup', e)
|
||||
}
|
||||
|
||||
// 进入
|
||||
onMouseenter(e) {
|
||||
this.emit('svg_mouseenter', e)
|
||||
}
|
||||
|
||||
// 离开
|
||||
onMouseleave(e) {
|
||||
this.emit('svg_mouseleave', e)
|
||||
}
|
||||
}
|
||||
|
||||
export default Event
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { imgToDataUrl, downloadFile } from './utils'
|
||||
import JsPDF from 'jspdf'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import drawBackgroundImageToCanvas from './utils/simulateCSSBackgroundInCanvas'
|
||||
import { transformToMarkdown } from './parse/toMarkdown'
|
||||
const URL = window.URL || window.webkitURL || window
|
||||
|
||||
// 导出类
|
||||
@@ -26,7 +28,11 @@ class Export {
|
||||
|
||||
// 获取svg数据
|
||||
async getSvgData() {
|
||||
let { svg, svgHTML } = this.mindMap.miniMap.getMiniMap()
|
||||
let { exportPaddingX, exportPaddingY } = this.mindMap.opt
|
||||
let { svg, svgHTML } = this.mindMap.getSvgData({
|
||||
paddingX: exportPaddingX,
|
||||
paddingY: exportPaddingY
|
||||
})
|
||||
// 把图片的url转换成data:url类型,否则导出会丢失图片
|
||||
let imageList = svg.find('image')
|
||||
let task = imageList.map(async item => {
|
||||
@@ -35,6 +41,9 @@ class Export {
|
||||
item.attr('href', imgData)
|
||||
})
|
||||
await Promise.all(task)
|
||||
if (imageList.length > 0) {
|
||||
svgHTML = svg.svg()
|
||||
}
|
||||
return {
|
||||
node: svg,
|
||||
str: svgHTML
|
||||
@@ -42,7 +51,7 @@ class Export {
|
||||
}
|
||||
|
||||
// svg转png
|
||||
svgToPng(svgSrc) {
|
||||
svgToPng(svgSrc, transparent) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
|
||||
@@ -54,7 +63,9 @@ class Export {
|
||||
canvas.height = img.height + this.exportPadding * 2
|
||||
let ctx = canvas.getContext('2d')
|
||||
// 绘制背景
|
||||
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
|
||||
if (!transparent) {
|
||||
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
|
||||
}
|
||||
// 图片绘制到canvas里
|
||||
ctx.drawImage(
|
||||
img,
|
||||
@@ -85,7 +96,9 @@ class Export {
|
||||
let {
|
||||
backgroundColor = '#fff',
|
||||
backgroundImage,
|
||||
backgroundRepeat = 'repeat'
|
||||
backgroundRepeat = 'no-repeat',
|
||||
backgroundPosition = 'center center',
|
||||
backgroundSize = 'cover',
|
||||
} = this.mindMap.themeConfig
|
||||
// 背景颜色
|
||||
ctx.save()
|
||||
@@ -96,19 +109,18 @@ class Export {
|
||||
// 背景图片
|
||||
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()
|
||||
drawBackgroundImageToCanvas(ctx, width, height, backgroundImage, {
|
||||
backgroundRepeat,
|
||||
backgroundPosition,
|
||||
backgroundSize
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
ctx.restore()
|
||||
resolve()
|
||||
}
|
||||
img.onerror = e => {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
@@ -120,8 +132,14 @@ class Export {
|
||||
* 方法1.把svg的图片都转化成data:url格式,再转换
|
||||
* 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换
|
||||
*/
|
||||
async png() {
|
||||
let { str } = await this.getSvgData()
|
||||
async png(name, transparent = false) {
|
||||
let { node, str } = await this.getSvgData()
|
||||
// 如果开启了富文本,则使用htmltocanvas转换为图片
|
||||
if (this.mindMap.richText) {
|
||||
let res = await this.mindMap.richText.handleExportPng(node.node)
|
||||
let imgDataUrl = await this.svgToPng(res, transparent)
|
||||
return imgDataUrl
|
||||
}
|
||||
// 转换成blob数据
|
||||
let blob = new Blob([str], {
|
||||
type: 'image/svg+xml'
|
||||
@@ -129,7 +147,7 @@ class Export {
|
||||
// 转换成data:url数据
|
||||
let svgUrl = URL.createObjectURL(blob)
|
||||
// 绘制到canvas上
|
||||
let imgDataUrl = await this.svgToPng(svgUrl)
|
||||
let imgDataUrl = await this.svgToPng(svgUrl, transparent)
|
||||
URL.revokeObjectURL(svgUrl)
|
||||
return imgDataUrl
|
||||
}
|
||||
@@ -189,8 +207,18 @@ class Export {
|
||||
}
|
||||
|
||||
// 导出为svg
|
||||
async svg(name) {
|
||||
// plusCssText:附加的css样式,如果svg中存在dom节点,想要设置一些针对节点的样式可以通过这个参数传入
|
||||
async svg(name, plusCssText) {
|
||||
let { node } = await this.getSvgData()
|
||||
// 开启了节点富文本编辑
|
||||
if (this.mindMap.richText) {
|
||||
if (plusCssText) {
|
||||
let foreignObjectList = node.find('foreignObject')
|
||||
if (foreignObjectList.length > 0) {
|
||||
foreignObjectList[0].add(SVG(`<style>${plusCssText}</style>`))
|
||||
}
|
||||
}
|
||||
}
|
||||
node.first().before(SVG(`<title>${name}</title>`))
|
||||
await this.drawBackgroundToSvg(node)
|
||||
let str = node.svg()
|
||||
@@ -213,6 +241,16 @@ class Export {
|
||||
smm(name, withConfig) {
|
||||
return this.json(name, withConfig)
|
||||
}
|
||||
|
||||
// markdown文件
|
||||
md() {
|
||||
let data = this.mindMap.getData()
|
||||
let content = transformToMarkdown(data)
|
||||
let blob = new Blob([content])
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
}
|
||||
|
||||
Export.instanceName = 'doExport'
|
||||
|
||||
export default Export
|
||||
|
||||
@@ -10,6 +10,7 @@ export default class KeyCommand {
|
||||
}
|
||||
this.shortcutMapCache = {}
|
||||
this.isPause = false
|
||||
this.isInSvg = false
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@@ -37,8 +38,21 @@ export default class KeyCommand {
|
||||
|
||||
// 绑定事件
|
||||
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) {
|
||||
return
|
||||
}
|
||||
this.isInSvg = false
|
||||
})
|
||||
window.addEventListener('keydown', e => {
|
||||
if (this.isPause) {
|
||||
if (this.isPause || (this.mindMap.opt.enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) {
|
||||
return
|
||||
}
|
||||
Object.keys(this.shortcutMap).forEach(key => {
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
import { isKey } from './utils/keyMap'
|
||||
import { bfsWalk } from './utils'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
|
||||
// 键盘导航类
|
||||
export default class KeyboardNavigation {
|
||||
class KeyboardNavigation {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
this.opt = opt
|
||||
this.mindMap = opt.mindMap
|
||||
this.onKeyup = this.onKeyup.bind(this)
|
||||
this.mindMap.on('keyup', this.onKeyup)
|
||||
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.LEFT, () => {
|
||||
this.onKeyup(CONSTANTS.KEY_DIR.LEFT)
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.UP, () => {
|
||||
this.onKeyup(CONSTANTS.KEY_DIR.UP)
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.RIGHT, () => {
|
||||
this.onKeyup(CONSTANTS.KEY_DIR.RIGHT)
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.DOWN, () => {
|
||||
this.onKeyup(CONSTANTS.KEY_DIR.DOWN)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理按键事件
|
||||
onKeyup(e) {
|
||||
;['Left', 'Up', 'Right', 'Down'].forEach(dir => {
|
||||
if (isKey(e, dir)) {
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.focus(dir)
|
||||
} else {
|
||||
let root = this.mindMap.renderer.root
|
||||
this.mindMap.renderer.moveNodeToCenter(root)
|
||||
root.active()
|
||||
}
|
||||
}
|
||||
})
|
||||
onKeyup(dir) {
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.focus(dir)
|
||||
} else {
|
||||
let root = this.mindMap.renderer.root
|
||||
this.mindMap.renderer.moveNodeToCenter(root)
|
||||
root.active()
|
||||
}
|
||||
}
|
||||
|
||||
// 聚焦到下一个节点
|
||||
@@ -95,19 +102,19 @@ export default class KeyboardNavigation {
|
||||
let { left, top, right, bottom } = rect
|
||||
let match = false
|
||||
// 按下了左方向键
|
||||
if (dir === 'Left') {
|
||||
if (dir === CONSTANTS.KEY_DIR.LEFT) {
|
||||
// 判断节点是否在当前节点的左侧
|
||||
match = right <= currentActiveNodeRect.left
|
||||
// 按下了右方向键
|
||||
} else if (dir === 'Right') {
|
||||
} else if (dir === CONSTANTS.KEY_DIR.RIGHT) {
|
||||
// 判断节点是否在当前节点的右侧
|
||||
match = left >= currentActiveNodeRect.right
|
||||
// 按下了上方向键
|
||||
} else if (dir === 'Up') {
|
||||
} else if (dir === CONSTANTS.KEY_DIR.UP) {
|
||||
// 判断节点是否在当前节点的上面
|
||||
match = bottom <= currentActiveNodeRect.top
|
||||
// 按下了下方向键
|
||||
} else if (dir === 'Down') {
|
||||
} else if (dir === CONSTANTS.KEY_DIR.DOWN) {
|
||||
// 判断节点是否在当前节点的下面
|
||||
match = top >= currentActiveNodeRect.bottom
|
||||
}
|
||||
@@ -130,22 +137,22 @@ export default class KeyboardNavigation {
|
||||
let rect = this.getNodeRect(node)
|
||||
let { left, top, right, bottom } = rect
|
||||
let match = false
|
||||
if (dir === 'Left') {
|
||||
if (dir === CONSTANTS.KEY_DIR.LEFT) {
|
||||
match =
|
||||
left < currentActiveNodeRect.left &&
|
||||
top < currentActiveNodeRect.bottom &&
|
||||
bottom > currentActiveNodeRect.top
|
||||
} else if (dir === 'Right') {
|
||||
} else if (dir === CONSTANTS.KEY_DIR.RIGHT) {
|
||||
match =
|
||||
right > currentActiveNodeRect.right &&
|
||||
top < currentActiveNodeRect.bottom &&
|
||||
bottom > currentActiveNodeRect.top
|
||||
} else if (dir === 'Up') {
|
||||
} else if (dir === CONSTANTS.KEY_DIR.UP) {
|
||||
match =
|
||||
top < currentActiveNodeRect.top &&
|
||||
left < currentActiveNodeRect.right &&
|
||||
right > currentActiveNodeRect.left
|
||||
} else if (dir === 'Down') {
|
||||
} else if (dir === CONSTANTS.KEY_DIR.DOWN) {
|
||||
match =
|
||||
bottom > currentActiveNodeRect.bottom &&
|
||||
left < currentActiveNodeRect.right &&
|
||||
@@ -179,13 +186,13 @@ export default class KeyboardNavigation {
|
||||
let offsetY = ccY - cY
|
||||
if (offsetX === 0 && offsetY === 0) return
|
||||
let match = false
|
||||
if (dir === 'Left') {
|
||||
if (dir === CONSTANTS.KEY_DIR.LEFT) {
|
||||
match = offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY
|
||||
} else if (dir === 'Right') {
|
||||
} else if (dir === CONSTANTS.KEY_DIR.RIGHT) {
|
||||
match = offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY
|
||||
} else if (dir === 'Up') {
|
||||
} else if (dir === CONSTANTS.KEY_DIR.UP) {
|
||||
match = offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX
|
||||
} else if (dir === 'Down') {
|
||||
} else if (dir === CONSTANTS.KEY_DIR.DOWN) {
|
||||
match = offsetY > 0 && -offsetY < offsetX && offsetY > offsetX
|
||||
}
|
||||
if (match) {
|
||||
@@ -224,3 +231,7 @@ export default class KeyboardNavigation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KeyboardNavigation.instanceName = 'keyboardNavigation'
|
||||
|
||||
export default KeyboardNavigation
|
||||
@@ -14,43 +14,6 @@ class MiniMap {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取小地图相关数据
|
||||
getMiniMap() {
|
||||
const svg = this.mindMap.svg
|
||||
const draw = this.mindMap.draw
|
||||
// 保存原始信息
|
||||
const origWidth = svg.width()
|
||||
const origHeight = svg.height()
|
||||
const origTransform = draw.transform()
|
||||
const elRect = this.mindMap.el.getBoundingClientRect()
|
||||
// 去除放大缩小的变换效果
|
||||
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
|
||||
// 获取变换后的位置尺寸信息,其实是getBoundingClientRect方法的包装方法
|
||||
const rect = draw.rbox()
|
||||
// 将svg设置为实际内容的宽高
|
||||
svg.size(rect.width, rect.height)
|
||||
// 把实际内容变换
|
||||
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
|
||||
// 克隆一份数据
|
||||
const clone = svg.clone()
|
||||
// 恢复原先的大小和变换信息
|
||||
svg.size(origWidth, origHeight)
|
||||
draw.transform(origTransform)
|
||||
|
||||
return {
|
||||
svg: clone, // 思维导图图形的整体svg元素,包括:svg(画布容器)、g(实际的思维导图组)
|
||||
svgHTML: clone.svg(), // svg字符串
|
||||
rect: {
|
||||
...rect, // 思维导图图形未缩放时的位置尺寸等信息
|
||||
ratio: rect.width / rect.height // 思维导图图形的宽高比
|
||||
},
|
||||
origWidth, // 画布宽度
|
||||
origHeight, // 画布高度
|
||||
scaleX: origTransform.scaleX, // 思维导图图形的水平缩放值
|
||||
scaleY: origTransform.scaleY // 思维导图图形的垂直缩放值
|
||||
}
|
||||
}
|
||||
|
||||
// 计算小地图的渲染数据
|
||||
/**
|
||||
* boxWidth:小地图容器的宽度
|
||||
@@ -58,7 +21,7 @@ class MiniMap {
|
||||
*/
|
||||
calculationMiniMap(boxWidth, boxHeight) {
|
||||
let { svgHTML, rect, origWidth, origHeight, scaleX, scaleY } =
|
||||
this.getMiniMap()
|
||||
this.mindMap.getSvgData()
|
||||
// 计算数据
|
||||
let boxRatio = boxWidth / boxHeight
|
||||
let actWidth = 0
|
||||
@@ -144,4 +107,6 @@ class MiniMap {
|
||||
}
|
||||
}
|
||||
|
||||
MiniMap.instanceName = 'miniMap'
|
||||
|
||||
export default MiniMap
|
||||
|
||||
481
simple-mind-map/src/RichText.js
Normal file
@@ -0,0 +1,481 @@
|
||||
import Quill from 'quill'
|
||||
import 'quill/dist/quill.snow.css'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { walk, getTextFromHtml } from './utils'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
|
||||
let extended = false
|
||||
|
||||
// 扩展quill的字体列表
|
||||
let fontFamilyList = [
|
||||
'宋体, SimSun, Songti SC',
|
||||
'微软雅黑, Microsoft YaHei',
|
||||
'楷体, 楷体_GB2312, SimKai, STKaiti',
|
||||
'黑体, SimHei, Heiti SC',
|
||||
'隶书, SimLi',
|
||||
'andale mono',
|
||||
'arial, helvetica, sans-serif',
|
||||
'arial black, avant garde',
|
||||
'comic sans ms',
|
||||
'impact, chicago',
|
||||
'times new roman',
|
||||
'sans-serif',
|
||||
'serif'
|
||||
]
|
||||
|
||||
// 扩展quill的字号列表
|
||||
let fontSizeList = new Array(100).fill(0).map((_, index) => {
|
||||
return index + 'px'
|
||||
})
|
||||
|
||||
// 节点支持富文本编辑功能
|
||||
class RichText {
|
||||
constructor({ mindMap, pluginOpt }) {
|
||||
this.mindMap = mindMap
|
||||
this.pluginOpt = pluginOpt
|
||||
this.textEditNode = null
|
||||
this.showTextEdit = false
|
||||
this.quill = null
|
||||
this.range = null
|
||||
this.lastRange = null
|
||||
this.node = null
|
||||
this.styleEl = null
|
||||
this.cacheEditingText = ''
|
||||
this.initOpt()
|
||||
this.extendQuill()
|
||||
this.appendCss()
|
||||
|
||||
// 处理数据,转成富文本格式
|
||||
if (this.mindMap.opt.data) {
|
||||
this.mindMap.opt.data = this.handleSetData(this.mindMap.opt.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 插入样式
|
||||
appendCss() {
|
||||
let cssText = `
|
||||
.ql-editor {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.ql-container {
|
||||
height: auto;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.ql-container.ql-snow {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.smm-richtext-node-wrap p {
|
||||
font-family: auto;
|
||||
}
|
||||
|
||||
.smm-richtext-node-edit-wrap p {
|
||||
font-family: auto;
|
||||
}
|
||||
`
|
||||
this.styleEl = document.createElement('style')
|
||||
this.styleEl.type = 'text/css'
|
||||
this.styleEl.innerHTML = cssText
|
||||
document.head.appendChild(this.styleEl)
|
||||
}
|
||||
|
||||
// 处理选项参数
|
||||
initOpt() {
|
||||
if (
|
||||
this.pluginOpt.fontFamilyList &&
|
||||
Array.isArray(this.pluginOpt.fontFamilyList)
|
||||
) {
|
||||
fontFamilyList = this.pluginOpt.fontFamilyList
|
||||
}
|
||||
if (
|
||||
this.pluginOpt.fontSizeList &&
|
||||
Array.isArray(this.pluginOpt.fontSizeList)
|
||||
) {
|
||||
fontSizeList = this.pluginOpt.fontSizeList
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展quill编辑器
|
||||
extendQuill() {
|
||||
if (extended) {
|
||||
return
|
||||
}
|
||||
extended = true
|
||||
|
||||
// 扩展quill的字体列表
|
||||
const FontAttributor = Quill.import('attributors/class/font')
|
||||
FontAttributor.whitelist = fontFamilyList
|
||||
Quill.register(FontAttributor, true)
|
||||
|
||||
const FontStyle = Quill.import('attributors/style/font')
|
||||
FontStyle.whitelist = fontFamilyList
|
||||
Quill.register(FontStyle, true)
|
||||
|
||||
// 扩展quill的字号列表
|
||||
const SizeAttributor = Quill.import('attributors/class/size')
|
||||
SizeAttributor.whitelist = fontSizeList
|
||||
Quill.register(SizeAttributor, true)
|
||||
|
||||
const SizeStyle = Quill.import('attributors/style/size')
|
||||
SizeStyle.whitelist = fontSizeList
|
||||
Quill.register(SizeStyle, true)
|
||||
}
|
||||
|
||||
// 显示文本编辑控件
|
||||
showEditText(node, rect) {
|
||||
if (this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
this.node = node
|
||||
if (!rect) rect = node._textData.node.node.getBoundingClientRect()
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
this.mindMap.renderer.textEdit.registerTmpShortcut()
|
||||
// 原始宽高
|
||||
let g = node._textData.node
|
||||
let originWidth = g.attr('data-width')
|
||||
let originHeight = g.attr('data-height')
|
||||
// 缩放值
|
||||
let scaleX = rect.width / originWidth
|
||||
let scaleY = rect.height / originHeight
|
||||
// 内边距
|
||||
const paddingX = 6
|
||||
const paddingY = 4
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.classList.add('smm-richtext-node-edit-wrap')
|
||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;box-shadow: 0 0 20px rgba(0,0,0,.5);outline: none; word-break: break-all;padding: ${paddingY}px ${paddingX}px;`
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
}
|
||||
// 使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
|
||||
let bgColor = node.style.merge('fillColor')
|
||||
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
|
||||
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
|
||||
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
|
||||
this.textEditNode.style.backgroundColor = bgColor === 'transparent' ? '#fff' : bgColor
|
||||
this.textEditNode.style.minWidth = originWidth + paddingX * 2 + 'px'
|
||||
this.textEditNode.style.minHeight = originHeight + '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 + paddingX * 2 + 'px'
|
||||
this.textEditNode.style.transform = `scale(${scaleX}, ${scaleY})`
|
||||
this.textEditNode.style.transformOrigin = 'left top'
|
||||
if (!node.nodeData.data.richText) {
|
||||
// 还不是富文本的情况
|
||||
let text = node.nodeData.data.text.split(/\n/gim).join('<br>')
|
||||
let html = `<p>${text}</p>`
|
||||
this.textEditNode.innerHTML = this.cacheEditingText || html
|
||||
} else {
|
||||
this.textEditNode.innerHTML = this.cacheEditingText || node.nodeData.data.text
|
||||
}
|
||||
this.initQuillEditor()
|
||||
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
|
||||
this.showTextEdit = true
|
||||
this.focus()
|
||||
if (!node.nodeData.data.richText) {
|
||||
// 如果是非富文本的情况,需要手动应用文本样式
|
||||
this.setTextStyleIfNotRichText(node)
|
||||
}
|
||||
this.cacheEditingText = ''
|
||||
}
|
||||
|
||||
// 如果是非富文本的情况,需要手动应用文本样式
|
||||
setTextStyleIfNotRichText(node) {
|
||||
let style = {
|
||||
font: node.style.merge('fontFamily'),
|
||||
color: node.style.merge('color'),
|
||||
italic: node.style.merge('fontStyle') === 'italic',
|
||||
bold: node.style.merge('fontWeight') === 'bold',
|
||||
size: node.style.merge('fontSize') + 'px',
|
||||
underline: node.style.merge('textDecoration') === 'underline',
|
||||
strike: node.style.merge('textDecoration') === 'line-through'
|
||||
}
|
||||
this.formatAllText(style)
|
||||
}
|
||||
|
||||
// 获取当前正在编辑的内容
|
||||
getEditText() {
|
||||
let html = this.quill.container.firstChild.innerHTML
|
||||
// 去除最后的空行
|
||||
return html.replace(/<p><br><\/p>$/, '')
|
||||
}
|
||||
|
||||
// 隐藏文本编辑控件,即完成编辑
|
||||
hideEditText(nodes) {
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
let html = this.getEditText()
|
||||
let list = nodes && nodes.length > 0 ? nodes : this.mindMap.renderer.activeNodeList
|
||||
list.forEach(node => {
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', node, html, true)
|
||||
if (node.isGeneralization) {
|
||||
// 概要节点
|
||||
node.generalizationBelongNode.updateGeneralization()
|
||||
}
|
||||
this.mindMap.render()
|
||||
})
|
||||
this.mindMap.emit(
|
||||
'hide_text_edit',
|
||||
this.textEditNode,
|
||||
list
|
||||
)
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.showTextEdit = false
|
||||
this.mindMap.emit('rich_text_selection_change', false)
|
||||
this.node = null
|
||||
}
|
||||
|
||||
// 初始化Quill富文本编辑器
|
||||
initQuillEditor() {
|
||||
this.quill = new Quill(this.textEditNode, {
|
||||
modules: {
|
||||
toolbar: false,
|
||||
keyboard: {
|
||||
bindings: {
|
||||
enter: {
|
||||
key: 13,
|
||||
handler: function () {
|
||||
// 覆盖默认的回车键换行
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
theme: 'snow'
|
||||
})
|
||||
this.quill.on('selection-change', range => {
|
||||
this.lastRange = this.range
|
||||
this.range = null
|
||||
if (range) {
|
||||
let bounds = this.quill.getBounds(range.index, range.length)
|
||||
let rect = this.textEditNode.getBoundingClientRect()
|
||||
let rectInfo = {
|
||||
left: bounds.left + rect.left,
|
||||
top: bounds.top + rect.top,
|
||||
right: bounds.right + rect.left,
|
||||
bottom: bounds.bottom + rect.top,
|
||||
width: bounds.width
|
||||
}
|
||||
let formatInfo = this.quill.getFormat(range.index, range.length)
|
||||
let hasRange = false
|
||||
if (range.length == 0) {
|
||||
hasRange = false
|
||||
} else {
|
||||
this.range = range
|
||||
hasRange = true
|
||||
}
|
||||
this.mindMap.emit(
|
||||
'rich_text_selection_change',
|
||||
hasRange,
|
||||
rectInfo,
|
||||
formatInfo
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 选中全部
|
||||
selectAll() {
|
||||
this.quill.setSelection(0, this.quill.getLength())
|
||||
}
|
||||
|
||||
// 聚焦
|
||||
focus() {
|
||||
let len = this.quill.getLength()
|
||||
this.quill.setSelection(len, len)
|
||||
}
|
||||
|
||||
// 格式化当前选中的文本
|
||||
formatText(config = {}, clear = false) {
|
||||
if (!this.range && !this.lastRange) return
|
||||
this.syncFormatToNodeConfig(config, clear)
|
||||
let rangeLost = !this.range
|
||||
let range = rangeLost ? this.lastRange : this.range
|
||||
clear ? this.quill.removeFormat(range.index, range.length) : this.quill.formatText(range.index, range.length, config)
|
||||
if (rangeLost) {
|
||||
this.quill.setSelection(this.lastRange.index, this.lastRange.length)
|
||||
}
|
||||
}
|
||||
|
||||
// 清除当前选中文本的样式
|
||||
removeFormat() {
|
||||
this.formatText({}, true)
|
||||
}
|
||||
|
||||
// 格式化指定范围的文本
|
||||
formatRangeText(range, config = {}) {
|
||||
if (!range) return
|
||||
this.syncFormatToNodeConfig(config)
|
||||
this.quill.formatText(range.index, range.length, config)
|
||||
}
|
||||
|
||||
// 格式化所有文本
|
||||
formatAllText(config = {}) {
|
||||
this.syncFormatToNodeConfig(config)
|
||||
this.quill.formatText(0, this.quill.getLength(), config)
|
||||
}
|
||||
|
||||
// 同步格式化到节点样式配置
|
||||
syncFormatToNodeConfig(config, clear) {
|
||||
if (!this.node) return
|
||||
if (clear) {
|
||||
// 清除文本样式
|
||||
['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'textDecoration', 'color'].forEach((prop) => {
|
||||
delete this.node.nodeData.data[prop]
|
||||
})
|
||||
} else {
|
||||
let data = this.richTextStyleToNormalStyle(config)
|
||||
this.mindMap.renderer.setNodeData(this.node, data)
|
||||
}
|
||||
}
|
||||
|
||||
// 将普通节点样式对象转换成富文本样式对象
|
||||
normalStyleToRichTextStyle(style) {
|
||||
let config = {}
|
||||
Object.keys(style).forEach(prop => {
|
||||
let value = style[prop]
|
||||
switch (prop) {
|
||||
case 'fontFamily':
|
||||
config.font = value
|
||||
break
|
||||
case 'fontSize':
|
||||
config.size = value + 'px'
|
||||
break
|
||||
case 'fontWeight':
|
||||
config.bold = value === 'bold'
|
||||
break
|
||||
case 'fontStyle':
|
||||
config.italic = value === 'italic'
|
||||
break
|
||||
case 'textDecoration':
|
||||
config.underline = value === 'underline'
|
||||
config.strike = value === 'line-through'
|
||||
break
|
||||
case 'color':
|
||||
config.color = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
return config
|
||||
}
|
||||
|
||||
// 将富文本样式对象转换成普通节点样式对象
|
||||
richTextStyleToNormalStyle(config) {
|
||||
let data = {}
|
||||
Object.keys(config).forEach(prop => {
|
||||
let value = config[prop]
|
||||
switch (prop) {
|
||||
case 'font':
|
||||
data.fontFamily = value
|
||||
break
|
||||
case 'size':
|
||||
data.fontSize = parseFloat(value)
|
||||
break
|
||||
case 'bold':
|
||||
data.fontWeight = value ? 'bold' : 'normal'
|
||||
break
|
||||
case 'italic':
|
||||
data.fontStyle = value ? 'italic' : 'normal'
|
||||
break
|
||||
case 'underline':
|
||||
data.textDecoration = value ? 'underline' : 'none'
|
||||
break
|
||||
case 'strike':
|
||||
data.textDecoration = value ? 'line-through' : 'none'
|
||||
break
|
||||
case 'color':
|
||||
data.color = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
// 处理导出为图片
|
||||
async handleExportPng(node) {
|
||||
let el = document.createElement('div')
|
||||
el.style.position = 'absolute'
|
||||
el.style.left = '-9999999px'
|
||||
el.appendChild(node)
|
||||
this.mindMap.el.appendChild(el)
|
||||
// 遍历所有节点,将它们的margin和padding设为0
|
||||
let walk = (root) => {
|
||||
root.style.margin = 0
|
||||
root.style.padding = 0
|
||||
if (root.hasChildNodes()) {
|
||||
Array.from(root.children).forEach((item) => {
|
||||
walk(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
walk(node)
|
||||
let canvas = await html2canvas(el, {
|
||||
backgroundColor: null
|
||||
})
|
||||
this.mindMap.el.removeChild(el)
|
||||
return canvas.toDataURL()
|
||||
}
|
||||
|
||||
// 将所有节点转换成非富文本节点
|
||||
transformAllNodesToNormalNode() {
|
||||
walk(
|
||||
this.mindMap.renderer.renderTree,
|
||||
null,
|
||||
node => {
|
||||
if (node.data.richText) {
|
||||
node.data.richText = false
|
||||
node.data.text = getTextFromHtml(node.data.text)
|
||||
// delete node.data.uid
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0,
|
||||
0
|
||||
)
|
||||
// 清空历史数据,并且触发数据变化
|
||||
this.mindMap.command.clearHistory()
|
||||
this.mindMap.command.addHistory()
|
||||
this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE)
|
||||
}
|
||||
|
||||
// 处理导入数据
|
||||
handleSetData(data) {
|
||||
let walk = (root) => {
|
||||
if (!root.data.richText) {
|
||||
root.data.richText = true
|
||||
root.data.resetRichText = true
|
||||
}
|
||||
if (root.children && root.children.length > 0) {
|
||||
Array.from(root.children).forEach((item) => {
|
||||
walk(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
walk(data)
|
||||
return data
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.transformAllNodesToNormalNode()
|
||||
document.head.removeChild(this.styleEl)
|
||||
}
|
||||
}
|
||||
|
||||
RichText.instanceName = 'richText'
|
||||
|
||||
export default RichText
|
||||
@@ -17,7 +17,7 @@ class Select {
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
this.checkInNodes = throttle(this.checkInNodes, 500, this)
|
||||
this.checkInNodes = throttle(this.checkInNodes, 300, this)
|
||||
this.mindMap.on('mousedown', e => {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
@@ -146,25 +146,31 @@ class Select {
|
||||
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 ((left >= minx && left <= maxx ||
|
||||
right >= minx && right <= maxx) &&
|
||||
(top >= miny && top <= maxy ||
|
||||
bottom >= 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, () => {
|
||||
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
|
||||
if (!node.nodeData.data.isActive) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.setNodeActive(node, false)
|
||||
this.mindMap.renderer.removeActiveNode(node)
|
||||
})
|
||||
// })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Select.instanceName = 'select'
|
||||
|
||||
export default Select
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Rect, Polygon, Path } from '@svgdotjs/svg.js'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
|
||||
// 节点形状类
|
||||
export default class Shape {
|
||||
constructor(node) {
|
||||
@@ -13,37 +16,37 @@ export default class Shape {
|
||||
const actHeight = height + paddingY * 2
|
||||
const actOffset = Math.abs(actWidth - actHeight)
|
||||
switch (shape) {
|
||||
case 'roundedRectangle':
|
||||
case CONSTANTS.SHAPE.ROUNDED_RECTANGLE:
|
||||
return {
|
||||
paddingX: height > width ? (height - width) / 2 : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case 'diamond':
|
||||
case CONSTANTS.SHAPE.DIAMOND:
|
||||
return {
|
||||
paddingX: width / 2,
|
||||
paddingY: height / 2
|
||||
}
|
||||
case 'parallelogram':
|
||||
case CONSTANTS.SHAPE.PARALLELOGRAM:
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case 'outerTriangularRectangle':
|
||||
case CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE:
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case 'innerTriangularRectangle':
|
||||
case CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE:
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: 0
|
||||
}
|
||||
case 'ellipse':
|
||||
case CONSTANTS.SHAPE.ELLIPSE:
|
||||
return {
|
||||
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
|
||||
paddingY: paddingY <= 0 ? defaultPaddingY : 0
|
||||
}
|
||||
case 'circle':
|
||||
case CONSTANTS.SHAPE.CIRCLE:
|
||||
return {
|
||||
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
|
||||
paddingY: actHeight < actWidth ? actOffset / 2 : 0
|
||||
@@ -62,30 +65,30 @@ export default class Shape {
|
||||
let { width, height } = this.node
|
||||
let node = null
|
||||
// 矩形
|
||||
if (shape === 'rectangle') {
|
||||
node = this.node.group.rect(width, height)
|
||||
} else if (shape === 'diamond') {
|
||||
if (shape === CONSTANTS.SHAPE.RECTANGLE) {
|
||||
node = new Rect().size(width, height)
|
||||
} else if (shape === CONSTANTS.SHAPE.DIAMOND) {
|
||||
// 菱形
|
||||
node = this.createDiamond()
|
||||
} else if (shape === 'parallelogram') {
|
||||
} else if (shape === CONSTANTS.SHAPE.PARALLELOGRAM) {
|
||||
// 平行四边形
|
||||
node = this.createParallelogram()
|
||||
} else if (shape === 'roundedRectangle') {
|
||||
} else if (shape === CONSTANTS.SHAPE.ROUNDED_RECTANGLE) {
|
||||
// 圆角矩形
|
||||
node = this.createRoundedRectangle()
|
||||
} else if (shape === 'octagonalRectangle') {
|
||||
} else if (shape === CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE) {
|
||||
// 八角矩形
|
||||
node = this.createOctagonalRectangle()
|
||||
} else if (shape === 'outerTriangularRectangle') {
|
||||
} else if (shape === CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE) {
|
||||
// 外三角矩形
|
||||
node = this.createOuterTriangularRectangle()
|
||||
} else if (shape === 'innerTriangularRectangle') {
|
||||
} else if (shape === CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE) {
|
||||
// 内三角矩形
|
||||
node = this.createInnerTriangularRectangle()
|
||||
} else if (shape === 'ellipse') {
|
||||
} else if (shape === CONSTANTS.SHAPE.ELLIPSE) {
|
||||
// 椭圆
|
||||
node = this.createEllipse()
|
||||
} else if (shape === 'circle') {
|
||||
} else if (shape === CONSTANTS.SHAPE.CIRCLE) {
|
||||
// 圆
|
||||
node = this.createCircle()
|
||||
}
|
||||
@@ -105,12 +108,12 @@ export default class Shape {
|
||||
let bottomY = height
|
||||
let leftX = 0
|
||||
let leftY = halfHeight
|
||||
return this.node.group.polygon(`
|
||||
${topX}, ${topY}
|
||||
${rightX}, ${rightY}
|
||||
${bottomX}, ${bottomY}
|
||||
${leftX}, ${leftY}
|
||||
`)
|
||||
return new Polygon().plot([
|
||||
[topX, topY],
|
||||
[rightX, rightY],
|
||||
[bottomX, bottomY],
|
||||
[leftX, leftY]
|
||||
])
|
||||
}
|
||||
|
||||
// 创建平行四边形
|
||||
@@ -118,41 +121,41 @@ export default class Shape {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.node
|
||||
return this.node.group.polygon(`
|
||||
${paddingX}, ${0}
|
||||
${width}, ${0}
|
||||
${width - paddingX}, ${height}
|
||||
${0}, ${height}
|
||||
`)
|
||||
return new Polygon().plot([
|
||||
[paddingX, 0],
|
||||
[width, 0],
|
||||
[width - paddingX, height],
|
||||
[0, height]
|
||||
])
|
||||
}
|
||||
|
||||
// 创建圆角矩形
|
||||
createRoundedRectangle() {
|
||||
let { width, height } = this.node
|
||||
let halfHeight = height / 2
|
||||
return this.node.group.path(`
|
||||
M${halfHeight},0
|
||||
L${width - halfHeight},0
|
||||
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
|
||||
L${halfHeight},${height}
|
||||
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
|
||||
`)
|
||||
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 this.node.group.polygon(`
|
||||
${0}, ${w}
|
||||
${w}, ${0}
|
||||
${width - w}, ${0}
|
||||
${width}, ${w}
|
||||
${width}, ${height - w}
|
||||
${width - w}, ${height}
|
||||
${w}, ${height}
|
||||
${0}, ${height - w}
|
||||
`)
|
||||
return new Polygon().plot([
|
||||
[0, w],
|
||||
[w, 0],
|
||||
[width - w, 0],
|
||||
[width, w],
|
||||
[width, height - w],
|
||||
[width - w, height],
|
||||
[w, height],
|
||||
[0, height - w]
|
||||
])
|
||||
}
|
||||
|
||||
// 创建外三角矩形
|
||||
@@ -160,14 +163,14 @@ export default class Shape {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.node
|
||||
return this.node.group.polygon(`
|
||||
${paddingX}, ${0}
|
||||
${width - paddingX}, ${0}
|
||||
${width}, ${height / 2}
|
||||
${width - paddingX}, ${height}
|
||||
${paddingX}, ${height}
|
||||
${0}, ${height / 2}
|
||||
`)
|
||||
return new Polygon().plot([
|
||||
[paddingX, 0],
|
||||
[width - paddingX, 0],
|
||||
[width, height / 2],
|
||||
[width - paddingX, height],
|
||||
[paddingX, height],
|
||||
[0, height / 2]
|
||||
])
|
||||
}
|
||||
|
||||
// 创建内三角矩形
|
||||
@@ -175,14 +178,14 @@ export default class Shape {
|
||||
let { paddingX } = this.node.getPaddingVale()
|
||||
paddingX = paddingX || this.node.shapePadding.paddingX
|
||||
let { width, height } = this.node
|
||||
return this.node.group.polygon(`
|
||||
${0}, ${0}
|
||||
${width}, ${0}
|
||||
${width - paddingX / 2}, ${height / 2}
|
||||
${width}, ${height}
|
||||
${0}, ${height}
|
||||
${paddingX / 2}, ${height / 2}
|
||||
`)
|
||||
return new Polygon().plot([
|
||||
[0, 0],
|
||||
[width, 0],
|
||||
[width - paddingX / 2, height / 2],
|
||||
[width, height],
|
||||
[0, height],
|
||||
[paddingX / 2, height / 2]
|
||||
])
|
||||
}
|
||||
|
||||
// 创建椭圆
|
||||
@@ -190,12 +193,12 @@ export default class Shape {
|
||||
let { width, height } = this.node
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
return this.node.group.path(`
|
||||
M${halfWidth},0
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
|
||||
M${halfWidth},${height}
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
|
||||
`)
|
||||
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}
|
||||
`)
|
||||
}
|
||||
|
||||
// 创建圆
|
||||
@@ -203,24 +206,24 @@ export default class Shape {
|
||||
let { width, height } = this.node
|
||||
let halfWidth = width / 2
|
||||
let halfHeight = height / 2
|
||||
return this.node.group.path(`
|
||||
M${halfWidth},0
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
|
||||
M${halfWidth},${height}
|
||||
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
|
||||
`)
|
||||
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 = [
|
||||
'rectangle',
|
||||
'diamond',
|
||||
'parallelogram',
|
||||
'roundedRectangle',
|
||||
'octagonalRectangle',
|
||||
'outerTriangularRectangle',
|
||||
'innerTriangularRectangle',
|
||||
'ellipse',
|
||||
'circle'
|
||||
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
|
||||
]
|
||||
|
||||
@@ -1,167 +1,195 @@
|
||||
import { tagColorList } from './utils/constant'
|
||||
const rootProp = ['paddingX', 'paddingY']
|
||||
|
||||
// 样式类
|
||||
|
||||
class Style {
|
||||
// 设置背景样式
|
||||
|
||||
static setBackgroundStyle(el, themeConfig) {
|
||||
let { backgroundColor, backgroundImage, backgroundRepeat } = themeConfig
|
||||
el.style.backgroundColor = backgroundColor
|
||||
if (backgroundImage) {
|
||||
el.style.backgroundImage = `url(${backgroundImage})`
|
||||
el.style.backgroundRepeat = backgroundRepeat
|
||||
}
|
||||
}
|
||||
|
||||
// 构造函数
|
||||
|
||||
constructor(ctx, themeConfig) {
|
||||
this.ctx = ctx
|
||||
this.themeConfig = themeConfig
|
||||
}
|
||||
|
||||
// 更新主题配置
|
||||
|
||||
updateThemeConfig(themeConfig) {
|
||||
this.themeConfig = themeConfig
|
||||
}
|
||||
|
||||
// 合并样式
|
||||
|
||||
merge(prop, root, isActive) {
|
||||
// 三级及以下节点
|
||||
let defaultConfig = this.themeConfig.node
|
||||
if (root || rootProp.includes(prop)) {
|
||||
// 直接使用最外层样式
|
||||
defaultConfig = this.themeConfig
|
||||
} else if (this.ctx.isGeneralization) {
|
||||
// 概要节点
|
||||
defaultConfig = this.themeConfig.generalization
|
||||
} else if (this.ctx.layerIndex === 0) {
|
||||
// 根节点
|
||||
defaultConfig = this.themeConfig.root
|
||||
} else if (this.ctx.layerIndex === 1) {
|
||||
// 二级节点
|
||||
defaultConfig = this.themeConfig.second
|
||||
}
|
||||
// 激活状态
|
||||
if (isActive !== undefined ? isActive : this.ctx.nodeData.data.isActive) {
|
||||
if (
|
||||
this.ctx.nodeData.data.activeStyle &&
|
||||
this.ctx.nodeData.data.activeStyle[prop] !== undefined
|
||||
) {
|
||||
return this.ctx.nodeData.data.activeStyle[prop]
|
||||
} else if (defaultConfig.active && defaultConfig.active[prop]) {
|
||||
return defaultConfig.active[prop]
|
||||
}
|
||||
}
|
||||
// 优先使用节点本身的样式
|
||||
return this.getSelfStyle(prop) !== undefined
|
||||
? this.getSelfStyle(prop)
|
||||
: defaultConfig[prop]
|
||||
}
|
||||
|
||||
// 获取某个样式值
|
||||
|
||||
getStyle(prop, root, isActive) {
|
||||
return this.merge(prop, root, isActive)
|
||||
}
|
||||
|
||||
// 获取自身自定义样式
|
||||
|
||||
getSelfStyle(prop) {
|
||||
return this.ctx.nodeData.data[prop]
|
||||
}
|
||||
|
||||
// 矩形
|
||||
|
||||
rect(node) {
|
||||
this.shape(node)
|
||||
node.radius(this.merge('borderRadius'))
|
||||
}
|
||||
|
||||
// 矩形外的其他形状
|
||||
|
||||
shape(node) {
|
||||
node.fill({
|
||||
color: this.merge('fillColor')
|
||||
})
|
||||
// 节点使用横线样式,不需要渲染非激活状态的边框样式
|
||||
if (
|
||||
!this.ctx.isRoot &&
|
||||
!this.ctx.isGeneralization &&
|
||||
this.themeConfig.nodeUseLineStyle &&
|
||||
!this.ctx.nodeData.data.isActive
|
||||
) {
|
||||
return
|
||||
}
|
||||
node.stroke({
|
||||
color: this.merge('borderColor'),
|
||||
width: this.merge('borderWidth'),
|
||||
dasharray: this.merge('borderDasharray')
|
||||
})
|
||||
}
|
||||
|
||||
// 文字
|
||||
|
||||
text(node) {
|
||||
node
|
||||
.fill({
|
||||
color: this.merge('color')
|
||||
})
|
||||
.css({
|
||||
'font-family': this.merge('fontFamily'),
|
||||
'font-size': this.merge('fontSize'),
|
||||
'font-weight': this.merge('fontWeight'),
|
||||
'font-style': this.merge('fontStyle'),
|
||||
'text-decoration': this.merge('textDecoration')
|
||||
})
|
||||
}
|
||||
|
||||
// html文字节点
|
||||
|
||||
domText(node, fontSizeScale = 1) {
|
||||
node.style.fontFamily = this.merge('fontFamily')
|
||||
node.style.fontSize = this.merge('fontSize') * fontSizeScale + 'px'
|
||||
node.style.fontWeight = this.merge('fontWeight') || 'normal'
|
||||
}
|
||||
|
||||
// 标签文字
|
||||
|
||||
tagText(node, index) {
|
||||
node
|
||||
.fill({
|
||||
color: tagColorList[index].color
|
||||
})
|
||||
.css({
|
||||
'font-size': '12px'
|
||||
})
|
||||
}
|
||||
|
||||
// 标签矩形
|
||||
|
||||
tagRect(node, index) {
|
||||
node.fill({
|
||||
color: tagColorList[index].background
|
||||
})
|
||||
}
|
||||
|
||||
// 内置图标
|
||||
|
||||
iconNode(node) {
|
||||
node.attr({
|
||||
fill: this.merge('color')
|
||||
})
|
||||
}
|
||||
|
||||
// 连线
|
||||
|
||||
line(node, { width, color, dasharray } = {}) {
|
||||
node.stroke({ width, color, dasharray }).fill({ color: 'none' })
|
||||
}
|
||||
|
||||
// 概要连线
|
||||
|
||||
generalizationLine(node) {
|
||||
import { tagColorList } from './utils/constant'
|
||||
const rootProp = ['paddingX', 'paddingY']
|
||||
|
||||
// 样式类
|
||||
class Style {
|
||||
// 设置背景样式
|
||||
static setBackgroundStyle(el, themeConfig) {
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
// 构造函数
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
export default Style
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getStrWithBrFromHtml } from './utils'
|
||||
import { getStrWithBrFromHtml, checkNodeOuter } from './utils'
|
||||
|
||||
// 节点文字编辑类
|
||||
export default class TextEdit {
|
||||
@@ -6,16 +6,21 @@ 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)
|
||||
// 点击事件
|
||||
@@ -23,6 +28,16 @@ export default class TextEdit {
|
||||
// 隐藏文本编辑框
|
||||
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()
|
||||
@@ -38,6 +53,7 @@ export default class TextEdit {
|
||||
}
|
||||
this.show(this.renderer.activeNodeList[0])
|
||||
})
|
||||
this.mindMap.on('scale', this.onScale)
|
||||
}
|
||||
|
||||
// 注册临时快捷键
|
||||
@@ -50,7 +66,28 @@ export default class TextEdit {
|
||||
|
||||
// 显示文本编辑框
|
||||
show(node) {
|
||||
this.showEditTextBox(node, node._textData.node.node.getBoundingClientRect())
|
||||
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)
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
@@ -59,25 +96,39 @@ export default class TextEdit {
|
||||
this.registerTmpShortcut()
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none;`
|
||||
this.textEditNode.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)
|
||||
}
|
||||
node.style.domText(this.textEditNode, this.mindMap.view.scale)
|
||||
this.textEditNode.innerHTML = node.nodeData.data.text
|
||||
.split(/\n/gim)
|
||||
.join('<br>')
|
||||
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
|
||||
// 选中文本
|
||||
this.selectNodeText()
|
||||
if (!this.cacheEditingText) {
|
||||
this.selectNodeText()
|
||||
}
|
||||
this.cacheEditingText = ''
|
||||
}
|
||||
|
||||
// 选中文本
|
||||
@@ -89,13 +140,22 @@ export default class TextEdit {
|
||||
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 = getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||
let str = this.getEditText()
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', node, str)
|
||||
if (node.isGeneralization) {
|
||||
// 概要节点
|
||||
@@ -113,6 +173,7 @@ export default class TextEdit {
|
||||
this.textEditNode.style.fontFamily = 'inherit'
|
||||
this.textEditNode.style.fontSize = 'inherit'
|
||||
this.textEditNode.style.fontWeight = 'normal'
|
||||
this.textEditNode.style.transform = 'translateY(0)'
|
||||
this.showTextEdit = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
|
||||
// 视图操作类
|
||||
class View {
|
||||
// 构造函数
|
||||
@@ -55,12 +57,41 @@ class View {
|
||||
})
|
||||
// 放大缩小视图
|
||||
this.mindMap.event.on('mousewheel', (e, dir) => {
|
||||
// // 放大
|
||||
if (dir === 'down') {
|
||||
this.enlarge()
|
||||
if (this.mindMap.opt.customHandleMousewheel && typeof this.mindMap.opt.customHandleMousewheel === 'function') {
|
||||
return this.mindMap.opt.customHandleMousewheel(e)
|
||||
}
|
||||
if (this.mindMap.opt.mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
|
||||
switch (dir) {
|
||||
// 鼠标滚轮,向上和向左,都是缩小
|
||||
case CONSTANTS.DIR.UP:
|
||||
case CONSTANTS.DIR.LEFT:
|
||||
this.narrow()
|
||||
break
|
||||
// 鼠标滚轮,向下和向右,都是放大
|
||||
case CONSTANTS.DIR.DOWN:
|
||||
case CONSTANTS.DIR.RIGHT:
|
||||
this.enlarge()
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// 缩小
|
||||
this.narrow()
|
||||
switch (dir){
|
||||
// 上移
|
||||
case CONSTANTS.DIR.DOWN:
|
||||
this.translateY(-this.mindMap.opt.mousewheelMoveStep)
|
||||
break
|
||||
// 下移
|
||||
case CONSTANTS.DIR.UP:
|
||||
this.translateY(this.mindMap.opt.mousewheelMoveStep)
|
||||
break
|
||||
// 右移
|
||||
case CONSTANTS.DIR.LEFT:
|
||||
this.translateX(-this.mindMap.opt.mousewheelMoveStep)
|
||||
break
|
||||
// 左移
|
||||
case CONSTANTS.DIR.RIGHT:
|
||||
this.translateX(this.mindMap.opt.mousewheelMoveStep)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -93,6 +124,13 @@ class View {
|
||||
}
|
||||
}
|
||||
|
||||
// 平移x,y方向
|
||||
translateXY(x, y) {
|
||||
this.x += x
|
||||
this.y += y
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移x方向
|
||||
translateX(step) {
|
||||
this.x += step
|
||||
@@ -129,10 +167,14 @@ class View {
|
||||
|
||||
// 恢复
|
||||
reset() {
|
||||
let scaleChange = this.scale !== 1
|
||||
this.scale = 1
|
||||
this.x = 0
|
||||
this.y = 0
|
||||
this.transform()
|
||||
if (scaleChange) {
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
}
|
||||
|
||||
// 缩小
|
||||
|
||||
@@ -3,7 +3,7 @@ import { degToRad, camelCaseToHyphen } from './utils'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 水印类
|
||||
export default class Watermark {
|
||||
class Watermark {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
this.lineSpacing = 0 // 水印行间距
|
||||
@@ -20,6 +20,11 @@ export default class Watermark {
|
||||
this.updateWatermark(this.mindMap.opt.watermarkConfig || {})
|
||||
}
|
||||
|
||||
// 获取是否存在水印
|
||||
hasWatermark() {
|
||||
return !!this.text.trim()
|
||||
}
|
||||
|
||||
// 处理水印配置
|
||||
handleConfig({ text, lineSpacing, textSpacing, angle, textStyle }) {
|
||||
this.text = text === undefined ? '' : String(text).trim()
|
||||
@@ -36,7 +41,7 @@ export default class Watermark {
|
||||
// 非精确绘制,会绘制一些超出可视区域的水印
|
||||
draw() {
|
||||
this.watermarkDraw.clear()
|
||||
if (!this.text.trim()) {
|
||||
if (!this.hasWatermark()) {
|
||||
return
|
||||
}
|
||||
let x = 0
|
||||
@@ -109,3 +114,7 @@ export default class Watermark {
|
||||
this.draw()
|
||||
}
|
||||
}
|
||||
|
||||
Watermark.instanceName = 'watermark'
|
||||
|
||||
export default Watermark
|
||||
@@ -1,4 +1,5 @@
|
||||
import Node from '../Node'
|
||||
import { CONSTANTS, initRootNodePositionMap } from '../utils/constant'
|
||||
|
||||
// 布局基类
|
||||
class Base {
|
||||
@@ -12,6 +13,8 @@ class Base {
|
||||
this.draw = this.mindMap.draw
|
||||
// 根节点
|
||||
this.root = null
|
||||
// 保存所有uid和节点,用于复用
|
||||
this.nodePool = {}
|
||||
}
|
||||
|
||||
// 计算节点位置
|
||||
@@ -32,26 +35,75 @@ class Base {
|
||||
// 概要节点
|
||||
renderGeneralization() {}
|
||||
|
||||
// 通过uid缓存节点
|
||||
cacheNode(uid, node) {
|
||||
// 记录本次渲染时的节点
|
||||
this.renderer.nodeCache[uid] = node
|
||||
// 记录所有渲染时的节点
|
||||
this.nodePool[uid] = node
|
||||
// 如果总缓存数量达到1000,直接清空
|
||||
if (Object.keys(this.nodePool).length > 1000) {
|
||||
this.clearNodePool()
|
||||
}
|
||||
}
|
||||
|
||||
// 清空节点存储池
|
||||
clearNodePool() {
|
||||
this.nodePool = {}
|
||||
}
|
||||
|
||||
// 检查当前来源是否需要重新计算节点大小
|
||||
checkIsNeedResizeSources() {
|
||||
return [CONSTANTS.CHANGE_THEME, CONSTANTS.TRANSFORM_TO_NORMAL_NODE].includes(this.renderer.renderSource)
|
||||
}
|
||||
|
||||
// 创建节点实例
|
||||
createNode(data, parent, isRoot, layerIndex) {
|
||||
// 创建节点
|
||||
let newNode = null
|
||||
// 复用节点
|
||||
// 数据上保存了节点引用,那么直接复用节点
|
||||
if (data && data._node && !this.renderer.reRender) {
|
||||
newNode = data._node
|
||||
newNode.reset()
|
||||
newNode.layerIndex = layerIndex
|
||||
this.cacheNode(data._node.uid, newNode)
|
||||
// 主题或主题配置改变了需要重新计算节点大小和布局
|
||||
if (this.checkIsNeedResizeSources()) {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
}
|
||||
} else if (this.nodePool[data.data.uid] && !this.renderer.reRender) {
|
||||
// 数据上没有保存节点引用,但是通过uid找到了缓存的节点,也可以复用
|
||||
newNode = this.nodePool[data.data.uid]
|
||||
// 保存该节点上一次的数据
|
||||
let lastData = JSON.stringify(newNode.nodeData.data)
|
||||
newNode.reset()
|
||||
newNode.nodeData = newNode.handleData(data || {})
|
||||
newNode.layerIndex = layerIndex
|
||||
this.cacheNode(data.data.uid, newNode)
|
||||
data._node = newNode
|
||||
// 主题或主题配置改变了需要重新计算节点大小和布局
|
||||
let isResizeSource = this.checkIsNeedResizeSources()
|
||||
// 节点数据改变了需要重新计算节点大小和布局
|
||||
let isNodeDataChange = lastData !== JSON.stringify(data.data)
|
||||
if (isResizeSource || isNodeDataChange) {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
}
|
||||
} else {
|
||||
// 创建新节点
|
||||
let uid = this.mindMap.uid++
|
||||
newNode = new Node({
|
||||
data,
|
||||
uid: this.mindMap.uid++,
|
||||
uid,
|
||||
renderer: this.renderer,
|
||||
mindMap: this.mindMap,
|
||||
draw: this.draw,
|
||||
layerIndex
|
||||
})
|
||||
newNode.getSize()
|
||||
// uid保存到数据上,为了节点复用
|
||||
data.data.uid = uid
|
||||
this.cacheNode(uid, newNode)
|
||||
// 数据关联实际节点
|
||||
data._node = newNode
|
||||
if (data.data.isActive) {
|
||||
@@ -70,10 +122,28 @@ class Base {
|
||||
return newNode
|
||||
}
|
||||
|
||||
// 格式化节点位置
|
||||
formatPosition(value, size, nodeSize) {
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
} else if (initRootNodePositionMap[value] !== undefined) {
|
||||
return size * initRootNodePositionMap[value]
|
||||
} else if (/^\d\d*%$/.test(value)) {
|
||||
return Number.parseFloat(value) / 100 * size
|
||||
} else {
|
||||
return (size - nodeSize) / 2
|
||||
}
|
||||
}
|
||||
|
||||
// 定位节点到画布中间
|
||||
setNodeCenter(node) {
|
||||
node.left = (this.mindMap.width - node.width) / 2
|
||||
node.top = (this.mindMap.height - node.height) / 2
|
||||
let { initRootNodePosition } = this.mindMap.opt
|
||||
let { CENTER }= CONSTANTS.INIT_ROOT_NODE_POSITION
|
||||
if (!initRootNodePosition || !Array.isArray(initRootNodePosition) || initRootNodePosition.length < 2) {
|
||||
initRootNodePosition = [CENTER, CENTER]
|
||||
}
|
||||
node.left = this.formatPosition(initRootNodePosition[0], this.mindMap.width, node.width)
|
||||
node.top = this.formatPosition(initRootNodePosition[1], this.mindMap.height, node.height)
|
||||
}
|
||||
|
||||
// 更新子节点属性
|
||||
@@ -87,6 +157,37 @@ class Base {
|
||||
})
|
||||
}
|
||||
|
||||
// 更新子节点多个属性
|
||||
updateChildrenPro(children, props) {
|
||||
children.forEach(item => {
|
||||
Object.keys(props).forEach((prop) => {
|
||||
item[prop] += props[prop]
|
||||
})
|
||||
if (item.children && item.children.length && !item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
this.updateChildrenPro(item.children, props)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaWidth(node) {
|
||||
let widthArr = []
|
||||
let loop = (node, width) => {
|
||||
if (node.children.length) {
|
||||
width += node.width / 2
|
||||
node.children.forEach(item => {
|
||||
loop(item, width)
|
||||
})
|
||||
} else {
|
||||
width += node.width
|
||||
widthArr.push(width)
|
||||
}
|
||||
}
|
||||
loop(node, 0)
|
||||
return Math.max(...widthArr)
|
||||
}
|
||||
|
||||
// 二次贝塞尔曲线
|
||||
quadraticCurvePath(x1, y1, x2, y2) {
|
||||
let cx = x1 + (x2 - x1) * 0.2
|
||||
@@ -196,6 +297,11 @@ class Base {
|
||||
generalizationNodeMargin
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点实际存在几个子节点
|
||||
getNodeActChildrenLength(node) {
|
||||
return node.nodeData.children && node.nodeData.children.length
|
||||
}
|
||||
}
|
||||
|
||||
export default Base
|
||||
|
||||
@@ -1,386 +1,354 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
|
||||
// 目录组织图
|
||||
|
||||
class CatalogOrganization extends Base {
|
||||
// 构造函数
|
||||
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据计算节点的left、width、height
|
||||
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(cur, parent, isRoot, layerIndex) => {
|
||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
if (parent._node.isRoot) {
|
||||
newNode.top =
|
||||
parent._node.top +
|
||||
parent._node.height +
|
||||
this.getMarginX(layerIndex)
|
||||
}
|
||||
}
|
||||
if (!cur.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
(cur, parent, isRoot, layerIndex) => {
|
||||
if (isRoot) {
|
||||
let len = cur.data.expand === false ? 0 : cur._node.children.length
|
||||
cur._node.childrenAreaWidth = len
|
||||
? cur._node.children.reduce((h, item) => {
|
||||
return h + item.width
|
||||
}, 0) +
|
||||
(len + 1) * this.getMarginX(layerIndex + 1)
|
||||
: 0
|
||||
}
|
||||
},
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
let marginX = this.getMarginX(layerIndex + 1)
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
if (isRoot) {
|
||||
let left = node.left + node.width / 2 - node.childrenAreaWidth / 2
|
||||
let totalLeft = left + marginX
|
||||
node.children.forEach(cur => {
|
||||
cur.left = totalLeft
|
||||
totalLeft += cur.width + marginX
|
||||
})
|
||||
} else {
|
||||
let totalTop = node.top + node.height + marginY + node.expandBtnSize
|
||||
node.children.forEach(cur => {
|
||||
cur.left = node.left + node.width * 0.5
|
||||
cur.top = totalTop
|
||||
totalTop += cur.height + marginY + node.expandBtnSize
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
// 调整left
|
||||
if (parent && parent.isRoot) {
|
||||
let areaWidth = this.getNodeAreaWidth(node)
|
||||
let difference = areaWidth - node.width
|
||||
if (difference > 0) {
|
||||
this.updateBrothersLeft(node, difference / 2)
|
||||
}
|
||||
}
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
if (parent && !parent.isRoot && len > 0) {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
let totalHeight =
|
||||
node.children.reduce((h, item) => {
|
||||
return h + item.height
|
||||
}, 0) +
|
||||
(len + 1) * marginY +
|
||||
len * node.expandBtnSize
|
||||
this.updateBrothersTop(node, totalHeight)
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
|
||||
getNodeAreaWidth(node) {
|
||||
let widthArr = []
|
||||
let loop = (node, width) => {
|
||||
if (node.children.length) {
|
||||
width += node.width / 2
|
||||
node.children.forEach(item => {
|
||||
loop(item, width)
|
||||
})
|
||||
} else {
|
||||
width += node.width
|
||||
widthArr.push(width)
|
||||
}
|
||||
}
|
||||
loop(node, 0)
|
||||
return Math.max(...widthArr)
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
|
||||
updateBrothersLeft(node, addWidth) {
|
||||
if (node.parent) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
// 存在大于一个节点时,第一个或最后一个节点自身也需要移动,否则两边不对称
|
||||
if (
|
||||
(index === 0 || index === childrenList.length - 1) &&
|
||||
childrenList.length > 1
|
||||
) {
|
||||
let _offset = index === 0 ? -addWidth : addWidth
|
||||
node.left += _offset
|
||||
if (
|
||||
node.children &&
|
||||
node.children.length &&
|
||||
!node.hasCustomPosition()
|
||||
) {
|
||||
this.updateChildren(node.children, 'left', _offset)
|
||||
}
|
||||
}
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
if (_index < index) {
|
||||
// 左边的节点往左移
|
||||
_offset = -addWidth
|
||||
} else if (_index > index) {
|
||||
// 右边的节点往右移
|
||||
_offset = addWidth
|
||||
}
|
||||
item.left += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersLeft(node.parent, addWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
|
||||
renderLine(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
let len = node.children.length
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
if (node.isRoot) {
|
||||
// 根节点
|
||||
let x1 = left + width / 2
|
||||
let y1 = top + height
|
||||
let s1 = marginX * 0.7
|
||||
let minx = Infinity
|
||||
let maxx = -Infinity
|
||||
node.children.forEach((item, index) => {
|
||||
let x2 = item.left + item.width / 2
|
||||
let y2 = item.top
|
||||
if (x2 < minx) {
|
||||
minx = x2
|
||||
}
|
||||
if (x2 > maxx) {
|
||||
maxx = x2
|
||||
}
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? ` L ${item.left},${y2} L ${item.left + item.width},${y2}`
|
||||
: ''
|
||||
let path =
|
||||
`M ${x2},${y1 + s1} L ${x2},${y1 + s1 > y2 ? y2 + item.height : y2}` +
|
||||
nodeUseLineStylePath
|
||||
// 竖线
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
minx = Math.min(minx, x1)
|
||||
maxx = Math.max(maxx, x1)
|
||||
// 父节点的竖线
|
||||
let line1 = this.draw.path()
|
||||
node.style.line(line1)
|
||||
line1.plot(`M ${x1},${y1} L ${x1},${y1 + s1}`)
|
||||
node._lines.push(line1)
|
||||
style && style(line1, node)
|
||||
// 水平线
|
||||
if (len > 0) {
|
||||
let lin2 = this.draw.path()
|
||||
node.style.line(lin2)
|
||||
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
|
||||
node._lines.push(lin2)
|
||||
style && style(lin2, node)
|
||||
}
|
||||
} else {
|
||||
// 非根节点
|
||||
let y1 = top + height
|
||||
let maxy = -Infinity
|
||||
let x2 = node.left + node.width * 0.3
|
||||
node.children.forEach((item, index) => {
|
||||
// 为了适配自定义位置,下面做了各种位置的兼容
|
||||
let y2 = item.top + item.height / 2
|
||||
if (y2 > maxy) {
|
||||
maxy = y2
|
||||
}
|
||||
// 水平线
|
||||
let path = ''
|
||||
let _left = item.left
|
||||
let _isLeft = item.left + item.width < x2
|
||||
let _isXCenter = false
|
||||
if (_isLeft) {
|
||||
// 水平位置在父节点左边
|
||||
_left = item.left + item.width
|
||||
} else if (item.left < x2 && item.left + item.width > x2) {
|
||||
// 水平位置在父节点之间
|
||||
_isXCenter = true
|
||||
y2 = item.top
|
||||
maxy = y2
|
||||
}
|
||||
if (y2 > top && y2 < y1) {
|
||||
// 自定义位置的情况:垂直位置节点在父节点之间
|
||||
path = `M ${
|
||||
_isLeft ? node.left : node.left + node.width
|
||||
},${y2} L ${_left},${y2}`
|
||||
} else if (y2 < y1) {
|
||||
// 自定义位置的情况:垂直位置节点在父节点上面
|
||||
if (_isXCenter) {
|
||||
y2 = item.top + item.height
|
||||
_left = x2
|
||||
}
|
||||
path = `M ${x2},${top} L ${x2},${y2} L ${_left},${y2}`
|
||||
} else {
|
||||
if (_isXCenter) {
|
||||
_left = x2
|
||||
}
|
||||
path = `M ${x2},${y2} L ${_left},${y2}`
|
||||
}
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? ` L ${_left},${y2 - item.height / 2} L ${_left},${
|
||||
y2 + item.height / 2
|
||||
}`
|
||||
: ''
|
||||
path += nodeUseLineStylePath
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let lin2 = this.draw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
node.style.line(lin2)
|
||||
if (maxy < y1 + expandBtnSize) {
|
||||
lin2.hide()
|
||||
} else {
|
||||
lin2.plot(`M ${x2},${y1 + expandBtnSize} L ${x2},${maxy}`)
|
||||
lin2.show()
|
||||
}
|
||||
node._lines.push(lin2)
|
||||
style && style(lin2, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
|
||||
// 目录组织图
|
||||
class CatalogOrganization extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据计算节点的left、width、height
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(cur, parent, isRoot, layerIndex) => {
|
||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
if (parent._node.isRoot) {
|
||||
newNode.top =
|
||||
parent._node.top +
|
||||
parent._node.height +
|
||||
this.getMarginX(layerIndex)
|
||||
}
|
||||
}
|
||||
if (!cur.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
(cur, parent, isRoot, layerIndex) => {
|
||||
if (isRoot) {
|
||||
let len = cur.data.expand === false ? 0 : cur._node.children.length
|
||||
cur._node.childrenAreaWidth = len
|
||||
? cur._node.children.reduce((h, item) => {
|
||||
return h + item.width
|
||||
}, 0) +
|
||||
(len + 1) * this.getMarginX(layerIndex + 1)
|
||||
: 0
|
||||
}
|
||||
},
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
let marginX = this.getMarginX(layerIndex + 1)
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
if (isRoot) {
|
||||
let left = node.left + node.width / 2 - node.childrenAreaWidth / 2
|
||||
let totalLeft = left + marginX
|
||||
node.children.forEach(cur => {
|
||||
cur.left = totalLeft
|
||||
totalLeft += cur.width + marginX
|
||||
})
|
||||
} else {
|
||||
let totalTop = node.top + node.height + marginY + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(cur => {
|
||||
cur.left = node.left + node.width * 0.5
|
||||
cur.top = totalTop
|
||||
totalTop += cur.height + marginY + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
// 调整left
|
||||
if (parent && parent.isRoot) {
|
||||
let areaWidth = this.getNodeAreaWidth(node)
|
||||
let difference = areaWidth - node.width
|
||||
if (difference > 0) {
|
||||
this.updateBrothersLeft(node, difference)
|
||||
}
|
||||
}
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
if (parent && !parent.isRoot && len > 0) {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
let totalHeight =
|
||||
node.children.reduce((h, item) => {
|
||||
return h + item.height + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
}, 0) +
|
||||
len * marginY
|
||||
this.updateBrothersTop(node, totalHeight)
|
||||
}
|
||||
},
|
||||
(node, parent, isRoot) => {
|
||||
if (isRoot) {
|
||||
let { right, left } = this.getNodeBoundaries(node, 'h')
|
||||
let childrenWidth = right - left
|
||||
let offset = (node.left - left) - (childrenWidth - node.width) / 2
|
||||
this.updateChildren(node.children, 'left', offset)
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node, addWidth) {
|
||||
if (node.parent) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition() || _index <= index) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
item.left += addWidth
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', addWidth)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersLeft(node.parent, addWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let len = node.children.length
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
if (node.isRoot) {
|
||||
// 根节点
|
||||
let x1 = left + width / 2
|
||||
let y1 = top + height
|
||||
let s1 = marginX * 0.7
|
||||
let minx = Infinity
|
||||
let maxx = -Infinity
|
||||
node.children.forEach((item, index) => {
|
||||
let x2 = item.left + item.width / 2
|
||||
let y2 = item.top
|
||||
if (x2 < minx) {
|
||||
minx = x2
|
||||
}
|
||||
if (x2 > maxx) {
|
||||
maxx = x2
|
||||
}
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? ` L ${item.left},${y2} L ${item.left + item.width},${y2}`
|
||||
: ''
|
||||
let path =
|
||||
`M ${x2},${y1 + s1} L ${x2},${y1 + s1 > y2 ? y2 + item.height : y2}` +
|
||||
nodeUseLineStylePath
|
||||
// 竖线
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
minx = Math.min(minx, x1)
|
||||
maxx = Math.max(maxx, x1)
|
||||
// 父节点的竖线
|
||||
let line1 = this.draw.path()
|
||||
node.style.line(line1)
|
||||
line1.plot(`M ${x1},${y1} L ${x1},${y1 + s1}`)
|
||||
node._lines.push(line1)
|
||||
style && style(line1, node)
|
||||
// 水平线
|
||||
if (len > 0) {
|
||||
let lin2 = this.draw.path()
|
||||
node.style.line(lin2)
|
||||
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
|
||||
node._lines.push(lin2)
|
||||
style && style(lin2, node)
|
||||
}
|
||||
} else {
|
||||
// 非根节点
|
||||
let y1 = top + height
|
||||
let maxy = -Infinity
|
||||
let x2 = node.left + node.width * 0.3
|
||||
node.children.forEach((item, index) => {
|
||||
// 为了适配自定义位置,下面做了各种位置的兼容
|
||||
let y2 = item.top + item.height / 2
|
||||
if (y2 > maxy) {
|
||||
maxy = y2
|
||||
}
|
||||
// 水平线
|
||||
let path = ''
|
||||
let _left = item.left
|
||||
let _isLeft = item.left + item.width < x2
|
||||
let _isXCenter = false
|
||||
if (_isLeft) {
|
||||
// 水平位置在父节点左边
|
||||
_left = item.left + item.width
|
||||
} else if (item.left < x2 && item.left + item.width > x2) {
|
||||
// 水平位置在父节点之间
|
||||
_isXCenter = true
|
||||
y2 = item.top
|
||||
maxy = y2
|
||||
}
|
||||
if (y2 > top && y2 < y1) {
|
||||
// 自定义位置的情况:垂直位置节点在父节点之间
|
||||
path = `M ${
|
||||
_isLeft ? node.left : node.left + node.width
|
||||
},${y2} L ${_left},${y2}`
|
||||
} else if (y2 < y1) {
|
||||
// 自定义位置的情况:垂直位置节点在父节点上面
|
||||
if (_isXCenter) {
|
||||
y2 = item.top + item.height
|
||||
_left = x2
|
||||
}
|
||||
path = `M ${x2},${top} L ${x2},${y2} L ${_left},${y2}`
|
||||
} else {
|
||||
if (_isXCenter) {
|
||||
_left = x2
|
||||
}
|
||||
path = `M ${x2},${y2} L ${_left},${y2}`
|
||||
}
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? ` L ${_left},${y2 - item.height / 2} L ${_left},${
|
||||
y2 + item.height / 2
|
||||
}`
|
||||
: ''
|
||||
path += nodeUseLineStylePath
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let lin2 = this.draw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
node.style.line(lin2)
|
||||
if (maxy < y1 + expandBtnSize) {
|
||||
lin2.hide()
|
||||
} else {
|
||||
lin2.plot(`M ${x2},${y1 + expandBtnSize} L ${x2},${maxy}`)
|
||||
lin2.show()
|
||||
}
|
||||
node._lines.push(lin2)
|
||||
style && style(lin2, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default CatalogOrganization
|
||||
|
||||
378
simple-mind-map/src/layouts/Fishbone.js
Normal file
@@ -0,0 +1,378 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun, degToRad } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
import utils from './fishboneUtils'
|
||||
|
||||
// 鱼骨图
|
||||
class Fishbone extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
this.indent = 0.3
|
||||
this.childIndent = 0.5
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
// 创建节点
|
||||
let newNode = this.createNode(node, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 三级及以下节点以上级方向为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
if (this.checkIsTop(newNode)) {
|
||||
newNode.top = parent._node.top - newNode.height
|
||||
} else {
|
||||
newNode.top = parent._node.top + parent._node.height
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!node.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (node.isRoot) {
|
||||
let topTotalLeft = node.left + node.width + node.height
|
||||
let bottomTotalLeft = node.left + node.width + node.height
|
||||
node.children.forEach(item => {
|
||||
if (this.checkIsTop(item)) {
|
||||
item.left = topTotalLeft
|
||||
topTotalLeft += item.width
|
||||
} else {
|
||||
item.left = bottomTotalLeft + 20
|
||||
bottomTotalLeft += item.width
|
||||
}
|
||||
})
|
||||
}
|
||||
let params = { layerIndex, node, ctx: this }
|
||||
if (this.checkIsTop(node)) {
|
||||
utils.top.computedLeftTopValue(params)
|
||||
} else {
|
||||
utils.bottom.computedLeftTopValue(params)
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
let params = { node, parent, layerIndex, ctx: this }
|
||||
if (this.checkIsTop(node)) {
|
||||
utils.top.adjustLeftTopValueBefore(params)
|
||||
} else {
|
||||
utils.bottom.adjustLeftTopValueBefore(params)
|
||||
}
|
||||
},
|
||||
(node, parent) => {
|
||||
let params = { parent, node, ctx: this }
|
||||
if (this.checkIsTop(node)) {
|
||||
utils.top.adjustLeftTopValueAfter(params)
|
||||
} else {
|
||||
utils.bottom.adjustLeftTopValueAfter(params)
|
||||
}
|
||||
// 调整二级节点的子节点的left值
|
||||
if (node.isRoot) {
|
||||
let topTotalLeft = 0
|
||||
let bottomTotalLeft = 0
|
||||
node.children.forEach(item => {
|
||||
if (this.checkIsTop(item)) {
|
||||
item.left += topTotalLeft
|
||||
this.updateChildren(item.children, 'left', topTotalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
topTotalLeft += right - left
|
||||
} else {
|
||||
item.left += bottomTotalLeft
|
||||
this.updateChildren(item.children, 'left', bottomTotalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
bottomTotalLeft += right - left
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
loop(node)
|
||||
return totalHeight
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node) {
|
||||
let childrenList = node.children
|
||||
let totalAddWidth = 0
|
||||
childrenList.forEach(item => {
|
||||
item.left += totalAddWidth
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
||||
}
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
let areaWidth = right - left
|
||||
let difference = areaWidth - item.width
|
||||
if (difference > 0) {
|
||||
totalAddWidth += difference
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
if (this.checkIsTop(node)) {
|
||||
this.updateBrothersTop(node.parent, addHeight)
|
||||
} else {
|
||||
this.updateBrothersTop(
|
||||
node.parent,
|
||||
node.layerIndex === 3 ? 0 : addHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查节点是否是上方节点
|
||||
checkIsTop(node) {
|
||||
return node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.layerIndex !== 1 && node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { top, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let len = node.children.length
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
let maxx = -Infinity
|
||||
node.children.forEach((item) => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
// 水平线段到二级节点的连线
|
||||
let nodeLineX = item.left
|
||||
let offset = node.height / 2
|
||||
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
let line = this.draw.path()
|
||||
if (this.checkIsTop(item)) {
|
||||
line.plot(
|
||||
`M ${nodeLineX - offsetX},${item.top + item.height + offset} L ${item.left},${
|
||||
item.top + item.height
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(
|
||||
`M ${nodeLineX - offsetX},${
|
||||
item.top - offset
|
||||
} L ${nodeLineX},${item.top}`
|
||||
)
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
})
|
||||
// 从根节点出发的水平线
|
||||
let nodeHalfTop = node.top + node.height / 2
|
||||
let offset = node.height / 2
|
||||
let line = this.draw.path()
|
||||
line.plot(
|
||||
`M ${node.left + node.width},${nodeHalfTop} L ${
|
||||
maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
},${nodeHalfTop}`
|
||||
)
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
let maxy = -Infinity
|
||||
let miny = Infinity
|
||||
let maxx = -Infinity
|
||||
let x = node.left + node.width * this.indent
|
||||
node.children.forEach((item, index) => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
let y = item.top + item.height / 2
|
||||
if (y > maxy) {
|
||||
maxy = y
|
||||
}
|
||||
if (y < miny) {
|
||||
miny = y
|
||||
}
|
||||
// 水平线
|
||||
if (node.layerIndex > 1) {
|
||||
let path = `M ${x},${y} L ${item.left},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
// 斜线
|
||||
if (len >= 0) {
|
||||
let line = this.draw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * this.indent
|
||||
lineLength = Math.max(lineLength, 0)
|
||||
let params = {
|
||||
node,
|
||||
line,
|
||||
top,
|
||||
x,
|
||||
lineLength,
|
||||
height,
|
||||
expandBtnSize,
|
||||
maxy,
|
||||
miny,
|
||||
ctx: this
|
||||
}
|
||||
if (this.checkIsTop(node)) {
|
||||
utils.top.renderLine(params)
|
||||
} else {
|
||||
utils.bottom.renderLine(params)
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
let params = {
|
||||
node,
|
||||
btn,
|
||||
expandBtnSize,
|
||||
translateX,
|
||||
translateY,
|
||||
width,
|
||||
height
|
||||
}
|
||||
if (this.checkIsTop(node)) {
|
||||
utils.top.renderExpandBtn(params)
|
||||
} else {
|
||||
utils.bottom.renderExpandBtn(params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default Fishbone
|
||||
369
simple-mind-map/src/layouts/FishboneBottom.js
Normal file
@@ -0,0 +1,369 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
|
||||
const degToRad = deg => {
|
||||
return (Math.PI / 180) * deg
|
||||
}
|
||||
|
||||
// 下方鱼骨图
|
||||
class Fishbone extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
// 创建节点
|
||||
let newNode = this.createNode(node, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 三级及以下节点以上级方向为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
newNode.top = parent._node.top + parent._node.height
|
||||
}
|
||||
}
|
||||
if (!node.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
if (node.isRoot) {
|
||||
let totalLeft = node.left + node.width
|
||||
node.children.forEach(item => {
|
||||
item.left = totalLeft
|
||||
totalLeft += item.width
|
||||
})
|
||||
}
|
||||
if (layerIndex === 1 && node.children) {
|
||||
// 遍历二级节点的子节点
|
||||
let startLeft = node.left + node.width * 0.5
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top =
|
||||
totalTop +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
totalTop +=
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
if (layerIndex > 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let startLeft = node.left + node.width * 0.5
|
||||
let totalTop =
|
||||
node.top -
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top = totalTop - item.height
|
||||
totalTop -=
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
// 调整三级节点的top
|
||||
// if (layerIndex === 2 && len > 0) {
|
||||
// let totalHeight = node.children.reduce((h, item) => {
|
||||
// return h + item.height
|
||||
// }, 0)
|
||||
// this.updateBrothersTop(node, totalHeight)
|
||||
// }
|
||||
if (layerIndex > 2 && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
)
|
||||
}, 0)
|
||||
this.updateBrothersTop(node, -totalHeight)
|
||||
}
|
||||
},
|
||||
(node, parent) => {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = 0
|
||||
let totalHeight2 = 0
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let hasChildren = this.getNodeActChildrenLength(item) > 0
|
||||
let nodeTotalHeight = this.getNodeAreaHeight(item)
|
||||
let offset =
|
||||
hasChildren > 0
|
||||
? nodeTotalHeight -
|
||||
item.height -
|
||||
(hasChildren ? item.expandBtnSize : 0)
|
||||
: 0
|
||||
let _top = totalHeight + offset
|
||||
item.top += _top
|
||||
// 调整left
|
||||
let offsetLeft =
|
||||
(totalHeight2 + nodeTotalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
item.left += offsetLeft
|
||||
totalHeight += offset
|
||||
totalHeight2 += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
this.updateChildrenPro(item.children, {
|
||||
top: _top,
|
||||
left: offsetLeft
|
||||
})
|
||||
})
|
||||
}
|
||||
// 调整二级节点的子节点的left值
|
||||
if (node.isRoot) {
|
||||
let totalLeft = 0
|
||||
node.children.forEach(item => {
|
||||
item.left += totalLeft
|
||||
this.updateChildren(item.children, 'left', totalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
totalLeft += right - left
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
loop(node)
|
||||
return totalHeight
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node) {
|
||||
let childrenList = node.children
|
||||
let totalAddWidth = 0
|
||||
childrenList.forEach(item => {
|
||||
item.left += totalAddWidth
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
||||
}
|
||||
// let areaWidth = this.getNodeAreaWidth(item)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
let areaWidth = right - left
|
||||
let difference = areaWidth - item.width
|
||||
if (difference > 0) {
|
||||
totalAddWidth += difference
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, node.layerIndex === 3 ? 0 : addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
let len = node.children.length
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 = prevBother.left + prevBother.width
|
||||
let x2 = item.left
|
||||
let y = node.top + node.height / 2
|
||||
let path = `M ${x1},${y} L ${x2},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
let maxy = -Infinity
|
||||
let miny = Infinity
|
||||
let maxx = -Infinity
|
||||
let x = node.left + node.width * 0.3
|
||||
node.children.forEach((item, index) => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
let y = item.top + item.height / 2
|
||||
if (y > maxy) {
|
||||
maxy = y
|
||||
}
|
||||
if (y < miny) {
|
||||
miny = y
|
||||
}
|
||||
// 水平线
|
||||
if (node.layerIndex > 1) {
|
||||
let path = `M ${x},${y} L ${item.left},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.draw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * 0.3
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top + height} L ${x + lineLength},${
|
||||
top + height + Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(`M ${x},${top} L ${x},${miny}`)
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default Fishbone
|
||||
350
simple-mind-map/src/layouts/FishboneTop.js
Normal file
@@ -0,0 +1,350 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
|
||||
const degToRad = deg => {
|
||||
return (Math.PI / 180) * deg
|
||||
}
|
||||
|
||||
// 上方鱼骨图
|
||||
class Fishbone extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
// 创建节点
|
||||
let newNode = this.createNode(node, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 三级及以下节点以上级方向为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
newNode.top = parent._node.top - newNode.height
|
||||
}
|
||||
}
|
||||
if (!node.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
if (node.isRoot) {
|
||||
let totalLeft = node.left + node.width
|
||||
node.children.forEach(item => {
|
||||
item.left = totalLeft
|
||||
totalLeft += item.width
|
||||
})
|
||||
}
|
||||
if (layerIndex >= 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let startLeft = node.left + node.width * 0.5
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top += totalTop
|
||||
totalTop +=
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
// 调整三级及以下节点的top
|
||||
if (parent && !parent.isRoot && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
)
|
||||
}, 0)
|
||||
this.updateBrothersTop(node, totalHeight)
|
||||
}
|
||||
},
|
||||
(node, parent) => {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = 0
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let nodeTotalHeight = this.getNodeAreaHeight(item)
|
||||
let _top = item.top
|
||||
item.top =
|
||||
node.top - (item.top - node.top) - nodeTotalHeight + node.height
|
||||
// 调整left
|
||||
let offsetLeft =
|
||||
(nodeTotalHeight + totalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
item.left += offsetLeft
|
||||
totalHeight += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
this.updateChildrenPro(item.children, {
|
||||
top: item.top - _top,
|
||||
left: offsetLeft
|
||||
})
|
||||
})
|
||||
}
|
||||
// 调整二级节点的子节点的left值
|
||||
if (node.isRoot) {
|
||||
let totalLeft = 0
|
||||
node.children.forEach(item => {
|
||||
item.left += totalLeft
|
||||
this.updateChildren(item.children, 'left', totalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
totalLeft += right - left
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
loop(node)
|
||||
return totalHeight
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node) {
|
||||
let childrenList = node.children
|
||||
let totalAddWidth = 0
|
||||
childrenList.forEach(item => {
|
||||
item.left += totalAddWidth
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
||||
}
|
||||
// let areaWidth = this.getNodeAreaWidth(item)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
let areaWidth = right - left
|
||||
let difference = areaWidth - item.width
|
||||
if (difference > 0) {
|
||||
totalAddWidth += difference
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
let len = node.children.length
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 = prevBother.left + prevBother.width
|
||||
let x2 = item.left
|
||||
let y = node.top + node.height / 2
|
||||
let path = `M ${x1},${y} L ${x2},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
let maxy = -Infinity
|
||||
let miny = Infinity
|
||||
let maxx = -Infinity
|
||||
let x = node.left + node.width * 0.3
|
||||
node.children.forEach((item, index) => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
let y = item.top + item.height / 2
|
||||
if (y > maxy) {
|
||||
maxy = y
|
||||
}
|
||||
if (y < miny) {
|
||||
miny = y
|
||||
}
|
||||
// 水平线
|
||||
if (node.layerIndex > 1) {
|
||||
let path = `M ${x},${y} L ${item.left},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.draw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * 0.3
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
top - Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
top - Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
|
||||
}
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default Fishbone
|
||||
@@ -1,267 +1,291 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
|
||||
// 逻辑结构图
|
||||
|
||||
class LogicalStructure extends Base {
|
||||
// 构造函数
|
||||
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据计算节点的left、width、height
|
||||
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(cur, parent, isRoot, layerIndex) => {
|
||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 定位到父节点右侧
|
||||
newNode.left =
|
||||
parent._node.left + parent._node.width + this.getMarginX(layerIndex)
|
||||
}
|
||||
if (!cur.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
(cur, parent, isRoot, layerIndex) => {
|
||||
// 返回时计算节点的areaHeight,也就是子节点所占的高度之和,包括外边距
|
||||
let len = cur.data.expand === false ? 0 : cur._node.children.length
|
||||
cur._node.childrenAreaHeight = len
|
||||
? cur._node.children.reduce((h, item) => {
|
||||
return h + item.height
|
||||
}, 0) +
|
||||
(len + 1) * this.getMarginY(layerIndex + 1)
|
||||
: 0
|
||||
},
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的top
|
||||
|
||||
computedTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
// 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半
|
||||
let top = node.top + node.height / 2 - node.childrenAreaHeight / 2
|
||||
let totalTop = top + marginY
|
||||
node.children.forEach(cur => {
|
||||
cur.top = totalTop
|
||||
totalTop += cur.height + marginY
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点top
|
||||
|
||||
adjustTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
|
||||
let difference =
|
||||
node.childrenAreaHeight -
|
||||
this.getMarginY(layerIndex + 1) * 2 -
|
||||
node.height
|
||||
if (difference > 0) {
|
||||
this.updateBrothers(node, difference / 2)
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 更新兄弟节点的top
|
||||
|
||||
updateBrothers(node, addHeight) {
|
||||
if (node.parent) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item === node || item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 上面的节点往上移
|
||||
if (_index < index) {
|
||||
_offset = -addHeight
|
||||
} else if (_index > index) {
|
||||
// 下面的节点往下移
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothers(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
|
||||
renderLine(node, lines, style, lineStyle) {
|
||||
if (lineStyle === 'curve') {
|
||||
this.renderLineCurve(node, lines, style)
|
||||
} else if (lineStyle === 'direct') {
|
||||
this.renderLineDirect(node, lines, style)
|
||||
} else {
|
||||
this.renderLineStraight(node, lines, style)
|
||||
}
|
||||
}
|
||||
|
||||
// 直线风格连线
|
||||
|
||||
renderLineStraight(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
let s1 = (marginX - expandBtnSize) * 0.6
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
node.layerIndex === 0 ? left + width : left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? item.width
|
||||
: 0
|
||||
let path = `M ${x1},${y1} L ${x1 + s1},${y1} L ${x1 + s1},${y2} L ${
|
||||
x2 + nodeUseLineStyleOffset
|
||||
},${y2}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
}
|
||||
|
||||
// 直连风格
|
||||
|
||||
renderLineDirect(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
node.layerIndex === 0 ? left + width / 2 : left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? ` L ${item.left + item.width},${y2}`
|
||||
: ''
|
||||
let path = `M ${x1},${y1} L ${x2},${y2}` + nodeUseLineStylePath
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
}
|
||||
|
||||
// 曲线风格连线
|
||||
|
||||
renderLineCurve(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
node.layerIndex === 0 ? left + width / 2 : left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
let path = ''
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? ` L ${item.left + item.width},${y2}`
|
||||
: ''
|
||||
if (node.isRoot) {
|
||||
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
} else {
|
||||
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
}
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height } = node
|
||||
let { translateX, translateY } = btn.transform()
|
||||
// 节点使用横线风格,需要调整展开收起按钮位置
|
||||
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? height / 2
|
||||
: 0
|
||||
btn.translate(
|
||||
width - translateX,
|
||||
height / 2 - translateY + nodeUseLineStyleOffset
|
||||
)
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
|
||||
// 逻辑结构图
|
||||
class LogicalStructure extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据计算节点的left、width、height
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(cur, parent, isRoot, layerIndex) => {
|
||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 定位到父节点右侧
|
||||
newNode.left =
|
||||
parent._node.left + parent._node.width + this.getMarginX(layerIndex)
|
||||
}
|
||||
if (!cur.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
(cur, parent, isRoot, layerIndex) => {
|
||||
// 返回时计算节点的areaHeight,也就是子节点所占的高度之和,包括外边距
|
||||
let len = cur.data.expand === false ? 0 : cur._node.children.length
|
||||
cur._node.childrenAreaHeight = len
|
||||
? cur._node.children.reduce((h, item) => {
|
||||
return h + item.height
|
||||
}, 0) +
|
||||
(len + 1) * this.getMarginY(layerIndex + 1)
|
||||
: 0
|
||||
},
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的top
|
||||
computedTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
// 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半
|
||||
let top = node.top + node.height / 2 - node.childrenAreaHeight / 2
|
||||
let totalTop = top + marginY
|
||||
node.children.forEach(cur => {
|
||||
cur.top = totalTop
|
||||
totalTop += cur.height + marginY
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点top
|
||||
adjustTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
|
||||
let difference =
|
||||
node.childrenAreaHeight -
|
||||
this.getMarginY(layerIndex + 1) * 2 -
|
||||
node.height
|
||||
if (difference > 0) {
|
||||
this.updateBrothers(node, difference / 2)
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 更新兄弟节点的top
|
||||
updateBrothers(node, addHeight) {
|
||||
if (node.parent) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item === node || item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 上面的节点往上移
|
||||
if (_index < index) {
|
||||
_offset = -addHeight
|
||||
} else if (_index > index) {
|
||||
// 下面的节点往下移
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothers(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style, lineStyle) {
|
||||
if (lineStyle === 'curve') {
|
||||
this.renderLineCurve(node, lines, style)
|
||||
} else if (lineStyle === 'direct') {
|
||||
this.renderLineDirect(node, lines, style)
|
||||
} else {
|
||||
this.renderLineStraight(node, lines, style)
|
||||
}
|
||||
}
|
||||
|
||||
// 直线风格连线
|
||||
renderLineStraight(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
let s1 = (marginX - expandBtnSize) * 0.6
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
node.layerIndex === 0 ? left + width : left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStyleOffset = nodeUseLineStyle
|
||||
? item.width
|
||||
: 0
|
||||
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
|
||||
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
|
||||
let path = `M ${x1},${y1} L ${x1 + s1},${y1} L ${x1 + s1},${y2} L ${
|
||||
x2 + nodeUseLineStyleOffset
|
||||
},${y2}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
}
|
||||
|
||||
// 直连风格
|
||||
renderLineDirect(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
node.layerIndex === 0 ? left + width / 2 : left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
|
||||
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = nodeUseLineStyle
|
||||
? ` L ${item.left + item.width},${y2}`
|
||||
: ''
|
||||
let path = `M ${x1},${y1} L ${x2},${y2}` + nodeUseLineStylePath
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
}
|
||||
|
||||
// 曲线风格连线
|
||||
renderLineCurve(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
node.layerIndex === 0 ? left + width / 2 : left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
let path = ''
|
||||
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
|
||||
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = nodeUseLineStyle
|
||||
? ` L ${item.left + item.width},${y2}`
|
||||
: ''
|
||||
if (node.isRoot) {
|
||||
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
} else {
|
||||
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
}
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height } = node
|
||||
let { translateX, translateY } = btn.transform()
|
||||
// 节点使用横线风格,需要调整展开收起按钮位置
|
||||
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? height / 2
|
||||
: 0
|
||||
// 位置没有变化则返回
|
||||
let _x = width
|
||||
let _y = height / 2 + nodeUseLineStyleOffset
|
||||
if (_x === translateX && _y === translateY) {
|
||||
return
|
||||
}
|
||||
btn.translate(
|
||||
_x - translateX,
|
||||
_y - translateY
|
||||
)
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default LogicalStructure
|
||||
|
||||
@@ -2,7 +2,6 @@ import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
|
||||
// 思维导图
|
||||
|
||||
class MindMap extends Base {
|
||||
// 构造函数
|
||||
// 在逻辑结构图的基础上增加一个变量来记录生长方向,向左还是向右,同时在计算left的时候根据方向来计算、调整top时只考虑同方向的节点即可
|
||||
@@ -11,7 +10,6 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 布局
|
||||
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
@@ -31,7 +29,6 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 遍历数据计算节点的left、width、height
|
||||
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
@@ -96,7 +93,6 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的top
|
||||
|
||||
computedTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
@@ -129,7 +125,6 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 调整节点top
|
||||
|
||||
adjustTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
@@ -152,7 +147,6 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 更新兄弟节点的top
|
||||
|
||||
updateBrothers(node, leftAddHeight, rightAddHeight) {
|
||||
if (node.parent) {
|
||||
// 过滤出和自己同方向的节点
|
||||
@@ -188,7 +182,6 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
|
||||
renderLine(node, lines, style, lineStyle) {
|
||||
if (lineStyle === 'curve') {
|
||||
this.renderLineCurve(node, lines, style)
|
||||
@@ -200,19 +193,22 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 直线风格连线
|
||||
|
||||
renderLineStraight(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
let s1 = (marginX - expandBtnSize) * 0.6
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 = 0
|
||||
let _s = 0
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
let nodeUseLineStyleOffset = nodeUseLineStyle
|
||||
? item.width
|
||||
: 0
|
||||
if (item.dir === 'left') {
|
||||
@@ -226,6 +222,8 @@ class MindMap extends Base {
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.dir === 'left' ? item.left + item.width : item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
|
||||
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
|
||||
let path = `M ${x1},${y1} L ${x1 + _s},${y1} L ${x1 + _s},${y2} L ${
|
||||
x2 + nodeUseLineStyleOffset
|
||||
},${y2}`
|
||||
@@ -235,12 +233,15 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 直连风格
|
||||
|
||||
renderLineDirect(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
node.layerIndex === 0
|
||||
@@ -251,9 +252,11 @@ class MindMap extends Base {
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.dir === 'left' ? item.left + item.width : item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
|
||||
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = ''
|
||||
if (this.mindMap.themeConfig.nodeUseLineStyle) {
|
||||
if (nodeUseLineStyle) {
|
||||
if (item.dir === 'left') {
|
||||
nodeUseLineStylePath = ` L ${item.left},${y2}`
|
||||
} else {
|
||||
@@ -267,23 +270,28 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 曲线风格连线
|
||||
|
||||
renderLineCurve(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let nodeUseLineStyle = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 =
|
||||
node.layerIndex === 0
|
||||
? left + width / 2
|
||||
: item.dir === 'left'
|
||||
? left - expandBtnSize
|
||||
: left + width + 20
|
||||
: left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.dir === 'left' ? item.left + item.width : item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
let path = ''
|
||||
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
|
||||
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = ''
|
||||
if (this.mindMap.themeConfig.nodeUseLineStyle) {
|
||||
@@ -304,7 +312,6 @@ class MindMap extends Base {
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize } = node
|
||||
let { translateX, translateY } = btn.transform()
|
||||
@@ -312,13 +319,18 @@ class MindMap extends Base {
|
||||
let nodeUseLineStyleOffset = this.mindMap.themeConfig.nodeUseLineStyle
|
||||
? height / 2
|
||||
: 0
|
||||
let x = (node.dir === 'left' ? 0 - expandBtnSize : width) - translateX
|
||||
let y = height / 2 - translateY + nodeUseLineStyleOffset
|
||||
// 位置没有变化则返回
|
||||
let _x = (node.dir === 'left' ? 0 - expandBtnSize : width)
|
||||
let _y = height / 2 + nodeUseLineStyleOffset
|
||||
if (_x === translateX && _y === translateY) {
|
||||
return
|
||||
}
|
||||
let x = _x - translateX
|
||||
let y = _y - translateY
|
||||
btn.translate(x, y)
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let isLeft = node.dir === 'left'
|
||||
let {
|
||||
|
||||
@@ -5,13 +5,11 @@ import { walk, asyncRun } from '../utils'
|
||||
// 和逻辑结构图基本一样,只是方向变成向下生长,所以先计算节点的top,后计算节点的left、最后调整节点的left即可
|
||||
class OrganizationStructure extends Base {
|
||||
// 构造函数
|
||||
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
@@ -31,7 +29,6 @@ class OrganizationStructure extends Base {
|
||||
}
|
||||
|
||||
// 遍历数据计算节点的left、width、height
|
||||
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
@@ -67,7 +64,6 @@ class OrganizationStructure extends Base {
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left
|
||||
|
||||
computedLeftValue() {
|
||||
walk(
|
||||
this.root,
|
||||
@@ -94,7 +90,6 @@ class OrganizationStructure extends Base {
|
||||
}
|
||||
|
||||
// 调整节点left
|
||||
|
||||
adjustLeftValue() {
|
||||
walk(
|
||||
this.root,
|
||||
@@ -118,7 +113,6 @@ class OrganizationStructure extends Base {
|
||||
}
|
||||
|
||||
// 更新兄弟节点的left
|
||||
|
||||
updateBrothers(node, addWidth) {
|
||||
if (node.parent) {
|
||||
let childrenList = node.parent.children
|
||||
@@ -150,7 +144,6 @@ class OrganizationStructure extends Base {
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
|
||||
renderLine(node, lines, style, lineStyle) {
|
||||
if (lineStyle === 'direct') {
|
||||
this.renderLineDirect(node, lines, style)
|
||||
@@ -160,7 +153,6 @@ class OrganizationStructure extends Base {
|
||||
}
|
||||
|
||||
// 直连风格
|
||||
|
||||
renderLineDirect(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
@@ -182,12 +174,14 @@ class OrganizationStructure extends Base {
|
||||
}
|
||||
|
||||
// 直线风格连线
|
||||
|
||||
renderLineStraight(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize, isRoot } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let x1 = left + width / 2
|
||||
let y1 = top + height
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
@@ -232,7 +226,6 @@ class OrganizationStructure extends Base {
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize } = node
|
||||
let { translateX, translateY } = btn.transform()
|
||||
@@ -243,7 +236,6 @@ class OrganizationStructure extends Base {
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
bottom,
|
||||
|
||||
341
simple-mind-map/src/layouts/Timeline.js
Normal file
@@ -0,0 +1,341 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
|
||||
// 时间轴
|
||||
class Timeline extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}, layout) {
|
||||
super(opt)
|
||||
this.layout = layout
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(cur, parent, isRoot, layerIndex, index) => {
|
||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 时间轴2类型需要交替显示
|
||||
if (this.layout === CONSTANTS.LAYOUT.TIMELINE2) {
|
||||
// 三级及以下节点以上级为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
: CONSTANTS.TIMELINE_DIR.TOP
|
||||
}
|
||||
} else {
|
||||
newNode.dir = ''
|
||||
}
|
||||
if (parent._node.isRoot) {
|
||||
newNode.top =
|
||||
parent._node.top +
|
||||
(cur._node.height > parent._node.height
|
||||
? -(cur._node.height - parent._node.height) / 2
|
||||
: (parent._node.height - cur._node.height) / 2)
|
||||
}
|
||||
}
|
||||
if (!cur.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
let marginX = this.getMarginX(layerIndex + 1)
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
if (isRoot) {
|
||||
let left = node.left + node.width
|
||||
let totalLeft = left + marginX
|
||||
node.children.forEach(cur => {
|
||||
cur.left = totalLeft
|
||||
totalLeft += cur.width + marginX
|
||||
})
|
||||
} else {
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
marginY +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(cur => {
|
||||
cur.left = node.left + node.width * 0.5
|
||||
cur.top = totalTop
|
||||
totalTop +=
|
||||
cur.height +
|
||||
marginY +
|
||||
(this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
// 调整left
|
||||
if (node.isRoot) {
|
||||
this.updateBrothersLeft(node)
|
||||
}
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
if (parent && !parent.isRoot && len > 0) {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
let totalHeight =
|
||||
node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0
|
||||
? item.expandBtnSize
|
||||
: 0)
|
||||
)
|
||||
}, 0) +
|
||||
len * marginY
|
||||
this.updateBrothersTop(node, totalHeight)
|
||||
}
|
||||
},
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (
|
||||
parent &&
|
||||
parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
) {
|
||||
// 遍历二级节点的子节点
|
||||
node.children.forEach(item => {
|
||||
let totalHeight = this.getNodeAreaHeight(item)
|
||||
let _top = item.top
|
||||
item.top =
|
||||
node.top - (item.top - node.top) - totalHeight + node.height
|
||||
this.updateChildren(item.children, 'top', item.top - _top)
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) +
|
||||
this.getMarginY(node.layerIndex)
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
loop(node)
|
||||
return totalHeight
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node) {
|
||||
let childrenList = node.children
|
||||
let totalAddWidth = 0
|
||||
childrenList.forEach(item => {
|
||||
item.left += totalAddWidth
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
||||
}
|
||||
// let areaWidth = this.getNodeAreaWidth(item)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
let areaWidth = right - left
|
||||
let difference = areaWidth - item.width
|
||||
if (difference > 0) {
|
||||
totalAddWidth += difference
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = childrenList.findIndex(item => {
|
||||
return item === node
|
||||
})
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let len = node.children.length
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 = prevBother.left + prevBother.width
|
||||
let x2 = item.left
|
||||
let y = node.top + node.height / 2
|
||||
let path = `M ${x1},${y} L ${x2},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
let maxy = -Infinity
|
||||
let miny = Infinity
|
||||
let x = node.left + node.width * 0.3
|
||||
node.children.forEach((item, index) => {
|
||||
let y = item.top + item.height / 2
|
||||
if (y > maxy) {
|
||||
maxy = y
|
||||
}
|
||||
if (y < miny) {
|
||||
miny = y
|
||||
}
|
||||
// 水平线
|
||||
let path = `M ${x},${y} L ${item.left},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.draw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
) {
|
||||
line.plot(`M ${x},${top} L ${x},${miny}`)
|
||||
} else {
|
||||
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default Timeline
|
||||
216
simple-mind-map/src/layouts/fishboneUtils.js
Normal file
@@ -0,0 +1,216 @@
|
||||
import { degToRad } from '../utils/'
|
||||
|
||||
export default {
|
||||
top: {
|
||||
renderExpandBtn({
|
||||
node,
|
||||
btn,
|
||||
expandBtnSize,
|
||||
translateX,
|
||||
translateY,
|
||||
width,
|
||||
height
|
||||
}) {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
},
|
||||
renderLine({
|
||||
node,
|
||||
line,
|
||||
top,
|
||||
x,
|
||||
lineLength,
|
||||
height,
|
||||
expandBtnSize,
|
||||
maxy,
|
||||
ctx
|
||||
}) {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
top - Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
|
||||
}
|
||||
},
|
||||
computedLeftTopValue({ layerIndex, node, ctx }) {
|
||||
if (layerIndex >= 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let startLeft = node.left + node.width * ctx.childIndent
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top += totalTop
|
||||
totalTop +=
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
adjustLeftTopValueBefore({ node, parent, ctx }) {
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
// 调整三级及以下节点的top
|
||||
if (parent && !parent.isRoot && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
)
|
||||
}, 0)
|
||||
ctx.updateBrothersTop(node, totalHeight)
|
||||
}
|
||||
},
|
||||
adjustLeftTopValueAfter({ parent, node, ctx }) {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = node.expandBtnSize
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
|
||||
let _top = item.top
|
||||
let _left = item.left
|
||||
item.top =
|
||||
node.top - (item.top - node.top) - nodeTotalHeight + node.height
|
||||
// 调整left
|
||||
item.left = node.left + node.width * ctx.indent + (nodeTotalHeight + totalHeight) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
|
||||
totalHeight += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
ctx.updateChildrenPro(item.children, {
|
||||
top: item.top - _top,
|
||||
left: item.left - _left
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
bottom: {
|
||||
renderExpandBtn({
|
||||
node,
|
||||
btn,
|
||||
expandBtnSize,
|
||||
translateX,
|
||||
translateY,
|
||||
width,
|
||||
height
|
||||
}) {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
},
|
||||
renderLine({ node, line, top, x, lineLength, height, miny, ctx }) {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top + height} L ${x + lineLength},${
|
||||
top + height + Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(`M ${x},${top} L ${x},${miny}`)
|
||||
}
|
||||
},
|
||||
computedLeftTopValue({ layerIndex, node, ctx }) {
|
||||
if (layerIndex === 1 && node.children) {
|
||||
// 遍历二级节点的子节点
|
||||
let startLeft = node.left + node.width * ctx.childIndent
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top =
|
||||
totalTop +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
totalTop +=
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
if (layerIndex > 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let startLeft = node.left + node.width * ctx.childIndent
|
||||
let totalTop =
|
||||
node.top -
|
||||
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top = totalTop - item.height
|
||||
totalTop -=
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
adjustLeftTopValueBefore({ node, ctx, layerIndex }) {
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
if (layerIndex > 2 && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
)
|
||||
}, 0)
|
||||
ctx.updateBrothersTop(node, -totalHeight)
|
||||
}
|
||||
},
|
||||
adjustLeftTopValueAfter({ parent, node, ctx }) {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = 0
|
||||
let totalHeight2 = node.expandBtnSize
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let hasChildren = ctx.getNodeActChildrenLength(item) > 0
|
||||
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
|
||||
let offset =
|
||||
hasChildren > 0
|
||||
? nodeTotalHeight -
|
||||
item.height -
|
||||
(hasChildren ? item.expandBtnSize : 0)
|
||||
: 0
|
||||
let _top = totalHeight + offset
|
||||
let _left = item.left
|
||||
item.top += _top
|
||||
// 调整left
|
||||
item.left = node.left + node.width * ctx.indent + (nodeTotalHeight + totalHeight2) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg))
|
||||
totalHeight += offset
|
||||
totalHeight2 += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
ctx.updateChildrenPro(item.children, {
|
||||
top: _top,
|
||||
left: item.left - _left
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
simple-mind-map/src/parse/markdown.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { transformToMarkdown } from './toMarkdown'
|
||||
import { transformMarkdownTo } from './markdownTo'
|
||||
|
||||
export default {
|
||||
transformToMarkdown,
|
||||
transformMarkdownTo
|
||||
}
|
||||
98
simple-mind-map/src/parse/markdownTo.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
|
||||
// 处理list的情况
|
||||
const handleList = node => {
|
||||
let list = []
|
||||
let walk = (arr, newArr) => {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let cur = arr[i]
|
||||
let node = {}
|
||||
node.data = {
|
||||
// 节点内容
|
||||
text: cur.children[0].children[0].value
|
||||
}
|
||||
node.children = []
|
||||
newArr.push(node)
|
||||
if (cur.children.length > 1) {
|
||||
for (let j = 1; j < cur.children.length; j++) {
|
||||
let cur2 = cur.children[j]
|
||||
if (cur2.type === 'list') {
|
||||
walk(cur2.children, node.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(node.children, list)
|
||||
return list
|
||||
}
|
||||
|
||||
// 将markdown转换成节点树
|
||||
export const transformMarkdownTo = async md => {
|
||||
const tree = fromMarkdown(md)
|
||||
let root = {
|
||||
children: []
|
||||
}
|
||||
let childrenQueue = [root.children]
|
||||
let currentChildren = root.children
|
||||
let depthQueue = [-1]
|
||||
let currentDepth = -1
|
||||
for (let i = 0; i < tree.children.length; i++) {
|
||||
let cur = tree.children[i]
|
||||
if (cur.type === 'heading') {
|
||||
if (!cur.children[0]) continue
|
||||
// 创建新节点
|
||||
let node = {}
|
||||
node.data = {
|
||||
// 节点内容
|
||||
text: cur.children[0].value
|
||||
}
|
||||
node.children = []
|
||||
// 如果当前的层级大于上一个节点的层级,那么是其子节点
|
||||
if (cur.depth > currentDepth) {
|
||||
// 添加到上一个节点的子节点列表里
|
||||
currentChildren.push(node)
|
||||
// 更新当前栈和数据
|
||||
childrenQueue.push(node.children)
|
||||
currentChildren = node.children
|
||||
depthQueue.push(cur.depth)
|
||||
currentDepth = cur.depth
|
||||
} else if (cur.depth === currentDepth) {
|
||||
// 如果当前层级等于上一个节点的层级,说明它们是同级节点
|
||||
// 将上一个节点出栈
|
||||
childrenQueue.pop()
|
||||
currentChildren = childrenQueue[childrenQueue.length - 1]
|
||||
depthQueue.pop()
|
||||
currentDepth = depthQueue[depthQueue.length - 1]
|
||||
// 追加到上上个节点的子节点列表里
|
||||
currentChildren.push(node)
|
||||
// 更新当前栈和数据
|
||||
childrenQueue.push(node.children)
|
||||
currentChildren = node.children
|
||||
depthQueue.push(cur.depth)
|
||||
currentDepth = cur.depth
|
||||
} else {
|
||||
// 如果当前层级小于上一个节点的层级,那么一直出栈,直到遇到比当前层级小的节点
|
||||
while (depthQueue.length) {
|
||||
childrenQueue.pop()
|
||||
currentChildren = childrenQueue[childrenQueue.length - 1]
|
||||
depthQueue.pop()
|
||||
currentDepth = depthQueue[depthQueue.length - 1]
|
||||
if (currentDepth < cur.depth) {
|
||||
// 追加到该节点的子节点列表里
|
||||
currentChildren.push(node)
|
||||
// 更新当前栈和数据
|
||||
childrenQueue.push(node.children)
|
||||
currentChildren = node.children
|
||||
depthQueue.push(cur.depth)
|
||||
currentDepth = cur.depth
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cur.type === 'list') {
|
||||
currentChildren.push(...handleList(cur))
|
||||
}
|
||||
}
|
||||
return root.children[0]
|
||||
}
|
||||
53
simple-mind-map/src/parse/toMarkdown.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { walk } from '../utils'
|
||||
|
||||
let el = null
|
||||
const getText = str => {
|
||||
if (!el) {
|
||||
el = document.createElement('div')
|
||||
}
|
||||
el.innerHTML = str
|
||||
return el.textContent
|
||||
}
|
||||
|
||||
const getTitleMark = level => {
|
||||
return new Array(level).fill('#').join('')
|
||||
}
|
||||
|
||||
const getIndentMark = level => {
|
||||
return new Array(level - 6).fill(' ').join('') + '*'
|
||||
}
|
||||
|
||||
// 转换成markdown格式
|
||||
export const transformToMarkdown = root => {
|
||||
let content = ''
|
||||
walk(
|
||||
root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
let level = layerIndex + 1
|
||||
let text = node.data.richText ? getText(node.data.text) : node.data.text
|
||||
if (level <= 6) {
|
||||
content += getTitleMark(level)
|
||||
} else {
|
||||
content += getIndentMark(level)
|
||||
}
|
||||
content += ' ' + text
|
||||
// 概要
|
||||
let generalization = node.data.generalization
|
||||
if (generalization && generalization.text) {
|
||||
let generalizationText = generalization.richText
|
||||
? getText(generalization.text)
|
||||
: generalization.text
|
||||
content += `[${generalizationText}]`
|
||||
}
|
||||
content += '\n\n'
|
||||
// 备注
|
||||
if (node.data.note) {
|
||||
content += node.data.note + '\n\n'
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
true
|
||||
)
|
||||
return content
|
||||
}
|
||||
@@ -279,9 +279,9 @@ export const nodeIconList = [
|
||||
]
|
||||
|
||||
// 获取nodeIconList icon内容
|
||||
const getNodeIconListIcon = name => {
|
||||
const getNodeIconListIcon = (name, extendIconList = []) => {
|
||||
let arr = name.split('_')
|
||||
let typeData = nodeIconList.find(item => {
|
||||
let typeData = [...nodeIconList, ...extendIconList].find(item => {
|
||||
return item.type === arr[0]
|
||||
})
|
||||
return typeData.list.find(item => {
|
||||
|
||||
57
simple-mind-map/src/themes/blackGold.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import defaultTheme from './default'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 黑金
|
||||
export default merge(defaultTheme, {
|
||||
// 背景颜色
|
||||
backgroundColor: 'rgb(18, 20, 20)',
|
||||
// 连线的颜色
|
||||
lineColor: 'rgb(205, 186, 156)',
|
||||
lineWidth: 3,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 3,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: 'rgb(245, 224, 191)',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(255, 208, 124)',
|
||||
color: 'rgb(111, 61, 6)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
fillColor: 'rgb(66, 57, 46)',
|
||||
color: 'rgb(225, 201, 158)',
|
||||
borderColor: 'rgb(245, 224, 191)',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 208, 124)'
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(231, 203, 155)',
|
||||
active: {
|
||||
borderColor: 'rgb(255, 208, 124)'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fontSize: 14,
|
||||
fillColor: 'rgb(56, 45, 34)',
|
||||
borderColor: 'rgb(104, 84, 61)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(242, 216, 176)',
|
||||
active: {
|
||||
borderColor: 'rgb(255, 208, 124)'
|
||||
}
|
||||
}
|
||||
})
|
||||
58
simple-mind-map/src/themes/blackHumour.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import defaultTheme from './default'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 黑色幽默
|
||||
export default merge(defaultTheme, {
|
||||
// 背景颜色
|
||||
backgroundColor: 'rgb(27, 31, 34)',
|
||||
// 连线的颜色
|
||||
lineColor: 'rgb(75, 81, 78)',
|
||||
lineWidth: 3,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 3,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: 'rgb(255, 119, 34)',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(36, 179, 96)',
|
||||
color: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: 'rgb(254, 199, 13)',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
fillColor: 'rgb(254, 199, 13)',
|
||||
color: 'rgb(0, 0, 0)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(36, 179, 96)',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(204, 204, 204)',
|
||||
active: {
|
||||
borderColor: 'rgb(254, 199, 13)'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fontSize: 14,
|
||||
fillColor: 'rgb(27, 31, 34)',
|
||||
borderColor: 'rgb(255, 119, 34)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(204, 204, 204)',
|
||||
active: {
|
||||
borderColor: 'rgb(36, 179, 96)'
|
||||
}
|
||||
}
|
||||
})
|
||||
55
simple-mind-map/src/themes/coffee.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import defaultTheme from './default'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 咖啡
|
||||
export default merge(defaultTheme, {
|
||||
// 连线的颜色
|
||||
lineColor: 'rgb(173, 123, 91)',
|
||||
lineWidth: 4,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 4,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: 'rgb(173, 123, 91)',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(202, 117, 79)',
|
||||
color: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: 'rgb(173, 123, 91)',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
fillColor: 'rgb(245, 231, 216)',
|
||||
color: 'rgb(125, 86, 42)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(173, 123, 91)'
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(96, 71, 47)',
|
||||
active: {
|
||||
borderColor: 'rgb(173, 123, 91)'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fontSize: 14,
|
||||
fillColor: 'rgb(255, 249, 239)',
|
||||
borderColor: 'rgb(173, 123, 91)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(122, 83, 44)',
|
||||
active: {
|
||||
borderColor: 'rgb(202, 117, 79)'
|
||||
}
|
||||
}
|
||||
})
|
||||
55
simple-mind-map/src/themes/courseGreen.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import defaultTheme from './default'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 课程绿
|
||||
export default merge(defaultTheme, {
|
||||
// 连线的颜色
|
||||
lineColor: 'rgb(113, 195, 169)',
|
||||
lineWidth: 3,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 3,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: 'rgb(113, 195, 169)',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(16, 160, 121)',
|
||||
color: '#fff',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: 'rgb(173, 91, 12)',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
fillColor: 'rgb(240, 252, 249)',
|
||||
color: 'rgb(50, 113, 96)',
|
||||
borderColor: 'rgb(113, 195, 169)',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(173, 91, 12)'
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(10, 59, 43)',
|
||||
active: {
|
||||
borderColor: 'rgb(173, 91, 12)'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fontSize: 14,
|
||||
fillColor: 'rgb(246, 238, 211)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
color: 'rgb(173, 91, 12)',
|
||||
active: {
|
||||
borderColor: 'rgb(113, 195, 169)'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,142 +1,151 @@
|
||||
// 默认主题
|
||||
|
||||
export default {
|
||||
// 节点内边距
|
||||
paddingX: 15,
|
||||
paddingY: 5,
|
||||
// 图片显示的最大宽度
|
||||
imgMaxWidth: 100,
|
||||
// 图片显示的最大高度
|
||||
imgMaxHeight: 100,
|
||||
// icon的大小
|
||||
iconSize: 20,
|
||||
// 连线的粗细
|
||||
lineWidth: 1,
|
||||
// 连线的颜色
|
||||
lineColor: '#549688',
|
||||
// 连线样式
|
||||
lineDasharray: 'none',
|
||||
// 连线风格
|
||||
lineStyle: 'straight', // 针对logicalStructure、mindMap两种结构。曲线(curve)、直线(straight)、直连(direct)
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 1,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: '#549688',
|
||||
// 概要曲线距节点的距离
|
||||
generalizationLineMargin: 0,
|
||||
// 概要节点距节点的距离
|
||||
generalizationNodeMargin: 20,
|
||||
// 背景颜色
|
||||
backgroundColor: '#fafafa',
|
||||
// 背景图片
|
||||
backgroundImage: 'none',
|
||||
// 背景重复
|
||||
backgroundRepeat: 'no-repeat',
|
||||
// 节点使用横线样式
|
||||
nodeUseLineStyle: false,
|
||||
// 根节点样式
|
||||
root: {
|
||||
shape: 'rectangle',
|
||||
fillColor: '#549688',
|
||||
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1.5,
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
borderDasharray: 'none',
|
||||
borderRadius: 5,
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
shape: 'rectangle',
|
||||
marginX: 100,
|
||||
marginY: 40,
|
||||
fillColor: '#fff',
|
||||
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||
color: '#565656',
|
||||
fontSize: 16,
|
||||
fontWeight: 'noraml',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1.5,
|
||||
borderColor: '#549688',
|
||||
borderWidth: 1,
|
||||
borderDasharray: 'none',
|
||||
borderRadius: 5,
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
shape: 'rectangle',
|
||||
marginX: 50,
|
||||
marginY: 0,
|
||||
fillColor: 'transparent',
|
||||
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||
color: '#6a6d6c',
|
||||
fontSize: 14,
|
||||
fontWeight: 'noraml',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1.5,
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
borderRadius: 5,
|
||||
borderDasharray: 'none',
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
shape: 'rectangle',
|
||||
marginX: 100,
|
||||
marginY: 40,
|
||||
fillColor: '#fff',
|
||||
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||
color: '#565656',
|
||||
fontSize: 16,
|
||||
fontWeight: 'noraml',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1.5,
|
||||
borderColor: '#549688',
|
||||
borderWidth: 1,
|
||||
borderDasharray: 'none',
|
||||
borderRadius: 5,
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 支持激活样式的属性
|
||||
// 简单来说,会改变节点大小的都不支持在激活时设置,为了性能考虑,节点切换激活态时不会重新计算节点大小
|
||||
export const supportActiveStyle = [
|
||||
'fillColor',
|
||||
'color',
|
||||
'fontWeight',
|
||||
'fontStyle',
|
||||
'borderColor',
|
||||
'borderWidth',
|
||||
'borderDasharray',
|
||||
'borderRadius',
|
||||
'textDecoration'
|
||||
]
|
||||
|
||||
// 默认主题
|
||||
|
||||
export default {
|
||||
// 节点内边距
|
||||
paddingX: 15,
|
||||
paddingY: 5,
|
||||
// 图片显示的最大宽度
|
||||
imgMaxWidth: 100,
|
||||
// 图片显示的最大高度
|
||||
imgMaxHeight: 100,
|
||||
// icon的大小
|
||||
iconSize: 20,
|
||||
// 连线的粗细
|
||||
lineWidth: 1,
|
||||
// 连线的颜色
|
||||
lineColor: '#549688',
|
||||
// 连线样式
|
||||
lineDasharray: 'none',
|
||||
// 连线风格
|
||||
lineStyle: 'straight', // 针对logicalStructure、mindMap两种结构。曲线(curve)、直线(straight)、直连(direct)
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 1,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: '#549688',
|
||||
// 概要曲线距节点的距离
|
||||
generalizationLineMargin: 0,
|
||||
// 概要节点距节点的距离
|
||||
generalizationNodeMargin: 20,
|
||||
// 关联线默认状态的粗细
|
||||
associativeLineWidth: 2,
|
||||
// 关联线默认状态的颜色
|
||||
associativeLineColor: 'rgb(51, 51, 51)',
|
||||
// 关联线激活状态的粗细
|
||||
associativeLineActiveWidth: 8,
|
||||
// 关联线激活状态的颜色
|
||||
associativeLineActiveColor: 'rgba(2, 167, 240, 1)',
|
||||
// 背景颜色
|
||||
backgroundColor: '#fafafa',
|
||||
// 背景图片
|
||||
backgroundImage: 'none',
|
||||
// 背景重复
|
||||
backgroundRepeat: 'no-repeat',
|
||||
// 设置背景图像的起始位置
|
||||
backgroundPosition: 'center center',
|
||||
// 设置背景图片大小
|
||||
backgroundSize: 'cover',
|
||||
// 节点使用横线样式
|
||||
nodeUseLineStyle: false,
|
||||
// 根节点样式
|
||||
root: {
|
||||
shape: 'rectangle',
|
||||
fillColor: '#549688',
|
||||
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1.5,
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
borderDasharray: 'none',
|
||||
borderRadius: 5,
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
shape: 'rectangle',
|
||||
marginX: 100,
|
||||
marginY: 40,
|
||||
fillColor: '#fff',
|
||||
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||
color: '#565656',
|
||||
fontSize: 16,
|
||||
fontWeight: 'noraml',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1.5,
|
||||
borderColor: '#549688',
|
||||
borderWidth: 1,
|
||||
borderDasharray: 'none',
|
||||
borderRadius: 5,
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
shape: 'rectangle',
|
||||
marginX: 50,
|
||||
marginY: 0,
|
||||
fillColor: 'transparent',
|
||||
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||
color: '#6a6d6c',
|
||||
fontSize: 14,
|
||||
fontWeight: 'noraml',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1.5,
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
borderRadius: 5,
|
||||
borderDasharray: 'none',
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
shape: 'rectangle',
|
||||
marginX: 100,
|
||||
marginY: 40,
|
||||
fillColor: '#fff',
|
||||
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||
color: '#565656',
|
||||
fontSize: 16,
|
||||
fontWeight: 'noraml',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1.5,
|
||||
borderColor: '#549688',
|
||||
borderWidth: 1,
|
||||
borderDasharray: 'none',
|
||||
borderRadius: 5,
|
||||
textDecoration: 'none',
|
||||
active: {
|
||||
borderColor: 'rgb(57, 80, 96)',
|
||||
borderWidth: 3,
|
||||
borderDasharray: 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 支持激活样式的属性
|
||||
// 简单来说,会改变节点大小的都不支持在激活时设置,为了性能考虑,节点切换激活态时不会重新计算节点大小
|
||||
export const supportActiveStyle = [
|
||||
'fillColor',
|
||||
'borderColor',
|
||||
'borderWidth',
|
||||
'borderDasharray',
|
||||
'borderRadius'
|
||||
]
|
||||
|
||||
export const lineStyleProps = ['lineColor', 'lineDasharray', 'lineWidth']
|
||||
|
||||
@@ -20,6 +20,13 @@ import vitalityOrange from './vitalityOrange'
|
||||
import greenLeaf from './greenLeaf'
|
||||
import dark2 from './dark2'
|
||||
import skyGreen from './skyGreen'
|
||||
import simpleBlack from './simpleBlack'
|
||||
import courseGreen from './courseGreen'
|
||||
import coffee from './coffee'
|
||||
import redSpirit from './redSpirit'
|
||||
import blackHumour from './blackHumour'
|
||||
import lateNightOffice from './lateNightOffice'
|
||||
import blackGold from './blackGold'
|
||||
|
||||
export default {
|
||||
default: defaultTheme,
|
||||
@@ -43,5 +50,12 @@ export default {
|
||||
vitalityOrange,
|
||||
greenLeaf,
|
||||
dark2,
|
||||
skyGreen
|
||||
skyGreen,
|
||||
simpleBlack,
|
||||
courseGreen,
|
||||
coffee,
|
||||
redSpirit,
|
||||
blackHumour,
|
||||
lateNightOffice,
|
||||
blackGold
|
||||
}
|
||||
|
||||
58
simple-mind-map/src/themes/lateNightOffice.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import defaultTheme from './default'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 深夜办公室
|
||||
export default merge(defaultTheme, {
|
||||
// 背景颜色
|
||||
backgroundColor: 'rgb(32, 37, 49)',
|
||||
// 连线的颜色
|
||||
lineColor: 'rgb(137, 167, 196)',
|
||||
lineWidth: 3,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 3,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: 'rgb(255, 119, 34)',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(23, 153, 243)',
|
||||
color: 'rgb(255, 255, 255)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 119, 34)',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
fillColor: 'rgb(70, 78, 94)',
|
||||
color: 'rgb(209, 210, 210)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 119, 34)',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(204, 204, 204)',
|
||||
active: {
|
||||
borderColor: 'rgb(255, 119, 34)'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fontSize: 14,
|
||||
fillColor: 'rgb(255, 119, 34)',
|
||||
borderColor: '',
|
||||
borderWidth: 2,
|
||||
color: '#fff',
|
||||
active: {
|
||||
borderColor: 'rgb(23, 153, 243)'
|
||||
}
|
||||
}
|
||||
})
|
||||
57
simple-mind-map/src/themes/redSpirit.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import defaultTheme from './default'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 红色精神
|
||||
export default merge(defaultTheme, {
|
||||
// 背景颜色
|
||||
backgroundColor: 'rgb(255, 238, 228)',
|
||||
// 连线的颜色
|
||||
lineColor: 'rgb(230, 138, 131)',
|
||||
lineWidth: 3,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 3,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: 'rgb(222, 101, 85)',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: 'rgb(207, 44, 44)',
|
||||
color: 'rgb(255, 233, 157)',
|
||||
borderColor: '',
|
||||
borderWidth: 0,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 233, 157)',
|
||||
borderWidth: 3
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
fillColor: 'rgb(255, 255, 255)',
|
||||
color: 'rgb(211, 58, 21)',
|
||||
borderColor: 'rgb(222, 101, 85)',
|
||||
borderWidth: 2,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: 'rgb(255, 233, 157)'
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(144, 71, 43)',
|
||||
active: {
|
||||
borderColor: 'rgb(255, 233, 157)'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fontSize: 14,
|
||||
fillColor: 'rgb(255, 247, 211)',
|
||||
borderColor: 'rgb(255, 202, 162)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(187, 101, 69)',
|
||||
active: {
|
||||
borderColor: 'rgb(222, 101, 85)'
|
||||
}
|
||||
}
|
||||
})
|
||||
54
simple-mind-map/src/themes/simpleBlack.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import defaultTheme from './default'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 简约黑
|
||||
export default merge(defaultTheme, {
|
||||
// 连线的颜色
|
||||
lineColor: 'rgb(34, 34, 34)',
|
||||
lineWidth: 4,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 4,
|
||||
// 概要连线的颜色
|
||||
generalizationLineColor: 'rgb(34, 34, 34)',
|
||||
// 根节点样式
|
||||
root: {
|
||||
fillColor: '#fff',
|
||||
color: 'rgb(34, 34, 34)',
|
||||
borderColor: 'rgb(34, 34, 34)',
|
||||
borderWidth: 3,
|
||||
fontSize: 24,
|
||||
active: {
|
||||
borderColor: '#a13600'
|
||||
}
|
||||
},
|
||||
// 二级节点样式
|
||||
second: {
|
||||
fillColor: 'rgb(241, 246, 248)',
|
||||
color: 'rgb(34, 34, 34)',
|
||||
borderColor: 'rgb(34, 34, 34)',
|
||||
borderWidth: 3,
|
||||
fontSize: 18,
|
||||
active: {
|
||||
borderColor: '#a13600'
|
||||
}
|
||||
},
|
||||
// 三级及以下节点样式
|
||||
node: {
|
||||
fontSize: 14,
|
||||
color: 'rgb(34, 34, 34)',
|
||||
active: {
|
||||
borderColor: '#a13600'
|
||||
}
|
||||
},
|
||||
// 概要节点样式
|
||||
generalization: {
|
||||
fontSize: 14,
|
||||
fillColor: 'transparent',
|
||||
borderColor: 'rgb(34, 34, 34)',
|
||||
borderWidth: 2,
|
||||
color: 'rgb(34, 34, 34)',
|
||||
active: {
|
||||
borderColor: '#a13600'
|
||||
}
|
||||
}
|
||||
})
|
||||
182
simple-mind-map/src/utils/associativeLineUtils.js
Normal file
@@ -0,0 +1,182 @@
|
||||
// 获取目标节点在起始节点的目标数组中的索引
|
||||
export const getAssociativeLineTargetIndex = (node, toNode) => {
|
||||
return node.nodeData.data.associativeLineTargets.findIndex(item => {
|
||||
return item === toNode.nodeData.data.id
|
||||
})
|
||||
}
|
||||
|
||||
// 计算贝塞尔曲线的控制点
|
||||
export const computeCubicBezierPathPoints = (x1, y1, x2, y2) => {
|
||||
let cx1 = x1 + (x2 - x1) / 2
|
||||
let cy1 = y1
|
||||
let cx2 = cx1
|
||||
let cy2 = y2
|
||||
if (Math.abs(x1 - x2) <= 5) {
|
||||
cx1 = x1 + (y2 - y1) / 2
|
||||
cx2 = cx1
|
||||
}
|
||||
return [
|
||||
{
|
||||
x: cx1,
|
||||
y: cy1
|
||||
},
|
||||
{
|
||||
x: cx2,
|
||||
y: cy2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 拼接贝塞尔曲线路径
|
||||
export const joinCubicBezierPath = (startPoint, endPoint, point1, point2) => {
|
||||
return `M ${startPoint.x},${startPoint.y} C ${point1.x},${point1.y} ${point2.x},${point2.y} ${endPoint.x},${endPoint.y}`
|
||||
}
|
||||
|
||||
// 获取节点的位置信息
|
||||
const getNodeRect = node => {
|
||||
let { left, top, width, height } = node
|
||||
return {
|
||||
right: left + width,
|
||||
bottom: top + height,
|
||||
left,
|
||||
top
|
||||
}
|
||||
}
|
||||
|
||||
// 三次贝塞尔曲线
|
||||
export const cubicBezierPath = (x1, y1, x2, y2) => {
|
||||
let points = computeCubicBezierPathPoints(x1, y1, x2, y2)
|
||||
return joinCubicBezierPath(
|
||||
{ x: x1, y: y1 },
|
||||
{ x: x2, y: y2 },
|
||||
points[0],
|
||||
points[1]
|
||||
)
|
||||
}
|
||||
|
||||
// 获取节点的连接点
|
||||
export const getNodePoint = (node, dir = 'right') => {
|
||||
let { left, top, width, height } = node
|
||||
switch (dir) {
|
||||
case 'left':
|
||||
return {
|
||||
x: left,
|
||||
y: top + height / 2
|
||||
}
|
||||
case 'right':
|
||||
return {
|
||||
x: left + width,
|
||||
y: top + height / 2
|
||||
}
|
||||
case 'top':
|
||||
return {
|
||||
x: left + width / 2,
|
||||
y: top
|
||||
}
|
||||
case 'bottom':
|
||||
return {
|
||||
x: left + width / 2,
|
||||
y: top + height
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 根据两个节点的位置计算节点的连接点
|
||||
export const computeNodePoints = (fromNode, toNode) => {
|
||||
let fromRect = getNodeRect(fromNode)
|
||||
let fromCx = (fromRect.right + fromRect.left) / 2
|
||||
let fromCy = (fromRect.bottom + fromRect.top) / 2
|
||||
let toRect = getNodeRect(toNode)
|
||||
let toCx = (toRect.right + toRect.left) / 2
|
||||
let toCy = (toRect.bottom + toRect.top) / 2
|
||||
// 中心点坐标的差值
|
||||
let offsetX = toCx - fromCx
|
||||
let offsetY = toCy - fromCy
|
||||
if (offsetX === 0 && offsetY === 0) return
|
||||
let fromDir = ''
|
||||
let toDir = ''
|
||||
if (offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY) {
|
||||
// left
|
||||
fromDir = 'left'
|
||||
toDir = 'right'
|
||||
} else if (offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY) {
|
||||
// right
|
||||
fromDir = 'right'
|
||||
toDir = 'left'
|
||||
} else if (offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX) {
|
||||
// up
|
||||
fromDir = 'top'
|
||||
toDir = 'bottom'
|
||||
} else if (offsetY > 0 && -offsetY < offsetX && offsetY > offsetX) {
|
||||
// down
|
||||
fromDir = 'bottom'
|
||||
toDir = 'top'
|
||||
}
|
||||
return [getNodePoint(fromNode, fromDir), getNodePoint(toNode, toDir)]
|
||||
}
|
||||
|
||||
// 获取节点的关联线路径
|
||||
export const getNodeLinePath = (startPoint, endPoint, node, toNode) => {
|
||||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||||
// 控制点
|
||||
let controlPoints = []
|
||||
let associativeLineTargetControlOffsets =
|
||||
node.nodeData.data.associativeLineTargetControlOffsets
|
||||
if (
|
||||
associativeLineTargetControlOffsets &&
|
||||
associativeLineTargetControlOffsets[targetIndex]
|
||||
) {
|
||||
// 节点保存了控制点差值
|
||||
let offsets = associativeLineTargetControlOffsets[targetIndex]
|
||||
controlPoints = [
|
||||
{
|
||||
x: startPoint.x + offsets[0].x,
|
||||
y: startPoint.y + offsets[0].y
|
||||
},
|
||||
{
|
||||
x: endPoint.x + offsets[1].x,
|
||||
y: endPoint.y + offsets[1].y
|
||||
}
|
||||
]
|
||||
} else {
|
||||
// 没有保存控制点则生成默认的
|
||||
controlPoints = computeCubicBezierPathPoints(
|
||||
startPoint.x,
|
||||
startPoint.y,
|
||||
endPoint.x,
|
||||
endPoint.y
|
||||
)
|
||||
}
|
||||
// 根据控制点拼接贝塞尔曲线路径
|
||||
return {
|
||||
path: joinCubicBezierPath(
|
||||
startPoint,
|
||||
endPoint,
|
||||
controlPoints[0],
|
||||
controlPoints[1]
|
||||
),
|
||||
controlPoints
|
||||
}
|
||||
}
|
||||
|
||||
// 获取默认的控制点差值
|
||||
export const getDefaultControlPointOffsets = (startPoint, endPoint) => {
|
||||
let controlPoints = computeCubicBezierPathPoints(
|
||||
startPoint.x,
|
||||
startPoint.y,
|
||||
endPoint.x,
|
||||
endPoint.y
|
||||
)
|
||||
return [
|
||||
{
|
||||
x: controlPoints[0].x - startPoint.x,
|
||||
y: controlPoints[0].y - startPoint.y
|
||||
},
|
||||
{
|
||||
x: controlPoints[1].x - endPoint.x,
|
||||
y: controlPoints[1].y - endPoint.y
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -22,32 +22,6 @@ export const tagColorList = [
|
||||
}
|
||||
]
|
||||
|
||||
// 布局结构列表
|
||||
export const layoutList = [
|
||||
{
|
||||
name: '逻辑结构图',
|
||||
value: 'logicalStructure',
|
||||
},
|
||||
{
|
||||
name: '思维导图',
|
||||
value: 'mindMap',
|
||||
},
|
||||
{
|
||||
name: '组织结构图',
|
||||
value: 'organizationStructure',
|
||||
},
|
||||
{
|
||||
name: '目录组织图',
|
||||
value: 'catalogOrganization',
|
||||
}
|
||||
]
|
||||
export const layoutValueList = [
|
||||
'logicalStructure',
|
||||
'mindMap',
|
||||
'catalogOrganization',
|
||||
'organizationStructure'
|
||||
]
|
||||
|
||||
// 主题列表
|
||||
export const themeList = [
|
||||
{
|
||||
@@ -137,5 +111,139 @@ export const themeList = [
|
||||
{
|
||||
name: '浪漫紫',
|
||||
value: 'romanticPurple',
|
||||
},
|
||||
{
|
||||
name: '简约黑',
|
||||
value: 'simpleBlack',
|
||||
},
|
||||
{
|
||||
name: '课程绿',
|
||||
value: 'courseGreen',
|
||||
},
|
||||
{
|
||||
name: '咖啡',
|
||||
value: 'coffee',
|
||||
},
|
||||
{
|
||||
name: '红色精神',
|
||||
value: 'redSpirit',
|
||||
},
|
||||
{
|
||||
name: '黑色幽默',
|
||||
value: 'blackHumour',
|
||||
},
|
||||
{
|
||||
name: '深夜办公室',
|
||||
value: 'lateNightOffice',
|
||||
},
|
||||
{
|
||||
name: '黑金',
|
||||
value: 'blackGold',
|
||||
}
|
||||
]
|
||||
|
||||
// 常量
|
||||
export const CONSTANTS = {
|
||||
CHANGE_THEME: 'changeTheme',
|
||||
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
|
||||
]
|
||||
@@ -122,27 +122,33 @@ export const simpleDeepClone = data => {
|
||||
}
|
||||
|
||||
// 复制渲染树数据
|
||||
export const copyRenderTree = (tree, root) => {
|
||||
export const copyRenderTree = (tree, root, removeActiveState = false) => {
|
||||
tree.data = simpleDeepClone(root.data)
|
||||
tree.children = []
|
||||
if (root.children && root.children.length > 0) {
|
||||
root.children.forEach((item, index) => {
|
||||
tree.children[index] = copyRenderTree({}, item)
|
||||
})
|
||||
}
|
||||
return tree
|
||||
}
|
||||
|
||||
// 复制节点树数据
|
||||
export const copyNodeTree = (tree, root, removeActiveState = false) => {
|
||||
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data)
|
||||
if (removeActiveState) {
|
||||
tree.data.isActive = false
|
||||
}
|
||||
tree.children = []
|
||||
if (root.children && root.children.length > 0) {
|
||||
root.children.forEach((item, index) => {
|
||||
tree.children[index] = copyNodeTree({}, item, removeActiveState)
|
||||
tree.children[index] = copyRenderTree({}, item, removeActiveState)
|
||||
})
|
||||
}
|
||||
return tree
|
||||
}
|
||||
|
||||
// 复制节点树数据
|
||||
export const copyNodeTree = (tree, root, removeActiveState = false, keepId = false) => {
|
||||
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data)
|
||||
// 去除节点id,因为节点id不能重复
|
||||
if (tree.data.id && !keepId) delete tree.data.id
|
||||
if (tree.data.uid) delete tree.data.uid
|
||||
if (removeActiveState) {
|
||||
tree.data.isActive = false
|
||||
}
|
||||
tree.children = []
|
||||
if (root.children && root.children.length > 0) {
|
||||
root.children.forEach((item, index) => {
|
||||
tree.children[index] = copyNodeTree({}, item, removeActiveState, keepId)
|
||||
})
|
||||
} else if (
|
||||
root.nodeData &&
|
||||
@@ -150,7 +156,7 @@ export const copyNodeTree = (tree, root, removeActiveState = false) => {
|
||||
root.nodeData.children.length > 0
|
||||
) {
|
||||
root.nodeData.children.forEach((item, index) => {
|
||||
tree.children[index] = copyNodeTree({}, item, removeActiveState)
|
||||
tree.children[index] = copyNodeTree({}, item, removeActiveState, keepId)
|
||||
})
|
||||
}
|
||||
return tree
|
||||
@@ -193,12 +199,12 @@ export const downloadFile = (file, fileName) => {
|
||||
// 节流函数
|
||||
export const throttle = (fn, time = 300, ctx) => {
|
||||
let timer = null
|
||||
return () => {
|
||||
return (...args) => {
|
||||
if (timer) {
|
||||
return
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
fn.call(ctx)
|
||||
fn.call(ctx, ...args)
|
||||
timer = null
|
||||
}, time)
|
||||
}
|
||||
@@ -235,4 +241,105 @@ export const camelCaseToHyphen = (str) => {
|
||||
return str.replace(/([a-z])([A-Z])/g, (...args) => {
|
||||
return args[1] + '-' + args[2].toLowerCase()
|
||||
})
|
||||
}
|
||||
|
||||
//计算节点的文本长宽
|
||||
let measureTextContext = null
|
||||
export const measureText = (text, { italic, bold, fontSize, fontFamily }) => {
|
||||
const font = joinFontStr({
|
||||
italic,
|
||||
bold,
|
||||
fontSize,
|
||||
fontFamily
|
||||
})
|
||||
if (!measureTextContext) {
|
||||
const canvas = document.createElement('canvas')
|
||||
measureTextContext = canvas.getContext('2d')
|
||||
}
|
||||
measureTextContext.save()
|
||||
measureTextContext.font = font
|
||||
const {
|
||||
width,
|
||||
actualBoundingBoxAscent,
|
||||
actualBoundingBoxDescent
|
||||
} = measureTextContext.measureText(text)
|
||||
measureTextContext.restore()
|
||||
const height = actualBoundingBoxAscent + actualBoundingBoxDescent
|
||||
return { width, height }
|
||||
}
|
||||
|
||||
// 拼接font字符串
|
||||
export const joinFontStr = ({ italic, bold, fontSize, fontFamily }) => {
|
||||
return `${italic ? 'italic ' : ''} ${bold ? 'bold ' : ''} ${fontSize}px ${fontFamily} `
|
||||
}
|
||||
|
||||
// 在下一个事件循环里执行任务
|
||||
export const nextTick = function (fn, ctx) {
|
||||
let pending = false
|
||||
let timerFunc = null
|
||||
let handle = () => {
|
||||
pending = false
|
||||
ctx ? fn.call(ctx) : fn()
|
||||
}
|
||||
// 支持MutationObserver接口的话使用MutationObserver
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
let counter = 1
|
||||
let observer = new MutationObserver(handle)
|
||||
let textNode = document.createTextNode(counter)
|
||||
observer.observe(textNode, {
|
||||
characterData: true // 设为 true 表示监视指定目标节点或子节点树中节点所包含的字符数据的变化
|
||||
})
|
||||
timerFunc = function () {
|
||||
counter = (counter + 1) % 2 // counter会在0和1两者循环变化
|
||||
textNode.data = counter // 节点变化会触发回调handle,
|
||||
}
|
||||
} else {
|
||||
// 否则使用定时器
|
||||
timerFunc = setTimeout
|
||||
}
|
||||
return function () {
|
||||
if (pending) return
|
||||
pending = true
|
||||
timerFunc(handle, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查节点是否超出画布
|
||||
export const checkNodeOuter = (mindMap, node) => {
|
||||
let elRect = mindMap.elRect
|
||||
let { scaleX, scaleY, translateX, translateY } = mindMap.draw.transform()
|
||||
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
|
||||
let offsetLeft = 0
|
||||
let offsetTop = 0
|
||||
if (left < 0) {
|
||||
offsetLeft = -left
|
||||
}
|
||||
if (right > elRect.width) {
|
||||
offsetLeft = -(right - elRect.width)
|
||||
}
|
||||
if (top < 0) {
|
||||
offsetTop = -top
|
||||
}
|
||||
if (bottom > elRect.height) {
|
||||
offsetTop = -(bottom - elRect.height)
|
||||
}
|
||||
return {
|
||||
isOuter: offsetLeft !== 0 || offsetTop !== 0,
|
||||
offsetLeft,
|
||||
offsetTop
|
||||
}
|
||||
}
|
||||
|
||||
// 提取html字符串里的纯文本
|
||||
let getTextFromHtmlEl = null
|
||||
export const getTextFromHtml = (html) => {
|
||||
if (!getTextFromHtmlEl) {
|
||||
getTextFromHtmlEl = document.createElement('div')
|
||||
}
|
||||
getTextFromHtmlEl.innerHTML = html
|
||||
return getTextFromHtmlEl.textContent
|
||||
}
|
||||
56
simple-mind-map/src/utils/nodeCommandWraps.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// 设置数据
|
||||
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
|
||||
}
|
||||
282
simple-mind-map/src/utils/nodeCreateContents.js
Normal file
@@ -0,0 +1,282 @@
|
||||
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 './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
|
||||
}
|
||||
142
simple-mind-map/src/utils/nodeExpandBtn.js
Normal file
@@ -0,0 +1,142 @@
|
||||
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
|
||||
}
|
||||
115
simple-mind-map/src/utils/nodeGeneralization.js
Normal file
@@ -0,0 +1,115 @@
|
||||
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
|
||||
}
|
||||
354
simple-mind-map/src/utils/simulateCSSBackgroundInCanvas.js
Normal file
@@ -0,0 +1,354 @@
|
||||
// 将以空格分隔的字符串值转换成成数字/单位/值数组
|
||||
const getNumberValueFromStr = value => {
|
||||
let arr = String(value).split(/\s+/)
|
||||
return arr.map(item => {
|
||||
if (/^[\d.]+/.test(item)) {
|
||||
// 数字+单位
|
||||
let res = /^([\d.]+)(.*)$/.exec(item)
|
||||
return [Number(res[1]), res[2]]
|
||||
} else {
|
||||
// 单个值
|
||||
return item
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 缩放宽度
|
||||
const zoomWidth = (ratio, height) => {
|
||||
// w / height = ratio
|
||||
return ratio * height
|
||||
}
|
||||
|
||||
// 缩放高度
|
||||
const zoomHeight = (ratio, width) => {
|
||||
// width / h = ratio
|
||||
return width / ratio
|
||||
}
|
||||
|
||||
// 关键词到百分比值的映射
|
||||
const keyWordToPercentageMap = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
center: 50,
|
||||
bottom: 100,
|
||||
right: 100
|
||||
}
|
||||
|
||||
// 模拟background-size
|
||||
const handleBackgroundSize = ({
|
||||
backgroundSize,
|
||||
drawOpt,
|
||||
imageRatio,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
canvasRatio
|
||||
}) => {
|
||||
if (backgroundSize) {
|
||||
// 将值转换成数组
|
||||
let backgroundSizeValueArr = getNumberValueFromStr(backgroundSize)
|
||||
// 两个值都为auto,那就相当于不设置
|
||||
if (
|
||||
backgroundSizeValueArr[0] === 'auto' &&
|
||||
backgroundSizeValueArr[1] === 'auto'
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 值为cover
|
||||
if (backgroundSizeValueArr[0] === 'cover') {
|
||||
if (imageRatio > canvasRatio) {
|
||||
// 图片的宽高比大于canvas的宽高比,那么图片高度缩放到和canvas的高度一致,宽度自适应
|
||||
drawOpt.height = canvasHeight
|
||||
drawOpt.width = zoomWidth(imageRatio, canvasHeight)
|
||||
} else {
|
||||
// 否则图片宽度缩放到和canvas的宽度一致,高度自适应
|
||||
drawOpt.width = canvasWidth
|
||||
drawOpt.height = zoomHeight(imageRatio, canvasWidth)
|
||||
}
|
||||
return
|
||||
}
|
||||
// 值为contain
|
||||
if (backgroundSizeValueArr[0] === 'contain') {
|
||||
if (imageRatio > canvasRatio) {
|
||||
// 图片的宽高比大于canvas的宽高比,那么图片宽度缩放到和canvas的宽度一致,高度自适应
|
||||
drawOpt.width = canvasWidth
|
||||
drawOpt.height = zoomHeight(imageRatio, canvasWidth)
|
||||
} else {
|
||||
// 否则图片高度缩放到和canvas的高度一致,宽度自适应
|
||||
drawOpt.height = canvasHeight
|
||||
drawOpt.width = zoomWidth(imageRatio, canvasHeight)
|
||||
}
|
||||
return
|
||||
}
|
||||
// 图片宽度
|
||||
let newNumberWidth = -1
|
||||
if (backgroundSizeValueArr[0]) {
|
||||
if (Array.isArray(backgroundSizeValueArr[0])) {
|
||||
// 数字+单位类型
|
||||
if (backgroundSizeValueArr[0][1] === '%') {
|
||||
// %单位
|
||||
drawOpt.width = (backgroundSizeValueArr[0][0] / 100) * canvasWidth
|
||||
newNumberWidth = drawOpt.width
|
||||
} else {
|
||||
// 其他都认为是px单位
|
||||
drawOpt.width = backgroundSizeValueArr[0][0]
|
||||
newNumberWidth = backgroundSizeValueArr[0][0]
|
||||
}
|
||||
} else if (backgroundSizeValueArr[0] === 'auto') {
|
||||
// auto类型,那么根据设置的新高度以图片原宽高比进行自适应
|
||||
if (backgroundSizeValueArr[1]) {
|
||||
if (backgroundSizeValueArr[1][1] === '%') {
|
||||
// 高度为%单位
|
||||
drawOpt.width = zoomWidth(
|
||||
imageRatio,
|
||||
(backgroundSizeValueArr[1][0] / 100) * canvasHeight
|
||||
)
|
||||
} else {
|
||||
// 其他都认为是px单位
|
||||
drawOpt.width = zoomWidth(imageRatio, backgroundSizeValueArr[1][0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 设置了图片高度
|
||||
if (backgroundSizeValueArr[1] && Array.isArray(backgroundSizeValueArr[1])) {
|
||||
// 数字+单位类型
|
||||
if (backgroundSizeValueArr[1][1] === '%') {
|
||||
// 高度为%单位
|
||||
drawOpt.height = (backgroundSizeValueArr[1][0] / 100) * canvasHeight
|
||||
} else {
|
||||
// 其他都认为是px单位
|
||||
drawOpt.height = backgroundSizeValueArr[1][0]
|
||||
}
|
||||
} else if (newNumberWidth !== -1) {
|
||||
// 没有设置图片高度或者设置为auto,那么根据设置的新宽度以图片原宽高比进行自适应
|
||||
drawOpt.height = zoomHeight(imageRatio, newNumberWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟background-position
|
||||
const handleBackgroundPosition = ({
|
||||
backgroundPosition,
|
||||
drawOpt,
|
||||
imgWidth,
|
||||
imgHeight,
|
||||
canvasWidth,
|
||||
canvasHeight
|
||||
}) => {
|
||||
if (backgroundPosition) {
|
||||
// 将值转换成数组
|
||||
let backgroundPositionValueArr = getNumberValueFromStr(backgroundPosition)
|
||||
// 将关键词转为百分比
|
||||
backgroundPositionValueArr = backgroundPositionValueArr.map(item => {
|
||||
if (typeof item === 'string') {
|
||||
return keyWordToPercentageMap[item] !== undefined
|
||||
? [keyWordToPercentageMap[item], '%']
|
||||
: item
|
||||
}
|
||||
return item
|
||||
})
|
||||
if (Array.isArray(backgroundPositionValueArr[0])) {
|
||||
if (backgroundPositionValueArr.length === 1) {
|
||||
// 如果只设置了一个值,第二个默认为50%
|
||||
backgroundPositionValueArr.push([50, '%'])
|
||||
}
|
||||
// 水平位置
|
||||
if (backgroundPositionValueArr[0][1] === '%') {
|
||||
// 单位为%
|
||||
let canvasX = (backgroundPositionValueArr[0][0] / 100) * canvasWidth
|
||||
let imgX = (backgroundPositionValueArr[0][0] / 100) * imgWidth
|
||||
// 计算差值
|
||||
drawOpt.x = canvasX - imgX
|
||||
} else {
|
||||
// 其他单位默认都为px
|
||||
drawOpt.x = backgroundPositionValueArr[0][0]
|
||||
}
|
||||
// 垂直位置
|
||||
if (backgroundPositionValueArr[1][1] === '%') {
|
||||
// 单位为%
|
||||
let canvasY = (backgroundPositionValueArr[1][0] / 100) * canvasHeight
|
||||
let imgY = (backgroundPositionValueArr[1][0] / 100) * imgHeight
|
||||
// 计算差值
|
||||
drawOpt.y = canvasY - imgY
|
||||
} else {
|
||||
// 其他单位默认都为px
|
||||
drawOpt.y = backgroundPositionValueArr[1][0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟background-repeat
|
||||
const handleBackgroundRepeat = ({
|
||||
ctx,
|
||||
image,
|
||||
backgroundRepeat,
|
||||
drawOpt,
|
||||
imgWidth,
|
||||
imgHeight,
|
||||
canvasWidth,
|
||||
canvasHeight
|
||||
}) => {
|
||||
if (backgroundRepeat) {
|
||||
// 保存在handleBackgroundPosition中计算出来的x、y
|
||||
let ox = drawOpt.x
|
||||
let oy = drawOpt.y
|
||||
// 计算ox和oy能平铺的图片数量
|
||||
let oxRepeatNum = Math.ceil(ox / imgWidth)
|
||||
let oyRepeatNum = Math.ceil(oy / imgHeight)
|
||||
// 计算ox和oy第一张图片的位置
|
||||
let oxRepeatX = ox - oxRepeatNum * imgWidth
|
||||
let oxRepeatY = oy - oyRepeatNum * imgHeight
|
||||
// 将值转换成数组
|
||||
let backgroundRepeatValueArr = getNumberValueFromStr(backgroundRepeat)
|
||||
// 不处理
|
||||
if (
|
||||
backgroundRepeatValueArr[0] === 'no-repeat' ||
|
||||
(imgWidth >= canvasWidth && imgHeight >= canvasHeight)
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 水平平铺
|
||||
if (backgroundRepeatValueArr[0] === 'repeat-x') {
|
||||
if (canvasWidth > imgWidth) {
|
||||
let x = oxRepeatX
|
||||
while (x < canvasWidth) {
|
||||
drawImage(ctx, image, {
|
||||
...drawOpt,
|
||||
x
|
||||
})
|
||||
x += imgWidth
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 垂直平铺
|
||||
if (backgroundRepeatValueArr[0] === 'repeat-y') {
|
||||
if (canvasHeight > imgHeight) {
|
||||
let y = oxRepeatY
|
||||
while (y < canvasHeight) {
|
||||
drawImage(ctx, image, {
|
||||
...drawOpt,
|
||||
y
|
||||
})
|
||||
y += imgHeight
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 平铺
|
||||
if (backgroundRepeatValueArr[0] === 'repeat') {
|
||||
let x = oxRepeatX
|
||||
while (x < canvasWidth) {
|
||||
if (canvasHeight > imgHeight) {
|
||||
let y = oxRepeatY
|
||||
while (y < canvasHeight) {
|
||||
drawImage(ctx, image, {
|
||||
...drawOpt,
|
||||
x,
|
||||
y
|
||||
})
|
||||
y += imgHeight
|
||||
}
|
||||
}
|
||||
x += imgWidth
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据参数绘制图片
|
||||
const drawImage = (ctx, image, drawOpt) => {
|
||||
ctx.drawImage(
|
||||
image,
|
||||
drawOpt.sx,
|
||||
drawOpt.sy,
|
||||
drawOpt.swidth,
|
||||
drawOpt.sheight,
|
||||
drawOpt.x,
|
||||
drawOpt.y,
|
||||
drawOpt.width,
|
||||
drawOpt.height
|
||||
)
|
||||
}
|
||||
|
||||
const drawBackgroundImageToCanvas = (
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
img,
|
||||
{ backgroundSize, backgroundPosition, backgroundRepeat },
|
||||
callback = () => {}
|
||||
) => {
|
||||
// 画布的长宽比
|
||||
let canvasRatio = width / height
|
||||
// 加载图片
|
||||
let image = new Image()
|
||||
image.src = img
|
||||
image.onload = () => {
|
||||
// 图片的宽度及长宽比
|
||||
let imgWidth = image.width
|
||||
let imgHeight = image.height
|
||||
let imageRatio = imgWidth / imgHeight
|
||||
// 绘制图片
|
||||
// drawImage方法的参数值
|
||||
let drawOpt = {
|
||||
sx: 0,
|
||||
sy: 0,
|
||||
swidth: imgWidth,
|
||||
sheight: imgHeight,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: imgWidth,
|
||||
height: imgHeight
|
||||
}
|
||||
// 模拟background-size
|
||||
handleBackgroundSize({
|
||||
backgroundSize,
|
||||
drawOpt,
|
||||
imageRatio,
|
||||
canvasWidth: width,
|
||||
canvasHeight: height,
|
||||
canvasRatio
|
||||
})
|
||||
|
||||
// 模拟background-position
|
||||
handleBackgroundPosition({
|
||||
backgroundPosition,
|
||||
drawOpt,
|
||||
imgWidth: drawOpt.width,
|
||||
imgHeight: drawOpt.height,
|
||||
imageRatio,
|
||||
canvasWidth: width,
|
||||
canvasHeight: height,
|
||||
canvasRatio
|
||||
})
|
||||
|
||||
// 模拟background-repeat
|
||||
let notNeedDraw = handleBackgroundRepeat({
|
||||
ctx,
|
||||
image,
|
||||
backgroundRepeat,
|
||||
drawOpt,
|
||||
imgWidth: drawOpt.width,
|
||||
imgHeight: drawOpt.height,
|
||||
imageRatio,
|
||||
canvasWidth: width,
|
||||
canvasHeight: height,
|
||||
canvasRatio
|
||||
})
|
||||
|
||||
// 绘制图片
|
||||
if (!notNeedDraw) {
|
||||
drawImage(ctx, image, drawOpt)
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
image.onerror = e => {
|
||||
callback(e)
|
||||
}
|
||||
}
|
||||
|
||||
export default drawBackgroundImageToCanvas
|
||||
597
web/package-lock.json
generated
@@ -24,6 +24,8 @@
|
||||
"@vue/cli-plugin-eslint": "^4.5.0",
|
||||
"@vue/cli-service": "^4.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"esbuild": "^0.17.15",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"less": "^3.12.2",
|
||||
@@ -1716,6 +1718,358 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.15.tgz",
|
||||
"integrity": "sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.15.tgz",
|
||||
"integrity": "sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.15.tgz",
|
||||
"integrity": "sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.15.tgz",
|
||||
"integrity": "sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.15.tgz",
|
||||
"integrity": "sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.15.tgz",
|
||||
"integrity": "sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.15.tgz",
|
||||
"integrity": "sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.15.tgz",
|
||||
"integrity": "sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.15.tgz",
|
||||
"integrity": "sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.15.tgz",
|
||||
"integrity": "sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.15.tgz",
|
||||
"integrity": "sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.15.tgz",
|
||||
"integrity": "sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.15.tgz",
|
||||
"integrity": "sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.15.tgz",
|
||||
"integrity": "sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@hapi/address": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
|
||||
@@ -3524,7 +3878,6 @@
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -4138,7 +4491,6 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
@@ -4160,7 +4512,6 @@
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
@@ -4173,7 +4524,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -4186,7 +4536,6 @@
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
@@ -4199,7 +4548,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
@@ -4209,7 +4557,6 @@
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
@@ -6236,6 +6583,43 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz",
|
||||
"integrity": "sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.17.15",
|
||||
"@esbuild/android-arm64": "0.17.15",
|
||||
"@esbuild/android-x64": "0.17.15",
|
||||
"@esbuild/darwin-arm64": "0.17.15",
|
||||
"@esbuild/darwin-x64": "0.17.15",
|
||||
"@esbuild/freebsd-arm64": "0.17.15",
|
||||
"@esbuild/freebsd-x64": "0.17.15",
|
||||
"@esbuild/linux-arm": "0.17.15",
|
||||
"@esbuild/linux-arm64": "0.17.15",
|
||||
"@esbuild/linux-ia32": "0.17.15",
|
||||
"@esbuild/linux-loong64": "0.17.15",
|
||||
"@esbuild/linux-mips64el": "0.17.15",
|
||||
"@esbuild/linux-ppc64": "0.17.15",
|
||||
"@esbuild/linux-riscv64": "0.17.15",
|
||||
"@esbuild/linux-s390x": "0.17.15",
|
||||
"@esbuild/linux-x64": "0.17.15",
|
||||
"@esbuild/netbsd-x64": "0.17.15",
|
||||
"@esbuild/openbsd-x64": "0.17.15",
|
||||
"@esbuild/sunos-x64": "0.17.15",
|
||||
"@esbuild/win32-arm64": "0.17.15",
|
||||
"@esbuild/win32-ia32": "0.17.15",
|
||||
"@esbuild/win32-x64": "0.17.15"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@@ -8606,7 +8990,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
@@ -12109,7 +12492,6 @@
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
@@ -17458,6 +17840,160 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@esbuild/android-arm": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.15.tgz",
|
||||
"integrity": "sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-arm64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.15.tgz",
|
||||
"integrity": "sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-arm64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.15.tgz",
|
||||
"integrity": "sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-arm64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.15.tgz",
|
||||
"integrity": "sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.15.tgz",
|
||||
"integrity": "sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.15.tgz",
|
||||
"integrity": "sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ia32": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.15.tgz",
|
||||
"integrity": "sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-loong64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.15.tgz",
|
||||
"integrity": "sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-mips64el": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.15.tgz",
|
||||
"integrity": "sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ppc64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.15.tgz",
|
||||
"integrity": "sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-riscv64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.15.tgz",
|
||||
"integrity": "sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-s390x": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.15.tgz",
|
||||
"integrity": "sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/netbsd-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/openbsd-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/sunos-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-arm64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.15.tgz",
|
||||
"integrity": "sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-ia32": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.15.tgz",
|
||||
"integrity": "sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-x64": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz",
|
||||
"integrity": "sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@hapi/address": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
|
||||
@@ -18943,8 +19479,7 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.5.0",
|
||||
@@ -19454,7 +19989,6 @@
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
@@ -19471,7 +20005,6 @@
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
@@ -19481,7 +20014,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
@@ -19491,7 +20023,6 @@
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
@@ -19500,15 +20031,13 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
@@ -21119,6 +21648,36 @@
|
||||
"is-symbol": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.17.15",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz",
|
||||
"integrity": "sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@esbuild/android-arm": "0.17.15",
|
||||
"@esbuild/android-arm64": "0.17.15",
|
||||
"@esbuild/android-x64": "0.17.15",
|
||||
"@esbuild/darwin-arm64": "0.17.15",
|
||||
"@esbuild/darwin-x64": "0.17.15",
|
||||
"@esbuild/freebsd-arm64": "0.17.15",
|
||||
"@esbuild/freebsd-x64": "0.17.15",
|
||||
"@esbuild/linux-arm": "0.17.15",
|
||||
"@esbuild/linux-arm64": "0.17.15",
|
||||
"@esbuild/linux-ia32": "0.17.15",
|
||||
"@esbuild/linux-loong64": "0.17.15",
|
||||
"@esbuild/linux-mips64el": "0.17.15",
|
||||
"@esbuild/linux-ppc64": "0.17.15",
|
||||
"@esbuild/linux-riscv64": "0.17.15",
|
||||
"@esbuild/linux-s390x": "0.17.15",
|
||||
"@esbuild/linux-x64": "0.17.15",
|
||||
"@esbuild/netbsd-x64": "0.17.15",
|
||||
"@esbuild/openbsd-x64": "0.17.15",
|
||||
"@esbuild/sunos-x64": "0.17.15",
|
||||
"@esbuild/win32-arm64": "0.17.15",
|
||||
"@esbuild/win32-ia32": "0.17.15",
|
||||
"@esbuild/win32-x64": "0.17.15"
|
||||
}
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@@ -22941,7 +23500,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
@@ -25800,7 +26358,6 @@
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build && node ../copy.js",
|
||||
"lint": "vue-cli-service lint",
|
||||
"buildLibrary": "vue-cli-service build --target lib --name simpleMindMap ../simple-mind-map/index.js --dest ../simple-mind-map/dist",
|
||||
"buildLibrary": "vue-cli-service build --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js",
|
||||
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
|
||||
"buildDoc": "node ./scripts/buildDoc.js"
|
||||
"buildDoc": "node ./scripts/buildDoc.js",
|
||||
"autoBuildDoc": "node ./scripts/autoBuildDoc.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@toast-ui/editor": "^3.1.5",
|
||||
@@ -27,6 +28,8 @@
|
||||
"@vue/cli-plugin-eslint": "^4.5.0",
|
||||
"@vue/cli-service": "^4.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"esbuild": "^0.17.15",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"less": "^3.12.2",
|
||||
|
||||
38
web/scripts/autoBuildDoc.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const chokidar = require('chokidar')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { exec } = require('node:child_process')
|
||||
const { transformMdToVue } = require('./transformMdToVue')
|
||||
|
||||
const reBuildAll = () => {
|
||||
exec(
|
||||
'node ./buildDoc.js',
|
||||
{
|
||||
cwd: path.resolve(__dirname)
|
||||
},
|
||||
(error, msg) => {
|
||||
console.log(error, msg)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const buildOne = file => {
|
||||
let content = fs.readFileSync(file, 'utf-8')
|
||||
let doc = transformMdToVue(content)
|
||||
let destPath = path.join(path.dirname(file), './index.vue')
|
||||
fs.writeFileSync(destPath, doc)
|
||||
}
|
||||
|
||||
chokidar
|
||||
.watch(path.join(__dirname, '../src/pages/Doc/'), {
|
||||
ignoreInitial: true
|
||||
})
|
||||
.on('all', (event, file) => {
|
||||
if (/\.md$/.test(file)) {
|
||||
if (event === 'change') {
|
||||
buildOne(file)
|
||||
} else {
|
||||
reBuildAll()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,24 +1,7 @@
|
||||
// 编译文档
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const hljs = require('highlight.js')
|
||||
const md = require('markdown-it')({
|
||||
highlight: function(str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return (
|
||||
'<pre class="hljs"><code>' +
|
||||
hljs.highlight(lang, str, true).value +
|
||||
'</code></pre>'
|
||||
)
|
||||
} catch (__) {}
|
||||
}
|
||||
|
||||
return (
|
||||
'<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
|
||||
)
|
||||
}
|
||||
}).use(require('markdown-it-checkbox'))
|
||||
const { transformMdToVue } = require('./transformMdToVue')
|
||||
|
||||
// 文档语言种类
|
||||
let langList = ['zh', 'en']
|
||||
@@ -48,15 +31,12 @@ const compilerDir = (dir, dirName, routerList) => {
|
||||
const compilerFile = (dir, file, dirName, routerList) => {
|
||||
let filePath = path.join(dir, file)
|
||||
let destPath = path.join(dir, './index.vue')
|
||||
let templatePath = path.join(__dirname, '../src/pages/Doc/Template.vue')
|
||||
let content = fs.readFileSync(filePath, 'utf-8')
|
||||
let title = /(^|\n\r)\s*#\s+([^\n\r]+)/g.exec(content)
|
||||
if (title && title[2]) {
|
||||
addRouter(dirName, routerList, title[2])
|
||||
}
|
||||
let result = md.render(content)
|
||||
let template = fs.readFileSync(templatePath, 'utf-8')
|
||||
let doc = template.replace('$$$$', result)
|
||||
let doc = transformMdToVue(content)
|
||||
fs.writeFileSync(destPath, doc)
|
||||
}
|
||||
|
||||
|
||||
33
web/scripts/transformMdToVue.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const hljs = require('highlight.js')
|
||||
const md = require('markdown-it')({
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
highlight: function(str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return (
|
||||
'<pre class="hljs"><code>' +
|
||||
hljs.highlight(str, {
|
||||
language: lang,
|
||||
ignoreIllegals: true
|
||||
}).value +
|
||||
'</code></pre>'
|
||||
)
|
||||
} catch (__) {}
|
||||
}
|
||||
|
||||
return (
|
||||
'<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
|
||||
)
|
||||
}
|
||||
}).use(require('markdown-it-checkbox'))
|
||||
|
||||
const templatePath = path.join(__dirname, '../src/pages/Doc/Template.vue')
|
||||
|
||||
exports.transformMdToVue = (content) => {
|
||||
let result = md.render(content)
|
||||
let template = fs.readFileSync(templatePath, 'utf-8')
|
||||
return template.replace('$$$$', result)
|
||||
}
|
||||
BIN
web/src/assets/.DS_Store
vendored
BIN
web/src/assets/avatar/Think.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |