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, walk,
bfsWalk, bfsWalk,
loadImage, loadImage,
isUndef isUndef,
getTopAncestorsFomNodeList,
addDataToAppointNodes,
createUidForAppointNodes,
formatDataToArray,
getNodeIndex
} from '../../utils' } from '../../utils'
import { shapeList } from './node/Shape' import { shapeList } from './node/Shape'
import { lineStyleProps } from '../../themes/default' import { lineStyleProps } from '../../themes/default'
@ -40,7 +45,6 @@ const layouts = {
} }
// 渲染 // 渲染
class Render { class Render {
// 构造函数 // 构造函数
constructor(opt = {}) { constructor(opt = {}) {
@ -132,9 +136,18 @@ class Render {
// 插入同级节点 // 插入同级节点
this.insertNode = this.insertNode.bind(this) this.insertNode = this.insertNode.bind(this)
this.mindMap.command.add('INSERT_NODE', this.insertNode) 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.insertChildNode = this.insertChildNode.bind(this)
this.mindMap.command.add('INSERT_CHILD_NODE', this.insertChildNode) 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.upNode = this.upNode.bind(this)
this.mindMap.command.add('UP_NODE', this.upNode) 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() { selectAll() {
walk( walk(
@ -439,31 +443,36 @@ class Render {
} }
} }
// 规范指定节点数据 // 插入同级节点
formatAppointNodes(appointNodes) {
if (!appointNodes) return []
return Array.isArray(appointNodes) ? appointNodes : [appointNodes]
}
// 插入同级节点,多个节点只会操作第一个节点
insertNode( insertNode(
openEdit = true, openEdit = true,
appointNodes = [], appointNodes = [],
appointData = null, appointData = null,
appointChildren = [] appointChildren = []
) { ) {
appointNodes = this.formatAppointNodes(appointNodes) appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) { if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return return
} }
this.textEdit.hideEditTextBox() this.textEdit.hideEditTextBox()
let { const {
defaultInsertSecondLevelNodeText, defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt } = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
let handleMultiNodes = list.length > 1 const handleMultiNodes = list.length > 1
let needDestroyNodeList = {} 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 => { list.forEach(node => {
if (node.isGeneralization || node.isRoot) { if (node.isGeneralization || node.isRoot) {
return return
@ -475,22 +484,18 @@ class Render {
needDestroyNodeList[parent.uid] = parent needDestroyNodeList[parent.uid] = parent
} }
// 新插入节点的默认文本 // 新插入节点的默认文本
let text = isOneLayer const text = isOneLayer
? defaultInsertSecondLevelNodeText ? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
// 计算插入位置 // 计算插入位置
let index = parent.nodeData.children.findIndex(item => { const index = parent.nodeData.children.findIndex(item => {
return item.data.uid === node.uid return item.data.uid === node.uid
}) })
let isRichText = !!this.mindMap.richText
parent.nodeData.children.splice(index + 1, 0, { parent.nodeData.children.splice(index + 1, 0, {
inserting: handleMultiNodes ? false : openEdit, // 如果同时对多个节点插入子节点,那么无需进入编辑模式, inserting: handleMultiNodes ? false : openEdit, // 如果同时对多个节点插入子节点,那么无需进入编辑模式,
data: { data: {
text: text, text: text,
expand: true, ...params,
richText: isRichText,
resetRichText: isRichText,
isActive: handleMultiNodes || !openEdit, // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
...(appointData || {}) ...(appointData || {})
}, },
children: [...appointChildren] children: [...appointChildren]
@ -506,6 +511,51 @@ class Render {
this.mindMap.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( insertChildNode(
openEdit = true, openEdit = true,
@ -513,17 +563,28 @@ class Render {
appointData = null, appointData = null,
appointChildren = [] appointChildren = []
) { ) {
appointNodes = this.formatAppointNodes(appointNodes) appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) { if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return return
} }
this.textEdit.hideEditTextBox() this.textEdit.hideEditTextBox()
let { const {
defaultInsertSecondLevelNodeText, defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt } = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList const list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
let handleMultiNodes = list.length > 1 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 => { list.forEach(node => {
if (node.isGeneralization) { if (node.isGeneralization) {
return return
@ -531,18 +592,14 @@ class Render {
if (!node.nodeData.children) { if (!node.nodeData.children) {
node.nodeData.children = [] node.nodeData.children = []
} }
let text = node.isRoot const text = node.isRoot
? defaultInsertSecondLevelNodeText ? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
let isRichText = !!this.mindMap.richText
node.nodeData.children.push({ node.nodeData.children.push({
inserting: handleMultiNodes ? false : openEdit, // 如果同时对多个节点插入子节点,那么无需进入编辑模式 inserting: handleMultiNodes ? false : openEdit, // 如果同时对多个节点插入子节点,那么无需进入编辑模式
data: { data: {
text: text, text: text,
expand: true, ...params,
richText: isRichText,
resetRichText: isRichText,
isActive: handleMultiNodes || !openEdit, // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
...(appointData || {}) ...(appointData || {})
}, },
children: [...appointChildren] children: [...appointChildren]
@ -560,6 +617,41 @@ class Render {
this.mindMap.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() { upNode() {
if (this.activeNodeList.length <= 0) { if (this.activeNodeList.length <= 0) {
@ -617,19 +709,19 @@ class Render {
// 复制节点 // 复制节点
copy() { copy() {
this.beingCopyData = this.copyNode() this.beingCopyData = this.copyNode()
this.setCoptyDataToClipboard(this.beingCopyData) this.setCopyDataToClipboard(this.beingCopyData)
} }
// 剪切节点 // 剪切节点
cut() { cut() {
this.mindMap.execCommand('CUT_NODE', copyData => { this.mindMap.execCommand('CUT_NODE', copyData => {
this.beingCopyData = copyData this.beingCopyData = copyData
this.setCoptyDataToClipboard(copyData) this.setCopyDataToClipboard(copyData)
}) })
} }
// 将粘贴或剪切的数据设置到用户剪切板中 // 将粘贴或剪切的数据设置到用户剪切板中
setCoptyDataToClipboard(data) { setCopyDataToClipboard(data) {
if (navigator.clipboard) { if (navigator.clipboard) {
navigator.clipboard.writeText( navigator.clipboard.writeText(
JSON.stringify({ JSON.stringify({
@ -720,13 +812,9 @@ class Render {
} }
if (smmData) { if (smmData) {
this.mindMap.execCommand( this.mindMap.execCommand(
'INSERT_CHILD_NODE', 'INSERT_MULTI_CHILD_NODE',
false,
[], [],
{ Array.isArray(smmData) ? smmData : [smmData]
...smmData.data
},
[...smmData.children]
) )
} else { } else {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], { this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
@ -770,7 +858,7 @@ class Render {
// 将节点移动到另一个节点的前面或后面 // 将节点移动到另一个节点的前面或后面
insertTo(node, exist, dir = 'before') { insertTo(node, exist, dir = 'before') {
let nodeList = this.formatAppointNodes(node) let nodeList = formatDataToArray(node)
nodeList = nodeList.filter(item => { nodeList = nodeList.filter(item => {
return !item.isRoot return !item.isRoot
}) })
@ -823,7 +911,7 @@ class Render {
// 移除节点 // 移除节点
removeNode(appointNodes = []) { removeNode(appointNodes = []) {
appointNodes = this.formatAppointNodes(appointNodes) appointNodes = formatDataToArray(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) { if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return return
} }
@ -895,32 +983,40 @@ class Render {
// 移除某个指定节点 // 移除某个指定节点
removeOneNode(node) { removeOneNode(node) {
let index = this.getNodeIndex(node) let index = getNodeIndex(node)
node.remove() node.remove()
node.parent.children.splice(index, 1) node.parent.children.splice(index, 1)
node.parent.nodeData.children.splice(index, 1) node.parent.nodeData.children.splice(index, 1)
} }
// 复制节点,多个节点只会操作第一个节点 // 复制节点
copyNode() { copyNode() {
if (this.activeNodeList.length <= 0) { if (this.activeNodeList.length <= 0) {
return return
} }
return copyNodeTree({}, this.activeNodeList[0], true) const nodeList = getTopAncestorsFomNodeList(this.activeNodeList)
return nodeList.map(node => {
return copyNodeTree({}, node, true)
})
} }
// 剪切节点,多个节点只会操作第一个节点 // 剪切节点
cutNode(callback) { cutNode(callback) {
if (this.activeNodeList.length <= 0) { if (this.activeNodeList.length <= 0) {
return return
} }
let node = this.activeNodeList[0] const nodeList = getTopAncestorsFomNodeList(this.activeNodeList).filter(
if (node.isRoot) { node => {
return null return !node.isRoot
} }
let copyData = copyNodeTree({}, node, true) )
this.removeActiveNode(node) const copyData = nodeList.map(node => {
this.removeOneNode(node) return copyNodeTree({}, node, true)
})
nodeList.forEach(node => {
this.removeActiveNode(node)
this.removeOneNode(node)
})
this.mindMap.emit('node_active', null, [...this.activeNodeList]) this.mindMap.emit('node_active', null, [...this.activeNodeList])
this.mindMap.render() this.mindMap.render()
if (callback && typeof callback === 'function') { if (callback && typeof callback === 'function') {
@ -928,9 +1024,9 @@ class Render {
} }
} }
// 移动一个节点作为另一个节点的子节点 // 移动节点作为另一个节点的子节点
moveNodeTo(node, toNode) { moveNodeTo(node, toNode) {
let nodeList = this.formatAppointNodes(node) let nodeList = formatDataToArray(node)
nodeList = nodeList.filter(item => { nodeList = nodeList.filter(item => {
return !item.isRoot return !item.isRoot
}) })
@ -949,11 +1045,16 @@ class Render {
// 粘贴节点到节点 // 粘贴节点到节点
pasteNode(data) { pasteNode(data) {
if (this.activeNodeList.length <= 0 || !data) { data = formatDataToArray(data)
if (this.activeNodeList.length <= 0 || data.length <= 0) {
return return
} }
this.activeNodeList.forEach(item => { this.activeNodeList.forEach(node => {
item.nodeData.children.push(simpleDeepClone(data)) node.nodeData.children.push(
...data.map(item => {
return simpleDeepClone(item)
})
)
}) })
this.mindMap.render() this.mindMap.render()
} }

View File

@ -716,3 +716,54 @@ export const selectAllInput = el => {
selection.removeAllRanges() selection.removeAllRanges()
selection.addRange(range) 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> <ShortcutKey></ShortcutKey>
<Contextmenu v-if="mindMap" :mindMap="mindMap"></Contextmenu> <Contextmenu v-if="mindMap" :mindMap="mindMap"></Contextmenu>
<RichTextToolbar v-if="mindMap" :mindMap="mindMap"></RichTextToolbar> <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> <NodeImgPreview v-if="mindMap" :mindMap="mindMap"></NodeImgPreview>
<SidebarTrigger v-if="!isZenMode"></SidebarTrigger> <SidebarTrigger v-if="!isZenMode"></SidebarTrigger>
<Search v-if="mindMap" :mindMap="mindMap"></Search> <Search v-if="mindMap" :mindMap="mindMap"></Search>
@ -264,10 +267,10 @@ export default {
// url使 // url使
if (hasFileURL) { if (hasFileURL) {
root = { root = {
"data": { data: {
"text": "根节点" text: '根节点'
}, },
"children": [] children: []
} }
layout = exampleData.layout layout = exampleData.layout
theme = exampleData.theme theme = exampleData.theme
@ -296,7 +299,7 @@ export default {
useLeftKeySelectionRightKeyDrag: this.useLeftKeySelectionRightKeyDrag, useLeftKeySelectionRightKeyDrag: this.useLeftKeySelectionRightKeyDrag,
customInnerElsAppendTo: null, customInnerElsAppendTo: null,
enableAutoEnterTextEditWhenKeydown: true, enableAutoEnterTextEditWhenKeydown: true,
customHandleClipboardText: handleClipboardText, customHandleClipboardText: handleClipboardText
// isUseCustomNodeContent: true, // isUseCustomNodeContent: true,
// 1routerstorei18nvue西 // 1routerstorei18nvue西
// customCreateNodeContent: (node) => { // customCreateNodeContent: (node) => {
@ -344,46 +347,33 @@ export default {
this.mindMap.keyCommand.addShortcut('Control+s', () => { this.mindMap.keyCommand.addShortcut('Control+s', () => {
this.manualSave() this.manualSave()
}) })
// //
;[ ;[
'node_active', 'node_active',
'data_change', 'data_change',
'view_data_change', 'view_data_change',
'back_forward', 'back_forward',
'node_contextmenu', 'node_contextmenu',
'node_click', 'node_click',
'draw_click', 'draw_click',
'expand_btn_click', 'expand_btn_click',
'svg_mousedown', 'svg_mousedown',
'mouseup', 'mouseup',
'mode_change', 'mode_change',
'node_tree_render_end', 'node_tree_render_end',
'rich_text_selection_change', 'rich_text_selection_change',
'transforming-dom-to-images', 'transforming-dom-to-images',
'generalization_node_contextmenu', 'generalization_node_contextmenu',
'painter_start', 'painter_start',
'painter_end', 'painter_end',
'scrollbar_change' 'scrollbar_change'
].forEach(event => { ].forEach(event => {
this.mindMap.on(event, (...args) => { this.mindMap.on(event, (...args) => {
this.$bus.$emit(event, ...args) this.$bus.$emit(event, ...args)
})
}) })
})
this.bindSaveEvent() this.bindSaveEvent()
// setTimeout(() => { this.testDynamicCreateNodes()
//
// 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);
// //
if (window.takeOverApp) { if (window.takeOverApp) {
this.$bus.$emit('app_inited', this.mindMap) this.$bus.$emit('app_inited', this.mindMap)
@ -477,6 +467,117 @@ export default {
// //
removeRichTextPlugin() { removeRichTextPlugin() {
this.mindMap.removePlugin(RichText) 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)
} }
} }
} }