Compare commits

...

33 Commits

Author SHA1 Message Date
wanglin2
f6609b3050 更新群二维码 2023-08-11 09:11:30 +08:00
wanglin2
4d69778a50 打包0.6.13 2023-08-11 09:10:34 +08:00
wanglin2
c8dfbd3b87 Doc: update 2023-08-11 09:08:44 +08:00
wanglin2
c0aab1e921 Demo:分类整理图片文件 2023-08-10 16:56:04 +08:00
wanglin2
6efee6e859 打包 2023-08-10 10:09:08 +08:00
wanglin2
dd82bf0879 优化代码 2023-08-10 10:06:02 +08:00
街角小林
2d41b5f9e6 Merge pull request #250 from kalcaddle/main
修复部分小问题
2023-08-10 09:23:02 +08:00
街角小林
7e7b6fae9d Merge branch 'main' into main 2023-08-10 09:22:56 +08:00
街角小林
29020daaf0 Merge pull request #248 from Xbs233/main
fix:手势缩小时 会先放大一次 再缩小
2023-08-10 08:55:25 +08:00
wanglin2
6bcead7784 Demo:修复在大纲中插入新节点报错的问题 2023-08-10 08:50:26 +08:00
wanglin2
9625129691 Demo:完善右键菜单和富文本工具条的暗黑模式 2023-08-10 08:47:16 +08:00
warlee
765d0ee212 还原. 2023-08-09 18:58:52 +08:00
warlee
099d4cd78e 画布背景图为none时处理为空. 2023-08-09 18:56:14 +08:00
wanglin2
d9a6981df4 Fix:修复主题配置中背景图片为none时会发起一个异常请求的问题 2023-08-09 18:43:43 +08:00
wanglin2
bbf424c6d2 Feat:1.非富文本输入框进入编辑状态时取消默认全选;2.存在一个激活节点时,支持按下中文、数字、英文按键时自动进入文本编辑模式 2023-08-09 18:26:50 +08:00
warlee
5cde3b76fe Merge branch 'main' of https://github.com/kalcaddle/mind-map
* 'main' of https://github.com/kalcaddle/mind-map:
  打包
  Demo:组件卸载时解除绑定的事件
  打包
  Demo:一些耗时的操作添加loading
  Feat:去掉异步渲染节点的逻辑
2023-08-09 18:14:46 +08:00
warlee
75af742053 Merge branch 'wanglin2:main' into main 2023-08-09 18:04:00 +08:00
wanglin2
69e192ea9d Fix:修复在移动端激活节点、展开收起时等操作时会拉起输入法的问题 2023-08-09 17:22:23 +08:00
wanglin2
5a32c1d99d Fix:修复在移动端激活节点、展开收起时等操作时会拉起输入法的问题 2023-08-09 17:16:31 +08:00
wanglin2
e81e0a5512 Fix:修复快速拖动节点几次后会概率性报错的问题 2023-08-09 17:12:55 +08:00
wanglin2
2fef76c55c Feat:导出图片由html2canvas库改为dom-to-image-more库 2023-08-09 17:01:35 +08:00
wanglin2
49735950f2 打包 2023-08-09 09:37:20 +08:00
wanglin2
c005455144 Demo:组件卸载时解除绑定的事件 2023-08-09 09:36:08 +08:00
wanglin2
ca4afb5440 打包 2023-08-09 09:13:57 +08:00
wanglin2
885647cedf Demo:一些耗时的操作添加loading 2023-08-09 09:09:43 +08:00
wanglin2
dc8efbe3ef Feat:去掉异步渲染节点的逻辑 2023-08-09 09:08:45 +08:00
Xbs233
888b8e725a fix:手势缩小时 会先放大一次 再缩小
修复初次onTouchmove时 必然是放大的情况
2023-08-08 21:23:12 +08:00
warlee
15d65db19d Merge branch 'main' of https://github.com/kalcaddle/mind-map
* 'main' of https://github.com/kalcaddle/mind-map:
  '打包'
  Demo:保存视图数据的逻辑增加防抖操作,优化性能
2023-08-08 17:22:00 +08:00
warlee
2ce0d4cd11 no message 2023-08-08 17:21:50 +08:00
warlee
13f3c2c20c - 移动端手势缩放优化: 按线性关系进行缩放,双指位移可以调整画布位置;
- 拖动画布优化: 只读模式拖拽画布时点击在子元素上时无法拖动问题处理(只读模式拖拽,鼠标中键拖拽,移动端拖拽)
2023-08-08 17:21:05 +08:00
wanglin2
d707329526 '打包' 2023-08-08 16:31:15 +08:00
wanglin2
69faa8bb3e Demo:保存视图数据的逻辑增加防抖操作,优化性能 2023-08-08 16:29:29 +08:00
wanglin2
86b184d5c1 Doc: update 2023-08-08 09:51:05 +08:00
129 changed files with 1766 additions and 540 deletions

View File

