Compare commits
276 Commits
0.5.11
...
0.6.15-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
731a5a504d | ||
|
|
008e697b74 | ||
|
|
fe1745e779 | ||
|
|
e6ac9be402 | ||
|
|
b427a9ed1b | ||
|
|
a4dc9210b3 | ||
|
|
fb681de1f5 | ||
|
|
3757622521 | ||
|
|
12265be7d4 | ||
|
|
df1aed7e04 | ||
|
|
c32c9d1ba1 | ||
|
|
26d75c9203 | ||
|
|
8d1e9fa8e9 | ||
|
|
ebc99e97af | ||
|
|
efe4aa0ec2 | ||
|
|
3f659af1e1 | ||
|
|
64209a6392 | ||
|
|
8d1e5dd2e9 | ||
|
|
d218122752 | ||
|
|
9af8afca22 | ||
|
|
002ec41ba8 | ||
|
|
ffff257f57 | ||
|
|
41d0b675a0 | ||
|
|
7601d6730d | ||
|
|
f0e9b76bb5 | ||
|
|
4ec33062a4 | ||
|
|
edc63b1293 | ||
|
|
ec83976818 | ||
|
|
393410757b | ||
|
|
f20748744a | ||
|
|
067131f76d | ||
|
|
f689d333f9 | ||
|
|
b3059bb6a3 | ||
|
|
b135f6a61c | ||
|
|
2a8b71497f | ||
|
|
726ebc5e88 | ||
|
|
2c56cd453c | ||
|
|
bf2f9b2697 | ||
|
|
fbd061e8b3 | ||
|
|
680320ba76 | ||
|
|
0d992bd6b1 | ||
|
|
c80eacc5e7 | ||
|
|
6d202b4b7d | ||
|
|
94478fe9f3 | ||
|
|
5745e4567b | ||
|
|
c6a3f4ac7b | ||
|
|
f6609b3050 | ||
|
|
4d69778a50 | ||
|
|
c8dfbd3b87 | ||
|
|
c0aab1e921 | ||
|
|
6efee6e859 | ||
|
|
dd82bf0879 | ||
|
|
2d41b5f9e6 | ||
|
|
7e7b6fae9d | ||
|
|
29020daaf0 | ||
|
|
6bcead7784 | ||
|
|
9625129691 | ||
|
|
765d0ee212 | ||
|
|
099d4cd78e | ||
|
|
d9a6981df4 | ||
|
|
bbf424c6d2 | ||
|
|
5cde3b76fe | ||
|
|
75af742053 | ||
|
|
69e192ea9d | ||
|
|
5a32c1d99d | ||
|
|
e81e0a5512 | ||
|
|
2fef76c55c | ||
|
|
49735950f2 | ||
|
|
c005455144 | ||
|
|
ca4afb5440 | ||
|
|
885647cedf | ||
|
|
dc8efbe3ef | ||
|
|
888b8e725a | ||
|
|
15d65db19d | ||
|
|
2ce0d4cd11 | ||
|
|
13f3c2c20c | ||
|
|
d707329526 | ||
|
|
69faa8bb3e | ||
|
|
86b184d5c1 | ||
|
|
2b6263acb4 | ||
|
|
187c940e56 | ||
|
|
4b59bec01c | ||
|
|
9427ee550c | ||
|
|
14975e117c | ||
|
|
d6254c0cc2 | ||
|
|
4a81ce9cc2 | ||
|
|
d17191c890 | ||
|
|
ff56fe3e68 | ||
|
|
6bdcec0fca | ||
|
|
27885aabe7 | ||
|
|
e345037f9b | ||
|
|
da5290e649 | ||
|
|
516676b484 | ||
|
|
20a8934da9 | ||
|
|
0075c44b29 | ||
|
|
ec7a8cdd43 | ||
|
|
1629bb7ccf | ||
|
|
35bff6ab57 | ||
|
|
ef9d8b0ea4 | ||
|
|
5d4c5703bb | ||
|
|
11bb519db8 | ||
|
|
1952280003 | ||
|
|
c845a0b7fa | ||
|
|
f1748e7e42 | ||
|
|
49063d257b | ||
|
|
803c83ac4f | ||
|
|
0bdf9f3add | ||
|
|
10ed3d4f7c | ||
|
|
8fc7f7d32c | ||
|
|
27a0efa4e0 | ||
|
|
7d227e901a | ||
|
|
080d7489e7 | ||
|
|
b11bd5a7ef | ||
|
|
80ae38d295 | ||
|
|
a4b7915196 | ||
|
|
4d4f1b993e | ||
|
|
b814bd35ca | ||
|
|
c57361a360 | ||
|
|
f8c2a62bd6 | ||
|
|
59241717f5 | ||
|
|
e90509cac9 | ||
|
|
8a8cc26c1d | ||
|
|
1d443b9f94 | ||
|
|
60a4f443a7 | ||
|
|
72c2540dcc | ||
|
|
18cec3b75a | ||
|
|
d4aae5268e | ||
|
|
99ca0738d4 | ||
|
|
408ca6d711 | ||
|
|
99dcb55397 | ||
|
|
e11b6647b8 | ||
|
|
78f5d4ec88 | ||
|
|
20eba7b29b | ||
|
|
4c9698a147 | ||
|
|
55da8eac83 | ||
|
|
17ea049393 | ||
|
|
d2e468287d | ||
|
|
ad6085890e | ||
|
|
cd4f1b1bd8 | ||
|
|
a2acf810cb | ||
|
|
14bd7c3705 | ||
|
|
5d0c9dcab1 | ||
|
|
def2f02eea | ||
|
|
a74a60c22d | ||
|
|
74d37f2cbc | ||
|
|
6ca9a116c2 | ||
|
|
8e59677623 | ||
|
|
d06f57a5dc | ||
|
|
07be48d342 | ||
|
|
5a5c7702f5 | ||
|
|
7e1a43143d | ||
|
|
5ff9137745 | ||
|
|
8e30d4a94f | ||
|
|
4b2ebb2e1c | ||
|
|
8c79ffd723 | ||
|
|
839c79405f | ||
|
|
d645adfd37 | ||
|
|
1b1551c6e3 | ||
|
|
78638dc291 | ||
|
|
e972924143 | ||
|
|
8871d8727b | ||
|
|
55945a1a2c | ||
|
|
d5e4044fb2 | ||
|
|
4ee458c509 | ||
|
|
a76ec0dad8 | ||
|
|
f2b247e85c | ||
|
|
3aed09a8c3 | ||
|
|
17b7a023ba | ||
|
|
10cb36829f | ||
|
|
90a55bb995 | ||
|
|
08a37d8971 | ||
|
|
89983f9f47 | ||
|
|
b71a80e383 | ||
|
|
2bf146816b | ||
|
|
daf9888da4 | ||
|
|
b42cee7a2f | ||
|
|
9ecb199608 | ||
|
|
adccef5699 | ||
|
|
94230f8ec6 | ||
|
|
a161661c6b | ||
|
|
08b971cd9a | ||
|
|
b64c8f132b | ||
|
|
d6181591c5 | ||
|
|
e093cb1741 | ||
|
|
a63a92c423 | ||
|
|
2c4f065626 | ||
|
|
ae5d4dd2a6 | ||
|
|
e574883e5f | ||
|
|
1bf60c49c7 | ||
|
|
ddc173cf84 | ||
|
|
2c71c7d102 | ||
|
|
f6cb08bdaa | ||
|
|
05728de21b | ||
|
|
b51027f641 | ||
|
|
ea4fdf8290 | ||
|
|
55796b4e39 | ||
|
|
e534c3138d | ||
|
|
e185abd223 | ||
|
|
3b73f72866 | ||
|
|
0db2f47133 | ||
|
|
80c7ec0fac | ||
|
|
780ce363de | ||
|
|
eaa8929457 | ||
|
|
ad0f62a5ac | ||
|
|
31f21ce013 | ||
|
|
7a20ce2f79 | ||
|
|
b98e7a97ec | ||
|
|
0bb50b3371 | ||
|
|
a03091b28c | ||
|
|
6c33984b8c | ||
|
|
28a4be0631 | ||
|
|
b8a3be7a62 | ||
|
|
259d4028f3 | ||
|
|
fb1251afc1 | ||
|
|
06e3fd428a | ||
|
|
a48e52f1f4 | ||
|
|
f8a345d8de | ||
|
|
d900c2f122 | ||
|
|
443a714549 | ||
|
|
74618a8a2b | ||
|
|
a25bd4c556 | ||
|
|
efe205ae70 | ||
|
|
a0d7473b1f | ||
|
|
7821781f20 | ||
|
|
b90d4ddf8a | ||
|
|
314562c167 | ||
|
|
5f0a9a7ce2 | ||
|
|
98e27841ad | ||
|
|
1b6467728c | ||
|
|
4bb349b2df | ||
|
|
b262336f08 | ||
|
|
2b59087461 | ||
|
|
66c9805efc | ||
|
|
710128901a | ||
|
|
61be0f7ac4 | ||
|
|
7289b3a0ad | ||
|
|
25243e2053 | ||
|
|
060a448cd5 | ||
|
|
bdb6078df6 | ||
|
|
749a4d0e81 | ||
|
|
1749705694 | ||
|
|
eec736be4d | ||
|
|
ffdf53941a | ||
|
|
5676e952f3 | ||
|
|
e049ee6260 | ||
|
|
f1355c9d2a | ||
|
|
d696e0fdc1 | ||
|
|
c8d2f284fd | ||
|
|
aa8ecd4f60 | ||
|
|
2323fe9bc0 | ||
|
|
b9a0b16fc8 | ||
|
|
b9c340afbf | ||
|
|
ee98d7128b | ||
|
|
d37febe306 | ||
|
|
5de97f05b3 | ||
|
|
662447bc69 | ||
|
|
51c1c46287 | ||
|
|
3b03d9798b | ||
|
|
17fbef810c | ||
|
|
e798975a9f | ||
|
|
2af65e322b | ||
|
|
23f09f9b4d | ||
|
|
d69668a488 | ||
|
|
5353888965 | ||
|
|
b33fd1908a | ||
|
|
97583ffcba | ||
|
|
360eca620e | ||
|
|
6d0682e821 | ||
|
|
834bdf37c3 | ||
|
|
e3d31f69bf | ||
|
|
ac55415de1 | ||
|
|
4d91be5be6 | ||
|
|
ee467cb155 | ||
|
|
6281f2360c | ||
|
|
cd8af6ae06 | ||
|
|
45cc199d7f |
86
README.md
@@ -11,13 +11,13 @@
|
||||
|
||||
本项目包含两部分:
|
||||
|
||||
1.一个js思维导图库,不依赖任何框架,你可以使用它来快速完成Web思维导图产品的开发。
|
||||
1.一个 js 思维导图库,不依赖任何框架,你可以使用它来快速完成 Web 思维导图产品的开发。
|
||||
|
||||
开发文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)
|
||||
开发文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)。
|
||||
|
||||
2.一个Web思维导图,基于思维导图库、Vue2.x、ElementUI开发,可以操作电脑本地文件,所以你可以直接把它当做一个在线版思维导图应用使用,如果觉得github的响应速度慢,你也可以部署到你的服务器上。
|
||||
2.一个 Web 思维导图,基于思维导图库、Vue2.x、ElementUI 开发,可以操作电脑本地文件,所以你可以直接把它当做一个在线版思维导图应用使用,如果觉得 github 的响应速度慢,你也可以部署到你的服务器上。
|
||||
|
||||
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
|
||||
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)。
|
||||
|
||||
另外也提供了客户端可供下载使用,支持`Windows`、`Mac`及`Linux`,下载地址:
|
||||
|
||||
@@ -28,18 +28,15 @@ Github:[releases](https://github.com/wanglin2/mind-map/releases)。
|
||||
# 特性
|
||||
|
||||
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
|
||||
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴、鱼骨图六种结构
|
||||
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴(横向、竖向)、鱼骨图等结构
|
||||
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
|
||||
- [x] 支持快捷键
|
||||
- [x] 节点内容支持图片、图标、超链接、备注、标签、概要
|
||||
- [x] 支持前进后退
|
||||
- [x] 支持拖动、缩放
|
||||
- [x] 支持右键和Ctrl+左键两种多选方式
|
||||
- [x] 支持节点自由拖拽、拖拽调整
|
||||
- [x] 支持多种节点形状
|
||||
- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`,支持从`json`、`xmind`、`markdown`导入
|
||||
- [x] 支持小地图、支持水印
|
||||
- [x] 支持关联线
|
||||
- [x] 节点内容支持文本(普通文本、富文本)、图片、图标、超链接、备注、标签、概要
|
||||
- [x] 节点支持拖拽(拖拽移动、自由调整)、多种节点形状,支持使用 DDM 完全自定义节点内容
|
||||
- [x] 支持画布拖动、缩放
|
||||
- [x] 支持鼠标按键拖动选择和Ctrl+左键两种多选节点方式
|
||||
- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`、`xmind`,支持从`json`、`xmind`、`markdown`导入
|
||||
- [x] 支持快捷键、前进后退、关联线、搜索替换、小地图、水印
|
||||
- [x] 提供丰富的配置,满足各种场景各种使用习惯
|
||||
|
||||
# 安装
|
||||
|
||||
@@ -86,17 +83,74 @@ const mindMap = new MindMap({
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
[MIT](./LICENSE)
|
||||
|
||||
# 微信交流群
|
||||
|
||||
<img src="./qrcode.jpg" style="width: 300px" />
|
||||
|
||||
如果已过期,可以微信添加`wanglinguanfang`拉你入群。
|
||||
|
||||
# 请作者喝杯咖啡
|
||||
|
||||
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡哟~
|
||||
|
||||
> 厚椰乳一盒 + 纯牛奶半盒 + 冰块 + 咖啡液 = 生椰拿铁 yyds
|
||||
|
||||
> 转账请备注【思维导图】。你的头像和名字将会出现在下面和[文档页面](https://wanglin2.github.io/mind-map/#/doc/zh/introduction/%E8%AF%B7%E4%BD%9C%E8%80%85%E5%96%9D%E6%9D%AF%E5%92%96%E5%95%A1)
|
||||
|
||||
<p>
|
||||
<img src="./web/src/assets/img/alipay.jpg" style="width: 300px" />
|
||||
<img src="./web/src/assets/img/wechat.jpg" style="width: 300px" />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/Think.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>Think</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/志斌.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>志斌</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/小土渣的宇宙.jpeg" style="width: 50px;height: 50px;" />
|
||||
<span>小土渣的宇宙</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/qp.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>qp</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/ZXR.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>ZXR</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/花儿朵朵.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>花儿朵朵</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/suka.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>suka</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/Chris.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>Chris</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/水车.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>水车</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>仓鼠</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/千帆.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>千帆</span>
|
||||
</span>
|
||||
<span>
|
||||
<img src="./web/src/assets/avatar/才镇.jpg" style="width: 50px;height: 50px;" />
|
||||
<span>才镇</span>
|
||||
</span>
|
||||
</p>
|
||||
70
index.html
BIN
qrcode.jpg
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@@ -1,28 +1,46 @@
|
||||
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 MiniMap from './src/plugins/MiniMap.js'
|
||||
import Watermark from './src/plugins/Watermark.js'
|
||||
import KeyboardNavigation from './src/plugins/KeyboardNavigation.js'
|
||||
import ExportXMind from './src/plugins/ExportXMind.js'
|
||||
import ExportPDF from './src/plugins/ExportPDF.js'
|
||||
import Export from './src/plugins/Export.js'
|
||||
import Drag from './src/plugins/Drag.js'
|
||||
import Select from './src/plugins/Select.js'
|
||||
import AssociativeLine from './src/plugins/AssociativeLine'
|
||||
import RichText from './src/plugins/RichText'
|
||||
import NodeImgAdjust from './src/plugins/NodeImgAdjust.js'
|
||||
import TouchEvent from './src/plugins/TouchEvent.js'
|
||||
import Search from './src/plugins/Search.js'
|
||||
import Painter from './src/plugins/Painter.js'
|
||||
import xmind from './src/parse/xmind.js'
|
||||
import markdown from './src/parse/markdown.js'
|
||||
import icons from './src/svg/icons.js'
|
||||
import * as constants from './src/constants/constant.js'
|
||||
import themes from './src/themes/index.js'
|
||||
import * as defaultTheme from './src/themes/default.js'
|
||||
|
||||
MindMap.xmind = xmind
|
||||
MindMap.markdown = markdown
|
||||
MindMap.iconList = icons.nodeIconList
|
||||
MindMap.constants = constants
|
||||
MindMap.themes = themes
|
||||
MindMap.defaultTheme = defaultTheme
|
||||
|
||||
MindMap
|
||||
.usePlugin(MiniMap)
|
||||
.usePlugin(Watermark)
|
||||
.usePlugin(Drag)
|
||||
.usePlugin(KeyboardNavigation)
|
||||
.usePlugin(ExportXMind)
|
||||
.usePlugin(ExportPDF)
|
||||
.usePlugin(Export)
|
||||
.usePlugin(Select)
|
||||
.usePlugin(AssociativeLine)
|
||||
.usePlugin(RichText)
|
||||
.usePlugin(TouchEvent)
|
||||
.usePlugin(NodeImgAdjust)
|
||||
.usePlugin(Search)
|
||||
.usePlugin(Painter)
|
||||
|
||||
export default MindMap
|
||||
@@ -1,129 +1,24 @@
|
||||
import View from './src/View'
|
||||
import Event from './src/Event'
|
||||
import Render from './src/Render'
|
||||
import View from './src/core/view/View'
|
||||
import Event from './src/core/event/Event'
|
||||
import Render from './src/core/render/Render'
|
||||
import merge from 'deepmerge'
|
||||
import theme from './src/themes'
|
||||
import Style from './src/Style'
|
||||
import KeyCommand from './src/KeyCommand'
|
||||
import Command from './src/Command'
|
||||
import BatchExecution from './src/BatchExecution'
|
||||
import { layoutValueList, CONSTANTS } from './src/utils/constant'
|
||||
import Style from './src/core/render/node/Style'
|
||||
import KeyCommand from './src/core/command/KeyCommand'
|
||||
import Command from './src/core/command/Command'
|
||||
import BatchExecution from './src/utils/BatchExecution'
|
||||
import {
|
||||
layoutValueList,
|
||||
CONSTANTS,
|
||||
commonCaches,
|
||||
ERROR_TYPES
|
||||
} from './src/constants/constant'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import { simpleDeepClone } from './src/utils'
|
||||
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
|
||||
|
||||
// 默认选项配置
|
||||
const defaultOpt = {
|
||||
// 是否只读
|
||||
readonly: false,
|
||||
// 布局
|
||||
layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
|
||||
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
|
||||
fishboneDeg: 45,
|
||||
// 主题
|
||||
theme: 'default', // 内置主题:default(默认主题)
|
||||
// 主题配置,会和所选择的主题进行合并
|
||||
themeConfig: {},
|
||||
// 放大缩小的增量比例
|
||||
scaleRatio: 0.1,
|
||||
// 最多显示几个标签
|
||||
maxTag: 5,
|
||||
// 导出图片时的内边距
|
||||
exportPadding: 20,
|
||||
// 展开收缩按钮尺寸
|
||||
expandBtnSize: 20,
|
||||
// 节点里图片和文字的间距
|
||||
imgTextMargin: 5,
|
||||
// 节点里各种文字信息的间距,如图标和文字的间距
|
||||
textContentMargin: 2,
|
||||
// 多选节点时鼠标移动到边缘时的画布移动偏移量
|
||||
selectTranslateStep: 3,
|
||||
// 多选节点时鼠标移动距边缘多少距离时开始偏移
|
||||
selectTranslateLimit: 20,
|
||||
// 自定义节点备注内容显示
|
||||
customNoteContentShow: null,
|
||||
/*
|
||||
{
|
||||
show(){},
|
||||
hide(){}
|
||||
}
|
||||
*/
|
||||
// 是否开启节点自由拖拽
|
||||
enableFreeDrag: false,
|
||||
// 水印配置
|
||||
watermarkConfig: {
|
||||
text: '',
|
||||
lineSpacing: 100,
|
||||
textSpacing: 100,
|
||||
angle: 30,
|
||||
textStyle: {
|
||||
color: '#999',
|
||||
opacity: 0.5,
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
// 达到该宽度文本自动换行
|
||||
textAutoWrapWidth: 500,
|
||||
// 自定义鼠标滚轮事件处理
|
||||
// 可以传一个函数,回调参数为事件对象
|
||||
customHandleMousewheel: null,
|
||||
// 鼠标滚动的行为,如果customHandleMousewheel传了自定义函数,这个属性不生效
|
||||
mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM,// zoom(放大缩小)、move(上下移动)
|
||||
// 当mousewheelAction设为move时,可以通过该属性控制鼠标滚动一下视图移动的步长,单位px
|
||||
mousewheelMoveStep: 100,
|
||||
// 默认插入的二级节点的文字
|
||||
defaultInsertSecondLevelNodeText: '二级节点',
|
||||
// 默认插入的二级以下节点的文字
|
||||
defaultInsertBelowSecondLevelNodeText: '分支主题',
|
||||
// 展开收起按钮的颜色
|
||||
expandBtnStyle: {
|
||||
color: '#808080',
|
||||
fill: '#fff'
|
||||
},
|
||||
// 自定义展开收起按钮的图标
|
||||
expandBtnIcon: {
|
||||
open: '',// svg字符串
|
||||
close: ''
|
||||
},
|
||||
// 是否只有当鼠标在画布内才响应快捷键事件
|
||||
enableShortcutOnlyWhenMouseInSvg: true,
|
||||
// 是否开启节点动画过渡
|
||||
enableNodeTransitionMove: true,
|
||||
// 如果开启节点动画过渡,可以通过该属性设置过渡的时间,单位ms
|
||||
nodeTransitionMoveDuration: 300,
|
||||
// 初始根节点的位置
|
||||
initRootNodePosition: null,
|
||||
// 导出png、svg、pdf时的图形内边距
|
||||
exportPaddingX: 10,
|
||||
exportPaddingY: 10,
|
||||
// 节点文本编辑框的z-index
|
||||
nodeTextEditZIndex: 3000,
|
||||
// 节点备注浮层的z-index
|
||||
nodeNoteTooltipZIndex: 3000,
|
||||
// 是否在点击了画布外的区域时结束节点文本的编辑状态
|
||||
isEndNodeTextEditOnClickOuter: true,
|
||||
// 最大历史记录数
|
||||
maxHistoryCount: 1000,
|
||||
// 是否一直显示节点的展开收起按钮,默认为鼠标移上去和激活时才显示
|
||||
alwaysShowExpandBtn: false,
|
||||
// 扩展节点可插入的图标
|
||||
iconList: [
|
||||
// {
|
||||
// name: '',// 分组名称
|
||||
// type: '',// 分组的值
|
||||
// list: [// 分组下的图标列表
|
||||
// {
|
||||
// name: '',// 图标名称
|
||||
// icon:''// 图标,可以传svg或图片
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
],
|
||||
// 节点最大缓存数量
|
||||
maxNodeCacheCount: 1000,
|
||||
// 关联线默认文字
|
||||
defaultAssociativeLineText: '关联'
|
||||
}
|
||||
import { simpleDeepClone, getType } from './src/utils'
|
||||
import defaultTheme, {
|
||||
checkIsNodeSizeIndependenceConfig
|
||||
} from './src/themes/default'
|
||||
import { defaultOpt } from './src/constants/defaultOptions'
|
||||
|
||||
// 思维导图
|
||||
class MindMap {
|
||||
@@ -134,22 +29,24 @@ class MindMap {
|
||||
|
||||
// 容器元素
|
||||
this.el = this.opt.el
|
||||
if (!this.el) throw new Error('缺少容器元素el')
|
||||
this.elRect = this.el.getBoundingClientRect()
|
||||
|
||||
// 画布宽高
|
||||
this.width = this.elRect.width
|
||||
this.height = this.elRect.height
|
||||
if (this.width <= 0 || this.height <= 0) throw new Error('容器元素el的宽高不能为0')
|
||||
|
||||
// 画布
|
||||
this.svg = SVG().addTo(this.el).size(this.width, this.height)
|
||||
this.draw = this.svg.group()
|
||||
|
||||
// 节点id
|
||||
this.uid = 1
|
||||
|
||||
// 初始化主题
|
||||
this.initTheme()
|
||||
|
||||
// 初始化缓存数据
|
||||
this.initCache()
|
||||
|
||||
// 事件类
|
||||
this.event = new Event({
|
||||
mindMap: this
|
||||
@@ -180,7 +77,7 @@ class MindMap {
|
||||
this.batchExecution = new BatchExecution()
|
||||
|
||||
// 注册插件
|
||||
MindMap.pluginList.forEach((plugin) => {
|
||||
MindMap.pluginList.forEach(plugin => {
|
||||
this.initPlugin(plugin)
|
||||
})
|
||||
|
||||
@@ -193,6 +90,8 @@ class MindMap {
|
||||
|
||||
// 配置参数处理
|
||||
handleOpt(opt) {
|
||||
// 深拷贝一份节点数据
|
||||
opt.data = simpleDeepClone(opt.data || {})
|
||||
// 检查布局配置
|
||||
if (!layoutValueList.includes(opt.layout)) {
|
||||
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
@@ -244,6 +143,23 @@ class MindMap {
|
||||
this.event.off(event, fn)
|
||||
}
|
||||
|
||||
// 初始化缓存数据
|
||||
initCache() {
|
||||
Object.keys(commonCaches).forEach(key => {
|
||||
let type = getType(commonCaches[key])
|
||||
let value = ''
|
||||
switch (type) {
|
||||
case 'Boolean':
|
||||
value = false
|
||||
break
|
||||
default:
|
||||
value = null
|
||||
break
|
||||
}
|
||||
commonCaches[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
// 设置主题
|
||||
initTheme() {
|
||||
// 合并主题配置
|
||||
@@ -257,6 +173,7 @@ class MindMap {
|
||||
this.renderer.clearAllActive()
|
||||
this.opt.theme = theme
|
||||
this.render(null, CONSTANTS.CHANGE_THEME)
|
||||
this.emit('view_theme_change', theme)
|
||||
}
|
||||
|
||||
// 获取当前主题
|
||||
@@ -306,7 +223,7 @@ class MindMap {
|
||||
this.opt.layout = layout
|
||||
this.view.reset()
|
||||
this.renderer.setLayout()
|
||||
this.render()
|
||||
this.render(null, CONSTANTS.CHANGE_LAYOUT)
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
@@ -350,7 +267,7 @@ class MindMap {
|
||||
|
||||
// 获取思维导图数据,节点树、主题、布局等
|
||||
getData(withConfig) {
|
||||
let nodeData = this.command.removeDataUid(this.command.getCopyData())
|
||||
let nodeData = this.command.getCopyData()
|
||||
let data = {}
|
||||
if (withConfig) {
|
||||
data = {
|
||||
@@ -370,8 +287,12 @@ class MindMap {
|
||||
|
||||
// 导出
|
||||
async export(...args) {
|
||||
let result = await this.doExport.export(...args)
|
||||
return result
|
||||
try {
|
||||
let result = await this.doExport.export(...args)
|
||||
return result
|
||||
} catch (error) {
|
||||
this.mindMap.opt.errorHandler(ERROR_TYPES.EXPORT_ERROR, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换位置
|
||||
@@ -409,9 +330,9 @@ class MindMap {
|
||||
// 获取变换后的位置尺寸信息,其实是getBoundingClientRect方法的包装方法
|
||||
const rect = draw.rbox()
|
||||
// 内边距
|
||||
rect.width += paddingX
|
||||
rect.height += paddingY
|
||||
draw.translate(paddingX / 2, paddingY / 2)
|
||||
rect.width += paddingX * 2
|
||||
rect.height += paddingY * 2
|
||||
draw.translate(paddingX, paddingY)
|
||||
// 将svg设置为实际内容的宽高
|
||||
svg.size(rect.width, rect.height)
|
||||
// 把实际内容变换
|
||||
@@ -419,7 +340,11 @@ class MindMap {
|
||||
// 克隆一份数据
|
||||
let clone = svg.clone()
|
||||
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
|
||||
if ((rect.width > origWidth || rect.height > origHeight) && this.watermark && this.watermark.hasWatermark()) {
|
||||
if (
|
||||
(rect.width > origWidth || rect.height > origHeight) &&
|
||||
this.watermark &&
|
||||
this.watermark.hasWatermark()
|
||||
) {
|
||||
this.width = rect.width
|
||||
this.height = rect.height
|
||||
this.watermark.draw()
|
||||
@@ -476,6 +401,21 @@ class MindMap {
|
||||
pluginOpt: plugin.pluginOpt
|
||||
})
|
||||
}
|
||||
|
||||
// 销毁
|
||||
destroy() {
|
||||
// 移除插件
|
||||
;[...MindMap.pluginList].forEach(plugin => {
|
||||
this[plugin.instanceName] = null
|
||||
})
|
||||
// 解绑事件
|
||||
this.event.unbind()
|
||||
// 移除画布节点
|
||||
this.svg.remove()
|
||||
// 去除给容器元素设置的背景样式
|
||||
Style.removeBackgroundStyle(this.el)
|
||||
this.el = null
|
||||
}
|
||||
}
|
||||
|
||||
// 插件列表
|
||||
@@ -485,8 +425,8 @@ MindMap.usePlugin = (plugin, opt = {}) => {
|
||||
MindMap.pluginList.push(plugin)
|
||||
return MindMap
|
||||
}
|
||||
MindMap.hasPlugin = (plugin) => {
|
||||
return MindMap.pluginList.findIndex((item) => {
|
||||
MindMap.hasPlugin = plugin => {
|
||||
return MindMap.pluginList.findIndex(item => {
|
||||
return item === plugin
|
||||
})
|
||||
}
|
||||
|
||||
61
simple-mind-map/package-lock.json
generated
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.4.7",
|
||||
"version": "0.6.13",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.4.7",
|
||||
"version": "0.6.13",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.0.16",
|
||||
"canvg": "^3.0.7",
|
||||
"deepmerge": "^1.5.2",
|
||||
"dom-to-image-more": "^3.1.6",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mdast-util-from-markdown": "^1.3.0",
|
||||
@@ -160,7 +159,8 @@
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz",
|
||||
"integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw=="
|
||||
"integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "2.0.6",
|
||||
@@ -255,6 +255,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
@@ -305,6 +306,7 @@
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
|
||||
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
@@ -381,6 +383,7 @@
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz",
|
||||
"integrity": "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
@@ -409,6 +412,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
@@ -514,6 +518,11 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-to-image-more": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.1.6.tgz",
|
||||
"integrity": "sha512-VMO0jNme32T06mWtkOC9QXfj+1npoJxkaTFW0DCwBLguwBKMjqwndiDANxDnbZ0kvNEecwxkv0Zmgdr96cGtAA=="
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
|
||||
@@ -935,6 +944,7 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
@@ -1813,7 +1823,8 @@
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
@@ -1908,6 +1919,7 @@
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
@@ -1982,6 +1994,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
@@ -2075,6 +2088,7 @@
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz",
|
||||
"integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
@@ -2127,6 +2141,7 @@
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
@@ -2135,6 +2150,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
@@ -2199,6 +2215,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
@@ -2386,7 +2403,8 @@
|
||||
"@types/raf": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz",
|
||||
"integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw=="
|
||||
"integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==",
|
||||
"optional": true
|
||||
},
|
||||
"@types/unist": {
|
||||
"version": "2.0.6",
|
||||
@@ -2453,7 +2471,8 @@
|
||||
"base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
@@ -2489,6 +2508,7 @@
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
|
||||
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
@@ -2544,7 +2564,8 @@
|
||||
"core-js": {
|
||||
"version": "3.27.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz",
|
||||
"integrity": "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww=="
|
||||
"integrity": "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==",
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.3",
|
||||
@@ -2566,6 +2587,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
@@ -2638,6 +2660,11 @@
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"dom-to-image-more": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.1.6.tgz",
|
||||
"integrity": "sha512-VMO0jNme32T06mWtkOC9QXfj+1npoJxkaTFW0DCwBLguwBKMjqwndiDANxDnbZ0kvNEecwxkv0Zmgdr96cGtAA=="
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
|
||||
@@ -2956,6 +2983,7 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
@@ -3511,7 +3539,8 @@
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"optional": true
|
||||
},
|
||||
"prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
@@ -3576,6 +3605,7 @@
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
@@ -3630,7 +3660,8 @@
|
||||
"rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"optional": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
@@ -3691,7 +3722,8 @@
|
||||
"stackblur-canvas": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz",
|
||||
"integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ=="
|
||||
"integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==",
|
||||
"optional": true
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
@@ -3728,12 +3760,14 @@
|
||||
"svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"optional": true
|
||||
},
|
||||
"text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
@@ -3785,6 +3819,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.5.11",
|
||||
"version": "0.6.15-fix.1",
|
||||
"description": "一个简单的web在线思维导图",
|
||||
"authors": [
|
||||
{
|
||||
@@ -25,10 +25,9 @@
|
||||
"__main": "./dist/simpleMindMap.umd.min.js",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.0.16",
|
||||
"canvg": "^3.0.7",
|
||||
"deepmerge": "^1.5.2",
|
||||
"dom-to-image-more": "^3.1.6",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mdast-util-from-markdown": "^1.3.0",
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
|
||||
// 视图操作类
|
||||
class View {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
this.opt = opt
|
||||
this.mindMap = this.opt.mindMap
|
||||
this.scale = 1
|
||||
this.sx = 0
|
||||
this.sy = 0
|
||||
this.x = 0
|
||||
this.y = 0
|
||||
this.firstDrag = true
|
||||
this.setTransformData(this.mindMap.opt.viewData)
|
||||
this.bind()
|
||||
}
|
||||
|
||||
// 绑定
|
||||
bind() {
|
||||
// 快捷键
|
||||
this.mindMap.keyCommand.addShortcut('Control+=', () => {
|
||||
this.enlarge()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Control+-', () => {
|
||||
this.narrow()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Control+Enter', () => {
|
||||
this.reset()
|
||||
})
|
||||
this.mindMap.svg.on('dblclick', () => {
|
||||
this.reset()
|
||||
})
|
||||
// 拖动视图
|
||||
this.mindMap.event.on('mousedown', () => {
|
||||
this.sx = this.x
|
||||
this.sy = this.y
|
||||
})
|
||||
this.mindMap.event.on('drag', (e, event) => {
|
||||
if (e.ctrlKey) {
|
||||
// 按住ctrl键拖动为多选
|
||||
return
|
||||
}
|
||||
if (this.firstDrag) {
|
||||
this.firstDrag = false
|
||||
// 清除激活节点
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
}
|
||||
}
|
||||
this.x = this.sx + event.mousemoveOffset.x
|
||||
this.y = this.sy + event.mousemoveOffset.y
|
||||
this.transform()
|
||||
})
|
||||
this.mindMap.event.on('mouseup', () => {
|
||||
this.firstDrag = true
|
||||
})
|
||||
// 放大缩小视图
|
||||
this.mindMap.event.on('mousewheel', (e, dir) => {
|
||||
if (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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前变换状态数据
|
||||
getTransformData() {
|
||||
return {
|
||||
transform: this.mindMap.draw.transform(),
|
||||
state: {
|
||||
scale: this.scale,
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
sx: this.sx,
|
||||
sy: this.sy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动态设置变换状态数据
|
||||
setTransformData(viewData) {
|
||||
if (viewData) {
|
||||
Object.keys(viewData.state).forEach(prop => {
|
||||
this[prop] = viewData.state[prop]
|
||||
})
|
||||
this.mindMap.draw.transform({
|
||||
...viewData.transform
|
||||
})
|
||||
this.mindMap.emit('view_data_change', this.getTransformData())
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
}
|
||||
|
||||
// 平移x,y方向
|
||||
translateXY(x, y) {
|
||||
this.x += x
|
||||
this.y += y
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移x方向
|
||||
translateX(step) {
|
||||
this.x += step
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移x方式到
|
||||
translateXTo(x) {
|
||||
this.x = x
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移y方向
|
||||
translateY(step) {
|
||||
this.y += step
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移y方向到
|
||||
translateYTo(y) {
|
||||
this.y = y
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 应用变换
|
||||
transform() {
|
||||
this.mindMap.draw.transform({
|
||||
scale: this.scale,
|
||||
// origin: 'center center',
|
||||
translate: [this.x, this.y]
|
||||
})
|
||||
this.mindMap.emit('view_data_change', this.getTransformData())
|
||||
}
|
||||
|
||||
// 恢复
|
||||
reset() {
|
||||
let scaleChange = this.scale !== 1
|
||||
this.scale = 1
|
||||
this.x = 0
|
||||
this.y = 0
|
||||
this.transform()
|
||||
if (scaleChange) {
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
}
|
||||
|
||||
// 缩小
|
||||
narrow() {
|
||||
if (this.scale - this.mindMap.opt.scaleRatio > 0.1) {
|
||||
this.scale -= this.mindMap.opt.scaleRatio
|
||||
} else {
|
||||
this.scale = 0.1
|
||||
}
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
|
||||
// 放大
|
||||
enlarge() {
|
||||
this.scale += this.mindMap.opt.scaleRatio
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
|
||||
// 设置缩放
|
||||
setScale(scale) {
|
||||
this.scale = scale
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
}
|
||||
|
||||
export default View
|
||||
@@ -27,136 +27,169 @@ export const themeList = [
|
||||
{
|
||||
name: '默认',
|
||||
value: 'default',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '暗色2',
|
||||
value: 'dark2',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '天清绿',
|
||||
value: 'skyGreen',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '脑图经典2',
|
||||
value: 'classic2',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '脑图经典3',
|
||||
value: 'classic3',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '经典绿',
|
||||
value: 'classicGreen',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '经典蓝',
|
||||
value: 'classicBlue',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '天空蓝',
|
||||
value: 'blueSky',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '脑残粉',
|
||||
value: 'brainImpairedPink',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '暗色',
|
||||
value: 'dark',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '泥土黄',
|
||||
value: 'earthYellow',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '清新绿',
|
||||
value: 'freshGreen',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '清新红',
|
||||
value: 'freshRed',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '浪漫紫',
|
||||
value: 'romanticPurple',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '粉红葡萄',
|
||||
value: 'pinkGrape',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '薄荷',
|
||||
value: 'mint',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '金色vip',
|
||||
value: 'gold',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '活力橙',
|
||||
value: 'vitalityOrange',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '绿叶',
|
||||
value: 'greenLeaf',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '脑图经典',
|
||||
value: 'classic',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '脑图经典4',
|
||||
value: 'classic4',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '小黄人',
|
||||
value: 'minions',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '简约黑',
|
||||
value: 'simpleBlack',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '课程绿',
|
||||
value: 'courseGreen',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '咖啡',
|
||||
value: 'coffee',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '红色精神',
|
||||
value: 'redSpirit',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '黑色幽默',
|
||||
value: 'blackHumour',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '深夜办公室',
|
||||
value: 'lateNightOffice',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '黑金',
|
||||
value: 'blackGold',
|
||||
dark: true
|
||||
},
|
||||
{
|
||||
name: '牛油果',
|
||||
value: 'avocado',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '秋天',
|
||||
value: 'autumn',
|
||||
dark: false
|
||||
},
|
||||
{
|
||||
name: '橙汁',
|
||||
value: 'orangeJuice',
|
||||
dark: true
|
||||
}
|
||||
]
|
||||
|
||||
// 常量
|
||||
export const CONSTANTS = {
|
||||
CHANGE_THEME: 'changeTheme',
|
||||
CHANGE_LAYOUT: 'changeLayout',
|
||||
SET_DATA: 'setData',
|
||||
TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode',
|
||||
MODE: {
|
||||
@@ -170,7 +203,8 @@ export const CONSTANTS = {
|
||||
CATALOG_ORGANIZATION: 'catalogOrganization',
|
||||
TIMELINE: 'timeline',
|
||||
TIMELINE2: 'timeline2',
|
||||
FISHBONE: 'fishbone'
|
||||
FISHBONE: 'fishbone',
|
||||
VERTICAL_TIMELINE: 'verticalTimeline'
|
||||
},
|
||||
DIR: {
|
||||
UP: 'up',
|
||||
@@ -206,9 +240,15 @@ export const CONSTANTS = {
|
||||
BOTTOM: 'bottom',
|
||||
CENTER: 'center'
|
||||
},
|
||||
TIMELINE_DIR: {
|
||||
LAYOUT_GROW_DIR: {
|
||||
LEFT: 'left',
|
||||
TOP: 'top',
|
||||
RIGHT: 'right',
|
||||
BOTTOM: 'bottom'
|
||||
},
|
||||
PASTE_TYPE: {
|
||||
CLIP_BOARD: 'clipBoard',
|
||||
CANVAS: 'canvas'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +286,10 @@ export const layoutList = [
|
||||
name: '时间轴2',
|
||||
value: CONSTANTS.LAYOUT.TIMELINE2,
|
||||
},
|
||||
{
|
||||
name: '竖向时间轴',
|
||||
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
|
||||
},
|
||||
{
|
||||
name: '鱼骨图',
|
||||
value: CONSTANTS.LAYOUT.FISHBONE,
|
||||
@@ -258,5 +302,44 @@ export const layoutValueList = [
|
||||
CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
|
||||
CONSTANTS.LAYOUT.TIMELINE,
|
||||
CONSTANTS.LAYOUT.TIMELINE2,
|
||||
CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
|
||||
CONSTANTS.LAYOUT.FISHBONE
|
||||
]
|
||||
]
|
||||
|
||||
// 节点数据中非样式的字段
|
||||
export const nodeDataNoStylePropList = [
|
||||
'text',
|
||||
'image',
|
||||
'imageTitle',
|
||||
'imageSize',
|
||||
'icon',
|
||||
'tag',
|
||||
'hyperlink',
|
||||
'hyperlinkTitle',
|
||||
'note',
|
||||
'expand',
|
||||
'isActive',
|
||||
'generalization',
|
||||
'richText',
|
||||
'resetRichText',
|
||||
'uid',
|
||||
'activeStyle',
|
||||
'associativeLineTargets',
|
||||
'associativeLineTargetControlOffsets'
|
||||
]
|
||||
|
||||
// 数据缓存
|
||||
export const commonCaches = {
|
||||
measureCustomNodeContentSizeEl: null,
|
||||
measureRichtextNodeTextSizeEl: null
|
||||
}
|
||||
|
||||
// 错误类型
|
||||
export const ERROR_TYPES = {
|
||||
READ_CLIPBOARD_ERROR: 'read_clipboard_error',
|
||||
PARSE_PASTE_DATA_ERROR: 'parse_paste_data_error',
|
||||
CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR: 'custom_handle_clipboard_text_error',
|
||||
LOAD_CLIPBOARD_IMAGE_ERROR: 'load_clipboard_image_error',
|
||||
BEFORE_TEXT_EDIT_ERROR: 'before_text_edit_error',
|
||||
EXPORT_ERROR: 'export_error'
|
||||
}
|
||||
159
simple-mind-map/src/constants/defaultOptions.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import { CONSTANTS } from './constant'
|
||||
|
||||
// 默认选项配置
|
||||
export const defaultOpt = {
|
||||
// 是否只读
|
||||
readonly: false,
|
||||
// 布局
|
||||
layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
|
||||
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
|
||||
fishboneDeg: 45,
|
||||
// 主题
|
||||
theme: 'default', // 内置主题:default(默认主题)
|
||||
// 主题配置,会和所选择的主题进行合并
|
||||
themeConfig: {},
|
||||
// 放大缩小的增量比例
|
||||
scaleRatio: 0.2,
|
||||
// 鼠标缩放是否以鼠标当前位置为中心点,否则以画布中心点
|
||||
mouseScaleCenterUseMousePosition: true,
|
||||
// 最多显示几个标签
|
||||
maxTag: 5,
|
||||
// 展开收缩按钮尺寸
|
||||
expandBtnSize: 20,
|
||||
// 节点里图片和文字的间距
|
||||
imgTextMargin: 5,
|
||||
// 节点里各种文字信息的间距,如图标和文字的间距
|
||||
textContentMargin: 2,
|
||||
// 多选节点时鼠标移动到边缘时的画布移动偏移量
|
||||
selectTranslateStep: 3,
|
||||
// 多选节点时鼠标移动距边缘多少距离时开始偏移
|
||||
selectTranslateLimit: 20,
|
||||
// 自定义节点备注内容显示
|
||||
customNoteContentShow: null,
|
||||
/*
|
||||
{
|
||||
show(){},
|
||||
hide(){}
|
||||
}
|
||||
*/
|
||||
// 是否开启节点自由拖拽
|
||||
enableFreeDrag: false,
|
||||
// 水印配置
|
||||
watermarkConfig: {
|
||||
text: '',
|
||||
lineSpacing: 100,
|
||||
textSpacing: 100,
|
||||
angle: 30,
|
||||
textStyle: {
|
||||
color: '#999',
|
||||
opacity: 0.5,
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
// 达到该宽度文本自动换行
|
||||
textAutoWrapWidth: 500,
|
||||
// 自定义鼠标滚轮事件处理
|
||||
// 可以传一个函数,回调参数为事件对象
|
||||
customHandleMousewheel: null,
|
||||
// 鼠标滚动的行为,如果customHandleMousewheel传了自定义函数,这个属性不生效
|
||||
mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM, // zoom(放大缩小)、move(上下移动)
|
||||
// 当mousewheelAction设为move时,可以通过该属性控制鼠标滚动一下视图移动的步长,单位px
|
||||
mousewheelMoveStep: 100,
|
||||
// 当mousewheelAction设为zoom时,默认向前滚动是缩小,向后滚动是放大,如果该属性设为true,那么会反过来
|
||||
mousewheelZoomActionReverse: false,
|
||||
// 默认插入的二级节点的文字
|
||||
defaultInsertSecondLevelNodeText: '二级节点',
|
||||
// 默认插入的二级以下节点的文字
|
||||
defaultInsertBelowSecondLevelNodeText: '分支主题',
|
||||
// 展开收起按钮的颜色
|
||||
expandBtnStyle: {
|
||||
color: '#808080',
|
||||
fill: '#fff'
|
||||
},
|
||||
// 自定义展开收起按钮的图标
|
||||
expandBtnIcon: {
|
||||
open: '', // svg字符串
|
||||
close: ''
|
||||
},
|
||||
// 是否只有当鼠标在画布内才响应快捷键事件
|
||||
enableShortcutOnlyWhenMouseInSvg: true,
|
||||
// 初始根节点的位置
|
||||
initRootNodePosition: null,
|
||||
// 导出png、svg、pdf时的图形内边距,注意是单侧内边距
|
||||
exportPaddingX: 10,
|
||||
exportPaddingY: 10,
|
||||
// 节点文本编辑框的z-index
|
||||
nodeTextEditZIndex: 3000,
|
||||
// 节点备注浮层的z-index
|
||||
nodeNoteTooltipZIndex: 3000,
|
||||
// 是否在点击了画布外的区域时结束节点文本的编辑状态
|
||||
isEndNodeTextEditOnClickOuter: true,
|
||||
// 最大历史记录数
|
||||
maxHistoryCount: 1000,
|
||||
// 是否一直显示节点的展开收起按钮,默认为鼠标移上去和激活时才显示
|
||||
alwaysShowExpandBtn: false,
|
||||
// 扩展节点可插入的图标
|
||||
iconList: [
|
||||
// {
|
||||
// name: '',// 分组名称
|
||||
// type: '',// 分组的值
|
||||
// list: [// 分组下的图标列表
|
||||
// {
|
||||
// name: '',// 图标名称
|
||||
// icon:''// 图标,可以传svg或图片
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
],
|
||||
// 节点最大缓存数量
|
||||
maxNodeCacheCount: 1000,
|
||||
// 关联线默认文字
|
||||
defaultAssociativeLineText: '关联',
|
||||
// 思维导图适应画布大小时的内边距
|
||||
fitPadding: 50,
|
||||
// 是否开启按住ctrl键多选节点功能
|
||||
enableCtrlKeyNodeSelection: true,
|
||||
// 设置为左键多选节点,右键拖动画布
|
||||
useLeftKeySelectionRightKeyDrag: false,
|
||||
// 节点即将进入编辑前的回调方法,如果该方法返回true以外的值,那么将取消编辑,函数可以返回一个值,或一个Promise,回调参数为节点实例
|
||||
beforeTextEdit: null,
|
||||
// 是否开启自定义节点内容
|
||||
isUseCustomNodeContent: false,
|
||||
// 自定义返回节点内容的方法
|
||||
customCreateNodeContent: null,
|
||||
// 指定内部一些元素(节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素)添加到的位置,默认添加到document.body下
|
||||
customInnerElsAppendTo: null,
|
||||
// 拖拽元素时,指示元素新位置的块的最大高度
|
||||
nodeDragPlaceholderMaxSize: 20,
|
||||
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
|
||||
// 开启该特性后,需要给你的输入框绑定keydown事件,并禁止冒泡
|
||||
enableAutoEnterTextEditWhenKeydown: false,
|
||||
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
|
||||
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
|
||||
richTextEditFakeInPlace: false,
|
||||
// 自定义对剪贴板文本的处理。当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片,默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据,如果你想处理其他思维导图的数据,比如processon、zhixi等,那么可以传递一个函数,接受当前剪贴板中的文本为参数,返回处理后的数据,可以返回两种类型:
|
||||
/*
|
||||
1.返回一个纯文本,那么会直接以该文本创建一个子节点
|
||||
|
||||
2.返回一个节点对象,格式如下:
|
||||
{
|
||||
// 代表是simple-mind-map格式的数据
|
||||
simpleMindMap: true,
|
||||
// 节点数据,同simple-mind-map节点数据格式
|
||||
data: {
|
||||
data: {
|
||||
text: ''
|
||||
},
|
||||
children: []
|
||||
}
|
||||
}
|
||||
*/
|
||||
// 如果你的处理逻辑存在异步逻辑,也可以返回一个promise
|
||||
customHandleClipboardText: null,
|
||||
// 禁止鼠标滚轮缩放,你仍旧可以使用api进行缩放
|
||||
disableMouseWheelZoom: false,
|
||||
// 错误处理函数
|
||||
errorHandler: (code, error) => {
|
||||
console.error(code, error)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { copyRenderTree, simpleDeepClone, nextTick } from './utils'
|
||||
import { copyRenderTree, simpleDeepClone, nextTick } from '../../utils'
|
||||
|
||||
// 命令类
|
||||
class Command {
|
||||
@@ -89,7 +89,7 @@ class Command {
|
||||
this.history.shift()
|
||||
}
|
||||
this.activeHistoryIndex = this.history.length - 1
|
||||
this.mindMap.emit('data_change', this.removeDataUid(data))
|
||||
this.mindMap.emit('data_change', data)
|
||||
this.mindMap.emit(
|
||||
'back_forward',
|
||||
this.activeHistoryIndex,
|
||||
@@ -110,7 +110,7 @@ class Command {
|
||||
this.history.length
|
||||
)
|
||||
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.mindMap.emit('data_change', this.removeDataUid(data))
|
||||
this.mindMap.emit('data_change', data)
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ class Command {
|
||||
this.activeHistoryIndex += step
|
||||
this.mindMap.emit('back_forward', this.activeHistoryIndex, this.history.length)
|
||||
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.mindMap.emit('data_change', this.removeDataUid(data))
|
||||
this.mindMap.emit('data_change', data)
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { keyMap } from './utils/keyMap'
|
||||
import { keyMap } from './keyMap'
|
||||
// 快捷按键、命令处理类
|
||||
export default class KeyCommand {
|
||||
// 构造函数
|
||||
@@ -57,8 +57,11 @@ export default class KeyCommand {
|
||||
}
|
||||
Object.keys(this.shortcutMap).forEach(key => {
|
||||
if (this.checkKey(e, key)) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
// 粘贴事件不组织,因为要监听paste事件
|
||||
if (!this.checkKey(e, 'Control+v')) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
this.shortcutMap[key].forEach(fn => {
|
||||
fn()
|
||||
})
|
||||
@@ -105,6 +108,11 @@ export default class KeyCommand {
|
||||
return arr
|
||||
}
|
||||
|
||||
// 判断是否按下了组合键
|
||||
hasCombinationKey(e) {
|
||||
return e.ctrlKey || e.metaKey || e.altKey || e.shiftKey
|
||||
}
|
||||
|
||||
// 获取快捷键对应的键值数组
|
||||
getKeyCodeArr(key) {
|
||||
let keyArr = key.split(/\s*\+\s*/)
|
||||
@@ -1,5 +1,5 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
import { CONSTANTS } from '../../constants/constant'
|
||||
|
||||
// 事件类
|
||||
class Event extends EventEmitter {
|
||||
@@ -9,6 +9,8 @@ class Event extends EventEmitter {
|
||||
this.opt = opt
|
||||
this.mindMap = opt.mindMap
|
||||
this.isLeftMousedown = false
|
||||
this.isRightMousedown = false
|
||||
this.isMiddleMousedown = false
|
||||
this.mousedownPos = {
|
||||
x: 0,
|
||||
y: 0
|
||||
@@ -89,6 +91,10 @@ class Event extends EventEmitter {
|
||||
// 鼠标左键
|
||||
if (e.which === 1) {
|
||||
this.isLeftMousedown = true
|
||||
} else if (e.which === 3) {
|
||||
this.isRightMousedown = true
|
||||
} else if (e.which === 2) {
|
||||
this.isMiddleMousedown = true
|
||||
}
|
||||
this.mousedownPos.x = e.clientX
|
||||
this.mousedownPos.y = e.clientY
|
||||
@@ -97,12 +103,18 @@ class Event extends EventEmitter {
|
||||
|
||||
// 鼠标移动事件
|
||||
onMousemove(e) {
|
||||
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
|
||||
this.mousemovePos.x = e.clientX
|
||||
this.mousemovePos.y = e.clientY
|
||||
this.mousemoveOffset.x = e.clientX - this.mousedownPos.x
|
||||
this.mousemoveOffset.y = e.clientY - this.mousedownPos.y
|
||||
this.emit('mousemove', e, this)
|
||||
if (this.isLeftMousedown) {
|
||||
if (
|
||||
this.isMiddleMousedown ||
|
||||
(useLeftKeySelectionRightKeyDrag
|
||||
? this.isRightMousedown
|
||||
: this.isLeftMousedown)
|
||||
) {
|
||||
e.preventDefault()
|
||||
this.emit('drag', e, this)
|
||||
}
|
||||
@@ -111,6 +123,8 @@ class Event extends EventEmitter {
|
||||
// 鼠标松开事件
|
||||
onMouseup(e) {
|
||||
this.isLeftMousedown = false
|
||||
this.isRightMousedown = false
|
||||
this.isMiddleMousedown = false
|
||||
this.emit('mouseup', e, this)
|
||||
}
|
||||
|
||||
@@ -131,7 +145,13 @@ class Event extends EventEmitter {
|
||||
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)
|
||||
// 判断是否是触控板
|
||||
let isTouchPad = false
|
||||
// mac、windows
|
||||
if (e.wheelDeltaY === e.deltaY * -3 || Math.abs(e.wheelDeltaY) <= 10) {
|
||||
isTouchPad = true
|
||||
}
|
||||
this.emit('mousewheel', e, dir, this, isTouchPad)
|
||||
}
|
||||
|
||||
// 鼠标右键菜单事件
|
||||
@@ -1,15 +1,23 @@
|
||||
import merge from 'deepmerge'
|
||||
import LogicalStructure from './layouts/LogicalStructure'
|
||||
import MindMap from './layouts/MindMap'
|
||||
import CatalogOrganization from './layouts/CatalogOrganization'
|
||||
import OrganizationStructure from './layouts/OrganizationStructure'
|
||||
import Timeline from './layouts/Timeline'
|
||||
import Fishbone from './layouts/Fishbone'
|
||||
import LogicalStructure from '../../layouts/LogicalStructure'
|
||||
import MindMap from '../../layouts/MindMap'
|
||||
import CatalogOrganization from '../../layouts/CatalogOrganization'
|
||||
import OrganizationStructure from '../../layouts/OrganizationStructure'
|
||||
import Timeline from '../../layouts/Timeline'
|
||||
import VerticalTimeline from '../../layouts/VerticalTimeline'
|
||||
import Fishbone from '../../layouts/Fishbone'
|
||||
import TextEdit from './TextEdit'
|
||||
import { copyNodeTree, simpleDeepClone, walk } from './utils'
|
||||
import { shapeList } from './Shape'
|
||||
import { lineStyleProps } from './themes/default'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
import {
|
||||
copyNodeTree,
|
||||
simpleDeepClone,
|
||||
walk,
|
||||
bfsWalk,
|
||||
loadImage,
|
||||
isUndef
|
||||
} from '../../utils'
|
||||
import { shapeList } from './node/Shape'
|
||||
import { lineStyleProps } from '../../themes/default'
|
||||
import { CONSTANTS, ERROR_TYPES } from '../../constants/constant'
|
||||
|
||||
// 布局列表
|
||||
const layouts = {
|
||||
@@ -25,8 +33,10 @@ const layouts = {
|
||||
[CONSTANTS.LAYOUT.TIMELINE]: Timeline,
|
||||
// 时间轴2
|
||||
[CONSTANTS.LAYOUT.TIMELINE2]: Timeline,
|
||||
// 竖向时间轴
|
||||
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline,
|
||||
// 鱼骨图
|
||||
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone,
|
||||
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone
|
||||
}
|
||||
|
||||
// 渲染
|
||||
@@ -57,6 +67,12 @@ class Render {
|
||||
this.root = null
|
||||
// 文本编辑框,需要再bindEvent之前实例化,否则单击事件只能触发隐藏文本编辑框,而无法保存文本修改
|
||||
this.textEdit = new TextEdit(this)
|
||||
// 当前复制的数据
|
||||
this.lastBeingCopyData = null
|
||||
this.beingCopyData = null
|
||||
this.beingPasteText = ''
|
||||
this.beingPasteImgSize = 0
|
||||
this.currentBeingPasteType = ''
|
||||
// 布局
|
||||
this.setLayout()
|
||||
// 绑定事件
|
||||
@@ -79,12 +95,27 @@ class Render {
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
// 点击事件
|
||||
this.mindMap.on('draw_click', () => {
|
||||
this.mindMap.on('draw_click', e => {
|
||||
// 清除激活状态
|
||||
if (this.activeNodeList.length > 0) {
|
||||
let isTrueClick = true
|
||||
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
|
||||
if (useLeftKeySelectionRightKeyDrag) {
|
||||
let mousedownPos = this.mindMap.event.mousedownPos
|
||||
isTrueClick =
|
||||
Math.abs(e.clientX - mousedownPos.x) <= 5 &&
|
||||
Math.abs(e.clientY - mousedownPos.y) <= 5
|
||||
}
|
||||
if (isTrueClick && this.activeNodeList.length > 0) {
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
}
|
||||
})
|
||||
// let timer = null
|
||||
// this.mindMap.on('view_data_change', () => {
|
||||
// clearTimeout(timer)
|
||||
// timer = setTimeout(() => {
|
||||
// this.render()
|
||||
// }, 300)
|
||||
// })
|
||||
}
|
||||
|
||||
// 注册命令
|
||||
@@ -126,9 +157,12 @@ class Render {
|
||||
// 剪切节点
|
||||
this.cutNode = this.cutNode.bind(this)
|
||||
this.mindMap.command.add('CUT_NODE', this.cutNode)
|
||||
// 修改节点样式
|
||||
// 修改节点单个样式
|
||||
this.setNodeStyle = this.setNodeStyle.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_STYLE', this.setNodeStyle)
|
||||
// 修改节点多个样式
|
||||
this.setNodeStyles = this.setNodeStyles.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_STYLES', this.setNodeStyles)
|
||||
// 切换节点是否激活
|
||||
this.setNodeActive = this.setNodeActive.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_ACTIVE', this.setNodeActive)
|
||||
@@ -186,6 +220,9 @@ class Render {
|
||||
// 设置节点形状
|
||||
this.setNodeShape = this.setNodeShape.bind(this)
|
||||
this.mindMap.command.add('SET_NODE_SHAPE', this.setNodeShape)
|
||||
// 定位节点
|
||||
this.goTargetNode = this.goTargetNode.bind(this)
|
||||
this.mindMap.command.add('GO_TARGET_NODE', this.goTargetNode)
|
||||
}
|
||||
|
||||
// 注册快捷键
|
||||
@@ -203,7 +240,7 @@ class Render {
|
||||
}
|
||||
this.mindMap.keyCommand.addShortcut('Enter', this.insertNodeWrap)
|
||||
// 插入概要
|
||||
this.mindMap.keyCommand.addShortcut('Control+s', this.addGeneralization)
|
||||
this.mindMap.keyCommand.addShortcut('Control+g', this.addGeneralization)
|
||||
// 展开/收起节点
|
||||
this.toggleActiveExpand = this.toggleActiveExpand.bind(this)
|
||||
this.mindMap.keyCommand.addShortcut('/', this.toggleActiveExpand)
|
||||
@@ -230,6 +267,13 @@ class Render {
|
||||
// 下移节点
|
||||
this.mindMap.keyCommand.addShortcut('Control+Down', this.downNode)
|
||||
// 复制节点、剪切节点、粘贴节点的快捷键需开发者自行注册实现,可参考demo
|
||||
this.copy = this.copy.bind(this)
|
||||
this.mindMap.keyCommand.addShortcut('Control+c', this.copy)
|
||||
this.mindMap.keyCommand.addShortcut('Control+v', () => {
|
||||
this.onPaste()
|
||||
})
|
||||
this.cut = this.cut.bind(this)
|
||||
this.mindMap.keyCommand.addShortcut('Control+x', this.cut)
|
||||
}
|
||||
|
||||
// 开启文字编辑,会禁用回车键和删除键相关快捷键防止冲突
|
||||
@@ -250,7 +294,6 @@ class Render {
|
||||
|
||||
// 渲染
|
||||
render(callback = () => {}, source) {
|
||||
let t = Date.now()
|
||||
// 如果当前还没有渲染完毕,不再触发渲染
|
||||
if (this.isRendering) {
|
||||
// 等待当前渲染完毕后再进行一次渲染
|
||||
@@ -270,7 +313,7 @@ class Render {
|
||||
// 计算布局
|
||||
this.layout.doLayout(root => {
|
||||
// 删除本次渲染时不再需要的节点
|
||||
Object.keys(this.lastNodeCache).forEach((uid) => {
|
||||
Object.keys(this.lastNodeCache).forEach(uid => {
|
||||
if (!this.nodeCache[uid]) {
|
||||
this.lastNodeCache[uid].destroy()
|
||||
if (this.lastNodeCache[uid].parent) {
|
||||
@@ -281,7 +324,7 @@ class Render {
|
||||
// 更新根节点
|
||||
this.root = root
|
||||
// 渲染节点
|
||||
const onEnd = () => {
|
||||
this.root.render(() => {
|
||||
this.isRendering = false
|
||||
this.mindMap.emit('node_tree_render_end')
|
||||
callback && callback()
|
||||
@@ -290,22 +333,13 @@ class Render {
|
||||
this.render(callback, source)
|
||||
} else {
|
||||
// 触发一次保存,因为修改了渲染树的数据
|
||||
if (this.mindMap.richText && [CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)) {
|
||||
if (
|
||||
this.mindMap.richText &&
|
||||
[CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)
|
||||
) {
|
||||
this.mindMap.command.addHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration } =
|
||||
this.mindMap.opt
|
||||
this.root.render(() => {
|
||||
let dur = Date.now() - t
|
||||
if (enableNodeTransitionMove && dur <= nodeTransitionMoveDuration) {
|
||||
setTimeout(() => {
|
||||
onEnd()
|
||||
}, nodeTransitionMoveDuration - dur);
|
||||
} else {
|
||||
onEnd()
|
||||
}
|
||||
})
|
||||
})
|
||||
this.mindMap.emit('node_active', null, this.activeNodeList)
|
||||
@@ -407,60 +441,95 @@ class Render {
|
||||
// 规范指定节点数据
|
||||
formatAppointNodes(appointNodes) {
|
||||
if (!appointNodes) return []
|
||||
return Array.isArray(appointNodes) ? appointNodes: [appointNodes]
|
||||
return Array.isArray(appointNodes) ? appointNodes : [appointNodes]
|
||||
}
|
||||
|
||||
// 插入同级节点,多个节点只会操作第一个节点
|
||||
insertNode(openEdit = true, appointNodes = [], appointData = null) {
|
||||
insertNode(
|
||||
openEdit = true,
|
||||
appointNodes = [],
|
||||
appointData = null,
|
||||
appointChildren = []
|
||||
) {
|
||||
appointNodes = this.formatAppointNodes(appointNodes)
|
||||
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
|
||||
return
|
||||
}
|
||||
let { defaultInsertSecondLevelNodeText, defaultInsertBelowSecondLevelNodeText } = this.mindMap.opt
|
||||
this.textEdit.hideEditTextBox()
|
||||
let {
|
||||
defaultInsertSecondLevelNodeText,
|
||||
defaultInsertBelowSecondLevelNodeText
|
||||
} = this.mindMap.opt
|
||||
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
|
||||
let first = list[0]
|
||||
if (first.isGeneralization) {
|
||||
return
|
||||
}
|
||||
if (first.isRoot) {
|
||||
this.insertChildNode(openEdit, appointNodes, appointData)
|
||||
} else {
|
||||
let text = first.layerIndex === 1 ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
|
||||
let text =
|
||||
first.layerIndex === 1
|
||||
? defaultInsertSecondLevelNodeText
|
||||
: defaultInsertBelowSecondLevelNodeText
|
||||
if (first.layerIndex === 1) {
|
||||
first.parent.destroy()
|
||||
}
|
||||
let index = this.getNodeIndex(first)
|
||||
let isRichText = !!this.mindMap.richText
|
||||
first.parent.nodeData.children.splice(index + 1, 0, {
|
||||
inserting: openEdit,
|
||||
data: {
|
||||
text: text,
|
||||
expand: true,
|
||||
richText: isRichText,
|
||||
resetRichText: isRichText,
|
||||
...(appointData || {})
|
||||
},
|
||||
children: []
|
||||
children: [...appointChildren]
|
||||
})
|
||||
this.mindMap.render()
|
||||
}
|
||||
}
|
||||
|
||||
// 插入子节点
|
||||
insertChildNode(openEdit = true, appointNodes = [], appointData = null) {
|
||||
insertChildNode(
|
||||
openEdit = true,
|
||||
appointNodes = [],
|
||||
appointData = null,
|
||||
appointChildren = []
|
||||
) {
|
||||
appointNodes = this.formatAppointNodes(appointNodes)
|
||||
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
|
||||
return
|
||||
}
|
||||
let { defaultInsertSecondLevelNodeText, defaultInsertBelowSecondLevelNodeText } = this.mindMap.opt
|
||||
this.textEdit.hideEditTextBox()
|
||||
let {
|
||||
defaultInsertSecondLevelNodeText,
|
||||
defaultInsertBelowSecondLevelNodeText
|
||||
} = this.mindMap.opt
|
||||
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
|
||||
list.forEach(node => {
|
||||
if (node.isGeneralization) {
|
||||
return
|
||||
}
|
||||
if (!node.nodeData.children) {
|
||||
node.nodeData.children = []
|
||||
}
|
||||
let text = node.isRoot ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
|
||||
let text = node.isRoot
|
||||
? defaultInsertSecondLevelNodeText
|
||||
: defaultInsertBelowSecondLevelNodeText
|
||||
let isRichText = !!this.mindMap.richText
|
||||
node.nodeData.children.push({
|
||||
inserting: openEdit,
|
||||
data: {
|
||||
text: text,
|
||||
expand: true,
|
||||
richText: isRichText,
|
||||
resetRichText: isRichText,
|
||||
...(appointData || {})
|
||||
},
|
||||
children: []
|
||||
children: [...appointChildren]
|
||||
})
|
||||
// 插入子节点时自动展开子节点
|
||||
node.nodeData.data.expand = true
|
||||
@@ -525,13 +594,159 @@ class Render {
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 复制节点
|
||||
copy() {
|
||||
this.beingCopyData = this.copyNode()
|
||||
this.setCoptyDataToClipboard(this.beingCopyData)
|
||||
}
|
||||
|
||||
// 剪切节点
|
||||
cut() {
|
||||
this.mindMap.execCommand('CUT_NODE', copyData => {
|
||||
this.beingCopyData = copyData
|
||||
this.setCoptyDataToClipboard(copyData)
|
||||
})
|
||||
}
|
||||
|
||||
// 将粘贴或剪切的数据设置到用户剪切板中
|
||||
setCoptyDataToClipboard(data) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(
|
||||
JSON.stringify({
|
||||
simpleMindMap: true,
|
||||
data
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 粘贴节点
|
||||
paste() {
|
||||
if (this.beingCopyData) {
|
||||
this.mindMap.execCommand('PASTE_NODE', this.beingCopyData)
|
||||
}
|
||||
}
|
||||
|
||||
// 粘贴事件
|
||||
async onPaste() {
|
||||
const { errorHandler } = this.mindMap.opt
|
||||
// 读取剪贴板的文字和图片
|
||||
let text = null
|
||||
let img = null
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
text = await navigator.clipboard.readText()
|
||||
const items = await navigator.clipboard.read()
|
||||
if (items && items.length > 0) {
|
||||
for (const clipboardItem of items) {
|
||||
for (const type of clipboardItem.types) {
|
||||
if (/^image\//.test(type)) {
|
||||
img = await clipboardItem.getType(type)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errorHandler(ERROR_TYPES.READ_CLIPBOARD_ERROR, error)
|
||||
}
|
||||
}
|
||||
// 检查剪切板数据是否有变化
|
||||
// 通过图片大小来判断图片是否发生变化,可能是不准确的,但是目前没有其他好方法
|
||||
const imgSize = img ? img.size : 0
|
||||
if (this.beingPasteText !== text || this.beingPasteImgSize !== imgSize) {
|
||||
this.currentBeingPasteType = CONSTANTS.PASTE_TYPE.CLIP_BOARD
|
||||
this.beingPasteText = text
|
||||
this.beingPasteImgSize = imgSize
|
||||
}
|
||||
// 检查要粘贴的节点数据是否有变化,节点优先级高于剪切板
|
||||
if (this.lastBeingCopyData !== this.beingCopyData) {
|
||||
this.lastBeingCopyData = this.beingCopyData
|
||||
this.currentBeingPasteType = CONSTANTS.PASTE_TYPE.CANVAS
|
||||
}
|
||||
// 粘贴剪切板的数据
|
||||
if (this.currentBeingPasteType === CONSTANTS.PASTE_TYPE.CLIP_BOARD) {
|
||||
// 存在文本,则创建子节点
|
||||
if (text) {
|
||||
// 判断粘贴的是否是simple-mind-map的数据
|
||||
let smmData = null
|
||||
let useDefault = true
|
||||
// 用户自定义处理
|
||||
if (this.mindMap.opt.customHandleClipboardText) {
|
||||
try {
|
||||
const res = await this.mindMap.opt.customHandleClipboardText(text)
|
||||
if (!isUndef(res)) {
|
||||
useDefault = false
|
||||
if (typeof res === 'object' && res.simpleMindMap) {
|
||||
smmData = res.data
|
||||
} else {
|
||||
text = String(res)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errorHandler(ERROR_TYPES.CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR, error)
|
||||
}
|
||||
}
|
||||
// 默认处理
|
||||
if (useDefault) {
|
||||
try {
|
||||
const parsedData = JSON.parse(text)
|
||||
if (parsedData && parsedData.simpleMindMap) {
|
||||
smmData = parsedData.data
|
||||
}
|
||||
} catch (error) {
|
||||
errorHandler(ERROR_TYPES.PARSE_PASTE_DATA_ERROR, error)
|
||||
}
|
||||
}
|
||||
if (smmData) {
|
||||
this.mindMap.execCommand(
|
||||
'INSERT_CHILD_NODE',
|
||||
false,
|
||||
[],
|
||||
{
|
||||
...smmData.data
|
||||
},
|
||||
[...smmData.children]
|
||||
)
|
||||
} else {
|
||||
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
|
||||
text
|
||||
})
|
||||
}
|
||||
}
|
||||
// 存在图片,则添加到当前激活节点
|
||||
if (img) {
|
||||
try {
|
||||
let imgData = await loadImage(img)
|
||||
if (this.activeNodeList.length > 0) {
|
||||
this.activeNodeList.forEach(node => {
|
||||
this.mindMap.execCommand('SET_NODE_IMAGE', node, {
|
||||
url: imgData.url,
|
||||
title: '',
|
||||
width: imgData.size.width,
|
||||
height: imgData.size.height
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
errorHandler(ERROR_TYPES.LOAD_CLIPBOARD_IMAGE_ERROR, error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 粘贴节点数据
|
||||
this.paste()
|
||||
}
|
||||
}
|
||||
|
||||
// 将节点移动到另一个节点的前面
|
||||
insertBefore(node, exist) {
|
||||
if (node.isRoot) {
|
||||
return
|
||||
}
|
||||
// 如果是二级节点变成了下级节点,或是下级节点变成了二级节点,节点样式需要更新
|
||||
let nodeLayerChanged = (node.layerIndex === 1 && exist.layerIndex !== 1) || (node.layerIndex !== 1 && exist.layerIndex === 1)
|
||||
let nodeLayerChanged =
|
||||
(node.layerIndex === 1 && exist.layerIndex !== 1) ||
|
||||
(node.layerIndex !== 1 && exist.layerIndex === 1)
|
||||
// 移动节点
|
||||
let nodeParent = node.parent
|
||||
let nodeBorthers = nodeParent.children
|
||||
@@ -568,7 +783,9 @@ class Render {
|
||||
return
|
||||
}
|
||||
// 如果是二级节点变成了下级节点,或是下级节点变成了二级节点,节点样式需要更新
|
||||
let nodeLayerChanged = (node.layerIndex === 1 && exist.layerIndex !== 1) || (node.layerIndex !== 1 && exist.layerIndex === 1)
|
||||
let nodeLayerChanged =
|
||||
(node.layerIndex === 1 && exist.layerIndex !== 1) ||
|
||||
(node.layerIndex !== 1 && exist.layerIndex === 1)
|
||||
// 移动节点
|
||||
let nodeParent = node.parent
|
||||
let nodeBorthers = nodeParent.children
|
||||
@@ -608,7 +825,7 @@ class Render {
|
||||
}
|
||||
let isAppointNodes = appointNodes.length > 0
|
||||
let list = isAppointNodes ? appointNodes : this.activeNodeList
|
||||
let root = list.find((node) => {
|
||||
let root = list.find(node => {
|
||||
return node.isRoot
|
||||
})
|
||||
if (root) {
|
||||
@@ -681,11 +898,11 @@ class Render {
|
||||
if (node.isRoot) {
|
||||
return
|
||||
}
|
||||
let copyData = copyNodeTree({}, node, false, true)
|
||||
// let copyData = copyNodeTree({}, node, false, true)
|
||||
this.removeActiveNode(node)
|
||||
this.removeOneNode(node)
|
||||
this.mindMap.emit('node_active', null, this.activeNodeList)
|
||||
toNode.nodeData.children.push(copyData)
|
||||
toNode.nodeData.children.push(node.nodeData)
|
||||
this.mindMap.render()
|
||||
if (toNode.isRoot) {
|
||||
toNode.destroy()
|
||||
@@ -694,7 +911,7 @@ class Render {
|
||||
|
||||
// 粘贴节点到节点
|
||||
pasteNode(data) {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
if (this.activeNodeList.length <= 0 || !data) {
|
||||
return
|
||||
}
|
||||
this.activeNodeList.forEach(item => {
|
||||
@@ -736,6 +953,42 @@ class Render {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置节点多个样式
|
||||
setNodeStyles(node, style, isActive) {
|
||||
let data = {}
|
||||
if (isActive) {
|
||||
data = {
|
||||
activeStyle: {
|
||||
...(node.nodeData.data.activeStyle || {}),
|
||||
...style
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = style
|
||||
}
|
||||
// 如果开启了富文本,则需要应用到富文本上
|
||||
if (this.mindMap.richText) {
|
||||
let config = this.mindMap.richText.normalStyleToRichTextStyle(style)
|
||||
if (Object.keys(config).length > 0) {
|
||||
this.mindMap.richText.showEditText(node)
|
||||
this.mindMap.richText.formatAllText(config)
|
||||
this.mindMap.richText.hideEditText([node])
|
||||
}
|
||||
}
|
||||
this.setNodeDataRender(node, data)
|
||||
// 更新了连线的样式
|
||||
let props = Object.keys(style)
|
||||
let hasLineStyleProps = false
|
||||
props.forEach(key => {
|
||||
if (lineStyleProps.includes(key)) {
|
||||
hasLineStyleProps = true
|
||||
}
|
||||
})
|
||||
if (hasLineStyleProps) {
|
||||
;(node.parent || node).renderLine(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置节点是否激活
|
||||
setNodeActive(node, active) {
|
||||
this.setNodeData(node, {
|
||||
@@ -849,21 +1102,24 @@ class Render {
|
||||
}
|
||||
|
||||
// 设置节点文本
|
||||
setNodeText(node, text, richText) {
|
||||
setNodeText(node, text, richText, resetRichText) {
|
||||
this.setNodeDataRender(node, {
|
||||
text,
|
||||
richText
|
||||
richText,
|
||||
resetRichText
|
||||
})
|
||||
}
|
||||
|
||||
// 设置节点图片
|
||||
setNodeImage(node, { url, title, width, height }) {
|
||||
setNodeImage(node, data) {
|
||||
const { url, title, width, height, custom = false } = data || { url: '', title: '', width: 0, height: 0, custom: false }
|
||||
this.setNodeDataRender(node, {
|
||||
image: url,
|
||||
imageTitle: title || '',
|
||||
imageSize: {
|
||||
width,
|
||||
height
|
||||
height,
|
||||
custom
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -976,6 +1232,20 @@ class Render {
|
||||
})
|
||||
}
|
||||
|
||||
// 定位到指定节点
|
||||
goTargetNode(node, callback = () => {}) {
|
||||
let uid = typeof node === 'string' ? node : node.nodeData.data.uid
|
||||
if (!uid) return
|
||||
this.expandToNodeUid(uid, () => {
|
||||
let targetNode = this.findNodeByUid(uid)
|
||||
if (targetNode) {
|
||||
targetNode.active()
|
||||
this.moveNodeToCenter(targetNode)
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 更新节点数据
|
||||
setNodeData(node, data) {
|
||||
Object.keys(data).forEach(key => {
|
||||
@@ -984,7 +1254,7 @@ class Render {
|
||||
}
|
||||
|
||||
// 设置节点数据,并判断是否渲染
|
||||
setNodeDataRender(node, data) {
|
||||
setNodeDataRender(node, data, notRender = false) {
|
||||
this.setNodeData(node, data)
|
||||
let changed = node.reRender()
|
||||
if (changed) {
|
||||
@@ -992,7 +1262,9 @@ class Render {
|
||||
// 概要节点
|
||||
node.generalizationBelongNode.updateGeneralization()
|
||||
}
|
||||
this.mindMap.render()
|
||||
if (!notRender) this.mindMap.render()
|
||||
} else {
|
||||
this.mindMap.emit('node_tree_render_end')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1012,6 +1284,44 @@ class Render {
|
||||
this.mindMap.view.translateY(offsetY)
|
||||
this.mindMap.view.setScale(1)
|
||||
}
|
||||
|
||||
// 展开到指定uid的节点
|
||||
expandToNodeUid(uid, callback = () => {}) {
|
||||
let parentsList = []
|
||||
const cache = {}
|
||||
bfsWalk(this.renderTree, (node, parent) => {
|
||||
if (node.data.uid === uid) {
|
||||
parentsList = parent ? [...cache[parent.data.uid], parent] : []
|
||||
return 'stop'
|
||||
} else {
|
||||
cache[node.data.uid] = parent ? [...cache[parent.data.uid], parent] : []
|
||||
}
|
||||
})
|
||||
let needRender = false
|
||||
parentsList.forEach(node => {
|
||||
if (!node.data.expand) {
|
||||
needRender = true
|
||||
node.data.expand = true
|
||||
}
|
||||
})
|
||||
if (needRender) {
|
||||
this.mindMap.render(callback)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
// 根据uid找到对应的节点实例
|
||||
findNodeByUid(uid) {
|
||||
let res = null
|
||||
walk(this.root, null, node => {
|
||||
if (node.nodeData.data.uid === uid) {
|
||||
res = node
|
||||
return true
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
export default Render
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getStrWithBrFromHtml, checkNodeOuter } from './utils'
|
||||
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils'
|
||||
import { ERROR_TYPES } from '../../constants/constant'
|
||||
|
||||
// 节点文字编辑类
|
||||
export default class TextEdit {
|
||||
@@ -54,6 +55,29 @@ export default class TextEdit {
|
||||
this.show(this.renderer.activeNodeList[0])
|
||||
})
|
||||
this.mindMap.on('scale', this.onScale)
|
||||
// // 监听按键事件,判断是否自动进入文本编辑模式
|
||||
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
|
||||
window.addEventListener('keydown', e => {
|
||||
const activeNodeList = this.mindMap.renderer.activeNodeList
|
||||
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
|
||||
const node = activeNodeList[0]
|
||||
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
|
||||
if (node && this.checkIsAutoEnterTextEditKey(e)) {
|
||||
this.show(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否是自动进入文本编模式的按钮
|
||||
checkIsAutoEnterTextEditKey(e) {
|
||||
const keyCode = e.keyCode
|
||||
return (
|
||||
(keyCode === 229 ||
|
||||
(keyCode >= 65 && keyCode <= 90) ||
|
||||
(keyCode >= 48 && keyCode <= 57)) &&
|
||||
!this.mindMap.keyCommand.hasCombinationKey(e)
|
||||
)
|
||||
}
|
||||
|
||||
// 注册临时快捷键
|
||||
@@ -62,26 +86,46 @@ export default class TextEdit {
|
||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Tab', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
show(node) {
|
||||
// isInserting:是否是刚创建的节点
|
||||
async show(node, e, isInserting = false) {
|
||||
// 使用了自定义节点内容那么不响应编辑事件
|
||||
if (node.isUseCustomNodeContent()) {
|
||||
return
|
||||
}
|
||||
let { beforeTextEdit } = this.mindMap.opt
|
||||
if (typeof beforeTextEdit === 'function') {
|
||||
let isShow = false
|
||||
try {
|
||||
isShow = await beforeTextEdit(node, isInserting)
|
||||
} catch (error) {
|
||||
isShow = false
|
||||
this.mindMap.opt.errorHandler(ERROR_TYPES.BEFORE_TEXT_EDIT_ERROR, error)
|
||||
}
|
||||
if (!isShow) return
|
||||
}
|
||||
this.currentNode = node
|
||||
let { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
|
||||
this.mindMap.view.translateXY(offsetLeft, offsetTop)
|
||||
let rect = node._textData.node.node.getBoundingClientRect()
|
||||
if (this.mindMap.richText) {
|
||||
this.mindMap.richText.showEditText(node, rect)
|
||||
this.mindMap.richText.showEditText(node, rect, isInserting)
|
||||
return
|
||||
}
|
||||
this.showEditTextBox(node, rect)
|
||||
this.showEditTextBox(node, rect, isInserting)
|
||||
}
|
||||
|
||||
// 处理画布缩放
|
||||
onScale() {
|
||||
if (!this.currentNode) return
|
||||
if (this.mindMap.richText) {
|
||||
this.mindMap.richText.cacheEditingText = this.mindMap.richText.getEditText()
|
||||
this.mindMap.richText.cacheEditingText =
|
||||
this.mindMap.richText.getEditText()
|
||||
this.mindMap.richText.showTextEdit = false
|
||||
} else {
|
||||
this.cacheEditingText = this.getEditText()
|
||||
@@ -91,7 +135,8 @@ export default class TextEdit {
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
showEditTextBox(node, rect) {
|
||||
showEditTextBox(node, rect, isInserting) {
|
||||
if (this.showTextEdit) return
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
this.registerTmpShortcut()
|
||||
if (!this.textEditNode) {
|
||||
@@ -104,12 +149,24 @@ export default class TextEdit {
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
this.textEditNode.addEventListener('mousedown', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.textEditNode.addEventListener('keydown', e => {
|
||||
if (this.checkIsAutoEnterTextEditKey(e)) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
})
|
||||
const targetNode =
|
||||
this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
let scale = this.mindMap.view.scale
|
||||
let lineHeight = node.style.merge('lineHeight')
|
||||
let fontSize = node.style.merge('fontSize')
|
||||
let textLines = (this.cacheEditingText || node.nodeData.data.text).split(/\n/gim)
|
||||
let textLines = (this.cacheEditingText || node.nodeData.data.text).split(
|
||||
/\n/gim
|
||||
)
|
||||
let isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
|
||||
node.style.domText(this.textEditNode, scale, isMultiLine)
|
||||
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
|
||||
@@ -119,18 +176,36 @@ export default class TextEdit {
|
||||
this.textEditNode.style.left = rect.left + 'px'
|
||||
this.textEditNode.style.top = rect.top + 'px'
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.textEditNode.style.maxWidth = this.mindMap.opt.textAutoWrapWidth * scale + 'px'
|
||||
this.textEditNode.style.maxWidth =
|
||||
this.mindMap.opt.textAutoWrapWidth * scale + 'px'
|
||||
if (isMultiLine && lineHeight !== 1) {
|
||||
this.textEditNode.style.transform = `translateY(${-((lineHeight * fontSize - fontSize) / 2) * scale}px)`
|
||||
this.textEditNode.style.transform = `translateY(${
|
||||
-((lineHeight * fontSize - fontSize) / 2) * scale
|
||||
}px)`
|
||||
}
|
||||
this.showTextEdit = true
|
||||
// 选中文本
|
||||
if (!this.cacheEditingText) {
|
||||
// if (!this.cacheEditingText) {
|
||||
// this.selectNodeText()
|
||||
// }
|
||||
if (isInserting) {
|
||||
this.selectNodeText()
|
||||
} else {
|
||||
this.focus()
|
||||
}
|
||||
this.cacheEditingText = ''
|
||||
}
|
||||
|
||||
// 聚焦
|
||||
focus() {
|
||||
let selection = window.getSelection()
|
||||
let range = document.createRange()
|
||||
range.selectNodeContents(this.textEditNode)
|
||||
range.collapse()
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
|
||||
// 选中文本
|
||||
selectNodeText() {
|
||||
let selection = window.getSelection()
|
||||
@@ -1,12 +1,12 @@
|
||||
import Style from './Style'
|
||||
import Shape from './Shape'
|
||||
import { asyncRun } from './utils'
|
||||
import { G, Rect } from '@svgdotjs/svg.js'
|
||||
import nodeGeneralizationMethods from './utils/nodeGeneralization'
|
||||
import nodeExpandBtnMethods from './utils/nodeExpandBtn'
|
||||
import nodeCommandWrapsMethods from './utils/nodeCommandWraps'
|
||||
import nodeCreateContentsMethods from './utils/nodeCreateContents'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
import { G, ForeignObject, SVG } from '@svgdotjs/svg.js'
|
||||
import nodeGeneralizationMethods from './nodeGeneralization'
|
||||
import nodeExpandBtnMethods from './nodeExpandBtn'
|
||||
import nodeCommandWrapsMethods from './nodeCommandWraps'
|
||||
import nodeCreateContentsMethods from './nodeCreateContents'
|
||||
import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect'
|
||||
import { CONSTANTS } from '../../../constants/constant'
|
||||
|
||||
// 节点类
|
||||
class Node {
|
||||
@@ -59,6 +59,7 @@ class Node {
|
||||
this.group = null
|
||||
this.shapeNode = null // 节点形状节点
|
||||
// 节点内容对象
|
||||
this._customNodeContent = null
|
||||
this._imgData = null
|
||||
this._iconData = null
|
||||
this._textData = null
|
||||
@@ -97,6 +98,8 @@ class Node {
|
||||
this.isMultipleChoice = false
|
||||
// 是否需要重新layout
|
||||
this.needLayout = false
|
||||
// 当前是否是隐藏状态
|
||||
this.isHide = false
|
||||
// 概要相关方法
|
||||
Object.keys(nodeGeneralizationMethods).forEach(item => {
|
||||
this[item] = nodeGeneralizationMethods[item].bind(this)
|
||||
@@ -105,6 +108,10 @@ class Node {
|
||||
Object.keys(nodeExpandBtnMethods).forEach(item => {
|
||||
this[item] = nodeExpandBtnMethods[item].bind(this)
|
||||
})
|
||||
// 展开收起按钮占位元素相关方法
|
||||
Object.keys(nodeExpandBtnPlaceholderRectMethods).forEach(item => {
|
||||
this[item] = nodeExpandBtnPlaceholderRectMethods[item].bind(this)
|
||||
})
|
||||
// 命令的相关方法
|
||||
Object.keys(nodeCommandWrapsMethods).forEach(item => {
|
||||
this[item] = nodeCommandWrapsMethods[item].bind(this)
|
||||
@@ -154,6 +161,13 @@ class Node {
|
||||
|
||||
// 创建节点的各个内容对象数据
|
||||
createNodeData() {
|
||||
// 自定义节点内容
|
||||
let { isUseCustomNodeContent, customCreateNodeContent } = this.mindMap.opt
|
||||
if (isUseCustomNodeContent && customCreateNodeContent) {
|
||||
this._customNodeContent = customCreateNodeContent(this)
|
||||
}
|
||||
// 如果没有返回内容,那么还是使用内置的节点内容
|
||||
if (this._customNodeContent) return
|
||||
this._imgData = this.createImgNode()
|
||||
this._iconData = this.createIconNode()
|
||||
this._textData = this.createTextNode()
|
||||
@@ -176,6 +190,14 @@ class Node {
|
||||
|
||||
// 计算节点尺寸信息
|
||||
getNodeRect() {
|
||||
// 自定义节点内容
|
||||
if (this.isUseCustomNodeContent()) {
|
||||
let rect = this.measureCustomNodeContentSize(this._customNodeContent)
|
||||
return {
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
}
|
||||
// 宽高
|
||||
let imgContentWidth = 0
|
||||
let imgContentHeight = 0
|
||||
@@ -235,9 +257,11 @@ class Node {
|
||||
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
|
||||
this.shapePadding.paddingX = shapePaddingX
|
||||
this.shapePadding.paddingY = shapePaddingY
|
||||
// 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点
|
||||
const borderWidth = this.getBorderWidth()
|
||||
return {
|
||||
width: _width + paddingX * 2 + shapePaddingX * 2,
|
||||
height: _height + paddingY * 2 + margin + shapePaddingY * 2
|
||||
width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth,
|
||||
height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,25 +271,29 @@ class Node {
|
||||
this.group.clear()
|
||||
let { width, height, textContentItemMargin } = this
|
||||
let { paddingY } = this.getPaddingVale()
|
||||
paddingY += this.shapePadding.paddingY
|
||||
const halfBorderWidth = this.getBorderWidth() / 2
|
||||
paddingY += this.shapePadding.paddingY + halfBorderWidth
|
||||
// 节点形状
|
||||
this.shapeNode = this.shapeInstance.createShape()
|
||||
this.shapeNode.addClass('smm-node-shape')
|
||||
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
|
||||
this.group.add(this.shapeNode)
|
||||
this.updateNodeShape()
|
||||
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
this._unVisibleRectRegionNode = new Rect()
|
||||
}
|
||||
this._unVisibleRectRegionNode.fill({
|
||||
color: 'transparent'
|
||||
}).size(this.expandBtnSize, height).x(width).y(0)
|
||||
this.group.add(this._unVisibleRectRegionNode)
|
||||
}
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
// 概要节点添加一个带所属节点id的类名
|
||||
if (this.isGeneralization && this.generalizationBelongNode) {
|
||||
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
|
||||
}
|
||||
// 如果存在自定义节点内容,那么使用自定义节点内容
|
||||
if (this.isUseCustomNodeContent()) {
|
||||
let foreignObject = new ForeignObject()
|
||||
foreignObject.width(width)
|
||||
foreignObject.height(height)
|
||||
foreignObject.add(SVG(this._customNodeContent))
|
||||
this.group.add(foreignObject)
|
||||
return
|
||||
}
|
||||
// 图片节点
|
||||
let imgHeight = 0
|
||||
if (this._imgData) {
|
||||
@@ -343,23 +371,23 @@ class Node {
|
||||
bindGroupEvent() {
|
||||
// 单击事件,选中节点
|
||||
this.group.on('click', e => {
|
||||
this.mindMap.emit('node_click', this, e)
|
||||
if (this.isMultipleChoice) {
|
||||
e.stopPropagation()
|
||||
this.isMultipleChoice = false
|
||||
return
|
||||
}
|
||||
this.mindMap.emit('node_click', this, e)
|
||||
this.active(e)
|
||||
})
|
||||
this.group.on('mousedown', e => {
|
||||
if (this.isRoot && e.which === 3) {
|
||||
if (this.isRoot && e.which === 3 && !this.mindMap.opt.readonly) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
if (!this.isRoot) {
|
||||
if (!this.isRoot && e.which !== 2 && !this.mindMap.opt.readonly) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
// 多选和取消多选
|
||||
if (e.ctrlKey) {
|
||||
if (e.ctrlKey && this.mindMap.opt.enableCtrlKeyNodeSelection) {
|
||||
this.isMultipleChoice = true
|
||||
let isActive = this.nodeData.data.isActive
|
||||
if (!isActive)
|
||||
@@ -381,7 +409,7 @@ class Node {
|
||||
this.mindMap.emit('node_mousedown', this, e)
|
||||
})
|
||||
this.group.on('mouseup', e => {
|
||||
if (!this.isRoot) {
|
||||
if (!this.isRoot && e.which !== 2 && !this.mindMap.opt.readonly) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
this.mindMap.emit('node_mouseup', this, e)
|
||||
@@ -408,7 +436,8 @@ class Node {
|
||||
// 右键菜单事件
|
||||
this.group.on('contextmenu', e => {
|
||||
// 按住ctrl键点击鼠标左键不知为何触发的是contextmenu事件
|
||||
if (this.mindMap.opt.readonly || e.ctrlKey) {// || this.isGeneralization
|
||||
if (this.mindMap.opt.readonly || e.ctrlKey) {
|
||||
// || this.isGeneralization
|
||||
return
|
||||
}
|
||||
e.stopPropagation()
|
||||
@@ -442,8 +471,7 @@ class Node {
|
||||
if (!this.group) {
|
||||
return
|
||||
}
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration, alwaysShowExpandBtn } =
|
||||
this.mindMap.opt
|
||||
let { alwaysShowExpandBtn } = this.mindMap.opt
|
||||
if (alwaysShowExpandBtn) {
|
||||
// 需要移除展开收缩按钮
|
||||
if (this._expandBtn && this.nodeData.children.length <= 0) {
|
||||
@@ -465,14 +493,45 @@ class Node {
|
||||
this.renderGeneralization()
|
||||
// 更新节点位置
|
||||
let t = this.group.transform()
|
||||
// // 如果上次不在可视区内,且本次也不在,那么直接返回
|
||||
// let { left: ox, top: oy } = this.getNodePosInClient(
|
||||
// t.translateX,
|
||||
// t.translateY
|
||||
// )
|
||||
// let oldIsInClient =
|
||||
// ox > 0 && oy > 0 && ox < this.mindMap.width && oy < this.mindMap.height
|
||||
// let { left: nx, top: ny } = this.getNodePosInClient(this.left, this.top)
|
||||
// let newIsNotInClient =
|
||||
// nx + this.width < 0 ||
|
||||
// ny + this.height < 0 ||
|
||||
// nx > this.mindMap.width ||
|
||||
// ny > this.mindMap.height
|
||||
// if (!oldIsInClient && newIsNotInClient) {
|
||||
// if (!this.isHide) {
|
||||
// this.isHide = true
|
||||
// this.group.hide()
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
// // 如果当前是隐藏状态,那么先显示
|
||||
// if (this.isHide) {
|
||||
// this.isHide = false
|
||||
// this.group.show()
|
||||
// }
|
||||
// 如果节点位置没有变化,则返回
|
||||
if (this.left === t.translateX && this.top === t.translateY) return
|
||||
if (!isLayout && enableNodeTransitionMove) {
|
||||
this.group
|
||||
.animate(nodeTransitionMoveDuration)
|
||||
.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
} else {
|
||||
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
}
|
||||
|
||||
// 获取节点相当于画布的位置
|
||||
getNodePosInClient(_left, _top) {
|
||||
let drawTransform = this.mindMap.draw.transform()
|
||||
let { scaleX, scaleY, translateX, translateY } = drawTransform
|
||||
let left = _left * scaleX + translateX
|
||||
let top = _top * scaleY + translateY
|
||||
return {
|
||||
left,
|
||||
top
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,8 +554,6 @@ class Node {
|
||||
|
||||
// 递归渲染
|
||||
render(callback = () => {}) {
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration } =
|
||||
this.mindMap.opt
|
||||
// 节点
|
||||
// 重新渲染连线
|
||||
this.renderLine()
|
||||
@@ -505,6 +562,7 @@ class Node {
|
||||
isLayout = true
|
||||
// 创建组
|
||||
this.group = new G()
|
||||
this.group.addClass('smm-node')
|
||||
this.group.css({
|
||||
cursor: 'default'
|
||||
})
|
||||
@@ -518,6 +576,7 @@ class Node {
|
||||
this.needLayout = false
|
||||
this.layout()
|
||||
}
|
||||
this.updateExpandBtnPlaceholderRect()
|
||||
this.update()
|
||||
}
|
||||
// 子节点
|
||||
@@ -527,33 +586,23 @@ class Node {
|
||||
this.nodeData.data.expand !== false
|
||||
) {
|
||||
let index = 0
|
||||
asyncRun(
|
||||
this.children.map(item => {
|
||||
return () => {
|
||||
item.render(() => {
|
||||
index++
|
||||
if (index >= this.children.length) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
this.children.forEach(item => {
|
||||
item.render(() => {
|
||||
index++
|
||||
if (index >= this.children.length) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
} else {
|
||||
if (enableNodeTransitionMove && !isLayout) {
|
||||
setTimeout(() => {
|
||||
callback()
|
||||
}, nodeTransitionMoveDuration)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
callback()
|
||||
}
|
||||
// 手动插入的节点立即获得焦点并且开启编辑模式
|
||||
if (this.nodeData.inserting) {
|
||||
delete this.nodeData.inserting
|
||||
this.active()
|
||||
setTimeout(() => {
|
||||
this.mindMap.emit('node_dblclick', this)
|
||||
this.mindMap.emit('node_dblclick', this, null, true)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
@@ -566,13 +615,9 @@ class Node {
|
||||
this.removeLine()
|
||||
// 子节点
|
||||
if (this.children && this.children.length) {
|
||||
asyncRun(
|
||||
this.children.map(item => {
|
||||
return () => {
|
||||
item.remove()
|
||||
}
|
||||
})
|
||||
)
|
||||
this.children.forEach(item => {
|
||||
item.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,13 +643,9 @@ class Node {
|
||||
}
|
||||
// 子节点
|
||||
if (this.children && this.children.length) {
|
||||
asyncRun(
|
||||
this.children.map(item => {
|
||||
return () => {
|
||||
item.hide()
|
||||
}
|
||||
})
|
||||
)
|
||||
this.children.forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,13 +665,9 @@ class Node {
|
||||
}
|
||||
// 子节点
|
||||
if (this.children && this.children.length) {
|
||||
asyncRun(
|
||||
this.children.map(item => {
|
||||
return () => {
|
||||
item.show()
|
||||
}
|
||||
})
|
||||
)
|
||||
this.children.forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,7 +795,7 @@ class Node {
|
||||
|
||||
// 获取padding值
|
||||
getPaddingVale() {
|
||||
let { isActive }= this.nodeData.data
|
||||
let { isActive } = this.nodeData.data
|
||||
return {
|
||||
paddingX: this.getStyle('paddingX', true, isActive),
|
||||
paddingY: this.getStyle('paddingY', true, isActive)
|
||||
@@ -794,10 +831,20 @@ class Node {
|
||||
) // 父级
|
||||
}
|
||||
|
||||
// 获取节点非节点状态的边框大小
|
||||
getBorderWidth() {
|
||||
return this.style.merge('borderWidth', false, false) || 0
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
getData(key) {
|
||||
return key ? this.nodeData.data[key] || '' : this.nodeData.data
|
||||
}
|
||||
|
||||
// 是否存在自定义样式
|
||||
hasCustomStyle() {
|
||||
return this.style.hasCustomStyle()
|
||||
}
|
||||
}
|
||||
|
||||
export default Node
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Rect, Polygon, Path } from '@svgdotjs/svg.js'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
import { CONSTANTS } from '../../../constants/constant'
|
||||
|
||||
// 节点形状类
|
||||
export default class Shape {
|
||||
@@ -62,7 +62,10 @@ export default class Shape {
|
||||
// 创建形状节点
|
||||
createShape() {
|
||||
const shape = this.node.getShape()
|
||||
const borderWidth = this.node.getBorderWidth()
|
||||
let { width, height } = this.node
|
||||
width -= borderWidth
|
||||
height -= borderWidth
|
||||
let node = null
|
||||
// 矩形
|
||||
if (shape === CONSTANTS.SHAPE.RECTANGLE) {
|
||||
@@ -1,13 +1,23 @@
|
||||
import { tagColorList } from './utils/constant'
|
||||
import { tagColorList, nodeDataNoStylePropList } from '../../../constants/constant'
|
||||
const rootProp = ['paddingX', 'paddingY']
|
||||
const backgroundStyleProps = ['backgroundColor', 'backgroundImage', 'backgroundRepeat', 'backgroundPosition', 'backgroundSize']
|
||||
|
||||
// 样式类
|
||||
class Style {
|
||||
// 设置背景样式
|
||||
static setBackgroundStyle(el, themeConfig) {
|
||||
// 缓存容器元素原本的样式
|
||||
if (!Style.cacheStyle) {
|
||||
Style.cacheStyle = {}
|
||||
let style = window.getComputedStyle(el)
|
||||
backgroundStyleProps.forEach((prop) => {
|
||||
Style.cacheStyle[prop] = style[prop]
|
||||
})
|
||||
}
|
||||
// 设置新样式
|
||||
let { backgroundColor, backgroundImage, backgroundRepeat, backgroundPosition, backgroundSize } = themeConfig
|
||||
el.style.backgroundColor = backgroundColor
|
||||
if (backgroundImage) {
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
el.style.backgroundImage = `url(${backgroundImage})`
|
||||
el.style.backgroundRepeat = backgroundRepeat
|
||||
el.style.backgroundPosition = backgroundPosition
|
||||
@@ -17,6 +27,15 @@ class Style {
|
||||
}
|
||||
}
|
||||
|
||||
// 移除背景样式
|
||||
static removeBackgroundStyle(el) {
|
||||
if (!Style.cacheStyle) return
|
||||
backgroundStyleProps.forEach((prop) => {
|
||||
el.style[prop] = Style.cacheStyle[prop]
|
||||
})
|
||||
Style.cacheStyle = null
|
||||
}
|
||||
|
||||
// 构造函数
|
||||
constructor(ctx) {
|
||||
this.ctx = ctx
|
||||
@@ -190,6 +209,19 @@ class Style {
|
||||
node2.fill({ color: color })
|
||||
fillNode.fill({ color: fill })
|
||||
}
|
||||
|
||||
// 是否设置了自定义的样式
|
||||
hasCustomStyle() {
|
||||
let res = false
|
||||
Object.keys(this.ctx.nodeData.data).forEach((item) => {
|
||||
if (!nodeDataNoStylePropList.includes(item)) {
|
||||
res = true
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
Style.cacheStyle = null
|
||||
|
||||
export default Style
|
||||
@@ -4,8 +4,8 @@ function setData(data = {}) {
|
||||
}
|
||||
|
||||
// 设置文本
|
||||
function setText(text, richText) {
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText)
|
||||
function setText(text, richText, resetRichText) {
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText, resetRichText)
|
||||
}
|
||||
|
||||
// 设置图片
|
||||
@@ -43,6 +43,11 @@ function setStyle(prop, value, isActive) {
|
||||
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value, isActive)
|
||||
}
|
||||
|
||||
// 修改多个样式
|
||||
function setStyles(style, isActive) {
|
||||
this.mindMap.execCommand('SET_NODE_STYLES', this, style, isActive)
|
||||
}
|
||||
|
||||
export default {
|
||||
setData,
|
||||
setText,
|
||||
@@ -52,5 +57,6 @@ export default {
|
||||
setNote,
|
||||
setTag,
|
||||
setShape,
|
||||
setStyle
|
||||
setStyle,
|
||||
setStyles
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import { measureText, resizeImgSize, getTextFromHtml } from '../utils'
|
||||
import {
|
||||
measureText,
|
||||
resizeImgSize,
|
||||
removeHtmlStyle,
|
||||
addHtmlStyle,
|
||||
checkIsRichText
|
||||
} from '../../../utils'
|
||||
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
|
||||
import iconsSvg from '../svg/icons'
|
||||
import { CONSTANTS } from './constant'
|
||||
import iconsSvg from '../../../svg/icons'
|
||||
import { CONSTANTS, commonCaches } from '../../../constants/constant'
|
||||
|
||||
// 创建图片节点
|
||||
function createImgNode() {
|
||||
@@ -17,6 +23,15 @@ function createImgNode() {
|
||||
node.on('dblclick', e => {
|
||||
this.mindMap.emit('node_img_dblclick', this, e)
|
||||
})
|
||||
node.on('mouseenter', e => {
|
||||
this.mindMap.emit('node_img_mouseenter', this, node, e)
|
||||
})
|
||||
node.on('mouseleave', e => {
|
||||
this.mindMap.emit('node_img_mouseleave', this, node, e)
|
||||
})
|
||||
node.on('mousemove', e => {
|
||||
this.mindMap.emit('node_img_mousemove', this, node, e)
|
||||
})
|
||||
return {
|
||||
node,
|
||||
width: imgSize[0],
|
||||
@@ -26,9 +41,12 @@ function createImgNode() {
|
||||
|
||||
// 获取图片显示宽高
|
||||
function getImgShowSize() {
|
||||
const { custom, width, height } = this.nodeData.data.imageSize
|
||||
// 如果是自定义了图片的宽高,那么不受最大宽高限制
|
||||
if (custom) return [width, height]
|
||||
return resizeImgSize(
|
||||
this.nodeData.data.imageSize.width,
|
||||
this.nodeData.data.imageSize.height,
|
||||
width,
|
||||
height,
|
||||
this.mindMap.themeConfig.imgMaxWidth,
|
||||
this.mindMap.themeConfig.imgMaxHeight
|
||||
)
|
||||
@@ -42,7 +60,10 @@ function createIconNode() {
|
||||
}
|
||||
let iconSize = this.mindMap.themeConfig.iconSize
|
||||
return _data.icon.map(item => {
|
||||
let src = iconsSvg.getNodeIconListIcon(item, this.mindMap.opt.iconList || [])
|
||||
let src = iconsSvg.getNodeIconListIcon(
|
||||
item,
|
||||
this.mindMap.opt.iconList || []
|
||||
)
|
||||
let node = null
|
||||
// svg图标
|
||||
if (/^<svg/.test(src)) {
|
||||
@@ -52,6 +73,9 @@ function createIconNode() {
|
||||
node = new Image().load(src)
|
||||
}
|
||||
node.size(iconSize, iconSize)
|
||||
node.on('click', e => {
|
||||
this.mindMap.emit('node_icon_click', this, item, e)
|
||||
})
|
||||
return {
|
||||
node,
|
||||
width: iconSize,
|
||||
@@ -64,13 +88,39 @@ function createIconNode() {
|
||||
function createRichTextNode() {
|
||||
let g = new G()
|
||||
// 重新设置富文本节点内容
|
||||
if (this.nodeData.data.resetRichText || [CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
|
||||
let recoverText = false
|
||||
if (this.nodeData.data.resetRichText) {
|
||||
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>`
|
||||
recoverText = true
|
||||
}
|
||||
if ([CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
|
||||
// 如果自定义过样式则不允许覆盖
|
||||
if (!this.hasCustomStyle()) {
|
||||
recoverText = true
|
||||
}
|
||||
}
|
||||
if (recoverText) {
|
||||
let text = this.nodeData.data.text
|
||||
// 判断节点内容是否是富文本
|
||||
let isRichText = checkIsRichText(text)
|
||||
// 样式字符串
|
||||
let style = this.style.createStyleText()
|
||||
if (isRichText) {
|
||||
// 如果是富文本那么线移除内联样式
|
||||
text = removeHtmlStyle(text)
|
||||
// 再添加新的内联样式
|
||||
text = addHtmlStyle(text, 'span', style)
|
||||
} else {
|
||||
// 非富文本
|
||||
text = `<p><span style="${style}">${text}</span></p>`
|
||||
}
|
||||
this.nodeData.data.text = text
|
||||
}
|
||||
let html = `<div>${this.nodeData.data.text}</div>`
|
||||
let div = document.createElement('div')
|
||||
if (!commonCaches.measureRichtextNodeTextSizeEl) {
|
||||
commonCaches.measureRichtextNodeTextSizeEl = document.createElement('div')
|
||||
}
|
||||
let div = commonCaches.measureRichtextNodeTextSizeEl
|
||||
div.innerHTML = html
|
||||
div.style.cssText = `position: fixed; left: -999999px;`
|
||||
let el = div.children[0]
|
||||
@@ -79,12 +129,18 @@ function createRichTextNode() {
|
||||
el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px'
|
||||
this.mindMap.el.appendChild(div)
|
||||
let { width, height } = el.getBoundingClientRect()
|
||||
width = Math.ceil(width)
|
||||
// 如果文本为空,那么需要计算一个默认高度
|
||||
if (height <= 0) {
|
||||
div.innerHTML = '<p>abc123我和你</p>'
|
||||
let elTmp = div.children[0]
|
||||
elTmp.classList.add('smm-richtext-node-wrap')
|
||||
height = elTmp.getBoundingClientRect().height
|
||||
}
|
||||
width = Math.ceil(width) + 1 // 修复getBoundingClientRect方法对实际宽度是小数的元素获取到的值是整数,导致宽度不够文本发生换行的问题
|
||||
height = Math.ceil(height)
|
||||
g.attr('data-width', width)
|
||||
g.attr('data-height', height)
|
||||
html = div.innerHTML
|
||||
this.mindMap.el.removeChild(div)
|
||||
let foreignObject = new ForeignObject()
|
||||
foreignObject.width(width)
|
||||
foreignObject.height(height)
|
||||
@@ -230,15 +286,17 @@ function createNoteNode() {
|
||||
if (!this.noteEl) {
|
||||
this.noteEl = document.createElement('div')
|
||||
this.noteEl.style.cssText = `
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
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 }
|
||||
z-index: ${this.mindMap.opt.nodeNoteTooltipZIndex}
|
||||
`
|
||||
document.body.appendChild(this.noteEl)
|
||||
const targetNode =
|
||||
this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.noteEl)
|
||||
}
|
||||
this.noteEl.innerText = this.nodeData.data.note
|
||||
}
|
||||
@@ -270,13 +328,40 @@ function createNoteNode() {
|
||||
}
|
||||
}
|
||||
|
||||
// 测量自定义节点内容元素的宽高
|
||||
function measureCustomNodeContentSize(content) {
|
||||
if (!commonCaches.measureCustomNodeContentSizeEl) {
|
||||
commonCaches.measureCustomNodeContentSizeEl = document.createElement('div')
|
||||
commonCaches.measureCustomNodeContentSizeEl.style.cssText = `
|
||||
position: fixed;
|
||||
left: -99999px;
|
||||
top: -99999px;
|
||||
`
|
||||
this.mindMap.el.appendChild(commonCaches.measureCustomNodeContentSizeEl)
|
||||
}
|
||||
commonCaches.measureCustomNodeContentSizeEl.innerHTML = ''
|
||||
commonCaches.measureCustomNodeContentSizeEl.appendChild(content)
|
||||
let rect = commonCaches.measureCustomNodeContentSizeEl.getBoundingClientRect()
|
||||
return {
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
}
|
||||
|
||||
// 是否使用的是自定义节点内容
|
||||
function isUseCustomNodeContent() {
|
||||
return !!this._customNodeContent
|
||||
}
|
||||
|
||||
export default {
|
||||
createImgNode,
|
||||
getImgShowSize,
|
||||
createIconNode,
|
||||
createRichTextNode,
|
||||
createTextNode,
|
||||
createHyperlinkNode,
|
||||
createTagNode,
|
||||
createNoteNode
|
||||
}
|
||||
createImgNode,
|
||||
getImgShowSize,
|
||||
createIconNode,
|
||||
createRichTextNode,
|
||||
createTextNode,
|
||||
createHyperlinkNode,
|
||||
createTagNode,
|
||||
createNoteNode,
|
||||
measureCustomNodeContentSize,
|
||||
isUseCustomNodeContent
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import btnsSvg from '../svg/btns'
|
||||
import btnsSvg from '../../../svg/btns'
|
||||
import { SVG, Circle, G } from '@svgdotjs/svg.js'
|
||||
|
||||
// 创建展开收起按钮的内容节点
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Rect } from '@svgdotjs/svg.js'
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
function renderExpandBtnPlaceholderRect() {
|
||||
// 根节点或没有子节点不需要渲染
|
||||
if (
|
||||
!this.nodeData.children ||
|
||||
this.nodeData.children.length <= 0 ||
|
||||
this.isRoot
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 默认显示展开按钮的情况下也不需要渲染
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
let { width, height } = this
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
this._unVisibleRectRegionNode = new Rect()
|
||||
this._unVisibleRectRegionNode.fill({
|
||||
color: 'transparent'
|
||||
})
|
||||
}
|
||||
this.group.add(this._unVisibleRectRegionNode)
|
||||
this.renderer.layout.renderExpandBtnRect(
|
||||
this._unVisibleRectRegionNode,
|
||||
this.expandBtnSize,
|
||||
width,
|
||||
height,
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除展开收起按钮的隐藏占位元素
|
||||
function clearExpandBtnPlaceholderRect() {
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
return
|
||||
}
|
||||
this._unVisibleRectRegionNode.remove()
|
||||
this._unVisibleRectRegionNode = null
|
||||
}
|
||||
|
||||
// 更新展开收起按钮的隐藏占位元素
|
||||
function updateExpandBtnPlaceholderRect() {
|
||||
// 布局改变需要重新渲染
|
||||
if (this.needRerenderExpandBtnPlaceholderRect) {
|
||||
this.needRerenderExpandBtnPlaceholderRect = false
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
}
|
||||
// 没有子节点到有子节点需要渲染
|
||||
if (this.nodeData.children && this.nodeData.children.length > 0) {
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
}
|
||||
} else {
|
||||
// 有子节点到没子节点,需要删除
|
||||
if (this._unVisibleRectRegionNode) {
|
||||
this.clearExpandBtnPlaceholderRect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
renderExpandBtnPlaceholderRect,
|
||||
clearExpandBtnPlaceholderRect,
|
||||
updateExpandBtnPlaceholderRect
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import Node from '../Node'
|
||||
import Node from './Node'
|
||||
import { createUid } from '../../../utils/index'
|
||||
|
||||
// 检查是否存在概要
|
||||
function checkHasGeneralization () {
|
||||
@@ -18,7 +19,7 @@ function createGeneralizationNode () {
|
||||
data: {
|
||||
data: this.nodeData.data.generalization
|
||||
},
|
||||
uid: this.mindMap.uid++,
|
||||
uid: createUid(),
|
||||
renderer: this.renderer,
|
||||
mindMap: this.mindMap,
|
||||
draw: this.draw,
|
||||
@@ -35,15 +36,14 @@ function createGeneralizationNode () {
|
||||
|
||||
// 更新概要节点
|
||||
function updateGeneralization () {
|
||||
if (this.isGeneralization) return
|
||||
this.removeGeneralization()
|
||||
this.createGeneralizationNode()
|
||||
}
|
||||
|
||||
// 渲染概要节点
|
||||
function renderGeneralization () {
|
||||
if (this.isGeneralization) {
|
||||
return
|
||||
}
|
||||
if (this.isGeneralization) return
|
||||
if (!this.checkHasGeneralization()) {
|
||||
this.removeGeneralization()
|
||||
this._generalizationNodeWidth = 0
|
||||
@@ -66,6 +66,7 @@ function renderGeneralization () {
|
||||
|
||||
// 删除概要节点
|
||||
function removeGeneralization () {
|
||||
if (this.isGeneralization) return
|
||||
if (this._generalizationLine) {
|
||||
this._generalizationLine.remove()
|
||||
this._generalizationLine = null
|
||||
@@ -86,6 +87,7 @@ function removeGeneralization () {
|
||||
|
||||
// 隐藏概要节点
|
||||
function hideGeneralization () {
|
||||
if (this.isGeneralization) return
|
||||
if (this._generalizationLine) {
|
||||
this._generalizationLine.hide()
|
||||
}
|
||||
@@ -96,6 +98,7 @@ function hideGeneralization () {
|
||||
|
||||
// 显示概要节点
|
||||
function showGeneralization () {
|
||||
if (this.isGeneralization) return
|
||||
if (this._generalizationLine) {
|
||||
this._generalizationLine.show()
|
||||
}
|
||||
309
simple-mind-map/src/core/view/View.js
Normal file
@@ -0,0 +1,309 @@
|
||||
import { CONSTANTS } from '../../constants/constant'
|
||||
|
||||
// 视图操作类
|
||||
class View {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
this.opt = opt
|
||||
this.mindMap = this.opt.mindMap
|
||||
this.scale = 1
|
||||
this.sx = 0
|
||||
this.sy = 0
|
||||
this.x = 0
|
||||
this.y = 0
|
||||
this.firstDrag = true
|
||||
this.setTransformData(this.mindMap.opt.viewData)
|
||||
this.bind()
|
||||
}
|
||||
|
||||
// 绑定
|
||||
bind() {
|
||||
// 快捷键
|
||||
this.mindMap.keyCommand.addShortcut('Control+=', () => {
|
||||
this.enlarge()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Control+-', () => {
|
||||
this.narrow()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Control+Enter', () => {
|
||||
this.reset()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Control+i', () => {
|
||||
this.fit()
|
||||
})
|
||||
this.mindMap.svg.on('dblclick', () => {
|
||||
this.reset()
|
||||
})
|
||||
// 拖动视图
|
||||
this.mindMap.event.on('mousedown', () => {
|
||||
this.sx = this.x
|
||||
this.sy = this.y
|
||||
})
|
||||
this.mindMap.event.on('drag', (e, event) => {
|
||||
if (e.ctrlKey) {
|
||||
// 按住ctrl键拖动为多选
|
||||
return
|
||||
}
|
||||
if (this.firstDrag) {
|
||||
this.firstDrag = false
|
||||
// 清除激活节点
|
||||
if (this.mindMap.renderer.activeNodeList.length > 0) {
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
}
|
||||
}
|
||||
this.x = this.sx + event.mousemoveOffset.x
|
||||
this.y = this.sy + event.mousemoveOffset.y
|
||||
this.transform()
|
||||
})
|
||||
this.mindMap.event.on('mouseup', () => {
|
||||
this.firstDrag = true
|
||||
})
|
||||
// 放大缩小视图
|
||||
this.mindMap.event.on('mousewheel', (e, dir, event, isTouchPad) => {
|
||||
let {
|
||||
customHandleMousewheel,
|
||||
mousewheelAction,
|
||||
mouseScaleCenterUseMousePosition,
|
||||
mousewheelMoveStep,
|
||||
mousewheelZoomActionReverse,
|
||||
disableMouseWheelZoom
|
||||
} = this.mindMap.opt
|
||||
// 是否自定义鼠标滚轮事件
|
||||
if (
|
||||
customHandleMousewheel &&
|
||||
typeof customHandleMousewheel === 'function'
|
||||
) {
|
||||
return customHandleMousewheel(e)
|
||||
}
|
||||
// 鼠标滚轮事件控制缩放
|
||||
if (mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
|
||||
if (disableMouseWheelZoom) return
|
||||
const { x: clientX, y: clientY } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
let cx = mouseScaleCenterUseMousePosition ? clientX : undefined
|
||||
let cy = mouseScaleCenterUseMousePosition ? clientY : undefined
|
||||
switch (dir) {
|
||||
// 鼠标滚轮,向上和向左,都是缩小
|
||||
case CONSTANTS.DIR.UP:
|
||||
case CONSTANTS.DIR.LEFT:
|
||||
mousewheelZoomActionReverse
|
||||
? this.enlarge(cx, cy, isTouchPad)
|
||||
: this.narrow(cx, cy, isTouchPad)
|
||||
break
|
||||
// 鼠标滚轮,向下和向右,都是放大
|
||||
case CONSTANTS.DIR.DOWN:
|
||||
case CONSTANTS.DIR.RIGHT:
|
||||
mousewheelZoomActionReverse
|
||||
? this.narrow(cx, cy, isTouchPad)
|
||||
: this.enlarge(cx, cy, isTouchPad)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// 鼠标滚轮事件控制画布移动
|
||||
let step = mousewheelMoveStep
|
||||
if (isTouchPad) {
|
||||
step = 5
|
||||
}
|
||||
switch (dir) {
|
||||
// 上移
|
||||
case CONSTANTS.DIR.DOWN:
|
||||
this.translateY(-step)
|
||||
break
|
||||
// 下移
|
||||
case CONSTANTS.DIR.UP:
|
||||
this.translateY(step)
|
||||
break
|
||||
// 右移
|
||||
case CONSTANTS.DIR.LEFT:
|
||||
this.translateX(-step)
|
||||
break
|
||||
// 左移
|
||||
case CONSTANTS.DIR.RIGHT:
|
||||
this.translateX(step)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前变换状态数据
|
||||
getTransformData() {
|
||||
return {
|
||||
transform: this.mindMap.draw.transform(),
|
||||
state: {
|
||||
scale: this.scale,
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
sx: this.sx,
|
||||
sy: this.sy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动态设置变换状态数据
|
||||
setTransformData(viewData) {
|
||||
if (viewData) {
|
||||
Object.keys(viewData.state).forEach(prop => {
|
||||
this[prop] = viewData.state[prop]
|
||||
})
|
||||
this.mindMap.draw.transform({
|
||||
...viewData.transform
|
||||
})
|
||||
this.mindMap.emit('view_data_change', this.getTransformData())
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
}
|
||||
|
||||
// 平移x,y方向
|
||||
translateXY(x, y) {
|
||||
if (x === 0 && y === 0) return
|
||||
this.x += x
|
||||
this.y += y
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移x方向
|
||||
translateX(step) {
|
||||
if (step === 0) return
|
||||
this.x += step
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移x方式到
|
||||
translateXTo(x) {
|
||||
if (x === 0) return
|
||||
this.x = x
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移y方向
|
||||
translateY(step) {
|
||||
if (step === 0) return
|
||||
this.y += step
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 平移y方向到
|
||||
translateYTo(y) {
|
||||
if (y === 0) return
|
||||
this.y = y
|
||||
this.transform()
|
||||
}
|
||||
|
||||
// 应用变换
|
||||
transform() {
|
||||
this.mindMap.draw.transform({
|
||||
origin: [0, 0],
|
||||
scale: this.scale,
|
||||
translate: [this.x, this.y]
|
||||
})
|
||||
this.mindMap.emit('view_data_change', this.getTransformData())
|
||||
}
|
||||
|
||||
// 恢复
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 缩小
|
||||
narrow(cx, cy, isTouchPad) {
|
||||
const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1)
|
||||
const scale = Math.max(this.scale - scaleRatio, 0.1)
|
||||
this.scaleInCenter(scale, cx, cy)
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
|
||||
// 放大
|
||||
enlarge(cx, cy, isTouchPad) {
|
||||
const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1)
|
||||
const scale = this.scale + scaleRatio
|
||||
this.scaleInCenter(scale, cx, cy)
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
|
||||
// 基于指定中心进行缩放,cx,cy 可不指定,此时会使用画布中心点
|
||||
scaleInCenter(scale, cx, cy) {
|
||||
if (cx === undefined || cy === undefined) {
|
||||
cx = this.mindMap.width / 2
|
||||
cy = this.mindMap.height / 2
|
||||
}
|
||||
const prevScale = this.scale
|
||||
const ratio = 1 - scale / prevScale
|
||||
const dx = (cx - this.x) * ratio
|
||||
const dy = (cy - this.y) * ratio
|
||||
this.x += dx
|
||||
this.y += dy
|
||||
this.scale = scale
|
||||
}
|
||||
|
||||
// 设置缩放
|
||||
setScale(scale, cx, cy) {
|
||||
if (cx !== undefined && cy !== undefined) {
|
||||
this.scaleInCenter(scale, cx, cy)
|
||||
} else {
|
||||
this.scale = scale
|
||||
}
|
||||
this.transform()
|
||||
this.mindMap.emit('scale', this.scale)
|
||||
}
|
||||
|
||||
// 适应画布大小
|
||||
fit() {
|
||||
let { fitPadding } = this.mindMap.opt
|
||||
let draw = this.mindMap.draw
|
||||
let origTransform = draw.transform()
|
||||
let rect = draw.rbox()
|
||||
let drawWidth = rect.width / origTransform.scaleX
|
||||
let drawHeight = rect.height / origTransform.scaleY
|
||||
let drawRatio = drawWidth / drawHeight
|
||||
let { width: elWidth, height: elHeight } =
|
||||
this.mindMap.el.getBoundingClientRect()
|
||||
elWidth = elWidth - fitPadding * 2
|
||||
elHeight = elHeight - fitPadding * 2
|
||||
let elRatio = elWidth / elHeight
|
||||
let newScale = 0
|
||||
let flag = ''
|
||||
if (drawWidth <= elWidth && drawHeight <= elHeight) {
|
||||
newScale = 1
|
||||
flag = 1
|
||||
} else {
|
||||
let newWidth = 0
|
||||
let newHeight = 0
|
||||
if (drawRatio > elRatio) {
|
||||
newWidth = elWidth
|
||||
newHeight = elWidth / drawRatio
|
||||
flag = 2
|
||||
} else {
|
||||
newHeight = elHeight
|
||||
newWidth = elHeight * drawRatio
|
||||
flag = 3
|
||||
}
|
||||
newScale = newWidth / drawWidth
|
||||
}
|
||||
this.setScale(newScale)
|
||||
let newRect = draw.rbox()
|
||||
let newX = 0
|
||||
let newY = 0
|
||||
if (flag === 1) {
|
||||
newX = -newRect.x + fitPadding + (elWidth - newRect.width) / 2
|
||||
newY = -newRect.y + fitPadding + (elHeight - newRect.height) / 2
|
||||
} else if (flag === 2) {
|
||||
newX = -newRect.x + fitPadding
|
||||
newY = -newRect.y + fitPadding + (elHeight - newRect.height) / 2
|
||||
} else if (flag === 3) {
|
||||
newX = -newRect.x + fitPadding + (elWidth - newRect.width) / 2
|
||||
newY = -newRect.y + fitPadding
|
||||
}
|
||||
this.translateXY(newX, newY)
|
||||
}
|
||||
}
|
||||
|
||||
export default View
|
||||
@@ -1,6 +1,7 @@
|
||||
import Node from '../Node'
|
||||
import { CONSTANTS, initRootNodePositionMap } from '../utils/constant'
|
||||
import Node from '../core/render/node/Node'
|
||||
import { CONSTANTS, initRootNodePositionMap } from '../constants/constant'
|
||||
import Lru from '../utils/Lru'
|
||||
import { createUid } from '../utils/index'
|
||||
|
||||
// 布局基类
|
||||
class Base {
|
||||
@@ -48,6 +49,20 @@ class Base {
|
||||
return [CONSTANTS.CHANGE_THEME, CONSTANTS.TRANSFORM_TO_NORMAL_NODE].includes(this.renderer.renderSource)
|
||||
}
|
||||
|
||||
// 层级类型改变
|
||||
checkIsLayerTypeChange(oldIndex, newIndex) {
|
||||
if (oldIndex >= 2 && newIndex >= 2) return false
|
||||
if (oldIndex >= 2 && newIndex < 2) return true
|
||||
if (oldIndex < 2 && newIndex >= 2) return true
|
||||
}
|
||||
|
||||
// 检查是否是结构布局改变重新渲染展开收起按钮占位元素
|
||||
checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) {
|
||||
if (this.renderer.renderSource === CONSTANTS.CHANGE_LAYOUT) {
|
||||
node.needRerenderExpandBtnPlaceholderRect = true
|
||||
}
|
||||
}
|
||||
|
||||
// 创建节点实例
|
||||
createNode(data, parent, isRoot, layerIndex) {
|
||||
// 创建节点
|
||||
@@ -55,11 +70,13 @@ class Base {
|
||||
// 数据上保存了节点引用,那么直接复用节点
|
||||
if (data && data._node && !this.renderer.reRender) {
|
||||
newNode = data._node
|
||||
let isLayerTypeChange = this.checkIsLayerTypeChange(newNode.layerIndex, layerIndex)
|
||||
newNode.reset()
|
||||
newNode.layerIndex = layerIndex
|
||||
this.cacheNode(data._node.uid, newNode)
|
||||
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
|
||||
// 主题或主题配置改变了需要重新计算节点大小和布局
|
||||
if (this.checkIsNeedResizeSources()) {
|
||||
if (this.checkIsNeedResizeSources() || isLayerTypeChange) {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
}
|
||||
@@ -68,22 +85,24 @@ class Base {
|
||||
newNode = this.lru.get(data.data.uid)
|
||||
// 保存该节点上一次的数据
|
||||
let lastData = JSON.stringify(newNode.nodeData.data)
|
||||
let isLayerTypeChange = this.checkIsLayerTypeChange(newNode.layerIndex, layerIndex)
|
||||
newNode.reset()
|
||||
newNode.nodeData = newNode.handleData(data || {})
|
||||
newNode.layerIndex = layerIndex
|
||||
this.cacheNode(data.data.uid, newNode)
|
||||
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
|
||||
data._node = newNode
|
||||
// 主题或主题配置改变了需要重新计算节点大小和布局
|
||||
let isResizeSource = this.checkIsNeedResizeSources()
|
||||
// 节点数据改变了需要重新计算节点大小和布局
|
||||
let isNodeDataChange = lastData !== JSON.stringify(data.data)
|
||||
if (isResizeSource || isNodeDataChange) {
|
||||
if (isResizeSource || isNodeDataChange || isLayerTypeChange) {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
}
|
||||
} else {
|
||||
// 创建新节点
|
||||
let uid = this.mindMap.uid++
|
||||
let uid = data.data.uid || createUid()
|
||||
newNode = new Node({
|
||||
data,
|
||||
uid,
|
||||
|
||||
@@ -349,6 +349,11 @@ class CatalogOrganization extends Base {
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
}
|
||||
}
|
||||
|
||||
export default CatalogOrganization
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun, degToRad } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
import utils from './fishboneUtils'
|
||||
|
||||
// 鱼骨图
|
||||
@@ -51,8 +51,8 @@ class Fishbone extends Base {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
@@ -222,7 +222,7 @@ class Fishbone extends Base {
|
||||
|
||||
// 检查节点是否是上方节点
|
||||
checkIsTop(node) {
|
||||
return node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
return node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
@@ -239,7 +239,7 @@ class Fishbone extends Base {
|
||||
// 当前节点是根节点
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
let maxx = -Infinity
|
||||
node.children.forEach((item) => {
|
||||
node.children.forEach(item => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
@@ -250,15 +250,15 @@ class Fishbone extends Base {
|
||||
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
|
||||
}`
|
||||
`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}`
|
||||
`M ${nodeLineX - offsetX},${item.top - offset} L ${nodeLineX},${
|
||||
item.top
|
||||
}`
|
||||
)
|
||||
}
|
||||
node.style.line(line)
|
||||
@@ -373,6 +373,27 @@ class Fishbone extends Base {
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
let dir = ''
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
|
||||
dir =
|
||||
node.layerIndex === 1
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
} else {
|
||||
dir =
|
||||
node.layerIndex === 1
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
}
|
||||
if (dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
|
||||
rect.size(width, expandBtnSize).x(0).y(-expandBtnSize)
|
||||
} else {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Fishbone
|
||||
|
||||
@@ -52,8 +52,8 @@ class Fishbone extends Base {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
|
||||
@@ -52,8 +52,8 @@ class Fishbone extends Base {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.TOP
|
||||
: CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
@@ -281,7 +281,7 @@ class Fishbone extends Base {
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
|
||||
@@ -172,9 +172,7 @@ class LogicalStructure extends Base {
|
||||
let x2 = item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStyleOffset = nodeUseLineStyle
|
||||
? item.width
|
||||
: 0
|
||||
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 ${
|
||||
@@ -236,7 +234,7 @@ class LogicalStructure extends Base {
|
||||
let nodeUseLineStylePath = nodeUseLineStyle
|
||||
? ` L ${item.left + item.width},${y2}`
|
||||
: ''
|
||||
if (node.isRoot) {
|
||||
if (node.isRoot && !this.mindMap.themeConfig.rootLineKeepSameInCurve) {
|
||||
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
} else {
|
||||
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
@@ -260,10 +258,7 @@ class LogicalStructure extends Base {
|
||||
if (_x === translateX && _y === translateY) {
|
||||
return
|
||||
}
|
||||
btn.translate(
|
||||
_x - translateX,
|
||||
_y - translateY
|
||||
)
|
||||
btn.translate(_x - translateX, _y - translateY)
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
@@ -286,6 +281,11 @@ class LogicalStructure extends Base {
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
rect.size(expandBtnSize, height).x(width).y(0)
|
||||
}
|
||||
}
|
||||
|
||||
export default LogicalStructure
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
// 思维导图
|
||||
class MindMap extends Base {
|
||||
@@ -45,11 +46,14 @@ class MindMap extends Base {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir = index % 2 === 0 ? 'right' : 'left'
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
}
|
||||
// 根据生长方向定位到父节点的左侧或右侧
|
||||
newNode.left =
|
||||
newNode.dir === 'right'
|
||||
newNode.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
? parent._node.left +
|
||||
parent._node.width +
|
||||
this.getMarginX(layerIndex)
|
||||
@@ -72,7 +76,7 @@ class MindMap extends Base {
|
||||
let leftChildrenAreaHeight = 0
|
||||
let rightChildrenAreaHeight = 0
|
||||
cur._node.children.forEach(item => {
|
||||
if (item.dir === 'left') {
|
||||
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
leftLen++
|
||||
leftChildrenAreaHeight += item.height
|
||||
} else {
|
||||
@@ -109,7 +113,7 @@ class MindMap extends Base {
|
||||
let leftTotalTop = baseTop - node.leftChildrenAreaHeight / 2
|
||||
let rightTotalTop = baseTop - node.rightChildrenAreaHeight / 2
|
||||
node.children.forEach(cur => {
|
||||
if (cur.dir === 'left') {
|
||||
if (cur.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
cur.top = leftTotalTop
|
||||
leftTotalTop += cur.height + marginY
|
||||
} else {
|
||||
@@ -162,7 +166,10 @@ class MindMap extends Base {
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
let addHeight = item.dir === 'left' ? leftAddHeight : rightAddHeight
|
||||
let addHeight =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? leftAddHeight
|
||||
: rightAddHeight
|
||||
// 上面的节点往上移
|
||||
if (_index < index) {
|
||||
_offset = -addHeight
|
||||
@@ -208,10 +215,8 @@ class MindMap extends Base {
|
||||
let x1 = 0
|
||||
let _s = 0
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStyleOffset = nodeUseLineStyle
|
||||
? item.width
|
||||
: 0
|
||||
if (item.dir === 'left') {
|
||||
let nodeUseLineStyleOffset = nodeUseLineStyle ? item.width : 0
|
||||
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
_s = -s1
|
||||
x1 = node.layerIndex === 0 ? left : left - expandBtnSize
|
||||
nodeUseLineStyleOffset = -nodeUseLineStyleOffset
|
||||
@@ -220,7 +225,10 @@ class MindMap extends Base {
|
||||
x1 = node.layerIndex === 0 ? left + width : left + width + expandBtnSize
|
||||
}
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.dir === 'left' ? item.left + item.width : item.left
|
||||
let x2 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_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
|
||||
@@ -246,18 +254,21 @@ class MindMap extends Base {
|
||||
let x1 =
|
||||
node.layerIndex === 0
|
||||
? left + width / 2
|
||||
: item.dir === 'left'
|
||||
: item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? left - expandBtnSize
|
||||
: left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.dir === 'left' ? item.left + item.width : item.left
|
||||
let x2 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_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 (nodeUseLineStyle) {
|
||||
if (item.dir === 'left') {
|
||||
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
nodeUseLineStylePath = ` L ${item.left},${y2}`
|
||||
} else {
|
||||
nodeUseLineStylePath = ` L ${item.left + item.width},${y2}`
|
||||
@@ -283,11 +294,14 @@ class MindMap extends Base {
|
||||
let x1 =
|
||||
node.layerIndex === 0
|
||||
? left + width / 2
|
||||
: item.dir === 'left'
|
||||
: item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? left - expandBtnSize
|
||||
: left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 = item.dir === 'left' ? item.left + item.width : item.left
|
||||
let x2 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_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
|
||||
@@ -295,13 +309,13 @@ class MindMap extends Base {
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = ''
|
||||
if (this.mindMap.themeConfig.nodeUseLineStyle) {
|
||||
if (item.dir === 'left') {
|
||||
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
nodeUseLineStylePath = ` L ${item.left},${y2}`
|
||||
} else {
|
||||
nodeUseLineStylePath = ` L ${item.left + item.width},${y2}`
|
||||
}
|
||||
}
|
||||
if (node.isRoot) {
|
||||
if (node.isRoot && !this.mindMap.themeConfig.rootLineKeepSameInCurve) {
|
||||
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
} else {
|
||||
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath
|
||||
@@ -320,7 +334,8 @@ class MindMap extends Base {
|
||||
? height / 2
|
||||
: 0
|
||||
// 位置没有变化则返回
|
||||
let _x = (node.dir === 'left' ? 0 - expandBtnSize : width)
|
||||
let _x =
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT ? 0 - expandBtnSize : width
|
||||
let _y = height / 2 + nodeUseLineStyleOffset
|
||||
if (_x === translateX && _y === translateY) {
|
||||
return
|
||||
@@ -332,7 +347,7 @@ class MindMap extends Base {
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let isLeft = node.dir === 'left'
|
||||
let isLeft = node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
@@ -358,6 +373,15 @@ class MindMap extends Base {
|
||||
(isLeft ? gNode.width : 0)
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
rect.size(expandBtnSize, height).x(-expandBtnSize).y(0)
|
||||
} else {
|
||||
rect.size(expandBtnSize, height).x(width).y(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MindMap
|
||||
|
||||
@@ -255,6 +255,11 @@ class OrganizationStructure extends Base {
|
||||
gNode.top = bottom + generalizationNodeMargin
|
||||
gNode.left = left + (right - left - gNode.width) / 2
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
}
|
||||
}
|
||||
|
||||
export default OrganizationStructure
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
// 时间轴
|
||||
class Timeline extends Base {
|
||||
@@ -50,8 +50,8 @@ class Timeline extends Base {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.TIMELINE_DIR.BOTTOM
|
||||
: CONSTANTS.TIMELINE_DIR.TOP
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
}
|
||||
} else {
|
||||
newNode.dir = ''
|
||||
@@ -151,7 +151,7 @@ class Timeline extends Base {
|
||||
if (
|
||||
parent &&
|
||||
parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
) {
|
||||
// 遍历二级节点的子节点
|
||||
node.children.forEach(item => {
|
||||
@@ -280,7 +280,7 @@ class Timeline extends Base {
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
) {
|
||||
line.plot(`M ${x},${top} L ${x},${miny}`)
|
||||
} else {
|
||||
@@ -301,7 +301,7 @@ class Timeline extends Base {
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.TIMELINE_DIR.TOP
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
@@ -336,6 +336,28 @@ class Timeline extends Base {
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
if (this.layout === CONSTANTS.LAYOUT.TIMELINE) {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
} else {
|
||||
let dir = ''
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
|
||||
dir =
|
||||
node.layerIndex === 1
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
} else {
|
||||
dir = CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
}
|
||||
if (dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
|
||||
rect.size(width, expandBtnSize).x(0).y(-expandBtnSize)
|
||||
} else {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Timeline
|
||||
|
||||
431
simple-mind-map/src/layouts/VerticalTimeline.js
Normal file
@@ -0,0 +1,431 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun } from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
// 竖向时间轴
|
||||
class VerticalTimeline extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}, layout) {
|
||||
super(opt)
|
||||
this.layout = layout
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedTopValue()
|
||||
},
|
||||
() => {
|
||||
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 {
|
||||
// 非根节点
|
||||
// 节点生长方向
|
||||
// 三级及以下节点以上级为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
}
|
||||
// 定位二级节点的left
|
||||
if (parent._node.isRoot) {
|
||||
newNode.left =
|
||||
parent._node.left +
|
||||
(cur._node.width > parent._node.width
|
||||
? -(cur._node.width - parent._node.width) / 2
|
||||
: (parent._node.width - cur._node.width) / 2)
|
||||
} else {
|
||||
newNode.left =
|
||||
newNode.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
? parent._node.left +
|
||||
parent._node.width +
|
||||
this.getMarginX(layerIndex)
|
||||
: parent._node.left -
|
||||
this.getMarginX(layerIndex) -
|
||||
newNode.width
|
||||
}
|
||||
}
|
||||
if (!cur.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
(cur, parent, isRoot, layerIndex) => {
|
||||
// 返回时计算节点的areaHeight,也就是子节点所占的高度之和,包括外边距
|
||||
if (isRoot) {
|
||||
return
|
||||
}
|
||||
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, index) => {
|
||||
if (
|
||||
node.nodeData.data.expand &&
|
||||
node.children &&
|
||||
node.children.length
|
||||
) {
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
// 定位二级节点的top
|
||||
if (isRoot) {
|
||||
let top = node.top + node.height
|
||||
let totalTop = top + marginY
|
||||
node.children.forEach(cur => {
|
||||
cur.top = totalTop
|
||||
totalTop += cur.height + marginY
|
||||
})
|
||||
} else {
|
||||
// 定位三级及以下节点的top
|
||||
let marginY = this.getMarginY(layerIndex + 1)
|
||||
let baseTop = node.top + node.height / 2 + marginY
|
||||
// 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半
|
||||
let totalTop = baseTop - node.childrenAreaHeight / 2
|
||||
node.children.forEach(cur => {
|
||||
cur.top = totalTop
|
||||
totalTop += cur.height + marginY
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.nodeData.data.expand) {
|
||||
return
|
||||
}
|
||||
if (isRoot) return
|
||||
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
|
||||
let base = this.getMarginY(layerIndex + 1) * 2 + node.height
|
||||
let difference = node.childrenAreaHeight - base
|
||||
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.hasCustomPosition()) return
|
||||
// 三级或三级以下节点自身位置不需要动
|
||||
if (!node.parent.isRoot && item === node) return
|
||||
let _offset = 0
|
||||
// 二级节点上面的兄弟节点不需要移动,自身需要往下移动
|
||||
if (node.parent.isRoot) {
|
||||
// 上面的节点不用移
|
||||
if (_index < index) {
|
||||
_offset = 0
|
||||
} else if (_index > index) {
|
||||
// 下面的节点往下移
|
||||
_offset = addHeight * 2
|
||||
} else {
|
||||
// 自身也要移动
|
||||
_offset = addHeight
|
||||
}
|
||||
} else {
|
||||
// 三级或三级以下节点两侧的兄弟节点向两侧移动
|
||||
// 上面的节点往上移
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 调整兄弟节点的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, 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 { expandBtnSize } = node
|
||||
if (!this.mindMap.opt.alwaysShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let y1 = prevBother.top + prevBother.height
|
||||
let y2 = item.top
|
||||
let x = node.left + node.width / 2
|
||||
let path = `M ${x},${y1} L ${x},${y2}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT) {
|
||||
let nodeRight = node.left + node.width
|
||||
let nodeYCenter = node.top + node.height / 2
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
let offset = (marginX - expandBtnSize) * 0.6
|
||||
node.children.forEach((item, index) => {
|
||||
let itemLeft = item.left
|
||||
let itemYCenter = item.top + item.height / 2
|
||||
let path = `
|
||||
M ${nodeRight},${nodeYCenter}
|
||||
L ${nodeRight + offset},${nodeYCenter}
|
||||
L ${nodeRight + offset},${itemYCenter}
|
||||
L ${itemLeft},${itemYCenter}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
})
|
||||
} else {
|
||||
let nodeLeft = node.left
|
||||
let nodeYCenter = node.top + node.height / 2
|
||||
let marginX = this.getMarginX(node.layerIndex + 1)
|
||||
let offset = (marginX - expandBtnSize) * 0.6
|
||||
node.children.forEach((item, index) => {
|
||||
let itemRight = item.left + item.width
|
||||
let itemYCenter = item.top + item.height / 2
|
||||
let path = `
|
||||
M ${nodeLeft},${nodeYCenter}
|
||||
L ${nodeLeft - offset},${nodeYCenter}
|
||||
L ${nodeLeft - offset},${itemYCenter}
|
||||
L ${itemRight},${itemYCenter}`
|
||||
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
|
||||
}
|
||||
node.children.forEach((item, index) => {
|
||||
if (node.isRoot) {
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let y1 = prevBother.top + prevBother.height
|
||||
let y2 = item.top
|
||||
let x = node.left + node.width / 2
|
||||
let path = `M ${x},${y1} L ${x},${y2}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
let x1 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? left - expandBtnSize
|
||||
: left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? item.left + item.width
|
||||
: item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
let path = `M ${x1},${y1} L ${x2},${y2}`
|
||||
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
|
||||
}
|
||||
node.children.forEach((item, index) => {
|
||||
if (node.isRoot) {
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let y1 = prevBother.top + prevBother.height
|
||||
let y2 = item.top
|
||||
let x = node.left + node.width / 2
|
||||
let path = `M ${x},${y1} L ${x},${y2}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
let x1 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? left - expandBtnSize
|
||||
: left + width + expandBtnSize
|
||||
let y1 = top + height / 2
|
||||
let x2 =
|
||||
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
? item.left + item.width
|
||||
: item.left
|
||||
let y2 = item.top + item.height / 2
|
||||
let path = this.cubicBezierPath(x1, y1, x2, y2)
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT) {
|
||||
btn.translate(width - translateX, height / 2 - translateY)
|
||||
} else {
|
||||
btn.translate(-expandBtnSize - translateX, height / 2 - translateY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let isLeft = node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h', isLeft)
|
||||
let x = isLeft
|
||||
? left - generalizationLineMargin
|
||||
: right + generalizationLineMargin
|
||||
let x1 = x
|
||||
let y1 = top
|
||||
let x2 = x
|
||||
let y2 = bottom
|
||||
let cx = x1 + (isLeft ? -20 : 20)
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left =
|
||||
x +
|
||||
(isLeft ? -generalizationNodeMargin : generalizationNodeMargin) -
|
||||
(isLeft ? gNode.width : 0)
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
|
||||
rect.size(expandBtnSize, height).x(-expandBtnSize).y(0)
|
||||
} else {
|
||||
rect.size(expandBtnSize, height).x(width).y(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default VerticalTimeline
|
||||
@@ -28,7 +28,7 @@ const handleList = node => {
|
||||
}
|
||||
|
||||
// 将markdown转换成节点树
|
||||
export const transformMarkdownTo = async md => {
|
||||
export const transformMarkdownTo = md => {
|
||||
const tree = fromMarkdown(md)
|
||||
let root = {
|
||||
children: []
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import JSZip from 'jszip'
|
||||
import xmlConvert from 'xml-js'
|
||||
import {
|
||||
getTextFromHtml,
|
||||
imgToDataUrl,
|
||||
parseDataUrl,
|
||||
getImageSize
|
||||
} from '../utils/index'
|
||||
|
||||
// 解析.xmind文件
|
||||
const parseXmindFile = file => {
|
||||
@@ -10,7 +16,7 @@ const parseXmindFile = file => {
|
||||
let content = ''
|
||||
if (zip.files['content.json']) {
|
||||
let json = await zip.files['content.json'].async('string')
|
||||
content = transformXmind(json)
|
||||
content = await transformXmind(json, zip.files)
|
||||
} else if (zip.files['content.xml']) {
|
||||
let xml = await zip.files['content.xml'].async('string')
|
||||
let json = xmlConvert.xml2json(xml)
|
||||
@@ -33,18 +39,20 @@ const parseXmindFile = file => {
|
||||
}
|
||||
|
||||
// 转换xmind数据
|
||||
const transformXmind = content => {
|
||||
const transformXmind = async (content, files) => {
|
||||
let data = JSON.parse(content)[0]
|
||||
let nodeTree = data.rootTopic
|
||||
let newTree = {}
|
||||
let walk = (node, newNode) => {
|
||||
let waitLoadImageList = []
|
||||
let walk = async (node, newNode) => {
|
||||
newNode.data = {
|
||||
// 节点内容
|
||||
text: node.title
|
||||
}
|
||||
// 节点备注
|
||||
if (node.notes) {
|
||||
newNode.data.note = (node.notes.realHTML || node.notes.plain).content
|
||||
let notesData = node.notes.realHTML || node.notes.plain
|
||||
newNode.data.note = notesData ? notesData.content || '' : ''
|
||||
}
|
||||
// 超链接
|
||||
if (node.href && /^https?:\/\//.test(node.href)) {
|
||||
@@ -54,6 +62,42 @@ const transformXmind = content => {
|
||||
if (node.labels && node.labels.length > 0) {
|
||||
newNode.data.tag = node.labels
|
||||
}
|
||||
// 图片
|
||||
if (node.image && /\.(jpg|jpeg|png|gif|webp)$/.test(node.image.src)) {
|
||||
// 处理异步逻辑
|
||||
let resolve = null
|
||||
let promise = new Promise(_resolve => {
|
||||
resolve = _resolve
|
||||
})
|
||||
waitLoadImageList.push(promise)
|
||||
try {
|
||||
// 读取图片
|
||||
let imageType = /\.([^.]+)$/.exec(node.image.src)[1]
|
||||
let imageBase64 =
|
||||
`data:image/${imageType};base64,` +
|
||||
(await files['resources/' + node.image.src.split('/')[1]].async(
|
||||
'base64'
|
||||
))
|
||||
newNode.data.image = imageBase64
|
||||
// 如果图片尺寸不存在
|
||||
if (!node.image.width && !node.image.height) {
|
||||
let imageSize = await getImageSize(imageBase64)
|
||||
newNode.data.imageSize = {
|
||||
width: imageSize.width,
|
||||
height: imageSize.height
|
||||
}
|
||||
} else {
|
||||
newNode.data.imageSize = {
|
||||
width: node.image.width,
|
||||
height: node.image.height
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
// 子节点
|
||||
newNode.children = []
|
||||
if (
|
||||
@@ -69,6 +113,7 @@ const transformXmind = content => {
|
||||
}
|
||||
}
|
||||
walk(nodeTree, newTree)
|
||||
await Promise.all(waitLoadImageList)
|
||||
return newTree
|
||||
}
|
||||
|
||||
@@ -78,6 +123,7 @@ const transformOldXmind = content => {
|
||||
let elements = data.elements
|
||||
let root = null
|
||||
let getRoot = arr => {
|
||||
if (!arr) return
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (!root && arr[i].name === 'topic') {
|
||||
root = arr[i]
|
||||
@@ -97,9 +143,10 @@ const transformOldXmind = content => {
|
||||
}
|
||||
let walk = (node, newNode) => {
|
||||
let nodeElements = node.elements
|
||||
let nodeTitle = getItemByName(nodeElements, 'title')
|
||||
newNode.data = {
|
||||
// 节点内容
|
||||
text: getItemByName(nodeElements, 'title').elements[0].text
|
||||
text: nodeTitle && nodeTitle.elements && nodeTitle.elements[0].text
|
||||
}
|
||||
try {
|
||||
// 节点备注
|
||||
@@ -157,8 +204,127 @@ const transformOldXmind = content => {
|
||||
return newTree
|
||||
}
|
||||
|
||||
// 数据转换为xmind文件
|
||||
const transformToXmind = async (data, name) => {
|
||||
const id = 'simpleMindMap_' + Date.now()
|
||||
const imageList = []
|
||||
// 转换核心数据
|
||||
let newTree = {}
|
||||
let waitLoadImageList = []
|
||||
let walk = async (node, newNode, isRoot) => {
|
||||
let newData = {
|
||||
structureClass: 'org.xmind.ui.logic.right',
|
||||
title: getTextFromHtml(node.data.text), // 节点文本
|
||||
children: {
|
||||
attached: []
|
||||
}
|
||||
}
|
||||
// 备注
|
||||
if (node.data.note !== undefined) {
|
||||
newData.notes = {
|
||||
realHTML: {
|
||||
content: node.data.note
|
||||
},
|
||||
plain: {
|
||||
content: node.data.note
|
||||
}
|
||||
}
|
||||
}
|
||||
// 超链接
|
||||
if (node.data.hyperlink !== undefined) {
|
||||
newData.href = node.data.hyperlink
|
||||
}
|
||||
// 标签
|
||||
if (node.data.tag !== undefined) {
|
||||
newData.labels = node.data.tag || []
|
||||
}
|
||||
// 图片
|
||||
if (node.data.image) {
|
||||
try {
|
||||
// 处理异步逻辑
|
||||
let resolve = null
|
||||
let promise = new Promise(_resolve => {
|
||||
resolve = _resolve
|
||||
})
|
||||
waitLoadImageList.push(promise)
|
||||
let imgName = ''
|
||||
let imgData = node.data.image
|
||||
// 网络图片要先转换成data:url
|
||||
if (/^https?:\/\//.test(node.data.image)) {
|
||||
imgData = await imgToDataUrl(node.data.image)
|
||||
}
|
||||
// 从data:url中解析出图片类型和base64
|
||||
let dataUrlRes = parseDataUrl(imgData)
|
||||
imgName = 'image_' + imageList.length + '.' + dataUrlRes.type
|
||||
imageList.push({
|
||||
name: imgName,
|
||||
data: dataUrlRes.base64
|
||||
})
|
||||
newData.image = {
|
||||
src: 'xap:resources/' + imgName,
|
||||
width: node.data.imageSize.width,
|
||||
height: node.data.imageSize.height
|
||||
}
|
||||
resolve()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
// 样式
|
||||
// 暂时不考虑样式
|
||||
if (isRoot) {
|
||||
newData.class = 'topic'
|
||||
newNode.id = id
|
||||
newNode.class = 'sheet'
|
||||
newNode.title = name
|
||||
newNode.extensions = []
|
||||
newNode.topicPositioning = 'fixed'
|
||||
newNode.topicOverlapping = 'overlap'
|
||||
newNode.coreVersion = '2.100.0'
|
||||
newNode.rootTopic = newData
|
||||
} else {
|
||||
Object.keys(newData).forEach(key => {
|
||||
newNode[key] = newData[key]
|
||||
})
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(child => {
|
||||
let newChild = {}
|
||||
walk(child, newChild)
|
||||
newData.children.attached.push(newChild)
|
||||
})
|
||||
}
|
||||
}
|
||||
walk(data, newTree, true)
|
||||
await Promise.all(waitLoadImageList)
|
||||
const contentData = [newTree]
|
||||
// 创建压缩包
|
||||
const zip = new JSZip()
|
||||
zip.file('content.json', JSON.stringify(contentData))
|
||||
zip.file(
|
||||
'metadata.json',
|
||||
`{"modifier":"","dataStructureVersion":"1","layoutEngineVersion":"2","activeSheetId":"${id}"}`
|
||||
)
|
||||
const manifestData = {
|
||||
'file-entries': { 'content.json': {}, 'metadata.json': {} }
|
||||
}
|
||||
// 图片
|
||||
if (imageList.length > 0) {
|
||||
imageList.forEach(item => {
|
||||
manifestData['file-entries']['resources/' + item.name] = {}
|
||||
const img = zip.folder('resources')
|
||||
img.file(item.name, item.data, { base64: true })
|
||||
})
|
||||
}
|
||||
zip.file('manifest.json', JSON.stringify(manifestData))
|
||||
const zipData = await zip.generateAsync({ type: 'blob' })
|
||||
return zipData
|
||||
}
|
||||
|
||||
export default {
|
||||
parseXmindFile,
|
||||
transformXmind,
|
||||
transformOldXmind
|
||||
transformOldXmind,
|
||||
transformToXmind
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { walk, bfsWalk, throttle } from './utils/'
|
||||
import { walk, bfsWalk, throttle } from '../utils'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import {
|
||||
getAssociativeLineTargetIndex,
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
getNodePoint,
|
||||
computeNodePoints,
|
||||
getNodeLinePath
|
||||
} from './utils/associativeLineUtils'
|
||||
import associativeLineControlsMethods from './utils/associativeLineControls'
|
||||
import associativeLineTextMethods from './utils/associativeLineText'
|
||||
} from './associativeLine/associativeLineUtils'
|
||||
import associativeLineControlsMethods from './associativeLine/associativeLineControls'
|
||||
import associativeLineTextMethods from './associativeLine/associativeLineText'
|
||||
|
||||
// 关联线类
|
||||
// 关联线插件
|
||||
class AssociativeLine {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
@@ -91,12 +91,7 @@ class AssociativeLine {
|
||||
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)
|
||||
})
|
||||
this.mindMap.on('mouseup', this.onControlPointMouseup.bind(this))
|
||||
// 缩放事件
|
||||
this.mindMap.on('scale', this.onScale)
|
||||
}
|
||||
@@ -266,12 +261,13 @@ class AssociativeLine {
|
||||
|
||||
// 鼠标移动事件
|
||||
onMousemove(e) {
|
||||
if (!this.isCreatingLine) return
|
||||
this.onControlPointMousemove(e)
|
||||
this.updateCreatingLine(e)
|
||||
}
|
||||
|
||||
// 更新创建过程中的连接线
|
||||
updateCreatingLine(e) {
|
||||
if (!this.isCreatingLine) return
|
||||
let { x, y } = this.getTransformedEventPos(e)
|
||||
let startPoint = getNodePoint(this.creatingStartNode)
|
||||
let offsetX = x > startPoint.x ? -10 : 10
|
||||
@@ -1,8 +1,7 @@
|
||||
import { bfsWalk, throttle } from './utils'
|
||||
import Base from './layouts/Base'
|
||||
|
||||
// 节点拖动类
|
||||
import { bfsWalk, throttle } from '../utils'
|
||||
import Base from '../layouts/Base'
|
||||
|
||||
// 节点拖动插件
|
||||
class Drag extends Base {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
@@ -45,6 +44,7 @@ class Drag extends Base {
|
||||
this.mouseMoveY = 0
|
||||
// 鼠标移动的距离距鼠标按下的位置距离多少以上才认为是拖动事件
|
||||
this.checkDragOffset = 10
|
||||
this.minOffset = 10
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
@@ -106,6 +106,9 @@ class Drag extends Base {
|
||||
this.node.isDrag = false
|
||||
this.node.show()
|
||||
this.removeCloneNode()
|
||||
let overlapNodeUid = this.overlapNode ? this.overlapNode.nodeData.data.uid : ''
|
||||
let prevNodeUid = this.prevNode ? this.prevNode.nodeData.data.uid : ''
|
||||
let nextNodeUid = this.nextNode ? this.nextNode.nodeData.data.uid : ''
|
||||
// 存在重叠子节点,则移动作为其子节点
|
||||
if (this.overlapNode) {
|
||||
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
|
||||
@@ -135,7 +138,11 @@ class Drag extends Base {
|
||||
this.mindMap.render()
|
||||
}
|
||||
this.reset()
|
||||
this.mindMap.emit('node_dragend')
|
||||
this.mindMap.emit('node_dragend', {
|
||||
overlapNodeUid,
|
||||
prevNodeUid,
|
||||
nextNodeUid
|
||||
})
|
||||
}
|
||||
|
||||
// 创建克隆节点
|
||||
@@ -197,9 +204,10 @@ class Drag extends Base {
|
||||
|
||||
// 检测重叠节点
|
||||
checkOverlapNode() {
|
||||
if (!this.drawTransform) {
|
||||
if (!this.drawTransform || !this.placeholder) {
|
||||
return
|
||||
}
|
||||
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt
|
||||
let x = this.mouseMoveX
|
||||
let y = this.mouseMoveY
|
||||
this.overlapNode = null
|
||||
@@ -241,19 +249,19 @@ class Drag extends Base {
|
||||
let prevNodeRect = this.getNodeRect(prevBrother)
|
||||
prevBrotherOffset = nodeRect.top - prevNodeRect.bottom
|
||||
// 间距小于10就当它不存在
|
||||
prevBrotherOffset = prevBrotherOffset >= 10 ? prevBrotherOffset / 2 : 0
|
||||
prevBrotherOffset = prevBrotherOffset >= this.minOffset ? prevBrotherOffset / 2 : 0
|
||||
} else {
|
||||
// 没有前一个兄弟节点,那么假设和前一个节点的距离为20
|
||||
prevBrotherOffset = 10
|
||||
prevBrotherOffset = this.minOffset
|
||||
}
|
||||
// 和后一个兄弟节点的距离
|
||||
let nextBrotherOffset = 0
|
||||
if (nextBrother) {
|
||||
let nextNodeRect = this.getNodeRect(nextBrother)
|
||||
nextBrotherOffset = nextNodeRect.top - nodeRect.bottom
|
||||
nextBrotherOffset = nextBrotherOffset >= 10 ? nextBrotherOffset / 2 : 0
|
||||
nextBrotherOffset = nextBrotherOffset >= this.minOffset ? nextBrotherOffset / 2 : 0
|
||||
} else {
|
||||
nextBrotherOffset = 10
|
||||
nextBrotherOffset = this.minOffset
|
||||
}
|
||||
if (nodeRect.left <= x && nodeRect.right >= x) {
|
||||
// 检测兄弟节点位置
|
||||
@@ -266,11 +274,11 @@ class Drag extends Base {
|
||||
y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
|
||||
if (checkIsPrevNode) {
|
||||
this.prevNode = node
|
||||
let size = nextBrotherOffset > 0 ? nextBrotherOffset : 5
|
||||
let size = nextBrotherOffset > 0 ? Math.min(nextBrotherOffset, nodeDragPlaceholderMaxSize) : 5
|
||||
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom)
|
||||
} else if (checkIsNextNode) {
|
||||
this.nextNode = node
|
||||
let size = prevBrotherOffset > 0 ? prevBrotherOffset : 5
|
||||
let size = prevBrotherOffset > 0 ? Math.min(prevBrotherOffset, nodeDragPlaceholderMaxSize) : 5
|
||||
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originTop - size)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
import { imgToDataUrl, downloadFile, readBlob } from './utils'
|
||||
import JsPDF from 'jspdf'
|
||||
import {
|
||||
imgToDataUrl,
|
||||
downloadFile,
|
||||
readBlob,
|
||||
removeHTMLEntities
|
||||
} from '../utils'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import drawBackgroundImageToCanvas from './utils/simulateCSSBackgroundInCanvas'
|
||||
import { transformToMarkdown } from './parse/toMarkdown'
|
||||
const URL = window.URL || window.webkitURL || window
|
||||
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
|
||||
import { transformToMarkdown } from '../parse/toMarkdown'
|
||||
|
||||
// 导出类
|
||||
// 导出插件
|
||||
class Export {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
this.mindMap = opt.mindMap
|
||||
this.exportPadding = this.mindMap.opt.exportPadding
|
||||
}
|
||||
|
||||
// 导出
|
||||
@@ -51,7 +53,7 @@ class Export {
|
||||
}
|
||||
|
||||
// svg转png
|
||||
svgToPng(svgSrc, transparent) {
|
||||
svgToPng(svgSrc, transparent, rotateWhenWidthLongerThenHeight = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
|
||||
@@ -59,12 +61,24 @@ class Export {
|
||||
img.onload = async () => {
|
||||
try {
|
||||
let canvas = document.createElement('canvas')
|
||||
canvas.width = img.width + this.exportPadding * 2
|
||||
canvas.height = img.height + this.exportPadding * 2
|
||||
// 如果宽比高长,那么旋转90度
|
||||
let needRotate =
|
||||
rotateWhenWidthLongerThenHeight && img.width / img.height > 1
|
||||
if (needRotate) {
|
||||
canvas.width = img.height
|
||||
canvas.height = img.width
|
||||
} else {
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
}
|
||||
let ctx = canvas.getContext('2d')
|
||||
if (needRotate) {
|
||||
ctx.rotate(0.5 * Math.PI)
|
||||
ctx.translate(0, -img.height)
|
||||
}
|
||||
// 绘制背景
|
||||
if (!transparent) {
|
||||
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
|
||||
await this.drawBackgroundToCanvas(ctx, img.width, img.height)
|
||||
}
|
||||
// 图片绘制到canvas里
|
||||
ctx.drawImage(
|
||||
@@ -73,8 +87,8 @@ class Export {
|
||||
0,
|
||||
img.width,
|
||||
img.height,
|
||||
this.exportPadding,
|
||||
this.exportPadding,
|
||||
0,
|
||||
0,
|
||||
img.width,
|
||||
img.height
|
||||
)
|
||||
@@ -98,7 +112,7 @@ class Export {
|
||||
backgroundImage,
|
||||
backgroundRepeat = 'no-repeat',
|
||||
backgroundPosition = 'center center',
|
||||
backgroundSize = 'cover',
|
||||
backgroundSize = 'cover'
|
||||
} = this.mindMap.themeConfig
|
||||
// 背景颜色
|
||||
ctx.save()
|
||||
@@ -109,18 +123,25 @@ class Export {
|
||||
// 背景图片
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
ctx.save()
|
||||
drawBackgroundImageToCanvas(ctx, width, height, backgroundImage, {
|
||||
backgroundRepeat,
|
||||
backgroundPosition,
|
||||
backgroundSize
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
drawBackgroundImageToCanvas(
|
||||
ctx,
|
||||
width,
|
||||
height,
|
||||
backgroundImage,
|
||||
{
|
||||
backgroundRepeat,
|
||||
backgroundPosition,
|
||||
backgroundSize
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
ctx.restore()
|
||||
})
|
||||
)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
@@ -154,12 +175,17 @@ class Export {
|
||||
* 方法1.把svg的图片都转化成data:url格式,再转换
|
||||
* 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换
|
||||
*/
|
||||
async png(name, transparent = false) {
|
||||
async png(name, transparent = false, rotateWhenWidthLongerThenHeight) {
|
||||
let { node, str } = await this.getSvgData()
|
||||
str = removeHTMLEntities(str)
|
||||
// 如果开启了富文本,则使用htmltocanvas转换为图片
|
||||
if (this.mindMap.richText) {
|
||||
let res = await this.mindMap.richText.handleExportPng(node.node)
|
||||
let imgDataUrl = await this.svgToPng(res, transparent)
|
||||
let res = await this.mindMap.richText.handleExportPng(node.node)
|
||||
let imgDataUrl = await this.svgToPng(
|
||||
res,
|
||||
transparent,
|
||||
rotateWhenWidthLongerThenHeight
|
||||
)
|
||||
return imgDataUrl
|
||||
}
|
||||
// 转换成blob数据
|
||||
@@ -169,40 +195,32 @@ class Export {
|
||||
// 转换成data:url数据
|
||||
let svgUrl = await readBlob(blob)
|
||||
// 绘制到canvas上
|
||||
let res = await this.svgToPng(svgUrl, transparent)
|
||||
let res = await this.svgToPng(
|
||||
svgUrl,
|
||||
transparent,
|
||||
rotateWhenWidthLongerThenHeight
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
// 导出为pdf
|
||||
async pdf(name) {
|
||||
let img = await this.png()
|
||||
let pdf = new JsPDF('', 'pt', 'a4')
|
||||
let a4Width = 595
|
||||
let a4Height = 841
|
||||
let a4Ratio = a4Width / a4Height
|
||||
let image = new Image()
|
||||
image.onload = () => {
|
||||
let imageWidth = image.width
|
||||
let imageHeight = image.height
|
||||
let imageRatio = imageWidth / imageHeight
|
||||
let w, h
|
||||
if (imageWidth <= a4Width && imageHeight <= a4Height) {
|
||||
// 使用图片原始宽高
|
||||
w = imageWidth
|
||||
h = imageHeight
|
||||
} else if (a4Ratio > imageRatio) {
|
||||
// 以a4Height为高度,缩放图片宽度
|
||||
w = imageRatio * a4Height
|
||||
h = a4Height
|
||||
} else {
|
||||
// 以a4Width为宽度,缩放图片高度
|
||||
w = a4Width
|
||||
h = a4Width / imageRatio
|
||||
}
|
||||
pdf.addImage(img, 'PNG', (a4Width - w) / 2, (a4Height - h) / 2, w, h)
|
||||
pdf.save(name)
|
||||
async pdf(name, useMultiPageExport) {
|
||||
if (!this.mindMap.doExportPDF) {
|
||||
throw new Error('请注册ExportPDF插件')
|
||||
}
|
||||
image.src = img
|
||||
let img = await this.png('', false, true)
|
||||
this.mindMap.doExportPDF.pdf(name, img, useMultiPageExport)
|
||||
}
|
||||
|
||||
// 导出为xmind
|
||||
async xmind(name) {
|
||||
if (!this.mindMap.doExportXMind) {
|
||||
throw new Error('请注册ExportXMind插件')
|
||||
}
|
||||
const data = this.mindMap.getData()
|
||||
const blob = await this.mindMap.doExportXMind.xmind(data, name)
|
||||
const res = await readBlob(blob)
|
||||
return res
|
||||
}
|
||||
|
||||
// 导出为svg
|
||||
@@ -221,6 +239,7 @@ class Export {
|
||||
node.first().before(SVG(`<title>${name}</title>`))
|
||||
await this.drawBackgroundToSvg(node)
|
||||
let str = node.svg()
|
||||
str = removeHTMLEntities(str)
|
||||
// 转换成blob数据
|
||||
let blob = new Blob([str], {
|
||||
type: 'image/svg+xml'
|
||||
99
simple-mind-map/src/plugins/ExportPDF.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import JsPDF from 'jspdf'
|
||||
|
||||
// 导出PDF插件,需要通过Export插件使用
|
||||
class ExportPDF {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
this.mindMap = opt.mindMap
|
||||
}
|
||||
|
||||
// 导出为pdf
|
||||
pdf(name, img, useMultiPageExport = false) {
|
||||
if (useMultiPageExport) {
|
||||
this.multiPageExport(name, img)
|
||||
} else {
|
||||
this.onePageExport(name, img)
|
||||
}
|
||||
}
|
||||
|
||||
// 单页导出
|
||||
onePageExport(name, img) {
|
||||
let pdf = new JsPDF('', 'pt', 'a4')
|
||||
let a4Width = 595
|
||||
let a4Height = 841
|
||||
let a4Ratio = a4Width / a4Height
|
||||
let image = new Image()
|
||||
image.onload = () => {
|
||||
let imageWidth = image.width
|
||||
let imageHeight = image.height
|
||||
let imageRatio = imageWidth / imageHeight
|
||||
let w, h
|
||||
if (imageWidth <= a4Width && imageHeight <= a4Height) {
|
||||
// 使用图片原始宽高
|
||||
w = imageWidth
|
||||
h = imageHeight
|
||||
} else if (a4Ratio > imageRatio) {
|
||||
// 以a4Height为高度,缩放图片宽度
|
||||
w = imageRatio * a4Height
|
||||
h = a4Height
|
||||
} else {
|
||||
// 以a4Width为宽度,缩放图片高度
|
||||
w = a4Width
|
||||
h = a4Width / imageRatio
|
||||
}
|
||||
pdf.addImage(img, 'PNG', (a4Width - w) / 2, (a4Height - h) / 2, w, h)
|
||||
pdf.save(name)
|
||||
}
|
||||
image.src = img
|
||||
}
|
||||
|
||||
// 多页导出
|
||||
multiPageExport(name, img) {
|
||||
let image = new Image()
|
||||
const a4Width = 592.28
|
||||
const a4Height = 841.89
|
||||
image.onload = () => {
|
||||
let imageWidth = image.width
|
||||
let imageHeight = image.height
|
||||
// 一页pdf显示高度
|
||||
let pageHeight = (imageWidth / a4Width) * a4Height
|
||||
// 未生成pdf的高度
|
||||
let leftHeight = imageHeight
|
||||
// 偏移
|
||||
let position = 0
|
||||
// a4纸的尺寸[595.28,841.89],图片在pdf中图片的宽高
|
||||
let imgWidth = a4Width
|
||||
let imgHeight = (a4Width / imageWidth) * imageHeight
|
||||
let pdf = new JsPDF('', 'pt', 'a4')
|
||||
// 有两个高度需要区分,一个是图片的实际高度,和生成pdf的页面高度(841.89)
|
||||
// 当内容未超过pdf一页显示的范围,无需分页
|
||||
if (leftHeight < pageHeight) {
|
||||
pdf.addImage(
|
||||
img,
|
||||
'PNG',
|
||||
(a4Width - imgWidth) / 2,
|
||||
(a4Height - imgHeight) / 2,
|
||||
imgWidth,
|
||||
imgHeight
|
||||
)
|
||||
} else {
|
||||
// 分页
|
||||
while (leftHeight > 0) {
|
||||
pdf.addImage(img, 'PNG', 0, position, imgWidth, imgHeight)
|
||||
leftHeight -= pageHeight
|
||||
position -= a4Height
|
||||
// 避免添加空白页
|
||||
if (leftHeight > 0) {
|
||||
pdf.addPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
pdf.save(name)
|
||||
}
|
||||
image.src = img
|
||||
}
|
||||
}
|
||||
|
||||
ExportPDF.instanceName = 'doExportPDF'
|
||||
|
||||
export default ExportPDF
|
||||
24
simple-mind-map/src/plugins/ExportXMind.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import xmind from '../parse/xmind'
|
||||
|
||||
// 导出XMind插件,需要通过Export插件使用
|
||||
class ExportXMind {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
this.mindMap = opt.mindMap
|
||||
}
|
||||
|
||||
// 导出xmind
|
||||
async xmind(data, name) {
|
||||
const zipData = await xmind.transformToXmind(data, name)
|
||||
return zipData
|
||||
}
|
||||
|
||||
// 获取解析器
|
||||
getXmind() {
|
||||
return xmind
|
||||
}
|
||||
}
|
||||
|
||||
ExportXMind.instanceName = 'doExportXMind'
|
||||
|
||||
export default ExportXMind
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bfsWalk } from './utils'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
import { bfsWalk } from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
// 键盘导航类
|
||||
// 键盘导航插件
|
||||
class KeyboardNavigation {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
@@ -28,8 +28,7 @@ class KeyboardNavigation {
|
||||
this.focus(dir)
|
||||
} else {
|
||||
let root = this.mindMap.renderer.root
|
||||
this.mindMap.renderer.moveNodeToCenter(root)
|
||||
root.active()
|
||||
this.mindMap.execCommand('GO_TARGET_NODE', root)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +80,7 @@ class KeyboardNavigation {
|
||||
|
||||
// 找到了则让目标节点聚焦
|
||||
if (targetNode) {
|
||||
this.mindMap.renderer.moveNodeToCenter(targetNode)
|
||||
targetNode.active()
|
||||
this.mindMap.execCommand('GO_TARGET_NODE', targetNode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// 小地图类
|
||||
import { isWhite, isTransparent, getVisibleColorFromTheme } from '../utils/index'
|
||||
|
||||
// 小地图插件
|
||||
class MiniMap {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
@@ -20,9 +22,14 @@ class MiniMap {
|
||||
* boxHeight:小地图容器的高度
|
||||
*/
|
||||
calculationMiniMap(boxWidth, boxHeight) {
|
||||
let { svgHTML, rect, origWidth, origHeight, scaleX, scaleY } =
|
||||
let { svg, rect, origWidth, origHeight, scaleX, scaleY } =
|
||||
this.mindMap.getSvgData()
|
||||
// 计算数据
|
||||
const elRect = this.mindMap.elRect
|
||||
rect.x -= elRect.left
|
||||
rect.x2 -= elRect.left
|
||||
rect.y -= elRect.top
|
||||
rect.y2 -= elRect.top
|
||||
let boxRatio = boxWidth / boxHeight
|
||||
let actWidth = 0
|
||||
let actHeight = 0
|
||||
@@ -53,20 +60,31 @@ class MiniMap {
|
||||
bottom: 0
|
||||
}
|
||||
viewBoxStyle.left =
|
||||
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft + 'px'
|
||||
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft
|
||||
viewBoxStyle.right =
|
||||
Math.max(0, ((_rectX2 - origWidth) / _rectWidth) * actWidth) +
|
||||
miniMapBoxLeft +
|
||||
'px'
|
||||
miniMapBoxLeft
|
||||
|
||||
viewBoxStyle.top =
|
||||
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop + 'px'
|
||||
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop
|
||||
viewBoxStyle.bottom =
|
||||
Math.max(0, ((_rectY2 - origHeight) / _rectHeight) * actHeight) +
|
||||
miniMapBoxTop +
|
||||
'px'
|
||||
miniMapBoxTop
|
||||
|
||||
if (viewBoxStyle.top > miniMapBoxTop + actHeight) {
|
||||
viewBoxStyle.top = miniMapBoxTop + actHeight
|
||||
}
|
||||
if (viewBoxStyle.left > miniMapBoxLeft + actWidth) {
|
||||
viewBoxStyle.left = miniMapBoxLeft + actWidth
|
||||
}
|
||||
|
||||
Object.keys(viewBoxStyle).forEach((key) => {
|
||||
viewBoxStyle[key] = viewBoxStyle[key] + 'px'
|
||||
})
|
||||
|
||||
this.removeNodeContent(svg)
|
||||
return {
|
||||
svgHTML, // 小地图html
|
||||
svgHTML: svg.svg(), // 小地图html
|
||||
viewBoxStyle, // 视图框的位置信息
|
||||
miniMapBoxScale, // 视图框的缩放值
|
||||
miniMapBoxLeft, // 视图框的left值
|
||||
@@ -74,6 +92,26 @@ class MiniMap {
|
||||
}
|
||||
}
|
||||
|
||||
// 移除节点的内容
|
||||
removeNodeContent(svg) {
|
||||
if (svg.hasClass('smm-node')) {
|
||||
let shape = svg.findOne('.smm-node-shape')
|
||||
let fill = shape.attr('fill')
|
||||
if (isWhite(fill) || isTransparent(fill)) {
|
||||
shape.attr('fill', getVisibleColorFromTheme(this.mindMap.themeConfig))
|
||||
}
|
||||
svg.clear()
|
||||
svg.add(shape)
|
||||
return
|
||||
}
|
||||
let children = svg.children()
|
||||
if (children && children.length > 0) {
|
||||
children.forEach((node) => {
|
||||
this.removeNodeContent(node)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 小地图鼠标按下事件
|
||||
onMousedown(e) {
|
||||
this.isMousedown = true
|
||||
270
simple-mind-map/src/plugins/NodeImgAdjust.js
Normal file
@@ -0,0 +1,270 @@
|
||||
// 节点图片大小调整插件
|
||||
import { resizeImgSizeByOriginRatio } from '../utils/index'
|
||||
import btnsSvg from '../svg/btns'
|
||||
|
||||
class NodeImgAdjust {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
this.mindMap = mindMap
|
||||
this.resizeBtnSize = 26 // 调整按钮的大小
|
||||
this.handleEl = null // 自定义元素,用来渲染临时图片、调整按钮
|
||||
this.isShowHandleEl = false // 自定义元素是否在显示中
|
||||
this.node = null // 当前节点实例
|
||||
this.img = null // 当前节点的图片节点
|
||||
this.rect = null // 当前图片节点的尺寸信息
|
||||
this.isMousedown = false // 当前是否是按住调整按钮状态
|
||||
this.currentImgWidth = 0 // 当前拖拽实时图片的大小
|
||||
this.currentImgHeight = 0
|
||||
this.isAdjusted = false // 是否是拖拽结束后的渲染期间
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
bindEvent() {
|
||||
this.onNodeImgMouseleave = this.onNodeImgMouseleave.bind(this)
|
||||
this.onNodeImgMousemove = this.onNodeImgMousemove.bind(this)
|
||||
this.onMousemove = this.onMousemove.bind(this)
|
||||
this.onMouseup = this.onMouseup.bind(this)
|
||||
this.onRenderEnd = this.onRenderEnd.bind(this)
|
||||
this.mindMap.on('node_img_mouseleave', this.onNodeImgMouseleave)
|
||||
this.mindMap.on('node_img_mousemove', this.onNodeImgMousemove)
|
||||
this.mindMap.on('mousemove', this.onMousemove)
|
||||
this.mindMap.on('mouseup', this.onMouseup)
|
||||
this.mindMap.on('node_mouseup', this.onMouseup)
|
||||
this.mindMap.on('node_tree_render_end', this.onRenderEnd)
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
unBindEvent() {
|
||||
this.mindMap.off('node_img_mouseleave', this.onNodeImgMouseleave)
|
||||
this.mindMap.off('node_img_mousemove', this.onNodeImgMousemove)
|
||||
this.mindMap.off('mousemove', this.onMousemove)
|
||||
this.mindMap.off('mouseup', this.onMouseup)
|
||||
this.mindMap.off('node_mouseup', this.onMouseup)
|
||||
this.mindMap.off('node_tree_render_end', this.onRenderEnd)
|
||||
}
|
||||
|
||||
// 节点图片鼠标移动事件
|
||||
onNodeImgMousemove(node, img) {
|
||||
// 如果当前正在拖动调整中那么直接返回
|
||||
if (this.isMousedown || this.isAdjusted || this.mindMap.opt.readonly) return
|
||||
// 如果在当前节点内移动,以及自定义元素已经是显示状态,那么直接返回
|
||||
if (this.node === node && this.isShowHandleEl) return
|
||||
// 更新当前节点信息
|
||||
this.node = node
|
||||
this.img = img
|
||||
this.rect = this.img.rbox()
|
||||
// 显示自定义元素
|
||||
this.showHandleEl()
|
||||
}
|
||||
|
||||
// 节点图片鼠标移出事件
|
||||
onNodeImgMouseleave() {
|
||||
if (this.isMousedown) return
|
||||
this.hideHandleEl()
|
||||
}
|
||||
|
||||
// 隐藏节点实际的图片
|
||||
hideNodeImage() {
|
||||
if (!this.img) return
|
||||
this.img.hide()
|
||||
}
|
||||
|
||||
// 显示节点实际的图片
|
||||
showNodeImage() {
|
||||
if (!this.img) return
|
||||
this.img.show()
|
||||
}
|
||||
|
||||
// 显示自定义元素
|
||||
showHandleEl() {
|
||||
if (!this.handleEl) {
|
||||
this.createResizeBtnEl()
|
||||
}
|
||||
this.setHandleElRect()
|
||||
this.handleEl.style.display = 'block'
|
||||
this.isShowHandleEl = true
|
||||
}
|
||||
|
||||
// 隐藏自定义元素
|
||||
hideHandleEl() {
|
||||
if (!this.isShowHandleEl) return
|
||||
this.isShowHandleEl = false
|
||||
this.handleEl.style.display = 'none'
|
||||
this.handleEl.style.backgroundImage = ``
|
||||
this.handleEl.style.width = 0
|
||||
this.handleEl.style.height = 0
|
||||
this.handleEl.style.left = 0
|
||||
this.handleEl.style.top = 0
|
||||
}
|
||||
|
||||
// 设置自定义元素尺寸位置信息
|
||||
setHandleElRect() {
|
||||
let { width, height, x, y } = this.rect
|
||||
this.handleEl.style.left = `${x}px`
|
||||
this.handleEl.style.top = `${y}px`
|
||||
this.currentImgWidth = width
|
||||
this.currentImgHeight = height
|
||||
this.updateHandleElSize()
|
||||
}
|
||||
|
||||
// 更新自定义元素宽高
|
||||
updateHandleElSize() {
|
||||
this.handleEl.style.width = `${this.currentImgWidth}px`
|
||||
this.handleEl.style.height = `${this.currentImgHeight}px`
|
||||
}
|
||||
|
||||
// 创建调整按钮元素
|
||||
createResizeBtnEl() {
|
||||
// 容器元素
|
||||
this.handleEl = document.createElement('div')
|
||||
this.handleEl.style.cssText = `
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
display:none;
|
||||
background-size: cover;
|
||||
`
|
||||
this.handleEl.className = 'node-img-handle'
|
||||
// 调整按钮元素
|
||||
const btnEl = document.createElement('div')
|
||||
btnEl.innerHTML = btnsSvg.imgAdjust
|
||||
btnEl.style.cssText = `
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: auto;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
width: ${this.resizeBtnSize}px;
|
||||
height: ${this.resizeBtnSize}px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: nwse-resize;
|
||||
`
|
||||
btnEl.className = 'node-image-resize'
|
||||
// 给按钮元素绑定事件
|
||||
btnEl.addEventListener('mouseenter', () => {
|
||||
// 移入按钮,会触发节点图片的移出事件,所以需要再次显示按钮
|
||||
this.showHandleEl()
|
||||
})
|
||||
btnEl.addEventListener('mouseleave', () => {
|
||||
// 移除按钮,需要隐藏按钮
|
||||
if (this.isMousedown) return
|
||||
this.hideHandleEl()
|
||||
})
|
||||
btnEl.addEventListener('mousedown', e => {
|
||||
e.stopPropagation()
|
||||
this.onMousedown(e)
|
||||
})
|
||||
btnEl.addEventListener('mouseup', e => {
|
||||
setTimeout(() => {
|
||||
//点击后直接松开异常处理; 其他事件响应之后处理
|
||||
this.hideHandleEl()
|
||||
this.isAdjusted = false
|
||||
}, 0)
|
||||
})
|
||||
btnEl.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.handleEl.appendChild(btnEl)
|
||||
// 删除按钮
|
||||
const btnRemove = document.createElement('div')
|
||||
this.handleEl.prepend(btnRemove)
|
||||
btnRemove.className = 'node-image-remove'
|
||||
btnRemove.innerHTML = btnsSvg.remove
|
||||
btnRemove.style.cssText = `
|
||||
position: absolute;
|
||||
right: 0;top:0;color:#fff;
|
||||
pointer-events: auto;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
width: ${this.resizeBtnSize}px;
|
||||
height: ${this.resizeBtnSize}px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
`
|
||||
btnRemove.addEventListener('mouseenter', e => {
|
||||
this.showHandleEl()
|
||||
})
|
||||
btnRemove.addEventListener('mouseleave', e => {
|
||||
if (this.isMousedown) return
|
||||
this.hideHandleEl()
|
||||
})
|
||||
btnRemove.addEventListener('click', e => {
|
||||
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null })
|
||||
})
|
||||
// 添加元素到页面
|
||||
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.handleEl)
|
||||
}
|
||||
|
||||
// 鼠标按钮按下事件
|
||||
onMousedown() {
|
||||
this.isMousedown = true
|
||||
// 隐藏节点实际图片
|
||||
this.hideNodeImage()
|
||||
// 将节点图片渲染到自定义元素上
|
||||
this.handleEl.style.backgroundImage = `url(${this.node.nodeData.data.image})`
|
||||
}
|
||||
|
||||
// 鼠标移动
|
||||
onMousemove(e) {
|
||||
if (!this.isMousedown) return
|
||||
e.preventDefault()
|
||||
// 计算当前拖拽位置对应的图片的实时大小
|
||||
let { width: imageOriginWidth, height: imageOriginHeight } =
|
||||
this.node.nodeData.data.imageSize
|
||||
let newWidth = e.clientX - this.rect.x
|
||||
let newHeight = e.clientY - this.rect.y
|
||||
if (newWidth <= 0 || newHeight <= 0) return
|
||||
let [actWidth, actHeight] = resizeImgSizeByOriginRatio(
|
||||
imageOriginWidth,
|
||||
imageOriginHeight,
|
||||
newWidth,
|
||||
newHeight
|
||||
)
|
||||
this.currentImgWidth = actWidth
|
||||
this.currentImgHeight = actHeight
|
||||
this.updateHandleElSize()
|
||||
}
|
||||
|
||||
// 鼠标松开
|
||||
onMouseup() {
|
||||
if (!this.isMousedown) return
|
||||
// 显示节点实际图片
|
||||
this.showNodeImage()
|
||||
// 隐藏自定义元素
|
||||
this.hideHandleEl()
|
||||
// 更新节点图片为新的大小
|
||||
let { image, imageTitle } = this.node.nodeData.data
|
||||
let { scaleX, scaleY } = this.mindMap.draw.transform()
|
||||
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, {
|
||||
url: image,
|
||||
title: imageTitle,
|
||||
width: this.currentImgWidth / scaleX,
|
||||
height: this.currentImgHeight / scaleY,
|
||||
custom: true // 代表自定义了图片大小
|
||||
})
|
||||
this.isAdjusted = true
|
||||
this.isMousedown = false
|
||||
}
|
||||
|
||||
// 渲染完成事件
|
||||
onRenderEnd() {
|
||||
if (!this.isAdjusted) {
|
||||
this.hideHandleEl()
|
||||
return
|
||||
}
|
||||
this.isAdjusted = false
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
NodeImgAdjust.instanceName = 'nodeImgAdjust'
|
||||
|
||||
export default NodeImgAdjust
|
||||
76
simple-mind-map/src/plugins/Painter.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { nodeDataNoStylePropList } from '../constants/constant'
|
||||
|
||||
// 格式刷插件
|
||||
class Painter {
|
||||
constructor({ mindMap }) {
|
||||
this.mindMap = mindMap
|
||||
this.isInPainter = false
|
||||
this.painterNode = null
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
bindEvent() {
|
||||
this.painterOneNode = this.painterOneNode.bind(this)
|
||||
this.onEndPainter = this.onEndPainter.bind(this)
|
||||
this.mindMap.on('node_click', this.painterOneNode)
|
||||
this.mindMap.on('draw_click', this.onEndPainter)
|
||||
}
|
||||
|
||||
unBindEvent() {
|
||||
this.mindMap.off('node_click', this.painterOneNode)
|
||||
this.mindMap.off('draw_click', this.onEndPainter)
|
||||
}
|
||||
|
||||
// 开始格式刷
|
||||
startPainter() {
|
||||
if (this.mindMap.opt.readonly) return
|
||||
let activeNodeList = this.mindMap.renderer.activeNodeList
|
||||
if (activeNodeList.length <= 0) return
|
||||
this.painterNode = activeNodeList[0]
|
||||
this.isInPainter = true
|
||||
this.mindMap.emit('painter_start')
|
||||
}
|
||||
|
||||
// 结束格式刷
|
||||
endPainter() {
|
||||
this.painterNode = null
|
||||
this.isInPainter = false
|
||||
}
|
||||
|
||||
onEndPainter() {
|
||||
this.endPainter()
|
||||
this.mindMap.emit('painter_end')
|
||||
}
|
||||
|
||||
// 格式刷某个节点
|
||||
painterOneNode(node) {
|
||||
if (
|
||||
!node ||
|
||||
!this.isInPainter ||
|
||||
!this.painterNode ||
|
||||
!node ||
|
||||
node === this.painterNode
|
||||
)
|
||||
return
|
||||
const style = {}
|
||||
const painterNodeData = this.painterNode.nodeData.data
|
||||
Object.keys(painterNodeData).forEach(key => {
|
||||
if (!nodeDataNoStylePropList.includes(key)) {
|
||||
style[key] = painterNodeData[key]
|
||||
}
|
||||
})
|
||||
node.setStyles(style)
|
||||
if (painterNodeData.activeStyle) {
|
||||
node.setStyles(painterNodeData.activeStyle, true)
|
||||
}
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
Painter.instanceName = 'painter'
|
||||
|
||||
export default Painter
|
||||
@@ -1,8 +1,13 @@
|
||||
import Quill from 'quill'
|
||||
import 'quill/dist/quill.snow.css'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { walk, getTextFromHtml } from './utils'
|
||||
import { CONSTANTS } from './utils/constant'
|
||||
import domtoimage from 'dom-to-image-more'
|
||||
import {
|
||||
walk,
|
||||
getTextFromHtml,
|
||||
isWhite,
|
||||
getVisibleColorFromTheme
|
||||
} from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
let extended = false
|
||||
|
||||
@@ -28,7 +33,7 @@ let fontSizeList = new Array(100).fill(0).map((_, index) => {
|
||||
return index + 'px'
|
||||
})
|
||||
|
||||
// 节点支持富文本编辑功能
|
||||
// 富文本编辑插件
|
||||
class RichText {
|
||||
constructor({ mindMap, pluginOpt }) {
|
||||
this.mindMap = mindMap
|
||||
@@ -39,11 +44,15 @@ class RichText {
|
||||
this.range = null
|
||||
this.lastRange = null
|
||||
this.node = null
|
||||
this.isInserting = false
|
||||
this.styleEl = null
|
||||
this.cacheEditingText = ''
|
||||
this.lostStyle = false
|
||||
this.isCompositing = false
|
||||
this.initOpt()
|
||||
this.extendQuill()
|
||||
this.appendCss()
|
||||
this.bindEvent()
|
||||
|
||||
// 处理数据,转成富文本格式
|
||||
if (this.mindMap.opt.data) {
|
||||
@@ -51,6 +60,20 @@ class RichText {
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
this.onCompositionStart = this.onCompositionStart.bind(this)
|
||||
this.onCompositionEnd = this.onCompositionEnd.bind(this)
|
||||
window.addEventListener('compositionstart', this.onCompositionStart)
|
||||
window.addEventListener('compositionend', this.onCompositionEnd)
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
unbindEvent() {
|
||||
window.removeEventListener('compositionstart', this.onCompositionStart)
|
||||
window.removeEventListener('compositionend', this.onCompositionEnd)
|
||||
}
|
||||
|
||||
// 插入样式
|
||||
appendCss() {
|
||||
let cssText = `
|
||||
@@ -59,6 +82,7 @@ class RichText {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
.ql-container {
|
||||
@@ -127,11 +151,18 @@ class RichText {
|
||||
}
|
||||
|
||||
// 显示文本编辑控件
|
||||
showEditText(node, rect) {
|
||||
showEditText(node, rect, isInserting) {
|
||||
if (this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
const {
|
||||
richTextEditFakeInPlace,
|
||||
customInnerElsAppendTo,
|
||||
nodeTextEditZIndex,
|
||||
textAutoWrapWidth
|
||||
} = this.mindMap.opt
|
||||
this.node = node
|
||||
this.isInserting = isInserting
|
||||
if (!rect) rect = node._textData.node.node.getBoundingClientRect()
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
this.mindMap.renderer.textEdit.registerTmpShortcut()
|
||||
@@ -143,43 +174,79 @@ class RichText {
|
||||
let scaleX = rect.width / originWidth
|
||||
let scaleY = rect.height / originHeight
|
||||
// 内边距
|
||||
const paddingX = 6
|
||||
const paddingY = 4
|
||||
let paddingX = 6
|
||||
let paddingY = 4
|
||||
if (richTextEditFakeInPlace) {
|
||||
let paddingValue = node.getPaddingVale()
|
||||
paddingX = paddingValue.paddingX
|
||||
paddingY = paddingValue.paddingY
|
||||
}
|
||||
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.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)
|
||||
this.textEditNode.addEventListener('mousedown', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.textEditNode.addEventListener('keydown', e => {
|
||||
if (this.mindMap.renderer.textEdit.checkIsAutoEnterTextEditKey(e)) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
})
|
||||
const targetNode = customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
// 使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
|
||||
let bgColor = node.style.merge('fillColor')
|
||||
let color = node.style.merge('color')
|
||||
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.zIndex = nodeTextEditZIndex
|
||||
this.textEditNode.style.backgroundColor =
|
||||
bgColor === 'transparent'
|
||||
? isWhite(color)
|
||||
? getVisibleColorFromTheme(this.mindMap.themeConfig)
|
||||
: '#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.maxWidth = textAutoWrapWidth + paddingX * 2 + 'px'
|
||||
this.textEditNode.style.transform = `scale(${scaleX}, ${scaleY})`
|
||||
this.textEditNode.style.transformOrigin = 'left top'
|
||||
if (richTextEditFakeInPlace) {
|
||||
this.textEditNode.style.borderRadius =
|
||||
(node.style.merge('borderRadius') || 5) + 'px'
|
||||
if (node.style.merge('shape') == 'roundedRectangle') {
|
||||
this.textEditNode.style.borderRadius = (node.height || 50) + 'px'
|
||||
}
|
||||
}
|
||||
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.textEditNode.innerHTML =
|
||||
this.cacheEditingText || node.nodeData.data.text
|
||||
}
|
||||
this.initQuillEditor()
|
||||
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
|
||||
this.showTextEdit = true
|
||||
this.focus()
|
||||
// 如果是刚创建的节点,那么默认全选,否则普通激活不全选
|
||||
this.focus(isInserting ? 0 : null)
|
||||
if (!node.nodeData.data.richText) {
|
||||
// 如果是非富文本的情况,需要手动应用文本样式
|
||||
this.setTextStyleIfNotRichText(node)
|
||||
@@ -198,7 +265,7 @@ class RichText {
|
||||
underline: node.style.merge('textDecoration') === 'underline',
|
||||
strike: node.style.merge('textDecoration') === 'line-through'
|
||||
}
|
||||
this.formatAllText(style)
|
||||
this.pureFormatAllText(style)
|
||||
}
|
||||
|
||||
// 获取当前正在编辑的内容
|
||||
@@ -214,7 +281,8 @@ class RichText {
|
||||
return
|
||||
}
|
||||
let html = this.getEditText()
|
||||
let list = nodes && nodes.length > 0 ? nodes : this.mindMap.renderer.activeNodeList
|
||||
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) {
|
||||
@@ -223,15 +291,12 @@ class RichText {
|
||||
}
|
||||
this.mindMap.render()
|
||||
})
|
||||
this.mindMap.emit(
|
||||
'hide_text_edit',
|
||||
this.textEditNode,
|
||||
list
|
||||
)
|
||||
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
|
||||
this.isInserting = false
|
||||
}
|
||||
|
||||
// 初始化Quill富文本编辑器
|
||||
@@ -246,6 +311,12 @@ class RichText {
|
||||
handler: function () {
|
||||
// 覆盖默认的回车键换行
|
||||
}
|
||||
},
|
||||
tab: {
|
||||
key: 9,
|
||||
handler: function () {
|
||||
// 覆盖默认的tab键
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,6 +324,8 @@ class RichText {
|
||||
theme: 'snow'
|
||||
})
|
||||
this.quill.on('selection-change', range => {
|
||||
// 刚创建的节点全选不需要显示操作条
|
||||
if (this.isInserting) return
|
||||
this.lastRange = this.range
|
||||
this.range = null
|
||||
if (range) {
|
||||
@@ -281,6 +354,37 @@ class RichText {
|
||||
)
|
||||
}
|
||||
})
|
||||
this.quill.on('text-change', () => {
|
||||
let contents = this.quill.getContents()
|
||||
let len = contents.ops.length
|
||||
// 如果编辑过程中删除所有字符,那么会丢失主题的样式
|
||||
if (len <= 0 || (len === 1 && contents.ops[0].insert === '\n')) {
|
||||
this.lostStyle = true
|
||||
// 需要删除节点的样式数据
|
||||
this.syncFormatToNodeConfig(null, true)
|
||||
} else if (this.lostStyle && !this.isCompositing) {
|
||||
// 如果处于样式丢失状态,那么需要进行格式化加回样式
|
||||
this.setTextStyleIfNotRichText(this.node)
|
||||
this.lostStyle = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 正则输入中文
|
||||
onCompositionStart() {
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
this.isCompositing = true
|
||||
}
|
||||
|
||||
// 中文输入结束
|
||||
onCompositionEnd() {
|
||||
if (!this.showTextEdit || !this.lostStyle) {
|
||||
return
|
||||
}
|
||||
this.isCompositing = false
|
||||
this.setTextStyleIfNotRichText(this.node)
|
||||
}
|
||||
|
||||
// 选中全部
|
||||
@@ -289,9 +393,9 @@ class RichText {
|
||||
}
|
||||
|
||||
// 聚焦
|
||||
focus() {
|
||||
focus(start) {
|
||||
let len = this.quill.getLength()
|
||||
this.quill.setSelection(len, len)
|
||||
this.quill.setSelection(typeof start === 'number' ? start : len, len)
|
||||
}
|
||||
|
||||
// 格式化当前选中的文本
|
||||
@@ -300,7 +404,9 @@ class RichText {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
@@ -321,6 +427,11 @@ class RichText {
|
||||
// 格式化所有文本
|
||||
formatAllText(config = {}) {
|
||||
this.syncFormatToNodeConfig(config)
|
||||
this.pureFormatAllText(config)
|
||||
}
|
||||
|
||||
// 纯粹的格式化所有文本
|
||||
pureFormatAllText(config = {}) {
|
||||
this.quill.formatText(0, this.quill.getLength(), config)
|
||||
}
|
||||
|
||||
@@ -329,7 +440,14 @@ class RichText {
|
||||
if (!this.node) return
|
||||
if (clear) {
|
||||
// 清除文本样式
|
||||
['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'textDecoration', 'color'].forEach((prop) => {
|
||||
;[
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontWeight',
|
||||
'fontStyle',
|
||||
'textDecoration',
|
||||
'color'
|
||||
].forEach(prop => {
|
||||
delete this.node.nodeData.data[prop]
|
||||
})
|
||||
} else {
|
||||
@@ -412,21 +530,26 @@ class RichText {
|
||||
el.appendChild(node)
|
||||
this.mindMap.el.appendChild(el)
|
||||
// 遍历所有节点,将它们的margin和padding设为0
|
||||
let walk = (root) => {
|
||||
let walk = root => {
|
||||
root.style.margin = 0
|
||||
root.style.padding = 0
|
||||
if (root.hasChildNodes()) {
|
||||
Array.from(root.children).forEach((item) => {
|
||||
Array.from(root.children).forEach(item => {
|
||||
walk(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
walk(node)
|
||||
let canvas = await html2canvas(el, {
|
||||
backgroundColor: null
|
||||
})
|
||||
|
||||
// 如果使用html2canvas
|
||||
// let canvas = await html2canvas(el, {
|
||||
// backgroundColor: null
|
||||
// })
|
||||
// return canvas.toDataURL()
|
||||
|
||||
const res = await domtoimage.toPng(el)
|
||||
this.mindMap.el.removeChild(el)
|
||||
return canvas.toDataURL()
|
||||
return res
|
||||
}
|
||||
|
||||
// 将所有节点转换成非富文本节点
|
||||
@@ -454,13 +577,13 @@ class RichText {
|
||||
|
||||
// 处理导入数据
|
||||
handleSetData(data) {
|
||||
let walk = (root) => {
|
||||
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) => {
|
||||
Array.from(root.children).forEach(item => {
|
||||
walk(item)
|
||||
})
|
||||
}
|
||||
172
simple-mind-map/src/plugins/Search.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import { bfsWalk, getTextFromHtml, isUndef, replaceHtmlText } from '../utils/index'
|
||||
|
||||
// 搜索插件
|
||||
class Search {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
this.mindMap = mindMap
|
||||
// 是否正在搜索
|
||||
this.isSearching = false
|
||||
// 搜索文本
|
||||
this.searchText = ''
|
||||
// 匹配的节点列表
|
||||
this.matchNodeList = []
|
||||
// 当前所在的节点列表索引
|
||||
this.currentIndex = -1
|
||||
// 不要复位搜索文本
|
||||
this.notResetSearchText = false
|
||||
// 是否自动跳转下一个匹配节点
|
||||
this.isJumpNext = false
|
||||
this.onDataChange = this.onDataChange.bind(this)
|
||||
this.mindMap.on('data_change', this.onDataChange)
|
||||
}
|
||||
|
||||
// 节点数据改变了,需要重新搜索
|
||||
onDataChange() {
|
||||
if (this.isJumpNext) {
|
||||
this.isJumpNext = false
|
||||
this.search(this.searchText)
|
||||
return
|
||||
}
|
||||
if (this.notResetSearchText) {
|
||||
this.notResetSearchText = false
|
||||
return
|
||||
}
|
||||
this.searchText = ''
|
||||
}
|
||||
|
||||
// 搜索
|
||||
search(text, callback = () => {}) {
|
||||
if (isUndef(text)) return this.endSearch()
|
||||
text = String(text)
|
||||
this.isSearching = true
|
||||
if (this.searchText === text) {
|
||||
// 和上一次搜索文本一样,那么搜索下一个
|
||||
this.searchNext(callback)
|
||||
} else {
|
||||
// 和上次搜索文本不一样,那么重新开始
|
||||
this.searchText = text
|
||||
this.doSearch()
|
||||
this.searchNext(callback)
|
||||
}
|
||||
this.emitEvent()
|
||||
}
|
||||
|
||||
// 结束搜索
|
||||
endSearch() {
|
||||
if (!this.isSearching) return
|
||||
this.searchText = ''
|
||||
this.matchNodeList = []
|
||||
this.currentIndex = -1
|
||||
this.notResetSearchText = false
|
||||
this.isSearching = false
|
||||
this.emitEvent()
|
||||
}
|
||||
|
||||
// 搜索匹配的节点
|
||||
doSearch() {
|
||||
this.matchNodeList = []
|
||||
this.currentIndex = -1
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
let { richText, text } = node.nodeData.data
|
||||
if (richText) {
|
||||
text = getTextFromHtml(text)
|
||||
}
|
||||
if (text.includes(this.searchText)) {
|
||||
this.matchNodeList.push(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索下一个,定位到下一个匹配节点
|
||||
searchNext(callback) {
|
||||
if (!this.isSearching || this.matchNodeList.length <= 0) return
|
||||
if (this.currentIndex < this.matchNodeList.length - 1) {
|
||||
this.currentIndex++
|
||||
} else {
|
||||
this.currentIndex = 0
|
||||
}
|
||||
let currentNode = this.matchNodeList[this.currentIndex]
|
||||
this.notResetSearchText = true
|
||||
this.mindMap.execCommand('GO_TARGET_NODE', currentNode, () => {
|
||||
this.notResetSearchText = false
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
// 替换当前节点
|
||||
replace(replaceText, jumpNext = false) {
|
||||
if (
|
||||
replaceText === null ||
|
||||
replaceText === undefined ||
|
||||
!this.isSearching ||
|
||||
this.matchNodeList.length <= 0
|
||||
)
|
||||
return
|
||||
// 自动跳转下一个匹配节点
|
||||
this.isJumpNext = jumpNext
|
||||
replaceText = String(replaceText)
|
||||
let currentNode = this.matchNodeList[this.currentIndex]
|
||||
if (!currentNode) return
|
||||
let text = this.getReplacedText(currentNode, this.searchText, replaceText)
|
||||
this.notResetSearchText = true
|
||||
currentNode.setText(text, currentNode.nodeData.data.richText, true)
|
||||
this.matchNodeList = this.matchNodeList.filter(node => {
|
||||
return currentNode !== node
|
||||
})
|
||||
if (this.currentIndex > this.matchNodeList.length - 1) {
|
||||
this.currentIndex = -1
|
||||
} else {
|
||||
this.currentIndex--
|
||||
}
|
||||
this.emitEvent()
|
||||
}
|
||||
|
||||
// 替换所有
|
||||
replaceAll(replaceText) {
|
||||
if (
|
||||
replaceText === null ||
|
||||
replaceText === undefined ||
|
||||
!this.isSearching ||
|
||||
this.matchNodeList.length <= 0
|
||||
)
|
||||
return
|
||||
replaceText = String(replaceText)
|
||||
this.matchNodeList.forEach(node => {
|
||||
let text = this.getReplacedText(node, this.searchText, replaceText)
|
||||
this.mindMap.renderer.setNodeDataRender(
|
||||
node,
|
||||
{
|
||||
text,
|
||||
resetRichText: !!node.nodeData.data.richText
|
||||
},
|
||||
true
|
||||
)
|
||||
})
|
||||
this.mindMap.render()
|
||||
this.mindMap.command.addHistory()
|
||||
this.endSearch()
|
||||
}
|
||||
|
||||
// 获取某个节点替换后的文本
|
||||
getReplacedText(node, searchText, replaceText) {
|
||||
let { richText, text } = node.nodeData.data
|
||||
if (richText) {
|
||||
return replaceHtmlText(text, searchText, replaceText)
|
||||
} else {
|
||||
return text.replaceAll(searchText, replaceText)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送事件
|
||||
emitEvent() {
|
||||
this.mindMap.emit('search_info_change', {
|
||||
currentIndex: this.currentIndex,
|
||||
total: this.matchNodeList.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Search.instanceName = 'search'
|
||||
|
||||
export default Search
|
||||
@@ -1,7 +1,6 @@
|
||||
import { bfsWalk, throttle } from './utils'
|
||||
|
||||
// 选择节点类
|
||||
import { bfsWalk, throttle } from '../utils'
|
||||
|
||||
// 节点选择插件
|
||||
class Select {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
@@ -22,9 +21,14 @@ class Select {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
if (!e.ctrlKey && e.which !== 3) {
|
||||
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
|
||||
if (
|
||||
!e.ctrlKey &&
|
||||
(useLeftKeySelectionRightKeyDrag ? e.which !== 1 : e.which !== 3)
|
||||
) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
this.isMousedown = true
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseDownX = x
|
||||
@@ -146,25 +150,24 @@ class Select {
|
||||
let bottom = (top + height) * scaleY + translateY
|
||||
left = left * scaleX + translateX
|
||||
top = top * scaleY + translateY
|
||||
if ((left >= minx && left <= maxx ||
|
||||
right >= minx && right <= maxx) &&
|
||||
(top >= miny && top <= maxy ||
|
||||
bottom >= miny && bottom <= maxy)
|
||||
) {
|
||||
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)
|
||||
if (node.nodeData.data.isActive) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.setNodeActive(node, true)
|
||||
this.mindMap.renderer.addActiveNode(node)
|
||||
// })
|
||||
} else if (node.nodeData.data.isActive) {
|
||||
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
|
||||
if (!node.nodeData.data.isActive) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.setNodeActive(node, false)
|
||||
this.mindMap.renderer.removeActiveNode(node)
|
||||
if (!node.nodeData.data.isActive) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.setNodeActive(node, false)
|
||||
this.mindMap.renderer.removeActiveNode(node)
|
||||
// })
|
||||
}
|
||||
})
|
||||
155
simple-mind-map/src/plugins/TouchEvent.js
Normal file
@@ -0,0 +1,155 @@
|
||||
// 手势事件支持插件
|
||||
class TouchEvent {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
this.mindMap = mindMap
|
||||
this.touchesNum = 0
|
||||
this.singleTouchstartEvent = null
|
||||
this.clickNum = 0
|
||||
this.touchStartScaleView = null
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
this.onTouchstart = this.onTouchstart.bind(this)
|
||||
this.onTouchmove = this.onTouchmove.bind(this)
|
||||
this.onTouchcancel = this.onTouchcancel.bind(this)
|
||||
this.onTouchend = this.onTouchend.bind(this)
|
||||
window.addEventListener('touchstart', this.onTouchstart)
|
||||
window.addEventListener('touchmove', this.onTouchmove)
|
||||
window.addEventListener('touchcancel', this.onTouchcancel)
|
||||
window.addEventListener('touchend', this.onTouchend)
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
unBindEvent() {
|
||||
window.removeEventListener('touchstart', this.onTouchstart)
|
||||
window.removeEventListener('touchmove', this.onTouchmove)
|
||||
window.removeEventListener('touchcancel', this.onTouchcancel)
|
||||
window.removeEventListener('touchend', this.onTouchend)
|
||||
}
|
||||
|
||||
// 手指按下事件
|
||||
onTouchstart(e) {
|
||||
this.touchesNum = e.touches.length
|
||||
this.touchStartScaleView = null
|
||||
if (this.touchesNum === 1) {
|
||||
let touch = e.touches[0]
|
||||
this.singleTouchstartEvent = touch
|
||||
this.dispatchMouseEvent('mousedown', touch.target, touch)
|
||||
}
|
||||
}
|
||||
|
||||
// 手指移动事件
|
||||
onTouchmove(e) {
|
||||
let len = e.touches.length
|
||||
if (len === 1) {
|
||||
let touch = e.touches[0]
|
||||
this.dispatchMouseEvent('mousemove', touch.target, touch)
|
||||
} else if (len === 2) {
|
||||
let touch1 = e.touches[0]
|
||||
let touch2 = e.touches[1]
|
||||
let ox = touch1.clientX - touch2.clientX
|
||||
let oy = touch1.clientY - touch2.clientY
|
||||
let distance = Math.sqrt(Math.pow(ox, 2) + Math.pow(oy, 2))
|
||||
// 以两指中心点进行缩放
|
||||
let { x: touch1ClientX, y: touch1ClientY } = this.mindMap.toPos(
|
||||
touch1.clientX,
|
||||
touch1.clientY
|
||||
)
|
||||
let { x: touch2ClientX, y: touch2ClientY } = this.mindMap.toPos(
|
||||
touch2.clientX,
|
||||
touch2.clientY
|
||||
)
|
||||
let cx = (touch1ClientX + touch2ClientX) / 2
|
||||
let cy = (touch1ClientY + touch2ClientY) / 2
|
||||
// 手势缩放,基于最开始的位置进行缩放(基于前一个位置缩放不是线性关系); 缩放同时支持位置拖动
|
||||
const view = this.mindMap.view
|
||||
if (!this.touchStartScaleView) {
|
||||
this.touchStartScaleView = {
|
||||
distance: distance,
|
||||
scale: view.scale,
|
||||
x: view.x,
|
||||
y: view.y,
|
||||
cx: cx,
|
||||
cy: cy
|
||||
}
|
||||
return
|
||||
}
|
||||
const viewBefore = this.touchStartScaleView
|
||||
let scale = viewBefore.scale * (distance / viewBefore.distance)
|
||||
if (Math.abs(distance - viewBefore.distance) <= 10) {
|
||||
scale = viewBefore.scale
|
||||
}
|
||||
const ratio = 1 - scale / viewBefore.scale
|
||||
view.scale = scale < 0.1 ? 0.1 : scale
|
||||
view.x =
|
||||
viewBefore.x +
|
||||
(cx - viewBefore.x) * ratio +
|
||||
(cx - viewBefore.cx) * scale
|
||||
view.y =
|
||||
viewBefore.y +
|
||||
(cy - viewBefore.y) * ratio +
|
||||
(cy - viewBefore.cy) * scale
|
||||
view.transform()
|
||||
this.mindMap.emit('scale', scale)
|
||||
}
|
||||
}
|
||||
|
||||
// 手指取消事件
|
||||
onTouchcancel(e) {}
|
||||
|
||||
// 手指松开事件
|
||||
onTouchend(e) {
|
||||
this.dispatchMouseEvent('mouseup', e.target)
|
||||
if (this.touchesNum === 1) {
|
||||
// 模拟双击事件
|
||||
this.clickNum++
|
||||
setTimeout(() => {
|
||||
this.clickNum = 0
|
||||
}, 300)
|
||||
let ev = this.singleTouchstartEvent
|
||||
if (this.clickNum > 1) {
|
||||
this.clickNum = 0
|
||||
this.dispatchMouseEvent('dblclick', ev.target, ev)
|
||||
} else {
|
||||
// 点击事件应该不用模拟
|
||||
// this.dispatchMouseEvent('click', ev.target, ev)
|
||||
}
|
||||
}
|
||||
this.touchesNum = 0
|
||||
this.singleTouchstartEvent = null
|
||||
this.touchStartScaleView = null
|
||||
}
|
||||
|
||||
// 发送鼠标事件
|
||||
dispatchMouseEvent(eventName, target, e) {
|
||||
let opt = {}
|
||||
if (e) {
|
||||
opt = {
|
||||
screenX: e.screenX,
|
||||
screenY: e.screenY,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
which: 1
|
||||
}
|
||||
}
|
||||
let event = new MouseEvent(eventName, {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
...opt
|
||||
})
|
||||
target.dispatchEvent(event)
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
TouchEvent.instanceName = 'touchEvent'
|
||||
|
||||
export default TouchEvent
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Text, G } from '@svgdotjs/svg.js'
|
||||
import { degToRad, camelCaseToHyphen } from './utils'
|
||||
import { degToRad, camelCaseToHyphen } from '../utils'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 水印类
|
||||
// 水印插件
|
||||
class Watermark {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Text } from '@svgdotjs/svg.js'
|
||||
import { getStrWithBrFromHtml } from './index'
|
||||
import { getStrWithBrFromHtml } from '../../utils/index'
|
||||
|
||||
// 创建文字节点
|
||||
function createText(data) {
|
||||
@@ -47,7 +47,8 @@ function showEditTextBox(g) {
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
let {
|
||||
associativeLineTextFontSize,
|
||||
@@ -4,7 +4,15 @@ const open = `<svg t="1618141562310" class="icon" viewBox="0 0 1024 1024" versio
|
||||
// 收缩按钮
|
||||
const close = `<svg t="1618141589243" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13611" width="200" height="200"><path d="M512 105.472c225.28 0 407.04 181.76 407.04 407.04s-181.76 407.04-407.04 407.04-407.04-181.76-407.04-407.04 181.76-407.04 407.04-407.04z m0-74.24c-265.216 0-480.768 215.552-480.768 480.768s215.552 480.768 480.768 480.768 480.768-215.552 480.768-480.768-215.552-480.768-480.768-480.768z" p-id="13612"></path><path d="M252.928 474.624h518.144v74.24h-518.144z" p-id="13613"></path></svg>`
|
||||
|
||||
// 删除按钮
|
||||
const remove = `<svg width="14px" height="14px" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13611" width="200" height="200"><path fill="#ffffff" d="M512 105.472c225.28 0 407.04 181.76 407.04 407.04s-181.76 407.04-407.04 407.04-407.04-181.76-407.04-407.04 181.76-407.04 407.04-407.04z m0-74.24c-265.216 0-480.768 215.552-480.768 480.768s215.552 480.768 480.768 480.768 480.768-215.552 480.768-480.768-215.552-480.768-480.768-480.768z" p-id="13612"></path><path fill="#ffffff" d="M252.928 474.624h518.144v74.24h-518.144z" p-id="13613"></path></svg>`
|
||||
|
||||
// 图片调整按钮
|
||||
const imgAdjust = `<svg width="12px" height="12px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M1008.128 614.4a25.6 25.6 0 0 0-27.648 5.632l-142.848 142.848L259.072 186.88 401.92 43.52A25.6 25.6 0 0 0 384 0h-358.4a25.6 25.6 0 0 0-25.6 25.6v358.4a25.6 25.6 0 0 0 43.52 17.92l143.36-142.848 578.048 578.048-142.848 142.848a25.6 25.6 0 0 0 17.92 43.52h358.4a25.6 25.6 0 0 0 25.6-25.6v-358.4a25.6 25.6 0 0 0-15.872-25.088z" /></svg>`
|
||||
|
||||
export default {
|
||||
open,
|
||||
close
|
||||
close,
|
||||
remove,
|
||||
imgAdjust
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ export default {
|
||||
lineDasharray: 'none',
|
||||
// 连线风格
|
||||
lineStyle: 'straight', // 针对logicalStructure、mindMap两种结构。曲线(curve)、直线(straight)、直连(direct)
|
||||
// 曲线连接时,根节点和其他节点的连接线样式保持统一,默认根节点为 ( 型,其他节点为 { 型,设为true后,都为 { 型
|
||||
rootLineKeepSameInCurve: true,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 1,
|
||||
// 概要连线的颜色
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nextTick } from './utils'
|
||||
import { nextTick } from '.'
|
||||
|
||||
// 批量执行
|
||||
class BatchExecution {
|
||||
@@ -1,3 +1,5 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
// 深度优先遍历树
|
||||
export const walk = (
|
||||
root,
|
||||
@@ -31,9 +33,11 @@ export const walk = (
|
||||
|
||||
// 广度优先遍历树
|
||||
export const bfsWalk = (root, callback) => {
|
||||
callback(root)
|
||||
let stack = [root]
|
||||
let isStop = false
|
||||
if (callback(root, null) === 'stop') {
|
||||
isStop = true
|
||||
}
|
||||
while (stack.length) {
|
||||
if (isStop) {
|
||||
break
|
||||
@@ -41,8 +45,9 @@ export const bfsWalk = (root, callback) => {
|
||||
let cur = stack.shift()
|
||||
if (cur.children && cur.children.length) {
|
||||
cur.children.forEach(item => {
|
||||
if (isStop) return
|
||||
stack.push(item)
|
||||
if (callback(item) === 'stop') {
|
||||
if (callback(item, cur) === 'stop') {
|
||||
isStop = true
|
||||
}
|
||||
})
|
||||
@@ -50,6 +55,26 @@ export const bfsWalk = (root, callback) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 按原比例缩放图片
|
||||
export const resizeImgSizeByOriginRatio = (
|
||||
width,
|
||||
height,
|
||||
newWidth,
|
||||
newHeight
|
||||
) => {
|
||||
let arr = []
|
||||
let nRatio = width / height
|
||||
let mRatio = newWidth / newHeight
|
||||
if (nRatio > mRatio) {
|
||||
// 固定高度
|
||||
arr = [nRatio * newHeight, newHeight]
|
||||
} else {
|
||||
// 固定宽度
|
||||
arr = [newWidth, newWidth / nRatio]
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// 缩放图片尺寸
|
||||
export const resizeImgSize = (width, height, maxWidth, maxHeight) => {
|
||||
let nRatio = width / height
|
||||
@@ -137,7 +162,12 @@ export const copyRenderTree = (tree, root, removeActiveState = false) => {
|
||||
}
|
||||
|
||||
// 复制节点树数据
|
||||
export const copyNodeTree = (tree, root, removeActiveState = false, keepId = false) => {
|
||||
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
|
||||
@@ -188,6 +218,18 @@ export const imgToDataUrl = src => {
|
||||
})
|
||||
}
|
||||
|
||||
// 解析dataUrl
|
||||
export const parseDataUrl = data => {
|
||||
if (!/^data:/.test(data)) return data
|
||||
let [typeStr, base64] = data.split(',')
|
||||
let res = /^data:[^/]+\/([^;]+);/.exec(typeStr)
|
||||
let type = res[1]
|
||||
return {
|
||||
type,
|
||||
base64
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
export const downloadFile = (file, fileName) => {
|
||||
let a = document.createElement('a')
|
||||
@@ -236,8 +278,8 @@ export const degToRad = deg => {
|
||||
return deg * (Math.PI / 180)
|
||||
}
|
||||
|
||||
// 驼峰转连字符
|
||||
export const camelCaseToHyphen = (str) => {
|
||||
// 驼峰转连字符
|
||||
export const camelCaseToHyphen = str => {
|
||||
return str.replace(/([a-z])([A-Z])/g, (...args) => {
|
||||
return args[1] + '-' + args[2].toLowerCase()
|
||||
})
|
||||
@@ -258,11 +300,8 @@ export const measureText = (text, { italic, bold, fontSize, fontFamily }) => {
|
||||
}
|
||||
measureTextContext.save()
|
||||
measureTextContext.font = font
|
||||
const {
|
||||
width,
|
||||
actualBoundingBoxAscent,
|
||||
actualBoundingBoxDescent
|
||||
} = measureTextContext.measureText(text)
|
||||
const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } =
|
||||
measureTextContext.measureText(text)
|
||||
measureTextContext.restore()
|
||||
const height = actualBoundingBoxAscent + actualBoundingBoxDescent
|
||||
return { width, height }
|
||||
@@ -270,7 +309,9 @@ export const measureText = (text, { italic, bold, fontSize, fontFamily }) => {
|
||||
|
||||
// 拼接font字符串
|
||||
export const joinFontStr = ({ italic, bold, fontSize, fontFamily }) => {
|
||||
return `${italic ? 'italic ' : ''} ${bold ? 'bold ' : ''} ${fontSize}px ${fontFamily} `
|
||||
return `${italic ? 'italic ' : ''} ${
|
||||
bold ? 'bold ' : ''
|
||||
} ${fontSize}px ${fontFamily} `
|
||||
}
|
||||
|
||||
// 在下一个事件循环里执行任务
|
||||
@@ -336,7 +377,7 @@ export const checkNodeOuter = (mindMap, node) => {
|
||||
|
||||
// 提取html字符串里的纯文本
|
||||
let getTextFromHtmlEl = null
|
||||
export const getTextFromHtml = (html) => {
|
||||
export const getTextFromHtml = html => {
|
||||
if (!getTextFromHtmlEl) {
|
||||
getTextFromHtmlEl = document.createElement('div')
|
||||
}
|
||||
@@ -345,15 +386,249 @@ export const getTextFromHtml = (html) => {
|
||||
}
|
||||
|
||||
// 将blob转成data:url
|
||||
export const readBlob = (blob) => {
|
||||
export const readBlob = blob => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader()
|
||||
reader.onload = (evt) => {
|
||||
reader.onload = evt => {
|
||||
resolve(evt.target.result)
|
||||
}
|
||||
reader.onerror = (err) => {
|
||||
reader.onerror = err => {
|
||||
reject(err)
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 将dom节点转换成html字符串
|
||||
let nodeToHTMLWrapEl = null
|
||||
export const nodeToHTML = node => {
|
||||
if (!nodeToHTMLWrapEl) {
|
||||
nodeToHTMLWrapEl = document.createElement('div')
|
||||
}
|
||||
nodeToHTMLWrapEl.innerHTML = ''
|
||||
nodeToHTMLWrapEl.appendChild(node)
|
||||
return nodeToHTMLWrapEl.innerHTML
|
||||
}
|
||||
|
||||
// 获取图片大小
|
||||
export const getImageSize = src => {
|
||||
return new Promise(resolve => {
|
||||
let img = new Image()
|
||||
img.src = src
|
||||
img.onload = () => {
|
||||
resolve({
|
||||
width: img.width,
|
||||
height: img.height
|
||||
})
|
||||
}
|
||||
img.onerror = () => {
|
||||
resolve({
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建节点唯一的id
|
||||
export const createUid = () => {
|
||||
return uuidv4()
|
||||
}
|
||||
|
||||
// 加载图片文件
|
||||
export const loadImage = imgFile => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let fr = new FileReader()
|
||||
fr.readAsDataURL(imgFile)
|
||||
fr.onload = async e => {
|
||||
let url = e.target.result
|
||||
let size = await getImageSize(url)
|
||||
resolve({
|
||||
url,
|
||||
size
|
||||
})
|
||||
}
|
||||
fr.onerror = error => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 移除字符串中的html实体
|
||||
export const removeHTMLEntities = str => {
|
||||
;[[' ', ' ']].forEach(item => {
|
||||
str = str.replaceAll(item[0], item[1])
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
// 获取一个数据的类型
|
||||
export const getType = data => {
|
||||
return Object.prototype.toString.call(data).slice(7, -1)
|
||||
}
|
||||
|
||||
// 判断一个数据是否是null和undefined和空字符串
|
||||
export const isUndef = data => {
|
||||
return data === null || data === undefined || data === ''
|
||||
}
|
||||
|
||||
// 移除html字符串中节点的内联样式
|
||||
export const removeHtmlStyle = html => {
|
||||
return html.replaceAll(/(<[^\s]+)\s+style=["'][^'"]+["']\s*(>)/g, '$1$2')
|
||||
}
|
||||
|
||||
// 给html标签中指定的标签添加内联样式
|
||||
export const addHtmlStyle = (html, tag, style) => {
|
||||
const reg = new RegExp(`(<${tag}[^>]*)(>[^<>]*</${tag}>)`, 'g')
|
||||
return html.replaceAll(reg, `$1 style="${style}"$2`)
|
||||
}
|
||||
|
||||
// 检查一个字符串是否是富文本字符
|
||||
let checkIsRichTextEl = null
|
||||
export const checkIsRichText = str => {
|
||||
if (!checkIsRichTextEl) {
|
||||
checkIsRichTextEl = document.createElement('div')
|
||||
}
|
||||
checkIsRichTextEl.innerHTML = str
|
||||
for (let c = checkIsRichTextEl.childNodes, i = c.length; i--; ) {
|
||||
if (c[i].nodeType == 1) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 搜索和替换html字符串中指定的文本
|
||||
let replaceHtmlTextEl = null
|
||||
export const replaceHtmlText = (html, searchText, replaceText) => {
|
||||
if (!replaceHtmlTextEl) {
|
||||
replaceHtmlTextEl = document.createElement('div')
|
||||
}
|
||||
replaceHtmlTextEl.innerHTML = html
|
||||
let walk = root => {
|
||||
let childNodes = root.childNodes
|
||||
childNodes.forEach(node => {
|
||||
if (node.nodeType === 1) {
|
||||
// 元素节点
|
||||
walk(node)
|
||||
} else if (node.nodeType === 3) {
|
||||
// 文本节点
|
||||
root.replaceChild(
|
||||
document.createTextNode(
|
||||
node.nodeValue.replaceAll(searchText, replaceText)
|
||||
),
|
||||
node
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
walk(replaceHtmlTextEl)
|
||||
return replaceHtmlTextEl.innerHTML
|
||||
}
|
||||
|
||||
// 判断一个颜色是否是白色
|
||||
export const isWhite = color => {
|
||||
color = String(color).replaceAll(/\s+/g, '')
|
||||
return (
|
||||
['#fff', '#ffffff', '#FFF', '#FFFFFF', 'rgb(255,255,255)'].includes(
|
||||
color
|
||||
) || /rgba\(255,255,255,[^)]+\)/.test(color)
|
||||
)
|
||||
}
|
||||
|
||||
// 判断一个颜色是否是透明
|
||||
export const isTransparent = color => {
|
||||
color = String(color).replaceAll(/\s+/g, '')
|
||||
return (
|
||||
['', 'transparent'].includes(color) || /rgba\(\d+,\d+,\d+,0\)/.test(color)
|
||||
)
|
||||
}
|
||||
|
||||
// 从当前主题里获取一个非透明非白色的颜色
|
||||
export const getVisibleColorFromTheme = themeConfig => {
|
||||
let { lineColor, root, second, node } = themeConfig
|
||||
let list = [
|
||||
lineColor,
|
||||
root.fillColor,
|
||||
root.color,
|
||||
second.fillColor,
|
||||
second.color,
|
||||
node.fillColor,
|
||||
node.color,
|
||||
root.borderColor,
|
||||
second.borderColor,
|
||||
node.borderColor
|
||||
]
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let color = list[i]
|
||||
if (!isTransparent(color) && !isWhite(color)) {
|
||||
return color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本
|
||||
let nodeRichTextToTextWithWrapEl = null
|
||||
export const nodeRichTextToTextWithWrap = html => {
|
||||
if (!nodeRichTextToTextWithWrapEl) {
|
||||
nodeRichTextToTextWithWrapEl = document.createElement('div')
|
||||
}
|
||||
nodeRichTextToTextWithWrapEl.innerHTML = html
|
||||
const childNodes = nodeRichTextToTextWithWrapEl.childNodes
|
||||
let res = ''
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const node = childNodes[i]
|
||||
if (node.nodeType === 1) {
|
||||
// 元素节点
|
||||
if (node.tagName.toLowerCase() === 'p') {
|
||||
res += node.textContent + '\n'
|
||||
} else {
|
||||
res += node.textContent
|
||||
}
|
||||
} else if (node.nodeType === 3) {
|
||||
// 文本节点
|
||||
res += node.nodeValue
|
||||
}
|
||||
}
|
||||
return res.replace(/\n$/, '')
|
||||
}
|
||||
|
||||
// 将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容
|
||||
let textToNodeRichTextWithWrapEl = null
|
||||
export const textToNodeRichTextWithWrap = html => {
|
||||
if (!textToNodeRichTextWithWrapEl) {
|
||||
textToNodeRichTextWithWrapEl = document.createElement('div')
|
||||
}
|
||||
textToNodeRichTextWithWrapEl.innerHTML = html
|
||||
const childNodes = textToNodeRichTextWithWrapEl.childNodes
|
||||
let list = []
|
||||
let str = ''
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const node = childNodes[i]
|
||||
if (node.nodeType === 1) {
|
||||
// 元素节点
|
||||
if (node.tagName.toLowerCase() === 'br') {
|
||||
list.push(str)
|
||||
str = ''
|
||||
} else {
|
||||
str += node.textContent
|
||||
}
|
||||
} else if (node.nodeType === 3) {
|
||||
// 文本节点
|
||||
str += node.nodeValue
|
||||
}
|
||||
}
|
||||
if (str) {
|
||||
list.push(str)
|
||||
}
|
||||
return list
|
||||
.map(item => {
|
||||
return `<p><span>${item}</span></p>`
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
|
||||
// 判断是否是移动端环境
|
||||
export const isMobile = () => {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
}
|
||||
|
||||
1
web/.env.library
Normal file
@@ -0,0 +1 @@
|
||||
NODE_ENV=library
|
||||
70
web/package-lock.json
generated
@@ -33,8 +33,10 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-checkbox": "^1.1.0",
|
||||
"prettier": "^1.19.1",
|
||||
"vconsole": "^3.15.1",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.44.2"
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-dynamic-public-path": "^1.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@achrinza/node-ipc": {
|
||||
@@ -5168,6 +5170,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-text-to-clipboard": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.1.0.tgz",
|
||||
"integrity": "sha512-PFM6BnjLnOON/lB3ta/Jg7Ywsv+l9kQGD4TWDCSlRBGmqnnTM5MrDkhAFgw+8HZt0wW6Q2BBE4cmy9sq+s9Qng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-webpack-plugin": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.2.tgz",
|
||||
@@ -10351,6 +10365,12 @@
|
||||
"integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mutation-observer": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mutation-observer/-/mutation-observer-1.0.3.tgz",
|
||||
"integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||
@@ -15185,6 +15205,18 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vconsole": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/vconsole/-/vconsole-3.15.1.tgz",
|
||||
"integrity": "sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"core-js": "^3.11.0",
|
||||
"mutation-observer": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/vendors": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz",
|
||||
@@ -16193,6 +16225,12 @@
|
||||
"decamelize": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dynamic-public-path": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dynamic-public-path/-/webpack-dynamic-public-path-1.0.8.tgz",
|
||||
"integrity": "sha512-AF6onorpvmiC+I/dQ19SOi+oN66oEy9h4deam7gPs1Qa1mOQ9i7IRsOahaukohKAciys7NfX+YFboRn4rmpuKw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/webpack-log": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz",
|
||||
@@ -20525,6 +20563,12 @@
|
||||
"integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==",
|
||||
"dev": true
|
||||
},
|
||||
"copy-text-to-clipboard": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.1.0.tgz",
|
||||
"integrity": "sha512-PFM6BnjLnOON/lB3ta/Jg7Ywsv+l9kQGD4TWDCSlRBGmqnnTM5MrDkhAFgw+8HZt0wW6Q2BBE4cmy9sq+s9Qng==",
|
||||
"dev": true
|
||||
},
|
||||
"copy-webpack-plugin": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.2.tgz",
|
||||
@@ -24567,6 +24611,12 @@
|
||||
"integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==",
|
||||
"dev": true
|
||||
},
|
||||
"mutation-observer": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mutation-observer/-/mutation-observer-1.0.3.tgz",
|
||||
"integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==",
|
||||
"dev": true
|
||||
},
|
||||
"mute-stream": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||
@@ -28566,6 +28616,18 @@
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"dev": true
|
||||
},
|
||||
"vconsole": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/vconsole/-/vconsole-3.15.1.tgz",
|
||||
"integrity": "sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"core-js": "^3.11.0",
|
||||
"mutation-observer": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"vendors": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz",
|
||||
@@ -29421,6 +29483,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"webpack-dynamic-public-path": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dynamic-public-path/-/webpack-dynamic-public-path-1.0.8.tgz",
|
||||
"integrity": "sha512-AF6onorpvmiC+I/dQ19SOi+oN66oEy9h4deam7gPs1Qa1mOQ9i7IRsOahaukohKAciys7NfX+YFboRn4rmpuKw==",
|
||||
"dev": true
|
||||
},
|
||||
"webpack-log": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz",
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
"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/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",
|
||||
"buildLibrary": "vue-cli-service build --mode library --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 && esbuild ../simple-mind-map/full.js --bundle --minify --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.min.js",
|
||||
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
|
||||
"buildDoc": "node ./scripts/buildDoc.js",
|
||||
"autoBuildDoc": "node ./scripts/autoBuildDoc.js"
|
||||
"autoBuildDoc": "node ./scripts/autoBuildDoc.js",
|
||||
"createNodeImageList": "node ./scripts/createNodeImageList.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@toast-ui/editor": "^3.1.5",
|
||||
@@ -37,8 +38,10 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-checkbox": "^1.1.0",
|
||||
"prettier": "^1.19.1",
|
||||
"vconsole": "^3.15.1",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.44.2"
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-dynamic-public-path": "^1.0.8"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
||||
|
Before Width: | Height: | Size: 695 KiB |
@@ -3,9 +3,15 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="./dist/logo.png">
|
||||
<title>一个简单的web思维导图实现</title>
|
||||
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
|
||||
<link rel="icon" href="dist/logo.ico">
|
||||
<title>思绪思维导图</title>
|
||||
<script>
|
||||
// 自定义静态资源的路径
|
||||
window.externalPublicPath = './dist/'
|
||||
// 接管应用
|
||||
window.takeOverApp = false
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
@@ -13,5 +19,73 @@
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script>
|
||||
const getDataFromBackend = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
mindMapData: {
|
||||
root:{
|
||||
"data": {
|
||||
"text": "根节点"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
theme:{
|
||||
"template":"avocado",
|
||||
"config":{}
|
||||
},
|
||||
layout:"logicalStructure",
|
||||
config: {},
|
||||
view: null,
|
||||
},
|
||||
lang: 'zh',
|
||||
localConfig: null
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
}
|
||||
const setTakeOverAppMethods = (data) => {
|
||||
window.takeOverAppMethods = {}
|
||||
// 获取思维导图数据的函数
|
||||
window.takeOverAppMethods.getMindMapData = () => {
|
||||
return data.mindMapData
|
||||
}
|
||||
// 保存思维导图数据的函数
|
||||
window.takeOverAppMethods.saveMindMapData = (data) => {
|
||||
console.log(data)
|
||||
}
|
||||
// 获取语言的函数
|
||||
window.takeOverAppMethods.getLanguage = () => {
|
||||
return data.lang
|
||||
}
|
||||
// 保存语言的函数
|
||||
window.takeOverAppMethods.saveLanguage = (lang) => {
|
||||
console.log(lang)
|
||||
}
|
||||
// 获取本地配置的函数
|
||||
window.takeOverAppMethods.getLocalConfig = () => {
|
||||
return data.localConfig
|
||||
}
|
||||
// 保存本地配置的函数
|
||||
window.takeOverAppMethods.saveLocalConfig = (config) => {
|
||||
console.log(config)
|
||||
}
|
||||
}
|
||||
window.onload = async () => {
|
||||
if (!window.takeOverApp) return
|
||||
// 请求数据
|
||||
const data = await getDataFromBackend()
|
||||
// 设置全局的方法
|
||||
setTakeOverAppMethods(data)
|
||||
// 思维导图实例创建完成事件
|
||||
window.$bus.$on('app_inited', (mindMap) => {
|
||||
console.log(mindMap)
|
||||
})
|
||||
// 可以通过window.$bus.$on()来监听应用的一些事件
|
||||
// 实例化页面
|
||||
window.initApp()
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
web/public/logo.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
50
web/scripts/createNodeImageList.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const fileDest = path.join(__dirname, '../src/assets/svg')
|
||||
const targetDest = path.join(__dirname, '../src/config/image.js')
|
||||
|
||||
const run = (dir) => {
|
||||
let dirs = fs.readdirSync(dir)
|
||||
dirs.forEach(item => {
|
||||
let cur = path.join(dir, item)
|
||||
if (fs.statSync(cur).isDirectory()) {
|
||||
walkDir(cur, item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const list = []
|
||||
const importList = []
|
||||
const walkDir = (dir, item) => {
|
||||
let files = fs.readdirSync(dir)
|
||||
let name = files.find((file) => {
|
||||
return !/\./.test(file)
|
||||
})
|
||||
let fileList = files.filter((file) => {
|
||||
return /\.svg$/.test(file)
|
||||
})
|
||||
let itemList = []
|
||||
fileList.forEach((file) => {
|
||||
let fileName = item + '_' + file.replace(/\.svg$/, '').replaceAll('-', '')
|
||||
importList.push(`import ${fileName} from '../assets/svg/${item}/${file}'`)
|
||||
itemList.push({
|
||||
url: fileName,
|
||||
width: 100,
|
||||
height: 100
|
||||
})
|
||||
})
|
||||
list.push({
|
||||
name,
|
||||
list: itemList
|
||||
})
|
||||
const content = `
|
||||
// 该文件请运行npm run createNodeImageList命令自动生成
|
||||
${importList.join('\n')}
|
||||
export default ${JSON.stringify(list, null, 2).replace(/(url":\s*)"([^"]+)"(,)/g, '$1$2$3')}
|
||||
`
|
||||
fs.writeFileSync(targetDest, content)
|
||||
}
|
||||
|
||||
run(fileDest)
|
||||
console.log('运行成功')
|
||||
BIN
web/src/.DS_Store
vendored
@@ -6,6 +6,8 @@ const SIMPLE_MIND_MAP_DATA = 'SIMPLE_MIND_MAP_DATA'
|
||||
const SIMPLE_MIND_MAP_LANG = 'SIMPLE_MIND_MAP_LANG'
|
||||
const SIMPLE_MIND_MAP_LOCAL_CONFIG = 'SIMPLE_MIND_MAP_LOCAL_CONFIG'
|
||||
|
||||
let mindMapData = null
|
||||
|
||||
/**
|
||||
* @Author: 王林
|
||||
* @Date: 2021-08-02 22:36:48
|
||||
@@ -29,6 +31,10 @@ const copyMindMapTreeData = (tree, root) => {
|
||||
* @Desc: 获取缓存的思维导图数据
|
||||
*/
|
||||
export const getData = () => {
|
||||
if (window.takeOverApp) {
|
||||
mindMapData = window.takeOverAppMethods.getMindMapData()
|
||||
return mindMapData
|
||||
}
|
||||
let store = localStorage.getItem(SIMPLE_MIND_MAP_DATA)
|
||||
if (store === null) {
|
||||
return simpleDeepClone(exampleData)
|
||||
@@ -48,8 +54,18 @@ export const getData = () => {
|
||||
*/
|
||||
export const storeData = data => {
|
||||
try {
|
||||
let originData = getData()
|
||||
let originData = null
|
||||
if (window.takeOverApp) {
|
||||
originData = mindMapData
|
||||
} else {
|
||||
originData = getData()
|
||||
}
|
||||
originData.root = copyMindMapTreeData({}, data)
|
||||
if (window.takeOverApp) {
|
||||
mindMapData = originData
|
||||
window.takeOverAppMethods.saveMindMapData(originData)
|
||||
return
|
||||
}
|
||||
Vue.prototype.$bus.$emit('write_local_file', originData)
|
||||
let dataStr = JSON.stringify(originData)
|
||||
localStorage.setItem(SIMPLE_MIND_MAP_DATA, dataStr)
|
||||
@@ -65,11 +81,21 @@ export const storeData = data => {
|
||||
*/
|
||||
export const storeConfig = config => {
|
||||
try {
|
||||
let originData = getData()
|
||||
let originData = null
|
||||
if (window.takeOverApp) {
|
||||
originData = mindMapData
|
||||
} else {
|
||||
originData = getData()
|
||||
}
|
||||
originData = {
|
||||
...originData,
|
||||
...config
|
||||
}
|
||||
if (window.takeOverApp) {
|
||||
mindMapData = originData
|
||||
window.takeOverAppMethods.saveMindMapData(originData)
|
||||
return
|
||||
}
|
||||
Vue.prototype.$bus.$emit('write_local_file', originData)
|
||||
let dataStr = JSON.stringify(originData)
|
||||
localStorage.setItem(SIMPLE_MIND_MAP_DATA, dataStr)
|
||||
@@ -85,6 +111,10 @@ export const storeConfig = config => {
|
||||
* @Desc: 存储语言
|
||||
*/
|
||||
export const storeLang = lang => {
|
||||
if (window.takeOverApp) {
|
||||
window.takeOverAppMethods.saveLanguage(lang)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(SIMPLE_MIND_MAP_LANG, lang)
|
||||
}
|
||||
|
||||
@@ -95,6 +125,9 @@ export const storeLang = lang => {
|
||||
* @Desc: 获取存储的语言
|
||||
*/
|
||||
export const getLang = () => {
|
||||
if (window.takeOverApp) {
|
||||
return window.takeOverAppMethods.getLanguage() || 'zh'
|
||||
}
|
||||
let lang = localStorage.getItem(SIMPLE_MIND_MAP_LANG)
|
||||
if (lang) {
|
||||
return lang
|
||||
@@ -110,6 +143,9 @@ export const getLang = () => {
|
||||
* @Desc: 存储本地配置
|
||||
*/
|
||||
export const storeLocalConfig = config => {
|
||||
if (window.takeOverApp) {
|
||||
return window.takeOverAppMethods.saveLocalConfig(config)
|
||||
}
|
||||
localStorage.setItem(SIMPLE_MIND_MAP_LOCAL_CONFIG, JSON.stringify(config))
|
||||
}
|
||||
|
||||
@@ -120,6 +156,9 @@ export const storeLocalConfig = config => {
|
||||
* @Desc: 获取本地配置
|
||||
*/
|
||||
export const getLocalConfig = () => {
|
||||
if (window.takeOverApp) {
|
||||
return window.takeOverAppMethods.getLocalConfig()
|
||||
}
|
||||
let config = localStorage.getItem(SIMPLE_MIND_MAP_LOCAL_CONFIG)
|
||||
if (config) {
|
||||
return JSON.parse(config)
|
||||
|
||||
BIN
web/src/assets/.DS_Store
vendored
BIN
web/src/assets/avatar/Chris.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
web/src/assets/avatar/ZXR.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
web/src/assets/avatar/default.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
web/src/assets/avatar/qp.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
web/src/assets/avatar/suka.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/src/assets/avatar/仓鼠.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
web/src/assets/avatar/千帆.jpg
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
web/src/assets/avatar/小土渣的宇宙.jpeg
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
web/src/assets/avatar/志斌.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
web/src/assets/avatar/才镇.jpg
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
web/src/assets/avatar/水车.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
web/src/assets/avatar/花儿朵朵.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 2479351 */
|
||||
src: url('iconfont.woff2?t=1679621707211') format('woff2'),
|
||||
url('iconfont.woff?t=1679621707211') format('woff'),
|
||||
url('iconfont.ttf?t=1679621707211') format('truetype');
|
||||
src: url('iconfont.woff2?t=1691822758372') format('woff2'),
|
||||
url('iconfont.woff?t=1691822758372') format('woff'),
|
||||
url('iconfont.ttf?t=1691822758372') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,82 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icontouming:before {
|
||||
content: "\e60c";
|
||||
}
|
||||
|
||||
.iconlieri:before {
|
||||
content: "\e60b";
|
||||
}
|
||||
|
||||
.iconmoon_line:before {
|
||||
content: "\e745";
|
||||
}
|
||||
|
||||
.iconsousuo:before {
|
||||
content: "\e693";
|
||||
}
|
||||
|
||||
.iconjiantouyou:before {
|
||||
content: "\e62d";
|
||||
}
|
||||
|
||||
.iconbianji1:before {
|
||||
content: "\e60a";
|
||||
}
|
||||
|
||||
.icondaohang1:before {
|
||||
content: "\e632";
|
||||
}
|
||||
|
||||
.iconyanjing:before {
|
||||
content: "\e8bf";
|
||||
}
|
||||
|
||||
.iconwangzhan:before {
|
||||
content: "\e628";
|
||||
}
|
||||
|
||||
.iconcsdn:before {
|
||||
content: "\e608";
|
||||
}
|
||||
|
||||
.iconshejiaotubiao-10:before {
|
||||
content: "\e644";
|
||||
}
|
||||
|
||||
.iconstar:before {
|
||||
content: "\e7df";
|
||||
}
|
||||
|
||||
.iconfork:before {
|
||||
content: "\e641";
|
||||
}
|
||||
|
||||
.iconxiazai:before {
|
||||
content: "\e613";
|
||||
}
|
||||
|
||||
.iconteamwork:before {
|
||||
content: "\e870";
|
||||
}
|
||||
|
||||
.iconshuiyin:before {
|
||||
content: "\e67a";
|
||||
}
|
||||
|
||||
.iconxmind:before {
|
||||
content: "\ea57";
|
||||
}
|
||||
|
||||
.iconmouseR:before {
|
||||
content: "\e6bd";
|
||||
}
|
||||
|
||||
.iconmouseL:before {
|
||||
content: "\e6c0";
|
||||
}
|
||||
|
||||
.iconwenjian:before {
|
||||
content: "\e607";
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 23 KiB |
BIN
web/src/assets/img/index/block1.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
web/src/assets/img/index/block3.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
web/src/assets/img/index/block4.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
web/src/assets/img/index/split.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 21 KiB |
BIN
web/src/assets/img/logo2.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB |
BIN
web/src/assets/img/structures/catalogOrganization.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
web/src/assets/img/structures/fishbone.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
web/src/assets/img/structures/logicalStructure.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
web/src/assets/img/structures/mindMap.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |