mirror of
https://github.com/wanglin2/mind-map.git
synced 2026-02-21 10:27:44 +08:00
Feat:优化drag插件,支持同时拖动多个节点
This commit is contained in:
parent
036f845968
commit
69ef7faf49
@ -365,7 +365,7 @@ export const cssContent = `
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.smm-node:hover .smm-hover-node{
|
||||
.smm-node:not(.smm-node-dragging):hover .smm-hover-node{
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
@ -188,5 +188,18 @@ export const defaultOpt = {
|
||||
// 拖拽节点时鼠标移动到画布边缘是否开启画布自动移动
|
||||
autoMoveWhenMouseInEdgeOnDrag: true,
|
||||
// 是否首次加载fit view
|
||||
fit: false
|
||||
fit: false,
|
||||
// 拖拽多个节点时随鼠标移动的示意矩形的样式配置
|
||||
dragMultiNodeRectConfig: {
|
||||
width: 40,
|
||||
height: 20,
|
||||
fill: '' // 填充颜色,如果不传默认使用连线的颜色
|
||||
},
|
||||
// 节点拖拽时新位置的示意矩形的填充颜色,如果不传默认使用连线的颜色
|
||||
dragPlaceholderRectFill: '',
|
||||
// 节点拖拽时的透明度配置
|
||||
dragOpacityConfig: {
|
||||
cloneNodeOpacity: 0.5, // 跟随鼠标移动的克隆节点或矩形的透明度
|
||||
beingDragNodeOpacity: 0.3 // 被拖拽节点的透明度
|
||||
}
|
||||
}
|
||||
|
||||
@ -440,12 +440,14 @@ class Node {
|
||||
this.mindMap.emit('node_mouseup', this, e)
|
||||
})
|
||||
this.group.on('mouseenter', e => {
|
||||
if (this.isDrag) return
|
||||
this._isMouseenter = true
|
||||
// 显示展开收起按钮
|
||||
this.showExpandBtn()
|
||||
this.mindMap.emit('node_mouseenter', this, e)
|
||||
})
|
||||
this.group.on('mouseleave', e => {
|
||||
if (!this._isMouseenter) return
|
||||
this._isMouseenter = false
|
||||
this.hideExpandBtn()
|
||||
this.mindMap.emit('node_mouseleave', this, e)
|
||||
@ -701,6 +703,61 @@ class Node {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置节点透明度
|
||||
// 包括连接线和下级节点
|
||||
setOpacity(val) {
|
||||
// 自身及连线
|
||||
this.group.opacity(val)
|
||||
this._lines.forEach(line => {
|
||||
line.opacity(val)
|
||||
})
|
||||
// 子节点
|
||||
this.children.forEach(item => {
|
||||
item.setOpacity(val)
|
||||
})
|
||||
// 概要节点
|
||||
if (this._generalizationNode) {
|
||||
this._generalizationLine.opacity(val)
|
||||
this._generalizationNode.group.opacity(val)
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏子节点
|
||||
hideChildren() {
|
||||
this._lines.forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
if (this.children && this.children.length) {
|
||||
this.children.forEach(item => {
|
||||
item.hide()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 显示子节点
|
||||
showChildren() {
|
||||
this._lines.forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
if (this.children && this.children.length) {
|
||||
this.children.forEach(item => {
|
||||
item.show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 被拖拽中
|
||||
startDrag() {
|
||||
this.isDrag = true
|
||||
this.group.addClass('smm-node-dragging')
|
||||
}
|
||||
|
||||
// 拖拽结束
|
||||
endDrag() {
|
||||
this.isDrag = false
|
||||
this.group.removeClass('smm-node-dragging')
|
||||
}
|
||||
|
||||
// 连线
|
||||
renderLine(deep = false) {
|
||||
if (this.nodeData.data.expand === false) {
|
||||
|
||||
@ -136,6 +136,7 @@ function renderExpandBtn() {
|
||||
this._expandBtn.on('dblclick', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this._expandBtn.addClass('smm-expand-btn')
|
||||
this.group.add(this._expandBtn)
|
||||
}
|
||||
this._showExpandBtn = true
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { bfsWalk, throttle } from '../utils'
|
||||
import { bfsWalk, throttle, getTopAncestorsFomNodeList } from '../utils'
|
||||
import Base from '../layouts/Base'
|
||||
|
||||
// 节点拖动插件
|
||||
@ -13,10 +13,14 @@ class Drag extends Base {
|
||||
|
||||
// 复位
|
||||
reset() {
|
||||
// 是否正在跳转中
|
||||
this.isDragging = false
|
||||
// 鼠标按下的节点
|
||||
this.mousedownNode = null
|
||||
// 被拖拽中的节点列表
|
||||
this.beingDragNodeList = []
|
||||
// 当前画布节点列表
|
||||
this.nodeList = []
|
||||
// 当前拖拽节点
|
||||
this.node = null
|
||||
// 当前重叠节点
|
||||
this.overlapNode = null
|
||||
// 当前上一个同级节点
|
||||
@ -27,16 +31,11 @@ class Drag extends Base {
|
||||
this.drawTransform = null
|
||||
// 克隆节点
|
||||
this.clone = null
|
||||
// 连接线
|
||||
this.line = null
|
||||
// 同级位置占位符
|
||||
this.placeholder = null
|
||||
// 鼠标按下位置和节点左上角的偏移量
|
||||
this.offsetX = 0
|
||||
this.offsetY = 0
|
||||
// 克隆节点左上角的坐标
|
||||
this.cloneNodeLeft = 0
|
||||
this.cloneNodeTop = 0
|
||||
// 当前鼠标是否按下
|
||||
this.isMousedown = false
|
||||
// 拖拽的鼠标位置变量
|
||||
@ -53,45 +52,42 @@ class Drag extends Base {
|
||||
bindEvent() {
|
||||
this.checkOverlapNode = throttle(this.checkOverlapNode, 300, this)
|
||||
this.mindMap.on('node_mousedown', (node, e) => {
|
||||
if (this.mindMap.opt.readonly || node.isGeneralization) {
|
||||
return
|
||||
}
|
||||
if (e.which !== 1 || node.isRoot) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
// 计算鼠标按下的位置距离节点左上角的距离
|
||||
this.drawTransform = this.mindMap.draw.transform()
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.offsetX = x - (node.left * scaleX + translateX)
|
||||
this.offsetY = y - (node.top * scaleY + translateY)
|
||||
this.node = node
|
||||
this.isMousedown = true
|
||||
this.mouseDownX = x
|
||||
this.mouseDownY = y
|
||||
this.nodeTreeToList()
|
||||
})
|
||||
this.mindMap.on('mousemove', e => {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
this.mindMap.emit('node_dragging', this.node)
|
||||
e.preventDefault()
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseMoveX = x
|
||||
this.mouseMoveY = y
|
||||
// 只读模式、不是鼠标左键按下、按下的是概要节点或根节点直接返回
|
||||
if (
|
||||
Math.abs(x - this.mouseDownX) <= this.checkDragOffset &&
|
||||
Math.abs(y - this.mouseDownY) <= this.checkDragOffset &&
|
||||
!this.node.isDrag
|
||||
this.mindMap.opt.readonly ||
|
||||
e.which !== 1 ||
|
||||
node.isGeneralization ||
|
||||
node.isRoot
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.mindMap.renderer.clearAllActive()
|
||||
e.preventDefault()
|
||||
this.isMousedown = true
|
||||
// 记录鼠标按下时的节点
|
||||
this.mousedownNode = node
|
||||
// 记录鼠标按下的坐标
|
||||
const { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseDownX = x
|
||||
this.mouseDownY = y
|
||||
})
|
||||
this.mindMap.on('mousemove', e => {
|
||||
if (this.mindMap.opt.readonly || !this.isMousedown) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
const { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
this.mouseMoveX = x
|
||||
this.mouseMoveY = y
|
||||
// 还没开始移动时鼠标位移过小不认为是拖拽
|
||||
if (
|
||||
!this.isDragging &&
|
||||
Math.abs(x - this.mouseDownX) <= this.checkDragOffset &&
|
||||
Math.abs(y - this.mouseDownY) <= this.checkDragOffset
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.mindMap.emit('node_dragging')
|
||||
this.handleStartMove()
|
||||
this.onMove(x, y, e)
|
||||
})
|
||||
this.onMouseup = this.onMouseup.bind(this)
|
||||
@ -105,9 +101,12 @@ class Drag extends Base {
|
||||
return
|
||||
}
|
||||
this.isMousedown = false
|
||||
let _nodeIsDrag = this.node.isDrag
|
||||
this.node.isDrag = false
|
||||
this.node.show()
|
||||
// 恢复被拖拽节点的临时设置
|
||||
this.beingDragNodeList.forEach(node => {
|
||||
node.setOpacity(1)
|
||||
node.showChildren()
|
||||
node.endDrag()
|
||||
})
|
||||
this.removeCloneNode()
|
||||
let overlapNodeUid = this.overlapNode
|
||||
? this.overlapNode.nodeData.data.uid
|
||||
@ -117,17 +116,33 @@ class Drag extends Base {
|
||||
// 存在重叠子节点,则移动作为其子节点
|
||||
if (this.overlapNode) {
|
||||
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
|
||||
this.mindMap.execCommand('MOVE_NODE_TO', this.node, this.overlapNode)
|
||||
this.mindMap.execCommand(
|
||||
'MOVE_NODE_TO',
|
||||
this.beingDragNodeList,
|
||||
this.overlapNode
|
||||
)
|
||||
} else if (this.prevNode) {
|
||||
// 存在前一个相邻节点,作为其下一个兄弟节点
|
||||
this.mindMap.renderer.setNodeActive(this.prevNode, false)
|
||||
this.mindMap.execCommand('INSERT_AFTER', this.node, this.prevNode)
|
||||
this.mindMap.execCommand(
|
||||
'INSERT_AFTER',
|
||||
this.beingDragNodeList,
|
||||
this.prevNode
|
||||
)
|
||||
} else if (this.nextNode) {
|
||||
// 存在下一个相邻节点,作为其前一个兄弟节点
|
||||
this.mindMap.renderer.setNodeActive(this.nextNode, false)
|
||||
this.mindMap.execCommand('INSERT_BEFORE', this.node, this.nextNode)
|
||||
} else if (_nodeIsDrag && this.mindMap.opt.enableFreeDrag) {
|
||||
// 自定义位置
|
||||
this.mindMap.execCommand(
|
||||
'INSERT_BEFORE',
|
||||
this.beingDragNodeList,
|
||||
this.nextNode
|
||||
)
|
||||
} else if (
|
||||
this.clone &&
|
||||
this.mindMap.opt.enableFreeDrag &&
|
||||
this.beingDragNodeList.length === 1
|
||||
) {
|
||||
// 如果只拖拽了一个节点,那么设置自定义位置
|
||||
let { x, y } = this.mindMap.toPos(
|
||||
e.clientX - this.offsetX,
|
||||
e.clientY - this.offsetY
|
||||
@ -135,11 +150,16 @@ class Drag extends Base {
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
x = (x - translateX) / scaleX
|
||||
y = (y - translateY) / scaleY
|
||||
this.node.left = x
|
||||
this.node.top = y
|
||||
this.node.customLeft = x
|
||||
this.node.customTop = y
|
||||
this.mindMap.execCommand('SET_NODE_CUSTOM_POSITION', this.node, x, y)
|
||||
this.mousedownNode.left = x
|
||||
this.mousedownNode.top = y
|
||||
this.mousedownNode.customLeft = x
|
||||
this.mousedownNode.customTop = y
|
||||
this.mindMap.execCommand(
|
||||
'SET_NODE_CUSTOM_POSITION',
|
||||
this.mousedownNode,
|
||||
x,
|
||||
y
|
||||
)
|
||||
this.mindMap.render()
|
||||
}
|
||||
this.reset()
|
||||
@ -150,24 +170,131 @@ class Drag extends Base {
|
||||
})
|
||||
}
|
||||
|
||||
// 拖动中
|
||||
onMove(x, y, e) {
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
// 更新克隆节点的位置
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
let cloneNodeLeft = x - this.offsetX
|
||||
let cloneNodeTop = y - this.offsetY
|
||||
x = (cloneNodeLeft - translateX) / scaleX
|
||||
y = (cloneNodeTop - translateY) / scaleY
|
||||
let t = this.clone.transform()
|
||||
this.clone.translate(x - t.translateX, y - t.translateY)
|
||||
// 检测新位置
|
||||
this.checkOverlapNode()
|
||||
// 如果注册了多选节点插件,那么复用它的边缘自动移动画布功能
|
||||
if (this.mindMap.opt.autoMoveWhenMouseInEdgeOnDrag && this.mindMap.select) {
|
||||
this.drawTransform = this.mindMap.draw.transform()
|
||||
this.mindMap.select.clearAutoMoveTimer()
|
||||
this.mindMap.select.onMove(e.clientX, e.clientY)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始拖拽时初始化一些数据
|
||||
handleStartMove() {
|
||||
if (!this.isDragging) {
|
||||
this.isDragging = true
|
||||
// 鼠标按下的节点
|
||||
let node = this.mousedownNode
|
||||
// 计算鼠标按下的位置距离节点左上角的距离
|
||||
this.drawTransform = this.mindMap.draw.transform()
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
this.offsetX = this.mouseDownX - (node.left * scaleX + translateX)
|
||||
this.offsetY = this.mouseDownY - (node.top * scaleY + translateY)
|
||||
// 如果鼠标按下的节点是激活节点,那么保存当前所有激活的节点
|
||||
if (node.nodeData.data.isActive) {
|
||||
// 找出这些激活节点中的最顶层节点
|
||||
this.beingDragNodeList = getTopAncestorsFomNodeList(
|
||||
// 过滤掉根节点和概要节点
|
||||
this.mindMap.renderer.activeNodeList.filter(item => {
|
||||
return !item.isRoot && !item.isGeneralization
|
||||
})
|
||||
)
|
||||
} else {
|
||||
// 否则只拖拽按下的节点
|
||||
this.beingDragNodeList = [node]
|
||||
}
|
||||
// 将节点树转为节点数组
|
||||
this.nodeTreeToList()
|
||||
// 创建克隆节点
|
||||
this.createCloneNode()
|
||||
// 清除当前所有激活的节点
|
||||
this.mindMap.renderer.clearAllActive()
|
||||
}
|
||||
}
|
||||
|
||||
// 节点由树转换成数组,从子节点到根节点
|
||||
nodeTreeToList() {
|
||||
const list = []
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
// 过滤掉当前被拖拽的节点
|
||||
if (this.checkIsInBeingDragNodeList(node)) {
|
||||
return
|
||||
}
|
||||
if (!list[node.layerIndex]) {
|
||||
list[node.layerIndex] = []
|
||||
}
|
||||
list[node.layerIndex].push(node)
|
||||
})
|
||||
this.nodeList = list.reduceRight((res, cur) => {
|
||||
return [...res, ...cur]
|
||||
}, [])
|
||||
}
|
||||
|
||||
// 创建克隆节点
|
||||
createCloneNode() {
|
||||
if (!this.clone) {
|
||||
// 节点
|
||||
this.clone = this.node.group.clone()
|
||||
this.clone.opacity(0.5)
|
||||
const {
|
||||
dragMultiNodeRectConfig,
|
||||
dragPlaceholderRectFill,
|
||||
dragOpacityConfig
|
||||
} = this.mindMap.opt
|
||||
const {
|
||||
width: rectWidth,
|
||||
height: rectHeight,
|
||||
fill: rectFill
|
||||
} = dragMultiNodeRectConfig
|
||||
const node = this.beingDragNodeList[0]
|
||||
const lineColor = node.style.merge('lineColor', true)
|
||||
// 如果当前被拖拽的节点数量大于1,那么创建一个矩形示意
|
||||
if (this.beingDragNodeList.length > 1) {
|
||||
this.clone = this.draw
|
||||
.rect()
|
||||
.size(rectWidth, rectHeight)
|
||||
.radius(rectHeight / 2)
|
||||
.fill({
|
||||
color: rectFill || lineColor
|
||||
})
|
||||
this.offsetX = rectWidth / 2
|
||||
this.offsetY = rectHeight / 2
|
||||
} else {
|
||||
// 否则克隆当前的节点
|
||||
this.clone = node.group.clone()
|
||||
// 删除展开收起按钮元素
|
||||
const expandEl = this.clone.findOne('.smm-expand-btn')
|
||||
if (expandEl) {
|
||||
expandEl.remove()
|
||||
}
|
||||
this.mindMap.draw.add(this.clone)
|
||||
}
|
||||
this.clone.opacity(dragOpacityConfig.cloneNodeOpacity)
|
||||
this.clone.css('z-index', 99999)
|
||||
this.node.isDrag = true
|
||||
this.node.hide()
|
||||
// 连接线
|
||||
this.line = this.draw.path()
|
||||
this.line.opacity(0.5)
|
||||
this.node.styleLine(this.line, this.node)
|
||||
// 同级位置占位符
|
||||
// 同级位置提示元素
|
||||
this.placeholder = this.draw.rect().fill({
|
||||
color: this.node.style.merge('lineColor', true)
|
||||
color: dragPlaceholderRectFill || lineColor
|
||||
})
|
||||
// 当前被拖拽的节点的临时设置
|
||||
this.beingDragNodeList.forEach(node => {
|
||||
// 降低透明度
|
||||
node.setOpacity(dragOpacityConfig.beingDragNodeOpacity)
|
||||
// 隐藏连线及下级节点
|
||||
node.hideChildren()
|
||||
// 设置拖拽状态
|
||||
node.startDrag()
|
||||
})
|
||||
this.mindMap.draw.add(this.clone)
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,42 +304,9 @@ class Drag extends Base {
|
||||
return
|
||||
}
|
||||
this.clone.remove()
|
||||
this.line.remove()
|
||||
this.placeholder.remove()
|
||||
}
|
||||
|
||||
// 拖动中
|
||||
onMove(x, y, e) {
|
||||
if (!this.isMousedown) {
|
||||
return
|
||||
}
|
||||
this.createCloneNode()
|
||||
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
|
||||
this.cloneNodeLeft = x - this.offsetX
|
||||
this.cloneNodeTop = y - this.offsetY
|
||||
x = (this.cloneNodeLeft - translateX) / scaleX
|
||||
y = (this.cloneNodeTop - translateY) / scaleY
|
||||
let t = this.clone.transform()
|
||||
this.clone.translate(x - t.translateX, y - t.translateY)
|
||||
// 连接线
|
||||
let parent = this.node.parent
|
||||
this.line.plot(
|
||||
this.quadraticCurvePath(
|
||||
parent.left + parent.width / 2,
|
||||
parent.top + parent.height / 2,
|
||||
x + this.node.width / 2,
|
||||
y + this.node.height / 2
|
||||
)
|
||||
)
|
||||
this.checkOverlapNode()
|
||||
// 如果注册了多选节点插件,那么复用它的边缘自动移动画布功能
|
||||
if (this.mindMap.opt.autoMoveWhenMouseInEdgeOnDrag && this.mindMap.select) {
|
||||
this.drawTransform = this.mindMap.draw.transform()
|
||||
this.mindMap.select.clearAutoMoveTimer()
|
||||
this.mindMap.select.onMove(e.clientX, e.clientY)
|
||||
}
|
||||
}
|
||||
|
||||
// 检测重叠节点
|
||||
checkOverlapNode() {
|
||||
if (!this.drawTransform || !this.placeholder) {
|
||||
@ -226,9 +320,6 @@ class Drag extends Base {
|
||||
if (node.nodeData.data.isActive) {
|
||||
this.mindMap.renderer.setNodeActive(node, false)
|
||||
}
|
||||
if (node.uid === this.node.uid || this.node.isParent(node)) {
|
||||
return
|
||||
}
|
||||
if (this.overlapNode || (this.prevNode && this.nextNode)) {
|
||||
return
|
||||
}
|
||||
@ -484,7 +575,7 @@ class Drag extends Base {
|
||||
if (node.layerIndex === 1) {
|
||||
sameDir = item.dir === node.dir
|
||||
}
|
||||
return item !== this.node && sameDir
|
||||
return sameDir && !this.checkIsInBeingDragNodeList(item)
|
||||
})
|
||||
: []
|
||||
this.handleVerticalCheck(node, checkList)
|
||||
@ -535,7 +626,7 @@ class Drag extends Base {
|
||||
handleFishbone(node) {
|
||||
let checkList = node.parent
|
||||
? node.parent.children.filter(item => {
|
||||
return item !== this.node && item.layerIndex > 1
|
||||
return item.layerIndex > 1 && !this.checkIsInBeingDragNodeList(item)
|
||||
})
|
||||
: []
|
||||
if (node.layerIndex === 1) {
|
||||
@ -553,8 +644,8 @@ class Drag extends Base {
|
||||
// 获取节点的兄弟节点列表通用方法
|
||||
commonGetNodeCheckList(node) {
|
||||
return node.parent
|
||||
? node.parent.children.filter(item => {
|
||||
return item !== this.node
|
||||
? [...node.parent.children].filter(item => {
|
||||
return !this.checkIsInBeingDragNodeList(item)
|
||||
})
|
||||
: []
|
||||
}
|
||||
@ -585,18 +676,11 @@ class Drag extends Base {
|
||||
}
|
||||
}
|
||||
|
||||
// 节点由树转换成数组,从子节点到根节点
|
||||
nodeTreeToList() {
|
||||
const list = []
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
if (!list[node.layerIndex]) {
|
||||
list[node.layerIndex] = []
|
||||
}
|
||||
list[node.layerIndex].push(node)
|
||||
// 检查某个节点是否在被拖拽节点内
|
||||
checkIsInBeingDragNodeList(node) {
|
||||
return !!this.beingDragNodeList.find(item => {
|
||||
return item.uid === node.uid || item.isParent(node)
|
||||
})
|
||||
this.nodeList = list.reduceRight((res, cur) => {
|
||||
return [...res, ...cur]
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user