Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
adccef5699 | ||
|
|
94230f8ec6 | ||
|
|
a161661c6b | ||
|
|
08b971cd9a | ||
|
|
b64c8f132b | ||
|
|
d6181591c5 |
47
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] 提供丰富的配置,满足各种场景各种使用习惯
|
||||
|
||||
# 安装
|
||||
|
||||
@@ -136,4 +133,24 @@ const mindMap = new MindMap({
|
||||
<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: 184 KiB After Width: | Height: | Size: 48 KiB |
@@ -2,6 +2,7 @@ import MindMap from './index'
|
||||
import MiniMap from './src/plugins/MiniMap.js'
|
||||
import Watermark from './src/plugins/Watermark.js'
|
||||
import KeyboardNavigation from './src/plugins/KeyboardNavigation.js'
|
||||
import 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'
|
||||
@@ -10,6 +11,8 @@ 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'
|
||||
@@ -29,6 +32,7 @@ MindMap
|
||||
.usePlugin(Watermark)
|
||||
.usePlugin(Drag)
|
||||
.usePlugin(KeyboardNavigation)
|
||||
.usePlugin(ExportXMind)
|
||||
.usePlugin(ExportPDF)
|
||||
.usePlugin(Export)
|
||||
.usePlugin(Select)
|
||||
@@ -36,5 +40,7 @@ MindMap
|
||||
.usePlugin(RichText)
|
||||
.usePlugin(TouchEvent)
|
||||
.usePlugin(NodeImgAdjust)
|
||||
.usePlugin(Search)
|
||||
.usePlugin(Painter)
|
||||
|
||||
export default MindMap
|
||||
@@ -7,9 +7,9 @@ import Style from './src/core/render/node/Style'
|
||||
import KeyCommand from './src/core/command/KeyCommand'
|
||||
import Command from './src/core/command/Command'
|
||||
import BatchExecution from './src/utils/BatchExecution'
|
||||
import { layoutValueList, CONSTANTS } from './src/constants/constant'
|
||||
import { layoutValueList, CONSTANTS, commonCaches } from './src/constants/constant'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import { simpleDeepClone } from './src/utils'
|
||||
import { simpleDeepClone, getType } from './src/utils'
|
||||
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
|
||||
import { defaultOpt } from './src/constants/defaultOptions'
|
||||
|
||||
@@ -32,12 +32,12 @@ class MindMap {
|
||||
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
|
||||
@@ -81,6 +81,8 @@ class MindMap {
|
||||
|
||||
// 配置参数处理
|
||||
handleOpt(opt) {
|
||||
// 深拷贝一份节点数据
|
||||
opt.data = simpleDeepClone(opt.data || {})
|
||||
// 检查布局配置
|
||||
if (!layoutValueList.includes(opt.layout)) {
|
||||
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
@@ -132,6 +134,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() {
|
||||
// 合并主题配置
|
||||
@@ -145,6 +164,7 @@ class MindMap {
|
||||
this.renderer.clearAllActive()
|
||||
this.opt.theme = theme
|
||||
this.render(null, CONSTANTS.CHANGE_THEME)
|
||||
this.emit('view_theme_change', theme)
|
||||
}
|
||||
|
||||
// 获取当前主题
|
||||
@@ -238,7 +258,7 @@ class MindMap {
|
||||
|
||||
// 获取思维导图数据,节点树、主题、布局等
|
||||
getData(withConfig) {
|
||||
let nodeData = this.command.removeDataUid(this.command.getCopyData())
|
||||
let nodeData = this.command.getCopyData()
|
||||
let data = {}
|
||||
if (withConfig) {
|
||||
data = {
|
||||
|
||||
28
simple-mind-map/package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.0.16",
|
||||
"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",
|
||||
@@ -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"
|
||||
}
|
||||
@@ -411,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"
|
||||
}
|
||||
@@ -516,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",
|
||||
@@ -937,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"
|
||||
@@ -2142,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"
|
||||
}
|
||||
@@ -2206,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"
|
||||
}
|
||||
@@ -2461,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",
|
||||
@@ -2576,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"
|
||||
}
|
||||
@@ -2648,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",
|
||||
@@ -2966,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"
|
||||
@@ -3749,6 +3767,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,
|
||||
"requires": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
@@ -3800,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.6.6",
|
||||
"version": "0.6.13",
|
||||
"description": "一个简单的web在线思维导图",
|
||||
"authors": [
|
||||
{
|
||||
@@ -26,8 +26,8 @@
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.0.16",
|
||||
"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",
|
||||
|
||||
@@ -27,130 +27,162 @@ 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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -213,6 +245,10 @@ export const CONSTANTS = {
|
||||
TOP: 'top',
|
||||
RIGHT: 'right',
|
||||
BOTTOM: 'bottom'
|
||||
},
|
||||
PASTE_TYPE: {
|
||||
CLIP_BOARD: 'clipBoard',
|
||||
CANVAS: 'canvas'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,5 +323,12 @@ export const nodeDataNoStylePropList = [
|
||||
'richText',
|
||||
'resetRichText',
|
||||
'uid',
|
||||
'activeStyle'
|
||||
]
|
||||
'activeStyle',
|
||||
'associativeLineTargets',
|
||||
'associativeLineTargetControlOffsets'
|
||||
]
|
||||
|
||||
// 数据缓存
|
||||
export const commonCaches = {
|
||||
measureCustomNodeContentSizeEl: null
|
||||
}
|
||||
@@ -79,10 +79,6 @@ export const defaultOpt = {
|
||||
},
|
||||
// 是否只有当鼠标在画布内才响应快捷键事件
|
||||
enableShortcutOnlyWhenMouseInSvg: true,
|
||||
// 是否开启节点动画过渡
|
||||
enableNodeTransitionMove: true,
|
||||
// 如果开启节点动画过渡,可以通过该属性设置过渡的时间,单位ms
|
||||
nodeTransitionMoveDuration: 300,
|
||||
// 初始根节点的位置
|
||||
initRootNodePosition: null,
|
||||
// 导出png、svg、pdf时的图形内边距
|
||||
@@ -126,5 +122,17 @@ export const defaultOpt = {
|
||||
// 是否开启自定义节点内容
|
||||
isUseCustomNodeContent: false,
|
||||
// 自定义返回节点内容的方法
|
||||
customCreateNodeContent: null
|
||||
customCreateNodeContent: null,
|
||||
// 指定内部一些元素(节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素)添加到的位置,默认添加到document.body下
|
||||
customInnerElsAppendTo: null,
|
||||
// 拖拽元素时,指示元素新位置的块的最大高度
|
||||
nodeDragPlaceholderMaxSize: 20,
|
||||
// 是否允许创建一个隐藏的输入框,该输入框会在节点激活时聚焦,用于粘贴数据和自动进入文本编辑状态
|
||||
enableCreateHiddenInput: true,
|
||||
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
|
||||
// 该配置在enableCreateHiddenInput设为true时生效
|
||||
enableAutoEnterTextEditWhenKeydown: true,
|
||||
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
|
||||
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
|
||||
richTextEditFakeInPlace: false
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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*/)
|
||||
|
||||
@@ -10,6 +10,7 @@ class Event extends EventEmitter {
|
||||
this.mindMap = opt.mindMap
|
||||
this.isLeftMousedown = false
|
||||
this.isRightMousedown = false
|
||||
this.isMiddleMousedown = false
|
||||
this.mousedownPos = {
|
||||
x: 0,
|
||||
y: 0
|
||||
@@ -92,6 +93,8 @@ class Event extends EventEmitter {
|
||||
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
|
||||
@@ -107,9 +110,10 @@ class Event extends EventEmitter {
|
||||
this.mousemoveOffset.y = e.clientY - this.mousedownPos.y
|
||||
this.emit('mousemove', e, this)
|
||||
if (
|
||||
useLeftKeySelectionRightKeyDrag
|
||||
this.isMiddleMousedown ||
|
||||
(useLeftKeySelectionRightKeyDrag
|
||||
? this.isRightMousedown
|
||||
: this.isLeftMousedown
|
||||
: this.isLeftMousedown)
|
||||
) {
|
||||
e.preventDefault()
|
||||
this.emit('drag', e, this)
|
||||
@@ -120,6 +124,7 @@ class Event extends EventEmitter {
|
||||
onMouseup(e) {
|
||||
this.isLeftMousedown = false
|
||||
this.isRightMousedown = false
|
||||
this.isMiddleMousedown = false
|
||||
this.emit('mouseup', e, this)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,13 @@ 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 {
|
||||
copyNodeTree,
|
||||
simpleDeepClone,
|
||||
walk,
|
||||
bfsWalk,
|
||||
loadImage
|
||||
} from '../../utils'
|
||||
import { shapeList } from './node/Shape'
|
||||
import { lineStyleProps } from '../../themes/default'
|
||||
import { CONSTANTS } from '../../constants/constant'
|
||||
@@ -29,7 +35,7 @@ const layouts = {
|
||||
// 竖向时间轴
|
||||
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline,
|
||||
// 鱼骨图
|
||||
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone,
|
||||
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone
|
||||
}
|
||||
|
||||
// 渲染
|
||||
@@ -60,6 +66,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()
|
||||
// 绑定事件
|
||||
@@ -82,18 +94,31 @@ class Render {
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
// 点击事件
|
||||
this.mindMap.on('draw_click', (e) => {
|
||||
this.mindMap.on('draw_click', e => {
|
||||
// 清除激活状态
|
||||
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
|
||||
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')
|
||||
}
|
||||
})
|
||||
// 粘贴事件
|
||||
this.mindMap.on('paste', data => {
|
||||
this.onPaste(data)
|
||||
})
|
||||
// let timer = null
|
||||
// this.mindMap.on('view_data_change', () => {
|
||||
// clearTimeout(timer)
|
||||
// timer = setTimeout(() => {
|
||||
// this.render()
|
||||
// }, 300)
|
||||
// })
|
||||
}
|
||||
|
||||
// 注册命令
|
||||
@@ -135,9 +160,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)
|
||||
@@ -195,6 +223,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)
|
||||
}
|
||||
|
||||
// 注册快捷键
|
||||
@@ -212,7 +243,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)
|
||||
@@ -239,6 +270,14 @@ 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.textEdit.focusHiddenInput()
|
||||
})
|
||||
this.cut = this.cut.bind(this)
|
||||
this.mindMap.keyCommand.addShortcut('Control+x', this.cut)
|
||||
}
|
||||
|
||||
// 开启文字编辑,会禁用回车键和删除键相关快捷键防止冲突
|
||||
@@ -259,7 +298,6 @@ class Render {
|
||||
|
||||
// 渲染
|
||||
render(callback = () => {}, source) {
|
||||
let t = Date.now()
|
||||
// 如果当前还没有渲染完毕,不再触发渲染
|
||||
if (this.isRendering) {
|
||||
// 等待当前渲染完毕后再进行一次渲染
|
||||
@@ -279,7 +317,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) {
|
||||
@@ -290,7 +328,7 @@ class Render {
|
||||
// 更新根节点
|
||||
this.root = root
|
||||
// 渲染节点
|
||||
const onEnd = () => {
|
||||
this.root.render(() => {
|
||||
this.isRendering = false
|
||||
this.mindMap.emit('node_tree_render_end')
|
||||
callback && callback()
|
||||
@@ -299,22 +337,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)
|
||||
@@ -416,7 +445,7 @@ class Render {
|
||||
// 规范指定节点数据
|
||||
formatAppointNodes(appointNodes) {
|
||||
if (!appointNodes) return []
|
||||
return Array.isArray(appointNodes) ? appointNodes: [appointNodes]
|
||||
return Array.isArray(appointNodes) ? appointNodes : [appointNodes]
|
||||
}
|
||||
|
||||
// 插入同级节点,多个节点只会操作第一个节点
|
||||
@@ -425,7 +454,11 @@ class Render {
|
||||
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) {
|
||||
@@ -434,16 +467,22 @@ class Render {
|
||||
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: []
|
||||
@@ -458,7 +497,11 @@ class Render {
|
||||
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) {
|
||||
@@ -467,12 +510,17 @@ class Render {
|
||||
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: []
|
||||
@@ -540,13 +588,81 @@ class Render {
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 复制节点
|
||||
copy() {
|
||||
this.beingCopyData = this.copyNode()
|
||||
}
|
||||
|
||||
// 剪切节点
|
||||
cut() {
|
||||
this.mindMap.execCommand('CUT_NODE', copyData => {
|
||||
this.beingCopyData = copyData
|
||||
})
|
||||
}
|
||||
|
||||
// 粘贴节点
|
||||
paste() {
|
||||
if (this.beingCopyData) {
|
||||
this.mindMap.execCommand('PASTE_NODE', this.beingCopyData)
|
||||
}
|
||||
}
|
||||
|
||||
// 粘贴事件
|
||||
async onPaste({ text, img }) {
|
||||
// 检查剪切板数据是否有变化
|
||||
// 通过图片大小来判断图片是否发生变化,可能是不准确的,但是目前没有其他好方法
|
||||
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) {
|
||||
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) {
|
||||
console.log(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
|
||||
@@ -583,7 +699,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
|
||||
@@ -623,7 +741,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) {
|
||||
@@ -709,7 +827,7 @@ class Render {
|
||||
|
||||
// 粘贴节点到节点
|
||||
pasteNode(data) {
|
||||
if (this.activeNodeList.length <= 0) {
|
||||
if (this.activeNodeList.length <= 0 || !data) {
|
||||
return
|
||||
}
|
||||
this.activeNodeList.forEach(item => {
|
||||
@@ -751,6 +869,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, {
|
||||
@@ -864,10 +1018,11 @@ class Render {
|
||||
}
|
||||
|
||||
// 设置节点文本
|
||||
setNodeText(node, text, richText) {
|
||||
setNodeText(node, text, richText, resetRichText) {
|
||||
this.setNodeDataRender(node, {
|
||||
text,
|
||||
richText
|
||||
richText,
|
||||
resetRichText
|
||||
})
|
||||
}
|
||||
|
||||
@@ -992,6 +1147,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 => {
|
||||
@@ -1000,7 +1169,7 @@ class Render {
|
||||
}
|
||||
|
||||
// 设置节点数据,并判断是否渲染
|
||||
setNodeDataRender(node, data) {
|
||||
setNodeDataRender(node, data, notRender = false) {
|
||||
this.setNodeData(node, data)
|
||||
let changed = node.reRender()
|
||||
if (changed) {
|
||||
@@ -1008,7 +1177,9 @@ class Render {
|
||||
// 概要节点
|
||||
node.generalizationBelongNode.updateGeneralization()
|
||||
}
|
||||
this.mindMap.render()
|
||||
if (!notRender) this.mindMap.render()
|
||||
} else {
|
||||
this.mindMap.emit('node_tree_render_end')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,6 +1199,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,4 @@
|
||||
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils'
|
||||
import { getStrWithBrFromHtml, checkNodeOuter, isMobile } from '../../utils'
|
||||
|
||||
// 节点文字编辑类
|
||||
export default class TextEdit {
|
||||
@@ -10,11 +10,16 @@ export default class TextEdit {
|
||||
this.currentNode = null
|
||||
// 文本编辑框
|
||||
this.textEditNode = null
|
||||
// 隐藏的文本输入框
|
||||
this.hiddenInputEl = null
|
||||
// 节点激活时默认聚焦到隐藏输入框
|
||||
this.enableFocus = true
|
||||
// 文本编辑框是否显示
|
||||
this.showTextEdit = false
|
||||
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
|
||||
this.cacheEditingText = ''
|
||||
this.bindEvent()
|
||||
this.createHiddenInput()
|
||||
}
|
||||
|
||||
// 事件
|
||||
@@ -46,6 +51,10 @@ export default class TextEdit {
|
||||
this.mindMap.on('before_node_active', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
// 节点激活事件
|
||||
this.mindMap.on('node_active', () => {
|
||||
this.focusHiddenInput()
|
||||
})
|
||||
// 注册编辑快捷键
|
||||
this.mindMap.keyCommand.addShortcut('F2', () => {
|
||||
if (this.renderer.activeNodeList.length <= 0) {
|
||||
@@ -56,12 +65,83 @@ export default class TextEdit {
|
||||
this.mindMap.on('scale', this.onScale)
|
||||
}
|
||||
|
||||
// 创建一个隐藏的文本输入框
|
||||
createHiddenInput() {
|
||||
const { enableCreateHiddenInput, enableAutoEnterTextEditWhenKeydown } =
|
||||
this.mindMap.opt
|
||||
if (this.hiddenInputEl || isMobile() || !enableCreateHiddenInput) return
|
||||
this.hiddenInputEl = document.createElement('input')
|
||||
this.hiddenInputEl.type = 'text'
|
||||
this.hiddenInputEl.style.cssText = `
|
||||
position: fixed;
|
||||
left: -99999px;
|
||||
top: -99999px;
|
||||
`
|
||||
// 监听按键事件
|
||||
if (enableAutoEnterTextEditWhenKeydown) {
|
||||
this.hiddenInputEl.addEventListener('keydown', e => {
|
||||
const activeNodeList = this.mindMap.renderer.activeNodeList
|
||||
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
|
||||
const node = activeNodeList[0]
|
||||
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
|
||||
const keyCode = e.keyCode
|
||||
if (
|
||||
node &&
|
||||
(keyCode === 229 ||
|
||||
(keyCode >= 65 && keyCode <= 90) ||
|
||||
(keyCode >= 48 && keyCode <= 57)) &&
|
||||
!this.mindMap.keyCommand.hasCombinationKey(e)
|
||||
) {
|
||||
this.show(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
// 监听粘贴事件
|
||||
this.hiddenInputEl.addEventListener('paste', async event => {
|
||||
event.preventDefault()
|
||||
const text = (event.clipboardData || window.clipboardData).getData('text')
|
||||
const files = event.clipboardData.files
|
||||
let img = null
|
||||
if (files.length > 0) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (/^image\//.test(files[i].type)) {
|
||||
img = files[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
this.mindMap.emit('paste', {
|
||||
text,
|
||||
img
|
||||
})
|
||||
})
|
||||
document.body.appendChild(this.hiddenInputEl)
|
||||
}
|
||||
|
||||
// 让隐藏的文本输入框聚焦
|
||||
focusHiddenInput() {
|
||||
if (this.hiddenInputEl && this.enableFocus) this.hiddenInputEl.focus()
|
||||
}
|
||||
|
||||
// 关闭默认聚焦
|
||||
stopFocusOnNodeActive() {
|
||||
this.enableFocus = false
|
||||
}
|
||||
|
||||
// 开启默认聚焦
|
||||
openFocusOnNodeActive() {
|
||||
this.enableFocus = true
|
||||
}
|
||||
|
||||
// 注册临时快捷键
|
||||
registerTmpShortcut() {
|
||||
// 注册回车快捷键
|
||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Tab', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
@@ -89,14 +169,15 @@ export default class TextEdit {
|
||||
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()
|
||||
@@ -106,7 +187,7 @@ export default class TextEdit {
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
showEditTextBox(node, rect) {
|
||||
showEditTextBox(node, rect, isInserting) {
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
this.registerTmpShortcut()
|
||||
if (!this.textEditNode) {
|
||||
@@ -119,12 +200,19 @@ export default class TextEdit {
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.body.appendChild(this.textEditNode)
|
||||
this.textEditNode.addEventListener('mousedown', 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
|
||||
@@ -134,18 +222,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,6 +1,5 @@
|
||||
import Style from './Style'
|
||||
import Shape from './Shape'
|
||||
import { asyncRun, nodeToHTML } from '../../../utils'
|
||||
import { G, Rect, ForeignObject, SVG } from '@svgdotjs/svg.js'
|
||||
import nodeGeneralizationMethods from './nodeGeneralization'
|
||||
import nodeExpandBtnMethods from './nodeExpandBtn'
|
||||
@@ -98,6 +97,8 @@ class Node {
|
||||
this.isMultipleChoice = false
|
||||
// 是否需要重新layout
|
||||
this.needLayout = false
|
||||
// 当前是否是隐藏状态
|
||||
this.isHide = false
|
||||
// 概要相关方法
|
||||
Object.keys(nodeGeneralizationMethods).forEach(item => {
|
||||
this[item] = nodeGeneralizationMethods[item].bind(this)
|
||||
@@ -266,6 +267,7 @@ class Node {
|
||||
paddingY += this.shapePadding.paddingY
|
||||
// 节点形状
|
||||
this.shapeNode = this.shapeInstance.createShape()
|
||||
this.shapeNode.addClass('smm-node-shape')
|
||||
this.group.add(this.shapeNode)
|
||||
this.updateNodeShape()
|
||||
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
|
||||
@@ -365,9 +367,15 @@ class Node {
|
||||
this._unVisibleRectRegionNode.fill({
|
||||
color: 'transparent'
|
||||
})
|
||||
this.group.add(this._unVisibleRectRegionNode)
|
||||
}
|
||||
this.renderer.layout.renderExpandBtnRect(this._unVisibleRectRegionNode, this.expandBtnSize, width, height, this)
|
||||
this.group.add(this._unVisibleRectRegionNode)
|
||||
this.renderer.layout.renderExpandBtnRect(
|
||||
this._unVisibleRectRegionNode,
|
||||
this.expandBtnSize,
|
||||
width,
|
||||
height,
|
||||
this
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,10 +392,10 @@ class Node {
|
||||
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()
|
||||
}
|
||||
// 多选和取消多选
|
||||
@@ -413,7 +421,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)
|
||||
@@ -475,11 +483,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) {
|
||||
@@ -501,14 +505,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,8 +566,6 @@ class Node {
|
||||
|
||||
// 递归渲染
|
||||
render(callback = () => {}) {
|
||||
let { enableNodeTransitionMove, nodeTransitionMoveDuration } =
|
||||
this.mindMap.opt
|
||||
// 节点
|
||||
// 重新渲染连线
|
||||
this.renderLine()
|
||||
@@ -541,6 +574,7 @@ class Node {
|
||||
isLayout = true
|
||||
// 创建组
|
||||
this.group = new G()
|
||||
this.group.addClass('smm-node')
|
||||
this.group.css({
|
||||
cursor: 'default'
|
||||
})
|
||||
@@ -567,26 +601,16 @@ 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) {
|
||||
@@ -606,13 +630,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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,13 +658,9 @@ class Node {
|
||||
}
|
||||
// 子节点
|
||||
if (this.children && this.children.length) {
|
||||
asyncRun(
|
||||
this.children.map(item => {
|
||||
return () => {
|
||||
item.hide()
|
||||
}
|
||||
})
|
||||
)
|
||||
this.children.forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,13 +680,9 @@ class Node {
|
||||
}
|
||||
// 子节点
|
||||
if (this.children && this.children.length) {
|
||||
asyncRun(
|
||||
this.children.map(item => {
|
||||
return () => {
|
||||
item.show()
|
||||
}
|
||||
})
|
||||
)
|
||||
this.children.forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Style {
|
||||
// 设置新样式
|
||||
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
|
||||
|
||||
@@ -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,7 @@
|
||||
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 '../../../constants/constant'
|
||||
import { CONSTANTS, commonCaches } from '../../../constants/constant'
|
||||
|
||||
// 创建图片节点
|
||||
function createImgNode() {
|
||||
@@ -64,6 +64,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,
|
||||
@@ -88,8 +91,21 @@ function createRichTextNode() {
|
||||
}
|
||||
}
|
||||
if (recoverText) {
|
||||
let text = getTextFromHtml(this.nodeData.data.text)
|
||||
this.nodeData.data.text = `<p><span style="${this.style.createStyleText()}">${text}</span></p>`
|
||||
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')
|
||||
@@ -252,7 +268,7 @@ 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%);
|
||||
@@ -260,7 +276,8 @@ function createNoteNode() {
|
||||
background-color: #fff;
|
||||
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
|
||||
}
|
||||
@@ -293,20 +310,19 @@ function createNoteNode() {
|
||||
}
|
||||
|
||||
// 测量自定义节点内容元素的宽高
|
||||
let warpEl = null
|
||||
function measureCustomNodeContentSize (content) {
|
||||
if (!warpEl) {
|
||||
warpEl = document.createElement('div')
|
||||
warpEl.style.cssText = `
|
||||
if (!commonCaches.measureCustomNodeContentSizeEl) {
|
||||
commonCaches.measureCustomNodeContentSizeEl = document.createElement('div')
|
||||
commonCaches.measureCustomNodeContentSizeEl.style.cssText = `
|
||||
position: fixed;
|
||||
left: -99999px;
|
||||
top: -99999px;
|
||||
`
|
||||
this.mindMap.el.appendChild(warpEl)
|
||||
this.mindMap.el.appendChild(commonCaches.measureCustomNodeContentSizeEl)
|
||||
}
|
||||
warpEl.innerHTML = ''
|
||||
warpEl.appendChild(content)
|
||||
let rect = warpEl.getBoundingClientRect()
|
||||
commonCaches.measureCustomNodeContentSizeEl.innerHTML = ''
|
||||
commonCaches.measureCustomNodeContentSizeEl.appendChild(content)
|
||||
let rect = commonCaches.measureCustomNodeContentSizeEl.getBoundingClientRect()
|
||||
return {
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -82,15 +82,20 @@ class View {
|
||||
// 鼠标滚轮,向上和向左,都是缩小
|
||||
case CONSTANTS.DIR.UP:
|
||||
case CONSTANTS.DIR.LEFT:
|
||||
mousewheelZoomActionReverse ? this.enlarge(cx, cy, isTouchPad) : this.narrow(cx, cy, isTouchPad)
|
||||
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)
|
||||
mousewheelZoomActionReverse
|
||||
? this.narrow(cx, cy, isTouchPad)
|
||||
: this.enlarge(cx, cy, isTouchPad)
|
||||
break
|
||||
}
|
||||
} else {// 鼠标滚轮事件控制画布移动
|
||||
} else {
|
||||
// 鼠标滚轮事件控制画布移动
|
||||
let step = mousewheelMoveStep
|
||||
if (isTouchPad) {
|
||||
step = 5
|
||||
@@ -147,6 +152,7 @@ class View {
|
||||
|
||||
// 平移x,y方向
|
||||
translateXY(x, y) {
|
||||
if (x === 0 && y === 0) return
|
||||
this.x += x
|
||||
this.y += y
|
||||
this.transform()
|
||||
@@ -154,24 +160,28 @@ class View {
|
||||
|
||||
// 平移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()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 {
|
||||
@@ -101,7 +102,7 @@ class Base {
|
||||
}
|
||||
} else {
|
||||
// 创建新节点
|
||||
let uid = this.mindMap.uid++
|
||||
let uid = data.data.uid || createUid()
|
||||
newNode = new Node({
|
||||
data,
|
||||
uid,
|
||||
|
||||
@@ -234,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
|
||||
|
||||
@@ -315,7 +315,7 @@ class MindMap extends Base {
|
||||
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
|
||||
|
||||
@@ -123,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]
|
||||
@@ -142,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 {
|
||||
// 节点备注
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import associativeLineControlsMethods from './associativeLine/associativeLineControls'
|
||||
import associativeLineTextMethods from './associativeLine/associativeLineText'
|
||||
|
||||
// 关联线类
|
||||
// 关联线插件
|
||||
class AssociativeLine {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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,9 +1,9 @@
|
||||
import { imgToDataUrl, downloadFile, readBlob } from '../utils'
|
||||
import { imgToDataUrl, downloadFile, readBlob, removeHTMLEntities } from '../utils'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
|
||||
import { transformToMarkdown } from '../parse/toMarkdown'
|
||||
|
||||
// 导出类
|
||||
// 导出插件
|
||||
class Export {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
@@ -154,6 +154,7 @@ class Export {
|
||||
*/
|
||||
async png(name, transparent = false) {
|
||||
let { node, str } = await this.getSvgData()
|
||||
str = removeHTMLEntities(str)
|
||||
// 如果开启了富文本,则使用htmltocanvas转换为图片
|
||||
if (this.mindMap.richText) {
|
||||
let res = await this.mindMap.richText.handleExportPng(node.node)
|
||||
@@ -207,6 +208,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'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import JsPDF from 'jspdf'
|
||||
|
||||
// 导出PDF类,需要通过Export插件使用
|
||||
// 导出PDF插件,需要通过Export插件使用
|
||||
class ExportPDF {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import xmind from '../parse/xmind'
|
||||
|
||||
// 导出XMind类,需要通过Export插件使用
|
||||
// 导出XMind插件,需要通过Export插件使用
|
||||
class ExportXMind {
|
||||
// 构造函数
|
||||
constructor(opt) {
|
||||
@@ -12,6 +12,11 @@ class ExportXMind {
|
||||
const zipData = await xmind.transformToXmind(data, name)
|
||||
return zipData
|
||||
}
|
||||
|
||||
// 获取解析器
|
||||
getXmind() {
|
||||
return xmind
|
||||
}
|
||||
}
|
||||
|
||||
ExportXMind.instanceName = 'doExportXMind'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -47,7 +47,7 @@ class NodeImgAdjust {
|
||||
// 节点图片鼠标移动事件
|
||||
onNodeImgMousemove(node, img) {
|
||||
// 如果当前正在拖动调整中那么直接返回
|
||||
if (this.isMousedown || this.isAdjusted) return
|
||||
if (this.isMousedown || this.isAdjusted || this.mindMap.opt.readonly) return
|
||||
// 如果在当前节点内移动,以及自定义元素已经是显示状态,那么直接返回
|
||||
if (this.node === node && this.isShowHandleEl) return
|
||||
// 更新当前节点信息
|
||||
@@ -82,7 +82,7 @@ class NodeImgAdjust {
|
||||
this.createResizeBtnEl()
|
||||
}
|
||||
this.setHandleElRect()
|
||||
document.body.appendChild(this.handleEl)
|
||||
this.handleEl.style.display = 'block'
|
||||
this.isShowHandleEl = true
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class NodeImgAdjust {
|
||||
hideHandleEl() {
|
||||
if (!this.isShowHandleEl) return
|
||||
this.isShowHandleEl = false
|
||||
document.body.removeChild(this.handleEl)
|
||||
this.handleEl.style.display = 'none'
|
||||
this.handleEl.style.backgroundImage = ``
|
||||
this.handleEl.style.width = 0
|
||||
this.handleEl.style.height = 0
|
||||
@@ -121,8 +121,10 @@ class NodeImgAdjust {
|
||||
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
|
||||
@@ -139,7 +141,7 @@ class NodeImgAdjust {
|
||||
align-items: center;
|
||||
cursor: nwse-resize;
|
||||
`
|
||||
this.handleEl.appendChild(btnEl)
|
||||
btnEl.className = 'node-image-resize'
|
||||
// 给按钮元素绑定事件
|
||||
btnEl.addEventListener('mouseenter', () => {
|
||||
// 移入按钮,会触发节点图片的移出事件,所以需要再次显示按钮
|
||||
@@ -151,8 +153,50 @@ class NodeImgAdjust {
|
||||
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)
|
||||
}
|
||||
|
||||
// 鼠标按钮按下事件
|
||||
@@ -208,7 +252,10 @@ class NodeImgAdjust {
|
||||
|
||||
// 渲染完成事件
|
||||
onRenderEnd() {
|
||||
if (!this.isAdjusted) return
|
||||
if (!this.isAdjusted) {
|
||||
this.hideHandleEl()
|
||||
return
|
||||
}
|
||||
this.isAdjusted = false
|
||||
}
|
||||
|
||||
|
||||
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,7 +1,12 @@
|
||||
import Quill from 'quill'
|
||||
import 'quill/dist/quill.snow.css'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { walk, getTextFromHtml } from '../utils'
|
||||
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
|
||||
@@ -150,6 +155,12 @@ class RichText {
|
||||
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()
|
||||
@@ -163,33 +174,60 @@ 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()
|
||||
})
|
||||
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.zIndex = nodeTextEditZIndex
|
||||
this.textEditNode.style.backgroundColor =
|
||||
bgColor === 'transparent' ? '#fff' : bgColor
|
||||
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>')
|
||||
@@ -268,6 +306,12 @@ class RichText {
|
||||
handler: function () {
|
||||
// 覆盖默认的回车键换行
|
||||
}
|
||||
},
|
||||
tab: {
|
||||
key: 9,
|
||||
handler: function () {
|
||||
// 覆盖默认的tab键
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,11 +535,16 @@ class RichText {
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 将所有节点转换成非富文本节点
|
||||
|
||||
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'
|
||||
|
||||
// 选择节点类
|
||||
|
||||
// 节点选择插件
|
||||
class Select {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// 手势事件支持类
|
||||
|
||||
// 手势事件支持插件
|
||||
class TouchEvent {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
@@ -7,7 +6,7 @@ class TouchEvent {
|
||||
this.touchesNum = 0
|
||||
this.singleTouchstartEvent = null
|
||||
this.clickNum = 0
|
||||
this.doubleTouchmoveDistance = 0
|
||||
this.touchStartScaleView = null
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@@ -34,6 +33,7 @@ class TouchEvent {
|
||||
// 手指按下事件
|
||||
onTouchstart(e) {
|
||||
this.touchesNum = e.touches.length
|
||||
this.touchStartScaleView = null
|
||||
if (this.touchesNum === 1) {
|
||||
let touch = e.touches[0]
|
||||
this.singleTouchstartEvent = touch
|
||||
@@ -54,18 +54,46 @@ class TouchEvent {
|
||||
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 { 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
|
||||
if (distance > this.doubleTouchmoveDistance) {
|
||||
// 放大
|
||||
this.mindMap.view.enlarge(cx, cy)
|
||||
} else {
|
||||
// 缩小
|
||||
this.mindMap.view.narrow(cx, cy)
|
||||
// 手势缩放,基于最开始的位置进行缩放(基于前一个位置缩放不是线性关系); 缩放同时支持位置拖动
|
||||
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
|
||||
}
|
||||
this.doubleTouchmoveDistance = distance
|
||||
const viewBefore = this.touchStartScaleView
|
||||
const 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +120,7 @@ class TouchEvent {
|
||||
}
|
||||
this.touchesNum = 0
|
||||
this.singleTouchstartEvent = null
|
||||
this.doubleTouchmoveDistance = 0
|
||||
this.touchStartScaleView = null
|
||||
}
|
||||
|
||||
// 发送鼠标事件
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Text, G } from '@svgdotjs/svg.js'
|
||||
import { degToRad, camelCaseToHyphen } from '../utils'
|
||||
import merge from 'deepmerge'
|
||||
|
||||
// 水印类
|
||||
// 水印插件
|
||||
class Watermark {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
|
||||
@@ -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,11 +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,
|
||||
remove,
|
||||
imgAdjust
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ export default {
|
||||
lineDasharray: 'none',
|
||||
// 连线风格
|
||||
lineStyle: 'straight', // 针对logicalStructure、mindMap两种结构。曲线(curve)、直线(straight)、直连(direct)
|
||||
// 曲线连接时,根节点和其他节点的连接线样式保持统一,默认根节点为 ( 型,其他节点为 { 型,设为true后,都为 { 型
|
||||
rootLineKeepSameInCurve: true,
|
||||
// 概要连线的粗细
|
||||
generalizationLineWidth: 1,
|
||||
// 概要连线的颜色
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -424,3 +429,206 @@ export const getImageSize = src => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建节点唯一的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
|
||||
15
web/package-lock.json
generated
@@ -35,7 +35,8 @@
|
||||
"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": {
|
||||
@@ -16224,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",
|
||||
@@ -29476,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",
|
||||
@@ -39,7 +40,8 @@
|
||||
"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 |
@@ -4,8 +4,14 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
|
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/仓鼠.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/才镇.jpg
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
web/src/assets/avatar/水车.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 2479351 */
|
||||
src: url('iconfont.woff2?t=1689210173189') format('woff2'),
|
||||
url('iconfont.woff?t=1689210173189') format('woff'),
|
||||
url('iconfont.ttf?t=1689210173189') format('truetype');
|
||||
src: url('iconfont.woff2?t=1690537337895') format('woff2'),
|
||||
url('iconfont.woff?t=1690537337895') format('woff'),
|
||||
url('iconfont.ttf?t=1690537337895') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,34 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.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";
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
BIN
web/src/assets/img/themes/darkNightLceBlade.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |