# 如何渲染一个大纲 思维导图本质就是一颗树,所以你可以使用树组件来完成大纲的显示。 可以监听`data_change`事件来获取当前最新的思维导图数据: ```js mindMap.on('data_change', (data) => { // data数据是不带节点对象的纯数据 // 如果你需要操作节点对象,可以使用mindMap.renderer.renderTree console.log(data, mindMap.renderer.renderTree) }) ``` 通常点击了大纲的某个节点,会将画布定位到该节点并激活该节点,这可以这么做: ```js const node = data._node mindMap.renderer.moveNodeToCenter(node) node.active() // 在v0.6.7+版本可以这么做: mindMap.execCommand('GO_TARGET_NODE', node)// 或者传节点的uid ``` 当在大纲树上编辑了某个节点的内容,需要同步到思维导图树上: ```js data._node.setText('xxx') ``` 要插入兄弟节点或子节点可以这么操作: ```js mindMap.execCommand('INSERT_NODE', false) mindMap.execCommand('INSERT_CHILD_NODE', false) ``` ## 进阶 要实现一个功能完善的大纲并不容易,下面介绍一下包含定位、编辑、拖拽、删除、单独编辑功能的大纲实现。 以[ElementUI Tree组件](https://element.eleme.cn/#/zh-CN/component/tree)为例。 实现监听`data_change`事件来刷新树数据: ```js import { nodeRichTextToTextWithWrap } from 'simple-mind-map/src/utils' this.mindMap.on('data_change', () => { this.refresh() }) { refresh() { let data = mindMap.getData()// 获取思维导图树数据 data.root = true // 标记根节点 // 遍历树,添加一些属性 let walk = root => { // 如果是富文本节点,那么调用nodeRichTextToTextWithWrap方法将
形式的节点富文本内容转换成\n换行的文本
const text = (root.data.richText
? nodeRichTextToTextWithWrap(root.data.text)
: root.data.text
).replaceAll(/\n/g, '
')
root.textCache = text // 保存一份修改前的数据,用于对比是否修改了
root.label = text// 用于树组件渲染
root.uid = root.data.uid// 用于树组件渲染
if (root.children && root.children.length > 0) {
root.children.forEach(item => {
walk(item)
})
}
}
walk(data)
this.data = [data]// 赋值给树组件
}
}
```
模板如下:
```html
换行的文本转换成
形式的节点富文本内容 // 第二个参数代表设置的是富文本内容 // 第三个参数指定要重置富文本节点的样式 targetNode.setText(textToNodeRichTextWithWrap(text), true, true) } else { targetNode.setText(text) } } ``` ### 拖拽 设置了`draggable`属性即可开启拖拽,首先根节点是不允许拖拽的,所以通过`allow-drag`属性传入一个判断方法: ```js // 根节点不允许拖拽 checkAllowDrag(node) { return !node.data.root } ``` 然后监听拖拽完成事件`node-drop`来实现画布内节点的调整: ```js // 拖拽结束事件 onNodeDrop(data, target, position) { // 被拖拽的节点 const node = this.mindMap.renderer.findNodeByUid(data.data.uid) // 拖拽到的目标节点 const targetNode = this.mindMap.renderer.findNodeByUid(target.data.uid) if (!node || !targetNode) { return } // 根据不同拖拽的情况调用不同的方法 switch (position) { case 'before': this.mindMap.execCommand('INSERT_BEFORE', node, targetNode) break case 'after': this.mindMap.execCommand('INSERT_AFTER', node, targetNode) break case 'inner': this.mindMap.execCommand('MOVE_NODE_TO', node, targetNode) break default: break } } ``` ### 删除节点 首先通过树组件的`current-change`事件来保存当前高亮的树节点: ```js // 当前选中的树节点变化事件 onCurrentChange(data) { this.currentData = data } ``` 然后通过监听`keydown`事件来完成删除节点的操作: ```js window.addEventListener('keydown', this.onKeyDown) // 删除节点 onKeyDown(e) { if ([46, 8].includes(e.keyCode) && this.currentData) { e.stopPropagation() // 处理当前正在编辑节点内容时删除的情况 this.mindMap.renderer.textEdit.hideEditTextBox() const node = this.mindMap.renderer.findNodeByUid(this.currentData.uid) if (node && !node.isRoot) { // 首先从树里删除 this.$refs.tree.remove(this.currentData) // 然后从画布里删除 this.mindMap.execCommand('REMOVE_NODE', [node]) } } } ``` ### 创建新节点 通过监听节点内容编辑框的`keydown`事件来完成添加新节点的操作: ```js import { createUid } from 'simple-mind-map/src/utils' // 节点输入区域按键事件 onNodeInputKeydown(e) { // 回车键添加同级节点 if (e.keyCode === 13 && !e.shiftKey) { e.preventDefault() this.insertNode() } // tab键添加子节点 if (e.keyCode === 9) { e.preventDefault() this.insertChildNode() } } // 插入兄弟节点 insertNode() { this.mindMap.execCommand('INSERT_NODE', false, [], { uid: createUid() }) } // 插入下级节点 insertChildNode() { this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], { uid: createUid() }) } ``` ### 拦截输入框的粘贴操作 为什么要拦截输入框的粘贴操作,因为用户可能粘贴的是富文本内容,也就是带html标签的,但是一般我们都不希望用户粘贴这种内容,只允许粘贴纯文本,所以我们要拦截粘贴事件,处理一下用户粘贴的内容: ```js import { getTextFromHtml } from 'simple-mind-map/src/utils' // 拦截粘贴事件 onPaste(e) { e.preventDefault() const selection = window.getSelection() if (!selection.rangeCount) return selection.deleteFromDocument()// 删除当前选区,也就是如果当前用户在输入框中选择了一些文本,会被删除 // 从剪贴板里取出文本数据 let text = (e.clipboardData || window.clipboardData).getData('text') // 调用库提供的getTextFromHtml方法去除格式 text = getTextFromHtml(text) // 去除换行 text = text.replaceAll(/\n/g, '') // 创建文本节点添加到当前选区 const node = document.createTextNode(text) selection.getRangeAt(0).insertNode(node) selection.collapseToEnd() } ``` 到这里基本功能就都完成了,是不是觉得挺简单的?核心原理和操作确实很简单,麻烦的是各种情况和冲突的处理,比如焦点的冲突、快捷键的冲突、操作的时间顺序等等,所以务必先阅读一下完整的源码[Outline.vue](https://github.com/wanglin2/mind-map/blob/main/web/src/pages/Edit/components/Outline.vue)。