diff --git a/index.html b/index.html index c55465f5..960e63dd 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -一个简单的web思维导图实现
\ No newline at end of file +思绪思维导图
\ No newline at end of file diff --git a/simple-mind-map/index.js b/simple-mind-map/index.js index 90ab5156..8cc6b91c 100644 --- a/simple-mind-map/index.js +++ b/simple-mind-map/index.js @@ -194,7 +194,7 @@ class MindMap { this.opt.layout = layout this.view.reset() this.renderer.setLayout() - this.render() + this.render(null, CONSTANTS.CHANGE_LAYOUT) } // 执行命令 diff --git a/simple-mind-map/package.json b/simple-mind-map/package.json index 33811747..8939adc3 100644 --- a/simple-mind-map/package.json +++ b/simple-mind-map/package.json @@ -1,6 +1,6 @@ { "name": "simple-mind-map", - "version": "0.6.5-fix.1", + "version": "0.6.6", "description": "一个简单的web在线思维导图", "authors": [ { diff --git a/simple-mind-map/src/constants/constant.js b/simple-mind-map/src/constants/constant.js index a441e99d..64826939 100644 --- a/simple-mind-map/src/constants/constant.js +++ b/simple-mind-map/src/constants/constant.js @@ -157,6 +157,7 @@ export const themeList = [ // 常量 export const CONSTANTS = { CHANGE_THEME: 'changeTheme', + CHANGE_LAYOUT: 'changeLayout', SET_DATA: 'setData', TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode', MODE: { @@ -170,7 +171,8 @@ export const CONSTANTS = { CATALOG_ORGANIZATION: 'catalogOrganization', TIMELINE: 'timeline', TIMELINE2: 'timeline2', - FISHBONE: 'fishbone' + FISHBONE: 'fishbone', + VERTICAL_TIMELINE: 'verticalTimeline' }, DIR: { UP: 'up', @@ -206,8 +208,10 @@ export const CONSTANTS = { BOTTOM: 'bottom', CENTER: 'center' }, - TIMELINE_DIR: { + LAYOUT_GROW_DIR: { + LEFT: 'left', TOP: 'top', + RIGHT: 'right', BOTTOM: 'bottom' } } @@ -246,6 +250,10 @@ export const layoutList = [ name: '时间轴2', value: CONSTANTS.LAYOUT.TIMELINE2, }, + { + name: '竖向时间轴', + value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE, + }, { name: '鱼骨图', value: CONSTANTS.LAYOUT.FISHBONE, @@ -258,6 +266,7 @@ export const layoutValueList = [ CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE, CONSTANTS.LAYOUT.TIMELINE, CONSTANTS.LAYOUT.TIMELINE2, + CONSTANTS.LAYOUT.VERTICAL_TIMELINE, CONSTANTS.LAYOUT.FISHBONE ] diff --git a/simple-mind-map/src/core/render/Render.js b/simple-mind-map/src/core/render/Render.js index 4d62128e..708b689f 100644 --- a/simple-mind-map/src/core/render/Render.js +++ b/simple-mind-map/src/core/render/Render.js @@ -4,6 +4,7 @@ import MindMap from '../../layouts/MindMap' import CatalogOrganization from '../../layouts/CatalogOrganization' import OrganizationStructure from '../../layouts/OrganizationStructure' import Timeline from '../../layouts/Timeline' +import VerticalTimeline from '../../layouts/VerticalTimeline' import Fishbone from '../../layouts/Fishbone' import TextEdit from './TextEdit' import { copyNodeTree, simpleDeepClone, walk } from '../../utils' @@ -25,6 +26,8 @@ const layouts = { [CONSTANTS.LAYOUT.TIMELINE]: Timeline, // 时间轴2 [CONSTANTS.LAYOUT.TIMELINE2]: Timeline, + // 竖向时间轴 + [CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline, // 鱼骨图 [CONSTANTS.LAYOUT.FISHBONE]: Fishbone, } @@ -693,11 +696,11 @@ class Render { if (node.isRoot) { return } - let copyData = copyNodeTree({}, node, false, true) + // let copyData = copyNodeTree({}, node, false, true) this.removeActiveNode(node) this.removeOneNode(node) this.mindMap.emit('node_active', null, this.activeNodeList) - toNode.nodeData.children.push(copyData) + toNode.nodeData.children.push(node.nodeData) this.mindMap.render() if (toNode.isRoot) { toNode.destroy() diff --git a/simple-mind-map/src/core/render/TextEdit.js b/simple-mind-map/src/core/render/TextEdit.js index 56efc9f6..6ae819a9 100644 --- a/simple-mind-map/src/core/render/TextEdit.js +++ b/simple-mind-map/src/core/render/TextEdit.js @@ -65,7 +65,8 @@ export default class TextEdit { } // 显示文本编辑框 - async show(node) { + // isInserting:是否是刚创建的节点 + async show(node, e, isInserting = false) { // 使用了自定义节点内容那么不响应编辑事件 if (node.isUseCustomNodeContent()) { return @@ -74,7 +75,7 @@ export default class TextEdit { if (typeof beforeTextEdit === 'function') { let isShow = false try { - isShow = await beforeTextEdit(node) + isShow = await beforeTextEdit(node, isInserting) } catch (error) { isShow = false } @@ -85,7 +86,7 @@ export default class TextEdit { this.mindMap.view.translateXY(offsetLeft, offsetTop) let rect = node._textData.node.node.getBoundingClientRect() if (this.mindMap.richText) { - this.mindMap.richText.showEditText(node, rect) + this.mindMap.richText.showEditText(node, rect, isInserting) return } this.showEditTextBox(node, rect) diff --git a/simple-mind-map/src/core/render/node/Node.js b/simple-mind-map/src/core/render/node/Node.js index 7934a6e7..e0f36761 100644 --- a/simple-mind-map/src/core/render/node/Node.js +++ b/simple-mind-map/src/core/render/node/Node.js @@ -269,15 +269,7 @@ class Node { this.group.add(this.shapeNode) this.updateNodeShape() // 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示 - if (!this.mindMap.opt.alwaysShowExpandBtn) { - if (!this._unVisibleRectRegionNode) { - this._unVisibleRectRegionNode = new Rect() - } - this._unVisibleRectRegionNode.fill({ - color: 'transparent' - }).size(this.expandBtnSize, height).x(width).y(0) - this.group.add(this._unVisibleRectRegionNode) - } + this.renderExpandBtnPlaceholderRect() // 概要节点添加一个带所属节点id的类名 if (this.isGeneralization && this.generalizationBelongNode) { this.group.addClass('generalization_' + this.generalizationBelongNode.uid) @@ -289,7 +281,7 @@ class Node { foreignObject.height(height) foreignObject.add(SVG(this._customNodeContent)) this.group.add(foreignObject) - return + return } // 图片节点 let imgHeight = 0 @@ -364,6 +356,21 @@ class Node { this.group.add(textContentNested) } + // 渲染展开收起按钮的隐藏占位元素 + renderExpandBtnPlaceholderRect() { + if (!this.mindMap.opt.alwaysShowExpandBtn) { + let { width, height } = this + if (!this._unVisibleRectRegionNode) { + this._unVisibleRectRegionNode = new Rect() + this._unVisibleRectRegionNode.fill({ + color: 'transparent' + }) + this.group.add(this._unVisibleRectRegionNode) + } + this.renderer.layout.renderExpandBtnRect(this._unVisibleRectRegionNode, this.expandBtnSize, width, height, this) + } + } + // 给节点绑定事件 bindGroupEvent() { // 单击事件,选中节点 @@ -433,7 +440,8 @@ class Node { // 右键菜单事件 this.group.on('contextmenu', e => { // 按住ctrl键点击鼠标左键不知为何触发的是contextmenu事件 - if (this.mindMap.opt.readonly || e.ctrlKey) {// || this.isGeneralization + if (this.mindMap.opt.readonly || e.ctrlKey) { + // || this.isGeneralization return } e.stopPropagation() @@ -467,8 +475,11 @@ class Node { if (!this.group) { return } - let { enableNodeTransitionMove, nodeTransitionMoveDuration, alwaysShowExpandBtn } = - this.mindMap.opt + let { + enableNodeTransitionMove, + nodeTransitionMoveDuration, + alwaysShowExpandBtn + } = this.mindMap.opt if (alwaysShowExpandBtn) { // 需要移除展开收缩按钮 if (this._expandBtn && this.nodeData.children.length <= 0) { @@ -543,6 +554,10 @@ class Node { this.needLayout = false this.layout() } + if (this.needRerenderExpandBtnPlaceholderRect) { + this.needRerenderExpandBtnPlaceholderRect = false + this.renderExpandBtnPlaceholderRect() + } this.update() } // 子节点 @@ -578,7 +593,7 @@ class Node { delete this.nodeData.inserting this.active() setTimeout(() => { - this.mindMap.emit('node_dblclick', this) + this.mindMap.emit('node_dblclick', this, null, true) }, 0) } } @@ -783,7 +798,7 @@ class Node { // 获取padding值 getPaddingVale() { - let { isActive }= this.nodeData.data + let { isActive } = this.nodeData.data return { paddingX: this.getStyle('paddingX', true, isActive), paddingY: this.getStyle('paddingY', true, isActive) diff --git a/simple-mind-map/src/core/view/View.js b/simple-mind-map/src/core/view/View.js index bafe7645..2f89bf66 100644 --- a/simple-mind-map/src/core/view/View.js +++ b/simple-mind-map/src/core/view/View.js @@ -82,12 +82,12 @@ class View { // 鼠标滚轮,向上和向左,都是缩小 case CONSTANTS.DIR.UP: case CONSTANTS.DIR.LEFT: - mousewheelZoomActionReverse ? this.enlarge(cx, cy) : this.narrow(cx, cy) + mousewheelZoomActionReverse ? this.enlarge(cx, cy, isTouchPad) : this.narrow(cx, cy, isTouchPad) break // 鼠标滚轮,向下和向右,都是放大 case CONSTANTS.DIR.DOWN: case CONSTANTS.DIR.RIGHT: - mousewheelZoomActionReverse ? this.narrow(cx, cy) : this.enlarge(cx, cy) + mousewheelZoomActionReverse ? this.narrow(cx, cy, isTouchPad) : this.enlarge(cx, cy, isTouchPad) break } } else {// 鼠标滚轮事件控制画布移动 @@ -199,16 +199,18 @@ class View { } // 缩小 - narrow(cx, cy) { - const scale = Math.max(this.scale - this.mindMap.opt.scaleRatio, 0.1) + narrow(cx, cy, isTouchPad) { + const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1) + const scale = Math.max(this.scale - scaleRatio, 0.1) this.scaleInCenter(scale, cx, cy) this.transform() this.mindMap.emit('scale', this.scale) } // 放大 - enlarge(cx, cy) { - const scale = this.scale + this.mindMap.opt.scaleRatio + enlarge(cx, cy, isTouchPad) { + const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1) + const scale = this.scale + scaleRatio this.scaleInCenter(scale, cx, cy) this.transform() this.mindMap.emit('scale', this.scale) diff --git a/simple-mind-map/src/layouts/Base.js b/simple-mind-map/src/layouts/Base.js index 701fffbf..ca27fcd4 100644 --- a/simple-mind-map/src/layouts/Base.js +++ b/simple-mind-map/src/layouts/Base.js @@ -48,6 +48,20 @@ class Base { return [CONSTANTS.CHANGE_THEME, CONSTANTS.TRANSFORM_TO_NORMAL_NODE].includes(this.renderer.renderSource) } + // 层级类型改变 + checkIsLayerTypeChange(oldIndex, newIndex) { + if (oldIndex >= 2 && newIndex >= 2) return false + if (oldIndex >= 2 && newIndex < 2) return true + if (oldIndex < 2 && newIndex >= 2) return true + } + + // 检查是否是结构布局改变重新渲染展开收起按钮占位元素 + checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) { + if (this.renderer.renderSource === CONSTANTS.CHANGE_LAYOUT) { + node.needRerenderExpandBtnPlaceholderRect = true + } + } + // 创建节点实例 createNode(data, parent, isRoot, layerIndex) { // 创建节点 @@ -55,11 +69,13 @@ class Base { // 数据上保存了节点引用,那么直接复用节点 if (data && data._node && !this.renderer.reRender) { newNode = data._node + let isLayerTypeChange = this.checkIsLayerTypeChange(newNode.layerIndex, layerIndex) newNode.reset() newNode.layerIndex = layerIndex this.cacheNode(data._node.uid, newNode) + this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode) // 主题或主题配置改变了需要重新计算节点大小和布局 - if (this.checkIsNeedResizeSources()) { + if (this.checkIsNeedResizeSources() || isLayerTypeChange) { newNode.getSize() newNode.needLayout = true } @@ -68,16 +84,18 @@ class Base { newNode = this.lru.get(data.data.uid) // 保存该节点上一次的数据 let lastData = JSON.stringify(newNode.nodeData.data) + let isLayerTypeChange = this.checkIsLayerTypeChange(newNode.layerIndex, layerIndex) newNode.reset() newNode.nodeData = newNode.handleData(data || {}) newNode.layerIndex = layerIndex this.cacheNode(data.data.uid, newNode) + this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode) data._node = newNode // 主题或主题配置改变了需要重新计算节点大小和布局 let isResizeSource = this.checkIsNeedResizeSources() // 节点数据改变了需要重新计算节点大小和布局 let isNodeDataChange = lastData !== JSON.stringify(data.data) - if (isResizeSource || isNodeDataChange) { + if (isResizeSource || isNodeDataChange || isLayerTypeChange) { newNode.getSize() newNode.needLayout = true } diff --git a/simple-mind-map/src/layouts/CatalogOrganization.js b/simple-mind-map/src/layouts/CatalogOrganization.js index 72a00175..3e780c0d 100644 --- a/simple-mind-map/src/layouts/CatalogOrganization.js +++ b/simple-mind-map/src/layouts/CatalogOrganization.js @@ -349,6 +349,11 @@ class CatalogOrganization extends Base { gNode.left = right + generalizationNodeMargin gNode.top = top + (bottom - top - gNode.height) / 2 } + + // 渲染展开收起按钮的隐藏占位元素 + renderExpandBtnRect(rect, expandBtnSize, width, height, node) { + rect.size(width, expandBtnSize).x(0).y(height) + } } export default CatalogOrganization diff --git a/simple-mind-map/src/layouts/Fishbone.js b/simple-mind-map/src/layouts/Fishbone.js index e8581852..b11c2634 100644 --- a/simple-mind-map/src/layouts/Fishbone.js +++ b/simple-mind-map/src/layouts/Fishbone.js @@ -51,8 +51,8 @@ class Fishbone extends Base { // 节点生长方向 newNode.dir = index % 2 === 0 - ? CONSTANTS.TIMELINE_DIR.TOP - : CONSTANTS.TIMELINE_DIR.BOTTOM + ? CONSTANTS.LAYOUT_GROW_DIR.TOP + : CONSTANTS.LAYOUT_GROW_DIR.BOTTOM } // 计算二级节点的top值 if (parent._node.isRoot) { @@ -222,7 +222,7 @@ class Fishbone extends Base { // 检查节点是否是上方节点 checkIsTop(node) { - return node.dir === CONSTANTS.TIMELINE_DIR.TOP + return node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP } // 绘制连线,连接该节点到其子节点 @@ -239,7 +239,7 @@ class Fishbone extends Base { // 当前节点是根节点 // 根节点的子节点是和根节点同一水平线排列 let maxx = -Infinity - node.children.forEach((item) => { + node.children.forEach(item => { if (item.left > maxx) { maxx = item.left } @@ -250,15 +250,15 @@ class Fishbone extends Base { let line = this.draw.path() if (this.checkIsTop(item)) { line.plot( - `M ${nodeLineX - offsetX},${item.top + item.height + offset} L ${item.left},${ - item.top + item.height - }` + `M ${nodeLineX - offsetX},${item.top + item.height + offset} L ${ + item.left + },${item.top + item.height}` ) } else { line.plot( - `M ${nodeLineX - offsetX},${ - item.top - offset - } L ${nodeLineX},${item.top}` + `M ${nodeLineX - offsetX},${item.top - offset} L ${nodeLineX},${ + item.top + }` ) } node.style.line(line) @@ -373,6 +373,27 @@ class Fishbone extends Base { gNode.left = right + generalizationNodeMargin gNode.top = top + (bottom - top - gNode.height) / 2 } + + // 渲染展开收起按钮的隐藏占位元素 + renderExpandBtnRect(rect, expandBtnSize, width, height, node) { + let dir = '' + if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) { + dir = + node.layerIndex === 1 + ? CONSTANTS.LAYOUT_GROW_DIR.TOP + : CONSTANTS.LAYOUT_GROW_DIR.BOTTOM + } else { + dir = + node.layerIndex === 1 + ? CONSTANTS.LAYOUT_GROW_DIR.BOTTOM + : CONSTANTS.LAYOUT_GROW_DIR.TOP + } + if (dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) { + rect.size(width, expandBtnSize).x(0).y(-expandBtnSize) + } else { + rect.size(width, expandBtnSize).x(0).y(height) + } + } } export default Fishbone diff --git a/simple-mind-map/src/layouts/FishboneBottom.js b/simple-mind-map/src/layouts/FishboneBottom.js index 25adc205..ca66b6a6 100644 --- a/simple-mind-map/src/layouts/FishboneBottom.js +++ b/simple-mind-map/src/layouts/FishboneBottom.js @@ -52,8 +52,8 @@ class Fishbone extends Base { // 节点生长方向 newNode.dir = index % 2 === 0 - ? CONSTANTS.TIMELINE_DIR.TOP - : CONSTANTS.TIMELINE_DIR.BOTTOM + ? CONSTANTS.LAYOUT_GROW_DIR.TOP + : CONSTANTS.LAYOUT_GROW_DIR.BOTTOM } // 计算二级节点的top值 if (parent._node.isRoot) { diff --git a/simple-mind-map/src/layouts/FishboneTop.js b/simple-mind-map/src/layouts/FishboneTop.js index f7799027..0f4fa57a 100644 --- a/simple-mind-map/src/layouts/FishboneTop.js +++ b/simple-mind-map/src/layouts/FishboneTop.js @@ -52,8 +52,8 @@ class Fishbone extends Base { // 节点生长方向 newNode.dir = index % 2 === 0 - ? CONSTANTS.TIMELINE_DIR.TOP - : CONSTANTS.TIMELINE_DIR.BOTTOM + ? CONSTANTS.LAYOUT_GROW_DIR.TOP + : CONSTANTS.LAYOUT_GROW_DIR.BOTTOM } // 计算二级节点的top值 if (parent._node.isRoot) { @@ -281,7 +281,7 @@ class Fishbone extends Base { if ( node.parent && node.parent.isRoot && - node.dir === CONSTANTS.TIMELINE_DIR.TOP + node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP ) { line.plot( `M ${x},${top} L ${x + lineLength},${ diff --git a/simple-mind-map/src/layouts/LogicalStructure.js b/simple-mind-map/src/layouts/LogicalStructure.js index c136b0b1..518bac05 100644 --- a/simple-mind-map/src/layouts/LogicalStructure.js +++ b/simple-mind-map/src/layouts/LogicalStructure.js @@ -172,9 +172,7 @@ class LogicalStructure extends Base { let x2 = item.left let y2 = item.top + item.height / 2 // 节点使用横线风格,需要额外渲染横线 - let nodeUseLineStyleOffset = nodeUseLineStyle - ? item.width - : 0 + let nodeUseLineStyleOffset = nodeUseLineStyle ? item.width : 0 y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1 y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2 let path = `M ${x1},${y1} L ${x1 + s1},${y1} L ${x1 + s1},${y2} L ${ @@ -260,10 +258,7 @@ class LogicalStructure extends Base { if (_x === translateX && _y === translateY) { return } - btn.translate( - _x - translateX, - _y - translateY - ) + btn.translate(_x - translateX, _y - translateY) } // 创建概要节点 @@ -286,6 +281,11 @@ class LogicalStructure extends Base { gNode.left = right + generalizationNodeMargin gNode.top = top + (bottom - top - gNode.height) / 2 } + + // 渲染展开收起按钮的隐藏占位元素 + renderExpandBtnRect(rect, expandBtnSize, width, height, node) { + rect.size(expandBtnSize, height).x(width).y(0) + } } export default LogicalStructure diff --git a/simple-mind-map/src/layouts/MindMap.js b/simple-mind-map/src/layouts/MindMap.js index 6c030c4e..bdab6c67 100644 --- a/simple-mind-map/src/layouts/MindMap.js +++ b/simple-mind-map/src/layouts/MindMap.js @@ -1,5 +1,6 @@ import Base from './Base' import { walk, asyncRun } from '../utils' +import { CONSTANTS } from '../constants/constant' // 思维导图 class MindMap extends Base { @@ -45,11 +46,14 @@ class MindMap extends Base { newNode.dir = parent._node.dir } else { // 节点生长方向 - newNode.dir = index % 2 === 0 ? 'right' : 'left' + newNode.dir = + index % 2 === 0 + ? CONSTANTS.LAYOUT_GROW_DIR.RIGHT + : CONSTANTS.LAYOUT_GROW_DIR.LEFT } // 根据生长方向定位到父节点的左侧或右侧 newNode.left = - newNode.dir === 'right' + newNode.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT ? parent._node.left + parent._node.width + this.getMarginX(layerIndex) @@ -72,7 +76,7 @@ class MindMap extends Base { let leftChildrenAreaHeight = 0 let rightChildrenAreaHeight = 0 cur._node.children.forEach(item => { - if (item.dir === 'left') { + if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) { leftLen++ leftChildrenAreaHeight += item.height } else { @@ -109,7 +113,7 @@ class MindMap extends Base { let leftTotalTop = baseTop - node.leftChildrenAreaHeight / 2 let rightTotalTop = baseTop - node.rightChildrenAreaHeight / 2 node.children.forEach(cur => { - if (cur.dir === 'left') { + if (cur.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) { cur.top = leftTotalTop leftTotalTop += cur.height + marginY } else { @@ -162,7 +166,10 @@ class MindMap extends Base { return } let _offset = 0 - let addHeight = item.dir === 'left' ? leftAddHeight : rightAddHeight + let addHeight = + item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT + ? leftAddHeight + : rightAddHeight // 上面的节点往上移 if (_index < index) { _offset = -addHeight @@ -208,10 +215,8 @@ class MindMap extends Base { let x1 = 0 let _s = 0 // 节点使用横线风格,需要额外渲染横线 - let nodeUseLineStyleOffset = nodeUseLineStyle - ? item.width - : 0 - if (item.dir === 'left') { + let nodeUseLineStyleOffset = nodeUseLineStyle ? item.width : 0 + if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) { _s = -s1 x1 = node.layerIndex === 0 ? left : left - expandBtnSize nodeUseLineStyleOffset = -nodeUseLineStyleOffset @@ -220,7 +225,10 @@ class MindMap extends Base { x1 = node.layerIndex === 0 ? left + width : left + width + expandBtnSize } let y1 = top + height / 2 - let x2 = item.dir === 'left' ? item.left + item.width : item.left + let x2 = + item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT + ? item.left + item.width + : item.left let y2 = item.top + item.height / 2 y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1 y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2 @@ -246,18 +254,21 @@ class MindMap extends Base { let x1 = node.layerIndex === 0 ? left + width / 2 - : item.dir === 'left' + : item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT ? left - expandBtnSize : left + width + expandBtnSize let y1 = top + height / 2 - let x2 = item.dir === 'left' ? item.left + item.width : item.left + let x2 = + item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT + ? item.left + item.width + : item.left let y2 = item.top + item.height / 2 y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1 y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2 // 节点使用横线风格,需要额外渲染横线 let nodeUseLineStylePath = '' if (nodeUseLineStyle) { - if (item.dir === 'left') { + if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) { nodeUseLineStylePath = ` L ${item.left},${y2}` } else { nodeUseLineStylePath = ` L ${item.left + item.width},${y2}` @@ -283,11 +294,14 @@ class MindMap extends Base { let x1 = node.layerIndex === 0 ? left + width / 2 - : item.dir === 'left' + : item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT ? left - expandBtnSize : left + width + expandBtnSize let y1 = top + height / 2 - let x2 = item.dir === 'left' ? item.left + item.width : item.left + let x2 = + item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT + ? item.left + item.width + : item.left let y2 = item.top + item.height / 2 let path = '' y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1 @@ -295,7 +309,7 @@ class MindMap extends Base { // 节点使用横线风格,需要额外渲染横线 let nodeUseLineStylePath = '' if (this.mindMap.themeConfig.nodeUseLineStyle) { - if (item.dir === 'left') { + if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) { nodeUseLineStylePath = ` L ${item.left},${y2}` } else { nodeUseLineStylePath = ` L ${item.left + item.width},${y2}` @@ -320,7 +334,8 @@ class MindMap extends Base { ? height / 2 : 0 // 位置没有变化则返回 - let _x = (node.dir === 'left' ? 0 - expandBtnSize : width) + let _x = + node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT ? 0 - expandBtnSize : width let _y = height / 2 + nodeUseLineStyleOffset if (_x === translateX && _y === translateY) { return @@ -332,7 +347,7 @@ class MindMap extends Base { // 创建概要节点 renderGeneralization(node, gLine, gNode) { - let isLeft = node.dir === 'left' + let isLeft = node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT let { top, bottom, @@ -358,6 +373,15 @@ class MindMap extends Base { (isLeft ? gNode.width : 0) gNode.top = top + (bottom - top - gNode.height) / 2 } + + // 渲染展开收起按钮的隐藏占位元素 + renderExpandBtnRect(rect, expandBtnSize, width, height, node) { + if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) { + rect.size(expandBtnSize, height).x(-expandBtnSize).y(0) + } else { + rect.size(expandBtnSize, height).x(width).y(0) + } + } } export default MindMap diff --git a/simple-mind-map/src/layouts/OrganizationStructure.js b/simple-mind-map/src/layouts/OrganizationStructure.js index f03883a0..179ef4c2 100644 --- a/simple-mind-map/src/layouts/OrganizationStructure.js +++ b/simple-mind-map/src/layouts/OrganizationStructure.js @@ -255,6 +255,11 @@ class OrganizationStructure extends Base { gNode.top = bottom + generalizationNodeMargin gNode.left = left + (right - left - gNode.width) / 2 } + + // 渲染展开收起按钮的隐藏占位元素 + renderExpandBtnRect(rect, expandBtnSize, width, height, node) { + rect.size(width, expandBtnSize).x(0).y(height) + } } export default OrganizationStructure diff --git a/simple-mind-map/src/layouts/Timeline.js b/simple-mind-map/src/layouts/Timeline.js index 60a41db2..a3f166fa 100644 --- a/simple-mind-map/src/layouts/Timeline.js +++ b/simple-mind-map/src/layouts/Timeline.js @@ -50,8 +50,8 @@ class Timeline extends Base { // 节点生长方向 newNode.dir = index % 2 === 0 - ? CONSTANTS.TIMELINE_DIR.BOTTOM - : CONSTANTS.TIMELINE_DIR.TOP + ? CONSTANTS.LAYOUT_GROW_DIR.BOTTOM + : CONSTANTS.LAYOUT_GROW_DIR.TOP } } else { newNode.dir = '' @@ -151,7 +151,7 @@ class Timeline extends Base { if ( parent && parent.isRoot && - node.dir === CONSTANTS.TIMELINE_DIR.TOP + node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP ) { // 遍历二级节点的子节点 node.children.forEach(item => { @@ -280,7 +280,7 @@ class Timeline extends Base { if ( node.parent && node.parent.isRoot && - node.dir === CONSTANTS.TIMELINE_DIR.TOP + node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP ) { line.plot(`M ${x},${top} L ${x},${miny}`) } else { @@ -301,7 +301,7 @@ class Timeline extends Base { if ( node.parent && node.parent.isRoot && - node.dir === CONSTANTS.TIMELINE_DIR.TOP + node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP ) { btn.translate( width * 0.3 - expandBtnSize / 2 - translateX, @@ -336,6 +336,28 @@ class Timeline extends Base { gNode.left = right + generalizationNodeMargin gNode.top = top + (bottom - top - gNode.height) / 2 } + + // 渲染展开收起按钮的隐藏占位元素 + renderExpandBtnRect(rect, expandBtnSize, width, height, node) { + if (this.layout === CONSTANTS.LAYOUT.TIMELINE) { + rect.size(width, expandBtnSize).x(0).y(height) + } else { + let dir = '' + if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) { + dir = + node.layerIndex === 1 + ? CONSTANTS.LAYOUT_GROW_DIR.TOP + : CONSTANTS.LAYOUT_GROW_DIR.BOTTOM + } else { + dir = CONSTANTS.LAYOUT_GROW_DIR.BOTTOM + } + if (dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) { + rect.size(width, expandBtnSize).x(0).y(-expandBtnSize) + } else { + rect.size(width, expandBtnSize).x(0).y(height) + } + } + } } export default Timeline diff --git a/simple-mind-map/src/layouts/VerticalTimeline.js b/simple-mind-map/src/layouts/VerticalTimeline.js new file mode 100644 index 00000000..d7e24829 --- /dev/null +++ b/simple-mind-map/src/layouts/VerticalTimeline.js @@ -0,0 +1,431 @@ +import Base from './Base' +import { walk, asyncRun } from '../utils' +import { CONSTANTS } from '../constants/constant' + +// 竖向时间轴 +class VerticalTimeline extends Base { + // 构造函数 + constructor(opt = {}, layout) { + super(opt) + this.layout = layout + } + + // 布局 + doLayout(callback) { + let task = [ + () => { + this.computedBaseValue() + }, + () => { + this.computedTopValue() + }, + () => { + this.adjustLeftTopValue() + }, + () => { + callback(this.root) + } + ] + asyncRun(task) + } + + // 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值 + computedBaseValue() { + walk( + this.renderer.renderTree, + null, + (cur, parent, isRoot, layerIndex, index) => { + let newNode = this.createNode(cur, parent, isRoot, layerIndex) + // 根节点定位在画布中心位置 + if (isRoot) { + this.setNodeCenter(newNode) + } else { + // 非根节点 + // 节点生长方向 + // 三级及以下节点以上级为准 + if (parent._node.dir) { + newNode.dir = parent._node.dir + } else { + newNode.dir = + index % 2 === 0 + ? CONSTANTS.LAYOUT_GROW_DIR.RIGHT + : CONSTANTS.LAYOUT_GROW_DIR.LEFT + } + // 定位二级节点的left + if (parent._node.isRoot) { + newNode.left = + parent._node.left + + (cur._node.width > parent._node.width + ? -(cur._node.width - parent._node.width) / 2 + : (parent._node.width - cur._node.width) / 2) + } else { + newNode.left = + newNode.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT + ? parent._node.left + + parent._node.width + + this.getMarginX(layerIndex) + : parent._node.left - + this.getMarginX(layerIndex) - + newNode.width + } + } + if (!cur.data.expand) { + return true + } + }, + (cur, parent, isRoot, layerIndex) => { + // 返回时计算节点的areaHeight,也就是子节点所占的高度之和,包括外边距 + if (isRoot) { + return + } + let len = cur.data.expand === false ? 0 : cur._node.children.length + cur._node.childrenAreaHeight = len + ? cur._node.children.reduce((h, item) => { + return h + item.height + }, 0) + + (len + 1) * this.getMarginY(layerIndex + 1) + : 0 + }, + true, + 0 + ) + } + + // 遍历节点树计算节点的top + computedTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex, index) => { + if ( + node.nodeData.data.expand && + node.children && + node.children.length + ) { + let marginY = this.getMarginY(layerIndex + 1) + // 定位二级节点的top + if (isRoot) { + let top = node.top + node.height + let totalTop = top + marginY + node.children.forEach(cur => { + cur.top = totalTop + totalTop += cur.height + marginY + }) + } else { + // 定位三级及以下节点的top + let marginY = this.getMarginY(layerIndex + 1) + let baseTop = node.top + node.height / 2 + marginY + // 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半 + let totalTop = baseTop - node.childrenAreaHeight / 2 + node.children.forEach(cur => { + cur.top = totalTop + totalTop += cur.height + marginY + }) + } + } + }, + null, + true + ) + } + + // 调整节点left、top + adjustLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex) => { + if (!node.nodeData.data.expand) { + return + } + if (isRoot) return + // 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置 + let base = this.getMarginY(layerIndex + 1) * 2 + node.height + let difference = node.childrenAreaHeight - base + if (difference > 0) { + this.updateBrothers(node, difference / 2) + } + }, + null, + true + ) + } + + // 更新兄弟节点的top + updateBrothers(node, addHeight) { + if (node.parent) { + let childrenList = node.parent.children + let index = childrenList.findIndex(item => { + return item === node + }) + childrenList.forEach((item, _index) => { + // 自定义节点位置 + if (item.hasCustomPosition()) return + // 三级或三级以下节点自身位置不需要动 + if (!node.parent.isRoot && item === node) return + let _offset = 0 + // 二级节点上面的兄弟节点不需要移动,自身需要往下移动 + if (node.parent.isRoot) { + // 上面的节点不用移 + if (_index < index) { + _offset = 0 + } else if (_index > index) { + // 下面的节点往下移 + _offset = addHeight * 2 + } else { + // 自身也要移动 + _offset = addHeight + } + } else { + // 三级或三级以下节点两侧的兄弟节点向两侧移动 + // 上面的节点往上移 + if (_index < index) { + _offset = -addHeight + } else if (_index > index) { + // 下面的节点往下移 + _offset = addHeight + } + } + item.top += _offset + // 同步更新子节点的位置 + if (item.children && item.children.length) { + this.updateChildren(item.children, 'top', _offset) + } + }) + // 更新父节点的位置 + this.updateBrothers(node.parent, addHeight) + } + } + + // 调整兄弟节点的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, lineStyle) { + if (lineStyle === 'curve') { + this.renderLineCurve(node, lines, style) + } else if (lineStyle === 'direct') { + this.renderLineDirect(node, lines, style) + } else { + this.renderLineStraight(node, lines, style) + } + } + + // 直线连接 + renderLineStraight(node, lines, style) { + if (node.children.length <= 0) { + return [] + } + let { expandBtnSize } = node + if (!this.mindMap.opt.alwaysShowExpandBtn) { + expandBtnSize = 0 + } + if (node.isRoot) { + // 当前节点是根节点 + let prevBother = node + // 根节点的子节点是和根节点同一水平线排列 + node.children.forEach((item, index) => { + let y1 = prevBother.top + prevBother.height + let y2 = item.top + let x = node.left + node.width / 2 + let path = `M ${x},${y1} L ${x},${y2}` + lines[index].plot(path) + style && style(lines[index], item) + prevBother = item + }) + } else { + // 当前节点为非根节点 + if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT) { + let nodeRight = node.left + node.width + let nodeYCenter = node.top + node.height / 2 + let marginX = this.getMarginX(node.layerIndex + 1) + let offset = (marginX - expandBtnSize) * 0.6 + node.children.forEach((item, index) => { + let itemLeft = item.left + let itemYCenter = item.top + item.height / 2 + let path = ` + M ${nodeRight},${nodeYCenter} + L ${nodeRight + offset},${nodeYCenter} + L ${nodeRight + offset},${itemYCenter} + L ${itemLeft},${itemYCenter}` + lines[index].plot(path) + style && style(lines[index], item) + }) + } else { + let nodeLeft = node.left + let nodeYCenter = node.top + node.height / 2 + let marginX = this.getMarginX(node.layerIndex + 1) + let offset = (marginX - expandBtnSize) * 0.6 + node.children.forEach((item, index) => { + let itemRight = item.left + item.width + let itemYCenter = item.top + item.height / 2 + let path = ` + M ${nodeLeft},${nodeYCenter} + L ${nodeLeft - offset},${nodeYCenter} + L ${nodeLeft - offset},${itemYCenter} + L ${itemRight},${itemYCenter}` + lines[index].plot(path) + style && style(lines[index], item) + }) + } + } + } + + // 直连 + renderLineDirect(node, lines, style) { + if (node.children.length <= 0) { + return [] + } + let { left, top, width, height, expandBtnSize } = node + if (!this.mindMap.opt.alwaysShowExpandBtn) { + expandBtnSize = 0 + } + node.children.forEach((item, index) => { + if (node.isRoot) { + let prevBother = node + // 根节点的子节点是和根节点同一水平线排列 + node.children.forEach((item, index) => { + let y1 = prevBother.top + prevBother.height + let y2 = item.top + let x = node.left + node.width / 2 + let path = `M ${x},${y1} L ${x},${y2}` + lines[index].plot(path) + style && style(lines[index], item) + prevBother = item + }) + } else { + let x1 = + item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT + ? left - expandBtnSize + : left + width + expandBtnSize + let y1 = top + height / 2 + let x2 = + item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT + ? item.left + item.width + : item.left + let y2 = item.top + item.height / 2 + let path = `M ${x1},${y1} L ${x2},${y2}` + lines[index].plot(path) + style && style(lines[index], item) + } + }) + } + + // 曲线风格连线 + renderLineCurve(node, lines, style) { + if (node.children.length <= 0) { + return [] + } + let { left, top, width, height, expandBtnSize } = node + if (!this.mindMap.opt.alwaysShowExpandBtn) { + expandBtnSize = 0 + } + node.children.forEach((item, index) => { + if (node.isRoot) { + let prevBother = node + // 根节点的子节点是和根节点同一水平线排列 + node.children.forEach((item, index) => { + let y1 = prevBother.top + prevBother.height + let y2 = item.top + let x = node.left + node.width / 2 + let path = `M ${x},${y1} L ${x},${y2}` + lines[index].plot(path) + style && style(lines[index], item) + prevBother = item + }) + } else { + let x1 = + item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT + ? left - expandBtnSize + : left + width + expandBtnSize + let y1 = top + height / 2 + let x2 = + item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT + ? item.left + item.width + : item.left + let y2 = item.top + item.height / 2 + let path = this.cubicBezierPath(x1, y1, x2, y2) + lines[index].plot(path) + style && style(lines[index], item) + } + }) + } + + // 渲染按钮 + renderExpandBtn(node, btn) { + let { width, height, expandBtnSize, isRoot } = node + if (!isRoot) { + let { translateX, translateY } = btn.transform() + if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT) { + btn.translate(width - translateX, height / 2 - translateY) + } else { + btn.translate(-expandBtnSize - translateX, height / 2 - translateY) + } + } + } + + // 创建概要节点 + renderGeneralization(node, gLine, gNode) { + let isLeft = node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT + let { + top, + bottom, + left, + right, + generalizationLineMargin, + generalizationNodeMargin + } = this.getNodeBoundaries(node, 'h', isLeft) + let x = isLeft + ? left - generalizationLineMargin + : right + generalizationLineMargin + let x1 = x + let y1 = top + let x2 = x + let y2 = bottom + let cx = x1 + (isLeft ? -20 : 20) + let cy = y1 + (y2 - y1) / 2 + let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}` + gLine.plot(path) + gNode.left = + x + + (isLeft ? -generalizationNodeMargin : generalizationNodeMargin) - + (isLeft ? gNode.width : 0) + gNode.top = top + (bottom - top - gNode.height) / 2 + } + + // 渲染展开收起按钮的隐藏占位元素 + renderExpandBtnRect(rect, expandBtnSize, width, height, node) { + if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) { + rect.size(expandBtnSize, height).x(-expandBtnSize).y(0) + } else { + rect.size(expandBtnSize, height).x(width).y(0) + } + } +} + +export default VerticalTimeline diff --git a/simple-mind-map/src/parse/xmind.js b/simple-mind-map/src/parse/xmind.js index 801a3e92..ed8810c2 100644 --- a/simple-mind-map/src/parse/xmind.js +++ b/simple-mind-map/src/parse/xmind.js @@ -1,5 +1,11 @@ import JSZip from 'jszip' import xmlConvert from 'xml-js' +import { + getTextFromHtml, + imgToDataUrl, + parseDataUrl, + getImageSize +} from '../utils/index' // 解析.xmind文件 const parseXmindFile = file => { @@ -10,7 +16,7 @@ const parseXmindFile = file => { let content = '' if (zip.files['content.json']) { let json = await zip.files['content.json'].async('string') - content = transformXmind(json) + content = await transformXmind(json, zip.files) } else if (zip.files['content.xml']) { let xml = await zip.files['content.xml'].async('string') let json = xmlConvert.xml2json(xml) @@ -33,11 +39,12 @@ const parseXmindFile = file => { } // 转换xmind数据 -const transformXmind = content => { +const transformXmind = async (content, files) => { let data = JSON.parse(content)[0] let nodeTree = data.rootTopic let newTree = {} - let walk = (node, newNode) => { + let waitLoadImageList = [] + let walk = async (node, newNode) => { newNode.data = { // 节点内容 text: node.title @@ -55,6 +62,42 @@ const transformXmind = content => { if (node.labels && node.labels.length > 0) { newNode.data.tag = node.labels } + // 图片 + if (node.image && /\.(jpg|jpeg|png|gif|webp)$/.test(node.image.src)) { + try { + // 处理异步逻辑 + let resolve = null + let promise = new Promise(_resolve => { + resolve = _resolve + }) + waitLoadImageList.push(promise) + // 读取图片 + let imageType = /\.([^.]+)$/.exec(node.image.src)[1] + let imageBase64 = + `data:image/${imageType};base64,` + + (await files['resources/' + node.image.src.split('/')[1]].async( + 'base64' + )) + newNode.data.image = imageBase64 + // 如果图片尺寸不存在 + if (!node.image.width && !node.image.height) { + let imageSize = await getImageSize(imageBase64) + newNode.data.imageSize = { + width: imageSize.width, + height: imageSize.height + } + } else { + newNode.data.imageSize = { + width: node.image.width, + height: node.image.height + } + } + resolve() + } catch (error) { + console.log(error) + resolve() + } + } // 子节点 newNode.children = [] if ( @@ -70,6 +113,7 @@ const transformXmind = content => { } } walk(nodeTree, newTree) + await Promise.all(waitLoadImageList) return newTree } @@ -158,8 +202,127 @@ const transformOldXmind = content => { return newTree } +// 数据转换为xmind文件 +const transformToXmind = async (data, name) => { + const id = 'simpleMindMap_' + Date.now() + const imageList = [] + // 转换核心数据 + let newTree = {} + let waitLoadImageList = [] + let walk = async (node, newNode, isRoot) => { + let newData = { + structureClass: 'org.xmind.ui.logic.right', + title: getTextFromHtml(node.data.text), // 节点文本 + children: { + attached: [] + } + } + // 备注 + if (node.data.note !== undefined) { + newData.notes = { + realHTML: { + content: node.data.note + }, + plain: { + content: node.data.note + } + } + } + // 超链接 + if (node.data.hyperlink !== undefined) { + newData.href = node.data.hyperlink + } + // 标签 + if (node.data.tag !== undefined) { + newData.labels = node.data.tag || [] + } + // 图片 + if (node.data.image) { + try { + // 处理异步逻辑 + let resolve = null + let promise = new Promise(_resolve => { + resolve = _resolve + }) + waitLoadImageList.push(promise) + let imgName = '' + let imgData = node.data.image + // 网络图片要先转换成data:url + if (/^https?:\/\//.test(node.data.image)) { + imgData = await imgToDataUrl(node.data.image) + } + // 从data:url中解析出图片类型和base64 + let dataUrlRes = parseDataUrl(imgData) + imgName = 'image_' + imageList.length + '.' + dataUrlRes.type + imageList.push({ + name: imgName, + data: dataUrlRes.base64 + }) + newData.image = { + src: 'xap:resources/' + imgName, + width: node.data.imageSize.width, + height: node.data.imageSize.height + } + resolve() + } catch (error) { + console.log(error) + resolve() + } + } + // 样式 + // 暂时不考虑样式 + if (isRoot) { + newData.class = 'topic' + newNode.id = id + newNode.class = 'sheet' + newNode.title = name + newNode.extensions = [] + newNode.topicPositioning = 'fixed' + newNode.topicOverlapping = 'overlap' + newNode.coreVersion = '2.100.0' + newNode.rootTopic = newData + } else { + Object.keys(newData).forEach(key => { + newNode[key] = newData[key] + }) + } + if (node.children && node.children.length > 0) { + node.children.forEach(child => { + let newChild = {} + walk(child, newChild) + newData.children.attached.push(newChild) + }) + } + } + walk(data, newTree, true) + await Promise.all(waitLoadImageList) + const contentData = [newTree] + // 创建压缩包 + const zip = new JSZip() + zip.file('content.json', JSON.stringify(contentData)) + zip.file( + 'metadata.json', + `{"modifier":"","dataStructureVersion":"1","layoutEngineVersion":"2","activeSheetId":"${id}"}` + ) + const manifestData = { + 'file-entries': { 'content.json': {}, 'metadata.json': {} } + } + // 图片 + if (imageList.length > 0) { + imageList.forEach(item => { + manifestData['file-entries']['resources/' + item.name] = {} + const img = zip.folder('resources') + img.file(item.name, item.data, { base64: true }) + }) + } + zip.file('manifest.json', JSON.stringify(manifestData)) + const zipData = await zip.generateAsync({ type: 'blob' }) + return zipData +} + export default { parseXmindFile, transformXmind, - transformOldXmind + transformOldXmind, + transformToXmind } diff --git a/simple-mind-map/src/plugins/Export.js b/simple-mind-map/src/plugins/Export.js index 5186ffe8..1f719d8f 100644 --- a/simple-mind-map/src/plugins/Export.js +++ b/simple-mind-map/src/plugins/Export.js @@ -180,6 +180,17 @@ class Export { this.mindMap.doExportPDF.pdf(name, img) } + // 导出为xmind + async xmind(name) { + if (!this.mindMap.doExportXMind) { + throw new Error('请注册ExportXMind插件') + } + const data = this.mindMap.getData() + const blob = await this.mindMap.doExportXMind.xmind(data, name) + const res = await readBlob(blob) + return res + } + // 导出为svg // plusCssText:附加的css样式,如果svg中存在dom节点,想要设置一些针对节点的样式可以通过这个参数传入 async svg(name, plusCssText) { diff --git a/simple-mind-map/src/plugins/ExportXMind.js b/simple-mind-map/src/plugins/ExportXMind.js new file mode 100644 index 00000000..5f54e220 --- /dev/null +++ b/simple-mind-map/src/plugins/ExportXMind.js @@ -0,0 +1,19 @@ +import xmind from '../parse/xmind' + +// 导出XMind类,需要通过Export插件使用 +class ExportXMind { + // 构造函数 + constructor(opt) { + this.mindMap = opt.mindMap + } + + // 导出xmind + async xmind(data, name) { + const zipData = await xmind.transformToXmind(data, name) + return zipData + } +} + +ExportXMind.instanceName = 'doExportXMind' + +export default ExportXMind diff --git a/simple-mind-map/src/plugins/RichText.js b/simple-mind-map/src/plugins/RichText.js index 47cc9ecd..d501c282 100644 --- a/simple-mind-map/src/plugins/RichText.js +++ b/simple-mind-map/src/plugins/RichText.js @@ -39,6 +39,7 @@ class RichText { this.range = null this.lastRange = null this.node = null + this.isInserting = false this.styleEl = null this.cacheEditingText = '' this.lostStyle = false @@ -145,11 +146,12 @@ class RichText { } // 显示文本编辑控件 - showEditText(node, rect) { + showEditText(node, rect, isInserting) { if (this.showTextEdit) { return } this.node = node + this.isInserting = isInserting if (!rect) rect = node._textData.node.node.getBoundingClientRect() this.mindMap.emit('before_show_text_edit') this.mindMap.renderer.textEdit.registerTmpShortcut() @@ -200,7 +202,8 @@ class RichText { this.initQuillEditor() document.querySelector('.ql-editor').style.minHeight = originHeight + 'px' this.showTextEdit = true - this.focus() + // 如果是刚创建的节点,那么默认全选,否则普通激活不全选 + this.focus(isInserting ? 0 : null) if (!node.nodeData.data.richText) { // 如果是非富文本的情况,需要手动应用文本样式 this.setTextStyleIfNotRichText(node) @@ -250,6 +253,7 @@ class RichText { this.showTextEdit = false this.mindMap.emit('rich_text_selection_change', false) this.node = null + this.isInserting = false } // 初始化Quill富文本编辑器 @@ -271,6 +275,8 @@ class RichText { theme: 'snow' }) this.quill.on('selection-change', range => { + // 刚创建的节点全选不需要显示操作条 + if (this.isInserting) return this.lastRange = this.range this.range = null if (range) { @@ -338,9 +344,9 @@ class RichText { } // 聚焦 - focus() { + focus(start) { let len = this.quill.getLength() - this.quill.setSelection(len, len) + this.quill.setSelection(typeof start === 'number' ? start : len, len) } // 格式化当前选中的文本 diff --git a/simple-mind-map/src/plugins/TouchEvent.js b/simple-mind-map/src/plugins/TouchEvent.js index 1cb7ec70..5afc1f04 100644 --- a/simple-mind-map/src/plugins/TouchEvent.js +++ b/simple-mind-map/src/plugins/TouchEvent.js @@ -86,7 +86,8 @@ class TouchEvent { this.clickNum = 0 this.dispatchMouseEvent('dblclick', ev.target, ev) } else { - this.dispatchMouseEvent('click', ev.target, ev) + // 点击事件应该不用模拟 + // this.dispatchMouseEvent('click', ev.target, ev) } } this.touchesNum = 0 diff --git a/simple-mind-map/src/utils/index.js b/simple-mind-map/src/utils/index.js index b5b2bd28..f84f7fd2 100644 --- a/simple-mind-map/src/utils/index.js +++ b/simple-mind-map/src/utils/index.js @@ -213,6 +213,18 @@ export const imgToDataUrl = src => { }) } +// 解析dataUrl +export const parseDataUrl = data => { + if (!/^data:/.test(data)) return data + let [typeStr, base64] = data.split(',') + let res = /^data:[^/]+\/([^;]+);/.exec(typeStr) + let type = res[1] + return { + type, + base64 + } +} + // 下载文件 export const downloadFile = (file, fileName) => { let a = document.createElement('a') @@ -392,3 +404,23 @@ export const nodeToHTML = node => { nodeToHTMLWrapEl.appendChild(node) return nodeToHTMLWrapEl.innerHTML } + +// 获取图片大小 +export const getImageSize = src => { + return new Promise(resolve => { + let img = new Image() + img.src = src + img.onload = () => { + resolve({ + width: img.width, + height: img.height + }) + } + img.onerror = () => { + resolve({ + width: 0, + height: 0 + }) + } + }) +} diff --git a/web/public/index.html b/web/public/index.html index 294e768a..a5e6d6ba 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -5,7 +5,7 @@ - 一个简单的web思维导图实现 + 思绪思维导图