Demo:扩展大纲功能,支持拖拽,删除

This commit is contained in:
wanglin2 2023-08-06 22:37:13 +08:00
parent 6bdcec0fca
commit ff56fe3e68
4 changed files with 195 additions and 53 deletions

View File

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

View File

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

View File

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

View File

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