mirror of
https://github.com/wanglin2/mind-map.git
synced 2026-02-17 22:08:25 +08:00
Doc: update
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"><link rel="icon" href="dist/logo.ico"><title>思绪思维导图</title><script>// 自定义静态资源的路径
|
||||
window.externalPublicPath = './dist/'
|
||||
// 接管应用
|
||||
window.takeOverApp = false</script><link href="dist/css/chunk-vendors.css?f6cb6d932d2f3720fc06" rel="stylesheet"><link href="dist/css/app.css?f6cb6d932d2f3720fc06" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
|
||||
window.takeOverApp = false</script><link href="dist/css/chunk-vendors.css?82cd2198e53e186a377d" rel="stylesheet"><link href="dist/css/app.css?82cd2198e53e186a377d" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
@@ -66,4 +66,4 @@
|
||||
// 可以通过window.$bus.$on()来监听应用的一些事件
|
||||
// 实例化页面
|
||||
window.initApp()
|
||||
}</script><script src="dist/js/chunk-vendors.js?f6cb6d932d2f3720fc06"></script><script src="dist/js/app.js?f6cb6d932d2f3720fc06"></script></body></html>
|
||||
}</script><script src="dist/js/chunk-vendors.js?82cd2198e53e186a377d"></script><script src="dist/js/app.js?82cd2198e53e186a377d"></script></body></html>
|
||||
@@ -541,7 +541,7 @@ export const getVisibleColorFromTheme = (themeConfig) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 将<p><span></span><p>形式的节点富文本内容转换成<br>换行的文本
|
||||
// 将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本
|
||||
let nodeRichTextToTextWithWrapEl = null
|
||||
export const nodeRichTextToTextWithWrap = (html) => {
|
||||
if (!nodeRichTextToTextWithWrapEl) {
|
||||
|
||||
@@ -11,7 +11,7 @@ let langList = [
|
||||
}
|
||||
]
|
||||
let StartList = ['introduction', 'start', 'deploy', 'client', 'translate', 'changelog']
|
||||
let CourseList = new Array(21).fill(0).map((_, index) => {
|
||||
let CourseList = new Array(22).fill(0).map((_, index) => {
|
||||
return 'course' + (index + 1)
|
||||
})
|
||||
let APIList = [
|
||||
|
||||
@@ -236,7 +236,7 @@ Determine whether a color is transparent.
|
||||
|
||||
> v0.6.12+
|
||||
|
||||
Convert the rich text content of nodes in the form of `<p><span></span><p>` into text wrapped in `<br>`.
|
||||
Convert the rich text content of nodes in the form of `<p><span></span><p>` into text wrapped in `\n`.
|
||||
|
||||
#### textToNodeRichTextWithWrap(html)
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ and copying the <code>data</code> of the data object, example:</p>
|
||||
<blockquote>
|
||||
<p>v0.6.12+</p>
|
||||
</blockquote>
|
||||
<p>Convert the rich text content of nodes in the form of <code><p><span></span><p></code> into text wrapped in <code><br></code>.</p>
|
||||
<p>Convert the rich text content of nodes in the form of <code><p><span></span><p></code> into text wrapped in <code>\n</code>.</p>
|
||||
<h4>textToNodeRichTextWithWrap(html)</h4>
|
||||
<blockquote>
|
||||
<p>v0.6.12+</p>
|
||||
|
||||
@@ -28,6 +28,7 @@ export default [
|
||||
{ path: 'course19', title: '插入和扩展节点图标' },
|
||||
{ path: 'course20', title: '如何自定义节点内容' },
|
||||
{ path: 'course21', title: '如何复制、剪切、粘贴' },
|
||||
{ path: 'course22', title: '如何实现搜索、替换' },
|
||||
{ path: 'doExport', title: 'Export 插件' },
|
||||
{ path: 'drag', title: 'Drag插件' },
|
||||
{ path: 'introduction', title: '简介' },
|
||||
|
||||
@@ -34,4 +34,269 @@ 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方法将<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 || 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)。
|
||||
@@ -24,6 +24,231 @@ mindMap.execCommand(<span class="hljs-string">'GO_TARGET_NODE'</span>,
|
||||
<pre class="hljs"><code>mindMap.execCommand(<span class="hljs-string">'INSERT_NODE'</span>, <span class="hljs-literal">false</span>)
|
||||
mindMap.execCommand(<span class="hljs-string">'INSERT_CHILD_NODE'</span>, <span class="hljs-literal">false</span>)
|
||||
</code></pre>
|
||||
<h2>进阶</h2>
|
||||
<p>要实现一个功能完善的大纲并不容易,下面介绍一下包含定位、编辑、拖拽、删除、单独编辑功能的大纲实现。</p>
|
||||
<p>以<a href="https://element.eleme.cn/#/zh-CN/component/tree">ElementUI Tree组件</a>为例。</p>
|
||||
<p>实现监听<code>data_change</code>事件来刷新树数据:</p>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">import</span> { nodeRichTextToTextWithWrap } <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map/src/utils'</span>
|
||||
|
||||
<span class="hljs-built_in">this</span>.mindMap.on(<span class="hljs-string">'data_change'</span>, <span class="hljs-function">() =></span> {
|
||||
<span class="hljs-built_in">this</span>.refresh()
|
||||
})
|
||||
|
||||
{
|
||||
<span class="hljs-function"><span class="hljs-title">refresh</span>(<span class="hljs-params"></span>)</span> {
|
||||
<span class="hljs-keyword">let</span> data = mindMap.getData()<span class="hljs-comment">// 获取思维导图树数据</span>
|
||||
data.root = <span class="hljs-literal">true</span> <span class="hljs-comment">// 标记根节点</span>
|
||||
<span class="hljs-comment">// 遍历树,添加一些属性</span>
|
||||
<span class="hljs-keyword">let</span> walk = <span class="hljs-function"><span class="hljs-params">root</span> =></span> {
|
||||
<span class="hljs-comment">// 如果是富文本节点,那么调用nodeRichTextToTextWithWrap方法将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本</span>
|
||||
<span class="hljs-keyword">const</span> text = (root.data.richText
|
||||
? nodeRichTextToTextWithWrap(root.data.text)
|
||||
: root.data.text
|
||||
).replaceAll(<span class="hljs-regexp">/\n/g</span>, <span class="hljs-string">'<br>'</span>)
|
||||
root.textCache = text <span class="hljs-comment">// 保存一份修改前的数据,用于对比是否修改了</span>
|
||||
root.label = text<span class="hljs-comment">// 用于树组件渲染</span>
|
||||
root.uid = root.data.uid<span class="hljs-comment">// 用于树组件渲染</span>
|
||||
<span class="hljs-keyword">if</span> (root.children && root.children.length > <span class="hljs-number">0</span>) {
|
||||
root.children.forEach(<span class="hljs-function"><span class="hljs-params">item</span> =></span> {
|
||||
walk(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
walk(data)
|
||||
<span class="hljs-built_in">this</span>.data = [data]<span class="hljs-comment">// 赋值给树组件</span>
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p>模板如下:</p>
|
||||
<pre class="hljs"><code><span class="hljs-tag"><<span class="hljs-name">el-tree</span>
|
||||
<span class="hljs-attr">ref</span>=<span class="hljs-string">"tree"</span>
|
||||
<span class="hljs-attr">node-key</span>=<span class="hljs-string">"uid"</span>
|
||||
<span class="hljs-attr">draggable</span>
|
||||
<span class="hljs-attr">default-expand-all</span>
|
||||
<span class="hljs-attr">:data</span>=<span class="hljs-string">"data"</span>
|
||||
<span class="hljs-attr">:highlight-current</span>=<span class="hljs-string">"true"</span>
|
||||
<span class="hljs-attr">:expand-on-click-node</span>=<span class="hljs-string">"false"</span>
|
||||
<span class="hljs-attr">:allow-drag</span>=<span class="hljs-string">"checkAllowDrag"</span>
|
||||
@<span class="hljs-attr">node-drop</span>=<span class="hljs-string">"onNodeDrop"</span>
|
||||
@<span class="hljs-attr">current-change</span>=<span class="hljs-string">"onCurrentChange"</span>
|
||||
@<span class="hljs-attr">mouseenter.native</span>=<span class="hljs-string">"isInTreArea = true"</span>
|
||||
@<span class="hljs-attr">mouseleave.native</span>=<span class="hljs-string">"isInTreArea = false"</span>
|
||||
></span>
|
||||
<span class="hljs-tag"><<span class="hljs-name">span</span>
|
||||
<span class="hljs-attr">class</span>=<span class="hljs-string">"customNode"</span>
|
||||
<span class="hljs-attr">slot-scope</span>=<span class="hljs-string">"{ node, data }"</span>
|
||||
<span class="hljs-attr">:data-id</span>=<span class="hljs-string">"data.uid"</span>
|
||||
@<span class="hljs-attr">click</span>=<span class="hljs-string">"onClick(data)"</span>
|
||||
></span>
|
||||
<span class="hljs-tag"><<span class="hljs-name">span</span>
|
||||
<span class="hljs-attr">class</span>=<span class="hljs-string">"nodeEdit"</span>
|
||||
<span class="hljs-attr">contenteditable</span>=<span class="hljs-string">"true"</span>
|
||||
<span class="hljs-attr">:key</span>=<span class="hljs-string">"getKey()"</span>
|
||||
@<span class="hljs-attr">keydown.stop</span>=<span class="hljs-string">"onNodeInputKeydown($event, node)"</span>
|
||||
@<span class="hljs-attr">keyup.stop</span>
|
||||
@<span class="hljs-attr">blur</span>=<span class="hljs-string">"onBlur($event, node)"</span>
|
||||
@<span class="hljs-attr">paste</span>=<span class="hljs-string">"onPaste($event, node)"</span>
|
||||
<span class="hljs-attr">v-html</span>=<span class="hljs-string">"node.label"</span>
|
||||
></span><span class="hljs-tag"></<span class="hljs-name">span</span>></span>
|
||||
<span class="hljs-tag"></<span class="hljs-name">span</span>></span>
|
||||
<span class="hljs-tag"></<span class="hljs-name">el-tree</span>></span>
|
||||
</code></pre>
|
||||
<h3>定位节点</h3>
|
||||
<p>给节点绑定了一个<code>click</code>事件用于在画布内定位点击的节点,可以调用思维导图的相关方法实现:</p>
|
||||
<pre class="hljs"><code><span class="hljs-comment">// 激活当前节点且移动当前节点到画布中间</span>
|
||||
<span class="hljs-function"><span class="hljs-title">onClick</span>(<span class="hljs-params">data</span>)</span> {
|
||||
<span class="hljs-comment">// 根据uid知道思维导图节点对象</span>
|
||||
<span class="hljs-keyword">const</span> targetNode = <span class="hljs-built_in">this</span>.mindMap.renderer.findNodeByUid(data.uid)
|
||||
<span class="hljs-comment">// 如果当前已经是激活状态,那么上面都不做</span>
|
||||
<span class="hljs-keyword">if</span> (targetNode && targetNode.nodeData.data.isActive) <span class="hljs-keyword">return</span>
|
||||
<span class="hljs-comment">// 思维导图节点激活时默认会聚焦到内部创建的一个隐藏输入框中,`stopFocusOnNodeActive`方法是用于关闭这个特性,因为我们想把焦点留在大纲的输入框中</span>
|
||||
<span class="hljs-built_in">this</span>.mindMap.renderer.textEdit.stopFocusOnNodeActive()
|
||||
<span class="hljs-comment">// 定位到目标节点</span>
|
||||
<span class="hljs-built_in">this</span>.mindMap.execCommand(<span class="hljs-string">'GO_TARGET_NODE'</span>, data.uid, <span class="hljs-function">() =></span> {
|
||||
<span class="hljs-comment">// 定位完成后再开启前面关闭的特性</span>
|
||||
<span class="hljs-built_in">this</span>.mindMap.renderer.textEdit.openFocusOnNodeActive()
|
||||
})
|
||||
}
|
||||
</code></pre>
|
||||
<h3>编辑</h3>
|
||||
<p>我们通过自定义树节点内容渲染了一个<code>contenteditable=true</code>的标签用于输入文本,然后在<code>blur</code>事件中修改节点文本:</p>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">import</span> { textToNodeRichTextWithWrap } <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map/src/utils'</span>
|
||||
|
||||
<span class="hljs-comment">// 失去焦点更新节点文本</span>
|
||||
<span class="hljs-function"><span class="hljs-title">onBlur</span>(<span class="hljs-params">e, node</span>)</span> {
|
||||
<span class="hljs-comment">// 节点数据没有修改那么什么也不用做</span>
|
||||
<span class="hljs-keyword">if</span> (node.data.textCache === e.target.innerHTML) {
|
||||
<span class="hljs-keyword">return</span>
|
||||
}
|
||||
<span class="hljs-comment">// 根据是否是富文本模式获取不同的文本数据</span>
|
||||
<span class="hljs-keyword">const</span> richText = node.data.data.richText
|
||||
<span class="hljs-keyword">const</span> text = richText ? e.target.innerHTML : e.target.innerText
|
||||
<span class="hljs-keyword">const</span> targetNode = <span class="hljs-built_in">this</span>.mindMap.renderer.findNodeByUid(node.data.uid)
|
||||
<span class="hljs-keyword">if</span> (!targetNode) <span class="hljs-keyword">return</span>
|
||||
<span class="hljs-keyword">if</span> (richText) {
|
||||
<span class="hljs-comment">// 如果是富文本节点,那么需要先调用textToNodeRichTextWithWrap方法将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容</span>
|
||||
<span class="hljs-comment">// 第二个参数代表设置的是富文本内容</span>
|
||||
<span class="hljs-comment">// 第三个参数指定要重置富文本节点的样式</span>
|
||||
targetNode.setText(textToNodeRichTextWithWrap(text), <span class="hljs-literal">true</span>, <span class="hljs-literal">true</span>)
|
||||
} <span class="hljs-keyword">else</span> {
|
||||
targetNode.setText(text)
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<h3>拖拽</h3>
|
||||
<p>设置了<code>draggable</code>属性即可开启拖拽,首先根节点是不允许拖拽的,所以通过<code>allow-drag</code>属性传入一个判断方法:</p>
|
||||
<pre class="hljs"><code><span class="hljs-comment">// 根节点不允许拖拽</span>
|
||||
<span class="hljs-function"><span class="hljs-title">checkAllowDrag</span>(<span class="hljs-params">node</span>)</span> {
|
||||
<span class="hljs-keyword">return</span> !node.data.root
|
||||
}
|
||||
</code></pre>
|
||||
<p>然后监听拖拽完成事件<code>node-drop</code>来实现画布内节点的调整:</p>
|
||||
<pre class="hljs"><code><span class="hljs-comment">// 拖拽结束事件</span>
|
||||
<span class="hljs-function"><span class="hljs-title">onNodeDrop</span>(<span class="hljs-params">data, target, position</span>)</span> {
|
||||
<span class="hljs-comment">// 被拖拽的节点</span>
|
||||
<span class="hljs-keyword">const</span> node = <span class="hljs-built_in">this</span>.mindMap.renderer.findNodeByUid(data.data.uid)
|
||||
<span class="hljs-comment">// 拖拽到的目标节点</span>
|
||||
<span class="hljs-keyword">const</span> targetNode = <span class="hljs-built_in">this</span>.mindMap.renderer.findNodeByUid(target.data.uid)
|
||||
<span class="hljs-keyword">if</span> (!node || !targetNode) {
|
||||
<span class="hljs-keyword">return</span>
|
||||
}
|
||||
<span class="hljs-comment">// 根据不同拖拽的情况调用不同的方法</span>
|
||||
<span class="hljs-keyword">switch</span> (position) {
|
||||
<span class="hljs-keyword">case</span> <span class="hljs-string">'before'</span>:
|
||||
<span class="hljs-built_in">this</span>.mindMap.execCommand(<span class="hljs-string">'INSERT_BEFORE'</span>, node, targetNode)
|
||||
<span class="hljs-keyword">break</span>
|
||||
<span class="hljs-keyword">case</span> <span class="hljs-string">'after'</span>:
|
||||
<span class="hljs-built_in">this</span>.mindMap.execCommand(<span class="hljs-string">'INSERT_AFTER'</span>, node, targetNode)
|
||||
<span class="hljs-keyword">break</span>
|
||||
<span class="hljs-keyword">case</span> <span class="hljs-string">'inner'</span>:
|
||||
<span class="hljs-built_in">this</span>.mindMap.execCommand(<span class="hljs-string">'MOVE_NODE_TO'</span>, node, targetNode)
|
||||
<span class="hljs-keyword">break</span>
|
||||
<span class="hljs-attr">default</span>:
|
||||
<span class="hljs-keyword">break</span>
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<h3>删除节点</h3>
|
||||
<p>首先通过树组件的<code>current-change</code>事件来保存当前高亮的树节点:</p>
|
||||
<pre class="hljs"><code><span class="hljs-comment">// 当前选中的树节点变化事件</span>
|
||||
<span class="hljs-function"><span class="hljs-title">onCurrentChange</span>(<span class="hljs-params">data</span>)</span> {
|
||||
<span class="hljs-built_in">this</span>.currentData = data
|
||||
}
|
||||
</code></pre>
|
||||
<p>然后通过监听<code>keydown</code>事件来完成删除节点的操作:</p>
|
||||
<pre class="hljs"><code><span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'keydown'</span>, <span class="hljs-built_in">this</span>.onKeyDown)
|
||||
|
||||
<span class="hljs-comment">// 删除节点</span>
|
||||
<span class="hljs-function"><span class="hljs-title">onKeyDown</span>(<span class="hljs-params">e</span>)</span> {
|
||||
<span class="hljs-keyword">if</span> ([<span class="hljs-number">46</span>, <span class="hljs-number">8</span>].includes(e.keyCode) && <span class="hljs-built_in">this</span>.currentData) {
|
||||
e.stopPropagation()
|
||||
<span class="hljs-comment">// 处理当前正在编辑节点内容时删除的情况</span>
|
||||
<span class="hljs-built_in">this</span>.mindMap.renderer.textEdit.hideEditTextBox()
|
||||
<span class="hljs-keyword">const</span> node = <span class="hljs-built_in">this</span>.mindMap.renderer.findNodeByUid(<span class="hljs-built_in">this</span>.currentData.uid)
|
||||
<span class="hljs-keyword">if</span> (node && !node.isRoot) {
|
||||
<span class="hljs-comment">// 首先从树里删除</span>
|
||||
<span class="hljs-built_in">this</span>.$refs.tree.remove(<span class="hljs-built_in">this</span>.currentData)
|
||||
<span class="hljs-comment">// 然后从画布里删除</span>
|
||||
<span class="hljs-built_in">this</span>.mindMap.execCommand(<span class="hljs-string">'REMOVE_NODE'</span>, [node])
|
||||
}
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<h3>创建新节点</h3>
|
||||
<p>通过监听节点内容编辑框的<code>keydown</code>事件来完成添加新节点的操作:</p>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">import</span> { createUid } <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map/src/utils'</span>
|
||||
|
||||
<span class="hljs-comment">// 节点输入区域按键事件</span>
|
||||
<span class="hljs-function"><span class="hljs-title">onNodeInputKeydown</span>(<span class="hljs-params">e</span>)</span> {
|
||||
<span class="hljs-comment">// 回车键添加同级节点</span>
|
||||
<span class="hljs-keyword">if</span> (e.keyCode === <span class="hljs-number">13</span> && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
<span class="hljs-built_in">this</span>.insertNode()
|
||||
}
|
||||
<span class="hljs-comment">// tab键添加子节点</span>
|
||||
<span class="hljs-keyword">if</span> (e.keyCode === <span class="hljs-number">9</span>) {
|
||||
e.preventDefault()
|
||||
<span class="hljs-built_in">this</span>.insertChildNode()
|
||||
}
|
||||
}
|
||||
|
||||
<span class="hljs-comment">// 插入兄弟节点</span>
|
||||
<span class="hljs-function"><span class="hljs-title">insertNode</span>(<span class="hljs-params"></span>)</span> {
|
||||
<span class="hljs-built_in">this</span>.mindMap.execCommand(<span class="hljs-string">'INSERT_NODE'</span>, <span class="hljs-literal">false</span>, [], {
|
||||
<span class="hljs-attr">uid</span>: createUid()
|
||||
})
|
||||
}
|
||||
|
||||
<span class="hljs-comment">// 插入下级节点</span>
|
||||
<span class="hljs-function"><span class="hljs-title">insertChildNode</span>(<span class="hljs-params"></span>)</span> {
|
||||
<span class="hljs-built_in">this</span>.mindMap.execCommand(<span class="hljs-string">'INSERT_CHILD_NODE'</span>, <span class="hljs-literal">false</span>, [], {
|
||||
<span class="hljs-attr">uid</span>: createUid()
|
||||
})
|
||||
}
|
||||
</code></pre>
|
||||
<h3>拦截输入框的粘贴操作</h3>
|
||||
<p>为什么要拦截输入框的粘贴操作,因为用户可能粘贴的是富文本内容,也就是带html标签的,但是一般我们都不希望用户粘贴这种内容,只允许粘贴纯文本,所以我们要拦截粘贴事件,处理一下用户粘贴的内容:</p>
|
||||
<pre class="hljs"><code><span class="hljs-keyword">import</span> { getTextFromHtml } <span class="hljs-keyword">from</span> <span class="hljs-string">'simple-mind-map/src/utils'</span>
|
||||
|
||||
<span class="hljs-comment">// 拦截粘贴事件</span>
|
||||
<span class="hljs-function"><span class="hljs-title">onPaste</span>(<span class="hljs-params">e</span>)</span> {
|
||||
e.preventDefault()
|
||||
<span class="hljs-keyword">const</span> selection = <span class="hljs-built_in">window</span>.getSelection()
|
||||
<span class="hljs-keyword">if</span> (!selection.rangeCount) <span class="hljs-keyword">return</span>
|
||||
selection.deleteFromDocument()<span class="hljs-comment">// 删除当前选区,也就是如果当前用户在输入框中选择了一些文本,会被删除</span>
|
||||
<span class="hljs-comment">// 从剪贴板里取出文本数据</span>
|
||||
<span class="hljs-keyword">let</span> text = (e.clipboardData || <span class="hljs-built_in">window</span>.clipboardData).getData(<span class="hljs-string">'text'</span>)
|
||||
<span class="hljs-comment">// 调用库提供的getTextFromHtml方法去除格式</span>
|
||||
text = getTextFromHtml(text)
|
||||
<span class="hljs-comment">// 去除换行</span>
|
||||
text = text.replaceAll(<span class="hljs-regexp">/\n/g</span>, <span class="hljs-string">''</span>)
|
||||
<span class="hljs-comment">// 创建文本节点添加到当前选区</span>
|
||||
<span class="hljs-keyword">const</span> node = <span class="hljs-built_in">document</span>.createTextNode(text)
|
||||
selection.getRangeAt(<span class="hljs-number">0</span>).insertNode(node)
|
||||
selection.collapseToEnd()
|
||||
}
|
||||
</code></pre>
|
||||
<p>到这里基本功能就都完成了,是不是觉得挺简单的?核心原理和操作确实很简单,麻烦的是各种情况和冲突的处理,比如焦点的冲突、快捷键的冲突、操作的时间顺序等等,所以务必先阅读一下完整的源码<a href="https://github.com/wanglin2/mind-map/blob/main/web/src/pages/Edit/components/Outline.vue">Outline.vue</a>。</p>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 如何持久化数据
|
||||
|
||||
> 目前提供了一种新方式,可以参考[对接自己的存储服务](https://wanglin2.github.io/mind-map/#/doc/zh/deploy/%E5%AF%B9%E6%8E%A5%E8%87%AA%E5%B7%B1%E7%9A%84%E5%AD%98%E5%82%A8%E6%9C%8D%E5%8A%A1)。
|
||||
|
||||
在线`demo`的数据是存储在电脑本地的,也就是`localStorage`里,当然,你也可以存储到数据库中。
|
||||
|
||||
## 保存数据
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>如何持久化数据</h1>
|
||||
<blockquote>
|
||||
<p>目前提供了一种新方式,可以参考<a href="https://wanglin2.github.io/mind-map/#/doc/zh/deploy/%E5%AF%B9%E6%8E%A5%E8%87%AA%E5%B7%B1%E7%9A%84%E5%AD%98%E5%82%A8%E6%9C%8D%E5%8A%A1">对接自己的存储服务</a>。</p>
|
||||
</blockquote>
|
||||
<p>在线<code>demo</code>的数据是存储在电脑本地的,也就是<code>localStorage</code>里,当然,你也可以存储到数据库中。</p>
|
||||
<h2>保存数据</h2>
|
||||
<p>保存数据,一般有两种做法,一是让用户手动保存,二是当画布上的数据改变后自动保存,显然,第二中体验更好一点。</p>
|
||||
|
||||
39
web/src/pages/Doc/zh/course22/index.md
Normal file
39
web/src/pages/Doc/zh/course22/index.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 如何实现搜索替换
|
||||
|
||||
> 需要先注册 Search 插件
|
||||
|
||||
要实现搜索替换很简单,你只要先创建两个输入框,两个按钮,然后调用相关方法即可。
|
||||
|
||||
第一个输入框用于搜索,可以绑定一个回车事件,然后调用如下方法:
|
||||
|
||||
```js
|
||||
mindMap.search.search(this.searchText, () => {
|
||||
this.$refs.searchInputRef.focus()
|
||||
})
|
||||
```
|
||||
|
||||
`search`方法调用一次就会跳转到下一个匹配的节点,当搜索文本改变后再调用,默认会重新搜索从头开始。
|
||||
|
||||
`search`方法第二个参数是一个回调函数,当本次搜索完成,即在跳转到节点后调用,一般需要在这个回调函数里重新让你的输入框聚焦,因为激活节点会拿走焦点,所以你需要把焦点拿回来。
|
||||
|
||||
第二个输入框用于替换,替换支持单个替换和全部替换,需要注意的是要先在调用了`search`方法后才能调用这两个方法,单个替换只需要调用如下方法:
|
||||
|
||||
```js
|
||||
mindMap.search.replace(this.replaceText, true)
|
||||
```
|
||||
|
||||
第二个参数传`true`会在替换完成后自动跳转到下一个匹配的节点,这样可以进行连续替换。
|
||||
|
||||
要进行全部替换可以调用如下方法:
|
||||
|
||||
```js
|
||||
mindMap.search.replaceAll(this.replaceText)
|
||||
```
|
||||
|
||||
最后你可以通过监听`search_info_change`方法来获取匹配的节点数量和当前定位到的索引:
|
||||
|
||||
```js
|
||||
mindMap.on('search_info_change', data => {
|
||||
console.log('当前所在:'+ (data.currentIndex + 1), '匹配总数:' + data.total)
|
||||
})
|
||||
```
|
||||
39
web/src/pages/Doc/zh/course22/index.vue
Normal file
39
web/src/pages/Doc/zh/course22/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>如何实现搜索替换</h1>
|
||||
<blockquote>
|
||||
<p>需要先注册 Search 插件</p>
|
||||
</blockquote>
|
||||
<p>要实现搜索替换很简单,你只要先创建两个输入框,两个按钮,然后调用相关方法即可。</p>
|
||||
<p>第一个输入框用于搜索,可以绑定一个回车事件,然后调用如下方法:</p>
|
||||
<pre class="hljs"><code>mindMap.search.search(<span class="hljs-built_in">this</span>.searchText, <span class="hljs-function">() =></span> {
|
||||
<span class="hljs-built_in">this</span>.$refs.searchInputRef.focus()
|
||||
})
|
||||
</code></pre>
|
||||
<p><code>search</code>方法调用一次就会跳转到下一个匹配的节点,当搜索文本改变后再调用,默认会重新搜索从头开始。</p>
|
||||
<p><code>search</code>方法第二个参数是一个回调函数,当本次搜索完成,即在跳转到节点后调用,一般需要在这个回调函数里重新让你的输入框聚焦,因为激活节点会拿走焦点,所以你需要把焦点拿回来。</p>
|
||||
<p>第二个输入框用于替换,替换支持单个替换和全部替换,需要注意的是要先在调用了<code>search</code>方法后才能调用这两个方法,单个替换只需要调用如下方法:</p>
|
||||
<pre class="hljs"><code>mindMap.search.replace(<span class="hljs-built_in">this</span>.replaceText, <span class="hljs-literal">true</span>)
|
||||
</code></pre>
|
||||
<p>第二个参数传<code>true</code>会在替换完成后自动跳转到下一个匹配的节点,这样可以进行连续替换。</p>
|
||||
<p>要进行全部替换可以调用如下方法:</p>
|
||||
<pre class="hljs"><code>mindMap.search.replaceAll(<span class="hljs-built_in">this</span>.replaceText)
|
||||
</code></pre>
|
||||
<p>最后你可以通过监听<code>search_info_change</code>方法来获取匹配的节点数量和当前定位到的索引:</p>
|
||||
<pre class="hljs"><code>mindMap.on(<span class="hljs-string">'search_info_change'</span>, <span class="hljs-function"><span class="hljs-params">data</span> =></span> {
|
||||
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'当前所在:'</span>+ (data.currentIndex + <span class="hljs-number">1</span>), <span class="hljs-string">'匹配总数:'</span> + data.total)
|
||||
})
|
||||
</code></pre>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -231,7 +231,7 @@ copyNodeTree({}, node)
|
||||
|
||||
> v0.6.12+
|
||||
|
||||
将`<p><span></span><p>`形式的节点富文本内容转换成`<br>`换行的文本。
|
||||
将`<p><span></span><p>`形式的节点富文本内容转换成`\n`换行的文本。
|
||||
|
||||
#### textToNodeRichTextWithWrap(html)
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
<blockquote>
|
||||
<p>v0.6.12+</p>
|
||||
</blockquote>
|
||||
<p>将<code><p><span></span><p></code>形式的节点富文本内容转换成<code><br></code>换行的文本。</p>
|
||||
<p>将<code><p><span></span><p></code>形式的节点富文本内容转换成<code>\n</code>换行的文本。</p>
|
||||
<h4>textToNodeRichTextWithWrap(html)</h4>
|
||||
<blockquote>
|
||||
<p>v0.6.12+</p>
|
||||
|
||||
Reference in New Issue
Block a user