如何渲染一个大纲
思维导图本质就是一颗树,所以你可以使用树组件来完成大纲的显示。
可以监听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组件为例。
实现监听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方法将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本
const text = (root.data.richText
? nodeRichTextToTextWithWrap(root.data.text)
: root.data.text
).replaceAll(/\n/g, '<br>')
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
<el-tree
ref="tree"
node-key="uid"
draggable
default-expand-all
:data="data"
:highlight-current="true"
:expand-on-click-node="false"
:allow-drag="checkAllowDrag"
@node-drop="onNodeDrop"
@current-change="onCurrentChange"
@mouseenter.native="isInTreArea = true"
@mouseleave.native="isInTreArea = false"
>
<span
class="customNode"
slot-scope="{ node, data }"
:data-id="data.uid"
@click="onClick(data)"
>
<span
class="nodeEdit"
contenteditable="true"
:key="getKey()"
@keydown.stop="onNodeInputKeydown($event, node)"
@keyup.stop
@blur="onBlur($event, node)"
@paste="onPaste($event, node)"
v-html="node.label"
></span>
</span>
</el-tree>
定位节点
给节点绑定了一个click
事件用于在画布内定位点击的节点,可以调用思维导图的相关方法实现:
js
// 激活当前节点且移动当前节点到画布中间
onClick(data) {
// 根据uid知道思维导图节点对象
const targetNode = this.mindMap.renderer.findNodeByUid(data.uid)
// 如果当前已经是激活状态,那么上面都不做
if (targetNode && targetNode.nodeData.data.isActive) return
// 思维导图节点激活时默认会聚焦到内部创建的一个隐藏输入框中,`stopFocusOnNodeActive`方法是用于关闭这个特性,因为我们想把焦点留在大纲的输入框中
this.mindMap.renderer.textEdit.stopFocusOnNodeActive()
// 定位到目标节点
this.mindMap.execCommand('GO_TARGET_NODE', data.uid, () => {
// 定位完成后再开启前面关闭的特性
this.mindMap.renderer.textEdit.openFocusOnNodeActive()
})
}
编辑
我们通过自定义树节点内容渲染了一个contenteditable=true
的标签用于输入文本,然后在blur
事件中修改节点文本:
js
import { textToNodeRichTextWithWrap } from 'simple-mind-map/src/utils'
// 失去焦点更新节点文本
onBlur(e, node) {
// 节点数据没有修改那么什么也不用做
if (node.data.textCache === e.target.innerHTML) {
return
}
// 根据是否是富文本模式获取不同的文本数据
const richText = node.data.data.richText
const text = richText ? e.target.innerHTML : e.target.innerText
const targetNode = this.mindMap.renderer.findNodeByUid(node.data.uid)
if (!targetNode) return
if (richText) {
// 如果是富文本节点,那么需要先调用textToNodeRichTextWithWrap方法将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容
// 第二个参数代表设置的是富文本内容
// 第三个参数指定要重置富文本节点的样式
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.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。