Feat:1.新增同时插入多个同级节点、多个子节点的命令;2.复制、剪切操作支持同时操作多个节点;3.新增对插入同级节点、子节点的命令插入的第四个参数数据的处理

This commit is contained in:
wanglin2 2023-09-22 10:00:44 +08:00
parent 443465eb86
commit a9ea4b8e33
3 changed files with 363 additions and 110 deletions

View File

@ -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()
}

View File

@ -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
}

View File

@ -12,7 +12,10 @@
<ShortcutKey></ShortcutKey>
<Contextmenu v-if="mindMap" :mindMap="mindMap"></Contextmenu>
<RichTextToolbar v-if="mindMap" :mindMap="mindMap"></RichTextToolbar>
<NodeNoteContentShow v-if="mindMap" :mindMap="mindMap"></NodeNoteContentShow>
<NodeNoteContentShow
v-if="mindMap"
:mindMap="mindMap"
></NodeNoteContentShow>
<NodeImgPreview v-if="mindMap" :mindMap="mindMap"></NodeImgPreview>
<SidebarTrigger v-if="!isZenMode"></SidebarTrigger>
<Search v-if="mindMap" :mindMap="mindMap"></Search>
@ -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,
// 1routerstorei18nvue西
// 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)
}
}
}