@@ -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?5afd1a8b2213b3355dce" rel="stylesheet"><link href="dist/css/app.css?5afd1a8b2213b3355dce" 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?5afd1a8b2213b3355dce"></script><script src="dist/js/app.js?5afd1a8b2213b3355dce"></script></body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,17 +1,17 @@
{
"name": "simple-mind-map",
"version": "0.6.11-fix.1",
"version": "0.6.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "0.6.11-fix.1",
"version": "0.6.12",
"license": "MIT",
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",
"deepmerge": "^1.5.2",
"dom-to-image-more": "^3.1.6",
"eventemitter3": "^4.0.7",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0",
@@ -255,6 +255,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
@@ -411,6 +412,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
@@ -516,6 +518,11 @@
"node": ">=6.0.0"
}
},
"node_modules/dom-to-image-more": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.1.6.tgz",
"integrity": "sha512-VMO0jNme32T06mWtkOC9QXfj+1npoJxkaTFW0DCwBLguwBKMjqwndiDANxDnbZ0kvNEecwxkv0Zmgdr96cGtAA=="
},
"node_modules/dompurify": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
@@ -937,6 +944,7 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
@@ -2142,6 +2150,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
@@ -2206,6 +2215,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
@@ -2461,7 +2471,8 @@
"base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
@@ -2576,6 +2587,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"requires": {
"utrie": "^1.0.2"
}
@@ -2648,6 +2660,11 @@
"esutils": "^2.0.2"
}
},
"dom-to-image-more": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.1.6.tgz",
"integrity": "sha512-VMO0jNme32T06mWtkOC9QXfj+1npoJxkaTFW0DCwBLguwBKMjqwndiDANxDnbZ0kvNEecwxkv0Zmgdr96cGtAA=="
},
"dompurify": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
@@ -2966,6 +2983,7 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"requires": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
@@ -3749,6 +3767,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"requires": {
"utrie": "^1.0.2"
}
@@ -3800,6 +3819,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"requires": {
"base64-arraybuffer": "^1.0.2"
}

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.6.12",
"version": "0.6.13",
"description": "一个简单的web在线思维导图",
"authors": [
{
@@ -26,8 +26,8 @@
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",
"deepmerge": "^1.5.2",
"dom-to-image-more": "^3.1.6",
"eventemitter3": "^4.0.7",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0",

View File

@@ -126,5 +126,13 @@ export const defaultOpt = {
// 指定内部一些元素节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素添加到的位置默认添加到document.body下
customInnerElsAppendTo: null,
// 拖拽元素时,指示元素新位置的块的最大高度
nodeDragPlaceholderMaxSize: 20
nodeDragPlaceholderMaxSize: 20,
// 是否允许创建一个隐藏的输入框,该输入框会在节点激活时聚焦,用于粘贴数据和自动进入文本编辑状态
enableCreateHiddenInput: true,
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
// 该配置在enableCreateHiddenInput设为true时生效
enableAutoEnterTextEditWhenKeydown: true,
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
richTextEditFakeInPlace: false
}

View File

@@ -108,6 +108,11 @@ export default class KeyCommand {
return arr
}
// 判断是否按下了组合键
hasCombinationKey(e) {
return e.ctrlKey || e.metaKey || e.altKey || e.shiftKey
}
// 获取快捷键对应的键值数组
getKeyCodeArr(key) {
let keyArr = key.split(/\s*\+\s*/)

View File

@@ -48,7 +48,7 @@ class Render {
this.themeConfig = this.mindMap.themeConfig
this.draw = this.mindMap.draw
// 渲染树,操作过程中修改的都是这里的数据
this.renderTree = merge({},this.mindMap.opt.data || {})
this.renderTree = merge({}, this.mindMap.opt.data || {})
// 是否重新渲染
this.reRender = false
// 是否正在渲染中
@@ -112,6 +112,13 @@ class Render {
this.mindMap.on('paste', data => {
this.onPaste(data)
})
// let timer = null
// this.mindMap.on('view_data_change', () => {
// clearTimeout(timer)
// timer = setTimeout(() => {
// this.render()
// }, 300)
// })
}
// 注册命令
@@ -888,7 +895,7 @@ class Render {
// 更新了连线的样式
let props = Object.keys(style)
let hasLineStyleProps = false
props.forEach((key) => {
props.forEach(key => {
if (lineStyleProps.includes(key)) {
hasLineStyleProps = true
}

View File

@@ -1,4 +1,4 @@
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils'
import { getStrWithBrFromHtml, checkNodeOuter, isMobile } from '../../utils'
// 节点文字编辑类
export default class TextEdit {
@@ -67,7 +67,9 @@ export default class TextEdit {
// 创建一个隐藏的文本输入框
createHiddenInput() {
if (this.hiddenInputEl) return
const { enableCreateHiddenInput, enableAutoEnterTextEditWhenKeydown } =
this.mindMap.opt
if (this.hiddenInputEl || isMobile() || !enableCreateHiddenInput) return
this.hiddenInputEl = document.createElement('input')
this.hiddenInputEl.type = 'text'
this.hiddenInputEl.style.cssText = `
@@ -75,6 +77,25 @@ export default class TextEdit {
left: -99999px;
top: -99999px;
`
// 监听按键事件
if (enableAutoEnterTextEditWhenKeydown) {
this.hiddenInputEl.addEventListener('keydown', e => {
const activeNodeList = this.mindMap.renderer.activeNodeList
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
const node = activeNodeList[0]
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
const keyCode = e.keyCode
if (
node &&
(keyCode === 229 ||
(keyCode >= 65 && keyCode <= 90) ||
(keyCode >= 48 && keyCode <= 57)) &&
!this.mindMap.keyCommand.hasCombinationKey(e)
) {
this.show(node)
}
})
}
// 监听粘贴事件
this.hiddenInputEl.addEventListener('paste', async event => {
event.preventDefault()
@@ -106,7 +127,7 @@ export default class TextEdit {
stopFocusOnNodeActive() {
this.enableFocus = false
}
// 开启默认聚焦
openFocusOnNodeActive() {
this.enableFocus = true
@@ -148,7 +169,7 @@ export default class TextEdit {
this.mindMap.richText.showEditText(node, rect, isInserting)
return
}
this.showEditTextBox(node, rect)
this.showEditTextBox(node, rect, isInserting)
}
// 处理画布缩放
@@ -166,7 +187,7 @@ export default class TextEdit {
}
// 显示文本编辑框
showEditTextBox(node, rect) {
showEditTextBox(node, rect, isInserting) {
this.mindMap.emit('before_show_text_edit')
this.registerTmpShortcut()
if (!this.textEditNode) {
@@ -179,10 +200,11 @@ export default class TextEdit {
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('mousedown', (e) => {
this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
let scale = this.mindMap.view.scale
@@ -209,12 +231,27 @@ export default class TextEdit {
}
this.showTextEdit = true
// 选中文本
if (!this.cacheEditingText) {
// if (!this.cacheEditingText) {
// this.selectNodeText()
// }
if (isInserting) {
this.selectNodeText()
} else {
this.focus()
}
this.cacheEditingText = ''
}
// 聚焦
focus() {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.textEditNode)
range.collapse()
selection.removeAllRanges()
selection.addRange(range)
}
// 选中文本
selectNodeText() {
let selection = window.getSelection()

View File

@@ -1,6 +1,5 @@
import Style from './Style'
import Shape from './Shape'
import { asyncRun, nodeToHTML } from '../../../utils'
import { G, Rect, ForeignObject, SVG } from '@svgdotjs/svg.js'
import nodeGeneralizationMethods from './nodeGeneralization'
import nodeExpandBtnMethods from './nodeExpandBtn'
@@ -98,6 +97,8 @@ class Node {
this.isMultipleChoice = false
// 是否需要重新layout
this.needLayout = false
// 当前是否是隐藏状态
this.isHide = false
// 概要相关方法
Object.keys(nodeGeneralizationMethods).forEach(item => {
this[item] = nodeGeneralizationMethods[item].bind(this)
@@ -368,7 +369,13 @@ class Node {
})
}
this.group.add(this._unVisibleRectRegionNode)
this.renderer.layout.renderExpandBtnRect(this._unVisibleRectRegionNode, this.expandBtnSize, width, height, this)
this.renderer.layout.renderExpandBtnRect(
this._unVisibleRectRegionNode,
this.expandBtnSize,
width,
height,
this
)
}
}
@@ -388,7 +395,7 @@ class Node {
if (this.isRoot && e.which === 3 && !this.mindMap.opt.readonly) {
e.stopPropagation()
}
if (!this.isRoot && !this.mindMap.opt.readonly) {
if (!this.isRoot && e.which !== 2 && !this.mindMap.opt.readonly) {
e.stopPropagation()
}
// 多选和取消多选
@@ -414,7 +421,7 @@ class Node {
this.mindMap.emit('node_mousedown', this, e)
})
this.group.on('mouseup', e => {
if (!this.isRoot && !this.mindMap.opt.readonly) {
if (!this.isRoot && e.which !== 2 && !this.mindMap.opt.readonly) {
e.stopPropagation()
}
this.mindMap.emit('node_mouseup', this, e)
@@ -476,9 +483,7 @@ class Node {
if (!this.group) {
return
}
let {
alwaysShowExpandBtn
} = this.mindMap.opt
let { alwaysShowExpandBtn } = this.mindMap.opt
if (alwaysShowExpandBtn) {
// 需要移除展开收缩按钮
if (this._expandBtn && this.nodeData.children.length <= 0) {
@@ -500,11 +505,48 @@ class Node {
this.renderGeneralization()
// 更新节点位置
let t = this.group.transform()
// // 如果上次不在可视区内,且本次也不在,那么直接返回
// let { left: ox, top: oy } = this.getNodePosInClient(
// t.translateX,
// t.translateY
// )
// let oldIsInClient =
// ox > 0 && oy > 0 && ox < this.mindMap.width && oy < this.mindMap.height
// let { left: nx, top: ny } = this.getNodePosInClient(this.left, this.top)
// let newIsNotInClient =
// nx + this.width < 0 ||
// ny + this.height < 0 ||
// nx > this.mindMap.width ||
// ny > this.mindMap.height
// if (!oldIsInClient && newIsNotInClient) {
// if (!this.isHide) {
// this.isHide = true
// this.group.hide()
// }
// return
// }
// // 如果当前是隐藏状态,那么先显示
// if (this.isHide) {
// this.isHide = false
// this.group.show()
// }
// 如果节点位置没有变化,则返回
if (this.left === t.translateX && this.top === t.translateY) return
this.group.translate(this.left - t.translateX, this.top - t.translateY)
}
// 获取节点相当于画布的位置
getNodePosInClient(_left, _top) {
let drawTransform = this.mindMap.draw.transform()
let { scaleX, scaleY, translateX, translateY } = drawTransform
let left = _left * scaleX + translateX
let top = _top * scaleY + translateY
return {
left,
top
}
}
// 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置
reRender() {
let sizeChange = this.getSize()
@@ -559,18 +601,14 @@ class Node {
this.nodeData.data.expand !== false
) {
let index = 0
asyncRun(
this.children.map(item => {
return () => {
item.render(() => {
index++
if (index >= this.children.length) {
callback()
}
})
this.children.forEach(item => {
item.render(() => {
index++
if (index >= this.children.length) {
callback()
}
})
)
})
} else {
callback()
}
@@ -592,13 +630,9 @@ class Node {
this.removeLine()
// 子节点
if (this.children && this.children.length) {
asyncRun(
this.children.map(item => {
return () => {
item.remove()
}
})
)
this.children.forEach(item => {
item.remove()
})
}
}
@@ -624,13 +658,9 @@ class Node {
}
// 子节点
if (this.children && this.children.length) {
asyncRun(
this.children.map(item => {
return () => {
item.hide()
}
})
)
this.children.forEach(item => {
item.hide()
})
}
}
@@ -650,13 +680,9 @@ class Node {
}
// 子节点
if (this.children && this.children.length) {
asyncRun(
this.children.map(item => {
return () => {
item.show()
}
})
)
this.children.forEach(item => {
item.show()
})
}
}

View File

@@ -17,7 +17,7 @@ class Style {
// 设置新样式
let { backgroundColor, backgroundImage, backgroundRepeat, backgroundPosition, backgroundSize } = themeConfig
el.style.backgroundColor = backgroundColor
if (backgroundImage) {
if (backgroundImage && backgroundImage !== 'none') {
el.style.backgroundImage = `url(${backgroundImage})`
el.style.backgroundRepeat = backgroundRepeat
el.style.backgroundPosition = backgroundPosition

View File

@@ -82,15 +82,20 @@ class View {
// 鼠标滚轮,向上和向左,都是缩小
case CONSTANTS.DIR.UP:
case CONSTANTS.DIR.LEFT:
mousewheelZoomActionReverse ? this.enlarge(cx, cy, isTouchPad) : this.narrow(cx, cy, isTouchPad)
mousewheelZoomActionReverse
? this.enlarge(cx, cy, isTouchPad)
: this.narrow(cx, cy, isTouchPad)
break
// 鼠标滚轮,向下和向右,都是放大
case CONSTANTS.DIR.DOWN:
case CONSTANTS.DIR.RIGHT:
mousewheelZoomActionReverse ? this.narrow(cx, cy, isTouchPad) : this.enlarge(cx, cy, isTouchPad)
mousewheelZoomActionReverse
? this.narrow(cx, cy, isTouchPad)
: this.enlarge(cx, cy, isTouchPad)
break
}
} else {// 鼠标滚轮事件控制画布移动
} else {
// 鼠标滚轮事件控制画布移动
let step = mousewheelMoveStep
if (isTouchPad) {
step = 5
@@ -147,6 +152,7 @@ class View {
// 平移x,y方向
translateXY(x, y) {
if (x === 0 && y === 0) return
this.x += x
this.y += y
this.transform()
@@ -154,24 +160,28 @@ class View {
// 平移x方向
translateX(step) {
if (step === 0) return
this.x += step
this.transform()
}
// 平移x方式到
translateXTo(x) {
if (x === 0) return
this.x = x
this.transform()
}
// 平移y方向
translateY(step) {
if (step === 0) return
this.y += step
this.transform()
}
// 平移y方向到
translateYTo(y) {
if (y === 0) return
this.y = y
this.transform()
}

View File

@@ -204,7 +204,7 @@ class Drag extends Base {
// 检测重叠节点
checkOverlapNode() {
if (!this.drawTransform) {
if (!this.drawTransform || !this.placeholder) {
return
}
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt

View File

@@ -1,7 +1,12 @@
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
import html2canvas from 'html2canvas'
import { walk, getTextFromHtml, isWhite, getVisibleColorFromTheme } from '../utils'
import domtoimage from 'dom-to-image-more'
import {
walk,
getTextFromHtml,
isWhite,
getVisibleColorFromTheme
} from '../utils'
import { CONSTANTS } from '../constants/constant'
let extended = false
@@ -150,6 +155,12 @@ class RichText {
if (this.showTextEdit) {
return
}
const {
richTextEditFakeInPlace,
customInnerElsAppendTo,
nodeTextEditZIndex,
textAutoWrapWidth
} = this.mindMap.opt
this.node = node
this.isInserting = isInserting
if (!rect) rect = node._textData.node.node.getBoundingClientRect()
@@ -163,19 +174,31 @@ class RichText {
let scaleX = rect.width / originWidth
let scaleY = rect.height / originHeight
// 内边距
const paddingX = 6
const paddingY = 4
let paddingX = 6
let paddingY = 4
if (richTextEditFakeInPlace) {
let paddingValue = node.getPaddingVale()
paddingX = paddingValue.paddingX
paddingY = paddingValue.paddingY
}
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.classList.add('smm-richtext-node-edit-wrap')
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;box-shadow: 0 0 20px rgba(0,0,0,.5);outline: none; word-break: break-all;padding: ${paddingY}px ${paddingX}px;`
this.textEditNode.style.cssText = `
position:fixed;
box-sizing: border-box;
box-shadow: 0 0 20px rgba(0,0,0,.5);
outline: none;
word-break:
break-all;padding: ${paddingY}px ${paddingX}px;
`
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('mousedown', (e) => {
this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
const targetNode = customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
// 使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
@@ -183,18 +206,28 @@ class RichText {
let color = node.style.merge('color')
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.style.zIndex = nodeTextEditZIndex
this.textEditNode.style.backgroundColor =
bgColor === 'transparent' ? isWhite(color) ? getVisibleColorFromTheme(this.mindMap.themeConfig) : '#fff' : bgColor
bgColor === 'transparent'
? isWhite(color)
? getVisibleColorFromTheme(this.mindMap.themeConfig)
: '#fff'
: bgColor
this.textEditNode.style.minWidth = originWidth + paddingX * 2 + 'px'
this.textEditNode.style.minHeight = originHeight + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
this.textEditNode.style.display = 'block'
this.textEditNode.style.maxWidth =
this.mindMap.opt.textAutoWrapWidth + paddingX * 2 + 'px'
this.textEditNode.style.maxWidth = textAutoWrapWidth + paddingX * 2 + 'px'
this.textEditNode.style.transform = `scale(${scaleX}, ${scaleY})`
this.textEditNode.style.transformOrigin = 'left top'
if (richTextEditFakeInPlace) {
this.textEditNode.style.borderRadius =
(node.style.merge('borderRadius') || 5) + 'px'
if (node.style.merge('shape') == 'roundedRectangle') {
this.textEditNode.style.borderRadius = (node.height || 50) + 'px'
}
}
if (!node.nodeData.data.richText) {
// 还不是富文本的情况
let text = node.nodeData.data.text.split(/\n/gim).join('<br>')
@@ -502,11 +535,16 @@ class RichText {
}
}
walk(node)
let canvas = await html2canvas(el, {
backgroundColor: null
})
// 如果使用html2canvas
// let canvas = await html2canvas(el, {
// backgroundColor: null
// })
// return canvas.toDataURL()
const res = await domtoimage.toPng(el)
this.mindMap.el.removeChild(el)
return canvas.toDataURL()
return res
}
// 将所有节点转换成非富文本节点

View File

@@ -6,7 +6,7 @@ class TouchEvent {
this.touchesNum = 0
this.singleTouchstartEvent = null
this.clickNum = 0
this.doubleTouchmoveDistance = 0
this.touchStartScaleView = null
this.bindEvent()
}
@@ -33,6 +33,7 @@ class TouchEvent {
// 手指按下事件
onTouchstart(e) {
this.touchesNum = e.touches.length
this.touchStartScaleView = null
if (this.touchesNum === 1) {
let touch = e.touches[0]
this.singleTouchstartEvent = touch
@@ -53,18 +54,46 @@ class TouchEvent {
let oy = touch1.clientY - touch2.clientY
let distance = Math.sqrt(Math.pow(ox, 2) + Math.pow(oy, 2))
// 以两指中心点进行缩放
let { x: touch1ClientX, y: touch1ClientY } = this.mindMap.toPos(touch1.clientX, touch1.clientY)
let { x: touch2ClientX, y: touch2ClientY } = this.mindMap.toPos(touch2.clientX, touch2.clientY)
let { x: touch1ClientX, y: touch1ClientY } = this.mindMap.toPos(
touch1.clientX,
touch1.clientY
)
let { x: touch2ClientX, y: touch2ClientY } = this.mindMap.toPos(
touch2.clientX,
touch2.clientY
)
let cx = (touch1ClientX + touch2ClientX) / 2
let cy = (touch1ClientY + touch2ClientY) / 2
if (distance > this.doubleTouchmoveDistance) {
// 放大
this.mindMap.view.enlarge(cx, cy, true)
} else {
// 缩小
this.mindMap.view.narrow(cx, cy, true)
// 手势缩放,基于最开始的位置进行缩放(基于前一个位置缩放不是线性关系); 缩放同时支持位置拖动
const view = this.mindMap.view
if (!this.touchStartScaleView) {
this.touchStartScaleView = {
distance: distance,
scale: view.scale,
x: view.x,
y: view.y,
cx: cx,
cy: cy
}
return
}
this.doubleTouchmoveDistance = distance
const viewBefore = this.touchStartScaleView
const scale = viewBefore.scale * (distance / viewBefore.distance)
if (Math.abs(distance - viewBefore.distance) <= 10) {
scale = viewBefore.scale
}
const ratio = 1 - scale / viewBefore.scale
view.scale = scale < 0.1 ? 0.1 : scale
view.x =
viewBefore.x +
(cx - viewBefore.x) * ratio +
(cx - viewBefore.cx) * scale
view.y =
viewBefore.y +
(cy - viewBefore.y) * ratio +
(cy - viewBefore.cy) * scale
view.transform()
this.mindMap.emit('scale', scale)
}
}
@@ -91,7 +120,7 @@ class TouchEvent {
}
this.touchesNum = 0
this.singleTouchstartEvent = null
this.doubleTouchmoveDistance = 0
this.touchStartScaleView = null
}
// 发送鼠标事件

View File

@@ -455,25 +455,25 @@ export const loadImage = imgFile => {
}
// 移除字符串中的html实体
export const removeHTMLEntities = (str) => {
[['&nbsp;', '&#160;']].forEach((item) => {
export const removeHTMLEntities = str => {
;[['&nbsp;', '&#160;']].forEach(item => {
str = str.replaceAll(item[0], item[1])
})
return str
}
// 获取一个数据的类型
export const getType = (data) => {
export const getType = data => {
return Object.prototype.toString.call(data).slice(7, -1)
}
// 判断一个数据是否是null和undefined和空字符串
export const isUndef = (data) => {
export const isUndef = data => {
return data === null || data === undefined || data === ''
}
// 移除html字符串中节点的内联样式
export const removeHtmlStyle = (html) => {
export const removeHtmlStyle = html => {
return html.replaceAll(/(<[^\s]+)\s+style=["'][^'"]+["']\s*(>)/g, '$1$2')
}
@@ -485,12 +485,12 @@ export const addHtmlStyle = (html, tag, style) => {
// 检查一个字符串是否是富文本字符
let checkIsRichTextEl = null
export const checkIsRichText = (str) => {
export const checkIsRichText = str => {
if (!checkIsRichTextEl) {
checkIsRichTextEl = document.createElement('div')
}
checkIsRichTextEl.innerHTML = str
for (let c = checkIsRichTextEl.childNodes, i = c.length; i--;) {
for (let c = checkIsRichTextEl.childNodes, i = c.length; i--; ) {
if (c[i].nodeType == 1) return true
}
return false
@@ -503,13 +503,20 @@ export const replaceHtmlText = (html, searchText, replaceText) => {
replaceHtmlTextEl = document.createElement('div')
}
replaceHtmlTextEl.innerHTML = html
let walk = (root) => {
let walk = root => {
let childNodes = root.childNodes
childNodes.forEach((node) => {
if (node.nodeType === 1) {// 元素节点
childNodes.forEach(node => {
if (node.nodeType === 1) {
// 元素节点
walk(node)
} else if (node.nodeType === 3) {// 文本节点
root.replaceChild(document.createTextNode(node.nodeValue.replaceAll(searchText, replaceText)), node)
} else if (node.nodeType === 3) {
// 文本节点
root.replaceChild(
document.createTextNode(
node.nodeValue.replaceAll(searchText, replaceText)
),
node
)
}
})
}
@@ -518,22 +525,39 @@ export const replaceHtmlText = (html, searchText, replaceText) => {
}
// 判断一个颜色是否是白色
export const isWhite = (color) => {
export const isWhite = color => {
color = String(color).replaceAll(/\s+/g, '')
return ['#fff', '#ffffff', '#FFF', '#FFFFFF', 'rgb(255,255,255)'].includes(color) || /rgba\(255,255,255,[^)]+\)/.test(color)
return (
['#fff', '#ffffff', '#FFF', '#FFFFFF', 'rgb(255,255,255)'].includes(
color
) || /rgba\(255,255,255,[^)]+\)/.test(color)
)
}
// 判断一个颜色是否是透明
export const isTransparent = (color) => {
export const isTransparent = color => {
color = String(color).replaceAll(/\s+/g, '')
return ['', 'transparent'].includes(color) || /rgba\(\d+,\d+,\d+,0\)/.test(color)
return (
['', 'transparent'].includes(color) || /rgba\(\d+,\d+,\d+,0\)/.test(color)
)
}
// 从当前主题里获取一个非透明非白色的颜色
export const getVisibleColorFromTheme = (themeConfig) => {
export const getVisibleColorFromTheme = themeConfig => {
let { lineColor, root, second, node } = themeConfig
let list = [lineColor, root.fillColor, root.color, second.fillColor, second.color, node.fillColor, node.color, root.borderColor, second.borderColor, node.borderColor]
for(let i = 0; i < list.length; i++) {
let list = [
lineColor,
root.fillColor,
root.color,
second.fillColor,
second.color,
node.fillColor,
node.color,
root.borderColor,
second.borderColor,
node.borderColor
]
for (let i = 0; i < list.length; i++) {
let color = list[i]
if (!isTransparent(color) && !isWhite(color)) {
return color
@@ -541,24 +565,26 @@ export const getVisibleColorFromTheme = (themeConfig) => {
}
}
// 将<p><span></span><p>形式的节点富文本内容转换成<br>换行的文本
// 将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本
let nodeRichTextToTextWithWrapEl = null
export const nodeRichTextToTextWithWrap = (html) => {
export const nodeRichTextToTextWithWrap = html => {
if (!nodeRichTextToTextWithWrapEl) {
nodeRichTextToTextWithWrapEl = document.createElement('div')
}
nodeRichTextToTextWithWrapEl.innerHTML = html
const childNodes = nodeRichTextToTextWithWrapEl.childNodes
let res = ''
for(let i = 0; i < childNodes.length; i++) {
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i]
if (node.nodeType === 1) {// 元素节点
if (node.nodeType === 1) {
// 元素节点
if (node.tagName.toLowerCase() === 'p') {
res += node.textContent + '\n'
} else {
res += node.textContent
}
} else if (node.nodeType === 3) {// 文本节点
} else if (node.nodeType === 3) {
// 文本节点
res += node.nodeValue
}
}
@@ -567,7 +593,7 @@ export const nodeRichTextToTextWithWrap = (html) => {
// 将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容
let textToNodeRichTextWithWrapEl = null
export const textToNodeRichTextWithWrap = (html) => {
export const textToNodeRichTextWithWrap = html => {
if (!textToNodeRichTextWithWrapEl) {
textToNodeRichTextWithWrapEl = document.createElement('div')
}
@@ -575,23 +601,34 @@ export const textToNodeRichTextWithWrap = (html) => {
const childNodes = textToNodeRichTextWithWrapEl.childNodes
let list = []
let str = ''
for(let i = 0; i < childNodes.length; i++) {
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i]
if (node.nodeType === 1) {// 元素节点
if (node.nodeType === 1) {
// 元素节点
if (node.tagName.toLowerCase() === 'br') {
list.push(str)
str = ''
} else {
str += node.textContent
}
} else if (node.nodeType === 3) {// 文本节点
} else if (node.nodeType === 3) {
// 文本节点
str += node.nodeValue
}
}
if (str) {
list.push(str)
}
return list.map((item) => {
return `<p><span>${item}</span></p>`
}).join('')
}
return list
.map(item => {
return `<p><span>${item}</span></p>`
})
.join('')
}
// 判断是否是移动端环境
export const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,56 +1,56 @@
// 布局结构图片映射
export const layoutImgMap = {
logicalStructure: require('../assets/img/logicalStructure.png'),
mindMap: require('../assets/img/mindMap.png'),
organizationStructure: require('../assets/img/organizationStructure.png'),
catalogOrganization: require('../assets/img/catalogOrganization.png'),
timeline: require('../assets/img/timeline.png'),
timeline2: require('../assets/img/timeline2.png'),
fishbone: require('../assets/img/fishbone.png'),
verticalTimeline: require('../assets/img/verticalTimeline.png'),
logicalStructure: require('../assets/img/structures/logicalStructure.png'),
mindMap: require('../assets/img/structures/mindMap.png'),
organizationStructure: require('../assets/img/structures/organizationStructure.png'),
catalogOrganization: require('../assets/img/structures/catalogOrganization.png'),
timeline: require('../assets/img/structures/timeline.png'),
timeline2: require('../assets/img/structures/timeline2.png'),
fishbone: require('../assets/img/structures/fishbone.png'),
verticalTimeline: require('../assets/img/structures/verticalTimeline.png'),
}
// 主题图片映射
export const themeMap = {
default: require('../assets/img/default.jpg'),
classic: require('../assets/img/classic.jpg'),
minions: require('../assets/img/minions.jpg'),
pinkGrape: require('../assets/img/pinkGrape.jpg'),
mint: require('../assets/img/mint.jpg'),
gold: require('../assets/img/gold.jpg'),
vitalityOrange: require('../assets/img/vitalityOrange.jpg'),
greenLeaf: require('../assets/img/greenLeaf.jpg'),
dark2: require('../assets/img/dark2.jpg'),
skyGreen: require('../assets/img/skyGreen.jpg'),
classic2: require('../assets/img/classic2.jpg'),
classic3: require('../assets/img/classic3.jpg'),
classic4: require('../assets/img/classic4.jpg'),
classicGreen: require('../assets/img/classicGreen.jpg'),
classicBlue: require('../assets/img/classicBlue.jpg'),
blueSky: require('../assets/img/blueSky.jpg'),
brainImpairedPink: require('../assets/img/brainImpairedPink.jpg'),
dark: require('../assets/img/dark.jpg'),
earthYellow: require('../assets/img/earthYellow.jpg'),
freshGreen: require('../assets/img/freshGreen.jpg'),
freshRed: require('../assets/img/freshRed.jpg'),
romanticPurple: require('../assets/img/romanticPurple.jpg'),
simpleBlack: require('../assets/img/simpleBlack.jpg'),
courseGreen: require('../assets/img/courseGreen.jpg'),
coffee: require('../assets/img/coffee.jpg'),
redSpirit: require('../assets/img/redSpirit.jpg'),
blackHumour: require('../assets/img/blackHumour.jpg'),
lateNightOffice: require('../assets/img/lateNightOffice.jpg'),
blackGold: require('../assets/img/blackGold.jpg'),
autumn: require('../assets/img/autumn.jpg'),
avocado: require('../assets/img/avocado.jpg'),
orangeJuice: require('../assets/img/orangeJuice.jpg'),
oreo: require('../assets/img/oreo.jpg'),
shallowSea: require('../assets/img/shallowSea.jpg'),
lemonBubbles: require('../assets/img/lemonBubbles.jpg'),
rose: require('../assets/img/rose.jpg'),
seaBlueLine: require('../assets/img/seaBlueLine.jpg'),
neonLamp: require('../assets/img/neonLamp.jpg'),
darkNightLceBlade: require('../assets/img/darkNightLceBlade.jpg'),
morandi: require('../assets/img/morandi.jpg'),
default: require('../assets/img/themes/default.jpg'),
classic: require('../assets/img/themes/classic.jpg'),
minions: require('../assets/img/themes/minions.jpg'),
pinkGrape: require('../assets/img/themes/pinkGrape.jpg'),
mint: require('../assets/img/themes/mint.jpg'),
gold: require('../assets/img/themes/gold.jpg'),
vitalityOrange: require('../assets/img/themes/vitalityOrange.jpg'),
greenLeaf: require('../assets/img/themes/greenLeaf.jpg'),
dark2: require('../assets/img/themes/dark2.jpg'),
skyGreen: require('../assets/img/themes/skyGreen.jpg'),
classic2: require('../assets/img/themes/classic2.jpg'),
classic3: require('../assets/img/themes/classic3.jpg'),
classic4: require('../assets/img/themes/classic4.jpg'),
classicGreen: require('../assets/img/themes/classicGreen.jpg'),
classicBlue: require('../assets/img/themes/classicBlue.jpg'),
blueSky: require('../assets/img/themes/blueSky.jpg'),
brainImpairedPink: require('../assets/img/themes/brainImpairedPink.jpg'),
dark: require('../assets/img/themes/dark.jpg'),
earthYellow: require('../assets/img/themes/earthYellow.jpg'),
freshGreen: require('../assets/img/themes/freshGreen.jpg'),
freshRed: require('../assets/img/themes/freshRed.jpg'),
romanticPurple: require('../assets/img/themes/romanticPurple.jpg'),
simpleBlack: require('../assets/img/themes/simpleBlack.jpg'),
courseGreen: require('../assets/img/themes/courseGreen.jpg'),
coffee: require('../assets/img/themes/coffee.jpg'),
redSpirit: require('../assets/img/themes/redSpirit.jpg'),
blackHumour: require('../assets/img/themes/blackHumour.jpg'),
lateNightOffice: require('../assets/img/themes/lateNightOffice.jpg'),
blackGold: require('../assets/img/themes/blackGold.jpg'),
autumn: require('../assets/img/themes/autumn.jpg'),
avocado: require('../assets/img/themes/avocado.jpg'),
orangeJuice: require('../assets/img/themes/orangeJuice.jpg'),
oreo: require('../assets/img/themes/oreo.jpg'),
shallowSea: require('../assets/img/themes/shallowSea.jpg'),
lemonBubbles: require('../assets/img/themes/lemonBubbles.jpg'),
rose: require('../assets/img/themes/rose.jpg'),
seaBlueLine: require('../assets/img/themes/seaBlueLine.jpg'),
neonLamp: require('../assets/img/themes/neonLamp.jpg'),
darkNightLceBlade: require('../assets/img/themes/darkNightLceBlade.jpg'),
morandi: require('../assets/img/themes/morandi.jpg'),
}

View File

@@ -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 = [

View File

@@ -1,5 +1,37 @@
# Changelog
## 0.6.13
Fix:
> 1.Fix the issue of the inability to drag the canvas while holding down the middle mouse button on a node in read-only mode.
>
> 2.Fixed the issue of probabilistic error reporting after quickly dragging nodes several times.
>
> 3.Fix the issue of pulling up the input method during operations such as activating nodes on the mobile end, expanding and collapsing.
>
> 4.Fix the issue where an exception request is initiated when the background image in the theme configuration is none.
New:
> 1.Mobile gesture scaling optimization: Scale according to a linear relationship, and adjust the canvas position with double finger displacement.
>
> 2.Remove the logic of asynchronous rendering nodes and improve the speed of creating new nodes.
>
> 3.The export of images has been changed from the html2canvas library to the dom to image more library to address the issue of missing text styles in exporting rich text nodes.
>
> 4.When a non rich text input box enters the editing state, it is deselected by default.
>
> 5.When there is an activation node, it supports automatically entering text editing mode when pressing the Chinese, numeric, or English buttons.
Demo
> 1.Add anti shake operations when saving view data to optimize performance.
>
> 2.Some time-consuming operations add loading effects.
>
> 3.Improve the dark mode of right-click menus and rich text toolbars.
## 0.6.12
Fix:

View File

@@ -1,6 +1,28 @@
<template>
<div>
<h1>Changelog</h1>
<h2>0.6.13</h2>
<p>Fix:</p>
<blockquote>
<p>1.Fix the issue of the inability to drag the canvas while holding down the middle mouse button on a node in read-only mode.</p>
<p>2.Fixed the issue of probabilistic error reporting after quickly dragging nodes several times.</p>
<p>3.Fix the issue of pulling up the input method during operations such as activating nodes on the mobile end, expanding and collapsing.</p>
<p>4.Fix the issue where an exception request is initiated when the background image in the theme configuration is none.</p>
</blockquote>
<p>New:</p>
<blockquote>
<p>1.Mobile gesture scaling optimization: Scale according to a linear relationship, and adjust the canvas position with double finger displacement.</p>
<p>2.Remove the logic of asynchronous rendering nodes and improve the speed of creating new nodes.</p>
<p>3.The export of images has been changed from the html2canvas library to the dom to image more library to address the issue of missing text styles in exporting rich text nodes.</p>
<p>4.When a non rich text input box enters the editing state, it is deselected by default.</p>
<p>5.When there is an activation node, it supports automatically entering text editing mode when pressing the Chinese, numeric, or English buttons.</p>
</blockquote>
<p>Demo</p>
<blockquote>
<p>1.Add anti shake operations when saving view data to optimize performance.</p>
<p>2.Some time-consuming operations add loading effects.</p>
<p>3.Improve the dark mode of right-click menus and rich text toolbars.</p>
</blockquote>
<h2>0.6.12</h2>
<p>Fix:</p>
<blockquote>

View File

@@ -73,6 +73,9 @@ const mindMap = new MindMap({
| mouseScaleCenterUseMousePositionv0.6.4-fix.1+ | Boolean | true | Is the mouse zoom centered around the current position of the mouse, otherwise centered around the canvas | |
| customInnerElsAppendTov0.6.12+ | null/HTMLElement | null | Specify the location where some internal elements (node text editing element, node note display element, associated line text editing element, node image adjustment button element) are added, and default to document.body | |
| nodeDragPlaceholderMaxSizev0.6.12+ | Number | 20 | When dragging an element, the maximum height of the block indicating the new position of the element | |
| enableCreateHiddenInputv0.6.13+ | Boolean | true | Is it allowed to create a hidden input box that will be focused when the node is activated for pasting data and automatically entering the text editing state | |
| enableAutoEnterTextEditWhenKeydownv0.6.13+ | Boolean | true | Does it automatically enter text editing mode when pressing the Chinese, English, or numeric buttons when there is an activation node? This configuration takes effect when enableCreateHiddenInput is set to true | |
| richTextEditFakeInPlacev0.6.13+ | Boolean | false | Set the rich text node edit box to match the size of the node, creating a pseudo in place editing effect. It should be noted that only when there is only text within the node and the shape is rectangular, can the effect be better | |
### Watermark config

View File

@@ -371,6 +371,27 @@
<td>When dragging an element, the maximum height of the block indicating the new position of the element</td>
<td></td>
</tr>
<tr>
<td>enableCreateHiddenInputv0.6.13+</td>
<td>Boolean</td>
<td>true</td>
<td>Is it allowed to create a hidden input box that will be focused when the node is activated for pasting data and automatically entering the text editing state</td>
<td></td>
</tr>
<tr>
<td>enableAutoEnterTextEditWhenKeydownv0.6.13+</td>
<td>Boolean</td>
<td>true</td>
<td>Does it automatically enter text editing mode when pressing the Chinese, English, or numeric buttons when there is an activation node? This configuration takes effect when enableCreateHiddenInput is set to true</td>
<td></td>
</tr>
<tr>
<td>richTextEditFakeInPlacev0.6.13+</td>
<td>Boolean</td>
<td>false</td>
<td>Set the rich text node edit box to match the size of the node, creating a pseudo in place editing effect. It should be noted that only when there is only text within the node and the shape is rectangular, can the effect be better</td>
<td></td>
</tr>
</tbody>
</table>
<h3>Watermark config</h3>

View File

@@ -58,4 +58,12 @@ Save the current registered shortcut data, then clear the shortcut data
> v0.2.3+
Restore saved shortcut data, then clear the cache data
Restore saved shortcut data, then clear the cache data
### hasCombinationKey(e)
> v0.6.13+
- `e`: Event object.
Determine if the combination key has been pressed.

View File

@@ -46,6 +46,14 @@ the shortcut will be removed</p>
<p>v0.2.3+</p>
</blockquote>
<p>Restore saved shortcut data, then clear the cache data</p>
<h3>hasCombinationKey(e)</h3>
<blockquote>
<p>v0.6.13+</p>
</blockquote>
<ul>
<li><code>e</code>: Event object.</li>
</ul>
<p>Determine if the combination key has been pressed.</p>
</div>
</template>

View File

@@ -14,7 +14,9 @@ The principle of this plugin is to use [Quill](https://github.com/quilljs/quill)
>
> This also caused a problem, that is, the function of exporting as a picture was affected, The original principle of exporting `svg` as an image is very simple, Get the `svg` string, and then create the `blob` data of the `type=image/svg+xml` type. Then use the `URL.createObjectURL` method to generate the `data:url` data. Then create a `Image` tag, use the `data:url` as the `src` of the image, and finally draw the image on the `canvas` object for export, However, after testing, when the `DOM` node is embedded in the `svg`, this method of export will cause errors, and after trying many ways, the perfect export effect cannot be achieved, The current method is to traverse the `foreignObject` node in `svg`, using [html2canvas](https://github.com/niklasvh/html2canvas) Convert the `DOM` node in the `foreignObject` node into an image and then replace the `foreignObject` node. This method can work, but it is very time-consuming. Because the `html2canvas` conversion takes a long time, it takes about 2 seconds to convert a node. This leads to the more nodes, the slower the conversion time. Therefore, it is recommended not to use this plugin if you cannot tolerate the long time of export.
The version of `v0.5.7+` directly uses `html2canvas` to convert the entire `svg`, which is no longer an issue with speed. However, there is currently a bug where the color of the node does not take effect after export.
> The version of `v0.5.7+` directly uses `html2canvas` to convert the entire `svg`, which is no longer an issue with speed. However, there is currently a bug where the color of the node does not take effect after export.
`V0.6.13+` version uses [dom-to-image-more](https://github.com/1904labs/dom-to-image-more) Replaced 'html2canvas' to address the issue of ineffective color export for nodes.
## Register

View File

@@ -14,7 +14,10 @@
<p>The following prompts exist in versions prior to v0.5.6:</p>
<p>This also caused a problem, that is, the function of exporting as a picture was affected, The original principle of exporting <code>svg</code> as an image is very simple, Get the <code>svg</code> string, and then create the <code>blob</code> data of the <code>type=image/svg+xml</code> type. Then use the <code>URL.createObjectURL</code> method to generate the <code>data:url</code> data. Then create a <code>Image</code> tag, use the <code>data:url</code> as the <code>src</code> of the image, and finally draw the image on the <code>canvas</code> object for export, However, after testing, when the <code>DOM</code> node is embedded in the <code>svg</code>, this method of export will cause errors, and after trying many ways, the perfect export effect cannot be achieved, The current method is to traverse the <code>foreignObject</code> node in <code>svg</code>, using <a href="https://github.com/niklasvh/html2canvas">html2canvas</a> Convert the <code>DOM</code> node in the <code>foreignObject</code> node into an image and then replace the <code>foreignObject</code> node. This method can work, but it is very time-consuming. Because the <code>html2canvas</code> conversion takes a long time, it takes about 2 seconds to convert a node. This leads to the more nodes, the slower the conversion time. Therefore, it is recommended not to use this plugin if you cannot tolerate the long time of export.</p>
</blockquote>
<blockquote>
<p>The version of <code>v0.5.7+</code> directly uses <code>html2canvas</code> to convert the entire <code>svg</code>, which is no longer an issue with speed. However, there is currently a bug where the color of the node does not take effect after export.</p>
</blockquote>
<p><code>V0.6.13+</code> version uses <a href="https://github.com/1904labs/dom-to-image-more">dom-to-image-more</a> Replaced 'html2canvas' to address the issue of ineffective color export for nodes.</p>
<h2>Register</h2>
<pre class="hljs"><code><span class="hljs-keyword">import</span> MindMap <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map&#x27;</span>
<span class="hljs-keyword">import</span> RichText <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/plugins/RichText.js&#x27;</span>

View File

@@ -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)
@@ -244,6 +244,12 @@ Convert the rich text content of nodes in the form of `<p><span></span><p>` into
Convert the wrapped text of `<br>` into node rich text content in the form of `<p><span></span><p>`.
#### isMobile()
> v0.6.13+
Determine if it is a mobile environment.
## Simulate CSS background in Canvas
Import:

View File

@@ -168,12 +168,17 @@ 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>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;p&gt;</code> into text wrapped in <code>&lt;br&gt;</code>.</p>
<p>Convert the rich text content of nodes in the form of <code>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;p&gt;</code> into text wrapped in <code>\n</code>.</p>
<h4>textToNodeRichTextWithWrap(html)</h4>
<blockquote>
<p>v0.6.12+</p>
</blockquote>
<p>Convert the wrapped text of <code>&lt;br&gt;</code> into node rich text content in the form of <code>&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;p&gt;</code>.</p>
<h4>isMobile()</h4>
<blockquote>
<p>v0.6.13+</p>
</blockquote>
<p>Determine if it is a mobile environment.</p>
<h2>Simulate CSS background in Canvas</h2>
<p>Import:</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> drawBackgroundImageToCanvas <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/utils/simulateCSSBackgroundInCanvas&#x27;</span>

View File

@@ -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: '简介' },

View File

@@ -1,5 +1,37 @@
# Changelog
## 0.6.13
修复:
> 1.修复只读模式下鼠标中键按住节点无法拖动画布的问题。
>
> 2.修复快速拖动节点几次后会概率性报错的问题。
>
> 3.修复在移动端激活节点、展开收起时等操作时会拉起输入法的问题。
>
> 4.修复主题配置中背景图片为none时会发起一个异常请求的问题。
新增:
> 1.移动端手势缩放优化: 按线性关系进行缩放、双指位移可以调整画布位置。
>
> 2.去掉异步渲染节点的逻辑,提升创建新节点的速度。
>
> 3.导出图片由html2canvas库改为使用dom-to-image-more库解决导出富文本节点文字样式丢失的问题。
>
> 4.非富文本输入框进入编辑状态时取消默认全选。
>
> 5.存在一个激活节点时,支持按下中文、数字、英文按键时自动进入文本编辑模式。
Demo
> 1.保存视图数据时增加防抖操作,优化性能。
>
> 2.一些耗时的操作添加loading效果。
>
> 3.完善右键菜单和富文本工具条的暗黑模式。
## 0.6.12
修复:

View File

@@ -1,6 +1,28 @@
<template>
<div>
<h1>Changelog</h1>
<h2>0.6.13</h2>
<p>修复</p>
<blockquote>
<p>1.修复只读模式下鼠标中键按住节点无法拖动画布的问题</p>
<p>2.修复快速拖动节点几次后会概率性报错的问题</p>
<p>3.修复在移动端激活节点展开收起时等操作时会拉起输入法的问题</p>
<p>4.修复主题配置中背景图片为none时会发起一个异常请求的问题</p>
</blockquote>
<p>新增</p>
<blockquote>
<p>1.移动端手势缩放优化: 按线性关系进行缩放双指位移可以调整画布位置</p>
<p>2.去掉异步渲染节点的逻辑提升创建新节点的速度</p>
<p>3.导出图片由html2canvas库改为使用dom-to-image-more库解决导出富文本节点文字样式丢失的问题</p>
<p>4.非富文本输入框进入编辑状态时取消默认全选</p>
<p>5.存在一个激活节点时支持按下中文数字英文按键时自动进入文本编辑模式</p>
</blockquote>
<p>Demo</p>
<blockquote>
<p>1.保存视图数据时增加防抖操作优化性能</p>
<p>2.一些耗时的操作添加loading效果</p>
<p>3.完善右键菜单和富文本工具条的暗黑模式</p>
</blockquote>
<h2>0.6.12</h2>
<p>修复</p>
<blockquote>

View File

@@ -73,6 +73,9 @@ const mindMap = new MindMap({
| mouseScaleCenterUseMousePositionv0.6.4-fix.1+ | Boolean | true | 鼠标缩放是否以鼠标当前位置为中心点,否则以画布中心点 | |
| customInnerElsAppendTov0.6.12+ | null/HTMLElement | null | 指定内部一些元素节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素添加到的位置默认添加到document.body下 | |
| nodeDragPlaceholderMaxSizev0.6.12+ | Number | 20 | 拖拽元素时,指示元素新位置的块的最大高度 | |
| enableCreateHiddenInputv0.6.13+ | Boolean | true | 是否允许创建一个隐藏的输入框,该输入框会在节点激活时聚焦,用于粘贴数据和自动进入文本编辑状态 | |
| enableAutoEnterTextEditWhenKeydownv0.6.13+ | Boolean | true | 是否在存在一个激活节点时当按下中文、英文、数字按键时自动进入文本编辑模式该配置在enableCreateHiddenInput设为true时生效 | |
| richTextEditFakeInPlacev0.6.13+ | Boolean | false | 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果,需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果 | |
### 水印配置

View File

@@ -371,6 +371,27 @@
<td>拖拽元素时指示元素新位置的块的最大高度</td>
<td></td>
</tr>
<tr>
<td>enableCreateHiddenInputv0.6.13+</td>
<td>Boolean</td>
<td>true</td>
<td>是否允许创建一个隐藏的输入框该输入框会在节点激活时聚焦用于粘贴数据和自动进入文本编辑状态</td>
<td></td>
</tr>
<tr>
<td>enableAutoEnterTextEditWhenKeydownv0.6.13+</td>
<td>Boolean</td>
<td>true</td>
<td>是否在存在一个激活节点时当按下中文英文数字按键时自动进入文本编辑模式该配置在enableCreateHiddenInput设为true时生效</td>
<td></td>
</tr>
<tr>
<td>richTextEditFakeInPlacev0.6.13+</td>
<td>Boolean</td>
<td>false</td>
<td>设置富文本节点编辑框和节点大小一致形成伪原地编辑的效果需要注意的是只有当节点内只有文本且形状是矩形才会有比较好的效果</td>
<td></td>
</tr>
</tbody>
</table>
<h3>水印配置</h3>

View File

@@ -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)。

View File

@@ -24,6 +24,231 @@ mindMap.execCommand(<span class="hljs-string">&#x27;GO_TARGET_NODE&#x27;</span>,
<pre class="hljs"><code>mindMap.execCommand(<span class="hljs-string">&#x27;INSERT_NODE&#x27;</span>, <span class="hljs-literal">false</span>)
mindMap.execCommand(<span class="hljs-string">&#x27;INSERT_CHILD_NODE&#x27;</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">&#x27;simple-mind-map/src/utils&#x27;</span>
<span class="hljs-built_in">this</span>.mindMap.on(<span class="hljs-string">&#x27;data_change&#x27;</span>, <span class="hljs-function">() =&gt;</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> =&gt;</span> {
<span class="hljs-comment">// 如果是富文本节点那么调用nodeRichTextToTextWithWrap方法将&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;p&gt;形式的节点富文本内容转换成\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">&#x27;&lt;br&gt;&#x27;</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 &amp;&amp; root.children.length &gt; <span class="hljs-number">0</span>) {
root.children.forEach(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</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">&lt;<span class="hljs-name">el-tree</span>
<span class="hljs-attr">ref</span>=<span class="hljs-string">&quot;tree&quot;</span>
<span class="hljs-attr">node-key</span>=<span class="hljs-string">&quot;uid&quot;</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">&quot;data&quot;</span>
<span class="hljs-attr">:highlight-current</span>=<span class="hljs-string">&quot;true&quot;</span>
<span class="hljs-attr">:expand-on-click-node</span>=<span class="hljs-string">&quot;false&quot;</span>
<span class="hljs-attr">:allow-drag</span>=<span class="hljs-string">&quot;checkAllowDrag&quot;</span>
@<span class="hljs-attr">node-drop</span>=<span class="hljs-string">&quot;onNodeDrop&quot;</span>
@<span class="hljs-attr">current-change</span>=<span class="hljs-string">&quot;onCurrentChange&quot;</span>
@<span class="hljs-attr">mouseenter.native</span>=<span class="hljs-string">&quot;isInTreArea = true&quot;</span>
@<span class="hljs-attr">mouseleave.native</span>=<span class="hljs-string">&quot;isInTreArea = false&quot;</span>
&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">span</span>
<span class="hljs-attr">class</span>=<span class="hljs-string">&quot;customNode&quot;</span>
<span class="hljs-attr">slot-scope</span>=<span class="hljs-string">&quot;{ node, data }&quot;</span>
<span class="hljs-attr">:data-id</span>=<span class="hljs-string">&quot;data.uid&quot;</span>
@<span class="hljs-attr">click</span>=<span class="hljs-string">&quot;onClick(data)&quot;</span>
&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">span</span>
<span class="hljs-attr">class</span>=<span class="hljs-string">&quot;nodeEdit&quot;</span>
<span class="hljs-attr">contenteditable</span>=<span class="hljs-string">&quot;true&quot;</span>
<span class="hljs-attr">:key</span>=<span class="hljs-string">&quot;getKey()&quot;</span>
@<span class="hljs-attr">keydown.stop</span>=<span class="hljs-string">&quot;onNodeInputKeydown($event, node)&quot;</span>
@<span class="hljs-attr">keyup.stop</span>
@<span class="hljs-attr">blur</span>=<span class="hljs-string">&quot;onBlur($event, node)&quot;</span>
@<span class="hljs-attr">paste</span>=<span class="hljs-string">&quot;onPaste($event, node)&quot;</span>
<span class="hljs-attr">v-html</span>=<span class="hljs-string">&quot;node.label&quot;</span>
&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">el-tree</span>&gt;</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 &amp;&amp; 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">&#x27;GO_TARGET_NODE&#x27;</span>, data.uid, <span class="hljs-function">() =&gt;</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">&#x27;simple-mind-map/src/utils&#x27;</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方法将&lt;br&gt;换行的文本转换成&lt;p&gt;&lt;span&gt;&lt;/span&gt;&lt;p&gt;形式的节点富文本内容</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">&#x27;before&#x27;</span>:
<span class="hljs-built_in">this</span>.mindMap.execCommand(<span class="hljs-string">&#x27;INSERT_BEFORE&#x27;</span>, node, targetNode)
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-string">&#x27;after&#x27;</span>:
<span class="hljs-built_in">this</span>.mindMap.execCommand(<span class="hljs-string">&#x27;INSERT_AFTER&#x27;</span>, node, targetNode)
<span class="hljs-keyword">break</span>
<span class="hljs-keyword">case</span> <span class="hljs-string">&#x27;inner&#x27;</span>:
<span class="hljs-built_in">this</span>.mindMap.execCommand(<span class="hljs-string">&#x27;MOVE_NODE_TO&#x27;</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">&#x27;keydown&#x27;</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) &amp;&amp; <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 &amp;&amp; !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">&#x27;REMOVE_NODE&#x27;</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">&#x27;simple-mind-map/src/utils&#x27;</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> &amp;&amp; !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">&#x27;INSERT_NODE&#x27;</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">&#x27;INSERT_CHILD_NODE&#x27;</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">&#x27;simple-mind-map/src/utils&#x27;</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">&#x27;text&#x27;</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">&#x27;&#x27;</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>

View File

@@ -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`里,当然,你也可以存储到数据库中。
## 保存数据

View File

@@ -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>

View File

@@ -4,7 +4,7 @@
`simple-mind-map`内置了一些图标:
<img src="../../../../assets/img/iconList.jpg" />
<img src="../../../../assets/img/docs/iconList.jpg" />
你可以通过如下方式获取:

View File

@@ -3,7 +3,7 @@
<h1>插入和扩展节点图标</h1>
<h2>插入图标</h2>
<p><code>simple-mind-map</code>内置了一些图标</p>
<img src="../../../../assets/img/iconList.jpg" />
<img src="../../../../assets/img/docs/iconList.jpg" />
<p>你可以通过如下方式获取</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> { nodeIconList } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/svg/icons&#x27;</span>
</code></pre>

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

Some files were not shown because too many files have changed in this diff Show More