417 lines
11 KiB
Vue
417 lines
11 KiB
Vue
<template>
|
|
<el-tree
|
|
ref="tree"
|
|
class="outlineTree"
|
|
node-key="uid"
|
|
draggable
|
|
default-expand-all
|
|
:class="{ isDark: isDark }"
|
|
:data="data"
|
|
:props="defaultProps"
|
|
:highlight-current="true"
|
|
:expand-on-click-node="false"
|
|
:allow-drag="checkAllowDrag"
|
|
@node-drop="onNodeDrop"
|
|
@node-drag-start="onNodeDragStart"
|
|
@node-drag-end="onNodeDragEnd"
|
|
@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="!isReadonly"
|
|
: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>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapState, mapMutations } from 'vuex'
|
|
import {
|
|
nodeRichTextToTextWithWrap,
|
|
textToNodeRichTextWithWrap,
|
|
createUid,
|
|
htmlEscape,
|
|
handleInputPasteText
|
|
} from 'simple-mind-map/src/utils'
|
|
|
|
// 大纲树
|
|
export default {
|
|
props: {
|
|
mindMap: {
|
|
type: Object
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
data: [],
|
|
defaultProps: {
|
|
label: 'label'
|
|
},
|
|
currentData: null,
|
|
notHandleDataChange: false,
|
|
isHandleNodeTreeRenderEnd: false,
|
|
beInsertNodeUid: '',
|
|
insertType: '',
|
|
isInTreArea: false,
|
|
isAfterCreateNewNode: false
|
|
}
|
|
},
|
|
computed: {
|
|
...mapState({
|
|
isReadonly: state => state.isReadonly,
|
|
isDark: state => state.localConfig.isDark
|
|
})
|
|
},
|
|
created() {
|
|
window.addEventListener('keydown', this.onKeyDown)
|
|
this.$bus.$on('data_change', this.handleDataChange)
|
|
this.$bus.$on('node_tree_render_end', this.handleNodeTreeRenderEnd)
|
|
this.$bus.$on('hide_text_edit', this.handleHideTextEdit)
|
|
},
|
|
mounted() {
|
|
this.refresh()
|
|
},
|
|
beforeDestroy() {
|
|
window.removeEventListener('keydown', this.onKeyDown)
|
|
this.$bus.$off('data_change', this.handleDataChange)
|
|
this.$bus.$off('node_tree_render_end', this.handleNodeTreeRenderEnd)
|
|
this.$bus.$off('hide_text_edit', this.handleHideTextEdit)
|
|
},
|
|
methods: {
|
|
...mapMutations(['setIsDragOutlineTreeNode']),
|
|
|
|
handleHideTextEdit() {
|
|
if (this.notHandleDataChange) {
|
|
this.notHandleDataChange = false
|
|
this.refresh()
|
|
}
|
|
},
|
|
|
|
handleDataChange() {
|
|
// 在大纲里操作节点时不要响应该事件,否则会重新刷新树
|
|
if (this.notHandleDataChange) {
|
|
this.notHandleDataChange = false
|
|
this.isAfterCreateNewNode = false
|
|
return
|
|
}
|
|
if (this.isAfterCreateNewNode) {
|
|
this.isAfterCreateNewNode = false
|
|
return
|
|
}
|
|
this.refresh()
|
|
},
|
|
|
|
handleNodeTreeRenderEnd() {
|
|
// 当前存在未完成的节点插入操作
|
|
if (this.insertType) {
|
|
this[this.insertType]()
|
|
this.insertType = ''
|
|
return
|
|
}
|
|
// 插入了新节点后需要做一些操作
|
|
if (this.isHandleNodeTreeRenderEnd) {
|
|
this.isHandleNodeTreeRenderEnd = false
|
|
this.refresh()
|
|
this.$nextTick(() => {
|
|
this.afterCreateNewNode()
|
|
})
|
|
}
|
|
},
|
|
|
|
// 刷新树数据
|
|
refresh() {
|
|
let data = this.mindMap.getData()
|
|
data.root = true // 标记根节点
|
|
let walk = root => {
|
|
let text = root.data.richText
|
|
? nodeRichTextToTextWithWrap(root.data.text)
|
|
: root.data.text
|
|
text = htmlEscape(text)
|
|
text = text.replace(/\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) {
|
|
try {
|
|
this.isAfterCreateNewNode = true
|
|
// 高亮树节点
|
|
this.$refs.tree.setCurrentKey(id)
|
|
let node = this.$refs.tree.getNode(id)
|
|
this.onCurrentChange(node.data)
|
|
// 定位该节点
|
|
this.onClick(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)
|
|
}
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
this.beInsertNodeUid = ''
|
|
},
|
|
|
|
// 根节点不允许拖拽
|
|
checkAllowDrag(node) {
|
|
return !node.data.root
|
|
},
|
|
|
|
// 失去焦点更新节点文本
|
|
onBlur(e, node) {
|
|
// 节点数据没有修改
|
|
if (node.data.textCache === e.target.innerHTML) {
|
|
// 如果存在未执行的插入新节点操作,那么直接执行
|
|
if (this.insertType) {
|
|
this[this.insertType]()
|
|
this.insertType = ''
|
|
}
|
|
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
|
|
this.notHandleDataChange = true
|
|
if (richText) {
|
|
targetNode.setText(textToNodeRichTextWithWrap(text), true)
|
|
} else {
|
|
targetNode.setText(text)
|
|
}
|
|
},
|
|
|
|
// 拦截粘贴事件
|
|
onPaste(e) {
|
|
handleInputPasteText(e)
|
|
},
|
|
|
|
// 生成唯一的key
|
|
getKey() {
|
|
return Math.random()
|
|
},
|
|
|
|
// 节点输入区域按键事件
|
|
onNodeInputKeydown(e) {
|
|
if (e.keyCode === 13 && !e.shiftKey) {
|
|
// 插入兄弟节点
|
|
e.preventDefault()
|
|
this.insertType = 'insertNode'
|
|
e.target.blur()
|
|
}
|
|
if (e.keyCode === 9) {
|
|
e.preventDefault()
|
|
if (e.shiftKey) {
|
|
// 节点上升一级
|
|
this.insertType = 'moveUp'
|
|
e.target.blur()
|
|
} else {
|
|
// 插入子节点
|
|
this.insertType = 'insertChildNode'
|
|
e.target.blur()
|
|
}
|
|
}
|
|
},
|
|
|
|
// 节点上移一个层级
|
|
moveUp() {
|
|
this.mindMap.execCommand('MOVE_UP_ONE_LEVEL')
|
|
},
|
|
|
|
// 插入兄弟节点
|
|
insertNode() {
|
|
this.notHandleDataChange = true
|
|
this.isHandleNodeTreeRenderEnd = true
|
|
this.beInsertNodeUid = createUid()
|
|
this.mindMap.execCommand('INSERT_NODE', false, [], {
|
|
uid: this.beInsertNodeUid
|
|
})
|
|
},
|
|
|
|
// 插入下级节点
|
|
insertChildNode() {
|
|
this.notHandleDataChange = true
|
|
this.isHandleNodeTreeRenderEnd = true
|
|
this.beInsertNodeUid = createUid()
|
|
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
|
|
uid: this.beInsertNodeUid
|
|
})
|
|
},
|
|
|
|
// 激活当前节点且移动当前节点到画布中间
|
|
onClick(data) {
|
|
this.notHandleDataChange = true
|
|
const targetNode = this.mindMap.renderer.findNodeByUid(data.uid)
|
|
if (targetNode && targetNode.nodeData.data.isActive) return
|
|
this.mindMap.execCommand('GO_TARGET_NODE', data.uid, () => {
|
|
this.notHandleDataChange = false
|
|
})
|
|
},
|
|
|
|
onNodeDragStart() {
|
|
this.setIsDragOutlineTreeNode(true)
|
|
},
|
|
|
|
onNodeDragEnd() {
|
|
this.setIsDragOutlineTreeNode(false)
|
|
},
|
|
|
|
// 拖拽结束事件
|
|
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])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.customNode {
|
|
width: 100%;
|
|
color: rgba(0, 0, 0, 0.85);
|
|
font-weight: bold;
|
|
|
|
.nodeEdit {
|
|
outline: none;
|
|
white-space: normal;
|
|
padding-right: 20px;
|
|
}
|
|
}
|
|
|
|
.outlineTree {
|
|
&.isDark {
|
|
background-color: #262a2e;
|
|
|
|
.customNode {
|
|
color: #fff;
|
|
}
|
|
|
|
&.el-tree--highlight-current {
|
|
/deep/ .el-tree-node.is-current > .el-tree-node__content {
|
|
background-color: hsla(0, 0%, 100%, 0.05) !important;
|
|
}
|
|
}
|
|
|
|
/deep/ .el-tree-node__content:hover,
|
|
.el-upload-list__item:hover {
|
|
background-color: hsla(0, 0%, 100%, 0.02) !important;
|
|
}
|
|
|
|
/deep/ .el-tree-node__content {
|
|
.el-tree-node__expand-icon {
|
|
color: #fff;
|
|
|
|
&.is-leaf {
|
|
&::after {
|
|
background-color: #fff;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/deep/ .el-tree-node > .el-tree-node__children {
|
|
overflow: inherit;
|
|
}
|
|
|
|
/deep/ .el-tree-node__content {
|
|
height: auto;
|
|
margin: 5px 0;
|
|
|
|
.el-tree-node__expand-icon {
|
|
color: #262a2e;
|
|
|
|
&.is-leaf {
|
|
color: transparent;
|
|
position: relative;
|
|
|
|
&::after {
|
|
background-color: #262a2e;
|
|
position: absolute;
|
|
content: '';
|
|
width: 5px;
|
|
height: 5px;
|
|
border-radius: 50%;
|
|
left: 10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|