UNPKG

leo-mind-map

Version:

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

414 lines (384 loc) 11.8 kB
import { formatDataToArray, walk, getNodeListBoundingRect, createUid } from '../utils' import { parseAddNodeList, getNodeOuterFrameList } from './outerFrame/outerFrameUtils' import outerFrameTextMethods from './outerFrame/outerFrameText' // 默认外框样式 const defaultStyle = { // 外框圆角大小 radius: 5, // 外框边框宽度 strokeWidth: 2, // 外框边框颜色 strokeColor: '#0984e3', // 外框边框虚线样式 strokeDasharray: '5,5', // 外框填充颜色 fill: 'rgba(9,132,227,0.05)', // 外框文字字号 fontSize: 14, // 外框文字字体 fontFamily: '微软雅黑, Microsoft YaHei', // 加粗 fontWeight: 'normal', // bold // 斜体 fontStyle: 'normal', // italic // 外框文字颜色 color: '#fff', // 外框文字行高 lineHeight: 1.2, // 外框文字背景 textFill: '#0984e3', // 外框文字圆角 textFillRadius: 5, // 外框文字矩内边距,左上右下 textFillPadding: [5, 5, 5, 5], // 外框文字水平显示位置,相对于外框 textAlign: 'left' // left、center、right } const OUTER_FRAME_TEXT_EDIT_WRAP = 'outer-frame-text-edit-warp' // 外框插件 class OuterFrame { constructor(opt = {}) { this.mindMap = opt.mindMap this.draw = null this.createDrawContainer() this.isNotRenderOuterFrames = false this.textNodeList = [] this.outerFrameElList = [] this.activeOuterFrame = null // 文字相关方法 this.textEditNode = null this.showTextEdit = false Object.keys(outerFrameTextMethods).forEach(item => { this[item] = outerFrameTextMethods[item].bind(this) }) this.mindMap.addEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP) 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.mindMap.on('scale', this.onScale) // 实例销毁事件 this.onBeforeDestroy = this.onBeforeDestroy.bind(this) this.mindMap.on('beforeDestroy', this.onBeforeDestroy) 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.off('scale', this.onScale) this.mindMap.off('beforeDestroy', this.onBeforeDestroy) this.mindMap.command.remove('ADD_OUTER_FRAME', this.addOuterFrame) this.mindMap.keyCommand.removeShortcut( 'Del|Backspace', this.removeActiveOuterFrame ) } // 实例销毁时清除关联线文字编辑框 onBeforeDestroy() { this.hideEditTextBox() this.removeTextEditEl() } // 给节点添加外框数据 /* 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') } // 删除当前激活外框的文字 removeActiveOuterFrameText() { this.updateActiveOuterFrame({ text: '' }) } // 更新当前激活的外框 updateActiveOuterFrame(config = {}) { if (!this.activeOuterFrame) return this.isNotRenderOuterFrames = true const { el, node, range } = this.activeOuterFrame let newStrokeDasharray = '' this.getRangeNodeList(node, range).forEach(node => { const outerFrame = node.getData('outerFrame') const newData = { ...outerFrame, ...config } newStrokeDasharray = newData.strokeDasharray this.mindMap.execCommand('SET_NODE_DATA', node, { outerFrame: newData }) }) el.cacheStyle = { dasharray: newStrokeDasharray } this.updateOuterFrameStyle() } // 更新当前激活外框的样式 updateOuterFrameStyle() { const { el, node, range, textNode } = this.activeOuterFrame const firstNode = this.getNodeRangeFirstNode(node, range) const styleConfig = this.getStyle(firstNode) this.styleOuterFrame(el, { ...styleConfig, strokeDasharray: 'none' }) const text = this.getText(firstNode) this.renderText(text, el, textNode, node, range) } // 获取某个节点指定范围的带外框的子节点列表 getRangeNodeList(node, range) { return node.children.slice(range[0], range[1] + 1).filter(child => { return child.getData('outerFrame') }) } // 获取某个节点指定范围的带外框的第一个子节点 getNodeRangeFirstNode(node, range) { return node.children[range[0]] } // 渲染外框 renderOuterFrames() { if (this.isNotRenderOuterFrames) { this.isNotRenderOuterFrames = false return } this.clearActiveOuterFrame() this.clearTextNodes() 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, this.getStyle(nodeList[0]) // 使用第一个节点的外框样式 ) // 渲染文字,如果有的话 const textNode = this.createText(el, cur, range) this.textNodeList.push(textNode) this.renderText(this.getText(nodeList[0]), el, textNode, cur, range) el.on('click', e => { e.stopPropagation() this.setActiveOuterFrame(el, cur, range, textNode) }) }) } }, () => {}, true, 0 ) } // 激活外框 setActiveOuterFrame(el, node, range, textNode) { this.mindMap.execCommand('CLEAR_ACTIVE_NODE') this.clearActiveOuterFrame() this.activeOuterFrame = { el, node, range, textNode } el.stroke({ dasharray: 'none' }) // 如果没有输入过文字,那么显示默认文字 if (!this.getText(this.getNodeRangeFirstNode(node, range))) { this.renderText( this.mindMap.opt.defaultOuterFrameText, el, textNode, node, range ) } this.mindMap.emit('outer_frame_active', el, node, range) } // 清除当前激活的外框 clearActiveOuterFrame() { if (!this.activeOuterFrame) return const { el, textNode, node, range } = this.activeOuterFrame el.stroke({ dasharray: el.cacheStyle.dasharray || defaultStyle.strokeDasharray }) // 隐藏文本编辑框 this.hideEditTextBox() // 如果没有输入过文字,那么隐藏 if (!this.getText(this.getNodeRangeFirstNode(node, range))) { textNode.clear() } this.activeOuterFrame = null this.mindMap.emit('outer_frame_deactivate') } // 获取指定外框的样式 getStyle(node) { return { ...defaultStyle, ...(node.getData('outerFrame') || {}) } } // 创建外框元素 createOuterFrameEl(x, y, width, height, styleConfig = {}) { const el = this.draw.rect().size(width, height).x(x).y(y) this.styleOuterFrame(el, styleConfig) el.cacheStyle = { dasharray: styleConfig.strokeDasharray } this.outerFrameElList.push(el) return el } // 设置外框样式 styleOuterFrame(el, styleConfig) { el.radius(styleConfig.radius) .stroke({ width: styleConfig.strokeWidth, color: styleConfig.strokeColor, dasharray: styleConfig.strokeDasharray }) .fill({ color: styleConfig.fill }) } // 清除文本元素 clearTextNodes() { this.textNodeList.forEach(item => { item.remove() }) } // 清除外框元素 clearOuterFrameElList() { this.outerFrameElList.forEach(item => { item.remove() }) this.outerFrameElList = [] this.activeOuterFrame = null } // 插件被移除前做的事情 beforePluginRemove() { this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP) this.unBindEvent() } // 插件被卸载前做的事情 beforePluginDestroy() { this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP) this.unBindEvent() } } OuterFrame.instanceName = 'outerFrame' OuterFrame.defaultStyle = defaultStyle export default OuterFrame