From 79ae08fc9ad82f6699b6f2fce1bc6f113a89ea95 Mon Sep 17 00:00:00 2001 From: wanglin2 <1013335014@qq.com> Date: Thu, 30 Mar 2023 14:16:47 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E6=94=B9=E4=B8=BB=E9=A2=98=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=AE=8C=E5=85=A8=E9=87=8D=E6=96=B0=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=EF=BC=9B2.=E9=87=8D=E6=9E=84Node=E7=B1=BB=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=9B3.Shape=E7=B1=BB=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E6=B7=BB=E5=8A=A0=E8=8A=82=E7=82=B9=EF=BC=8C?= =?UTF-8?q?=E8=80=8C=E6=98=AF=E8=BF=94=E5=9B=9E=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple-mind-map/index.js | 10 +- simple-mind-map/src/Node.js | 705 +++--------------- simple-mind-map/src/Render.js | 25 +- simple-mind-map/src/Shape.js | 118 +-- simple-mind-map/src/layouts/Base.js | 5 + simple-mind-map/src/utils/nodeCommandWraps.js | 56 ++ .../src/utils/nodeCreateContents.js | 256 +++++++ simple-mind-map/src/utils/nodeExpandBtn.js | 84 +++ .../src/utils/nodeGeneralization.js | 115 +++ 9 files changed, 706 insertions(+), 668 deletions(-) create mode 100644 simple-mind-map/src/utils/nodeCommandWraps.js create mode 100644 simple-mind-map/src/utils/nodeCreateContents.js create mode 100644 simple-mind-map/src/utils/nodeExpandBtn.js create mode 100644 simple-mind-map/src/utils/nodeGeneralization.js diff --git a/simple-mind-map/index.js b/simple-mind-map/index.js index 314c9ff6..9f57350a 100644 --- a/simple-mind-map/index.js +++ b/simple-mind-map/index.js @@ -135,7 +135,7 @@ class MindMap { }) // 初始渲染 - this.reRender() + this.render() setTimeout(() => { this.command.addHistory() }, 0) @@ -153,11 +153,11 @@ class MindMap { } // 渲染,部分渲染 - render(callback) { + render(callback, source = '') { this.batchExecution.push('render', () => { this.initTheme() this.renderer.reRender = false - this.renderer.render(callback) + this.renderer.render(callback, source) }) } @@ -206,7 +206,7 @@ class MindMap { setTheme(theme) { this.renderer.clearAllActive() this.opt.theme = theme - this.reRender() + this.render(null, 'changeTheme') } // 获取当前主题 @@ -217,7 +217,7 @@ class MindMap { // 设置主题配置 setThemeConfig(config) { this.opt.themeConfig = config - this.reRender() + this.render(null, 'changeTheme') } // 获取自定义主题配置 diff --git a/simple-mind-map/src/Node.js b/simple-mind-map/src/Node.js index d411e056..07402290 100644 --- a/simple-mind-map/src/Node.js +++ b/simple-mind-map/src/Node.js @@ -1,12 +1,14 @@ import Style from './Style' import Shape from './Shape' -import { resizeImgSize, asyncRun, measureText } from './utils' -import { Image, SVG, Circle, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js' -import btnsSvg from './svg/btns' -import iconsSvg from './svg/icons' +import { asyncRun } from './utils' +import { G } from '@svgdotjs/svg.js' +import nodeGeneralizationMethods from './utils/nodeGeneralization' +import nodeExpandBtnMethods from './utils/nodeExpandBtn' +import nodeCommandWrapsMethods from './utils/nodeCommandWraps' +import nodeCreateContentsMethods from './utils/nodeCreateContents' + // 节点类 - class Node { // 构造函数 constructor(opt = {}) { @@ -84,8 +86,26 @@ class Node { this.blockContentMargin = this.mindMap.opt.imgTextMargin // 展开收缩按钮尺寸 this.expandBtnSize = this.mindMap.opt.expandBtnSize - // 初始渲染 - this.initRender = true + // 是否是多选节点 + this.isMultipleChoice = false + // 是否需要重新layout + this.needLayout = false + // 概要相关方法 + Object.keys(nodeGeneralizationMethods).forEach((item) => { + this[item] = nodeGeneralizationMethods[item].bind(this) + }) + // 展开收起按钮相关方法 + Object.keys(nodeExpandBtnMethods).forEach((item) => { + this[item] = nodeExpandBtnMethods[item].bind(this) + }) + // 命令的相关方法 + Object.keys(nodeCommandWrapsMethods).forEach((item) => { + this[item] = nodeCommandWrapsMethods[item].bind(this) + }) + // 创建节点内容的相关方法 + Object.keys(nodeCreateContentsMethods).forEach((item) => { + this[item] = nodeCreateContentsMethods[item].bind(this) + }) // 初始化 this.getSize() } @@ -125,28 +145,6 @@ class Node { return data } - // 检查节点是否存在自定义数据 - hasCustomPosition() { - return this.customLeft !== undefined && this.customTop !== undefined - } - - // 检查节点是否存在自定义位置的祖先节点 - ancestorHasCustomPosition() { - let node = this - while (node) { - if (node.hasCustomPosition()) { - return true - } - node = node.parent - } - return false - } - - // 添加子节点 - addChildren(node) { - this.children.push(node) - } - // 创建节点的各个内容对象数据 createNodeData() { this._imgData = this.createImgNode() @@ -155,68 +153,11 @@ class Node { this._hyperlinkData = this.createHyperlinkNode() this._tagData = this.createTagNode() this._noteData = this.createNoteNode() - this.createGeneralizationNode() - } - - // 解绑所有事件 - removeAllEvent() { - if (this._noteData) { - this._noteData.node.off(['mouseover', 'mouseout']) - } - if (this._expandBtn) { - this._expandBtn.off(['mouseover', 'mouseout', 'click']) - } - if (this.group) { - this.group.off([ - 'click', - 'dblclick', - 'contextmenu', - 'mousedown', - 'mouseup', - 'mouseenter', - 'mouseleave' - ]) - } - } - - // 移除节点内容 - removeAllNode() { - // 节点内的内容 - ;[ - this._imgData, - this._iconData, - this._textData, - this._hyperlinkData, - this._tagData, - this._noteData - ].forEach(item => { - if (item && item.node) item.node.remove() - }) - this._imgData = null - this._iconData = null - this._textData = null - this._hyperlinkData = null - this._tagData = null - this._noteData = null - // 展开收缩按钮 - if (this._expandBtn) { - this._expandBtn.remove() - this._expandBtn = null - } - // 组 - if (this.group) { - this.group.clear() - this.group.remove() - this.group = null - } - // 概要 - this.removeGeneralization() } // 计算节点的宽高 getSize() { - this.removeAllEvent() - this.removeAllNode() + this.updateGeneralization() this.createNodeData() let { width, height } = this.getNodeRect() // 判断节点尺寸是否有变化 @@ -293,276 +234,23 @@ class Node { } } - // 创建图片节点 - createImgNode() { - let img = this.nodeData.data.image - if (!img) { - return - } - let imgSize = this.getImgShowSize() - let node = new Image().load(img).size(...imgSize) - if (this.nodeData.data.imageTitle) { - node.attr('title', this.nodeData.data.imageTitle) - } - node.on('dblclick', e => { - this.mindMap.emit('node_img_dblclick', this, e) - }) - return { - node, - width: imgSize[0], - height: imgSize[1] - } - } - - // 获取图片显示宽高 - getImgShowSize() { - return resizeImgSize( - this.nodeData.data.imageSize.width, - this.nodeData.data.imageSize.height, - this.mindMap.themeConfig.imgMaxWidth, - this.mindMap.themeConfig.imgMaxHeight - ) - } - - // 创建icon节点 - createIconNode() { - let _data = this.nodeData.data - if (!_data.icon || _data.icon.length <= 0) { - return [] - } - let iconSize = this.mindMap.themeConfig.iconSize - return _data.icon.map(item => { - return { - node: SVG(iconsSvg.getNodeIconListIcon(item)).size(iconSize, iconSize), - width: iconSize, - height: iconSize - } - }) - } - - // 创建富文本节点 - createRichTextNode() { - let g = new G() - let html = `
${this.nodeData.data.text}
` - let div = document.createElement('div') - div.innerHTML = html - div.style.cssText = `position: fixed; left: -999999px;` - let el = div.children[0] - el.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml') - el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px' - this.mindMap.el.appendChild(div) - let { width, height } = el.getBoundingClientRect() - width = Math.ceil(width) - height = Math.ceil(height) - g.attr('data-width', width) - g.attr('data-height', height) - html = div.innerHTML - this.mindMap.el.removeChild(div) - let foreignObject = new ForeignObject() - foreignObject.width(width) - foreignObject.height(height) - foreignObject.add(SVG(html)) - g.add(foreignObject) - return { - node: g, - width, - height - } - } - - // 创建文本节点 - createTextNode() { - if (this.nodeData.data.richText) { - return this.createRichTextNode() - } - let g = new G() - let fontSize = this.getStyle( - 'fontSize', - false, - this.nodeData.data.isActive - ) - let lineHeight = this.getStyle( - 'lineHeight', - false, - this.nodeData.data.isActive - ) - // 文本超长自动换行 - let textStyle = this.style.getTextFontStyle() - let textArr = this.nodeData.data.text.split(/\n/gim) - let maxWidth = this.mindMap.opt.textAutoWrapWidth - textArr.forEach((item, index) => { - let arr = item.split('') - let lines = [] - let line = [] - while(arr.length) { - line.push(arr.shift()) - let text = line.join('') - if (measureText(text, textStyle).width >= maxWidth) { - lines.push(text) - line = [] - } - } - if (line.length > 0) { - lines.push(line.join('')) - } - textArr[index] = lines.join('\n') - }) - textArr = textArr.join('\n').split(/\n/gim) - textArr.forEach((item, index) => { - let node = new Text().text(item) - this.style.text(node) - node.y(fontSize * lineHeight * index) - g.add(node) - }) - let { width, height } = g.bbox() - width = Math.ceil(width) - height = Math.ceil(height) - g.attr('data-width', width) - g.attr('data-height', height) - return { - node: g, - width, - height - } - } - - // 创建超链接节点 - createHyperlinkNode() { - let { hyperlink, hyperlinkTitle } = this.nodeData.data - if (!hyperlink) { - return - } - let iconSize = this.mindMap.themeConfig.iconSize - let node = new SVG() - // 超链接节点 - let a = new A().to(hyperlink).target('_blank') - a.node.addEventListener('click', e => { - e.stopPropagation() - }) - if (hyperlinkTitle) { - a.attr('title', hyperlinkTitle) - } - // 添加一个透明的层,作为鼠标区域 - a.rect(iconSize, iconSize).fill({ color: 'transparent' }) - // 超链接图标 - let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize) - this.style.iconNode(iconNode) - a.add(iconNode) - node.add(a) - return { - node, - width: iconSize, - height: iconSize - } - } - - // 创建标签节点 - createTagNode() { - let tagData = this.nodeData.data.tag - if (!tagData || tagData.length <= 0) { - return [] - } - let nodes = [] - tagData.slice(0, this.mindMap.opt.maxTag).forEach((item, index) => { - let tag = new G() - // 标签文本 - let text = new Text().text(item).x(8).cy(10) - this.style.tagText(text, index) - let { width } = text.bbox() - // 标签矩形 - let rect = new Rect().size(width + 16, 20) - this.style.tagRect(rect, index) - tag.add(rect).add(text) - nodes.push({ - node: tag, - width: width + 16, - height: 20 - }) - }) - return nodes - } - - // 创建备注节点 - createNoteNode() { - if (!this.nodeData.data.note) { - return null - } - let iconSize = this.mindMap.themeConfig.iconSize - let node = new SVG().attr('cursor', 'pointer') - // 透明的层,用来作为鼠标区域 - node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' })) - // 备注图标 - let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize) - this.style.iconNode(iconNode) - node.add(iconNode) - // 备注tooltip - if (!this.mindMap.opt.customNoteContentShow) { - if (!this.noteEl) { - this.noteEl = document.createElement('div') - this.noteEl.style.cssText = ` - position: absolute; - padding: 10px; - border-radius: 5px; - box-shadow: 0 2px 5px rgb(0 0 0 / 10%); - display: none; - background-color: #fff; - ` - document.body.appendChild(this.noteEl) - } - this.noteEl.innerText = this.nodeData.data.note - } - node.on('mouseover', () => { - let { left, top } = node.node.getBoundingClientRect() - if (!this.mindMap.opt.customNoteContentShow) { - this.noteEl.style.left = left + 'px' - this.noteEl.style.top = top + iconSize + 'px' - this.noteEl.style.display = 'block' - } else { - this.mindMap.opt.customNoteContentShow.show( - this.nodeData.data.note, - left, - top + iconSize - ) - } - }) - node.on('mouseout', () => { - if (!this.mindMap.opt.customNoteContentShow) { - this.noteEl.style.display = 'none' - } else { - this.mindMap.opt.customNoteContentShow.hide() - } - }) - return { - node, - width: iconSize, - height: iconSize - } - } - - // 获取节点形状 - getShape() { - // 节点使用功能横线风格的话不支持设置形状,直接使用默认的矩形 - return this.mindMap.themeConfig.nodeUseLineStyle - ? 'rectangle' - : this.style.getStyle('shape', false, false) - } - // 定位节点内容 layout() { + // 清除之前的内容 + this.group.clear() let { width, textContentItemMargin } = this let { paddingY } = this.getPaddingVale() paddingY += this.shapePadding.paddingY - // 创建组 - this.group = new G() + // 展开收起按钮 + this.renderExpandBtn() + // 节点形状 + this.shapeNode = this.shapeInstance.createShape() + this.group.add(this.shapeNode) + this.updateNodeShape() // 概要节点添加一个带所属节点id的类名 if (this.isGeneralization && this.generalizationBelongNode) { this.group.addClass('generalization_' + this.generalizationBelongNode.uid) } - this.draw.add(this.group) - this.update(true) - // 节点形状 - this.shapeNode = this.shapeInstance.createShape() - this.updateNodeShape() // 图片节点 let imgHeight = 0 if (this._imgData) { @@ -634,8 +322,17 @@ class Node { : 0) ) this.group.add(textContentNested) + } + + // 给节点绑定事件 + bindGroupEvent() { // 单击事件,选中节点 this.group.on('click', e => { + if (this.isMultipleChoice) { + e.stopPropagation() + this.isMultipleChoice = false + return + } this.mindMap.emit('node_click', this, e) this.active(e) }) @@ -645,8 +342,10 @@ class Node { } // 多选和取消多选 if (e.ctrlKey) { + this.isMultipleChoice = true let isActive = this.nodeData.data.isActive - this.mindMap.renderer.setNodeActive(this, !isActive) + if (!isActive) this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList) + this.mindMap.execCommand('SET_NODE_ACTIVE', this, !isActive) this.mindMap.renderer[isActive ? 'removeActiveNode' : 'addActiveNode'](this) this.mindMap.emit( 'node_active', @@ -708,16 +407,6 @@ class Node { this.mindMap.emit('node_active', this, this.renderer.activeNodeList) } - // 渲染节点到画布,会移除旧的,创建新的 - renderNode() { - // 连线 - this.renderLine() - this.removeAllEvent() - this.removeAllNode() - this.createNodeData() - this.layout() - } - // 更新节点 update(isLayout = false) { if (!this.group) { @@ -730,9 +419,12 @@ class Node { // 需要添加展开收缩按钮 this.renderExpandBtn() } else { + // 更新展开收起按钮 this.updateExpandBtnPos() } + // 更新概要 this.renderGeneralization() + // 更新节点位置 let t = this.group.transform() if (!isLayout) { this.group @@ -749,6 +441,14 @@ class Node { } } + // 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置 + reRender() { + let sizeChange = this.getSize() + this.layout() + this.update() + return sizeChange + } + // 更新节点形状样式 updateNodeShape() { const shape = this.getShape() @@ -758,12 +458,21 @@ class Node { // 递归渲染 render(callback = () => {}) { // 节点 - if (this.initRender) { - this.initRender = false - this.renderNode() + // 重新渲染连线 + this.renderLine() + if (!this.group) { + // 创建组 + this.group = new G() + this.bindGroupEvent() + this.draw.add(this.group) + this.layout() + this.update(true) } else { - // 连线 - this.renderLine() + this.draw.add(this.group) + if (this.needLayout) { + this.needLayout = false + this.layout() + } this.update() } // 子节点 @@ -798,11 +507,11 @@ class Node { } } - // 递归删除 + // 递归删除,只是从画布删除,节点容器还在,后续还可以重新插回画布 remove() { - this.initRender = true - this.removeAllEvent() - this.removeAllNode() + if (!this.group) return + this.group.remove() + this.removeGeneralization() this.removeLine() // 子节点 if (this.children && this.children.length) { @@ -816,6 +525,13 @@ class Node { } } + // 销毁节点,不但会从画布删除,而且原节点直接置空,后续无法再插回画布 + destroy() { + if (!this.group) return + this.group.remove() + this.group = null + } + // 隐藏节点 hide() { this.group.hide() @@ -895,6 +611,36 @@ class Node { } } + // 获取节点形状 + getShape() { + // 节点使用功能横线风格的话不支持设置形状,直接使用默认的矩形 + return this.mindMap.themeConfig.nodeUseLineStyle + ? '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 + } + + // 添加子节点 + addChildren(node) { + this.children.push(node) + } + // 设置连线样式 styleLine(line, node) { let width = @@ -919,184 +665,6 @@ class Node { this._lines = [] } - // 检查是否存在概要 - checkHasGeneralization() { - return !!this.nodeData.data.generalization - } - - // 创建概要节点 - createGeneralizationNode() { - if (this.isGeneralization || !this.checkHasGeneralization()) { - return - } - if (!this._generalizationLine) { - this._generalizationLine = this.draw.path() - } - if (!this._generalizationNode) { - this._generalizationNode = new Node({ - data: { - data: this.nodeData.data.generalization - }, - uid: this.mindMap.uid++, - renderer: this.renderer, - mindMap: this.mindMap, - draw: this.draw, - isGeneralization: true - }) - this._generalizationNodeWidth = this._generalizationNode.width - this._generalizationNodeHeight = this._generalizationNode.height - this._generalizationNode.generalizationBelongNode = this - if (this.nodeData.data.generalization.isActive) { - this.renderer.addActiveNode(this._generalizationNode) - } - } - } - - // 更新概要节点 - updateGeneralization() { - this.removeGeneralization() - this.createGeneralizationNode() - } - - // 渲染概要节点 - renderGeneralization() { - if (this.isGeneralization) { - return - } - if (!this.checkHasGeneralization()) { - this.removeGeneralization() - this._generalizationNodeWidth = 0 - this._generalizationNodeHeight = 0 - return - } - if (this.nodeData.data.expand === false) { - this.removeGeneralization() - return - } - this.createGeneralizationNode() - this.renderer.layout.renderGeneralization( - this, - this._generalizationLine, - this._generalizationNode - ) - this.style.generalizationLine(this._generalizationLine) - this._generalizationNode.render() - } - - // 删除概要节点 - removeGeneralization() { - if (this._generalizationLine) { - this._generalizationLine.remove() - this._generalizationLine = null - } - if (this._generalizationNode) { - // 删除概要节点时要同步从激活节点里删除 - this.renderer.removeActiveNode(this._generalizationNode) - this._generalizationNode.remove() - this._generalizationNode = null - } - // hack修复当激活一个节点时创建概要,然后立即激活创建的概要节点后会重复创建概要节点并且无法删除的问题 - if (this.generalizationBelongNode) { - this.draw - .find('.generalization_' + this.generalizationBelongNode.uid) - .remove() - } - } - - // 隐藏概要节点 - hideGeneralization() { - if (this._generalizationLine) { - this._generalizationLine.hide() - } - if (this._generalizationNode) { - this._generalizationNode.hide() - } - } - - // 显示概要节点 - showGeneralization() { - if (this._generalizationLine) { - this._generalizationLine.show() - } - if (this._generalizationNode) { - this._generalizationNode.show() - } - } - - // 创建或更新展开收缩按钮内容 - updateExpandBtnNode() { - if (this._expandBtn) { - this._expandBtn.clear() - } - let iconSvg - if (this.nodeData.data.expand === false) { - iconSvg = btnsSvg.open - } else { - iconSvg = btnsSvg.close - } - let node = SVG(iconSvg).size(this.expandBtnSize, this.expandBtnSize) - let fillNode = new Circle().size(this.expandBtnSize) - node.x(0).y(-this.expandBtnSize / 2) - fillNode.x(0).y(-this.expandBtnSize / 2) - this.style.iconBtn(node, fillNode) - if (this._expandBtn) this._expandBtn.add(fillNode).add(node) - } - - // 更新展开收缩按钮位置 - updateExpandBtnPos() { - if (!this._expandBtn) { - return - } - this.renderer.layout.renderExpandBtn(this, this._expandBtn) - } - - // 展开收缩按钮 - renderExpandBtn() { - if ( - !this.nodeData.children || - this.nodeData.children.length <= 0 || - this.isRoot - ) { - return - } - this._expandBtn = new G() - this.updateExpandBtnNode() - this._expandBtn.on('mouseover', e => { - e.stopPropagation() - this._expandBtn.css({ - cursor: 'pointer' - }) - }) - this._expandBtn.on('mouseout', e => { - e.stopPropagation() - this._expandBtn.css({ - cursor: 'auto' - }) - }) - this._expandBtn.on('click', e => { - e.stopPropagation() - // 展开收缩 - this.mindMap.execCommand( - 'SET_NODE_EXPAND', - this, - !this.nodeData.data.expand - ) - this.mindMap.emit('expand_btn_click', this) - }) - this.group.add(this._expandBtn) - this.updateExpandBtnPos() - } - - // 移除展开收缩按钮 - removeExpandBtn() { - if (this._expandBtn) { - this._expandBtn.off(['mouseover', 'mouseout', 'click']) - this._expandBtn.clear() - this._expandBtn.remove() - this._expandBtn = null - } - } - // 检测当前节点是否是某个节点的祖先节点 isParent(node) { if (this === node) { @@ -1159,55 +727,10 @@ class Node { ) // 父级 } - // 修改某个样式 - setStyle(prop, value, isActive) { - this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value, isActive) - } - // 获取数据 getData(key) { return key ? this.nodeData.data[key] || '' : this.nodeData.data } - - // 设置数据 - setData(data = {}) { - this.mindMap.execCommand('SET_NODE_DATA', this, data) - } - - // 设置文本 - setText(text, richText) { - this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText) - } - - // 设置图片 - setImage(imgData) { - this.mindMap.execCommand('SET_NODE_IMAGE', this, imgData) - } - - // 设置图标 - setIcon(icons) { - this.mindMap.execCommand('SET_NODE_ICON', this, icons) - } - - // 设置超链接 - setHyperlink(link, title) { - this.mindMap.execCommand('SET_NODE_HYPERLINK', this, link, title) - } - - // 设置备注 - setNote(note) { - this.mindMap.execCommand('SET_NODE_NOTE', this, note) - } - - // 设置标签 - setTag(tag) { - this.mindMap.execCommand('SET_NODE_TAG', this, tag) - } - - // 设置形状 - setShape(shape) { - this.mindMap.execCommand('SET_NODE_SHAPE', this, shape) - } } export default Node diff --git a/simple-mind-map/src/Render.js b/simple-mind-map/src/Render.js index 64df860a..72f8076b 100644 --- a/simple-mind-map/src/Render.js +++ b/simple-mind-map/src/Render.js @@ -33,6 +33,8 @@ class Render { this.renderTree = merge({}, this.mindMap.opt.data || {}) // 是否重新渲染 this.reRender = false + // 触发render的来源 + this.renderSource = '' // 当前激活的节点列表 this.activeNodeList = [] // 根节点 @@ -231,7 +233,8 @@ class Render { } // 渲染 - render(callback = () => {}) { + render(callback = () => {}, source) { + this.renderSource = source if (this.reRender) { this.clearActive() } @@ -239,7 +242,7 @@ class Render { this.root = root this.root.render(() => { this.mindMap.emit('node_tree_render_end') - callback() + callback && callback() }) }) this.mindMap.emit('node_active', null, this.activeNodeList) @@ -356,7 +359,7 @@ class Render { } else { let text = first.layerIndex === 1 ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText if (first.layerIndex === 1) { - first.parent.initRender = true + first.parent.destroy() } let index = this.getNodeIndex(first) first.parent.nodeData.children.splice(index + 1, 0, { @@ -397,10 +400,7 @@ class Render { // 插入子节点时自动展开子节点 node.nodeData.data.expand = true if (node.isRoot) { - node.initRender = true - // this.mindMap.batchExecution.push('renderNode' + index, () => { - // node.renderNode() - // }) + node.destroy() } }) this.mindMap.render() @@ -492,8 +492,7 @@ class Render { existParent.nodeData.children.splice(existIndex, 0, node.nodeData) this.mindMap.render(() => { if (nodeLayerChanged) { - node.getSize() - node.layout() + node.reRender() } }) } @@ -531,8 +530,7 @@ class Render { existParent.nodeData.children.splice(existIndex, 0, node.nodeData) this.mindMap.render(() => { if (nodeLayerChanged) { - node.getSize() - node.layout() + node.reRender() } }) } @@ -625,7 +623,7 @@ class Render { toNode.nodeData.children.push(copyData) this.mindMap.render() if (toNode.isRoot) { - toNode.renderNode() + toNode.destroy() } } @@ -913,8 +911,7 @@ class Render { // 设置节点数据,并判断是否渲染 setNodeDataRender(node, data) { this.setNodeData(node, data) - let changed = node.getSize() - node.layout() + let changed = node.reRender() if (changed) { if (node.isGeneralization) { // 概要节点 diff --git a/simple-mind-map/src/Shape.js b/simple-mind-map/src/Shape.js index ea446471..b2b8a724 100644 --- a/simple-mind-map/src/Shape.js +++ b/simple-mind-map/src/Shape.js @@ -1,3 +1,5 @@ +import { Rect, Polygon, Path } from '@svgdotjs/svg.js' + // 节点形状类 export default class Shape { constructor(node) { @@ -63,7 +65,7 @@ export default class Shape { let node = null // 矩形 if (shape === 'rectangle') { - node = this.node.group.rect(width, height) + node = new Rect().size(width, height) } else if (shape === 'diamond') { // 菱形 node = this.createDiamond() @@ -105,12 +107,12 @@ export default class Shape { let bottomY = height let leftX = 0 let leftY = halfHeight - return this.node.group.polygon(` - ${topX}, ${topY} - ${rightX}, ${rightY} - ${bottomX}, ${bottomY} - ${leftX}, ${leftY} - `) + return new Polygon().plot([ + [topX, topY], + [rightX, rightY], + [bottomX, bottomY], + [leftX, leftY] + ]) } // 创建平行四边形 @@ -118,41 +120,41 @@ export default class Shape { let { paddingX } = this.node.getPaddingVale() paddingX = paddingX || this.node.shapePadding.paddingX let { width, height } = this.node - return this.node.group.polygon(` - ${paddingX}, ${0} - ${width}, ${0} - ${width - paddingX}, ${height} - ${0}, ${height} - `) + return new Polygon().plot([ + [paddingX, 0], + [width, 0], + [width - paddingX, height], + [0, height] + ]) } // 创建圆角矩形 createRoundedRectangle() { let { width, height } = this.node let halfHeight = height / 2 - return this.node.group.path(` - M${halfHeight},0 - L${width - halfHeight},0 - A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height} - L${halfHeight},${height} - A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0} - `) + return new Path().plot(` + M${halfHeight},0 + L${width - halfHeight},0 + A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height} + L${halfHeight},${height} + A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0} + `) } // 创建八角矩形 createOctagonalRectangle() { let w = 5 let { width, height } = this.node - return this.node.group.polygon(` - ${0}, ${w} - ${w}, ${0} - ${width - w}, ${0} - ${width}, ${w} - ${width}, ${height - w} - ${width - w}, ${height} - ${w}, ${height} - ${0}, ${height - w} - `) + return new Polygon().plot([ + [0, w], + [w, 0], + [width - w, 0], + [width, w], + [width, height - w], + [width - w, height], + [w, height], + [0, height - w] + ]) } // 创建外三角矩形 @@ -160,14 +162,14 @@ export default class Shape { let { paddingX } = this.node.getPaddingVale() paddingX = paddingX || this.node.shapePadding.paddingX let { width, height } = this.node - return this.node.group.polygon(` - ${paddingX}, ${0} - ${width - paddingX}, ${0} - ${width}, ${height / 2} - ${width - paddingX}, ${height} - ${paddingX}, ${height} - ${0}, ${height / 2} - `) + return new Polygon().plot([ + [paddingX, 0], + [width - paddingX, 0], + [width, height / 2], + [width - paddingX, height], + [paddingX, height], + [0, height / 2] + ]) } // 创建内三角矩形 @@ -175,14 +177,14 @@ export default class Shape { let { paddingX } = this.node.getPaddingVale() paddingX = paddingX || this.node.shapePadding.paddingX let { width, height } = this.node - return this.node.group.polygon(` - ${0}, ${0} - ${width}, ${0} - ${width - paddingX / 2}, ${height / 2} - ${width}, ${height} - ${0}, ${height} - ${paddingX / 2}, ${height / 2} - `) + return new Polygon().plot([ + [0, 0], + [width, 0], + [width - paddingX / 2, height / 2], + [width, height], + [0, height], + [paddingX / 2, height / 2] + ]) } // 创建椭圆 @@ -190,12 +192,12 @@ export default class Shape { let { width, height } = this.node let halfWidth = width / 2 let halfHeight = height / 2 - return this.node.group.path(` - M${halfWidth},0 - A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height} - M${halfWidth},${height} - A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0} - `) + return new Path().plot(` + M${halfWidth},0 + A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height} + M${halfWidth},${height} + A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0} + `) } // 创建圆 @@ -203,12 +205,12 @@ export default class Shape { let { width, height } = this.node let halfWidth = width / 2 let halfHeight = height / 2 - return this.node.group.path(` - M${halfWidth},0 - A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height} - M${halfWidth},${height} - A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0} - `) + return new Path().plot(` + M${halfWidth},0 + A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height} + M${halfWidth},${height} + A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0} + `) } } diff --git a/simple-mind-map/src/layouts/Base.js b/simple-mind-map/src/layouts/Base.js index caca9bed..436c41e4 100644 --- a/simple-mind-map/src/layouts/Base.js +++ b/simple-mind-map/src/layouts/Base.js @@ -41,6 +41,11 @@ class Base { newNode = data._node newNode.reset() newNode.layerIndex = layerIndex + // 主题或主题配置改变了需要重新计算节点大小和布局 + if (this.renderer.renderSource === 'changeTheme') { + newNode.getSize() + newNode.needLayout = true + } } else { // 创建新节点 newNode = new Node({ diff --git a/simple-mind-map/src/utils/nodeCommandWraps.js b/simple-mind-map/src/utils/nodeCommandWraps.js new file mode 100644 index 00000000..ceb66ad2 --- /dev/null +++ b/simple-mind-map/src/utils/nodeCommandWraps.js @@ -0,0 +1,56 @@ +// 设置数据 +function setData(data = {}) { + this.mindMap.execCommand('SET_NODE_DATA', this, data) +} + +// 设置文本 +function setText(text, richText) { + this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText) +} + +// 设置图片 +function setImage(imgData) { + this.mindMap.execCommand('SET_NODE_IMAGE', this, imgData) +} + +// 设置图标 +function setIcon(icons) { + this.mindMap.execCommand('SET_NODE_ICON', this, icons) +} + +// 设置超链接 +function setHyperlink(link, title) { + this.mindMap.execCommand('SET_NODE_HYPERLINK', this, link, title) +} + +// 设置备注 +function setNote(note) { + this.mindMap.execCommand('SET_NODE_NOTE', this, note) +} + +// 设置标签 +function setTag(tag) { + this.mindMap.execCommand('SET_NODE_TAG', this, tag) +} + +// 设置形状 +function setShape(shape) { + this.mindMap.execCommand('SET_NODE_SHAPE', this, shape) +} + +// 修改某个样式 +function setStyle(prop, value, isActive) { + this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value, isActive) +} + +export default { + setData, + setText, + setImage, + setIcon, + setHyperlink, + setNote, + setTag, + setShape, + setStyle +} diff --git a/simple-mind-map/src/utils/nodeCreateContents.js b/simple-mind-map/src/utils/nodeCreateContents.js new file mode 100644 index 00000000..e2ec64c5 --- /dev/null +++ b/simple-mind-map/src/utils/nodeCreateContents.js @@ -0,0 +1,256 @@ +import { measureText, resizeImgSize } from '../utils' +import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js' +import iconsSvg from '../svg/icons' + +// 创建图片节点 +function createImgNode() { + let img = this.nodeData.data.image + if (!img) { + return + } + let imgSize = this.getImgShowSize() + let node = new Image().load(img).size(...imgSize) + if (this.nodeData.data.imageTitle) { + node.attr('title', this.nodeData.data.imageTitle) + } + node.on('dblclick', e => { + this.mindMap.emit('node_img_dblclick', this, e) + }) + return { + node, + width: imgSize[0], + height: imgSize[1] + } +} + +// 获取图片显示宽高 +function getImgShowSize() { + return resizeImgSize( + this.nodeData.data.imageSize.width, + this.nodeData.data.imageSize.height, + this.mindMap.themeConfig.imgMaxWidth, + this.mindMap.themeConfig.imgMaxHeight + ) +} + +// 创建icon节点 +function createIconNode() { + let _data = this.nodeData.data + if (!_data.icon || _data.icon.length <= 0) { + return [] + } + let iconSize = this.mindMap.themeConfig.iconSize + return _data.icon.map(item => { + return { + node: SVG(iconsSvg.getNodeIconListIcon(item)).size(iconSize, iconSize), + width: iconSize, + height: iconSize + } + }) +} + +// 创建富文本节点 +function createRichTextNode() { + let g = new G() + let html = `
${this.nodeData.data.text}
` + let div = document.createElement('div') + div.innerHTML = html + div.style.cssText = `position: fixed; left: -999999px;` + let el = div.children[0] + el.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml') + el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px' + this.mindMap.el.appendChild(div) + let { width, height } = el.getBoundingClientRect() + width = Math.ceil(width) + height = Math.ceil(height) + g.attr('data-width', width) + g.attr('data-height', height) + html = div.innerHTML + this.mindMap.el.removeChild(div) + let foreignObject = new ForeignObject() + foreignObject.width(width) + foreignObject.height(height) + foreignObject.add(SVG(html)) + g.add(foreignObject) + return { + node: g, + width, + height + } +} + +// 创建文本节点 +function createTextNode() { + if (this.nodeData.data.richText) { + return this.createRichTextNode() + } + let g = new G() + let fontSize = this.getStyle('fontSize', false, this.nodeData.data.isActive) + let lineHeight = this.getStyle( + 'lineHeight', + false, + this.nodeData.data.isActive + ) + // 文本超长自动换行 + let textStyle = this.style.getTextFontStyle() + let textArr = this.nodeData.data.text.split(/\n/gim) + let maxWidth = this.mindMap.opt.textAutoWrapWidth + textArr.forEach((item, index) => { + let arr = item.split('') + let lines = [] + let line = [] + while (arr.length) { + line.push(arr.shift()) + let text = line.join('') + if (measureText(text, textStyle).width >= maxWidth) { + lines.push(text) + line = [] + } + } + if (line.length > 0) { + lines.push(line.join('')) + } + textArr[index] = lines.join('\n') + }) + textArr = textArr.join('\n').split(/\n/gim) + textArr.forEach((item, index) => { + let node = new Text().text(item) + this.style.text(node) + node.y(fontSize * lineHeight * index) + g.add(node) + }) + let { width, height } = g.bbox() + width = Math.ceil(width) + height = Math.ceil(height) + g.attr('data-width', width) + g.attr('data-height', height) + return { + node: g, + width, + height + } +} + +// 创建超链接节点 +function createHyperlinkNode() { + let { hyperlink, hyperlinkTitle } = this.nodeData.data + if (!hyperlink) { + return + } + let iconSize = this.mindMap.themeConfig.iconSize + let node = new SVG() + // 超链接节点 + let a = new A().to(hyperlink).target('_blank') + a.node.addEventListener('click', e => { + e.stopPropagation() + }) + if (hyperlinkTitle) { + a.attr('title', hyperlinkTitle) + } + // 添加一个透明的层,作为鼠标区域 + a.rect(iconSize, iconSize).fill({ color: 'transparent' }) + // 超链接图标 + let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize) + this.style.iconNode(iconNode) + a.add(iconNode) + node.add(a) + return { + node, + width: iconSize, + height: iconSize + } +} + +// 创建标签节点 +function createTagNode() { + let tagData = this.nodeData.data.tag + if (!tagData || tagData.length <= 0) { + return [] + } + let nodes = [] + tagData.slice(0, this.mindMap.opt.maxTag).forEach((item, index) => { + let tag = new G() + // 标签文本 + let text = new Text().text(item).x(8).cy(10) + this.style.tagText(text, index) + let { width } = text.bbox() + // 标签矩形 + let rect = new Rect().size(width + 16, 20) + this.style.tagRect(rect, index) + tag.add(rect).add(text) + nodes.push({ + node: tag, + width: width + 16, + height: 20 + }) + }) + return nodes +} + +// 创建备注节点 +function createNoteNode() { + if (!this.nodeData.data.note) { + return null + } + let iconSize = this.mindMap.themeConfig.iconSize + let node = new SVG().attr('cursor', 'pointer') + // 透明的层,用来作为鼠标区域 + node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' })) + // 备注图标 + let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize) + this.style.iconNode(iconNode) + node.add(iconNode) + // 备注tooltip + if (!this.mindMap.opt.customNoteContentShow) { + if (!this.noteEl) { + this.noteEl = document.createElement('div') + this.noteEl.style.cssText = ` + position: absolute; + padding: 10px; + border-radius: 5px; + box-shadow: 0 2px 5px rgb(0 0 0 / 10%); + display: none; + background-color: #fff; + ` + document.body.appendChild(this.noteEl) + } + this.noteEl.innerText = this.nodeData.data.note + } + node.on('mouseover', () => { + let { left, top } = node.node.getBoundingClientRect() + if (!this.mindMap.opt.customNoteContentShow) { + this.noteEl.style.left = left + 'px' + this.noteEl.style.top = top + iconSize + 'px' + this.noteEl.style.display = 'block' + } else { + this.mindMap.opt.customNoteContentShow.show( + this.nodeData.data.note, + left, + top + iconSize + ) + } + }) + node.on('mouseout', () => { + if (!this.mindMap.opt.customNoteContentShow) { + this.noteEl.style.display = 'none' + } else { + this.mindMap.opt.customNoteContentShow.hide() + } + }) + return { + node, + width: iconSize, + height: iconSize + } +} + +export default { + createImgNode, + getImgShowSize, + createIconNode, + createRichTextNode, + createTextNode, + createHyperlinkNode, + createTagNode, + createNoteNode +} \ No newline at end of file diff --git a/simple-mind-map/src/utils/nodeExpandBtn.js b/simple-mind-map/src/utils/nodeExpandBtn.js new file mode 100644 index 00000000..e9f5ef06 --- /dev/null +++ b/simple-mind-map/src/utils/nodeExpandBtn.js @@ -0,0 +1,84 @@ +import btnsSvg from '../svg/btns' +import { SVG, Circle, G } from '@svgdotjs/svg.js' + +// 创建或更新展开收缩按钮内容 +function updateExpandBtnNode() { + if (this._expandBtn) { + this._expandBtn.clear() + } + let iconSvg + if (this.nodeData.data.expand === false) { + iconSvg = btnsSvg.open + } else { + iconSvg = btnsSvg.close + } + let node = SVG(iconSvg).size(this.expandBtnSize, this.expandBtnSize) + let fillNode = new Circle().size(this.expandBtnSize) + node.x(0).y(-this.expandBtnSize / 2) + fillNode.x(0).y(-this.expandBtnSize / 2) + this.style.iconBtn(node, fillNode) + if (this._expandBtn) this._expandBtn.add(fillNode).add(node) +} + +// 更新展开收缩按钮位置 +function updateExpandBtnPos() { + if (!this._expandBtn) { + return + } + this.renderer.layout.renderExpandBtn(this, this._expandBtn) +} + +// 创建展开收缩按钮 +function renderExpandBtn() { + if ( + !this.nodeData.children || + this.nodeData.children.length <= 0 || + this.isRoot + ) { + return + } + if (this._expandBtn) { + this.group.add(this._expandBtn) + } else { + this._expandBtn = new G() + this._expandBtn.on('mouseover', e => { + e.stopPropagation() + this._expandBtn.css({ + cursor: 'pointer' + }) + }) + this._expandBtn.on('mouseout', e => { + e.stopPropagation() + this._expandBtn.css({ + cursor: 'auto' + }) + }) + this._expandBtn.on('click', e => { + e.stopPropagation() + // 展开收缩 + this.mindMap.execCommand( + 'SET_NODE_EXPAND', + this, + !this.nodeData.data.expand + ) + this.mindMap.emit('expand_btn_click', this) + }) + this.group.add(this._expandBtn) + } + this.updateExpandBtnNode() + this.updateExpandBtnPos() +} + +// 移除展开收缩按钮 +function removeExpandBtn() { + if (this._expandBtn) { + this._expandBtn.remove() + } +} + +export default { + updateExpandBtnNode, + updateExpandBtnPos, + renderExpandBtn, + removeExpandBtn +} \ No newline at end of file diff --git a/simple-mind-map/src/utils/nodeGeneralization.js b/simple-mind-map/src/utils/nodeGeneralization.js new file mode 100644 index 00000000..6521de97 --- /dev/null +++ b/simple-mind-map/src/utils/nodeGeneralization.js @@ -0,0 +1,115 @@ +import Node from '../Node' + +// 检查是否存在概要 +function checkHasGeneralization () { + return !!this.nodeData.data.generalization +} + +// 创建概要节点 +function createGeneralizationNode () { + if (this.isGeneralization || !this.checkHasGeneralization()) { + return + } + if (!this._generalizationLine) { + this._generalizationLine = this.draw.path() + } + if (!this._generalizationNode) { + this._generalizationNode = new Node({ + data: { + data: this.nodeData.data.generalization + }, + uid: this.mindMap.uid++, + renderer: this.renderer, + mindMap: this.mindMap, + draw: this.draw, + isGeneralization: true + }) + this._generalizationNodeWidth = this._generalizationNode.width + this._generalizationNodeHeight = this._generalizationNode.height + this._generalizationNode.generalizationBelongNode = this + if (this.nodeData.data.generalization.isActive) { + this.renderer.addActiveNode(this._generalizationNode) + } + } +} + +// 更新概要节点 +function updateGeneralization () { + this.removeGeneralization() + this.createGeneralizationNode() +} + +// 渲染概要节点 +function renderGeneralization () { + if (this.isGeneralization) { + return + } + if (!this.checkHasGeneralization()) { + this.removeGeneralization() + this._generalizationNodeWidth = 0 + this._generalizationNodeHeight = 0 + return + } + if (this.nodeData.data.expand === false) { + this.removeGeneralization() + return + } + this.createGeneralizationNode() + this.renderer.layout.renderGeneralization( + this, + this._generalizationLine, + this._generalizationNode + ) + this.style.generalizationLine(this._generalizationLine) + this._generalizationNode.render() +} + +// 删除概要节点 +function removeGeneralization () { + if (this._generalizationLine) { + this._generalizationLine.remove() + this._generalizationLine = null + } + if (this._generalizationNode) { + // 删除概要节点时要同步从激活节点里删除 + this.renderer.removeActiveNode(this._generalizationNode) + this._generalizationNode.remove() + this._generalizationNode = null + } + // hack修复当激活一个节点时创建概要,然后立即激活创建的概要节点后会重复创建概要节点并且无法删除的问题 + if (this.generalizationBelongNode) { + this.draw + .find('.generalization_' + this.generalizationBelongNode.uid) + .remove() + } +} + +// 隐藏概要节点 +function hideGeneralization () { + if (this._generalizationLine) { + this._generalizationLine.hide() + } + if (this._generalizationNode) { + this._generalizationNode.hide() + } +} + +// 显示概要节点 +function showGeneralization () { + if (this._generalizationLine) { + this._generalizationLine.show() + } + if (this._generalizationNode) { + this._generalizationNode.show() + } +} + +export default { + checkHasGeneralization, + createGeneralizationNode, + updateGeneralization, + renderGeneralization, + removeGeneralization, + hideGeneralization, + showGeneralization +} \ No newline at end of file