import Style from './Style' import Shape from './Shape' import { G, Rect, Text } 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 nodeCooperateMethods from './nodeCooperate' import { CONSTANTS } from '../../../constants/constant' import { copyNodeTree, createForeignObjectNode, createUid, addXmlns } from '../../../utils/index' // 节点类 class MindMapNode { // 构造函数 constructor(opt = {}) { this.opt = opt // 节点数据 this.nodeData = this.handleData(opt.data || {}) // 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 // 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._numberData = 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._rectInfo = { imgContentWidth: 0, imgContentHeight: 0, textContentWidth: 0, textContentHeight: 0 } // 概要节点的宽高 this._generalizationNodeWidth = 0 this._generalizationNodeHeight = 0 // 编号字符 this.number = opt.number || '' // 各种文字信息的间距 this.textContentItemMargin = this.mindMap.opt.textContentMargin // 图片和文字节点的间距 this.blockContentMargin = this.mindMap.opt.imgTextMargin // 展开收缩按钮尺寸 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(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] }) } proto.bindEvent = true } // 初始化 this.getSize() } // 支持自定义位置 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 } // 创建节点的各个内容对象数据 createNodeData() { // 自定义节点内容 let { isUseCustomNodeContent, customCreateNodeContent, createNodePrefixContent, createNodePostfixContent } = this.mindMap.opt if (isUseCustomNodeContent && customCreateNodeContent) { this._customNodeContent = customCreateNodeContent(this) } // 如果没有返回内容,那么还是使用内置的节点内容 if (this._customNodeContent) { addXmlns(this._customNodeContent) return } this._imgData = this.createImgNode() this._iconData = this.createIconNode() this._textData = this.createTextNode() this._hyperlinkData = this.createHyperlinkNode() this._tagData = this.createTagNode() this._noteData = this.createNoteNode() this._attachmentData = this.createAttachmentNode() if (this.mindMap.numbers) { this._numberData = this.mindMap.numbers.createNumberContent(this) } this._prefixData = createNodePrefixContent ? createNodePrefixContent(this) : null if (this._prefixData && this._prefixData.el) { addXmlns(this._prefixData.el) } this._postfixData = createNodePostfixContent ? createNodePostfixContent(this) : null if (this._postfixData && this._postfixData.el) { addXmlns(this._postfixData.el) } } // 计算节点的宽高 getSize() { this.customLeft = this.getData('customLeft') || undefined this.customTop = this.getData('customTop') || undefined // 这里不要更新概要,不然即使概要没修改,每次也会重新渲染 // this.updateGeneralization() this.createNodeData() let { width, height } = this.getNodeRect() // 判断节点尺寸是否有变化 let changed = this.width !== width || this.height !== height this.width = width this.height = height return changed } // 计算节点尺寸信息 getNodeRect() { // 自定义节点内容 if (this.isUseCustomNodeContent()) { let rect = this.measureCustomNodeContentSize(this._customNodeContent) return { width: rect.width, height: rect.height } } const { tagPosition } = this.mindMap.opt const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM // 宽高 let imgContentWidth = 0 let imgContentHeight = 0 let textContentWidth = 0 let textContentHeight = 0 let tagContentWidth = 0 let tagContentHeight = 0 // 存在图片 if (this._imgData) { this._rectInfo.imgContentWidth = imgContentWidth = this._imgData.width this._rectInfo.imgContentHeight = imgContentHeight = this._imgData.height } // 编号内容 if (this._numberData) { textContentWidth += this._numberData.width textContentHeight = Math.max(textContentHeight, this._numberData.height) } // 自定义前置内容 if (this._prefixData) { textContentWidth += this._prefixData.width textContentHeight = Math.max(textContentHeight, this._prefixData.height) } // 图标 if (this._iconData.length > 0) { textContentWidth += this._iconData.reduce((sum, cur) => { textContentHeight = Math.max(textContentHeight, cur.height) return (sum += cur.width + this.textContentItemMargin) }, 0) } // 文字 if (this._textData) { textContentWidth += this._textData.width textContentHeight = Math.max(textContentHeight, this._textData.height) } // 超链接 if (this._hyperlinkData) { textContentWidth += this._hyperlinkData.width textContentHeight = Math.max( textContentHeight, this._hyperlinkData.height ) } // 标签 if (this._tagData.length > 0) { let maxTagHeight = 0 const totalTagWidth = this._tagData.reduce((sum, cur) => { maxTagHeight = Math.max(maxTagHeight, cur.height) return (sum += cur.width + this.textContentItemMargin) }, 0) if (tagIsBottom) { // 文字下方 tagContentWidth = totalTagWidth tagContentHeight = maxTagHeight } else { // 否则在右侧 textContentWidth += totalTagWidth textContentHeight = Math.max(textContentHeight, maxTagHeight) } } // 备注 if (this._noteData) { textContentWidth += this._noteData.width textContentHeight = Math.max(textContentHeight, this._noteData.height) } // 附件 if (this._attachmentData) { textContentWidth += this._attachmentData.width textContentHeight = Math.max( textContentHeight, this._attachmentData.height ) } // 自定义后置内容 if (this._postfixData) { textContentWidth += this._postfixData.width textContentHeight = Math.max(textContentHeight, this._postfixData.height) } // 文字内容部分的尺寸 this._rectInfo.textContentWidth = textContentWidth this._rectInfo.textContentHeight = textContentHeight // 间距 let margin = imgContentHeight > 0 && textContentHeight > 0 ? this.blockContentMargin : 0 let { paddingX, paddingY } = this.getPaddingVale() // 纯内容宽高 let _width = Math.max(imgContentWidth, textContentWidth) let _height = imgContentHeight + textContentHeight // 如果标签在文字下方 if (tagIsBottom && tagContentHeight > 0 && textContentHeight > 0) { // 那么文字和标签之间也需要间距 margin += this.blockContentMargin // 整体高度要考虑标签宽度 _width = Math.max(_width, tagContentWidth) // 整体高度要加上标签的高度 _height += tagContentHeight } // 计算节点形状需要的附加内边距 let { paddingX: shapePaddingX, paddingY: shapePaddingY } = this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY) this.shapePadding.paddingX = shapePaddingX this.shapePadding.paddingY = shapePaddingY // 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点 const borderWidth = this.getBorderWidth() return { width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth, height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth } } // 定位节点内容 layout() { if (!this.group) return // 清除之前的内容 this.group.clear() const { hoverRectPadding, tagPosition } = this.mindMap.opt let { width, height, textContentItemMargin } = this let { paddingY } = this.getPaddingVale() const halfBorderWidth = this.getBorderWidth() / 2 paddingY += this.shapePadding.paddingY + halfBorderWidth // 节点形状 this.shapeNode = this.shapeInstance.createShape() this.shapeNode.addClass('smm-node-shape') this.shapeNode.translate(halfBorderWidth, halfBorderWidth) this.style.shape(this.shapeNode) this.group.add(this.shapeNode) // 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示 this.renderExpandBtnPlaceholderRect() // 创建协同头像节点 if (this.createUserListNode) this.createUserListNode() // 概要节点添加一个带所属节点id的类名 if (this.isGeneralization && this.generalizationBelongNode) { this.group.addClass('generalization_' + this.generalizationBelongNode.uid) } // 激活hover和激活边框 const addHoverNode = () => { this.hoverNode = new Rect() .size(width + hoverRectPadding * 2, height + hoverRectPadding * 2) .x(-hoverRectPadding) .y(-hoverRectPadding) this.hoverNode.addClass('smm-hover-node') this.style.hoverNode(this.hoverNode, width, height) this.group.add(this.hoverNode) } // 如果存在自定义节点内容,那么使用自定义节点内容 if (this.isUseCustomNodeContent()) { const foreignObject = createForeignObjectNode({ el: this._customNodeContent, width, height }) this.group.add(foreignObject) addHoverNode() return } const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM const { textContentHeight } = this._rectInfo // 图片节点 let imgHeight = 0 if (this._imgData) { imgHeight = this._imgData.height this.group.add(this._imgData.node) this._imgData.node.cx(width / 2).y(paddingY) } // 内容节点 let textContentNested = new G() let textContentOffsetX = 0 // 编号内容 if (this._numberData) { this._numberData.node .x(textContentOffsetX) .y((textContentHeight - this._numberData.height) / 2) textContentNested.add(this._numberData.node) textContentOffsetX += this._numberData.width + textContentItemMargin } // 自定义前置内容 if (this._prefixData) { const foreignObject = createForeignObjectNode({ el: this._prefixData.el, width: this._prefixData.width, height: this._prefixData.height }) foreignObject .x(textContentOffsetX) .y((textContentHeight - this._prefixData.height) / 2) textContentNested.add(foreignObject) textContentOffsetX += this._prefixData.width + textContentItemMargin } // icon let iconNested = new G() if (this._iconData && this._iconData.length > 0) { let iconLeft = 0 this._iconData.forEach(item => { item.node .x(textContentOffsetX + iconLeft) .y((textContentHeight - item.height) / 2) iconNested.add(item.node) iconLeft += item.width + textContentItemMargin }) textContentNested.add(iconNested) textContentOffsetX += iconLeft } // 文字 if (this._textData) { const oldX = this._textData.node.attr('data-offsetx') || 0 this._textData.node.attr('data-offsetx', textContentOffsetX) // 修复safari浏览器节点存在图标时文字位置不正确的问题 ;(this._textData.nodeContent || this._textData.node) .x(-oldX) // 修复非富文本模式下同时存在图标和换行的文本时,被收起和展开时图标与文字距离会逐渐拉大的问题 .x(textContentOffsetX) .y((textContentHeight - this._textData.height) / 2) textContentNested.add(this._textData.node) textContentOffsetX += this._textData.width + textContentItemMargin } // 超链接 if (this._hyperlinkData) { this._hyperlinkData.node .x(textContentOffsetX) .y((textContentHeight - this._hyperlinkData.height) / 2) textContentNested.add(this._hyperlinkData.node) textContentOffsetX += this._hyperlinkData.width + textContentItemMargin } // 标签 let tagNested = new G() if (this._tagData && this._tagData.length > 0) { if (tagIsBottom) { // 标签显示在文字下方 let tagLeft = 0 this._tagData.forEach(item => { item.node.x(tagLeft).y(0) tagNested.add(item.node) tagLeft += item.width + textContentItemMargin }) tagNested.cx(width / 2).y( paddingY + // 内边距 imgHeight + // 图片高度 textContentHeight + // 文本区域高度 (imgHeight > 0 && textContentHeight > 0 ? this.blockContentMargin : 0) + // 图片和文本之间的间距 this.blockContentMargin // 标签和文本之间的间距 ) this.group.add(tagNested) } else { // 标签显示在文字右侧 let tagLeft = 0 this._tagData.forEach(item => { item.node .x(textContentOffsetX + tagLeft) .y((textContentHeight - item.height) / 2) tagNested.add(item.node) tagLeft += item.width + textContentItemMargin }) textContentNested.add(tagNested) textContentOffsetX += tagLeft } } // 备注 if (this._noteData) { this._noteData.node .x(textContentOffsetX) .y((textContentHeight - this._noteData.height) / 2) textContentNested.add(this._noteData.node) textContentOffsetX += this._noteData.width } // 附件 if (this._attachmentData) { this._attachmentData.node .x(textContentOffsetX) .y((textContentHeight - this._attachmentData.height) / 2) textContentNested.add(this._attachmentData.node) textContentOffsetX += this._attachmentData.width } // 自定义后置内容 if (this._postfixData) { const foreignObject = createForeignObjectNode({ el: this._postfixData.el, width: this._postfixData.width, height: this._postfixData.height }) foreignObject .x(textContentOffsetX) .y((textContentHeight - this._postfixData.height) / 2) textContentNested.add(foreignObject) textContentOffsetX += this._postfixData.width } this.group.add(textContentNested) // 文字内容整体 textContentNested.translate( width / 2 - textContentNested.bbox().width / 2, paddingY + // 内边距 imgHeight + // 图片高度 (imgHeight > 0 && textContentHeight > 0 ? this.blockContentMargin : 0) // 和图片的间距 ) addHoverNode() this.mindMap.emit('node_layout_end', this) } // 给节点绑定事件 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 => { e.preventDefault() const { readonly, enableCtrlKeyNodeSelection, useLeftKeySelectionRightKeyDrag } = this.mindMap.opt // 只读模式不需要阻止冒泡 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 let 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 } = this.mindMap.opt // 不显示展开收起按钮则不需要处理 if (!notShowExpandBtn) { const childrenLength = this.nodeData.children.length if (alwaysShowExpandBtn) { // 需要移除展开收缩按钮 if (this._expandBtn && childrenLength <= 0) { this.removeExpandBtn() } else { // 更新展开收起按钮 this.renderExpandBtn() } } else { let { isActive, expand } = this.getData() // 展开状态且非激活状态,且当前鼠标不在它上面,才隐藏 if (childrenLength <= 0) { this.removeExpandBtn() } else if (expand && !isActive && !this._isMouseenter) { this.hideExpandBtn() } else { this.showExpandBtn() } } } // 更新概要 this.renderGeneralization(forceRender) // 更新协同头像 if (this.updateUserListNode) this.updateUserListNode() // 更新节点位置 let t = this.group.transform() // 如果节点位置没有变化,则返回 if (this.left === t.translateX && this.top === t.translateY) return this.group.translate(this.left - t.translateX, this.top - t.translateY) } // 获取节点相当于画布的位置 getNodePosInClient(_left, _top) { let drawTransform = this.mindMap.draw.transform() let { scaleX, scaleY, translateX, translateY } = drawTransform let left = _left * scaleX + translateX let 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() { let sizeChange = this.getSize() 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) { // 切换激活状态,需要切换展开收起按钮的显隐 if (active) { this.showExpandBtn() } else { this.hideExpandBtn() } this.updateNodeActiveClass() } } // 递归渲染 // 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() { this.group.hide() this.hideGeneralization() if (this.parent) { let 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) { let 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) { // 自身及连线 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 this.group.addClass('smm-node-dragging') } // 拖拽结束 endDrag() { this.isDrag = false this.group.removeClass('smm-node-dragging') } // 连线 renderLine(deep = false) { if (this.getData('expand') === false) { return } let childrenLen = this.nodeData.children.length // 切换为鱼骨结构时,清空根节点和二级节点的连线 if ( this.mindMap.opt.layout === CONSTANTS.LAYOUT.FISHBONE && (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 width = childNode.getSelfInhertStyle('lineWidth') || childNode.getStyle('lineWidth', true) const color = childNode.getSelfInhertStyle('lineColor') || this.getRainbowLineColor(childNode) || childNode.getStyle('lineColor', true) const dasharray = childNode.getSelfInhertStyle('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) { let 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.rbox() } // 获取节点的尺寸和位置信息,宽高是应用了缩放效果后的实际宽高,位置信息相对于画布 getRectInSvg() { let { scaleX, scaleY, translateX, translateY } = this.mindMap.draw.transform() let { left, top, width, height } = this let right = (left + width) * scaleX + translateX let 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) } } export default MindMapNode