Demo:扩展大纲功能,支持拖拽,删除
This commit is contained in:
parent
6bdcec0fca
commit
ff56fe3e68
@ -1,22 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-tree
|
<el-tree
|
||||||
|
ref="tree"
|
||||||
class="outlineTree"
|
class="outlineTree"
|
||||||
|
node-key="uid"
|
||||||
|
draggable
|
||||||
|
default-expand-all
|
||||||
:class="{ isDark: isDark }"
|
:class="{ isDark: isDark }"
|
||||||
:data="data"
|
:data="data"
|
||||||
:props="defaultProps"
|
:props="defaultProps"
|
||||||
|
:highlight-current="true"
|
||||||
:expand-on-click-node="false"
|
:expand-on-click-node="false"
|
||||||
default-expand-all
|
:allow-drag="checkAllowDrag"
|
||||||
|
@node-drop="onNodeDrop"
|
||||||
|
@current-change="onCurrentChange"
|
||||||
|
@mouseenter.native="isInTreArea = true"
|
||||||
|
@mouseleave.native="isInTreArea = false"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="customNode"
|
class="customNode"
|
||||||
slot-scope="{ node, data }"
|
slot-scope="{ node, data }"
|
||||||
@click="onClick($event, node)"
|
:data-id="data.uid"
|
||||||
|
@click="onClick($event, data)"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="nodeEdit"
|
class="nodeEdit"
|
||||||
:key="getKey()"
|
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
@keydown.stop="onKeydown($event, node)"
|
:key="getKey()"
|
||||||
|
@keydown.stop="onNodeInputKeydown($event, node)"
|
||||||
@keyup.stop
|
@keyup.stop
|
||||||
@blur="onBlur($event, node)"
|
@blur="onBlur($event, node)"
|
||||||
@paste="onPaste($event, node)"
|
@paste="onPaste($event, node)"
|
||||||
@ -31,7 +41,8 @@ import { mapState } from 'vuex'
|
|||||||
import {
|
import {
|
||||||
nodeRichTextToTextWithWrap,
|
nodeRichTextToTextWithWrap,
|
||||||
textToNodeRichTextWithWrap,
|
textToNodeRichTextWithWrap,
|
||||||
getTextFromHtml
|
getTextFromHtml,
|
||||||
|
createUid
|
||||||
} from 'simple-mind-map/src/utils'
|
} from 'simple-mind-map/src/utils'
|
||||||
|
|
||||||
// 大纲树
|
// 大纲树
|
||||||
@ -46,47 +57,114 @@ export default {
|
|||||||
return {
|
return {
|
||||||
data: [],
|
data: [],
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
label(data) {
|
label: 'label'
|
||||||
const text = (data.data.richText
|
|
||||||
? nodeRichTextToTextWithWrap(data.data.text)
|
|
||||||
: data.data.text
|
|
||||||
).replaceAll(/\n/g, '<br>')
|
|
||||||
data.data.textCache = text
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
notHandleDataChange: false
|
currentData: null,
|
||||||
|
notHandleDataChange: false,
|
||||||
|
handleNodeTreeRenderEnd: false,
|
||||||
|
beInsertNodeUid: '',
|
||||||
|
isInTreArea: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['isDark','isOutlineEdit'])
|
...mapState(['isDark', 'isOutlineEdit'])
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$bus.$on('data_change', data => {
|
window.addEventListener('keydown', this.onKeyDown)
|
||||||
|
this.$bus.$on('data_change', () => {
|
||||||
// 激活节点会让当前大纲失去焦点
|
// 激活节点会让当前大纲失去焦点
|
||||||
if (this.notHandleDataChange) {
|
if (this.notHandleDataChange) {
|
||||||
this.notHandleDataChange = false
|
this.notHandleDataChange = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.data = [this.mindMap.renderer.renderTree]
|
this.refresh()
|
||||||
|
})
|
||||||
|
this.$bus.$on('node_tree_render_end', () => {
|
||||||
|
// 激活节点会让当前大纲失去焦点
|
||||||
|
if (this.handleNodeTreeRenderEnd) {
|
||||||
|
this.handleNodeTreeRenderEnd = false
|
||||||
|
this.notHandleDataChange = false
|
||||||
|
this.refresh()
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.afterCreateNewNode()
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.refresh()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 刷新树数据
|
||||||
refresh() {
|
refresh() {
|
||||||
this.data = [this.mindMap.renderer.renderTree]
|
let data = this.mindMap.getData()
|
||||||
|
data.root = true // 标记根节点
|
||||||
|
let walk = root => {
|
||||||
|
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]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 插入了新节点之后
|
||||||
|
afterCreateNewNode() {
|
||||||
|
// 如果是新插入节点,那么需要手动高亮该节点、定位该节点及聚焦
|
||||||
|
let id = this.beInsertNodeUid
|
||||||
|
if (id && this.$refs.tree) {
|
||||||
|
// 高亮树节点
|
||||||
|
this.$refs.tree.setCurrentKey(id)
|
||||||
|
let node = this.$refs.tree.getNode(id)
|
||||||
|
this.onCurrentChange(node.data)
|
||||||
|
// 定位该节点
|
||||||
|
this.onClick(null, node.data)
|
||||||
|
// 聚焦该树节点的编辑框
|
||||||
|
const el = document.querySelector(
|
||||||
|
`.customNode[data-id="${id}"] .nodeEdit`
|
||||||
|
)
|
||||||
|
if (el) {
|
||||||
|
let selection = window.getSelection()
|
||||||
|
let range = document.createRange()
|
||||||
|
range.selectNodeContents(el)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
let offsetTop = el.offsetTop
|
||||||
|
this.$emit('scrollTo', offsetTop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 根节点不允许拖拽
|
||||||
|
checkAllowDrag(node) {
|
||||||
|
return !node.data.root
|
||||||
|
},
|
||||||
|
|
||||||
|
// 失去焦点更新节点文本
|
||||||
onBlur(e, node) {
|
onBlur(e, node) {
|
||||||
if (node.data.data.textCache === e.target.innerHTML) {
|
if (node.data.textCache === e.target.innerHTML) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delete node.data.data.textCache
|
|
||||||
const richText = node.data.data.richText
|
const richText = node.data.data.richText
|
||||||
const text = richText ? e.target.innerHTML : e.target.innerText
|
const text = richText ? e.target.innerHTML : e.target.innerText
|
||||||
|
const targetNode = this.mindMap.renderer.findNodeByUid(node.data.uid)
|
||||||
|
if (!targetNode) return
|
||||||
if (richText) {
|
if (richText) {
|
||||||
node.data._node.setText(textToNodeRichTextWithWrap(text), true, true)
|
targetNode.setText(textToNodeRichTextWithWrap(text), true, true)
|
||||||
} else {
|
} else {
|
||||||
node.data._node.setText(text)
|
targetNode.setText(text)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -106,11 +184,13 @@ export default {
|
|||||||
selection.collapseToEnd()
|
selection.collapseToEnd()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 生成唯一的key
|
||||||
getKey() {
|
getKey() {
|
||||||
return Math.random()
|
return Math.random()
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeydown(e) {
|
// 节点输入区域按键事件
|
||||||
|
onNodeInputKeydown(e) {
|
||||||
if (e.keyCode === 13 && !e.shiftKey) {
|
if (e.keyCode === 13 && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.insertNode()
|
this.insertNode()
|
||||||
@ -123,25 +203,76 @@ export default {
|
|||||||
|
|
||||||
// 插入兄弟节点
|
// 插入兄弟节点
|
||||||
insertNode() {
|
insertNode() {
|
||||||
this.notHandleDataChange = false
|
this.notHandleDataChange = true
|
||||||
this.mindMap.execCommand('INSERT_NODE', false)
|
this.handleNodeTreeRenderEnd = true
|
||||||
|
this.beInsertNodeUid = createUid()
|
||||||
|
this.mindMap.execCommand('INSERT_NODE', false, [], {
|
||||||
|
uid: this.beInsertNodeUid
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 插入下级节点
|
// 插入下级节点
|
||||||
insertChildNode() {
|
insertChildNode() {
|
||||||
this.notHandleDataChange = false
|
this.notHandleDataChange = true
|
||||||
this.mindMap.execCommand('INSERT_CHILD_NODE', false)
|
this.handleNodeTreeRenderEnd = true
|
||||||
|
this.beInsertNodeUid = createUid()
|
||||||
|
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
|
||||||
|
uid: this.beInsertNodeUid
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 激活当前节点且移动当前节点到画布中间
|
// 激活当前节点且移动当前节点到画布中间
|
||||||
onClick(e, node) {
|
onClick(e, data) {
|
||||||
this.notHandleDataChange = true
|
this.notHandleDataChange = true
|
||||||
let targetNode = node.data._node
|
const targetNode = this.mindMap.renderer.findNodeByUid(data.uid)
|
||||||
if (targetNode && targetNode.nodeData.data.isActive) return
|
if (targetNode && targetNode.nodeData.data.isActive) return
|
||||||
this.mindMap.renderer.textEdit.stopFocusOnNodeActive()
|
this.mindMap.renderer.textEdit.stopFocusOnNodeActive()
|
||||||
this.mindMap.execCommand('GO_TARGET_NODE', node.data.data.uid, () => {
|
this.mindMap.execCommand('GO_TARGET_NODE', data.uid, () => {
|
||||||
this.mindMap.renderer.textEdit.openFocusOnNodeActive()
|
this.mindMap.renderer.textEdit.openFocusOnNodeActive()
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 拖拽结束事件
|
||||||
|
onNodeDrop(data, target, postion) {
|
||||||
|
this.notHandleDataChange = true
|
||||||
|
const node = this.mindMap.renderer.findNodeByUid(data.data.uid)
|
||||||
|
const targetNode = this.mindMap.renderer.findNodeByUid(target.data.uid)
|
||||||
|
if (!node || !targetNode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (postion) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 当前选中的树节点变化事件
|
||||||
|
onCurrentChange(data) {
|
||||||
|
this.currentData = data
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除节点
|
||||||
|
onKeyDown(e) {
|
||||||
|
if (!this.isInTreArea) return
|
||||||
|
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.notHandleDataChange = true
|
||||||
|
this.$refs.tree.remove(this.currentData)
|
||||||
|
this.mindMap.execCommand('REMOVE_NODE', [node])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,26 +284,10 @@ export default {
|
|||||||
color: rgba(0, 0, 0, 0.85);
|
color: rgba(0, 0, 0, 0.85);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 7px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
box-shadow: none;
|
|
||||||
background: transparent;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeEdit {
|
.nodeEdit {
|
||||||
outline: none;
|
outline: none;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<span class="icon iconfont iconguanbi"></span>
|
<span class="icon iconfont iconguanbi"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="outlineEdit">
|
<div class="outlineEdit">
|
||||||
<Outline :mindMap="mindMap" ref="outline"></Outline>
|
<Outline :mindMap="mindMap" @scrollTo="onScrollTo"></Outline>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -35,7 +35,6 @@ export default {
|
|||||||
isOutlineEdit(val) {
|
isOutlineEdit(val) {
|
||||||
if (val) {
|
if (val) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.outline.refresh()
|
|
||||||
document.body.appendChild(this.$refs.outlineEditContainer)
|
document.body.appendChild(this.$refs.outlineEditContainer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -46,6 +45,16 @@ export default {
|
|||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
this.setIsOutlineEdit(false)
|
this.setIsOutlineEdit(false)
|
||||||
|
},
|
||||||
|
|
||||||
|
onScrollTo(y) {
|
||||||
|
let container = this.$refs.outlineEditContainer
|
||||||
|
let height = container.offsetHeight
|
||||||
|
let top = container.scrollTop
|
||||||
|
y += 50
|
||||||
|
if (y > top + height) {
|
||||||
|
container.scrollTo(0, y - height / 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,7 +72,6 @@ export default {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 50px 0;
|
|
||||||
|
|
||||||
.closeBtn {
|
.closeBtn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -80,6 +88,7 @@ export default {
|
|||||||
width: 1000px;
|
width: 1000px;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 50px 0;
|
||||||
|
|
||||||
/deep/ .customNode {
|
/deep/ .customNode {
|
||||||
.nodeEdit {
|
.nodeEdit {
|
||||||
|
|||||||
@ -3,7 +3,11 @@
|
|||||||
<div class="changeBtn" @click="onChangeToOutlineEdit">
|
<div class="changeBtn" @click="onChangeToOutlineEdit">
|
||||||
<span class="icon iconfont iconquanping1"></span>
|
<span class="icon iconfont iconquanping1"></span>
|
||||||
</div>
|
</div>
|
||||||
<Outline :mindMap="mindMap"></Outline>
|
<Outline
|
||||||
|
:mindMap="mindMap"
|
||||||
|
v-if="activeSidebar === 'outline'"
|
||||||
|
@scrollTo="onScrollTo"
|
||||||
|
></Outline>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -37,10 +41,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(['setIsOutlineEdit']),
|
...mapMutations(['setIsOutlineEdit', 'setActiveSidebar']),
|
||||||
|
|
||||||
onChangeToOutlineEdit() {
|
onChangeToOutlineEdit() {
|
||||||
|
this.setActiveSidebar('')
|
||||||
this.setIsOutlineEdit(true)
|
this.setIsOutlineEdit(true)
|
||||||
|
},
|
||||||
|
|
||||||
|
onScrollTo(y) {
|
||||||
|
let container = this.$refs.sidebar.getEl()
|
||||||
|
let height = container.offsetHeight
|
||||||
|
let top = container.scrollTop
|
||||||
|
if (y > top + height) {
|
||||||
|
container.scrollTo(0, y - height / 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<div class="sidebarHeader" v-if="title">
|
<div class="sidebarHeader" v-if="title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebarContent">
|
<div class="sidebarContent" ref="sidebarContent">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -59,6 +59,10 @@ export default {
|
|||||||
close() {
|
close() {
|
||||||
this.show = false
|
this.show = false
|
||||||
this.setActiveSidebar('')
|
this.setActiveSidebar('')
|
||||||
|
},
|
||||||
|
|
||||||
|
getEl() {
|
||||||
|
return this.$refs.sidebarContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user