mirror of
https://github.com/wanglin2/mind-map.git
synced 2026-02-17 22:08:25 +08:00
1141 lines
31 KiB
JavaScript
1141 lines
31 KiB
JavaScript
import Style from './Style'
|
||
import Shape from './Shape'
|
||
import { G, Rect, Text, SVG } from '@svgdotjs/svg.js'
|
||
import nodeGeneralizationMethods from './nodeGeneralization'
|
||
import nodeExpandBtnMethods from './nodeExpandBtn'
|
||
import nodeCommandWrapsMethods from './nodeCommandWraps'
|
||
import nodeCreateContentsMethods from './nodeCreateContents'
|
||
import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect'
|
||
import nodeModifyWidthMethods from './nodeModifyWidth'
|
||
import nodeCooperateMethods from './nodeCooperate'
|
||
import quickCreateChildBtnMethods from './quickCreateChildBtn'
|
||
import nodeLayoutMethods from './nodeLayout'
|
||
import { CONSTANTS } from '../../../constants/constant'
|
||
import { copyNodeTree, createUid, addXmlns } from '../../../utils/index'
|
||
|
||
// 节点类
|
||
class MindMapNode {
|
||
// 构造函数
|
||
constructor(opt = {}) {
|
||
this.opt = opt
|
||
// 节点数据
|
||
this.nodeData = this.handleData(opt.data || {})
|
||
// 保存本次更新时的节点数据快照
|
||
this.nodeDataSnapshot = ''
|
||
// uid
|
||
this.uid = opt.uid
|
||
// 控制实例
|
||
this.mindMap = opt.mindMap
|
||
// 渲染实例
|
||
this.renderer = opt.renderer
|
||
// 渲染器
|
||
this.draw = this.mindMap.draw
|
||
this.nodeDraw = this.mindMap.nodeDraw
|
||
this.lineDraw = this.mindMap.lineDraw
|
||
// 样式实例
|
||
this.style = new Style(this)
|
||
// 节点当前生效的全部样式
|
||
this.effectiveStyles = {}
|
||
// 形状实例
|
||
this.shapeInstance = new Shape(this)
|
||
this.shapePadding = {
|
||
paddingX: 0,
|
||
paddingY: 0
|
||
}
|
||
// 是否是根节点
|
||
this.isRoot = opt.isRoot === undefined ? false : opt.isRoot
|
||
// 是否是概要节点
|
||
this.isGeneralization =
|
||
opt.isGeneralization === undefined ? false : opt.isGeneralization
|
||
this.generalizationBelongNode = null
|
||
// 节点层级
|
||
this.layerIndex = opt.layerIndex === undefined ? 0 : opt.layerIndex
|
||
// 节点宽
|
||
this.width = opt.width || 0
|
||
// 节点高
|
||
this.height = opt.height || 0
|
||
// 自定义文本的宽度
|
||
this.customTextWidth = opt.data.data.customTextWidth || undefined
|
||
// left
|
||
this._left = opt.left || 0
|
||
// top
|
||
this._top = opt.top || 0
|
||
// 自定义位置
|
||
this.customLeft = opt.data.data.customLeft || undefined
|
||
this.customTop = opt.data.data.customTop || undefined
|
||
// 是否正在拖拽中
|
||
this.isDrag = false
|
||
// 父节点
|
||
this.parent = opt.parent || null
|
||
// 子节点
|
||
this.children = opt.children || []
|
||
// 当前同时操作该节点的用户列表
|
||
this.userList = []
|
||
// 节点内容的容器
|
||
this.group = null
|
||
this.shapeNode = null // 节点形状节点
|
||
this.hoverNode = null // 节点hover和激活的节点
|
||
// 节点内容对象
|
||
this._customNodeContent = null
|
||
this._imgData = null
|
||
this._iconData = null
|
||
this._textData = null
|
||
this._hyperlinkData = null
|
||
this._tagData = null
|
||
this._noteData = null
|
||
this.noteEl = null
|
||
this.noteContentIsShow = false
|
||
this._attachmentData = null
|
||
this._prefixData = null
|
||
this._postfixData = null
|
||
this._expandBtn = null
|
||
this._lastExpandBtnType = null
|
||
this._showExpandBtn = false
|
||
this._openExpandNode = null
|
||
this._closeExpandNode = null
|
||
this._fillExpandNode = null
|
||
this._userListGroup = null
|
||
this._lines = []
|
||
this._generalizationList = []
|
||
this._unVisibleRectRegionNode = null
|
||
this._isMouseenter = false
|
||
this._customContentAddToNodeAdd = null
|
||
// 尺寸信息
|
||
this._rectInfo = {
|
||
textContentWidth: 0,
|
||
textContentHeight: 0,
|
||
textContentWidthWithoutTag: 0
|
||
}
|
||
// 概要节点的宽高
|
||
this._generalizationNodeWidth = 0
|
||
this._generalizationNodeHeight = 0
|
||
// 展开收缩按钮尺寸
|
||
this.expandBtnSize = this.mindMap.opt.expandBtnSize
|
||
// 是否是多选节点
|
||
this.isMultipleChoice = false
|
||
// 是否需要重新layout
|
||
this.needLayout = false
|
||
// 当前是否是隐藏状态
|
||
this.isHide = false
|
||
const proto = Object.getPrototypeOf(this)
|
||
if (!proto.bindEvent) {
|
||
// 节点尺寸计算和布局相关方法
|
||
Object.keys(nodeLayoutMethods).forEach(item => {
|
||
proto[item] = nodeLayoutMethods[item]
|
||
})
|
||
// 概要相关方法
|
||
Object.keys(nodeGeneralizationMethods).forEach(item => {
|
||
proto[item] = nodeGeneralizationMethods[item]
|
||
})
|
||
// 展开收起按钮相关方法
|
||
Object.keys(nodeExpandBtnMethods).forEach(item => {
|
||
proto[item] = nodeExpandBtnMethods[item]
|
||
})
|
||
// 展开收起按钮占位元素相关方法
|
||
Object.keys(nodeExpandBtnPlaceholderRectMethods).forEach(item => {
|
||
proto[item] = nodeExpandBtnPlaceholderRectMethods[item]
|
||
})
|
||
// 命令的相关方法
|
||
Object.keys(nodeCommandWrapsMethods).forEach(item => {
|
||
proto[item] = nodeCommandWrapsMethods[item]
|
||
})
|
||
// 创建节点内容的相关方法
|
||
Object.keys(nodeCreateContentsMethods).forEach(item => {
|
||
proto[item] = nodeCreateContentsMethods[item]
|
||
})
|
||
// 协同相关
|
||
if (this.mindMap.cooperate) {
|
||
Object.keys(nodeCooperateMethods).forEach(item => {
|
||
proto[item] = nodeCooperateMethods[item]
|
||
})
|
||
}
|
||
// 拖拽调整节点宽度
|
||
Object.keys(nodeModifyWidthMethods).forEach(item => {
|
||
proto[item] = nodeModifyWidthMethods[item]
|
||
})
|
||
// 快捷创建子节点按钮
|
||
if (this.mindMap.opt.isShowCreateChildBtnIcon) {
|
||
Object.keys(quickCreateChildBtnMethods).forEach(item => {
|
||
proto[item] = quickCreateChildBtnMethods[item]
|
||
})
|
||
this.initQuickCreateChildBtn()
|
||
}
|
||
proto.bindEvent = true
|
||
}
|
||
// 初始化
|
||
this.getSize()
|
||
// 初始需要计算一下概要节点的大小,否则计算布局时获取不到概要的大小
|
||
this.updateGeneralization()
|
||
this.initDragHandle()
|
||
}
|
||
|
||
// 支持自定义位置
|
||
get left() {
|
||
return this.customLeft || this._left
|
||
}
|
||
|
||
set left(val) {
|
||
this._left = val
|
||
}
|
||
|
||
get top() {
|
||
return this.customTop || this._top
|
||
}
|
||
|
||
set top(val) {
|
||
this._top = val
|
||
}
|
||
|
||
// 复位部分布局时会重新设置的数据
|
||
reset() {
|
||
this.children = []
|
||
this.parent = null
|
||
this.isRoot = false
|
||
this.layerIndex = 0
|
||
this.left = 0
|
||
this.top = 0
|
||
}
|
||
|
||
// 节点被删除时需要复位的数据
|
||
resetWhenDelete() {
|
||
this._isMouseenter = false
|
||
}
|
||
|
||
// 处理数据
|
||
handleData(data) {
|
||
data.data.expand = data.data.expand === false ? false : true
|
||
data.data.isActive = data.data.isActive === true ? true : false
|
||
data.children = data.children || []
|
||
return data
|
||
}
|
||
|
||
// 创建节点的各个内容对象数据
|
||
// recreateTypes:[] custom、image、icon、text、hyperlink、tag、note、attachment、numbers、prefix、postfix、checkbox
|
||
createNodeData(recreateTypes) {
|
||
// 自定义节点内容
|
||
const {
|
||
isUseCustomNodeContent,
|
||
customCreateNodeContent,
|
||
createNodePrefixContent,
|
||
createNodePostfixContent,
|
||
addCustomContentToNode
|
||
} = this.mindMap.opt
|
||
// 需要创建的内容类型
|
||
const typeList = [
|
||
'custom',
|
||
'image',
|
||
'icon',
|
||
'text',
|
||
'hyperlink',
|
||
'tag',
|
||
'note',
|
||
'attachment',
|
||
'prefix',
|
||
'postfix',
|
||
...this.mindMap.nodeInnerPrefixList.map(item => {
|
||
return item.name
|
||
})
|
||
]
|
||
const createTypes = {}
|
||
if (Array.isArray(recreateTypes)) {
|
||
// 重新创建指定的内容类型
|
||
typeList.forEach(item => {
|
||
if (recreateTypes.includes(item)) {
|
||
createTypes[item] = true
|
||
}
|
||
})
|
||
} else {
|
||
// 创建所有类型
|
||
typeList.forEach(item => {
|
||
createTypes[item] = true
|
||
})
|
||
}
|
||
if (
|
||
isUseCustomNodeContent &&
|
||
customCreateNodeContent &&
|
||
createTypes.custom
|
||
) {
|
||
this._customNodeContent = customCreateNodeContent(this)
|
||
}
|
||
// 如果没有返回内容,那么还是使用内置的节点内容
|
||
if (this._customNodeContent) {
|
||
addXmlns(this._customNodeContent)
|
||
return
|
||
}
|
||
if (createTypes.image) this._imgData = this.createImgNode()
|
||
if (createTypes.icon) this._iconData = this.createIconNode()
|
||
if (createTypes.text) this._textData = this.createTextNode()
|
||
if (createTypes.hyperlink) this._hyperlinkData = this.createHyperlinkNode()
|
||
if (createTypes.tag) this._tagData = this.createTagNode()
|
||
if (createTypes.note) this._noteData = this.createNoteNode()
|
||
if (createTypes.attachment)
|
||
this._attachmentData = this.createAttachmentNode()
|
||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||
if (createTypes[item.name]) {
|
||
this[`_${item.name}Data`] = item.createContent(this)
|
||
}
|
||
})
|
||
if (createTypes.prefix) {
|
||
this._prefixData = createNodePrefixContent
|
||
? createNodePrefixContent(this)
|
||
: null
|
||
if (this._prefixData && this._prefixData.el) {
|
||
addXmlns(this._prefixData.el)
|
||
}
|
||
}
|
||
if (createTypes.postfix) {
|
||
this._postfixData = createNodePostfixContent
|
||
? createNodePostfixContent(this)
|
||
: null
|
||
if (this._postfixData && this._postfixData.el) {
|
||
addXmlns(this._postfixData.el)
|
||
}
|
||
}
|
||
if (
|
||
addCustomContentToNode &&
|
||
typeof addCustomContentToNode.create === 'function'
|
||
) {
|
||
this._customContentAddToNodeAdd = addCustomContentToNode.create(this)
|
||
if (
|
||
this._customContentAddToNodeAdd &&
|
||
this._customContentAddToNodeAdd.el
|
||
) {
|
||
addXmlns(this._customContentAddToNodeAdd.el)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 计算节点的宽高
|
||
getSize(recreateTypes, opt = {}) {
|
||
const ignoreUpdateCustomTextWidth = opt.ignoreUpdateCustomTextWidth || false
|
||
if (!ignoreUpdateCustomTextWidth) {
|
||
this.customTextWidth = this.getData('customTextWidth') || undefined
|
||
}
|
||
this.customLeft = this.getData('customLeft') || undefined
|
||
this.customTop = this.getData('customTop') || undefined
|
||
// 这里不要更新概要,不然即使概要没修改,每次也会重新渲染
|
||
// this.updateGeneralization()
|
||
this.createNodeData(recreateTypes)
|
||
const { width, height } = this.getNodeRect()
|
||
// 判断节点尺寸是否有变化
|
||
const changed = this.width !== width || this.height !== height
|
||
this.width = width
|
||
this.height = height
|
||
return changed
|
||
}
|
||
|
||
// 给节点绑定事件
|
||
bindGroupEvent() {
|
||
// 单击事件,选中节点
|
||
this.group.on('click', e => {
|
||
this.mindMap.emit('node_click', this, e)
|
||
if (this.isMultipleChoice) {
|
||
e.stopPropagation()
|
||
this.isMultipleChoice = false
|
||
return
|
||
}
|
||
if (
|
||
this.mindMap.opt.onlyOneEnableActiveNodeOnCooperate &&
|
||
this.userList.length > 0
|
||
) {
|
||
return
|
||
}
|
||
this.active(e)
|
||
})
|
||
this.group.on('mousedown', e => {
|
||
const {
|
||
readonly,
|
||
enableCtrlKeyNodeSelection,
|
||
useLeftKeySelectionRightKeyDrag,
|
||
mousedownEventPreventDefault
|
||
} = this.mindMap.opt
|
||
if (mousedownEventPreventDefault) {
|
||
e.preventDefault()
|
||
}
|
||
// 只读模式不需要阻止冒泡
|
||
if (!readonly) {
|
||
if (this.isRoot) {
|
||
// 根节点,右键拖拽画布模式下不需要阻止冒泡
|
||
if (e.which === 3 && !useLeftKeySelectionRightKeyDrag) {
|
||
e.stopPropagation()
|
||
}
|
||
} else {
|
||
// 非根节点,且按下的是非鼠标中键,需要阻止事件冒泡
|
||
if (e.which !== 2) {
|
||
e.stopPropagation()
|
||
}
|
||
}
|
||
}
|
||
// 多选和取消多选
|
||
if (!readonly && (e.ctrlKey || e.metaKey) && enableCtrlKeyNodeSelection) {
|
||
this.isMultipleChoice = true
|
||
const isActive = this.getData('isActive')
|
||
if (!isActive)
|
||
this.mindMap.emit(
|
||
'before_node_active',
|
||
this,
|
||
this.renderer.activeNodeList
|
||
)
|
||
this.mindMap.renderer[
|
||
isActive ? 'removeNodeFromActiveList' : 'addNodeToActiveList'
|
||
](this, true)
|
||
this.renderer.emitNodeActiveEvent(isActive ? null : this)
|
||
}
|
||
this.mindMap.emit('node_mousedown', this, e)
|
||
})
|
||
this.group.on('mouseup', e => {
|
||
if (!this.isRoot && e.which !== 2 && !this.mindMap.opt.readonly) {
|
||
e.stopPropagation()
|
||
}
|
||
this.mindMap.emit('node_mouseup', this, e)
|
||
})
|
||
this.group.on('mouseenter', e => {
|
||
if (this.isDrag) return
|
||
this._isMouseenter = true
|
||
// 显示展开收起按钮
|
||
this.showExpandBtn()
|
||
if (this.isGeneralization) {
|
||
this.handleGeneralizationMouseenter()
|
||
}
|
||
this.mindMap.emit('node_mouseenter', this, e)
|
||
})
|
||
this.group.on('mouseleave', e => {
|
||
if (!this._isMouseenter) return
|
||
this._isMouseenter = false
|
||
this.hideExpandBtn()
|
||
if (this.isGeneralization) {
|
||
this.handleGeneralizationMouseleave()
|
||
}
|
||
this.mindMap.emit('node_mouseleave', this, e)
|
||
})
|
||
// 双击事件
|
||
this.group.on('dblclick', e => {
|
||
const { readonly, onlyOneEnableActiveNodeOnCooperate } = this.mindMap.opt
|
||
if (readonly || e.ctrlKey || e.metaKey) {
|
||
return
|
||
}
|
||
e.stopPropagation()
|
||
if (onlyOneEnableActiveNodeOnCooperate && this.userList.length > 0) {
|
||
return
|
||
}
|
||
this.mindMap.emit('node_dblclick', this, e)
|
||
})
|
||
// 右键菜单事件
|
||
this.group.on('contextmenu', e => {
|
||
const { readonly, useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
|
||
// Mac上按住ctrl键点击鼠标左键不知为何触发的是contextmenu事件
|
||
if (readonly || e.ctrlKey) {
|
||
return
|
||
}
|
||
e.stopPropagation()
|
||
e.preventDefault()
|
||
// 如果是多选节点结束,那么不要触发右键菜单事件
|
||
if (
|
||
this.mindMap.select &&
|
||
!useLeftKeySelectionRightKeyDrag &&
|
||
this.mindMap.select.hasSelectRange()
|
||
) {
|
||
return
|
||
}
|
||
// 如果有且只有当前节点激活了,那么不需要重新激活
|
||
if (
|
||
!(this.getData('isActive') && this.renderer.activeNodeList.length === 1)
|
||
) {
|
||
this.renderer.clearActiveNodeList()
|
||
this.active(e)
|
||
}
|
||
this.mindMap.emit('node_contextmenu', e, this)
|
||
})
|
||
}
|
||
|
||
// 激活节点
|
||
active(e) {
|
||
if (this.mindMap.opt.readonly) {
|
||
return
|
||
}
|
||
e && e.stopPropagation()
|
||
if (this.getData('isActive')) {
|
||
return
|
||
}
|
||
this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList)
|
||
this.renderer.clearActiveNodeList()
|
||
this.renderer.addNodeToActiveList(this, true)
|
||
this.renderer.emitNodeActiveEvent(this)
|
||
}
|
||
|
||
// 取消激活该节点
|
||
deactivate() {
|
||
this.mindMap.renderer.removeNodeFromActiveList(this)
|
||
this.mindMap.renderer.emitNodeActiveEvent()
|
||
}
|
||
|
||
// 更新节点
|
||
update(forceRender) {
|
||
if (!this.group) {
|
||
return
|
||
}
|
||
this.updateNodeActiveClass()
|
||
const {
|
||
alwaysShowExpandBtn,
|
||
notShowExpandBtn,
|
||
isShowCreateChildBtnIcon,
|
||
readonly
|
||
} = this.mindMap.opt
|
||
const childrenLength = this.getChildrenLength()
|
||
// 不显示展开收起按钮则不需要处理
|
||
if (!notShowExpandBtn) {
|
||
if (alwaysShowExpandBtn) {
|
||
// 需要移除展开收缩按钮
|
||
if (this._expandBtn && childrenLength <= 0) {
|
||
this.removeExpandBtn()
|
||
} else {
|
||
// 更新展开收起按钮
|
||
this.renderExpandBtn()
|
||
}
|
||
} else {
|
||
const { isActive, expand } = this.getData()
|
||
// 展开状态且非激活状态,且当前鼠标不在它上面,才隐藏
|
||
if (childrenLength <= 0) {
|
||
this.removeExpandBtn()
|
||
} else if (expand && !isActive && !this._isMouseenter) {
|
||
this.hideExpandBtn()
|
||
} else {
|
||
this.showExpandBtn()
|
||
}
|
||
}
|
||
}
|
||
// 更新快速创建子节点按钮
|
||
if (isShowCreateChildBtnIcon) {
|
||
if (childrenLength > 0) {
|
||
this.removeQuickCreateChildBtn()
|
||
} else {
|
||
const { isActive } = this.getData()
|
||
if (isActive) {
|
||
this.showQuickCreateChildBtn()
|
||
} else {
|
||
this.hideQuickCreateChildBtn()
|
||
}
|
||
}
|
||
}
|
||
// 更新拖拽手柄的显示与否
|
||
this.updateDragHandle()
|
||
// 更新概要
|
||
this.renderGeneralization(forceRender)
|
||
// 更新协同头像
|
||
if (this.updateUserListNode) this.updateUserListNode()
|
||
// 更新节点位置
|
||
const t = this.group.transform()
|
||
// 保存一份当前节点数据快照
|
||
this.nodeDataSnapshot = readonly ? '' : JSON.stringify(this.getData())
|
||
// 节点位置变化才更新,因为即使值没有变化属性设置操作也是耗时的
|
||
if (this.left !== t.translateX || this.top !== t.translateY) {
|
||
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
||
}
|
||
}
|
||
|
||
// 获取节点相当于画布的位置
|
||
getNodePosInClient(_left, _top) {
|
||
const drawTransform = this.mindMap.draw.transform()
|
||
const { scaleX, scaleY, translateX, translateY } = drawTransform
|
||
const left = _left * scaleX + translateX
|
||
const top = _top * scaleY + translateY
|
||
return {
|
||
left,
|
||
top
|
||
}
|
||
}
|
||
|
||
// 判断节点是否可见
|
||
checkIsInClient(padding = 0) {
|
||
const { left: nx, top: ny } = this.getNodePosInClient(this.left, this.top)
|
||
return (
|
||
nx + this.width > 0 - padding &&
|
||
ny + this.height > 0 - padding &&
|
||
nx < this.mindMap.width + padding &&
|
||
ny < this.mindMap.height + padding
|
||
)
|
||
}
|
||
|
||
// 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置
|
||
reRender(recreateTypes, opt) {
|
||
const sizeChange = this.getSize(recreateTypes, opt)
|
||
this.layout()
|
||
this.update()
|
||
return sizeChange
|
||
}
|
||
|
||
// 更新节点激活状态
|
||
updateNodeActiveClass() {
|
||
if (!this.group) return
|
||
const isActive = this.getData('isActive')
|
||
this.group[isActive ? 'addClass' : 'removeClass']('active')
|
||
}
|
||
|
||
// 根据是否激活更新节点
|
||
updateNodeByActive(active) {
|
||
if (this.group) {
|
||
const { isShowCreateChildBtnIcon } = this.mindMap.opt
|
||
// 切换激活状态,需要切换展开收起按钮的显隐
|
||
if (active) {
|
||
this.showExpandBtn()
|
||
if (isShowCreateChildBtnIcon) {
|
||
this.showQuickCreateChildBtn()
|
||
}
|
||
} else {
|
||
this.hideExpandBtn()
|
||
if (isShowCreateChildBtnIcon) {
|
||
this.hideQuickCreateChildBtn()
|
||
}
|
||
}
|
||
this.updateNodeActiveClass()
|
||
this.updateDragHandle()
|
||
}
|
||
}
|
||
|
||
// 递归渲染
|
||
// forceRender:强制渲染,无论是否处于画布可视区域
|
||
// async:异步渲染
|
||
render(callback = () => {}, forceRender = false, async = false) {
|
||
// 节点
|
||
// 重新渲染连线
|
||
this.renderLine()
|
||
const { openPerformance, performanceConfig } = this.mindMap.opt
|
||
// 强制渲染、或没有开启性能模式、或不在画布可视区域内不渲染节点内容
|
||
// 根节点不进行懒加载,始终渲染,因为滚动条插件依赖根节点进行计算
|
||
if (
|
||
forceRender ||
|
||
!openPerformance ||
|
||
this.checkIsInClient(performanceConfig.padding) ||
|
||
this.isRoot
|
||
) {
|
||
if (!this.group) {
|
||
// 创建组
|
||
this.group = new G()
|
||
this.group.addClass('smm-node')
|
||
this.group.css({
|
||
cursor: 'default'
|
||
})
|
||
this.bindGroupEvent()
|
||
this.nodeDraw.add(this.group)
|
||
this.layout()
|
||
this.update(forceRender)
|
||
} else {
|
||
if (!this.nodeDraw.has(this.group)) {
|
||
this.nodeDraw.add(this.group)
|
||
}
|
||
if (this.needLayout) {
|
||
this.needLayout = false
|
||
this.layout()
|
||
}
|
||
this.updateExpandBtnPlaceholderRect()
|
||
this.update(forceRender)
|
||
}
|
||
} else if (openPerformance && performanceConfig.removeNodeWhenOutCanvas) {
|
||
this.removeSelf()
|
||
}
|
||
// 子节点
|
||
if (
|
||
this.children &&
|
||
this.children.length &&
|
||
this.getData('expand') !== false
|
||
) {
|
||
let index = 0
|
||
this.children.forEach(item => {
|
||
const renderChild = () => {
|
||
item.render(
|
||
() => {
|
||
index++
|
||
if (index >= this.children.length) {
|
||
callback()
|
||
}
|
||
},
|
||
forceRender,
|
||
async
|
||
)
|
||
}
|
||
if (async) {
|
||
setTimeout(renderChild, 0)
|
||
} else {
|
||
renderChild()
|
||
}
|
||
})
|
||
} else {
|
||
callback()
|
||
}
|
||
// 手动插入的节点立即获得焦点并且开启编辑模式
|
||
if (this.nodeData.inserting) {
|
||
delete this.nodeData.inserting
|
||
this.active()
|
||
// setTimeout(() => {
|
||
this.mindMap.emit('node_dblclick', this, null, true)
|
||
// }, 0)
|
||
}
|
||
}
|
||
|
||
// 删除自身,只是从画布删除,节点容器还在,后续还可以重新插回画布
|
||
removeSelf() {
|
||
if (!this.group) return
|
||
this.group.remove()
|
||
this.removeGeneralization()
|
||
}
|
||
|
||
// 递归删除,只是从画布删除,节点容器还在,后续还可以重新插回画布
|
||
remove() {
|
||
if (!this.group) return
|
||
this.group.remove()
|
||
this.removeGeneralization()
|
||
this.removeLine()
|
||
// 子节点
|
||
if (this.children && this.children.length) {
|
||
this.children.forEach(item => {
|
||
item.remove()
|
||
})
|
||
}
|
||
}
|
||
|
||
// 销毁节点,不但会从画布删除,而且原节点直接置空,后续无法再插回画布
|
||
destroy() {
|
||
this.removeLine()
|
||
if (this.parent) {
|
||
this.parent.removeLine()
|
||
}
|
||
if (!this.group) return
|
||
if (this.emptyUser) {
|
||
this.emptyUser()
|
||
}
|
||
this.resetWhenDelete()
|
||
this.group.remove()
|
||
this.removeGeneralization()
|
||
this.group = null
|
||
this.style.onRemove()
|
||
}
|
||
|
||
// 隐藏节点
|
||
hide() {
|
||
if (this.group) this.group.hide()
|
||
this.hideGeneralization()
|
||
if (this.parent) {
|
||
const index = this.parent.children.indexOf(this)
|
||
this.parent._lines[index] && this.parent._lines[index].hide()
|
||
this._lines.forEach(item => {
|
||
item.hide()
|
||
})
|
||
}
|
||
// 子节点
|
||
if (this.children && this.children.length) {
|
||
this.children.forEach(item => {
|
||
item.hide()
|
||
})
|
||
}
|
||
}
|
||
|
||
// 显示节点
|
||
show() {
|
||
if (!this.group) {
|
||
return
|
||
}
|
||
this.group.show()
|
||
this.showGeneralization()
|
||
if (this.parent) {
|
||
const index = this.parent.children.indexOf(this)
|
||
this.parent._lines[index] && this.parent._lines[index].show()
|
||
this._lines.forEach(item => {
|
||
item.show()
|
||
})
|
||
}
|
||
// 子节点
|
||
if (this.children && this.children.length) {
|
||
this.children.forEach(item => {
|
||
item.show()
|
||
})
|
||
}
|
||
}
|
||
|
||
// 设置节点透明度
|
||
// 包括连接线和下级节点
|
||
setOpacity(val) {
|
||
// 自身及连线
|
||
if (this.group) this.group.opacity(val)
|
||
this._lines.forEach(line => {
|
||
line.opacity(val)
|
||
})
|
||
// 子节点
|
||
this.children.forEach(item => {
|
||
item.setOpacity(val)
|
||
})
|
||
// 概要节点
|
||
this.setGeneralizationOpacity(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
|
||
if (this.group) this.group.addClass('smm-node-dragging')
|
||
}
|
||
|
||
// 拖拽结束
|
||
endDrag() {
|
||
this.isDrag = false
|
||
if (this.group) this.group.removeClass('smm-node-dragging')
|
||
}
|
||
|
||
// 连线
|
||
renderLine(deep = false) {
|
||
if (this.getData('expand') === false) {
|
||
return
|
||
}
|
||
let childrenLen = this.getChildrenLength()
|
||
// 切换为鱼骨结构时,清空根节点和二级节点的连线
|
||
if (
|
||
[CONSTANTS.LAYOUT.FISHBONE, CONSTANTS.LAYOUT.RIGHT_FISHBONE].includes(
|
||
this.mindMap.opt.layout
|
||
) &&
|
||
(this.isRoot || this.layerIndex === 1)
|
||
) {
|
||
childrenLen = 0
|
||
}
|
||
if (childrenLen > this._lines.length) {
|
||
// 创建缺少的线
|
||
new Array(childrenLen - this._lines.length).fill(0).forEach(() => {
|
||
this._lines.push(this.lineDraw.path())
|
||
})
|
||
} else if (childrenLen < this._lines.length) {
|
||
// 删除多余的线
|
||
this._lines.slice(childrenLen).forEach(line => {
|
||
line.remove()
|
||
})
|
||
this._lines = this._lines.slice(0, childrenLen)
|
||
}
|
||
// 画线
|
||
this.renderer.layout.renderLine(
|
||
this,
|
||
this._lines,
|
||
(...args) => {
|
||
// 添加样式
|
||
this.styleLine(...args)
|
||
},
|
||
this.style.getStyle('lineStyle', true)
|
||
)
|
||
// 子级的连线也需要更新
|
||
if (deep && this.children && this.children.length > 0) {
|
||
this.children.forEach(item => {
|
||
item.renderLine(deep)
|
||
})
|
||
}
|
||
}
|
||
|
||
// 获取节点形状
|
||
getShape() {
|
||
// 节点使用功能横线风格的话不支持设置形状,直接使用默认的矩形
|
||
return this.mindMap.themeConfig.nodeUseLineStyle
|
||
? CONSTANTS.SHAPE.RECTANGLE
|
||
: this.style.getStyle('shape', false, false)
|
||
}
|
||
|
||
// 检查节点是否存在自定义数据
|
||
hasCustomPosition() {
|
||
return this.customLeft !== undefined && this.customTop !== undefined
|
||
}
|
||
|
||
// 检查节点是否存在自定义位置的祖先节点,包含自身
|
||
ancestorHasCustomPosition() {
|
||
let node = this
|
||
while (node) {
|
||
if (node.hasCustomPosition()) {
|
||
return true
|
||
}
|
||
node = node.parent
|
||
}
|
||
return false
|
||
}
|
||
|
||
// 检查是否存在有概要的祖先节点
|
||
ancestorHasGeneralization() {
|
||
let node = this.parent
|
||
while (node) {
|
||
if (node.checkHasGeneralization()) {
|
||
return true
|
||
}
|
||
node = node.parent
|
||
}
|
||
return false
|
||
}
|
||
|
||
// 添加子节点
|
||
addChildren(node) {
|
||
this.children.push(node)
|
||
}
|
||
|
||
// 设置连线样式
|
||
styleLine(line, childNode, enableMarker) {
|
||
const { enableInheritAncestorLineStyle } = this.mindMap.opt
|
||
const getName = enableInheritAncestorLineStyle
|
||
? 'getSelfInhertStyle'
|
||
: 'getSelfStyle'
|
||
const width =
|
||
childNode[getName]('lineWidth') || childNode.getStyle('lineWidth', true)
|
||
const color =
|
||
childNode[getName]('lineColor') ||
|
||
this.getRainbowLineColor(childNode) ||
|
||
childNode.getStyle('lineColor', true)
|
||
const dasharray =
|
||
childNode[getName]('lineDasharray') ||
|
||
childNode.getStyle('lineDasharray', true)
|
||
this.style.line(
|
||
line,
|
||
{
|
||
width,
|
||
color,
|
||
dasharray
|
||
},
|
||
enableMarker,
|
||
childNode
|
||
)
|
||
}
|
||
|
||
// 获取彩虹线条颜色
|
||
getRainbowLineColor(node) {
|
||
return this.mindMap.rainbowLines
|
||
? this.mindMap.rainbowLines.getNodeColor(node)
|
||
: ''
|
||
}
|
||
|
||
// 移除连线
|
||
removeLine() {
|
||
this._lines.forEach(line => {
|
||
line.remove()
|
||
})
|
||
this._lines = []
|
||
}
|
||
|
||
// 检测当前节点是否是某个节点的祖先节点
|
||
isAncestor(node) {
|
||
if (this.uid === node.uid) {
|
||
return false
|
||
}
|
||
let parent = node.parent
|
||
while (parent) {
|
||
if (this.uid === parent.uid) {
|
||
return true
|
||
}
|
||
parent = parent.parent
|
||
}
|
||
return false
|
||
}
|
||
|
||
// 检查当前节点是否是某个节点的父节点
|
||
isParent(node) {
|
||
if (this.uid === node.uid) {
|
||
return false
|
||
}
|
||
const parent = node.parent
|
||
if (parent && this.uid === parent.uid) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// 检测当前节点是否是某个节点的兄弟节点
|
||
isBrother(node) {
|
||
if (!this.parent || this.uid === node.uid) {
|
||
return false
|
||
}
|
||
return this.parent.children.find(item => {
|
||
return item.uid === node.uid
|
||
})
|
||
}
|
||
|
||
// 获取该节点在兄弟节点列表中的索引
|
||
getIndexInBrothers() {
|
||
return this.parent && this.parent.children
|
||
? this.parent.children.findIndex(item => {
|
||
return item.uid === this.uid
|
||
})
|
||
: -1
|
||
}
|
||
|
||
// 获取padding值
|
||
getPaddingVale() {
|
||
return {
|
||
paddingX: this.getStyle('paddingX'),
|
||
paddingY: this.getStyle('paddingY')
|
||
}
|
||
}
|
||
|
||
// 获取某个样式
|
||
getStyle(prop, root) {
|
||
const v = this.style.merge(prop, root)
|
||
return v === undefined ? '' : v
|
||
}
|
||
|
||
// 获取自定义样式
|
||
getSelfStyle(prop) {
|
||
return this.style.getSelfStyle(prop)
|
||
}
|
||
|
||
// 获取最近一个存在自身自定义样式的祖先节点的自定义样式
|
||
getParentSelfStyle(prop) {
|
||
if (this.parent) {
|
||
return (
|
||
this.parent.getSelfStyle(prop) || this.parent.getParentSelfStyle(prop)
|
||
)
|
||
}
|
||
return null
|
||
}
|
||
|
||
// 获取自身可继承的自定义样式
|
||
getSelfInhertStyle(prop) {
|
||
return (
|
||
this.getSelfStyle(prop) || // 自身
|
||
this.getParentSelfStyle(prop)
|
||
) // 父级
|
||
}
|
||
|
||
// 获取节点非节点状态的边框大小
|
||
getBorderWidth() {
|
||
return this.style.merge('borderWidth', false) || 0
|
||
}
|
||
|
||
// 获取数据
|
||
getData(key) {
|
||
return key ? this.nodeData.data[key] : this.nodeData.data
|
||
}
|
||
|
||
// 获取该节点的纯数据,即不包含对节点实例的引用
|
||
getPureData(removeActiveState = true, removeId = false) {
|
||
return copyNodeTree({}, this, removeActiveState, removeId)
|
||
}
|
||
|
||
// 获取祖先节点列表
|
||
getAncestorNodes() {
|
||
const list = []
|
||
let parent = this.parent
|
||
while (parent) {
|
||
list.unshift(parent)
|
||
parent = parent.parent
|
||
}
|
||
return list
|
||
}
|
||
|
||
// 是否存在自定义样式
|
||
hasCustomStyle() {
|
||
return this.style.hasCustomStyle()
|
||
}
|
||
|
||
// 获取节点的尺寸和位置信息,宽高是应用了缩放效果后的实际宽高,位置是相对于浏览器窗口左上角的位置
|
||
getRect() {
|
||
return this.group ? this.group.rbox() : null
|
||
}
|
||
|
||
// 获取节点的尺寸和位置信息,宽高是应用了缩放效果后的实际宽高,位置信息相对于画布
|
||
getRectInSvg() {
|
||
const { scaleX, scaleY, translateX, translateY } =
|
||
this.mindMap.draw.transform()
|
||
let { left, top, width, height } = this
|
||
const right = (left + width) * scaleX + translateX
|
||
const bottom = (top + height) * scaleY + translateY
|
||
left = left * scaleX + translateX
|
||
top = top * scaleY + translateY
|
||
return {
|
||
left,
|
||
right,
|
||
top,
|
||
bottom,
|
||
width: width * scaleX,
|
||
height: height * scaleY
|
||
}
|
||
}
|
||
|
||
// 高亮节点
|
||
highlight() {
|
||
if (this.group) this.group.addClass('smm-node-highlight')
|
||
}
|
||
|
||
// 取消高亮节点
|
||
closeHighlight() {
|
||
if (this.group) this.group.removeClass('smm-node-highlight')
|
||
}
|
||
|
||
// 伪克隆节点
|
||
// 克隆出的节点并不能真正当做一个节点使用
|
||
fakeClone() {
|
||
const newNode = new MindMapNode({
|
||
...this.opt,
|
||
uid: createUid()
|
||
})
|
||
Object.keys(this).forEach(item => {
|
||
newNode[item] = this[item]
|
||
})
|
||
return newNode
|
||
}
|
||
|
||
// 创建SVG文本节点
|
||
createSvgTextNode(text = '') {
|
||
return new Text().text(text)
|
||
}
|
||
|
||
// 获取SVG.js库的一些对象
|
||
getSvgObjects() {
|
||
return {
|
||
SVG,
|
||
G,
|
||
Rect
|
||
}
|
||
}
|
||
|
||
// 检查是否支持拖拽调整宽度
|
||
// 1.富文本模式
|
||
// 2.自定义节点内容
|
||
checkEnableDragModifyNodeWidth() {
|
||
const {
|
||
enableDragModifyNodeWidth,
|
||
isUseCustomNodeContent,
|
||
customCreateNodeContent
|
||
} = this.mindMap.opt
|
||
return (
|
||
enableDragModifyNodeWidth &&
|
||
(this.mindMap.richText ||
|
||
(isUseCustomNodeContent && customCreateNodeContent))
|
||
)
|
||
}
|
||
|
||
// 是否存在自定义宽度
|
||
hasCustomWidth() {
|
||
return (
|
||
this.checkEnableDragModifyNodeWidth() &&
|
||
this.customTextWidth !== undefined
|
||
)
|
||
}
|
||
|
||
// 获取子节点的数量
|
||
getChildrenLength() {
|
||
return this.nodeData.children ? this.nodeData.children.length : 0
|
||
}
|
||
}
|
||
|
||
export default MindMapNode
|