From a9ea4b8e338c5be6b1509c87de0d2241be3d47bf Mon Sep 17 00:00:00 2001
From: wanglin2 <1013335014@qq.com>
Date: Fri, 22 Sep 2023 10:00:44 +0800
Subject: [PATCH] =?UTF-8?q?Feat=EF=BC=9A1.=E6=96=B0=E5=A2=9E=E5=90=8C?=
=?UTF-8?q?=E6=97=B6=E6=8F=92=E5=85=A5=E5=A4=9A=E4=B8=AA=E5=90=8C=E7=BA=A7?=
=?UTF-8?q?=E8=8A=82=E7=82=B9=E3=80=81=E5=A4=9A=E4=B8=AA=E5=AD=90=E8=8A=82?=
=?UTF-8?q?=E7=82=B9=E7=9A=84=E5=91=BD=E4=BB=A4=EF=BC=9B2.=E5=A4=8D?=
=?UTF-8?q?=E5=88=B6=E3=80=81=E5=89=AA=E5=88=87=E6=93=8D=E4=BD=9C=E6=94=AF?=
=?UTF-8?q?=E6=8C=81=E5=90=8C=E6=97=B6=E6=93=8D=E4=BD=9C=E5=A4=9A=E4=B8=AA?=
=?UTF-8?q?=E8=8A=82=E7=82=B9=EF=BC=9B3.=E6=96=B0=E5=A2=9E=E5=AF=B9?=
=?UTF-8?q?=E6=8F=92=E5=85=A5=E5=90=8C=E7=BA=A7=E8=8A=82=E7=82=B9=E3=80=81?=
=?UTF-8?q?=E5=AD=90=E8=8A=82=E7=82=B9=E7=9A=84=E5=91=BD=E4=BB=A4=E6=8F=92?=
=?UTF-8?q?=E5=85=A5=E7=9A=84=E7=AC=AC=E5=9B=9B=E4=B8=AA=E5=8F=82=E6=95=B0?=
=?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9A=84=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
simple-mind-map/src/core/render/Render.js | 235 ++++++++++++++++------
simple-mind-map/src/utils/index.js | 51 +++++
web/src/pages/Edit/components/Edit.vue | 187 +++++++++++++----
3 files changed, 363 insertions(+), 110 deletions(-)
diff --git a/simple-mind-map/src/core/render/Render.js b/simple-mind-map/src/core/render/Render.js
index 3c8ad5e5..1f1c7a6d 100644
--- a/simple-mind-map/src/core/render/Render.js
+++ b/simple-mind-map/src/core/render/Render.js
@@ -13,7 +13,12 @@ import {
walk,
bfsWalk,
loadImage,
- isUndef
+ isUndef,
+ getTopAncestorsFomNodeList,
+ addDataToAppointNodes,
+ createUidForAppointNodes,
+ formatDataToArray,
+ getNodeIndex
} from '../../utils'
import { shapeList } from './node/Shape'
import { lineStyleProps } from '../../themes/default'
@@ -40,7 +45,6 @@ const layouts = {
}
// 渲染
-
class Render {
// 构造函数
constructor(opt = {}) {
@@ -132,9 +136,18 @@ class Render {
// 插入同级节点
this.insertNode = this.insertNode.bind(this)
this.mindMap.command.add('INSERT_NODE', this.insertNode)
+ // 插入多个同级节点
+ this.insertMultiNode = this.insertMultiNode.bind(this)
+ this.mindMap.command.add('INSERT_MULTI_NODE', this.insertMultiNode)
// 插入子节点
this.insertChildNode = this.insertChildNode.bind(this)
this.mindMap.command.add('INSERT_CHILD_NODE', this.insertChildNode)
+ // 插入多个子节点
+ this.insertMultiChildNode = this.insertMultiChildNode.bind(this)
+ this.mindMap.command.add(
+ 'INSERT_MULTI_CHILD_NODE',
+ this.insertMultiChildNode
+ )
// 上移节点
this.upNode = this.upNode.bind(this)
this.mindMap.command.add('UP_NODE', this.upNode)
@@ -386,15 +399,6 @@ class Render {
})
}
- // 获取节点在同级里的索引位置
- getNodeIndex(node) {
- return node.parent
- ? node.parent.children.findIndex(item => {
- return item.uid === node.uid
- })
- : 0
- }
-
// 全选
selectAll() {
walk(
@@ -439,31 +443,36 @@ class Render {
}
}
- // 规范指定节点数据
- formatAppointNodes(appointNodes) {
- if (!appointNodes) return []
- return Array.isArray(appointNodes) ? appointNodes : [appointNodes]
- }
-
- // 插入同级节点,多个节点只会操作第一个节点
+ // 插入同级节点
insertNode(
openEdit = true,
appointNodes = [],
appointData = null,
appointChildren = []
) {
- appointNodes = this.formatAppointNodes(appointNodes)
+ appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.textEdit.hideEditTextBox()
- let {
+ const {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt
- let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
- let handleMultiNodes = list.length > 1
- let needDestroyNodeList = {}
+ const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
+ const handleMultiNodes = list.length > 1
+ const isRichText = !!this.mindMap.richText
+ const params = {
+ expand: true,
+ richText: isRichText,
+ resetRichText: isRichText,
+ isActive: handleMultiNodes || !openEdit // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
+ }
+ // 动态指定的子节点数据也需要添加相关属性
+ appointChildren = addDataToAppointNodes(appointChildren, {
+ ...params
+ })
+ const needDestroyNodeList = {}
list.forEach(node => {
if (node.isGeneralization || node.isRoot) {
return
@@ -475,22 +484,18 @@ class Render {
needDestroyNodeList[parent.uid] = parent
}
// 新插入节点的默认文本
- let text = isOneLayer
+ const text = isOneLayer
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
// 计算插入位置
- let index = parent.nodeData.children.findIndex(item => {
+ const index = parent.nodeData.children.findIndex(item => {
return item.data.uid === node.uid
})
- let isRichText = !!this.mindMap.richText
parent.nodeData.children.splice(index + 1, 0, {
inserting: handleMultiNodes ? false : openEdit, // 如果同时对多个节点插入子节点,那么无需进入编辑模式,
data: {
text: text,
- expand: true,
- richText: isRichText,
- resetRichText: isRichText,
- isActive: handleMultiNodes || !openEdit, // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
+ ...params,
...(appointData || {})
},
children: [...appointChildren]
@@ -506,6 +511,51 @@ class Render {
this.mindMap.render()
}
+ // 插入多个同级节点
+ insertMultiNode(appointNodes, nodeList) {
+ if (!nodeList || nodeList.length <= 0) return
+ appointNodes = formatDataToArray(appointNodes)
+ if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
+ return
+ }
+ this.textEdit.hideEditTextBox()
+ const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
+ const isRichText = !!this.mindMap.richText
+ const params = {
+ expand: true,
+ richText: isRichText,
+ resetRichText: isRichText,
+ isActive: true
+ }
+ nodeList = addDataToAppointNodes(nodeList, params)
+ const needDestroyNodeList = {}
+ list.forEach(node => {
+ if (node.isGeneralization || node.isRoot) {
+ return
+ }
+ const parent = node.parent
+ const isOneLayer = node.layerIndex === 1
+ // 插入二级节点时根节点需要重新渲染
+ if (isOneLayer && !needDestroyNodeList[parent.uid]) {
+ needDestroyNodeList[parent.uid] = parent
+ }
+ // 计算插入位置
+ const index = parent.nodeData.children.findIndex(item => {
+ return item.data.uid === node.uid
+ })
+ parent.nodeData.children.splice(
+ index + 1,
+ 0,
+ ...createUidForAppointNodes(simpleDeepClone(nodeList))
+ )
+ })
+ Object.keys(needDestroyNodeList).forEach(key => {
+ needDestroyNodeList[key].destroy()
+ })
+ this.clearActive()
+ this.mindMap.render()
+ }
+
// 插入子节点
insertChildNode(
openEdit = true,
@@ -513,17 +563,28 @@ class Render {
appointData = null,
appointChildren = []
) {
- appointNodes = this.formatAppointNodes(appointNodes)
+ appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.textEdit.hideEditTextBox()
- let {
+ const {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt
- let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
- let handleMultiNodes = list.length > 1
+ const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
+ const handleMultiNodes = list.length > 1
+ const isRichText = !!this.mindMap.richText
+ const params = {
+ expand: true,
+ richText: isRichText,
+ resetRichText: isRichText,
+ isActive: handleMultiNodes || !openEdit // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
+ }
+ // 动态指定的子节点数据也需要添加相关属性
+ appointChildren = addDataToAppointNodes(appointChildren, {
+ ...params
+ })
list.forEach(node => {
if (node.isGeneralization) {
return
@@ -531,18 +592,14 @@ class Render {
if (!node.nodeData.children) {
node.nodeData.children = []
}
- let text = node.isRoot
+ const text = node.isRoot
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
- let isRichText = !!this.mindMap.richText
node.nodeData.children.push({
inserting: handleMultiNodes ? false : openEdit, // 如果同时对多个节点插入子节点,那么无需进入编辑模式
data: {
text: text,
- expand: true,
- richText: isRichText,
- resetRichText: isRichText,
- isActive: handleMultiNodes || !openEdit, // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
+ ...params,
...(appointData || {})
},
children: [...appointChildren]
@@ -560,6 +617,41 @@ class Render {
this.mindMap.render()
}
+ // 插入多个子节点
+ insertMultiChildNode(appointNodes, childList) {
+ if (!childList || childList.length <= 0) return
+ appointNodes = formatDataToArray(appointNodes)
+ if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
+ return
+ }
+ this.textEdit.hideEditTextBox()
+ const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
+ const isRichText = !!this.mindMap.richText
+ const params = {
+ expand: true,
+ richText: isRichText,
+ resetRichText: isRichText,
+ isActive: true
+ }
+ childList = addDataToAppointNodes(childList, params)
+ list.forEach(node => {
+ if (node.isGeneralization) {
+ return
+ }
+ if (!node.nodeData.children) {
+ node.nodeData.children = []
+ }
+ node.nodeData.children.push(...childList)
+ // 插入子节点时自动展开子节点
+ node.nodeData.data.expand = true
+ if (node.isRoot) {
+ node.destroy()
+ }
+ })
+ this.clearActive()
+ this.mindMap.render()
+ }
+
// 上移节点,多个节点只会操作第一个节点
upNode() {
if (this.activeNodeList.length <= 0) {
@@ -617,19 +709,19 @@ class Render {
// 复制节点
copy() {
this.beingCopyData = this.copyNode()
- this.setCoptyDataToClipboard(this.beingCopyData)
+ this.setCopyDataToClipboard(this.beingCopyData)
}
// 剪切节点
cut() {
this.mindMap.execCommand('CUT_NODE', copyData => {
this.beingCopyData = copyData
- this.setCoptyDataToClipboard(copyData)
+ this.setCopyDataToClipboard(copyData)
})
}
// 将粘贴或剪切的数据设置到用户剪切板中
- setCoptyDataToClipboard(data) {
+ setCopyDataToClipboard(data) {
if (navigator.clipboard) {
navigator.clipboard.writeText(
JSON.stringify({
@@ -720,13 +812,9 @@ class Render {
}
if (smmData) {
this.mindMap.execCommand(
- 'INSERT_CHILD_NODE',
- false,
+ 'INSERT_MULTI_CHILD_NODE',
[],
- {
- ...smmData.data
- },
- [...smmData.children]
+ Array.isArray(smmData) ? smmData : [smmData]
)
} else {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
@@ -770,7 +858,7 @@ class Render {
// 将节点移动到另一个节点的前面或后面
insertTo(node, exist, dir = 'before') {
- let nodeList = this.formatAppointNodes(node)
+ let nodeList = formatDataToArray(node)
nodeList = nodeList.filter(item => {
return !item.isRoot
})
@@ -823,7 +911,7 @@ class Render {
// 移除节点
removeNode(appointNodes = []) {
- appointNodes = this.formatAppointNodes(appointNodes)
+ appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
@@ -895,32 +983,40 @@ class Render {
// 移除某个指定节点
removeOneNode(node) {
- let index = this.getNodeIndex(node)
+ let index = getNodeIndex(node)
node.remove()
node.parent.children.splice(index, 1)
node.parent.nodeData.children.splice(index, 1)
}
- // 复制节点,多个节点只会操作第一个节点
+ // 复制节点
copyNode() {
if (this.activeNodeList.length <= 0) {
return
}
- return copyNodeTree({}, this.activeNodeList[0], true)
+ const nodeList = getTopAncestorsFomNodeList(this.activeNodeList)
+ return nodeList.map(node => {
+ return copyNodeTree({}, node, true)
+ })
}
- // 剪切节点,多个节点只会操作第一个节点
+ // 剪切节点
cutNode(callback) {
if (this.activeNodeList.length <= 0) {
return
}
- let node = this.activeNodeList[0]
- if (node.isRoot) {
- return null
- }
- let copyData = copyNodeTree({}, node, true)
- this.removeActiveNode(node)
- this.removeOneNode(node)
+ const nodeList = getTopAncestorsFomNodeList(this.activeNodeList).filter(
+ node => {
+ return !node.isRoot
+ }
+ )
+ const copyData = nodeList.map(node => {
+ return copyNodeTree({}, node, true)
+ })
+ nodeList.forEach(node => {
+ this.removeActiveNode(node)
+ this.removeOneNode(node)
+ })
this.mindMap.emit('node_active', null, [...this.activeNodeList])
this.mindMap.render()
if (callback && typeof callback === 'function') {
@@ -928,9 +1024,9 @@ class Render {
}
}
- // 移动一个节点作为另一个节点的子节点
+ // 移动节点作为另一个节点的子节点
moveNodeTo(node, toNode) {
- let nodeList = this.formatAppointNodes(node)
+ let nodeList = formatDataToArray(node)
nodeList = nodeList.filter(item => {
return !item.isRoot
})
@@ -949,11 +1045,16 @@ class Render {
// 粘贴节点到节点
pasteNode(data) {
- if (this.activeNodeList.length <= 0 || !data) {
+ data = formatDataToArray(data)
+ if (this.activeNodeList.length <= 0 || data.length <= 0) {
return
}
- this.activeNodeList.forEach(item => {
- item.nodeData.children.push(simpleDeepClone(data))
+ this.activeNodeList.forEach(node => {
+ node.nodeData.children.push(
+ ...data.map(item => {
+ return simpleDeepClone(item)
+ })
+ )
})
this.mindMap.render()
}
diff --git a/simple-mind-map/src/utils/index.js b/simple-mind-map/src/utils/index.js
index c854eaaf..cd52b936 100644
--- a/simple-mind-map/src/utils/index.js
+++ b/simple-mind-map/src/utils/index.js
@@ -716,3 +716,54 @@ export const selectAllInput = el => {
selection.removeAllRanges()
selection.addRange(range)
}
+
+// 给指定的节点列表树数据添加附加数据,会修改原数据
+export const addDataToAppointNodes = (appointNodes, data = {}) => {
+ const walk = list => {
+ list.forEach(node => {
+ node.data = {
+ ...node.data,
+ ...data
+ }
+ if (node.children && node.children.length > 0) {
+ walk(node.children)
+ }
+ })
+ }
+ walk(appointNodes)
+ return appointNodes
+}
+
+// 给指定的节点列表树数据添加uid,如果不存在的话,会修改原数据
+export const createUidForAppointNodes = appointNodes => {
+ const walk = list => {
+ list.forEach(node => {
+ if (!node.data) {
+ node.data = {}
+ }
+ if (isUndef(node.data.uid)) {
+ node.data.uid = createUid()
+ }
+ if (node.children && node.children.length > 0) {
+ walk(node.children)
+ }
+ })
+ }
+ walk(appointNodes)
+ return appointNodes
+}
+
+// 传入一个数据,如果该数据是数组,那么返回该数组,否则返回一个以该数据为成员的数组
+export const formatDataToArray = data => {
+ if (!data) return []
+ return Array.isArray(data) ? data : [data]
+}
+
+// 获取节点在同级里的位置索引
+export const getNodeIndex = node => {
+ return node.parent
+ ? node.parent.children.findIndex(item => {
+ return item.uid === node.uid
+ })
+ : 0
+}
diff --git a/web/src/pages/Edit/components/Edit.vue b/web/src/pages/Edit/components/Edit.vue
index d0044080..c8315c24 100644
--- a/web/src/pages/Edit/components/Edit.vue
+++ b/web/src/pages/Edit/components/Edit.vue
@@ -12,7 +12,10 @@
-
+
@@ -264,10 +267,10 @@ export default {
// 如果url中存在要打开的文件,那么思维导图数据、主题、布局都使用默认的
if (hasFileURL) {
root = {
- "data": {
- "text": "根节点"
+ data: {
+ text: '根节点'
},
- "children": []
+ children: []
}
layout = exampleData.layout
theme = exampleData.theme
@@ -296,7 +299,7 @@ export default {
useLeftKeySelectionRightKeyDrag: this.useLeftKeySelectionRightKeyDrag,
customInnerElsAppendTo: null,
enableAutoEnterTextEditWhenKeydown: true,
- customHandleClipboardText: handleClipboardText,
+ customHandleClipboardText: handleClipboardText
// isUseCustomNodeContent: true,
// 示例1:组件里用到了router、store、i18n等实例化vue组件时需要用到的东西
// customCreateNodeContent: (node) => {
@@ -344,46 +347,33 @@ export default {
this.mindMap.keyCommand.addShortcut('Control+s', () => {
this.manualSave()
})
- // 转发事件
- ;[
- 'node_active',
- 'data_change',
- 'view_data_change',
- 'back_forward',
- 'node_contextmenu',
- 'node_click',
- 'draw_click',
- 'expand_btn_click',
- 'svg_mousedown',
- 'mouseup',
- 'mode_change',
- 'node_tree_render_end',
- 'rich_text_selection_change',
- 'transforming-dom-to-images',
- 'generalization_node_contextmenu',
- 'painter_start',
- 'painter_end',
- 'scrollbar_change'
- ].forEach(event => {
- this.mindMap.on(event, (...args) => {
- this.$bus.$emit(event, ...args)
- })
+ // 转发事件
+ ;[
+ 'node_active',
+ 'data_change',
+ 'view_data_change',
+ 'back_forward',
+ 'node_contextmenu',
+ 'node_click',
+ 'draw_click',
+ 'expand_btn_click',
+ 'svg_mousedown',
+ 'mouseup',
+ 'mode_change',
+ 'node_tree_render_end',
+ 'rich_text_selection_change',
+ 'transforming-dom-to-images',
+ 'generalization_node_contextmenu',
+ 'painter_start',
+ 'painter_end',
+ 'scrollbar_change'
+ ].forEach(event => {
+ this.mindMap.on(event, (...args) => {
+ this.$bus.$emit(event, ...args)
})
+ })
this.bindSaveEvent()
- // setTimeout(() => {
- // 动态给指定节点添加子节点
- // this.mindMap.execCommand('INSERT_CHILD_NODE', false, this.mindMap.renderer.root, {
- // text: '自定义内容'
- // })
-
- // 动态给指定节点添加同级节点
- // this.mindMap.execCommand('INSERT_NODE', false, this.mindMap.renderer.root, {
- // text: '自定义内容'
- // })
-
- // 动态删除指定节点
- // this.mindMap.execCommand('REMOVE_NODE', this.mindMap.renderer.root.children[0])
- // }, 5000);
+ this.testDynamicCreateNodes()
// 如果应用被接管,那么抛出事件传递思维导图实例
if (window.takeOverApp) {
this.$bus.$emit('app_inited', this.mindMap)
@@ -477,6 +467,117 @@ export default {
// 移除节点富文本编辑插件
removeRichTextPlugin() {
this.mindMap.removePlugin(RichText)
+ },
+
+ // 测试动态插入节点
+ testDynamicCreateNodes() {
+ return
+ setTimeout(() => {
+ // 动态给指定节点添加子节点
+ // this.mindMap.execCommand(
+ // 'INSERT_CHILD_NODE',
+ // false,
+ // this.mindMap.renderer.root,
+ // {
+ // text: '自定义内容'
+ // },
+ // [
+ // {
+ // data: {
+ // text: '自定义子节点'
+ // }
+ // }
+ // ]
+ // )
+
+ // 动态给指定节点添加同级节点
+ // this.mindMap.execCommand(
+ // 'INSERT_NODE',
+ // false,
+ // null,
+ // {
+ // text: '自定义内容'
+ // },
+ // [
+ // {
+ // data: {
+ // text: '自定义同级节点'
+ // },
+ // children: [
+ // {
+ // data: {
+ // text: '自定义同级节点2'
+ // },
+ // children: []
+ // }
+ // ]
+ // }
+ // ]
+ // )
+
+ // 动态插入多个子节点
+ // this.mindMap.execCommand('INSERT_MULTI_CHILD_NODE', null, [
+ // {
+ // data: {
+ // text: '自定义节点1'
+ // },
+ // children: [
+ // {
+ // data: {
+ // text: '自定义节点1-1'
+ // },
+ // children: []
+ // }
+ // ]
+ // },
+ // {
+ // data: {
+ // text: '自定义节点2'
+ // },
+ // children: [
+ // {
+ // data: {
+ // text: '自定义节点2-1'
+ // },
+ // children: []
+ // }
+ // ]
+ // }
+ // ])
+
+ // 动态插入多个同级节点
+ // this.mindMap.execCommand('INSERT_MULTI_NODE', null, [
+ // {
+ // data: {
+ // text: '自定义节点1'
+ // },
+ // children: [
+ // {
+ // data: {
+ // text: '自定义节点1-1'
+ // },
+ // children: []
+ // }
+ // ]
+ // },
+ // {
+ // data: {
+ // text: '自定义节点2'
+ // },
+ // children: [
+ // {
+ // data: {
+ // text: '自定义节点2-1'
+ // },
+ // children: []
+ // }
+ // ]
+ // }
+ // ])
+
+ // 动态删除指定节点
+ // this.mindMap.execCommand('REMOVE_NODE', this.mindMap.renderer.root.children[0])
+ }, 5000)
}
}
}