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