From 7a248723311820eb49b672bab356bf8b1dfdab52 Mon Sep 17 00:00:00 2001 From: wanglin2 <1013335014@qq.com> Date: Tue, 14 Feb 2023 09:35:56 +0800 Subject: [PATCH] =?UTF-8?q?Fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E6=A0=B9?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=97=A0=E6=B3=95=E9=BB=84=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple-mind-map/package.json | 2 +- simple-mind-map/src/Node.js | 2343 +++++++++++----------- web/src/pages/Doc/en/changelog/index.md | 4 + web/src/pages/Doc/en/changelog/index.vue | 2 + web/src/pages/Doc/zh/changelog/index.md | 4 + web/src/pages/Doc/zh/changelog/index.vue | 2 + 6 files changed, 1215 insertions(+), 1142 deletions(-) diff --git a/simple-mind-map/package.json b/simple-mind-map/package.json index d8164d0b..927d88af 100644 --- a/simple-mind-map/package.json +++ b/simple-mind-map/package.json @@ -1,6 +1,6 @@ { "name": "simple-mind-map", - "version": "0.3.2", + "version": "0.3.3", "description": "一个简单的web在线思维导图", "authors": [ { diff --git a/simple-mind-map/src/Node.js b/simple-mind-map/src/Node.js index 34b749e3..1e6a9cbe 100644 --- a/simple-mind-map/src/Node.js +++ b/simple-mind-map/src/Node.js @@ -1,1141 +1,1202 @@ -import Style from './Style' -import Shape from './Shape' -import { resizeImgSize, asyncRun } from './utils' -import { Image, SVG, Circle, A, G, Rect, Text } from '@svgdotjs/svg.js' -import btnsSvg from './svg/btns' -import iconsSvg from './svg/icons' - -// 节点类 -class Node { - // 构造函数 - constructor(opt = {}) { - // 节点数据 - this.nodeData = this.handleData(opt.data || {}) - // id - this.uid = opt.uid - // 控制实例 - this.mindMap = opt.mindMap - // 渲染实例 - this.renderer = opt.renderer - // 渲染器 - this.draw = opt.draw || null - // 主题配置 - this.themeConfig = this.mindMap.themeConfig - // 样式实例 - this.style = new Style(this, this.themeConfig) - // 形状实例 - 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.group = null - // 节点内容对象 - this._imgData = null - this._iconData = null - this._textData = null - this._hyperlinkData = null - this._tagData = null - this._noteData = null - this.noteEl = null - this._expandBtn = null - this._lines = [] - this._generalizationLine = null - this._generalizationNode = null - // 尺寸信息 - this._rectInfo = { - imgContentWidth: 0, - imgContentHeight: 0, - textContentWidth: 0, - textContentHeight: 0 - } - // 概要节点的宽高 - this._generalizationNodeWidth = 0 - this._generalizationNodeHeight = 0 - // 各种文字信息的间距 - this.textContentItemMargin = this.mindMap.opt.textContentMargin - // 图片和文字节点的间距 - this.blockContentMargin = this.mindMap.opt.imgTextMargin - // 展开收缩按钮尺寸 - this.expandBtnSize = this.mindMap.opt.expandBtnSize - // 初始渲染 - this.initRender = true - // 初始化 - // this.createNodeData() - 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 - } - - // 更新主题配置 - updateThemeConfig() { - // 主题配置 - this.themeConfig = this.mindMap.themeConfig - // 样式实例 - this.style.updateThemeConfig(this.themeConfig) - } - - // 复位部分布局时会重新设置的数据 - reset() { - this.children = [] - this.parent = null - this.isRoot = false - this.layerIndex = 0 - this.left = 0 - this.top = 0 - } - - // 处理数据 - 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 - } - - // 检查节点是否存在自定义数据 - 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() - this._iconData = this.createIconNode() - this._textData = this.createTextNode() - 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' - ]) - } - } - - // 移除节点内容 - 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.removeAllNode() - this.createNodeData() - let { width, height } = this.getNodeRect() - // 判断节点尺寸是否有变化 - let changed = this.width !== width || this.height !== height - this.width = width - this.height = height - return changed - } - - // 计算节点尺寸信息 - getNodeRect() { - // 宽高 - let imgContentWidth = 0 - let imgContentHeight = 0 - let textContentWidth = 0 - let textContentHeight = 0 - // 存在图片 - if (this._imgData) { - this._rectInfo.imgContentWidth = imgContentWidth = this._imgData.width - this._rectInfo.imgContentHeight = imgContentHeight = this._imgData.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) { - textContentWidth += this._tagData.reduce((sum, cur) => { - textContentHeight = Math.max(textContentHeight, cur.height) - return (sum += cur.width + this.textContentItemMargin) - }, 0) - } - // 备注 - if (this._noteData) { - textContentWidth += this._noteData.width - textContentHeight = Math.max(textContentHeight, this._noteData.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 - // 计算节点形状需要的附加内边距 - let { paddingX: shapePaddingX, paddingY: shapePaddingY } = - this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY) - this.shapePadding.paddingX = shapePaddingX - this.shapePadding.paddingY = shapePaddingY - return { - width: _width + paddingX * 2 + shapePaddingX * 2, - height: _height + paddingY * 2 + margin + shapePaddingY * 2 - } - } - - // 创建图片节点 - 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.themeConfig.imgMaxWidth, - this.themeConfig.imgMaxHeight - ) - } - - // 创建icon节点 - createIconNode() { - let _data = this.nodeData.data - if (!_data.icon || _data.icon.length <= 0) { - return [] - } - let iconSize = this.themeConfig.iconSize - return _data.icon.map(item => { - return { - node: SVG(iconsSvg.getNodeIconListIcon(item)).size(iconSize, iconSize), - width: iconSize, - height: iconSize - } - }) - } - - // 创建文本节点 - createTextNode() { - let g = new G() - let fontSize = this.getStyle( - 'fontSize', - this.isRoot, - this.nodeData.data.isActive - ) - let lineHeight = this.getStyle( - 'lineHeight', - this.isRoot, - this.nodeData.data.isActive - ) - this.nodeData.data.text.split(/\n/gim).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() - return { - node: g, - width, - height - } - } - - // 创建超链接节点 - createHyperlinkNode() { - let { hyperlink, hyperlinkTitle } = this.nodeData.data - if (!hyperlink) { - return - } - let iconSize = this.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.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.themeConfig.nodeUseLineStyle - ? 'rectangle' - : this.style.getStyle('shape', false, false) - } - - // 定位节点内容 - layout() { - let { width, textContentItemMargin } = this - let { paddingY } = this.getPaddingVale() - paddingY += this.shapePadding.paddingY - // 创建组 - this.group = new G() - // 概要节点添加一个带所属节点id的类名 - if (this.isGeneralization && this.generalizationBelongNode) { - this.group.addClass('generalization_' + this.generalizationBelongNode.uid) - } - this.draw.add(this.group) - this.update(true) - // 节点形状 - const shape = this.getShape() - this.style[shape === 'rectangle' ? 'rect' : 'shape']( - this.shapeInstance.createShape() - ) - // 图片节点 - 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 - // 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((this._rectInfo.textContentHeight - item.height) / 2) - iconNested.add(item.node) - iconLeft += item.width + textContentItemMargin - }) - textContentNested.add(iconNested) - textContentOffsetX += iconLeft - } - // 文字 - if (this._textData) { - this._textData.node.x(textContentOffsetX).y(0) - textContentNested.add(this._textData.node) - textContentOffsetX += this._textData.width + textContentItemMargin - } - // 超链接 - if (this._hyperlinkData) { - this._hyperlinkData.node - .x(textContentOffsetX) - .y((this._rectInfo.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) { - let tagLeft = 0 - this._tagData.forEach(item => { - item.node - .x(textContentOffsetX + tagLeft) - .y((this._rectInfo.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((this._rectInfo.textContentHeight - this._noteData.height) / 2) - textContentNested.add(this._noteData.node) - textContentOffsetX += this._noteData.width - } - // 文字内容整体 - textContentNested.translate( - width / 2 - textContentNested.bbox().width / 2, - imgHeight + - paddingY + - (imgHeight > 0 && this._rectInfo.textContentHeight > 0 - ? this.blockContentMargin - : 0) - ) - this.group.add(textContentNested) - // 单击事件,选中节点 - this.group.on('click', e => { - this.mindMap.emit('node_click', this, e) - this.active(e) - }) - this.group.on('mousedown', e => { - e.stopPropagation() - this.mindMap.emit('node_mousedown', this, e) - }) - this.group.on('mouseup', e => { - e.stopPropagation() - this.mindMap.emit('node_mouseup', this, e) - }) - // 双击事件 - this.group.on('dblclick', e => { - if (this.mindMap.opt.readonly) { - return - } - e.stopPropagation() - this.mindMap.emit('node_dblclick', this, e) - }) - // 右键菜单事件 - this.group.on('contextmenu', e => { - if (this.mindMap.opt.readonly || this.isGeneralization) { - return - } - e.stopPropagation() - e.preventDefault() - if (this.nodeData.data.isActive) { - this.renderer.clearActive() - } - this.active(e) - this.mindMap.emit('node_contextmenu', e, this) - }) - } - - // 激活节点 - active(e) { - if (this.mindMap.opt.readonly) { - return - } - e && e.stopPropagation() - if (this.nodeData.data.isActive) { - return - } - this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList) - this.renderer.clearActive() - this.mindMap.execCommand('SET_NODE_ACTIVE', this, true) - this.renderer.addActiveNode(this) - this.mindMap.emit('node_active', this, this.renderer.activeNodeList) - } - - // 渲染节点到画布,会移除旧的,创建新的 - renderNode() { - // 连线 - this.renderLine() - this.removeAllEvent() - this.removeAllNode() - this.createNodeData() - this.layout() - } - - // 更新节点 - update(layout = false) { - if (!this.group) { - return - } - // 需要移除展开收缩按钮 - if (this._expandBtn && this.nodeData.children.length <= 0) { - this.removeExpandBtn() - } else if (!this._expandBtn && this.nodeData.children.length > 0) { - // 需要添加展开收缩按钮 - this.renderExpandBtn() - } else { - this.updateExpandBtnPos() - } - this.renderGeneralization() - let t = this.group.transform() - // 节点使用横线风格,有两种结构需要调整节点的位置 - let nodeUseLineStyleOffset = 0 - if ( - ['logicalStructure', 'mindMap'].includes(this.mindMap.opt.layout) && - !this.isRoot && - !this.isGeneralization && - this.themeConfig.nodeUseLineStyle - ) { - nodeUseLineStyleOffset = this.height / 2 - } - if (!layout) { - this.group - .animate(300) - .translate( - this.left - t.translateX, - this.top - t.translateY - nodeUseLineStyleOffset - ) - } else { - this.group.translate( - this.left - t.translateX, - this.top - t.translateY - nodeUseLineStyleOffset - ) - } - } - - // 递归渲染 - render(callback = () => {}) { - // 节点 - if (this.initRender) { - this.initRender = false - this.renderNode() - } else { - // 连线 - this.renderLine() - this.update() - } - // 子节点 - if ( - this.children && - this.children.length && - this.nodeData.data.expand !== false - ) { - let index = 0 - asyncRun( - this.children.map(item => { - return () => { - item.render(() => { - index++ - if (index >= this.children.length) { - callback() - } - }) - } - }) - ) - } else { - callback() - } - // 手动插入的节点立即获得焦点并且开启编辑模式 - if (this.nodeData.inserting) { - delete this.nodeData.inserting - this.active() - this.mindMap.emit('node_dblclick', this) - } - } - - // 递归删除 - remove() { - this.initRender = true - this.removeAllEvent() - this.removeAllNode() - this.removeLine() - // 子节点 - if (this.children && this.children.length) { - asyncRun( - this.children.map(item => { - return () => { - item.remove() - } - }) - ) - } - } - - // 隐藏节点 - hide() { - this.group.hide() - this.hideGeneralization() - if (this.parent) { - let index = this.parent.children.indexOf(this) - this.parent._lines[index].hide() - } - // 子节点 - if (this.children && this.children.length) { - asyncRun( - this.children.map(item => { - return () => { - 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() - } - // 子节点 - if (this.children && this.children.length) { - asyncRun( - this.children.map(item => { - return () => { - item.show() - } - }) - ) - } - } - - // 连线 - renderLine(deep = false) { - if (this.nodeData.data.expand === false) { - return - } - let childrenLen = this.nodeData.children.length - if (childrenLen > this._lines.length) { - // 创建缺少的线 - new Array(childrenLen - this._lines.length).fill(0).forEach(() => { - this._lines.push(this.draw.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, - (line, node) => { - // 添加样式 - this.styleLine(line, node) - }, - this.style.getStyle('lineStyle', true) - ) - // 子级的连线也需要更新 - if (deep && this.children && this.children.length > 0) { - this.children.forEach(item => { - item.renderLine(deep) - }) - } - } - - // 设置连线样式 - styleLine(line, node) { - let width = - node.getSelfInhertStyle('lineWidth') || node.getStyle('lineWidth', true) - let color = - node.getSelfInhertStyle('lineColor') || node.getStyle('lineColor', true) - let dasharray = - node.getSelfInhertStyle('lineDasharray') || - node.getStyle('lineDasharray', true) - this.style.line(line, { - width, - color, - dasharray - }) - } - - // 移除连线 - removeLine() { - this._lines.forEach(line => { - line.remove() - }) - 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) { - return false - } - let parent = node.parent - while (parent) { - if (this === parent) { - return true - } - parent = parent.parent - } - return false - } - - // 检测当前节点是否是某个节点的兄弟节点 - isBrother(node) { - if (!this.parent || this === node) { - return false - } - return this.parent.children.find(item => { - return item === node - }) - } - - // 获取padding值 - getPaddingVale() { - return { - paddingX: this.getStyle('paddingX', true, this.nodeData.data.isActive), - paddingY: this.getStyle('paddingY', true, this.nodeData.data.isActive) - } - } - - // 获取某个样式 - getStyle(prop, root, isActive) { - let v = this.style.merge(prop, root, isActive) - 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) - ) // 父级 - } - - // 修改某个样式 - 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) { - this.mindMap.execCommand('SET_NODE_TEXT', this, text) - } - - // 设置图片 - 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 +import Style from './Style' +import Shape from './Shape' +import { resizeImgSize, asyncRun } from './utils' +import { Image, SVG, Circle, A, G, Rect, Text } from '@svgdotjs/svg.js' +import btnsSvg from './svg/btns' +import iconsSvg from './svg/icons' + +// 节点类 + +class Node { + // 构造函数 + + constructor(opt = {}) { + // 节点数据 + this.nodeData = this.handleData(opt.data || {}) + // id + this.uid = opt.uid + // 控制实例 + this.mindMap = opt.mindMap + // 渲染实例 + this.renderer = opt.renderer + // 渲染器 + this.draw = opt.draw || null + // 主题配置 + this.themeConfig = this.mindMap.themeConfig + // 样式实例 + this.style = new Style(this, this.themeConfig) + // 形状实例 + 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.group = null + // 节点内容对象 + this._imgData = null + this._iconData = null + this._textData = null + this._hyperlinkData = null + this._tagData = null + this._noteData = null + this.noteEl = null + this._expandBtn = null + this._lines = [] + this._generalizationLine = null + this._generalizationNode = null + // 尺寸信息 + this._rectInfo = { + imgContentWidth: 0, + imgContentHeight: 0, + textContentWidth: 0, + textContentHeight: 0 + } + // 概要节点的宽高 + this._generalizationNodeWidth = 0 + this._generalizationNodeHeight = 0 + // 各种文字信息的间距 + this.textContentItemMargin = this.mindMap.opt.textContentMargin + // 图片和文字节点的间距 + this.blockContentMargin = this.mindMap.opt.imgTextMargin + // 展开收缩按钮尺寸 + this.expandBtnSize = this.mindMap.opt.expandBtnSize + // 初始渲染 + this.initRender = true + // 初始化 + // this.createNodeData() + 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 + } + + // 更新主题配置 + + updateThemeConfig() { + // 主题配置 + this.themeConfig = this.mindMap.themeConfig + // 样式实例 + this.style.updateThemeConfig(this.themeConfig) + } + + // 复位部分布局时会重新设置的数据 + + reset() { + this.children = [] + this.parent = null + this.isRoot = false + this.layerIndex = 0 + this.left = 0 + this.top = 0 + } + + // 处理数据 + + 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 + } + + // 检查节点是否存在自定义数据 + + 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() + this._iconData = this.createIconNode() + this._textData = this.createTextNode() + 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' + ]) + } + } + + // 移除节点内容 + + 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.removeAllNode() + this.createNodeData() + let { width, height } = this.getNodeRect() + // 判断节点尺寸是否有变化 + let changed = this.width !== width || this.height !== height + this.width = width + this.height = height + return changed + } + + // 计算节点尺寸信息 + + getNodeRect() { + // 宽高 + let imgContentWidth = 0 + let imgContentHeight = 0 + let textContentWidth = 0 + let textContentHeight = 0 + // 存在图片 + if (this._imgData) { + this._rectInfo.imgContentWidth = imgContentWidth = this._imgData.width + this._rectInfo.imgContentHeight = imgContentHeight = this._imgData.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) { + textContentWidth += this._tagData.reduce((sum, cur) => { + textContentHeight = Math.max(textContentHeight, cur.height) + return (sum += cur.width + this.textContentItemMargin) + }, 0) + } + // 备注 + if (this._noteData) { + textContentWidth += this._noteData.width + textContentHeight = Math.max(textContentHeight, this._noteData.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 + // 计算节点形状需要的附加内边距 + let { paddingX: shapePaddingX, paddingY: shapePaddingY } = + this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY) + this.shapePadding.paddingX = shapePaddingX + this.shapePadding.paddingY = shapePaddingY + return { + width: _width + paddingX * 2 + shapePaddingX * 2, + height: _height + paddingY * 2 + margin + shapePaddingY * 2 + } + } + + // 创建图片节点 + + 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.themeConfig.imgMaxWidth, + this.themeConfig.imgMaxHeight + ) + } + + // 创建icon节点 + + createIconNode() { + let _data = this.nodeData.data + if (!_data.icon || _data.icon.length <= 0) { + return [] + } + let iconSize = this.themeConfig.iconSize + return _data.icon.map(item => { + return { + node: SVG(iconsSvg.getNodeIconListIcon(item)).size(iconSize, iconSize), + width: iconSize, + height: iconSize + } + }) + } + + // 创建文本节点 + + createTextNode() { + let g = new G() + let fontSize = this.getStyle( + 'fontSize', + false, + this.nodeData.data.isActive + ) + let lineHeight = this.getStyle( + 'lineHeight', + false, + this.nodeData.data.isActive + ) + this.nodeData.data.text.split(/\n/gim).forEach((item, index) => { + let node = new Text().text(item) + this.style.text(node) + console.log(this.isRoot, fontSize, lineHeight, index); + node.y(fontSize * lineHeight * index) + g.add(node) + }) + let { width, height } = g.bbox() + return { + node: g, + width, + height + } + } + + // 创建超链接节点 + + createHyperlinkNode() { + let { hyperlink, hyperlinkTitle } = this.nodeData.data + if (!hyperlink) { + return + } + let iconSize = this.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.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.themeConfig.nodeUseLineStyle + ? 'rectangle' + : this.style.getStyle('shape', false, false) + } + + // 定位节点内容 + + layout() { + let { width, textContentItemMargin } = this + let { paddingY } = this.getPaddingVale() + paddingY += this.shapePadding.paddingY + // 创建组 + this.group = new G() + // 概要节点添加一个带所属节点id的类名 + if (this.isGeneralization && this.generalizationBelongNode) { + this.group.addClass('generalization_' + this.generalizationBelongNode.uid) + } + this.draw.add(this.group) + this.update(true) + // 节点形状 + const shape = this.getShape() + this.style[shape === 'rectangle' ? 'rect' : 'shape']( + this.shapeInstance.createShape() + ) + // 图片节点 + 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 + // 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((this._rectInfo.textContentHeight - item.height) / 2) + iconNested.add(item.node) + iconLeft += item.width + textContentItemMargin + }) + textContentNested.add(iconNested) + textContentOffsetX += iconLeft + } + // 文字 + if (this._textData) { + this._textData.node.x(textContentOffsetX).y(0) + textContentNested.add(this._textData.node) + textContentOffsetX += this._textData.width + textContentItemMargin + } + // 超链接 + if (this._hyperlinkData) { + this._hyperlinkData.node + .x(textContentOffsetX) + .y((this._rectInfo.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) { + let tagLeft = 0 + this._tagData.forEach(item => { + item.node + .x(textContentOffsetX + tagLeft) + .y((this._rectInfo.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((this._rectInfo.textContentHeight - this._noteData.height) / 2) + textContentNested.add(this._noteData.node) + textContentOffsetX += this._noteData.width + } + // 文字内容整体 + textContentNested.translate( + width / 2 - textContentNested.bbox().width / 2, + imgHeight + + paddingY + + (imgHeight > 0 && this._rectInfo.textContentHeight > 0 + ? this.blockContentMargin + : 0) + ) + this.group.add(textContentNested) + // 单击事件,选中节点 + this.group.on('click', e => { + this.mindMap.emit('node_click', this, e) + this.active(e) + }) + this.group.on('mousedown', e => { + e.stopPropagation() + this.mindMap.emit('node_mousedown', this, e) + }) + this.group.on('mouseup', e => { + e.stopPropagation() + this.mindMap.emit('node_mouseup', this, e) + }) + // 双击事件 + this.group.on('dblclick', e => { + if (this.mindMap.opt.readonly) { + return + } + e.stopPropagation() + this.mindMap.emit('node_dblclick', this, e) + }) + // 右键菜单事件 + this.group.on('contextmenu', e => { + if (this.mindMap.opt.readonly || this.isGeneralization) { + return + } + e.stopPropagation() + e.preventDefault() + if (this.nodeData.data.isActive) { + this.renderer.clearActive() + } + this.active(e) + this.mindMap.emit('node_contextmenu', e, this) + }) + } + + // 激活节点 + + active(e) { + if (this.mindMap.opt.readonly) { + return + } + e && e.stopPropagation() + if (this.nodeData.data.isActive) { + return + } + this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList) + this.renderer.clearActive() + this.mindMap.execCommand('SET_NODE_ACTIVE', this, true) + this.renderer.addActiveNode(this) + this.mindMap.emit('node_active', this, this.renderer.activeNodeList) + } + + // 渲染节点到画布,会移除旧的,创建新的 + + renderNode() { + // 连线 + this.renderLine() + this.removeAllEvent() + this.removeAllNode() + this.createNodeData() + this.layout() + } + + // 更新节点 + + update(layout = false) { + if (!this.group) { + return + } + // 需要移除展开收缩按钮 + if (this._expandBtn && this.nodeData.children.length <= 0) { + this.removeExpandBtn() + } else if (!this._expandBtn && this.nodeData.children.length > 0) { + // 需要添加展开收缩按钮 + this.renderExpandBtn() + } else { + this.updateExpandBtnPos() + } + this.renderGeneralization() + let t = this.group.transform() + // 节点使用横线风格,有两种结构需要调整节点的位置 + let nodeUseLineStyleOffset = 0 + if ( + ['logicalStructure', 'mindMap'].includes(this.mindMap.opt.layout) && + !this.isRoot && + !this.isGeneralization && + this.themeConfig.nodeUseLineStyle + ) { + nodeUseLineStyleOffset = this.height / 2 + } + if (!layout) { + this.group + .animate(300) + .translate( + this.left - t.translateX, + this.top - t.translateY - nodeUseLineStyleOffset + ) + } else { + this.group.translate( + this.left - t.translateX, + this.top - t.translateY - nodeUseLineStyleOffset + ) + } + } + + // 递归渲染 + + render(callback = () => {}) { + // 节点 + if (this.initRender) { + this.initRender = false + this.renderNode() + } else { + // 连线 + this.renderLine() + this.update() + } + // 子节点 + if ( + this.children && + this.children.length && + this.nodeData.data.expand !== false + ) { + let index = 0 + asyncRun( + this.children.map(item => { + return () => { + item.render(() => { + index++ + if (index >= this.children.length) { + callback() + } + }) + } + }) + ) + } else { + callback() + } + // 手动插入的节点立即获得焦点并且开启编辑模式 + if (this.nodeData.inserting) { + delete this.nodeData.inserting + this.active() + this.mindMap.emit('node_dblclick', this) + } + } + + // 递归删除 + + remove() { + this.initRender = true + this.removeAllEvent() + this.removeAllNode() + this.removeLine() + // 子节点 + if (this.children && this.children.length) { + asyncRun( + this.children.map(item => { + return () => { + item.remove() + } + }) + ) + } + } + + // 隐藏节点 + + hide() { + this.group.hide() + this.hideGeneralization() + if (this.parent) { + let index = this.parent.children.indexOf(this) + this.parent._lines[index].hide() + } + // 子节点 + if (this.children && this.children.length) { + asyncRun( + this.children.map(item => { + return () => { + 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() + } + // 子节点 + if (this.children && this.children.length) { + asyncRun( + this.children.map(item => { + return () => { + item.show() + } + }) + ) + } + } + + // 连线 + + renderLine(deep = false) { + if (this.nodeData.data.expand === false) { + return + } + let childrenLen = this.nodeData.children.length + if (childrenLen > this._lines.length) { + // 创建缺少的线 + new Array(childrenLen - this._lines.length).fill(0).forEach(() => { + this._lines.push(this.draw.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, + (line, node) => { + // 添加样式 + this.styleLine(line, node) + }, + this.style.getStyle('lineStyle', true) + ) + // 子级的连线也需要更新 + if (deep && this.children && this.children.length > 0) { + this.children.forEach(item => { + item.renderLine(deep) + }) + } + } + + // 设置连线样式 + + styleLine(line, node) { + let width = + node.getSelfInhertStyle('lineWidth') || node.getStyle('lineWidth', true) + let color = + node.getSelfInhertStyle('lineColor') || node.getStyle('lineColor', true) + let dasharray = + node.getSelfInhertStyle('lineDasharray') || + node.getStyle('lineDasharray', true) + this.style.line(line, { + width, + color, + dasharray + }) + } + + // 移除连线 + + removeLine() { + this._lines.forEach(line => { + line.remove() + }) + 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) { + return false + } + let parent = node.parent + while (parent) { + if (this === parent) { + return true + } + parent = parent.parent + } + return false + } + + // 检测当前节点是否是某个节点的兄弟节点 + + isBrother(node) { + if (!this.parent || this === node) { + return false + } + return this.parent.children.find(item => { + return item === node + }) + } + + // 获取padding值 + + getPaddingVale() { + return { + paddingX: this.getStyle('paddingX', true, this.nodeData.data.isActive), + paddingY: this.getStyle('paddingY', true, this.nodeData.data.isActive) + } + } + + // 获取某个样式 + + getStyle(prop, root, isActive) { + let v = this.style.merge(prop, root, isActive) + 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) + ) // 父级 + } + + // 修改某个样式 + + 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) { + this.mindMap.execCommand('SET_NODE_TEXT', this, text) + } + + // 设置图片 + + 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/web/src/pages/Doc/en/changelog/index.md b/web/src/pages/Doc/en/changelog/index.md index 8a0e5098..15507b77 100644 --- a/web/src/pages/Doc/en/changelog/index.md +++ b/web/src/pages/Doc/en/changelog/index.md @@ -1,5 +1,9 @@ # Changelog +## 0.3.3 + +Fix: The root node text cannot wrap. + ## 0.3.2 Fix: 1.Fix the problem that the node style is not updated when the secondary node is dragged to other nodes or other nodes are dragged to the secondary node; 2.Fix the problem that when the actual content of the mind map is larger than the screen width and height, the excess part is not watermarked when exporting. diff --git a/web/src/pages/Doc/en/changelog/index.vue b/web/src/pages/Doc/en/changelog/index.vue index 49e2f474..9de415dc 100644 --- a/web/src/pages/Doc/en/changelog/index.vue +++ b/web/src/pages/Doc/en/changelog/index.vue @@ -1,6 +1,8 @@