diff --git a/README.md b/README.md index 999f18cd..f424d638 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Demo:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-ma # 特性 +- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小整体体积 - [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图四种结构 - [x] 内置多种主题,允许高度自定义样式,支持注册新主题 - [x] 支持快捷键 @@ -24,8 +25,8 @@ Demo:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-ma - [x] 支持右键和Ctrl+左键两种多选方式 - [x] 支持节点自由拖拽、拖拽调整 - [x] 支持多种节点形状 -- [x] 支持导出为`json`、`png`、`svg`、`pdf`,支持从`json`、`xmind`导入 -- [x] 支持小地图 +- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`,支持从`json`、`xmind`、`markdown`导入 +- [x] 支持小地图、支持水印 - [x] 支持关联线 # 安装 diff --git a/index.html b/index.html index 119b9651..de42302b 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -一个简单的web思维导图实现
\ No newline at end of file +一个简单的web思维导图实现
\ No newline at end of file diff --git a/simple-mind-map/index.js b/simple-mind-map/index.js index 314c9ff6..022c7f68 100644 --- a/simple-mind-map/index.js +++ b/simple-mind-map/index.js @@ -7,7 +7,7 @@ import Style from './src/Style' import KeyCommand from './src/KeyCommand' import Command from './src/Command' import BatchExecution from './src/BatchExecution' -import { layoutValueList } from './src/utils/constant' +import { layoutValueList, CONSTANTS } from './src/utils/constant' import { SVG } from '@svgdotjs/svg.js' import { simpleDeepClone } from './src/utils' import defaultTheme from './src/themes/default' @@ -17,7 +17,7 @@ const defaultOpt = { // 是否只读 readonly: false, // 布局 - layout: 'logicalStructure', + layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE, // 主题 theme: 'default', // 内置主题:default(默认主题) // 主题配置,会和所选择的主题进行合并 @@ -66,13 +66,23 @@ const defaultOpt = { // 可以传一个函数,回调参数为事件对象 customHandleMousewheel: null, // 鼠标滚动的行为,如果customHandleMousewheel传了自定义函数,这个属性不生效 - mousewheelAction: 'zoom',// zoom(放大缩小)、move(上下移动) + mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM,// zoom(放大缩小)、move(上下移动) // 当mousewheelAction设为move时,可以通过该属性控制鼠标滚动一下视图移动的步长,单位px mousewheelMoveStep: 100, // 默认插入的二级节点的文字 defaultInsertSecondLevelNodeText: '二级节点', // 默认插入的二级以下节点的文字 - defaultInsertBelowSecondLevelNodeText: '分支主题' + defaultInsertBelowSecondLevelNodeText: '分支主题', + // 展开收起按钮的颜色 + expandBtnStyle: { + color: '#808080', + fill: '#fff' + }, + // 自定义展开收起按钮的图标 + expandBtnIcon: { + open: '',// svg字符串 + close: '' + } } // 思维导图 @@ -95,7 +105,7 @@ class MindMap { this.draw = this.svg.group() // 节点id - this.uid = 0 + this.uid = 1 // 初始化主题 this.initTheme() @@ -135,7 +145,7 @@ class MindMap { }) // 初始渲染 - this.reRender() + this.render() setTimeout(() => { this.command.addHistory() }, 0) @@ -145,7 +155,7 @@ class MindMap { handleOpt(opt) { // 检查布局配置 if (!layoutValueList.includes(opt.layout)) { - opt.layout = 'logicalStructure' + opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE } // 检查主题配置 opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default' @@ -153,11 +163,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 +216,7 @@ class MindMap { setTheme(theme) { this.renderer.clearAllActive() this.opt.theme = theme - this.reRender() + this.render(null, CONSTANTS.CHANGE_THEME) } // 获取当前主题 @@ -217,7 +227,7 @@ class MindMap { // 设置主题配置 setThemeConfig(config) { this.opt.themeConfig = config - this.reRender() + this.render(null, CONSTANTS.CHANGE_THEME) } // 获取自定义主题配置 @@ -249,7 +259,7 @@ class MindMap { setLayout(layout) { // 检查布局配置 if (!layoutValueList.includes(layout)) { - layout = 'logicalStructure' + layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE } this.opt.layout = layout this.renderer.setLayout() @@ -265,6 +275,7 @@ class MindMap { setData(data) { this.execCommand('CLEAR_ACTIVE_NODE') this.command.clearHistory() + this.command.addHistory() this.renderer.renderTree = data this.reRender() } @@ -326,10 +337,10 @@ class MindMap { // 设置只读模式、编辑模式 setMode(mode) { - if (!['readonly', 'edit'].includes(mode)) { + if (![CONSTANTS.MODE.READONLY, CONSTANTS.MODE.EDIT].includes(mode)) { return } - this.opt.readonly = mode === 'readonly' + this.opt.readonly = mode === CONSTANTS.MODE.READONLY if (this.opt.readonly) { // 取消当前激活的元素 this.renderer.clearAllActive() diff --git a/simple-mind-map/package.json b/simple-mind-map/package.json index ccc78cac..2b1ee325 100644 --- a/simple-mind-map/package.json +++ b/simple-mind-map/package.json @@ -1,6 +1,6 @@ { "name": "simple-mind-map", - "version": "0.4.7", + "version": "0.5.0", "description": "一个简单的web在线思维导图", "authors": [ { diff --git a/simple-mind-map/src/Command.js b/simple-mind-map/src/Command.js index 0d1880e2..7b49ef0d 100644 --- a/simple-mind-map/src/Command.js +++ b/simple-mind-map/src/Command.js @@ -85,7 +85,7 @@ class Command { this.history = this.history.slice(0, this.activeHistoryIndex + 1) this.history.push(simpleDeepClone(data)) this.activeHistoryIndex = this.history.length - 1 - this.mindMap.emit('data_change', data) + this.mindMap.emit('data_change', this.removeDataUid(data)) this.mindMap.emit( 'back_forward', this.activeHistoryIndex, @@ -106,7 +106,7 @@ class Command { this.history.length ) let data = simpleDeepClone(this.history[this.activeHistoryIndex]) - this.mindMap.emit('data_change', data) + this.mindMap.emit('data_change', this.removeDataUid(data)) return data } } @@ -119,9 +119,9 @@ class Command { let len = this.history.length if (this.activeHistoryIndex + step <= len - 1) { this.activeHistoryIndex += step - this.mindMap.emit('back_forward', this.activeHistoryIndex) + this.mindMap.emit('back_forward', this.activeHistoryIndex, this.history.length) let data = simpleDeepClone(this.history[this.activeHistoryIndex]) - this.mindMap.emit('data_change', data) + this.mindMap.emit('data_change', this.removeDataUid(data)) return data } } @@ -130,6 +130,21 @@ class Command { getCopyData() { return copyRenderTree({}, this.mindMap.renderer.renderTree, true) } + + // 移除节点数据中的uid + removeDataUid(data) { + data = simpleDeepClone(data) + let walk = (root) => { + delete root.data.uid + if (root.children && root.children.length > 0) { + root.children.forEach((item) => { + walk(item) + }) + } + } + walk(data) + return data + } } export default Command diff --git a/simple-mind-map/src/Drag.js b/simple-mind-map/src/Drag.js index 287cb989..ffadf0c5 100644 --- a/simple-mind-map/src/Drag.js +++ b/simple-mind-map/src/Drag.js @@ -5,7 +5,6 @@ import Base from './layouts/Base' class Drag extends Base { // 构造函数 - constructor({ mindMap }) { super(mindMap.renderer) this.mindMap = mindMap @@ -14,7 +13,6 @@ class Drag extends Base { } // 复位 - reset() { // 当前拖拽节点 this.node = null @@ -45,10 +43,11 @@ class Drag extends Base { this.mouseDownY = 0 this.mouseMoveX = 0 this.mouseMoveY = 0 + // 鼠标移动的距离距鼠标按下的位置距离多少以上才认为是拖动事件 + this.checkDragOffset = 10 } // 绑定事件 - bindEvent() { this.checkOverlapNode = throttle(this.checkOverlapNode, 300, this) this.mindMap.on('node_mousedown', (node, e) => { @@ -83,8 +82,8 @@ class Drag extends Base { this.mouseMoveX = x this.mouseMoveY = y if ( - Math.abs(x - this.mouseDownX) <= 10 && - Math.abs(y - this.mouseDownY) <= 10 && + Math.abs(x - this.mouseDownX) <= this.checkDragOffset && + Math.abs(y - this.mouseDownY) <= this.checkDragOffset && !this.node.isDrag ) { return @@ -98,7 +97,6 @@ class Drag extends Base { } // 鼠标松开事件 - onMouseup(e) { if (!this.isMousedown) { return @@ -141,7 +139,6 @@ class Drag extends Base { } // 创建克隆节点 - createCloneNode() { if (!this.clone) { // 节点 @@ -163,7 +160,6 @@ class Drag extends Base { } // 移除克隆节点 - removeCloneNode() { if (!this.clone) { return @@ -174,7 +170,6 @@ class Drag extends Base { } // 拖动中 - onMove(x, y) { if (!this.isMousedown) { return @@ -201,14 +196,12 @@ class Drag extends Base { } // 检测重叠节点 - checkOverlapNode() { if (!this.drawTransform) { return } - let { scaleX, scaleY, translateX, translateY } = this.drawTransform - let checkRight = this.cloneNodeLeft + this.node.width * scaleX - let checkBottom = this.cloneNodeTop + this.node.height * scaleX + let x = this.mouseMoveX + let y = this.mouseMoveY this.overlapNode = null this.prevNode = null this.nextNode = null @@ -223,35 +216,71 @@ class Drag extends Base { if (this.overlapNode || (this.prevNode && this.nextNode)) { return } - let { left, top, width, height } = node - let _left = left - let _top = top - let _bottom = top + height - let right = (left + width) * scaleX + translateX - let bottom = (top + height) * scaleY + translateY - left = left * scaleX + translateX - top = top * scaleY + translateY - // 检测是否重叠 - if (!this.overlapNode) { - if ( - left <= checkRight && - right >= this.cloneNodeLeft && - top <= checkBottom && - bottom >= this.cloneNodeTop - ) { - this.overlapNode = node + let nodeRect = this.getNodeRect(node) + let oneFourthHeight = nodeRect.height / 4 + // 前一个和后一个节点 + let checkList = node.parent ? node.parent.children.filter((item) => { + return item !== this.node + }) : [] + let index = checkList.findIndex((item) => { + return item === node + }) + let prevBrother = null + let nextBrother = null + if (index !== -1) { + if (index - 1 >= 0) { + prevBrother = checkList[index - 1] + } + if (index + 1 <= checkList.length - 1) { + nextBrother = checkList[index + 1] } } - // 检测兄弟节点位置 - if (!this.prevNode && !this.nextNode && !node.isRoot) { - // && this.node.isBrother(node) - if (left <= checkRight && right >= this.cloneNodeLeft) { - if (this.cloneNodeTop > bottom && this.cloneNodeTop <= bottom + 10) { + // 和前一个兄弟节点的距离 + let prevBrotherOffset = 0 + if (prevBrother) { + let prevNodeRect = this.getNodeRect(prevBrother) + prevBrotherOffset = nodeRect.top - prevNodeRect.bottom + // 间距小于10就当它不存在 + prevBrotherOffset = prevBrotherOffset >= 10 ? prevBrotherOffset / 2 : 0 + } else { + // 没有前一个兄弟节点,那么假设和前一个节点的距离为20 + prevBrotherOffset = 10 + } + // 和后一个兄弟节点的距离 + let nextBrotherOffset = 0 + if (nextBrother) { + let nextNodeRect = this.getNodeRect(nextBrother) + nextBrotherOffset = nextNodeRect.top - nodeRect.bottom + nextBrotherOffset = nextBrotherOffset >= 10 ? nextBrotherOffset / 2 : 0 + } else { + nextBrotherOffset = 10 + } + if (nodeRect.left <= x && nodeRect.right >= x) { + // 检测兄弟节点位置 + if (!this.overlapNode && !this.prevNode && !this.nextNode && !node.isRoot) { + let checkIsPrevNode = nextBrotherOffset > 0 ? // 距离下一个兄弟节点的距离大于0 + y > nodeRect.bottom && y <= (nodeRect.bottom + nextBrotherOffset) : // 那么在当前节点外底部判断 + y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断 + let checkIsNextNode = prevBrotherOffset > 0 ? // 距离上一个兄弟节点的距离大于0 + y < nodeRect.top && y >= (nodeRect.top - prevBrotherOffset) : // 那么在当前节点外底部判断 + y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight + if (checkIsPrevNode) { this.prevNode = node - this.placeholder.size(node.width, 10).move(_left, _bottom) - } else if (checkBottom < top && checkBottom >= top - 10) { + let size = nextBrotherOffset > 0 ? nextBrotherOffset : 5 + this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom) + } else if (checkIsNextNode) { this.nextNode = node - this.placeholder.size(node.width, 10).move(_left, _top - 10) + let size = prevBrotherOffset > 0 ? prevBrotherOffset : 5 + this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originTop - size) + } + } + // 检测是否重叠 + if (!this.overlapNode && !this.prevNode && !this.nextNode) { + if ( + nodeRect.top + (prevBrotherOffset > 0 ? 0 : oneFourthHeight) <= y && + nodeRect.bottom - (nextBrotherOffset > 0 ? 0 : oneFourthHeight) >= y + ) { + this.overlapNode = node } } } @@ -260,6 +289,30 @@ class Drag extends Base { this.mindMap.renderer.setNodeActive(this.overlapNode, true) } } + + // 计算节点的位置尺寸信息 + getNodeRect(node) { + let { scaleX, scaleY, translateX, translateY } = this.drawTransform + let { left, top, width, height } = node + let originLeft = left + let originTop = top + let originBottom = top + height + let right = (left + width) * scaleX + translateX + let bottom = (top + height) * scaleY + translateY + left = left * scaleX + translateX + top = top * scaleY + translateY + return { + width, + height, + left, + top, + right, + bottom, + originLeft, + originTop, + originBottom + } + } } Drag.instanceName = 'drag' diff --git a/simple-mind-map/src/Event.js b/simple-mind-map/src/Event.js index cccc79d7..39374272 100644 --- a/simple-mind-map/src/Event.js +++ b/simple-mind-map/src/Event.js @@ -1,4 +1,5 @@ import EventEmitter from 'eventemitter3' +import { CONSTANTS } from './utils/constant' // 事件类 class Event extends EventEmitter { @@ -71,7 +72,6 @@ class Event extends EventEmitter { // 鼠标按下事件 onMousedown(e) { - // e.preventDefault() // 鼠标左键 if (e.which === 1) { this.isLeftMousedown = true @@ -83,13 +83,13 @@ class Event extends EventEmitter { // 鼠标移动事件 onMousemove(e) { - // e.preventDefault() this.mousemovePos.x = e.clientX this.mousemovePos.y = e.clientY this.mousemoveOffset.x = e.clientX - this.mousedownPos.x this.mousemoveOffset.y = e.clientY - this.mousedownPos.y this.emit('mousemove', e, this) if (this.isLeftMousedown) { + e.preventDefault() this.emit('drag', e, this) } } @@ -107,15 +107,15 @@ class Event extends EventEmitter { let dir // 解决mac触控板双指缩放方向相反的问题 if (e.ctrlKey) { - if (e.deltaY > 0) dir = 'up' - if (e.deltaY < 0) dir = 'down' - if (e.deltaX > 0) dir = 'left' - if (e.deltaX < 0) dir = 'right' + if (e.deltaY > 0) dir = CONSTANTS.DIR.UP + if (e.deltaY < 0) dir = CONSTANTS.DIR.DOWN + if (e.deltaX > 0) dir = CONSTANTS.DIR.LEFT + if (e.deltaX < 0) dir = CONSTANTS.DIR.RIGHT } else { - if ((e.wheelDeltaY || e.detail) > 0) dir = 'up' - if ((e.wheelDeltaY || e.detail) < 0) dir = 'down' - if ((e.wheelDeltaX || e.detail) > 0) dir = 'left' - if ((e.wheelDeltaX || e.detail) < 0) dir = 'right' + if ((e.wheelDeltaY || e.detail) > 0) dir = CONSTANTS.DIR.UP + if ((e.wheelDeltaY || e.detail) < 0) dir = CONSTANTS.DIR.DOWN + if ((e.wheelDeltaX || e.detail) > 0) dir = CONSTANTS.DIR.LEFT + if ((e.wheelDeltaX || e.detail) < 0) dir = CONSTANTS.DIR.RIGHT } this.emit('mousewheel', e, dir, this) } diff --git a/simple-mind-map/src/KeyboardNavigation.js b/simple-mind-map/src/KeyboardNavigation.js index 0408dfae..c5adf041 100644 --- a/simple-mind-map/src/KeyboardNavigation.js +++ b/simple-mind-map/src/KeyboardNavigation.js @@ -1,4 +1,5 @@ import { bfsWalk } from './utils' +import { CONSTANTS } from './utils/constant' // 键盘导航类 class KeyboardNavigation { @@ -7,17 +8,17 @@ class KeyboardNavigation { this.opt = opt this.mindMap = opt.mindMap this.onKeyup = this.onKeyup.bind(this) - this.mindMap.keyCommand.addShortcut('Left', () => { - this.onKeyup('Left') + this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.LEFT, () => { + this.onKeyup(CONSTANTS.KEY_DIR.LEFT) }) - this.mindMap.keyCommand.addShortcut('Up', () => { - this.onKeyup('Up') + this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.UP, () => { + this.onKeyup(CONSTANTS.KEY_DIR.UP) }) - this.mindMap.keyCommand.addShortcut('Right', () => { - this.onKeyup('Right') + this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.RIGHT, () => { + this.onKeyup(CONSTANTS.KEY_DIR.RIGHT) }) - this.mindMap.keyCommand.addShortcut('Down', () => { - this.onKeyup('Down') + this.mindMap.keyCommand.addShortcut(CONSTANTS.KEY_DIR.DOWN, () => { + this.onKeyup(CONSTANTS.KEY_DIR.DOWN) }) } @@ -101,19 +102,19 @@ class KeyboardNavigation { let { left, top, right, bottom } = rect let match = false // 按下了左方向键 - if (dir === 'Left') { + if (dir === CONSTANTS.KEY_DIR.LEFT) { // 判断节点是否在当前节点的左侧 match = right <= currentActiveNodeRect.left // 按下了右方向键 - } else if (dir === 'Right') { + } else if (dir === CONSTANTS.KEY_DIR.RIGHT) { // 判断节点是否在当前节点的右侧 match = left >= currentActiveNodeRect.right // 按下了上方向键 - } else if (dir === 'Up') { + } else if (dir === CONSTANTS.KEY_DIR.UP) { // 判断节点是否在当前节点的上面 match = bottom <= currentActiveNodeRect.top // 按下了下方向键 - } else if (dir === 'Down') { + } else if (dir === CONSTANTS.KEY_DIR.DOWN) { // 判断节点是否在当前节点的下面 match = top >= currentActiveNodeRect.bottom } @@ -136,22 +137,22 @@ class KeyboardNavigation { let rect = this.getNodeRect(node) let { left, top, right, bottom } = rect let match = false - if (dir === 'Left') { + if (dir === CONSTANTS.KEY_DIR.LEFT) { match = left < currentActiveNodeRect.left && top < currentActiveNodeRect.bottom && bottom > currentActiveNodeRect.top - } else if (dir === 'Right') { + } else if (dir === CONSTANTS.KEY_DIR.RIGHT) { match = right > currentActiveNodeRect.right && top < currentActiveNodeRect.bottom && bottom > currentActiveNodeRect.top - } else if (dir === 'Up') { + } else if (dir === CONSTANTS.KEY_DIR.UP) { match = top < currentActiveNodeRect.top && left < currentActiveNodeRect.right && right > currentActiveNodeRect.left - } else if (dir === 'Down') { + } else if (dir === CONSTANTS.KEY_DIR.DOWN) { match = bottom > currentActiveNodeRect.bottom && left < currentActiveNodeRect.right && @@ -185,13 +186,13 @@ class KeyboardNavigation { let offsetY = ccY - cY if (offsetX === 0 && offsetY === 0) return let match = false - if (dir === 'Left') { + if (dir === CONSTANTS.KEY_DIR.LEFT) { match = offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY - } else if (dir === 'Right') { + } else if (dir === CONSTANTS.KEY_DIR.RIGHT) { match = offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY - } else if (dir === 'Up') { + } else if (dir === CONSTANTS.KEY_DIR.UP) { match = offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX - } else if (dir === 'Down') { + } else if (dir === CONSTANTS.KEY_DIR.DOWN) { match = offsetY > 0 && -offsetY < offsetX && offsetY > offsetX } if (match) { diff --git a/simple-mind-map/src/Node.js b/simple-mind-map/src/Node.js index 386e9a71..287fe21b 100644 --- a/simple-mind-map/src/Node.js +++ b/simple-mind-map/src/Node.js @@ -1,15 +1,17 @@ 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' +import { CONSTANTS } from './utils/constant' + // 节点类 - class Node { // 构造函数 - constructor(opt = {}) { // 节点数据 this.nodeData = this.handleData(opt.data || {}) @@ -21,10 +23,8 @@ class Node { this.renderer = opt.renderer // 渲染器 this.draw = opt.draw || null - // 主题配置 - this.themeConfig = this.mindMap.themeConfig // 样式实例 - this.style = new Style(this, this.themeConfig) + this.style = new Style(this) // 形状实例 this.shapeInstance = new Shape(this) this.shapePadding = { @@ -58,6 +58,7 @@ class Node { this.children = opt.children || [] // 节点内容的容器 this.group = null + this.shapeNode = null// 节点形状节点 // 节点内容对象 this._imgData = null this._iconData = null @@ -67,6 +68,9 @@ class Node { this._noteData = null this.noteEl = null this._expandBtn = null + this._openExpandNode = null + this._closeExpandNode = null + this._fillExpandNode = null this._lines = [] this._generalizationLine = null this._generalizationNode = null @@ -86,10 +90,27 @@ 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.createNodeData() this.getSize() } @@ -110,17 +131,7 @@ class Node { this._top = val } - // 更新主题配置 - - updateThemeConfig() { - // 主题配置 - this.themeConfig = this.mindMap.themeConfig - // 样式实例 - this.style.updateThemeConfig(this.themeConfig) - } - // 复位部分布局时会重新设置的数据 - reset() { this.children = [] this.parent = null @@ -131,7 +142,6 @@ class Node { } // 处理数据 - handleData(data) { data.data.expand = data.data.expand === false ? false : true data.data.isActive = data.data.isActive === true ? true : false @@ -139,33 +149,7 @@ 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() this._iconData = this.createIconNode() @@ -173,70 +157,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.removeAllNode() + this.updateGeneralization() this.createNodeData() let { width, height } = this.getNodeRect() // 判断节点尺寸是否有变化 @@ -247,7 +172,6 @@ class Node { } // 计算节点尺寸信息 - getNodeRect() { // 宽高 let imgContentWidth = 0 @@ -314,287 +238,21 @@ 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.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 - } - }) - } - - // 创建富文本节点 - 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.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() { + // 清除之前的内容 + this.group.clear() let { width, textContentItemMargin } = this let { paddingY } = this.getPaddingVale() paddingY += this.shapePadding.paddingY - // 创建组 - this.group = new G() + // 节点形状 + 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) - // 节点形状 - const shape = this.getShape() - this.style[shape === 'rectangle' ? 'rect' : 'shape']( - this.shapeInstance.createShape() - ) // 图片节点 let imgHeight = 0 if (this._imgData) { @@ -666,8 +324,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) }) @@ -677,8 +344,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', @@ -725,7 +394,6 @@ class Node { } // 激活节点 - active(e) { if (this.mindMap.opt.readonly) { return @@ -741,35 +409,23 @@ class Node { this.mindMap.emit('node_active', this, this.renderer.activeNodeList) } - // 渲染节点到画布,会移除旧的,创建新的 - - renderNode() { - // 连线 - this.renderLine() - this.removeAllEvent() - this.removeAllNode() - this.createNodeData() - this.layout() - } - // 更新节点 - - update(layout = false) { + update(isLayout = 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.renderExpandBtn() } + // 更新概要 this.renderGeneralization() + // 更新节点位置 let t = this.group.transform() - if (!layout) { + if (!isLayout) { this.group .animate(300) .translate( @@ -784,16 +440,42 @@ class Node { } } - // 递归渲染 + // 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置 + reRender() { + let sizeChange = this.getSize() + this.layout() + this.update() + return sizeChange + } + // 更新节点形状样式 + updateNodeShape() { + if (!this.shapeNode) return + const shape = this.getShape() + this.style[shape === CONSTANTS.SHAPE.RECTANGLE ? 'rect' : 'shape'](this.shapeNode) + } + + // 递归渲染 render(callback = () => {}) { // 节点 - if (this.initRender) { - this.initRender = false - this.renderNode() + // 重新渲染连线 + this.renderLine() + if (!this.group) { + // 创建组 + this.group = new G() + this.group.css({ + cursor: 'default' + }) + 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() } // 子节点 @@ -828,12 +510,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) { @@ -847,8 +528,16 @@ class Node { } } - // 隐藏节点 + // 销毁节点,不但会从画布删除,而且原节点直接置空,后续无法再插回画布 + destroy() { + if (!this.group) return + this.group.remove() + this.removeGeneralization() + this.removeLine() + this.group = null + } + // 隐藏节点 hide() { this.group.hide() this.hideGeneralization() @@ -869,7 +558,6 @@ class Node { } // 显示节点 - show() { if (!this.group) { return @@ -893,7 +581,6 @@ class Node { } // 连线 - renderLine(deep = false) { if (this.nodeData.data.expand === false) { return @@ -929,8 +616,37 @@ class Node { } } - // 设置连线样式 + // 获取节点形状 + getShape() { + // 节点使用功能横线风格的话不支持设置形状,直接使用默认的矩形 + return this.mindMap.themeConfig.nodeUseLineStyle + ? CONSTANTS.SHAPE.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 = node.getSelfInhertStyle('lineWidth') || node.getStyle('lineWidth', true) @@ -947,7 +663,6 @@ class Node { } // 移除连线 - removeLine() { this._lines.forEach(line => { line.remove() @@ -955,197 +670,7 @@ 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) { return false @@ -1161,7 +686,6 @@ class Node { } // 检测当前节点是否是某个节点的兄弟节点 - isBrother(node) { if (!this.parent || this === node) { return false @@ -1172,7 +696,6 @@ class Node { } // 获取padding值 - getPaddingVale() { return { paddingX: this.getStyle('paddingX', true, this.nodeData.data.isActive), @@ -1181,20 +704,17 @@ class Node { } // 获取某个样式 - 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 ( @@ -1205,7 +725,6 @@ class Node { } // 获取自身可继承的自定义样式 - getSelfInhertStyle(prop) { return ( this.getSelfStyle(prop) || // 自身 @@ -1213,65 +732,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 5c9a0b64..f0261ee2 100644 --- a/simple-mind-map/src/Render.js +++ b/simple-mind-map/src/Render.js @@ -7,24 +7,24 @@ import TextEdit from './TextEdit' import { copyNodeTree, simpleDeepClone, walk } from './utils' import { shapeList } from './Shape' import { lineStyleProps } from './themes/default' +import { CONSTANTS } from './utils/constant' // 布局列表 const layouts = { // 逻辑结构图 - logicalStructure: LogicalStructure, + [CONSTANTS.LAYOUT.LOGICAL_STRUCTURE]: LogicalStructure, // 思维导图 - mindMap: MindMap, + [CONSTANTS.LAYOUT.MIND_MAP]: MindMap, // 目录组织图 - catalogOrganization: CatalogOrganization, + [CONSTANTS.LAYOUT.CATALOG_ORGANIZATION]: CatalogOrganization, // 组织结构图 - organizationStructure: OrganizationStructure + [CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE]: OrganizationStructure } // 渲染 class Render { // 构造函数 - constructor(opt = {}) { this.opt = opt this.mindMap = opt.mindMap @@ -34,6 +34,15 @@ class Render { this.renderTree = merge({}, this.mindMap.opt.data || {}) // 是否重新渲染 this.reRender = false + // 是否正在渲染中 + this.isRendering = false + // 是否存在等待渲染 + this.hasWaitRendering = false + // 用于缓存节点 + this.nodeCache = {} + this.lastNodeCache = {} + // 触发render的来源 + this.renderSource = '' // 当前激活的节点列表 this.activeNodeList = [] // 根节点 @@ -51,17 +60,15 @@ class Render { } // 设置布局结构 - setLayout() { this.layout = new ( layouts[this.mindMap.opt.layout] ? layouts[this.mindMap.opt.layout] - : layouts.logicalStructure + : layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE] )(this) } // 绑定事件 - bindEvent() { // 点击事件 this.mindMap.on('draw_click', () => { @@ -73,7 +80,6 @@ class Render { } // 注册命令 - registerCommands() { // 全选 this.selectAll = this.selectAll.bind(this) @@ -175,7 +181,6 @@ class Render { } // 注册快捷键 - registerShortcutKeys() { // 插入下级节点 this.mindMap.keyCommand.addShortcut('Tab', () => { @@ -220,7 +225,6 @@ class Render { } // 开启文字编辑,会禁用回车键和删除键相关快捷键防止冲突 - startTextEdit() { this.mindMap.keyCommand.save() // this.mindMap.keyCommand.removeShortcut('Del|Backspace') @@ -229,7 +233,6 @@ class Render { } // 结束文字编辑,会恢复回车键和删除键相关快捷键 - endTextEdit() { this.mindMap.keyCommand.restore() // this.mindMap.keyCommand.addShortcut('Del|Backspace', this.removeNodeWrap) @@ -238,23 +241,51 @@ class Render { } // 渲染 - - render(callback = () => {}) { + render(callback = () => {}, source) { + // 如果当前还没有渲染完毕,不再触发渲染 + if (this.isRendering) { + // 等待当前渲染完毕后再进行一次渲染 + this.hasWaitRendering = true + return + } + this.isRendering = true + // 触发当前重新渲染的来源 + this.renderSource = source + // 节点缓存 + this.lastNodeCache = this.nodeCache + this.nodeCache = {} + // 重新渲染需要清除激活状态 if (this.reRender) { this.clearActive() } + // 计算布局 this.layout.doLayout(root => { + // 删除本次渲染时不再需要的节点 + Object.keys(this.lastNodeCache).forEach((uid) => { + if (!this.nodeCache[uid]) { + this.lastNodeCache[uid].destroy() + if (this.lastNodeCache[uid].parent) { + this.lastNodeCache[uid].parent.removeLine() + } + } + }) + // 更新根节点 this.root = root + // 渲染节点 this.root.render(() => { + this.isRendering = false this.mindMap.emit('node_tree_render_end') - callback() + callback && callback() + if (this.hasWaitRendering) { + this.hasWaitRendering = false + this.render(callback, source) + } }) }) this.mindMap.emit('node_active', null, this.activeNodeList) } // 清除当前激活的节点 - clearActive() { this.activeNodeList.forEach(item => { this.setNodeActive(item, false) @@ -263,7 +294,6 @@ class Render { } // 清除当前所有激活节点,并会触发事件 - clearAllActive() { if (this.activeNodeList.length <= 0) { return @@ -273,7 +303,6 @@ class Render { } // 添加节点到激活列表里 - addActiveNode(node) { let index = this.findActiveNodeIndex(node) if (index === -1) { @@ -282,7 +311,6 @@ class Render { } // 在激活列表里移除某个节点 - removeActiveNode(node) { let index = this.findActiveNodeIndex(node) if (index === -1) { @@ -292,7 +320,6 @@ class Render { } // 检索某个节点在激活列表里的索引 - findActiveNodeIndex(node) { return this.activeNodeList.findIndex(item => { return item === node @@ -300,7 +327,6 @@ class Render { } // 获取节点在同级里的索引位置 - getNodeIndex(node) { return node.parent ? node.parent.children.findIndex(item => { @@ -310,7 +336,6 @@ class Render { } // 全选 - selectAll() { walk( this.root, @@ -320,7 +345,7 @@ class Render { node.nodeData.data.isActive = true this.addActiveNode(node) setTimeout(() => { - node.renderNode() + node.updateNodeShape() }, 0) } }, @@ -332,24 +357,22 @@ class Render { } // 回退 - back(step) { this.clearAllActive() let data = this.mindMap.command.back(step) if (data) { this.renderTree = data - this.mindMap.reRender() + this.mindMap.render() } } // 前进 - forward(step) { this.clearAllActive() let data = this.mindMap.command.forward(step) if (data) { this.renderTree = data - this.mindMap.reRender() + this.mindMap.render() } } @@ -360,7 +383,6 @@ class Render { } // 插入同级节点,多个节点只会操作第一个节点 - insertNode(openEdit = true, appointNodes = [], appointData = null) { appointNodes = this.formatAppointNodes(appointNodes) if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) { @@ -374,7 +396,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, { @@ -391,7 +413,6 @@ class Render { } // 插入子节点 - insertChildNode(openEdit = true, appointNodes = [], appointData = null) { appointNodes = this.formatAppointNodes(appointNodes) if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) { @@ -416,17 +437,13 @@ 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() } // 上移节点,多个节点只会操作第一个节点 - upNode() { if (this.activeNodeList.length <= 0) { return @@ -454,7 +471,6 @@ class Render { } // 下移节点,多个节点只会操作第一个节点 - downNode() { if (this.activeNodeList.length <= 0) { return @@ -482,7 +498,6 @@ class Render { } // 将节点移动到另一个节点的前面 - insertBefore(node, exist) { if (node.isRoot) { return @@ -514,14 +529,12 @@ class Render { existParent.nodeData.children.splice(existIndex, 0, node.nodeData) this.mindMap.render(() => { if (nodeLayerChanged) { - node.getSize() - node.renderNode() + node.reRender() } }) } // 将节点移动到另一个节点的后面 - insertAfter(node, exist) { if (node.isRoot) { return @@ -554,14 +567,12 @@ class Render { existParent.nodeData.children.splice(existIndex, 0, node.nodeData) this.mindMap.render(() => { if (nodeLayerChanged) { - node.getSize() - node.renderNode() + node.reRender() } }) } // 移除节点 - removeNode(appointNodes = []) { appointNodes = this.formatAppointNodes(appointNodes) if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) { @@ -603,7 +614,6 @@ class Render { } // 移除某个指定节点 - removeOneNode(node) { let index = this.getNodeIndex(node) node.remove() @@ -612,7 +622,6 @@ class Render { } // 复制节点,多个节点只会操作第一个节点 - copyNode() { if (this.activeNodeList.length <= 0) { return @@ -621,7 +630,6 @@ class Render { } // 剪切节点,多个节点只会操作第一个节点 - cutNode(callback) { if (this.activeNodeList.length <= 0) { return @@ -641,7 +649,6 @@ class Render { } // 移动一个节点作为另一个节点的子节点 - moveNodeTo(node, toNode) { if (node.isRoot) { return @@ -653,12 +660,11 @@ class Render { toNode.nodeData.children.push(copyData) this.mindMap.render() if (toNode.isRoot) { - toNode.renderNode() + toNode.destroy() } } // 粘贴节点到节点 - pasteNode(data) { if (this.activeNodeList.length <= 0) { return @@ -670,7 +676,6 @@ class Render { } // 设置节点样式 - setNodeStyle(node, prop, value, isActive) { let data = {} if (isActive) { @@ -687,12 +692,14 @@ class Render { } // 如果开启了富文本,则需要应用到富文本上 if (this.mindMap.richText) { - this.mindMap.richText.showEditText(node) let config = this.mindMap.richText.normalStyleToRichTextStyle({ [prop]: value }) - this.mindMap.richText.formatAllText(config) - this.mindMap.richText.hideEditText() + if (Object.keys(config).length > 0) { + this.mindMap.richText.showEditText(node) + this.mindMap.richText.formatAllText(config) + this.mindMap.richText.hideEditText() + } } this.setNodeDataRender(node, data) // 更新了连线的样式 @@ -702,16 +709,14 @@ class Render { } // 设置节点是否激活 - setNodeActive(node, active) { this.setNodeData(node, { isActive: active }) - node.renderNode() + node.updateNodeShape() } // 设置节点是否展开 - setNodeExpand(node, expand) { this.setNodeData(node, { expand @@ -735,7 +740,6 @@ class Render { } // 展开所有 - expandAllNode() { walk( this.renderTree, @@ -750,11 +754,10 @@ class Render { 0, 0 ) - this.mindMap.reRender() + this.mindMap.render() } // 收起所有 - unexpandAllNode() { walk( this.renderTree, @@ -770,11 +773,12 @@ class Render { 0, 0 ) - this.mindMap.reRender() + this.mindMap.render(() => { + this.mindMap.view.reset() + }) } // 展开到指定层级 - expandToLevel(level) { walk( this.renderTree, @@ -788,11 +792,10 @@ class Render { 0, 0 ) - this.mindMap.reRender() + this.mindMap.render() } // 切换激活节点的展开状态 - toggleActiveExpand() { this.activeNodeList.forEach(node => { if (node.nodeData.children.length <= 0) { @@ -803,7 +806,6 @@ class Render { } // 切换节点展开状态 - toggleNodeExpand(node) { this.mindMap.execCommand( 'SET_NODE_EXPAND', @@ -813,7 +815,6 @@ class Render { } // 设置节点文本 - setNodeText(node, text, richText) { this.setNodeDataRender(node, { text, @@ -822,7 +823,6 @@ class Render { } // 设置节点图片 - setNodeImage(node, { url, title, width, height }) { this.setNodeDataRender(node, { image: url, @@ -835,7 +835,6 @@ class Render { } // 设置节点图标 - setNodeIcon(node, icons) { this.setNodeDataRender(node, { icon: icons @@ -843,7 +842,6 @@ class Render { } // 设置节点超链接 - setNodeHyperlink(node, link, title = '') { this.setNodeDataRender(node, { hyperlink: link, @@ -852,7 +850,6 @@ class Render { } // 设置节点备注 - setNodeNote(node, note) { this.setNodeDataRender(node, { note @@ -860,7 +857,6 @@ class Render { } // 设置节点标签 - setNodeTag(node, tag) { this.setNodeDataRender(node, { tag @@ -868,7 +864,6 @@ class Render { } // 添加节点概要 - addGeneralization(data) { if (this.activeNodeList.length <= 0) { return @@ -888,7 +883,6 @@ class Render { } // 删除节点概要 - removeGeneralization() { if (this.activeNodeList.length <= 0) { return @@ -906,7 +900,6 @@ class Render { } // 设置节点自定义位置 - setNodeCustomPosition(node, left = undefined, top = undefined) { let nodeList = [node] || this.activeNodeList nodeList.forEach(item => { @@ -918,7 +911,6 @@ class Render { } // 一键整理布局,即去除自定义位置 - resetLayout() { walk( this.root, @@ -940,7 +932,6 @@ class Render { } // 设置节点形状 - setNodeShape(node, shape) { if (!shape || !shapeList.includes(shape)) { return @@ -952,7 +943,6 @@ class Render { } // 更新节点数据 - setNodeData(node, data) { Object.keys(data).forEach(key => { node.nodeData.data[key] = data[key] @@ -960,11 +950,9 @@ class Render { } // 设置节点数据,并判断是否渲染 - setNodeDataRender(node, data) { this.setNodeData(node, data) - let changed = node.getSize() - node.renderNode() + let changed = node.reRender() if (changed) { if (node.isGeneralization) { // 概要节点 @@ -975,7 +963,6 @@ class Render { } // 移动节点到画布中心 - moveNodeToCenter(node) { let halfWidth = this.mindMap.width / 2 let halfHeight = this.mindMap.height / 2 diff --git a/simple-mind-map/src/RichText.js b/simple-mind-map/src/RichText.js index 14e97703..c9489d40 100644 --- a/simple-mind-map/src/RichText.js +++ b/simple-mind-map/src/RichText.js @@ -4,6 +4,7 @@ import './css/quill.css' import html2canvas from 'html2canvas' import { Image as SvgImage } from '@svgdotjs/svg.js' import { walk } from './utils' +import { CONSTANTS } from './utils/constant' let extended = false @@ -97,7 +98,7 @@ class RichText { this.mindMap.renderer.textEdit.registerTmpShortcut() if (!this.textEditNode) { this.textEditNode = document.createElement('div') - this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;box-shadow: 0 0 20px rgba(0,0,0,.5);outline: none; word-break: break-all;` + this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;box-shadow: 0 0 20px rgba(0,0,0,.5);outline: none; word-break: break-all;padding: 3px 5px;margin-left: -5px;margin-top: -3px;` document.body.appendChild(this.textEditNode) } // 原始宽高 @@ -147,7 +148,7 @@ class RichText { underline: node.style.merge('textDecoration') === 'underline', strike: node.style.merge('textDecoration') === 'line-through' } - this.formatText(style) + this.formatAllText(style) } // 隐藏文本编辑控件,即完成编辑 @@ -435,6 +436,7 @@ class RichText { node.data.richText = false div.innerHTML = node.data.text node.data.text = div.textContent + // delete node.data.uid } }, null, @@ -445,7 +447,7 @@ class RichText { // 清空历史数据,并且触发数据变化 this.mindMap.command.clearHistory() this.mindMap.command.addHistory() - this.mindMap.reRender() + this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE) } // 插件被移除前做的事情 diff --git a/simple-mind-map/src/Shape.js b/simple-mind-map/src/Shape.js index ea446471..b0df207b 100644 --- a/simple-mind-map/src/Shape.js +++ b/simple-mind-map/src/Shape.js @@ -1,3 +1,6 @@ +import { Rect, Polygon, Path } from '@svgdotjs/svg.js' +import { CONSTANTS } from './utils/constant' + // 节点形状类 export default class Shape { constructor(node) { @@ -13,37 +16,37 @@ export default class Shape { const actHeight = height + paddingY * 2 const actOffset = Math.abs(actWidth - actHeight) switch (shape) { - case 'roundedRectangle': + case CONSTANTS.SHAPE.ROUNDED_RECTANGLE: return { paddingX: height > width ? (height - width) / 2 : 0, paddingY: 0 } - case 'diamond': + case CONSTANTS.SHAPE.DIAMOND: return { paddingX: width / 2, paddingY: height / 2 } - case 'parallelogram': + case CONSTANTS.SHAPE.PARALLELOGRAM: return { paddingX: paddingX <= 0 ? defaultPaddingX : 0, paddingY: 0 } - case 'outerTriangularRectangle': + case CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE: return { paddingX: paddingX <= 0 ? defaultPaddingX : 0, paddingY: 0 } - case 'innerTriangularRectangle': + case CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE: return { paddingX: paddingX <= 0 ? defaultPaddingX : 0, paddingY: 0 } - case 'ellipse': + case CONSTANTS.SHAPE.ELLIPSE: return { paddingX: paddingX <= 0 ? defaultPaddingX : 0, paddingY: paddingY <= 0 ? defaultPaddingY : 0 } - case 'circle': + case CONSTANTS.SHAPE.CIRCLE: return { paddingX: actHeight > actWidth ? actOffset / 2 : 0, paddingY: actHeight < actWidth ? actOffset / 2 : 0 @@ -62,30 +65,30 @@ export default class Shape { let { width, height } = this.node let node = null // 矩形 - if (shape === 'rectangle') { - node = this.node.group.rect(width, height) - } else if (shape === 'diamond') { + if (shape === CONSTANTS.SHAPE.RECTANGLE) { + node = new Rect().size(width, height) + } else if (shape === CONSTANTS.SHAPE.DIAMOND) { // 菱形 node = this.createDiamond() - } else if (shape === 'parallelogram') { + } else if (shape === CONSTANTS.SHAPE.PARALLELOGRAM) { // 平行四边形 node = this.createParallelogram() - } else if (shape === 'roundedRectangle') { + } else if (shape === CONSTANTS.SHAPE.ROUNDED_RECTANGLE) { // 圆角矩形 node = this.createRoundedRectangle() - } else if (shape === 'octagonalRectangle') { + } else if (shape === CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE) { // 八角矩形 node = this.createOctagonalRectangle() - } else if (shape === 'outerTriangularRectangle') { + } else if (shape === CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE) { // 外三角矩形 node = this.createOuterTriangularRectangle() - } else if (shape === 'innerTriangularRectangle') { + } else if (shape === CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE) { // 内三角矩形 node = this.createInnerTriangularRectangle() - } else if (shape === 'ellipse') { + } else if (shape === CONSTANTS.SHAPE.ELLIPSE) { // 椭圆 node = this.createEllipse() - } else if (shape === 'circle') { + } else if (shape === CONSTANTS.SHAPE.CIRCLE) { // 圆 node = this.createCircle() } @@ -105,12 +108,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 +121,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 +163,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 +178,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 +193,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,24 +206,24 @@ 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} + `) } } // 形状列表 export const shapeList = [ - 'rectangle', - 'diamond', - 'parallelogram', - 'roundedRectangle', - 'octagonalRectangle', - 'outerTriangularRectangle', - 'innerTriangularRectangle', - 'ellipse', - 'circle' + CONSTANTS.SHAPE.RECTANGLE, + CONSTANTS.SHAPE.DIAMOND, + CONSTANTS.SHAPE.PARALLELOGRAM, + CONSTANTS.SHAPE.ROUNDED_RECTANGLE, + CONSTANTS.SHAPE.OCTAGONAL_RECTANGLE, + CONSTANTS.SHAPE.OUTER_TRIANGULAR_RECTANGLE, + CONSTANTS.SHAPE.INNER_TRIANGULAR_RECTANGLE, + CONSTANTS.SHAPE.ELLIPSE, + CONSTANTS.SHAPE.CIRCLE ] diff --git a/simple-mind-map/src/Style.js b/simple-mind-map/src/Style.js index 083120b2..72f109c9 100644 --- a/simple-mind-map/src/Style.js +++ b/simple-mind-map/src/Style.js @@ -2,10 +2,8 @@ import { tagColorList } from './utils/constant' const rootProp = ['paddingX', 'paddingY'] // 样式类 - class Style { // 设置背景样式 - static setBackgroundStyle(el, themeConfig) { let { backgroundColor, backgroundImage, backgroundRepeat, backgroundPosition, backgroundSize } = themeConfig el.style.backgroundColor = backgroundColor @@ -20,35 +18,27 @@ class Style { } // 构造函数 - - constructor(ctx, themeConfig) { + constructor(ctx) { this.ctx = ctx - this.themeConfig = themeConfig - } - - // 更新主题配置 - - updateThemeConfig(themeConfig) { - this.themeConfig = themeConfig } // 合并样式 - merge(prop, root, isActive) { + let themeConfig = this.ctx.mindMap.themeConfig // 三级及以下节点 - let defaultConfig = this.themeConfig.node + let defaultConfig = themeConfig.node if (root || rootProp.includes(prop)) { // 直接使用最外层样式 - defaultConfig = this.themeConfig + defaultConfig = themeConfig } else if (this.ctx.isGeneralization) { // 概要节点 - defaultConfig = this.themeConfig.generalization + defaultConfig = themeConfig.generalization } else if (this.ctx.layerIndex === 0) { // 根节点 - defaultConfig = this.themeConfig.root + defaultConfig = themeConfig.root } else if (this.ctx.layerIndex === 1) { // 二级节点 - defaultConfig = this.themeConfig.second + defaultConfig = themeConfig.second } // 激活状态 if (isActive !== undefined ? isActive : this.ctx.nodeData.data.isActive) { @@ -68,39 +58,35 @@ class Style { } // 获取某个样式值 - getStyle(prop, root, isActive) { return this.merge(prop, root, isActive) } // 获取自身自定义样式 - getSelfStyle(prop) { return this.ctx.nodeData.data[prop] } // 矩形 - rect(node) { this.shape(node) node.radius(this.merge('borderRadius')) } // 矩形外的其他形状 - shape(node) { node.fill({ color: this.merge('fillColor') }) // 节点使用横线样式,不需要渲染非激活状态的边框样式 - if ( - !this.ctx.isRoot && - !this.ctx.isGeneralization && - this.themeConfig.nodeUseLineStyle && - !this.ctx.nodeData.data.isActive - ) { - return - } + // if ( + // !this.ctx.isRoot && + // !this.ctx.isGeneralization && + // this.ctx.mindMap.themeConfig.nodeUseLineStyle && + // !this.ctx.nodeData.data.isActive + // ) { + // return + // } node.stroke({ color: this.merge('borderColor'), width: this.merge('borderWidth'), @@ -109,7 +95,6 @@ class Style { } // 文字 - text(node) { node .fill({ @@ -135,17 +120,15 @@ class Style { } // html文字节点 - - domText(node, fontSizeScale = 1) { + domText(node, fontSizeScale = 1, textLines) { node.style.fontFamily = this.merge('fontFamily') node.style.fontSize = this.merge('fontSize') * fontSizeScale + 'px' node.style.fontWeight = this.merge('fontWeight') || 'normal' - node.style.lineHeight = this.merge('lineHeight') + node.style.lineHeight = textLines === 1 ? 'normal' : this.merge('lineHeight') node.style.fontStyle = this.merge('fontStyle') } // 标签文字 - tagText(node, index) { node .fill({ @@ -157,7 +140,6 @@ class Style { } // 标签矩形 - tagRect(node, index) { node.fill({ color: tagColorList[index].background @@ -165,7 +147,6 @@ class Style { } // 内置图标 - iconNode(node) { node.attr({ fill: this.merge('color') @@ -173,13 +154,11 @@ class Style { } // 连线 - line(node, { width, color, dasharray } = {}) { node.stroke({ width, color, dasharray }).fill({ color: 'none' }) } // 概要连线 - generalizationLine(node) { node .stroke({ @@ -189,11 +168,15 @@ class Style { .fill({ color: 'none' }) } - // 按钮 - - iconBtn(node, fillNode) { - node.fill({ color: '#808080' }) - fillNode.fill({ color: '#fff' }) + // 展开收起按钮 + iconBtn(node, node2, fillNode) { + let { color, fill } = this.ctx.mindMap.opt.expandBtnStyle || { + color: '#808080', + fill: '#fff' + } + node.fill({ color: color }) + node2.fill({ color: color }) + fillNode.fill({ color: fill }) } } diff --git a/simple-mind-map/src/TextEdit.js b/simple-mind-map/src/TextEdit.js index a71e31b0..388e8090 100644 --- a/simple-mind-map/src/TextEdit.js +++ b/simple-mind-map/src/TextEdit.js @@ -23,6 +23,10 @@ export default class TextEdit { // 隐藏文本编辑框 this.hideEditTextBox() }) + this.mindMap.on('svg_mousedown', () => { + // 隐藏文本编辑框 + this.hideEditTextBox() + }) // 展开收缩按钮点击事件 this.mindMap.on('expand_btn_click', () => { this.hideEditTextBox() @@ -74,17 +78,18 @@ export default class TextEdit { let scale = this.mindMap.view.scale let lineHeight = node.style.merge('lineHeight') let fontSize = node.style.merge('fontSize') - node.style.domText(this.textEditNode, scale) - this.textEditNode.innerHTML = node.nodeData.data.text - .split(/\n/gim) - .join('
') + let textLines = node.nodeData.data.text.split(/\n/gim) + node.style.domText(this.textEditNode, scale, textLines.length) + this.textEditNode.innerHTML = textLines.join('
') this.textEditNode.style.minWidth = rect.width + 10 + 'px' this.textEditNode.style.minHeight = rect.height + 6 + 'px' this.textEditNode.style.left = rect.left + 'px' this.textEditNode.style.top = rect.top + 'px' this.textEditNode.style.display = 'block' this.textEditNode.style.maxWidth = this.mindMap.opt.textAutoWrapWidth * scale + 'px' - this.textEditNode.style.transform = `translateY(${-(lineHeight * fontSize - fontSize) / 2 * scale}px)` + if (textLines.length > 1 && lineHeight !== 1) { + this.textEditNode.style.transform = `translateY(${-((lineHeight * fontSize - fontSize) / 2 - 2) * scale}px)` + } this.showTextEdit = true // 选中文本 this.selectNodeText() @@ -126,6 +131,7 @@ export default class TextEdit { this.textEditNode.style.fontFamily = 'inherit' this.textEditNode.style.fontSize = 'inherit' this.textEditNode.style.fontWeight = 'normal' + this.textEditNode.style.transform = 'translateY(0)' this.showTextEdit = false } } diff --git a/simple-mind-map/src/View.js b/simple-mind-map/src/View.js index 3aa10747..16f160bc 100644 --- a/simple-mind-map/src/View.js +++ b/simple-mind-map/src/View.js @@ -1,3 +1,5 @@ +import { CONSTANTS } from './utils/constant' + // 视图操作类 class View { // 构造函数 @@ -58,35 +60,35 @@ class View { if (this.mindMap.opt.customHandleMousewheel && typeof this.mindMap.opt.customHandleMousewheel === 'function') { return this.mindMap.opt.customHandleMousewheel(e) } - if (this.mindMap.opt.mousewheelAction === 'zoom') { + if (this.mindMap.opt.mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) { switch (dir) { // 鼠标滚轮,向上和向左,都是缩小 - case 'up': - case 'left': + case CONSTANTS.DIR.UP: + case CONSTANTS.DIR.LEFT: this.narrow() break // 鼠标滚轮,向下和向右,都是放大 - case 'down': - case 'right': + case CONSTANTS.DIR.DOWN: + case CONSTANTS.DIR.RIGHT: this.enlarge() break } } else { switch (dir){ // 上移 - case 'down': + case CONSTANTS.DIR.DOWN: this.translateY(-this.mindMap.opt.mousewheelMoveStep) break // 下移 - case 'up': + case CONSTANTS.DIR.UP: this.translateY(this.mindMap.opt.mousewheelMoveStep) break // 右移 - case 'left': + case CONSTANTS.DIR.LEFT: this.translateX(-this.mindMap.opt.mousewheelMoveStep) break // 左移 - case 'right': + case CONSTANTS.DIR.RIGHT: this.translateX(this.mindMap.opt.mousewheelMoveStep) break } diff --git a/simple-mind-map/src/css/quill.css b/simple-mind-map/src/css/quill.css index ac0bb00f..7afdf142 100644 --- a/simple-mind-map/src/css/quill.css +++ b/simple-mind-map/src/css/quill.css @@ -2,6 +2,7 @@ overflow: hidden; padding: 0; height: auto; + line-height: normal; } .ql-container { diff --git a/simple-mind-map/src/layouts/Base.js b/simple-mind-map/src/layouts/Base.js index aecdfdd3..70569c31 100644 --- a/simple-mind-map/src/layouts/Base.js +++ b/simple-mind-map/src/layouts/Base.js @@ -1,4 +1,5 @@ import Node from '../Node' +import { CONSTANTS } from '../utils/constant' // 布局基类 class Base { @@ -12,6 +13,8 @@ class Base { this.draw = this.mindMap.draw // 根节点 this.root = null + // 保存所有uid和节点,用于复用 + this.nodePool = {} } // 计算节点位置 @@ -32,26 +35,70 @@ class Base { // 概要节点 renderGeneralization() {} + // 通过uid缓存节点 + cacheNode(uid, node) { + // 记录本次渲染时的节点 + this.renderer.nodeCache[uid] = node + // 记录所有渲染时的节点 + this.nodePool[uid] = node + // 如果总缓存数量达到1000,直接清空 + if (Object.keys(this.nodePool).length > 1000) { + this.nodePool = {} + } + } + + // 检查当前来源是否需要重新计算节点大小 + checkIsNeedResizeSources() { + return [CONSTANTS.CHANGE_THEME, CONSTANTS.TRANSFORM_TO_NORMAL_NODE].includes(this.renderer.renderSource) + } + // 创建节点实例 createNode(data, parent, isRoot, layerIndex) { // 创建节点 let newNode = null - // 复用节点 + // 数据上保存了节点引用,那么直接复用节点 if (data && data._node && !this.renderer.reRender) { newNode = data._node newNode.reset() newNode.layerIndex = layerIndex + this.cacheNode(data._node.uid, newNode) + // 主题或主题配置改变了需要重新计算节点大小和布局 + if (this.checkIsNeedResizeSources()) { + newNode.getSize() + newNode.needLayout = true + } + } else if (this.nodePool[data.data.uid]) { + // 数据上没有保存节点引用,但是通过uid找到了缓存的节点,也可以复用 + newNode = this.nodePool[data.data.uid] + // 保存该节点上一次的数据 + let lastData = JSON.stringify(newNode.nodeData.data) + newNode.reset() + newNode.nodeData = newNode.handleData(data || {}) + newNode.layerIndex = layerIndex + this.cacheNode(data.data.uid, newNode) + data._node = newNode + // 主题或主题配置改变了需要重新计算节点大小和布局 + let isResizeSource = this.checkIsNeedResizeSources() + // 节点数据改变了需要重新计算节点大小和布局 + let isNodeDataChange = lastData !== JSON.stringify(data.data) + if (isResizeSource || isNodeDataChange) { + newNode.getSize() + newNode.needLayout = true + } } else { // 创建新节点 + let uid = this.mindMap.uid++ newNode = new Node({ data, - uid: this.mindMap.uid++, + uid, renderer: this.renderer, mindMap: this.mindMap, draw: this.draw, layerIndex }) - newNode.getSize() + // uid保存到数据上,为了节点复用 + data.data.uid = uid + this.cacheNode(uid, newNode) // 数据关联实际节点 data._node = newNode if (data.data.isActive) { diff --git a/simple-mind-map/src/layouts/CatalogOrganization.js b/simple-mind-map/src/layouts/CatalogOrganization.js index 0b102655..4cf06569 100644 --- a/simple-mind-map/src/layouts/CatalogOrganization.js +++ b/simple-mind-map/src/layouts/CatalogOrganization.js @@ -1,386 +1,386 @@ -import Base from './Base' -import { walk, asyncRun } from '../utils' - -// 目录组织图 -class CatalogOrganization extends Base { - // 构造函数 - constructor(opt = {}) { - super(opt) - } - - // 布局 - doLayout(callback) { - let task = [ - () => { - this.computedBaseValue() - }, - () => { - this.computedLeftTopValue() - }, - () => { - this.adjustLeftTopValue() - }, - () => { - callback(this.root) - } - ] - asyncRun(task) - } - - // 遍历数据计算节点的left、width、height - computedBaseValue() { - walk( - this.renderer.renderTree, - null, - (cur, parent, isRoot, layerIndex) => { - let newNode = this.createNode(cur, parent, isRoot, layerIndex) - // 根节点定位在画布中心位置 - if (isRoot) { - this.setNodeCenter(newNode) - } else { - // 非根节点 - if (parent._node.isRoot) { - newNode.top = - parent._node.top + - parent._node.height + - this.getMarginX(layerIndex) - } - } - if (!cur.data.expand) { - return true - } - }, - (cur, parent, isRoot, layerIndex) => { - if (isRoot) { - let len = cur.data.expand === false ? 0 : cur._node.children.length - cur._node.childrenAreaWidth = len - ? cur._node.children.reduce((h, item) => { - return h + item.width - }, 0) + - (len + 1) * this.getMarginX(layerIndex + 1) - : 0 - } - }, - true, - 0 - ) - } - - // 遍历节点树计算节点的left、top - computedLeftTopValue() { - walk( - this.root, - null, - (node, parent, isRoot, layerIndex) => { - if ( - node.nodeData.data.expand && - node.children && - node.children.length - ) { - let marginX = this.getMarginX(layerIndex + 1) - let marginY = this.getMarginY(layerIndex + 1) - if (isRoot) { - let left = node.left + node.width / 2 - node.childrenAreaWidth / 2 - let totalLeft = left + marginX - node.children.forEach(cur => { - cur.left = totalLeft - totalLeft += cur.width + marginX - }) - } else { - let totalTop = node.top + node.height + marginY + node.expandBtnSize - node.children.forEach(cur => { - cur.left = node.left + node.width * 0.5 - cur.top = totalTop - totalTop += cur.height + marginY + node.expandBtnSize - }) - } - } - }, - null, - true - ) - } - - // 调整节点left、top - adjustLeftTopValue() { - walk( - this.root, - null, - (node, parent, isRoot, layerIndex) => { - if (!node.nodeData.data.expand) { - return - } - // 调整left - if (parent && parent.isRoot) { - let areaWidth = this.getNodeAreaWidth(node) - let difference = areaWidth - node.width - if (difference > 0) { - this.updateBrothersLeft(node, difference / 2) - } - } - // 调整top - let len = node.children.length - if (parent && !parent.isRoot && len > 0) { - let marginY = this.getMarginY(layerIndex + 1) - let totalHeight = - node.children.reduce((h, item) => { - return h + item.height - }, 0) + - (len + 1) * marginY + - len * node.expandBtnSize - this.updateBrothersTop(node, totalHeight) - } - }, - null, - true - ) - } - - // 递归计算节点的宽度 - getNodeAreaWidth(node) { - let widthArr = [] - let loop = (node, width) => { - if (node.children.length) { - width += node.width / 2 - node.children.forEach(item => { - loop(item, width) - }) - } else { - width += node.width - widthArr.push(width) - } - } - loop(node, 0) - return Math.max(...widthArr) - } - - // 调整兄弟节点的left - updateBrothersLeft(node, addWidth) { - if (node.parent) { - let childrenList = node.parent.children - let index = childrenList.findIndex(item => { - return item === node - }) - // 存在大于一个节点时,第一个或最后一个节点自身也需要移动,否则两边不对称 - if ( - (index === 0 || index === childrenList.length - 1) && - childrenList.length > 1 - ) { - let _offset = index === 0 ? -addWidth : addWidth - node.left += _offset - if ( - node.children && - node.children.length && - !node.hasCustomPosition() - ) { - this.updateChildren(node.children, 'left', _offset) - } - } - childrenList.forEach((item, _index) => { - if (item.hasCustomPosition()) { - // 适配自定义位置 - return - } - let _offset = 0 - if (_index < index) { - // 左边的节点往左移 - _offset = -addWidth - } else if (_index > index) { - // 右边的节点往右移 - _offset = addWidth - } - item.left += _offset - // 同步更新子节点的位置 - if (item.children && item.children.length) { - this.updateChildren(item.children, 'left', _offset) - } - }) - // 更新父节点的位置 - this.updateBrothersLeft(node.parent, addWidth) - } - } - - // 调整兄弟节点的top - updateBrothersTop(node, addHeight) { - if (node.parent && !node.parent.isRoot) { - let childrenList = node.parent.children - let index = childrenList.findIndex(item => { - return item === node - }) - childrenList.forEach((item, _index) => { - if (item.hasCustomPosition()) { - // 适配自定义位置 - return - } - let _offset = 0 - // 下面的节点往下移 - if (_index > index) { - _offset = addHeight - } - item.top += _offset - // 同步更新子节点的位置 - if (item.children && item.children.length) { - this.updateChildren(item.children, 'top', _offset) - } - }) - // 更新父节点的位置 - this.updateBrothersTop(node.parent, addHeight) - } - } - - // 绘制连线,连接该节点到其子节点 - renderLine(node, lines, style) { - if (node.children.length <= 0) { - return [] - } - let { left, top, width, height, expandBtnSize } = node - let len = node.children.length - let marginX = this.getMarginX(node.layerIndex + 1) - if (node.isRoot) { - // 根节点 - let x1 = left + width / 2 - let y1 = top + height - let s1 = marginX * 0.7 - let minx = Infinity - let maxx = -Infinity - node.children.forEach((item, index) => { - let x2 = item.left + item.width / 2 - let y2 = item.top - if (x2 < minx) { - minx = x2 - } - if (x2 > maxx) { - maxx = x2 - } - // 节点使用横线风格,需要额外渲染横线 - let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle - ? ` L ${item.left},${y2} L ${item.left + item.width},${y2}` - : '' - let path = - `M ${x2},${y1 + s1} L ${x2},${y1 + s1 > y2 ? y2 + item.height : y2}` + - nodeUseLineStylePath - // 竖线 - lines[index].plot(path) - style && style(lines[index], item) - }) - minx = Math.min(minx, x1) - maxx = Math.max(maxx, x1) - // 父节点的竖线 - let line1 = this.draw.path() - node.style.line(line1) - line1.plot(`M ${x1},${y1} L ${x1},${y1 + s1}`) - node._lines.push(line1) - style && style(line1, node) - // 水平线 - if (len > 0) { - let lin2 = this.draw.path() - node.style.line(lin2) - lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`) - node._lines.push(lin2) - style && style(lin2, node) - } - } else { - // 非根节点 - let y1 = top + height - let maxy = -Infinity - let x2 = node.left + node.width * 0.3 - node.children.forEach((item, index) => { - // 为了适配自定义位置,下面做了各种位置的兼容 - let y2 = item.top + item.height / 2 - if (y2 > maxy) { - maxy = y2 - } - // 水平线 - let path = '' - let _left = item.left - let _isLeft = item.left + item.width < x2 - let _isXCenter = false - if (_isLeft) { - // 水平位置在父节点左边 - _left = item.left + item.width - } else if (item.left < x2 && item.left + item.width > x2) { - // 水平位置在父节点之间 - _isXCenter = true - y2 = item.top - maxy = y2 - } - if (y2 > top && y2 < y1) { - // 自定义位置的情况:垂直位置节点在父节点之间 - path = `M ${ - _isLeft ? node.left : node.left + node.width - },${y2} L ${_left},${y2}` - } else if (y2 < y1) { - // 自定义位置的情况:垂直位置节点在父节点上面 - if (_isXCenter) { - y2 = item.top + item.height - _left = x2 - } - path = `M ${x2},${top} L ${x2},${y2} L ${_left},${y2}` - } else { - if (_isXCenter) { - _left = x2 - } - path = `M ${x2},${y2} L ${_left},${y2}` - } - // 节点使用横线风格,需要额外渲染横线 - let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle - ? ` L ${_left},${y2 - item.height / 2} L ${_left},${ - y2 + item.height / 2 - }` - : '' - path += nodeUseLineStylePath - lines[index].plot(path) - style && style(lines[index], item) - }) - // 竖线 - if (len > 0) { - let lin2 = this.draw.path() - expandBtnSize = len > 0 ? expandBtnSize : 0 - node.style.line(lin2) - if (maxy < y1 + expandBtnSize) { - lin2.hide() - } else { - lin2.plot(`M ${x2},${y1 + expandBtnSize} L ${x2},${maxy}`) - lin2.show() - } - node._lines.push(lin2) - style && style(lin2, node) - } - } - } - - // 渲染按钮 - renderExpandBtn(node, btn) { - let { width, height, expandBtnSize, isRoot } = node - if (!isRoot) { - let { translateX, translateY } = btn.transform() - btn.translate( - width * 0.3 - expandBtnSize / 2 - translateX, - height + expandBtnSize / 2 - translateY - ) - } - } - - // 创建概要节点 - renderGeneralization(node, gLine, gNode) { - let { - top, - bottom, - right, - generalizationLineMargin, - generalizationNodeMargin - } = this.getNodeBoundaries(node, 'h') - let x1 = right + generalizationLineMargin - let y1 = top - let x2 = right + generalizationLineMargin - let y2 = bottom - let cx = x1 + 20 - let cy = y1 + (y2 - y1) / 2 - let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}` - gLine.plot(path) - gNode.left = right + generalizationNodeMargin - gNode.top = top + (bottom - top - gNode.height) / 2 - } -} - -export default CatalogOrganization +import Base from './Base' +import { walk, asyncRun } from '../utils' + +// 目录组织图 +class CatalogOrganization extends Base { + // 构造函数 + constructor(opt = {}) { + super(opt) + } + + // 布局 + doLayout(callback) { + let task = [ + () => { + this.computedBaseValue() + }, + () => { + this.computedLeftTopValue() + }, + () => { + this.adjustLeftTopValue() + }, + () => { + callback(this.root) + } + ] + asyncRun(task) + } + + // 遍历数据计算节点的left、width、height + computedBaseValue() { + walk( + this.renderer.renderTree, + null, + (cur, parent, isRoot, layerIndex) => { + let newNode = this.createNode(cur, parent, isRoot, layerIndex) + // 根节点定位在画布中心位置 + if (isRoot) { + this.setNodeCenter(newNode) + } else { + // 非根节点 + if (parent._node.isRoot) { + newNode.top = + parent._node.top + + parent._node.height + + this.getMarginX(layerIndex) + } + } + if (!cur.data.expand) { + return true + } + }, + (cur, parent, isRoot, layerIndex) => { + if (isRoot) { + let len = cur.data.expand === false ? 0 : cur._node.children.length + cur._node.childrenAreaWidth = len + ? cur._node.children.reduce((h, item) => { + return h + item.width + }, 0) + + (len + 1) * this.getMarginX(layerIndex + 1) + : 0 + } + }, + true, + 0 + ) + } + + // 遍历节点树计算节点的left、top + computedLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex) => { + if ( + node.nodeData.data.expand && + node.children && + node.children.length + ) { + let marginX = this.getMarginX(layerIndex + 1) + let marginY = this.getMarginY(layerIndex + 1) + if (isRoot) { + let left = node.left + node.width / 2 - node.childrenAreaWidth / 2 + let totalLeft = left + marginX + node.children.forEach(cur => { + cur.left = totalLeft + totalLeft += cur.width + marginX + }) + } else { + let totalTop = node.top + node.height + marginY + node.expandBtnSize + node.children.forEach(cur => { + cur.left = node.left + node.width * 0.5 + cur.top = totalTop + totalTop += cur.height + marginY + node.expandBtnSize + }) + } + } + }, + null, + true + ) + } + + // 调整节点left、top + adjustLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex) => { + if (!node.nodeData.data.expand) { + return + } + // 调整left + if (parent && parent.isRoot) { + let areaWidth = this.getNodeAreaWidth(node) + let difference = areaWidth - node.width + if (difference > 0) { + this.updateBrothersLeft(node, difference / 2) + } + } + // 调整top + let len = node.children.length + if (parent && !parent.isRoot && len > 0) { + let marginY = this.getMarginY(layerIndex + 1) + let totalHeight = + node.children.reduce((h, item) => { + return h + item.height + }, 0) + + (len + 1) * marginY + + len * node.expandBtnSize + this.updateBrothersTop(node, totalHeight) + } + }, + null, + true + ) + } + + // 递归计算节点的宽度 + getNodeAreaWidth(node) { + let widthArr = [] + let loop = (node, width) => { + if (node.children.length) { + width += node.width / 2 + node.children.forEach(item => { + loop(item, width) + }) + } else { + width += node.width + widthArr.push(width) + } + } + loop(node, 0) + return Math.max(...widthArr) + } + + // 调整兄弟节点的left + updateBrothersLeft(node, addWidth) { + if (node.parent) { + let childrenList = node.parent.children + let index = childrenList.findIndex(item => { + return item === node + }) + // 存在大于一个节点时,第一个或最后一个节点自身也需要移动,否则两边不对称 + if ( + (index === 0 || index === childrenList.length - 1) && + childrenList.length > 1 + ) { + let _offset = index === 0 ? -addWidth : addWidth + node.left += _offset + if ( + node.children && + node.children.length && + !node.hasCustomPosition() + ) { + this.updateChildren(node.children, 'left', _offset) + } + } + childrenList.forEach((item, _index) => { + if (item.hasCustomPosition()) { + // 适配自定义位置 + return + } + let _offset = 0 + if (_index < index) { + // 左边的节点往左移 + _offset = -addWidth + } else if (_index > index) { + // 右边的节点往右移 + _offset = addWidth + } + item.left += _offset + // 同步更新子节点的位置 + if (item.children && item.children.length) { + this.updateChildren(item.children, 'left', _offset) + } + }) + // 更新父节点的位置 + this.updateBrothersLeft(node.parent, addWidth) + } + } + + // 调整兄弟节点的top + updateBrothersTop(node, addHeight) { + if (node.parent && !node.parent.isRoot) { + let childrenList = node.parent.children + let index = childrenList.findIndex(item => { + return item === node + }) + childrenList.forEach((item, _index) => { + if (item.hasCustomPosition()) { + // 适配自定义位置 + return + } + let _offset = 0 + // 下面的节点往下移 + if (_index > index) { + _offset = addHeight + } + item.top += _offset + // 同步更新子节点的位置 + if (item.children && item.children.length) { + this.updateChildren(item.children, 'top', _offset) + } + }) + // 更新父节点的位置 + this.updateBrothersTop(node.parent, addHeight) + } + } + + // 绘制连线,连接该节点到其子节点 + renderLine(node, lines, style) { + if (node.children.length <= 0) { + return [] + } + let { left, top, width, height, expandBtnSize } = node + let len = node.children.length + let marginX = this.getMarginX(node.layerIndex + 1) + if (node.isRoot) { + // 根节点 + let x1 = left + width / 2 + let y1 = top + height + let s1 = marginX * 0.7 + let minx = Infinity + let maxx = -Infinity + node.children.forEach((item, index) => { + let x2 = item.left + item.width / 2 + let y2 = item.top + if (x2 < minx) { + minx = x2 + } + if (x2 > maxx) { + maxx = x2 + } + // 节点使用横线风格,需要额外渲染横线 + let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle + ? ` L ${item.left},${y2} L ${item.left + item.width},${y2}` + : '' + let path = + `M ${x2},${y1 + s1} L ${x2},${y1 + s1 > y2 ? y2 + item.height : y2}` + + nodeUseLineStylePath + // 竖线 + lines[index].plot(path) + style && style(lines[index], item) + }) + minx = Math.min(minx, x1) + maxx = Math.max(maxx, x1) + // 父节点的竖线 + let line1 = this.draw.path() + node.style.line(line1) + line1.plot(`M ${x1},${y1} L ${x1},${y1 + s1}`) + node._lines.push(line1) + style && style(line1, node) + // 水平线 + if (len > 0) { + let lin2 = this.draw.path() + node.style.line(lin2) + lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`) + node._lines.push(lin2) + style && style(lin2, node) + } + } else { + // 非根节点 + let y1 = top + height + let maxy = -Infinity + let x2 = node.left + node.width * 0.3 + node.children.forEach((item, index) => { + // 为了适配自定义位置,下面做了各种位置的兼容 + let y2 = item.top + item.height / 2 + if (y2 > maxy) { + maxy = y2 + } + // 水平线 + let path = '' + let _left = item.left + let _isLeft = item.left + item.width < x2 + let _isXCenter = false + if (_isLeft) { + // 水平位置在父节点左边 + _left = item.left + item.width + } else if (item.left < x2 && item.left + item.width > x2) { + // 水平位置在父节点之间 + _isXCenter = true + y2 = item.top + maxy = y2 + } + if (y2 > top && y2 < y1) { + // 自定义位置的情况:垂直位置节点在父节点之间 + path = `M ${ + _isLeft ? node.left : node.left + node.width + },${y2} L ${_left},${y2}` + } else if (y2 < y1) { + // 自定义位置的情况:垂直位置节点在父节点上面 + if (_isXCenter) { + y2 = item.top + item.height + _left = x2 + } + path = `M ${x2},${top} L ${x2},${y2} L ${_left},${y2}` + } else { + if (_isXCenter) { + _left = x2 + } + path = `M ${x2},${y2} L ${_left},${y2}` + } + // 节点使用横线风格,需要额外渲染横线 + let nodeUseLineStylePath = this.mindMap.themeConfig.nodeUseLineStyle + ? ` L ${_left},${y2 - item.height / 2} L ${_left},${ + y2 + item.height / 2 + }` + : '' + path += nodeUseLineStylePath + lines[index].plot(path) + style && style(lines[index], item) + }) + // 竖线 + if (len > 0) { + let lin2 = this.draw.path() + expandBtnSize = len > 0 ? expandBtnSize : 0 + node.style.line(lin2) + if (maxy < y1 + expandBtnSize) { + lin2.hide() + } else { + lin2.plot(`M ${x2},${y1 + expandBtnSize} L ${x2},${maxy}`) + lin2.show() + } + node._lines.push(lin2) + style && style(lin2, node) + } + } + } + + // 渲染按钮 + renderExpandBtn(node, btn) { + let { width, height, expandBtnSize, isRoot } = node + if (!isRoot) { + let { translateX, translateY } = btn.transform() + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + height + expandBtnSize / 2 - translateY + ) + } + } + + // 创建概要节点 + renderGeneralization(node, gLine, gNode) { + let { + top, + bottom, + right, + generalizationLineMargin, + generalizationNodeMargin + } = this.getNodeBoundaries(node, 'h') + let x1 = right + generalizationLineMargin + let y1 = top + let x2 = right + generalizationLineMargin + let y2 = bottom + let cx = x1 + 20 + let cy = y1 + (y2 - y1) / 2 + let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}` + gLine.plot(path) + gNode.left = right + generalizationNodeMargin + gNode.top = top + (bottom - top - gNode.height) / 2 + } +} + +export default CatalogOrganization diff --git a/simple-mind-map/src/layouts/LogicalStructure.js b/simple-mind-map/src/layouts/LogicalStructure.js index 5b28fa0d..a9ed8ff2 100644 --- a/simple-mind-map/src/layouts/LogicalStructure.js +++ b/simple-mind-map/src/layouts/LogicalStructure.js @@ -2,16 +2,13 @@ import Base from './Base' import { walk, asyncRun } from '../utils' // 逻辑结构图 - class LogicalStructure extends Base { // 构造函数 - constructor(opt = {}) { super(opt) } // 布局 - doLayout(callback) { let task = [ () => { @@ -31,7 +28,6 @@ class LogicalStructure extends Base { } // 遍历数据计算节点的left、width、height - computedBaseValue() { walk( this.renderer.renderTree, @@ -67,7 +63,6 @@ class LogicalStructure extends Base { } // 遍历节点树计算节点的top - computedTopValue() { walk( this.root, @@ -94,7 +89,6 @@ class LogicalStructure extends Base { } // 调整节点top - adjustTopValue() { walk( this.root, @@ -118,7 +112,6 @@ class LogicalStructure extends Base { } // 更新兄弟节点的top - updateBrothers(node, addHeight) { if (node.parent) { let childrenList = node.parent.children @@ -150,7 +143,6 @@ class LogicalStructure extends Base { } // 绘制连线,连接该节点到其子节点 - renderLine(node, lines, style, lineStyle) { if (lineStyle === 'curve') { this.renderLineCurve(node, lines, style) @@ -162,7 +154,6 @@ class LogicalStructure extends Base { } // 直线风格连线 - renderLineStraight(node, lines, style) { if (node.children.length <= 0) { return [] @@ -192,7 +183,6 @@ class LogicalStructure extends Base { } // 直连风格 - renderLineDirect(node, lines, style) { if (node.children.length <= 0) { return [] @@ -218,7 +208,6 @@ class LogicalStructure extends Base { } // 曲线风格连线 - renderLineCurve(node, lines, style) { if (node.children.length <= 0) { return [] @@ -249,7 +238,6 @@ class LogicalStructure extends Base { } // 渲染按钮 - renderExpandBtn(node, btn) { let { width, height } = node let { translateX, translateY } = btn.transform() @@ -264,7 +252,6 @@ class LogicalStructure extends Base { } // 创建概要节点 - renderGeneralization(node, gLine, gNode) { let { top, diff --git a/simple-mind-map/src/layouts/MindMap.js b/simple-mind-map/src/layouts/MindMap.js index 87e6ced3..384c301a 100644 --- a/simple-mind-map/src/layouts/MindMap.js +++ b/simple-mind-map/src/layouts/MindMap.js @@ -2,7 +2,6 @@ import Base from './Base' import { walk, asyncRun } from '../utils' // 思维导图 - class MindMap extends Base { // 构造函数 // 在逻辑结构图的基础上增加一个变量来记录生长方向,向左还是向右,同时在计算left的时候根据方向来计算、调整top时只考虑同方向的节点即可 @@ -11,7 +10,6 @@ class MindMap extends Base { } // 布局 - doLayout(callback) { let task = [ () => { @@ -31,7 +29,6 @@ class MindMap extends Base { } // 遍历数据计算节点的left、width、height - computedBaseValue() { walk( this.renderer.renderTree, @@ -96,7 +93,6 @@ class MindMap extends Base { } // 遍历节点树计算节点的top - computedTopValue() { walk( this.root, @@ -129,7 +125,6 @@ class MindMap extends Base { } // 调整节点top - adjustTopValue() { walk( this.root, @@ -152,7 +147,6 @@ class MindMap extends Base { } // 更新兄弟节点的top - updateBrothers(node, leftAddHeight, rightAddHeight) { if (node.parent) { // 过滤出和自己同方向的节点 @@ -188,7 +182,6 @@ class MindMap extends Base { } // 绘制连线,连接该节点到其子节点 - renderLine(node, lines, style, lineStyle) { if (lineStyle === 'curve') { this.renderLineCurve(node, lines, style) @@ -200,7 +193,6 @@ class MindMap extends Base { } // 直线风格连线 - renderLineStraight(node, lines, style) { if (node.children.length <= 0) { return [] @@ -238,7 +230,6 @@ class MindMap extends Base { } // 直连风格 - renderLineDirect(node, lines, style) { if (node.children.length <= 0) { return [] @@ -273,7 +264,6 @@ class MindMap extends Base { } // 曲线风格连线 - renderLineCurve(node, lines, style) { if (node.children.length <= 0) { return [] @@ -313,7 +303,6 @@ class MindMap extends Base { } // 渲染按钮 - renderExpandBtn(node, btn) { let { width, height, expandBtnSize } = node let { translateX, translateY } = btn.transform() @@ -327,7 +316,6 @@ class MindMap extends Base { } // 创建概要节点 - renderGeneralization(node, gLine, gNode) { let isLeft = node.dir === 'left' let { diff --git a/simple-mind-map/src/layouts/OrganizationStructure.js b/simple-mind-map/src/layouts/OrganizationStructure.js index 4c2dc460..36a26a84 100644 --- a/simple-mind-map/src/layouts/OrganizationStructure.js +++ b/simple-mind-map/src/layouts/OrganizationStructure.js @@ -5,13 +5,11 @@ import { walk, asyncRun } from '../utils' // 和逻辑结构图基本一样,只是方向变成向下生长,所以先计算节点的top,后计算节点的left、最后调整节点的left即可 class OrganizationStructure extends Base { // 构造函数 - constructor(opt = {}) { super(opt) } // 布局 - doLayout(callback) { let task = [ () => { @@ -31,7 +29,6 @@ class OrganizationStructure extends Base { } // 遍历数据计算节点的left、width、height - computedBaseValue() { walk( this.renderer.renderTree, @@ -67,7 +64,6 @@ class OrganizationStructure extends Base { } // 遍历节点树计算节点的left - computedLeftValue() { walk( this.root, @@ -94,7 +90,6 @@ class OrganizationStructure extends Base { } // 调整节点left - adjustLeftValue() { walk( this.root, @@ -118,7 +113,6 @@ class OrganizationStructure extends Base { } // 更新兄弟节点的left - updateBrothers(node, addWidth) { if (node.parent) { let childrenList = node.parent.children @@ -150,7 +144,6 @@ class OrganizationStructure extends Base { } // 绘制连线,连接该节点到其子节点 - renderLine(node, lines, style, lineStyle) { if (lineStyle === 'direct') { this.renderLineDirect(node, lines, style) @@ -160,7 +153,6 @@ class OrganizationStructure extends Base { } // 直连风格 - renderLineDirect(node, lines, style) { if (node.children.length <= 0) { return [] @@ -182,7 +174,6 @@ class OrganizationStructure extends Base { } // 直线风格连线 - renderLineStraight(node, lines, style) { if (node.children.length <= 0) { return [] @@ -232,7 +223,6 @@ class OrganizationStructure extends Base { } // 渲染按钮 - renderExpandBtn(node, btn) { let { width, height, expandBtnSize } = node let { translateX, translateY } = btn.transform() @@ -243,7 +233,6 @@ class OrganizationStructure extends Base { } // 创建概要节点 - renderGeneralization(node, gLine, gNode) { let { bottom, diff --git a/simple-mind-map/src/themes/default.js b/simple-mind-map/src/themes/default.js index 3436b7fa..0f2ad5ad 100644 --- a/simple-mind-map/src/themes/default.js +++ b/simple-mind-map/src/themes/default.js @@ -142,14 +142,10 @@ export default { // 简单来说,会改变节点大小的都不支持在激活时设置,为了性能考虑,节点切换激活态时不会重新计算节点大小 export const supportActiveStyle = [ 'fillColor', - 'color', - 'fontWeight', - 'fontStyle', 'borderColor', 'borderWidth', 'borderDasharray', - 'borderRadius', - 'textDecoration' + 'borderRadius' ] export const lineStyleProps = ['lineColor', 'lineDasharray', 'lineWidth'] diff --git a/simple-mind-map/src/utils/constant.js b/simple-mind-map/src/utils/constant.js index d13d4231..795fa79b 100644 --- a/simple-mind-map/src/utils/constant.js +++ b/simple-mind-map/src/utils/constant.js @@ -22,32 +22,6 @@ export const tagColorList = [ } ] -// 布局结构列表 -export const layoutList = [ - { - name: '逻辑结构图', - value: 'logicalStructure', - }, - { - name: '思维导图', - value: 'mindMap', - }, - { - name: '组织结构图', - value: 'organizationStructure', - }, - { - name: '目录组织图', - value: 'catalogOrganization', - } -] -export const layoutValueList = [ - 'logicalStructure', - 'mindMap', - 'catalogOrganization', - 'organizationStructure' -] - // 主题列表 export const themeList = [ { @@ -139,3 +113,72 @@ export const themeList = [ value: 'romanticPurple', } ] + +// 常量 +export const CONSTANTS = { + CHANGE_THEME: 'changeTheme', + TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode', + MODE: { + READONLY: 'readonly', + EDIT: 'edit' + }, + LAYOUT: { + LOGICAL_STRUCTURE: 'logicalStructure', + MIND_MAP: 'mindMap', + ORGANIZATION_STRUCTURE: 'organizationStructure', + CATALOG_ORGANIZATION: 'catalogOrganization' + }, + DIR: { + UP: 'up', + LEFT: 'left', + DOWN: 'down', + RIGHT: 'right' + }, + KEY_DIR: { + LEFT: 'Left', + UP: 'Up', + RIGHT: 'Right', + DOWN: 'Down' + }, + SHAPE: { + RECTANGLE: 'rectangle', + DIAMOND: 'diamond', + PARALLELOGRAM: 'parallelogram', + ROUNDED_RECTANGLE: 'roundedRectangle', + OCTAGONAL_RECTANGLE: 'octagonalRectangle', + OUTER_TRIANGULAR_RECTANGLE: 'outerTriangularRectangle', + INNER_TRIANGULAR_RECTANGLE: 'innerTriangularRectangle', + ELLIPSE: 'ellipse', + CIRCLE: 'circle' + }, + MOUSE_WHEEL_ACTION: { + ZOOM: 'zoom', + MOVE: 'move' + } +} + +// 布局结构列表 +export const layoutList = [ + { + name: '逻辑结构图', + value: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE, + }, + { + name: '思维导图', + value: CONSTANTS.LAYOUT.MIND_MAP, + }, + { + name: '组织结构图', + value: CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE, + }, + { + name: '目录组织图', + value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION, + } +] +export const layoutValueList = [ + CONSTANTS.LAYOUT.LOGICAL_STRUCTURE, + CONSTANTS.LAYOUT.MIND_MAP, + CONSTANTS.LAYOUT.CATALOG_ORGANIZATION, + CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE +] \ No newline at end of file diff --git a/simple-mind-map/src/utils/index.js b/simple-mind-map/src/utils/index.js index 3f0bf725..3dade497 100644 --- a/simple-mind-map/src/utils/index.js +++ b/simple-mind-map/src/utils/index.js @@ -141,13 +141,14 @@ export const copyNodeTree = (tree, root, removeActiveState = false, keepId = fal tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data) // 去除节点id,因为节点id不能重复 if (tree.data.id && !keepId) delete tree.data.id + if (tree.data.uid) delete tree.data.uid if (removeActiveState) { tree.data.isActive = false } tree.children = [] if (root.children && root.children.length > 0) { root.children.forEach((item, index) => { - tree.children[index] = copyNodeTree({}, item, removeActiveState) + tree.children[index] = copyNodeTree({}, item, removeActiveState, keepId) }) } else if ( root.nodeData && @@ -155,7 +156,7 @@ export const copyNodeTree = (tree, root, removeActiveState = false, keepId = fal root.nodeData.children.length > 0 ) { root.nodeData.children.forEach((item, index) => { - tree.children[index] = copyNodeTree({}, item, removeActiveState) + tree.children[index] = copyNodeTree({}, item, removeActiveState, keepId) }) } return tree 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..2119863c --- /dev/null +++ b/simple-mind-map/src/utils/nodeExpandBtn.js @@ -0,0 +1,113 @@ +import btnsSvg from '../svg/btns' +import { SVG, Circle, G } from '@svgdotjs/svg.js' + +// 创建展开收起按钮的内容节点 +function createExpandNodeContent() { + if (this._openExpandNode) { + return + } + let { open, close } = this.mindMap.opt.expandBtnIcon || {} + // 展开的节点 + this._openExpandNode = SVG(open || btnsSvg.open).size( + this.expandBtnSize, + this.expandBtnSize + ) + this._openExpandNode.x(0).y(-this.expandBtnSize / 2) + // 收起的节点 + this._closeExpandNode = SVG(close || btnsSvg.close).size( + this.expandBtnSize, + this.expandBtnSize + ) + this._closeExpandNode.x(0).y(-this.expandBtnSize / 2) + // 填充节点 + this._fillExpandNode = new Circle().size(this.expandBtnSize) + this._fillExpandNode.x(0).y(-this.expandBtnSize / 2) + // 设置样式 + this.style.iconBtn( + this._openExpandNode, + this._closeExpandNode, + this._fillExpandNode + ) +} + +// 创建或更新展开收缩按钮内容 +function updateExpandBtnNode() { + if (this._expandBtn) { + this._expandBtn.clear() + } + this.createExpandNodeContent() + let node + if (this.nodeData.data.expand === false) { + node = this._openExpandNode + } else { + node = this._closeExpandNode + } + if (this._expandBtn) this._expandBtn.add(this._fillExpandNode).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._expandBtn.on('dblclick', e => { + e.stopPropagation() + }) + this.group.add(this._expandBtn) + } + this.updateExpandBtnNode() + this.updateExpandBtnPos() +} + +// 移除展开收缩按钮 +function removeExpandBtn() { + if (this._expandBtn) { + this._expandBtn.remove() + } +} + +export default { + createExpandNodeContent, + updateExpandBtnNode, + updateExpandBtnPos, + renderExpandBtn, + removeExpandBtn +} 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 diff --git a/web/src/pages/Doc/en/changelog/index.md b/web/src/pages/Doc/en/changelog/index.md index 9a6b402e..e2bb042d 100644 --- a/web/src/pages/Doc/en/changelog/index.md +++ b/web/src/pages/Doc/en/changelog/index.md @@ -1,5 +1,35 @@ # Changelog +## 0.5.0 + +This version is mainly about code level changes and optimization, with the core goal of improving rendering performance and reducing stuck issues. + +New: 1.Support custom expansion and collapse node icons and colors; + +optimization: 1.Optimize rendering logic, set the theme, move forward and backward, and other operations no longer require full rendering; + + 2.Optimize node drag logic, and fix the problem of being unable to drag between two nodes; + + 3.Collapse all nodes adds logic to return to the center point; + + 4.Fix the problem of nodes flying and scrambling caused by triggering rendering multiple times in a short time; + + 5.Optimize the experience of node editing; + +Fix: 1.Fix the issue where the setData method does not trigger history; + +modify: Starting from version 0.5.0, considering performance issues, the node activation state can only modify shape related styles: + +```js +[ + 'fillColor', + 'borderColor', + 'borderWidth', + 'borderDasharray', + 'borderRadius' +] +``` + ## 0.4.7 optimization: 1.During rich text editing, when initially focusing, all are no longer selected by default; 2.When editing rich text, use the node fill color as the background color to avoid being invisible when the node color is white. 3.Node activation state switching no longer triggers history. 4.Triggering history multiple times in a short time will only add the last data. 5.Optimize the addition of historical records. When there is a rollback, delete the historical data after the current pointer when adding a new record again. diff --git a/web/src/pages/Doc/en/changelog/index.vue b/web/src/pages/Doc/en/changelog/index.vue index 46210955..170d400d 100644 --- a/web/src/pages/Doc/en/changelog/index.vue +++ b/web/src/pages/Doc/en/changelog/index.vue @@ -1,6 +1,28 @@