import { formatDataToArray, walk, getTopAncestorsFomNodeList, getNodeListBoundingRect, createUid } from '../utils' // 解析要添加外框的节点实例列表 const parseAddNodeList = list => { // 找出顶层节点 list = getTopAncestorsFomNodeList(list) const cache = {} const uidToParent = {} // 找出列表中节点在兄弟节点中的索引,并和父节点关联起来 list.forEach(node => { const parent = node.parent if (parent) { const pUid = parent.uid uidToParent[pUid] = parent const index = node.getIndexInBrothers() const data = { node, index } if (cache[pUid]) { if ( !cache[pUid].find(item => { return item.index === data.index }) ) { cache[pUid].push(data) } } else { cache[pUid] = [data] } } }) const res = [] Object.keys(cache).forEach(uid => { const indexList = cache[uid] const parentNode = uidToParent[uid] if (indexList.length > 1) { // 多个节点 const rangeList = indexList .map(item => { return item.index }) .sort((a, b) => { return a - b }) const minIndex = rangeList[0] const maxIndex = rangeList[rangeList.length - 1] let curStart = -1 let curEnd = -1 for (let i = minIndex; i <= maxIndex; i++) { // 连续索引 if (rangeList.includes(i)) { if (curStart === -1) { curStart = i } curEnd = i } else { // 连续断开 if (curStart !== -1 && curEnd !== -1) { res.push({ node: parentNode, range: [curStart, curEnd] }) } curStart = -1 curEnd = -1 } } // 不要忘了最后一段索引 if (curStart !== -1 && curEnd !== -1) { res.push({ node: parentNode, range: [curStart, curEnd] }) } } else { // 单个节点 res.push({ node: parentNode, range: [indexList[0].index, indexList[0].index] }) } }) return res } // 解析获取节点的子节点生成的外框列表 const getNodeOuterFrameList = node => { const children = node.children if (!children || children.length <= 0) return const res = [] const map = {} children.forEach((item, index) => { const outerFrameData = item.getData('outerFrame') if (!outerFrameData) return const groupId = outerFrameData.groupId if (groupId) { if (!map[groupId]) { map[groupId] = [] } map[groupId].push({ node: item, index }) } else { res.push({ nodeList: [item], range: [index, index] }) } }) Object.keys(map).forEach(id => { const list = map[id] res.push({ nodeList: list.map(item => { return item.node }), range: [list[0].index, list[list.length - 1].index] }) }) return res } // 默认外框样式 const defaultStyle = { radius: 5, strokeWidth: 2, strokeColor: '#0984e3', strokeDasharray: '5,5', fill: 'rgba(9,132,227,0.05)' } // 外框插件 class OuterFrame { constructor(opt = {}) { this.mindMap = opt.mindMap this.draw = null this.createDrawContainer() this.outerFrameElList = [] this.activeOuterFrame = null this.bindEvent() } // 创建容器 createDrawContainer() { this.draw = this.mindMap.draw.group() this.draw.addClass('smm-outer-frame-container') this.draw.back() // 最底层 this.draw.forward() // 连线层上面 } // 绑定事件 bindEvent() { this.renderOuterFrames = this.renderOuterFrames.bind(this) this.mindMap.on('node_tree_render_end', this.renderOuterFrames) this.mindMap.on('data_change', this.renderOuterFrames) // 监听画布和节点点击事件,用于清除当前激活的连接线 this.clearActiveOuterFrame = this.clearActiveOuterFrame.bind(this) this.mindMap.on('draw_click', this.clearActiveOuterFrame) this.mindMap.on('node_click', this.clearActiveOuterFrame) this.addOuterFrame = this.addOuterFrame.bind(this) this.mindMap.command.add('ADD_OUTER_FRAME', this.addOuterFrame) this.removeActiveOuterFrame = this.removeActiveOuterFrame.bind(this) this.mindMap.keyCommand.addShortcut( 'Del|Backspace', this.removeActiveOuterFrame ) } // 解绑事件 unBindEvent() { this.mindMap.off('node_tree_render_end', this.renderOuterFrames) this.mindMap.off('data_change', this.renderOuterFrames) this.mindMap.off('draw_click', this.clearActiveOuterFrame) this.mindMap.off('node_click', this.clearActiveOuterFrame) this.mindMap.command.remove('ADD_OUTER_FRAME', this.addOuterFrame) this.mindMap.keyCommand.removeShortcut( 'Del|Backspace', this.removeActiveOuterFrame ) } // 给节点添加外框数据 /* config: { text: '', radius: 5, strokeWidth: 2, strokeColor: '#0984e3', strokeDasharray: '5,5', fill: 'rgba(9,132,227,0.05)' } */ addOuterFrame(appointNodes, config = {}) { appointNodes = formatDataToArray(appointNodes) const activeNodeList = this.mindMap.renderer.activeNodeList if (activeNodeList.length <= 0 && appointNodes.length <= 0) { return } let nodeList = appointNodes.length > 0 ? appointNodes : activeNodeList nodeList = nodeList.filter(node => { return !node.isRoot && !node.isGeneralization }) const list = parseAddNodeList(nodeList) list.forEach(({ node, range }) => { const childNodeList = node.children.slice(range[0], range[1] + 1) const groupId = createUid() childNodeList.forEach(child => { let outerFrame = child.getData('outerFrame') // 检查该外框是否已存在 if (outerFrame) { outerFrame = { ...outerFrame, ...config, groupId } } else { outerFrame = { ...config, groupId } } this.mindMap.execCommand('SET_NODE_DATA', child, { outerFrame }) }) }) } // 获取当前激活的外框 getActiveOuterFrame() { return this.activeOuterFrame ? { ...this.activeOuterFrame } : null } // 删除当前激活的外框 removeActiveOuterFrame() { if (!this.activeOuterFrame) return const { node, range } = this.activeOuterFrame this.getRangeNodeList(node, range).forEach(child => { this.mindMap.execCommand('SET_NODE_DATA', child, { outerFrame: null }) }) this.mindMap.emit('outer_frame_delete') } // 更新当前激活的外框 // 执行了该方法后请立即隐藏你的样式面板,因为会清除当前激活的外框 updateActiveOuterFrame(config = {}) { if (!this.activeOuterFrame) return const { node, range } = this.activeOuterFrame this.getRangeNodeList(node, range).forEach(node => { const outerFrame = node.getData('outerFrame') this.mindMap.execCommand('SET_NODE_DATA', node, { outerFrame: { ...outerFrame, ...config } }) }) } // 获取某个节点指定范围的带外框的子节点列表 getRangeNodeList(node, range) { return node.children.slice(range[0], range[1] + 1).filter(child => { return child.getData('outerFrame') }) } // 渲染外框 renderOuterFrames() { this.clearOuterFrameElList() let tree = this.mindMap.renderer.root if (!tree) return const t = this.mindMap.draw.transform() const { outerFramePaddingX, outerFramePaddingY } = this.mindMap.opt walk( tree, null, cur => { if (!cur) return const outerFrameList = getNodeOuterFrameList(cur) if (outerFrameList && outerFrameList.length > 0) { outerFrameList.forEach(({ nodeList, range }) => { if (range[0] === -1 || range[1] === -1) return const { left, top, width, height } = getNodeListBoundingRect(nodeList) if ( !Number.isFinite(left) || !Number.isFinite(top) || !Number.isFinite(width) || !Number.isFinite(height) ) return const el = this.createOuterFrameEl( (left - outerFramePaddingX - this.mindMap.elRect.left - t.translateX) / t.scaleX, (top - outerFramePaddingY - this.mindMap.elRect.top - t.translateY) / t.scaleY, (width + outerFramePaddingX * 2) / t.scaleX, (height + outerFramePaddingY * 2) / t.scaleY, nodeList[0].getData('outerFrame') // 使用第一个节点的外框样式 ) el.on('click', e => { e.stopPropagation() this.setActiveOuterFrame(el, cur, range) }) }) } }, () => {}, true, 0 ) } // 激活外框 setActiveOuterFrame(el, node, range) { this.mindMap.execCommand('CLEAR_ACTIVE_NODE') this.clearActiveOuterFrame() this.activeOuterFrame = { el, node, range } el.stroke({ dasharray: 'none' }) this.mindMap.emit('outer_frame_active', el, node, range) } // 清除当前激活的外框 clearActiveOuterFrame() { if (!this.activeOuterFrame) return const { el } = this.activeOuterFrame el.stroke({ dasharray: el.cacheStyle.dasharray || defaultStyle.strokeDasharray }) this.activeOuterFrame = null } // 创建外框元素 createOuterFrameEl(x, y, width, height, styleConfig = {}) { styleConfig = { ...defaultStyle, ...styleConfig } const el = this.draw .rect() .size(width, height) .radius(styleConfig.radius) .stroke({ width: styleConfig.strokeWidth, color: styleConfig.strokeColor, dasharray: styleConfig.strokeDasharray }) .fill({ color: styleConfig.fill }) .x(x) .y(y) el.cacheStyle = { dasharray: styleConfig.strokeDasharray } this.outerFrameElList.push(el) return el } // 清除外框元素 clearOuterFrameElList() { this.outerFrameElList.forEach(item => { item.remove() }) this.outerFrameElList = [] this.activeOuterFrame = null } // 插件被移除前做的事情 beforePluginRemove() { this.unBindEvent() } // 插件被卸载前做的事情 beforePluginDestroy() { this.unBindEvent() } } OuterFrame.instanceName = 'outerFrame' export default OuterFrame