Feat:修改复制、剪切、粘贴逻辑;支持粘贴剪切板中的数据

This commit is contained in:
wanglin2 2023-07-21 17:40:09 +08:00
parent f2b247e85c
commit a76ec0dad8
6 changed files with 217 additions and 70 deletions

View File

@ -213,6 +213,10 @@ export const CONSTANTS = {
TOP: 'top',
RIGHT: 'right',
BOTTOM: 'bottom'
},
PASTE_TYPE: {
CLIP_BOARD: 'clipBoard',
CANVAS: 'canvas'
}
}

View File

@ -57,8 +57,11 @@ export default class KeyCommand {
}
Object.keys(this.shortcutMap).forEach(key => {
if (this.checkKey(e, key)) {
e.stopPropagation()
e.preventDefault()
// 粘贴事件不组织因为要监听paste事件
if (!this.checkKey(e, 'Control+v')) {
e.stopPropagation()
e.preventDefault()
}
this.shortcutMap[key].forEach(fn => {
fn()
})

View File

@ -7,7 +7,13 @@ import Timeline from '../../layouts/Timeline'
import VerticalTimeline from '../../layouts/VerticalTimeline'
import Fishbone from '../../layouts/Fishbone'
import TextEdit from './TextEdit'
import { copyNodeTree, simpleDeepClone, walk, bfsWalk } from '../../utils'
import {
copyNodeTree,
simpleDeepClone,
walk,
bfsWalk,
loadImage
} from '../../utils'
import { shapeList } from './node/Shape'
import { lineStyleProps } from '../../themes/default'
import { CONSTANTS } from '../../constants/constant'
@ -29,7 +35,7 @@ const layouts = {
// 竖向时间轴
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline,
// 鱼骨图
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone,
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone
}
// 渲染
@ -60,6 +66,12 @@ class Render {
this.root = null
// 文本编辑框需要再bindEvent之前实例化否则单击事件只能触发隐藏文本编辑框而无法保存文本修改
this.textEdit = new TextEdit(this)
// 当前复制的数据
this.lastBeingCopyData = null
this.beingCopyData = null
this.beingPasteText = ''
this.beingPasteImgSize = 0
this.currentBeingPasteType = ''
// 布局
this.setLayout()
// 绑定事件
@ -82,18 +94,24 @@ class Render {
// 绑定事件
bindEvent() {
// 点击事件
this.mindMap.on('draw_click', (e) => {
this.mindMap.on('draw_click', e => {
// 清除激活状态
let isTrueClick = true
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
if (useLeftKeySelectionRightKeyDrag) {
let mousedownPos = this.mindMap.event.mousedownPos
isTrueClick = Math.abs(e.clientX - mousedownPos.x) <= 5 && Math.abs(e.clientY - mousedownPos.y) <= 5
isTrueClick =
Math.abs(e.clientX - mousedownPos.x) <= 5 &&
Math.abs(e.clientY - mousedownPos.y) <= 5
}
if (isTrueClick && this.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
})
// 粘贴事件
this.mindMap.on('paste', data => {
this.onPaste(data)
})
}
// 注册命令
@ -242,6 +260,14 @@ class Render {
// 下移节点
this.mindMap.keyCommand.addShortcut('Control+Down', this.downNode)
// 复制节点、剪切节点、粘贴节点的快捷键需开发者自行注册实现可参考demo
this.copy = this.copy.bind(this)
this.mindMap.keyCommand.addShortcut('Control+c', this.copy)
this.mindMap.keyCommand.addShortcut('Control+v', () => {
// 隐藏输入框可能会失去焦点,所以要重新聚焦
this.textEdit.focusHiddenInput()
})
this.cut = this.cut.bind(this)
this.mindMap.keyCommand.addShortcut('Control+x', this.cut)
}
// 开启文字编辑,会禁用回车键和删除键相关快捷键防止冲突
@ -281,7 +307,7 @@ class Render {
// 计算布局
this.layout.doLayout(root => {
// 删除本次渲染时不再需要的节点
Object.keys(this.lastNodeCache).forEach((uid) => {
Object.keys(this.lastNodeCache).forEach(uid => {
if (!this.nodeCache[uid]) {
this.lastNodeCache[uid].destroy()
if (this.lastNodeCache[uid].parent) {
@ -301,7 +327,10 @@ class Render {
this.render(callback, source)
} else {
// 触发一次保存,因为修改了渲染树的数据
if (this.mindMap.richText && [CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)) {
if (
this.mindMap.richText &&
[CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)
) {
this.mindMap.command.addHistory()
}
}
@ -406,7 +435,7 @@ class Render {
// 规范指定节点数据
formatAppointNodes(appointNodes) {
if (!appointNodes) return []
return Array.isArray(appointNodes) ? appointNodes: [appointNodes]
return Array.isArray(appointNodes) ? appointNodes : [appointNodes]
}
// 插入同级节点,多个节点只会操作第一个节点
@ -415,7 +444,10 @@ class Render {
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
let { defaultInsertSecondLevelNodeText, defaultInsertBelowSecondLevelNodeText } = this.mindMap.opt
let {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
let first = list[0]
if (first.isGeneralization) {
@ -424,7 +456,10 @@ class Render {
if (first.isRoot) {
this.insertChildNode(openEdit, appointNodes, appointData)
} else {
let text = first.layerIndex === 1 ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
let text =
first.layerIndex === 1
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
if (first.layerIndex === 1) {
first.parent.destroy()
}
@ -451,7 +486,10 @@ class Render {
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
let { defaultInsertSecondLevelNodeText, defaultInsertBelowSecondLevelNodeText } = this.mindMap.opt
let {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
list.forEach(node => {
if (node.isGeneralization) {
@ -460,7 +498,9 @@ class Render {
if (!node.nodeData.children) {
node.nodeData.children = []
}
let text = node.isRoot ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
let text = node.isRoot
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
let isRichText = !!this.mindMap.richText
node.nodeData.children.push({
inserting: openEdit,
@ -536,13 +576,81 @@ class Render {
this.mindMap.render()
}
// 复制节点
copy() {
this.beingCopyData = this.copyNode()
}
// 剪切节点
cut() {
this.mindMap.execCommand('CUT_NODE', copyData => {
this.beingCopyData = copyData
})
}
// 粘贴节点
paste() {
if (this.beingCopyData) {
this.mindMap.execCommand('PASTE_NODE', this.beingCopyData)
}
}
// 粘贴事件
async onPaste({ text, img }) {
// 检查剪切板数据是否有变化
// 通过图片大小来判断图片是否发生变化,可能是不准确的,但是目前没有其他好方法
const imgSize = img ? img.size : 0
if (this.beingPasteText !== text || this.beingPasteImgSize !== imgSize) {
this.currentBeingPasteType = CONSTANTS.PASTE_TYPE.CLIP_BOARD
this.beingPasteText = text
this.beingPasteImgSize = imgSize
}
// 检查要粘贴的节点数据是否有变化,节点优先级高于剪切板
if (this.lastBeingCopyData !== this.beingCopyData) {
this.lastBeingCopyData = this.beingCopyData
this.currentBeingPasteType = CONSTANTS.PASTE_TYPE.CANVAS
}
// 粘贴剪切板的数据
if (this.currentBeingPasteType === CONSTANTS.PASTE_TYPE.CLIP_BOARD) {
// 存在文本,则创建子节点
if (text) {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
}
// 存在图片,则添加到当前激活节点
if (img) {
try {
let imgData = await loadImage(img)
if (this.activeNodeList.length > 0) {
this.activeNodeList.forEach(node => {
this.mindMap.execCommand('SET_NODE_IMAGE', node, {
url: imgData.url,
title: '',
width: imgData.size.width,
height: imgData.size.height
})
})
}
} catch (error) {
console.log(error)
}
}
} else {
// 粘贴节点数据
this.paste()
}
}
// 将节点移动到另一个节点的前面
insertBefore(node, exist) {
if (node.isRoot) {
return
}
// 如果是二级节点变成了下级节点,或是下级节点变成了二级节点,节点样式需要更新
let nodeLayerChanged = (node.layerIndex === 1 && exist.layerIndex !== 1) || (node.layerIndex !== 1 && exist.layerIndex === 1)
let nodeLayerChanged =
(node.layerIndex === 1 && exist.layerIndex !== 1) ||
(node.layerIndex !== 1 && exist.layerIndex === 1)
// 移动节点
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
@ -579,7 +687,9 @@ class Render {
return
}
// 如果是二级节点变成了下级节点,或是下级节点变成了二级节点,节点样式需要更新
let nodeLayerChanged = (node.layerIndex === 1 && exist.layerIndex !== 1) || (node.layerIndex !== 1 && exist.layerIndex === 1)
let nodeLayerChanged =
(node.layerIndex === 1 && exist.layerIndex !== 1) ||
(node.layerIndex !== 1 && exist.layerIndex === 1)
// 移动节点
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
@ -619,7 +729,7 @@ class Render {
}
let isAppointNodes = appointNodes.length > 0
let list = isAppointNodes ? appointNodes : this.activeNodeList
let root = list.find((node) => {
let root = list.find(node => {
return node.isRoot
})
if (root) {
@ -705,7 +815,7 @@ class Render {
// 粘贴节点到节点
pasteNode(data) {
if (this.activeNodeList.length <= 0) {
if (this.activeNodeList.length <= 0 || !data) {
return
}
this.activeNodeList.forEach(item => {
@ -997,7 +1107,7 @@ class Render {
if (targetNode) {
targetNode.active()
this.moveNodeToCenter(targetNode)
}
}
})
}
@ -1038,7 +1148,7 @@ class Render {
this.mindMap.view.setScale(1)
}
// 展开到指定uid的节点
// 展开到指定uid的节点
expandToNodeUid(uid, callback = () => {}) {
let parentsList = []
const cache = {}
@ -1047,11 +1157,11 @@ class Render {
parentsList = parent ? [...cache[parent.data.uid], parent] : []
return 'stop'
} else {
cache[node.data.uid] = parent ? [...cache[parent.data.uid], parent]: []
cache[node.data.uid] = parent ? [...cache[parent.data.uid], parent] : []
}
})
let needRender = false
parentsList.forEach((node) => {
parentsList.forEach(node => {
if (!node.data.expand) {
needRender = true
node.data.expand = true
@ -1067,11 +1177,11 @@ class Render {
// 根据uid找到对应的节点实例
findNodeByUid(uid) {
let res = null
walk(this.root, null, (node) => {
walk(this.root, null, node => {
if (node.nodeData.data.uid === uid) {
res = node
return true
}
}
})
return res
}

View File

@ -10,11 +10,14 @@ export default class TextEdit {
this.currentNode = null
// 文本编辑框
this.textEditNode = null
// 隐藏的文本输入框
this.hiddenInputEl = null
// 文本编辑框是否显示
this.showTextEdit = false
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
this.cacheEditingText = ''
this.bindEvent()
this.createHiddenInput()
}
// 事件
@ -46,6 +49,10 @@ export default class TextEdit {
this.mindMap.on('before_node_active', () => {
this.hideEditTextBox()
})
// 节点激活事件
this.mindMap.on('node_active', () => {
this.focusHiddenInput()
})
// 注册编辑快捷键
this.mindMap.keyCommand.addShortcut('F2', () => {
if (this.renderer.activeNodeList.length <= 0) {
@ -56,6 +63,43 @@ export default class TextEdit {
this.mindMap.on('scale', this.onScale)
}
// 创建一个隐藏的文本输入框
createHiddenInput() {
if (this.hiddenInputEl) return
this.hiddenInputEl = document.createElement('input')
this.hiddenInputEl.type = 'text'
this.hiddenInputEl.style.cssText = `
position: fixed;
left: -99999px;
top: -99999px;
`
// 监听粘贴事件
this.hiddenInputEl.addEventListener('paste', async event => {
event.preventDefault()
const text = (event.clipboardData || window.clipboardData).getData('text')
const files = event.clipboardData.files
let img = null
if (files.length > 0) {
for (let i = 0; i < files.length; i++) {
if (/^image\//.test(files[i].type)) {
img = files[i]
break
}
}
}
this.mindMap.emit('paste', {
text,
img
})
})
document.body.appendChild(this.hiddenInputEl)
}
// 让隐藏的文本输入框聚焦
focusHiddenInput() {
if (this.hiddenInputEl) this.hiddenInputEl.focus()
}
// 注册临时快捷键
registerTmpShortcut() {
// 注册回车快捷键
@ -96,7 +140,8 @@ export default class TextEdit {
onScale() {
if (!this.currentNode) return
if (this.mindMap.richText) {
this.mindMap.richText.cacheEditingText = this.mindMap.richText.getEditText()
this.mindMap.richText.cacheEditingText =
this.mindMap.richText.getEditText()
this.mindMap.richText.showTextEdit = false
} else {
this.cacheEditingText = this.getEditText()
@ -124,7 +169,9 @@ export default class TextEdit {
let scale = this.mindMap.view.scale
let lineHeight = node.style.merge('lineHeight')
let fontSize = node.style.merge('fontSize')
let textLines = (this.cacheEditingText || node.nodeData.data.text).split(/\n/gim)
let textLines = (this.cacheEditingText || node.nodeData.data.text).split(
/\n/gim
)
let isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
node.style.domText(this.textEditNode, scale, isMultiLine)
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
@ -134,9 +181,12 @@ export default class TextEdit {
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 * scale + 'px'
this.textEditNode.style.maxWidth =
this.mindMap.opt.textAutoWrapWidth * scale + 'px'
if (isMultiLine && lineHeight !== 1) {
this.textEditNode.style.transform = `translateY(${-((lineHeight * fontSize - fontSize) / 2) * scale}px)`
this.textEditNode.style.transform = `translateY(${
-((lineHeight * fontSize - fontSize) / 2) * scale
}px)`
}
this.showTextEdit = true
// 选中文本

View File

@ -433,4 +433,23 @@ export const getImageSize = src => {
// 创建节点唯一的id
export const createUid = () => {
return uuidv4()
}
}
// 加载图片文件
export const loadImage = imgFile => {
return new Promise((resolve, reject) => {
let fr = new FileReader()
fr.readAsDataURL(imgFile)
fr.onload = async e => {
let url = e.target.result
let size = await getImageSize(url)
resolve({
url,
size
})
}
fr.onerror = error => {
reject(error)
}
})
}

View File

@ -55,7 +55,6 @@
</div>
<div
class="item"
:class="{ disabled: copyData === null }"
@click="exec('PASTE_NODE')"
>
{{ $t('contextmenu.pasteNode') }}
@ -123,7 +122,6 @@ export default {
left: 0,
top: 0,
node: null,
copyData: null,
type: '',
isMousedown: false,
mosuedownX: 0,
@ -180,10 +178,6 @@ export default {
this.$bus.$on('expand_btn_click', this.hide)
this.$bus.$on('svg_mousedown', this.onMousedown)
this.$bus.$on('mouseup', this.onMouseup)
//
this.mindMap.keyCommand.addShortcut('Control+c', this.copy)
this.mindMap.keyCommand.addShortcut('Control+v', this.paste)
this.mindMap.keyCommand.addShortcut('Control+x', this.cut)
},
beforeDestroy() {
this.$bus.$off('node_contextmenu', this.show)
@ -192,10 +186,6 @@ export default {
this.$bus.$off('expand_btn_click', this.hide)
this.$bus.$on('svg_mousedown', this.onMousedown)
this.$bus.$on('mouseup', this.onMouseup)
//
this.mindMap.keyCommand.removeShortcut('Control+c', this.copy)
this.mindMap.keyCommand.removeShortcut('Control+v', this.paste)
this.mindMap.keyCommand.removeShortcut('Control+x', this.cut)
},
methods: {
...mapMutations(['setLocalConfig']),
@ -282,15 +272,13 @@ export default {
}
switch (key) {
case 'COPY_NODE':
this.copyData = this.mindMap.renderer.copyNode()
this.mindMap.renderer.copy()
break
case 'CUT_NODE':
this.$bus.$emit('execCommand', key, copyData => {
this.copyData = copyData
})
this.mindMap.renderer.cut()
break
case 'PASTE_NODE':
this.$bus.$emit('execCommand', key, this.copyData)
this.mindMap.renderer.paste()
break
case 'RETURN_CENTER':
this.mindMap.view.reset()
@ -308,33 +296,6 @@ export default {
break
}
this.hide()
},
/**
* @Author: 王林25
* @Date: 2022-08-04 14:25:45
* @Desc: 复制
*/
copy() {
this.exec('COPY_NODE')
},
/**
* @Author: 王林25
* @Date: 2022-08-04 14:26:43
* @Desc: 粘贴
*/
paste() {
this.exec('PASTE_NODE')
},
/**
* @Author: 王林25
* @Date: 2022-08-04 14:27:32
* @Desc: 剪切
*/
cut() {
this.exec('CUT_NODE')
}
}
}