UNPKG

leo-mind-map

Version:

一个简单的web在线思维导图

568 lines (542 loc) 16.9 kB
import Base from './Base' import { walk, asyncRun, degToRad, getNodeIndexInNodeList } from '../utils' import { CONSTANTS } from '../constants/constant' import utils from './fishboneUtils' import { SVG } from '@svgdotjs/svg.js' import { shapeStyleProps } from '../core/render/node/Style' // 鱼骨图 class Fishbone extends Base { // 构造函数 constructor(opt = {}, layout) { super(opt) this.layout = layout this.indent = 0.3 this.childIndent = 0.5 this.fishTail = null this.maxx = 0 this.headRatio = 1 this.tailRatio = 0.6 this.paddingXRatio = 0.3 this.fishHeadPathStr = 'M4,181 C4,181, 0,177, 4,173 Q 96.09523809523809,0, 288.2857142857143,0 L 288.2857142857143,354 Q 48.047619047619044,354, 8,218.18367346938777 C8,218.18367346938777, 6,214.18367346938777, 8,214.18367346938777 L 41.183673469387756,214.18367346938777 Z' this.fishTailPathStr = 'M 606.9342905223708 0 Q 713.1342905223709 -177 819.3342905223708 -177 L 766.2342905223709 0 L 819.3342905223708 177 Q 713.1342905223709 177 606.9342905223708 0 z' this.bindEvent() this.extendShape() this.beforeChange = this.beforeChange.bind(this) } // 重新渲染时,节点连线是否全部删除 // 鱼尾鱼骨图会多渲染一些连线,按需删除无法删除掉,只能全部删除重新创建 nodeIsRemoveAllLines(node) { return node.isRoot || node.layerIndex === 1 } // 是否是带鱼头鱼尾的鱼骨图 isFishbone2() { return this.layout === CONSTANTS.LAYOUT.FISHBONE2 } bindEvent() { if (!this.isFishbone2()) return this.onCheckUpdateFishTail = this.onCheckUpdateFishTail.bind(this) this.mindMap.on('afterExecCommand', this.onCheckUpdateFishTail) } unBindEvent() { this.mindMap.off('afterExecCommand', this.onCheckUpdateFishTail) } // 扩展节点形状 extendShape() { if (!this.isFishbone2()) return // 扩展鱼头形状 this.mindMap.addShape({ name: 'fishHead', createShape: node => { const rect = SVG(`<path d="${this.fishHeadPathStr}"></path>`) const { width, height } = node.shapeInstance.getNodeSize() rect.size(width, height) return rect }, getPadding: ({ width, height, paddingX, paddingY }) => { width += paddingX * 2 height += paddingY * 2 let shapePaddingX = this.paddingXRatio * width let shapePaddingY = 0 width += shapePaddingX * 2 const newHeight = width / this.headRatio shapePaddingY = (newHeight - height) / 2 return { paddingX: shapePaddingX, paddingY: shapePaddingY } } }) } // 布局 doLayout(callback) { let task = [ () => { this.computedBaseValue() this.addFishTail() }, () => { this.computedLeftTopValue() }, () => { this.adjustLeftTopValue() this.updateFishTailPosition() }, () => { callback(this.root) } ] asyncRun(task) } // 创建鱼尾 addFishTail() { if (!this.isFishbone2()) return const exist = this.mindMap.lineDraw.findOne('.smm-layout-fishbone-tail') if (!exist) { this.fishTail = SVG(`<path d="${this.fishTailPathStr}"></path>`) this.fishTail.addClass('smm-layout-fishbone-tail') } else { this.fishTail = exist } const tailHeight = this.root.height const tailWidth = tailHeight * this.tailRatio this.fishTail.size(tailWidth, tailHeight) this.styleFishTail() this.mindMap.lineDraw.add(this.fishTail) } // 如果根节点更新了形状样式,那么鱼尾也要更新 onCheckUpdateFishTail(name, node, data) { if (name === 'SET_NODE_DATA') { let hasShapeProp = false Object.keys(data).forEach(key => { if (shapeStyleProps.includes(key)) { hasShapeProp = true } }) if (hasShapeProp) { this.styleFishTail() } } } styleFishTail() { this.root.style.shape(this.fishTail) } // 删除鱼尾 removeFishTail() { const exist = this.mindMap.lineDraw.findOne('.smm-layout-fishbone-tail') if (exist) { exist.remove() } } // 更新鱼尾形状位置 updateFishTailPosition() { if (!this.isFishbone2()) return this.fishTail.x(this.maxx).cy(this.root.top + this.root.height / 2) } // 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值 computedBaseValue() { walk( this.renderer.renderTree, null, (node, parent, isRoot, layerIndex, index, ancestors) => { if (isRoot && this.isFishbone2()) { // 将根节点形状强制修改为鱼头 node.data.shape = 'fishHead' } // 创建节点 let newNode = this.createNode( node, parent, isRoot, layerIndex, index, ancestors ) // 根节点定位在画布中心位置 if (isRoot) { this.setNodeCenter(newNode) } else { // 非根节点 // 三级及以下节点以上级方向为准 if (parent._node.dir) { newNode.dir = parent._node.dir } else { // 节点生长方向 newNode.dir = index % 2 === 0 ? CONSTANTS.LAYOUT_GROW_DIR.TOP : CONSTANTS.LAYOUT_GROW_DIR.BOTTOM } // 计算二级节点的top值 if (parent._node.isRoot) { let marginY = this.getMarginY(layerIndex) // 带鱼头鱼尾的鱼骨图因为根节点高度比较大,所以二级节点需要向中间靠一点 const topOffset = this.isFishbone2() ? parent._node.height / 4 : 0 if (this.checkIsTop(newNode)) { newNode.top = parent._node.top - newNode.height - marginY + topOffset } else { newNode.top = parent._node.top + parent._node.height + marginY - topOffset } } } if (!node.data.expand) { return true } }, null, true, 0 ) } // 遍历节点树计算节点的left、top computedLeftTopValue() { walk( this.root, null, (node, parent, isRoot, layerIndex) => { if (node.isRoot) { let marginX = this.getMarginX(layerIndex + 1) const heightOffsetRatio = this.isFishbone2() ? 2 : 1 let topTotalLeft = node.left + node.width + node.height / heightOffsetRatio + marginX let bottomTotalLeft = node.left + node.width + node.height / heightOffsetRatio + marginX node.children.forEach(item => { if (this.checkIsTop(item)) { item.left = topTotalLeft topTotalLeft += item.width + marginX } else { item.left = bottomTotalLeft + 20 bottomTotalLeft += item.width + marginX } }) } let params = { layerIndex, node, ctx: this } if (this.checkIsTop(node)) { utils.top.computedLeftTopValue(params) } else { utils.bottom.computedLeftTopValue(params) } }, null, true ) } // 调整节点left、top adjustLeftTopValue() { walk( this.root, null, (node, parent, isRoot, layerIndex) => { if (!node.getData('expand')) { return } let params = { node, parent, layerIndex, ctx: this } if (this.checkIsTop(node)) { utils.top.adjustLeftTopValueBefore(params) } else { utils.bottom.adjustLeftTopValueBefore(params) } }, (node, parent) => { let params = { parent, node, ctx: this } if (this.checkIsTop(node)) { utils.top.adjustLeftTopValueAfter(params) } else { utils.bottom.adjustLeftTopValueAfter(params) } // 调整二级节点的子节点的left值 if (node.isRoot) { let topTotalLeft = 0 let bottomTotalLeft = 0 let maxx = -Infinity node.children.forEach(item => { if (this.checkIsTop(item)) { item.left += topTotalLeft this.updateChildren(item.children, 'left', topTotalLeft) let { left, right } = this.getNodeBoundaries(item, 'h') if (right > maxx) { maxx = right } topTotalLeft += right - left } else { item.left += bottomTotalLeft this.updateChildren(item.children, 'left', bottomTotalLeft) let { left, right } = this.getNodeBoundaries(item, 'h') if (right > maxx) { maxx = right } bottomTotalLeft += right - left } }) this.maxx = maxx } }, true ) } // 递归计算节点的宽度 getNodeAreaHeight(node) { let totalHeight = 0 let loop = node => { let marginY = this.getMarginY(node.layerIndex) totalHeight += node.height + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + marginY if (node.children.length) { node.children.forEach(item => { loop(item) }) } } loop(node) return totalHeight } // 调整兄弟节点的left updateBrothersLeft(node) { let childrenList = node.children let totalAddWidth = 0 childrenList.forEach(item => { item.left += totalAddWidth if (item.children && item.children.length) { this.updateChildren(item.children, 'left', totalAddWidth) } let { left, right } = this.getNodeBoundaries(item, 'h') let areaWidth = right - left let difference = areaWidth - item.width if (difference > 0) { totalAddWidth += difference } }) } // 调整兄弟节点的top updateBrothersTop(node, addHeight) { if (node.parent && !node.parent.isRoot) { let childrenList = node.parent.children let index = getNodeIndexInNodeList(node, childrenList) childrenList.forEach((item, _index) => { if (item.hasCustomPosition()) { // 适配自定义位置 return } let _offset = 0 // 下面的节点往下移 if (_index > index) { _offset = addHeight } item.top += _offset // 同步更新子节点的位置 if (item.children && item.children.length) { this.updateChildren(item.children, 'top', _offset) } }) // 更新父节点的位置 if (this.checkIsTop(node)) { this.updateBrothersTop(node.parent, addHeight) } else { this.updateBrothersTop( node.parent, node.layerIndex === 3 ? 0 : addHeight ) } } } // 检查节点是否是上方节点 checkIsTop(node) { return node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP } // 绘制连线,连接该节点到其子节点 renderLine(node, lines, style) { if (node.layerIndex !== 1 && node.children.length <= 0) { return [] } let { top, height, expandBtnSize } = node const { alwaysShowExpandBtn, notShowExpandBtn } = this.mindMap.opt if (!alwaysShowExpandBtn || notShowExpandBtn) { expandBtnSize = 0 } let len = node.children.length if (node.isRoot) { // 当前节点是根节点 // 根节点的子节点是和根节点同一水平线排列 let maxx = -Infinity node.children.forEach(item => { if (item.left > maxx) { maxx = item.left } // 水平线段到二级节点的连线 let marginY = this.getMarginY(item.layerIndex) let nodeLineX = item.left let offset = node.height / 2 + marginY - (this.isFishbone2() ? node.height / 4 : 0) let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) let line = this.lineDraw.path() if (this.checkIsTop(item)) { line.plot( this.transformPath( `M ${nodeLineX - offsetX},${item.top + item.height + offset} L ${ item.left },${item.top + item.height}` ) ) } else { line.plot( this.transformPath( `M ${nodeLineX - offsetX},${item.top - offset} L ${nodeLineX},${ item.top }` ) ) } node.style.line(line) node._lines.push(line) style && style(line, node) }) // 从根节点出发的水平线 let nodeHalfTop = node.top + node.height / 2 let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1) let line = this.lineDraw.path() const lineEndX = this.isFishbone2() ? this.maxx : maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) line.plot( this.transformPath( `M ${ node.left + node.width },${nodeHalfTop} L ${lineEndX},${nodeHalfTop}` ) ) node.style.line(line) node._lines.push(line) style && style(line, node) } else { // 当前节点为非根节点 let maxy = -Infinity let miny = Infinity let maxx = -Infinity let x = node.left + node.width * this.indent node.children.forEach((item, index) => { if (item.left > maxx) { maxx = item.left } let y = item.top + item.height / 2 if (y > maxy) { maxy = y } if (y < miny) { miny = y } // 水平线 if (node.layerIndex > 1) { let path = `M ${x},${y} L ${item.left},${y}` this.setLineStyle(style, lines[index], path, item) } }) // 斜线 if (len >= 0) { let line = this.lineDraw.path() expandBtnSize = len > 0 ? expandBtnSize : 0 let lineLength = maxx - node.left - node.width * this.indent lineLength = Math.max(lineLength, 0) let params = { node, line, top, x, lineLength, height, expandBtnSize, maxy, miny, ctx: this } if (this.checkIsTop(node)) { utils.top.renderLine(params) } else { utils.bottom.renderLine(params) } node.style.line(line) node._lines.push(line) style && style(line, node) } } } // 渲染按钮 renderExpandBtn(node, btn) { let { width, height, expandBtnSize, isRoot } = node if (!isRoot) { let { translateX, translateY } = btn.transform() let params = { node, btn, expandBtnSize, translateX, translateY, width, height } if (this.checkIsTop(node)) { utils.top.renderExpandBtn(params) } else { utils.bottom.renderExpandBtn(params) } } } // 创建概要节点 renderGeneralization(list) { list.forEach(item => { let { top, bottom, right, generalizationLineMargin, generalizationNodeMargin } = this.getNodeGeneralizationRenderBoundaries(item, 'h') let x1 = right + generalizationLineMargin let y1 = top let x2 = right + generalizationLineMargin let y2 = bottom let cx = x1 + 20 let cy = y1 + (y2 - y1) / 2 let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}` item.generalizationLine.plot(this.transformPath(path)) item.generalizationNode.left = right + generalizationNodeMargin item.generalizationNode.top = top + (bottom - top - item.generalizationNode.height) / 2 }) } // 渲染展开收起按钮的隐藏占位元素 renderExpandBtnRect(rect, expandBtnSize, width, height, node) { let dir = '' if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) { dir = node.layerIndex === 1 ? CONSTANTS.LAYOUT_GROW_DIR.TOP : CONSTANTS.LAYOUT_GROW_DIR.BOTTOM } else { dir = node.layerIndex === 1 ? CONSTANTS.LAYOUT_GROW_DIR.BOTTOM : CONSTANTS.LAYOUT_GROW_DIR.TOP } if (dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) { rect.size(width, expandBtnSize).x(0).y(-expandBtnSize) } else { rect.size(width, expandBtnSize).x(0).y(height) } } // 切换切换为其他结构时的处理 beforeChange() { // 删除鱼尾 if (!this.isFishbone2()) return this.root.nodeData.data.shape = CONSTANTS.SHAPE.RECTANGLE this.removeFishTail() this.unBindEvent() this.mindMap.removeShape('fishHead') } } export default Fishbone