diff --git a/README.md b/README.md index 8f40f711..dc3cca95 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Github:[releases](https://github.com/wanglin2/mind-map/releases)。百度云 官方提供了如下插件,可根据需求按需引入(某个功能不生效大概率是因为你没有引入对应的插件),具体使用方式请查看文档: -> RichText(节点富文本插件)、Select(鼠标多选节点插件)、Drag(节点拖拽插件)、AssociativeLine(关联线插件)、Export(导出插件)、KeyboardNavigation(键盘导航插件)、MiniMap(小地图插件)、Watermark(水印插件)、TouchEvent(移动端触摸事件支持插件)、NodeImgAdjust(拖拽调整节点图片大小插件)、Search(搜索插件)、Painter(节点格式刷插件)、Scrollbar(滚动条插件)、Formula(数学公式插件)、Cooperate(协同编辑插件)、RainbowLines(彩虹线条插件)、Demonstrate(演示模式插件)、OuterFrame(外框插件)、HandDrawnLikeStyle(手绘风格插件)[收费]、Notation(节点标记插件)[收费]、Numbers(节点编号插件)[收费] +> RichText(节点富文本插件)、Select(鼠标多选节点插件)、Drag(节点拖拽插件)、AssociativeLine(关联线插件)、Export(导出插件)、KeyboardNavigation(键盘导航插件)、MiniMap(小地图插件)、Watermark(水印插件)、TouchEvent(移动端触摸事件支持插件)、NodeImgAdjust(拖拽调整节点图片大小插件)、Search(搜索插件)、Painter(节点格式刷插件)、Scrollbar(滚动条插件)、Formula(数学公式插件)、Cooperate(协同编辑插件)、RainbowLines(彩虹线条插件)、Demonstrate(演示模式插件)、OuterFrame(外框插件)、HandDrawnLikeStyle(手绘风格插件)[收费]、Notation(节点标记插件)[收费]、Numbers(节点编号插件)[收费]、Freemind(Freemind格式导入导出插件)[收费]、Excel(Excel格式导入导出插件)[收费] 本项目不会实现的特性: @@ -457,4 +457,16 @@ const mindMap = new MindMap({ + + + Lawliet + + + + 一叶孤舟 + + + + 晏江 +

diff --git a/copy.js b/copy.js index 913096d8..0c452623 100644 --- a/copy.js +++ b/copy.js @@ -13,4 +13,4 @@ if (fs.existsSync(src)) { fs.unlinkSync(src) } -console.warn('请检查手绘风格、标记插件、编号插件是否启用!!!') \ No newline at end of file +console.warn('请检查付费插件是否启用!!!') \ No newline at end of file diff --git a/simple-mind-map/full.js b/simple-mind-map/full.js index 563a4696..4658f4bb 100644 --- a/simple-mind-map/full.js +++ b/simple-mind-map/full.js @@ -31,7 +31,7 @@ MindMap.iconList = icons.nodeIconList MindMap.constants = constants MindMap.themes = themes MindMap.defaultTheme = defaultTheme -MindMap.version = '0.11.0' +MindMap.version = '0.11.1' MindMap.usePlugin(MiniMap) .usePlugin(Watermark) diff --git a/simple-mind-map/package.json b/simple-mind-map/package.json index 9ca1fba6..2f83a86f 100644 --- a/simple-mind-map/package.json +++ b/simple-mind-map/package.json @@ -1,6 +1,6 @@ { "name": "simple-mind-map", - "version": "0.11.0", + "version": "0.11.1", "description": "一个简单的web在线思维导图", "authors": [ { diff --git a/simple-mind-map/src/constants/constant.js b/simple-mind-map/src/constants/constant.js index 7acd677d..818b2ffe 100644 --- a/simple-mind-map/src/constants/constant.js +++ b/simple-mind-map/src/constants/constant.js @@ -328,7 +328,9 @@ export const nodeDataNoStylePropList = [ 'notation', 'outerFrame', 'number', - 'range' + 'range', + 'customLeft', + 'customTop' ] // 错误类型 diff --git a/simple-mind-map/src/constants/defaultOptions.js b/simple-mind-map/src/constants/defaultOptions.js index 5da7c1c6..f6ed199b 100644 --- a/simple-mind-map/src/constants/defaultOptions.js +++ b/simple-mind-map/src/constants/defaultOptions.js @@ -67,9 +67,7 @@ export const defaultOpt = { close: '' }, // 处理收起节点数量 - expandBtnNumHandler: num => { - return num - }, + expandBtnNumHandler: null, // 是否显示带数量的收起按钮 isShowExpandNum: true, // 是否只有当鼠标在画布内才响应快捷键事件 @@ -176,11 +174,6 @@ export const defaultOpt = { addHistoryTime: 100, // 是否禁止拖动画布 isDisableDrag: false, - // 鼠标移入概要高亮所属节点时的高亮框样式 - highlightNodeBoxStyle: { - stroke: 'rgb(94, 200, 248)', - fill: 'transparent' - }, // 创建新节点时的行为 /* DEFAULT :默认会激活新创建的节点,并且进入编辑模式。如果同时创建了多个新节点,那么只会激活而不会进入编辑模式 @@ -238,6 +231,10 @@ export const defaultOpt = { padding: 100, // 超出画布四周指定范围内依旧渲染节点 removeNodeWhenOutCanvas: true // 节点移除画布可视区域后从画布删除 }, + // 如果节点文本为空,那么为了避免空白节点高度塌陷,会用该字段指定的文本测量一个高度 + emptyTextMeasureHeightText: 'abc123我和你', + // 是否在进行节点文本编辑时实时更新节点大小和节点位置,开启后当节点数量比较多时可能会造成卡顿 + openRealtimeRenderOnNodeTextEdit: false, // 【Select插件】 // 多选节点时鼠标移动到边缘时的画布移动偏移量 @@ -409,5 +406,13 @@ export const defaultOpt = { // 【OuterFrame】插件 outerFramePaddingX: 10, - outerFramePaddingY: 10 + outerFramePaddingY: 10, + + // 【Painter】插件 + // 是否只格式刷节点手动设置的样式,不考虑节点通过主题的应用的样式 + onlyPainterNodeCustomStyles: false, + + // 【NodeImgAdjust】插件 + // 拦截节点图片的删除,点击节点图片上的删除按钮删除图片前会调用该函数,如果函数返回true则取消删除 + beforeDeleteNodeImg: null } diff --git a/simple-mind-map/src/core/render/Render.js b/simple-mind-map/src/core/render/Render.js index ec2bf3b8..dd9dbf9a 100644 --- a/simple-mind-map/src/core/render/Render.js +++ b/simple-mind-map/src/core/render/Render.js @@ -99,6 +99,7 @@ class Render { this.currentBeingPasteType = '' // 节点高亮框 this.highlightBoxNode = null + this.highlightBoxNodeStyle = null // 上一次节点激活数据 this.lastActiveNode = null this.lastActiveNodeList = [] @@ -147,6 +148,19 @@ class Render { }) // 性能模式 this.performanceMode() + // 实时渲染当节点文本编辑时 + if (this.mindMap.opt.openRealtimeRenderOnNodeTextEdit) { + this.mindMap.on('node_text_edit_change', ({ node, text }) => { + node._textData = node.createTextNode(text) + const { width, height } = node.getNodeRect() + node.width = width + node.height = height + node.layout() + this.mindMap.render(() => { + this.textEdit.updateTextEditNode() + }) + }) + } } // 性能模式,懒加载节点 @@ -1593,40 +1607,53 @@ class Render { } // 展开所有 - expandAllNode() { + expandAllNode(uid = '') { if (!this.renderTree) return - walk( - this.renderTree, - null, - node => { - if (!node.data.expand) { - node.data.expand = true - } - }, - null, - true, - 0, - 0 - ) + + const _walk = (node, enableExpand) => { + // 如果该节点为目标节点,那么修改允许展开的标志 + if (!enableExpand && node.data.uid === uid) { + enableExpand = true + } + if (enableExpand && !node.data.expand) { + node.data.expand = true + } + if (node.children && node.children.length > 0) { + node.children.forEach(child => { + _walk(child, enableExpand) + }) + } + } + _walk(this.renderTree, !uid) + this.mindMap.render() } // 收起所有 - unexpandAllNode(isSetRootNodeCenter = true) { + unexpandAllNode(isSetRootNodeCenter = true, uid = '') { if (!this.renderTree) return - walk( - this.renderTree, - null, - (node, parent, isRoot) => { - if (!isRoot && node.children && node.children.length > 0) { - node.data.expand = false - } - }, - null, - true, - 0, - 0 - ) + + const _walk = (node, isRoot, enableUnExpand) => { + // 如果该节点为目标节点,那么修改允许展开的标志 + if (!enableUnExpand && node.data.uid === uid) { + enableUnExpand = true + } + if ( + enableUnExpand && + !isRoot && + node.children && + node.children.length > 0 + ) { + node.data.expand = false + } + if (node.children && node.children.length > 0) { + node.children.forEach(child => { + _walk(child, false, enableUnExpand) + }) + } + } + _walk(this.renderTree, true, !uid) + this.mindMap.render(() => { if (isSetRootNodeCenter) { this.setRootNodeCenter() @@ -2035,19 +2062,39 @@ class Render { } // 高亮节点或子节点 - highlightNode(node, range) { + highlightNode(node, range, style) { // 如果当前正在渲染,那么不进行高亮,因为节点位置可能不正确 if (this.isRendering) return - const { highlightNodeBoxStyle = {} } = this.mindMap.opt + style = { + stroke: 'rgb(94, 200, 248)', + fill: 'transparent', + ...(style || {}) + } + // 尚未创建 if (!this.highlightBoxNode) { this.highlightBoxNode = new Polygon() .stroke({ - color: highlightNodeBoxStyle.stroke || 'transparent' + color: style.stroke || 'transparent' }) .fill({ - color: highlightNodeBoxStyle.fill || 'transparent' + color: style.fill || 'transparent' }) + } else if (this.highlightBoxNodeStyle) { + // 样式更新了 + if ( + this.highlightBoxNodeStyle.stroke !== style.stroke || + this.highlightBoxNodeStyle.fill !== style.fill + ) { + this.highlightBoxNode + .stroke({ + color: style.stroke || 'transparent' + }) + .fill({ + color: style.fill || 'transparent' + }) + } } + this.highlightBoxNodeStyle = { ...style } let minx = Infinity, miny = Infinity, maxx = -Infinity, diff --git a/simple-mind-map/src/core/render/TextEdit.js b/simple-mind-map/src/core/render/TextEdit.js index 0663ed68..6aa334bd 100644 --- a/simple-mind-map/src/core/render/TextEdit.js +++ b/simple-mind-map/src/core/render/TextEdit.js @@ -25,6 +25,8 @@ export default class TextEdit { // 如果编辑过程中缩放画布了,那么缓存当前编辑的内容 this.cacheEditingText = '' this.hasBodyMousedown = false + this.textNodePaddingX = 5 + this.textNodePaddingY = 3 this.bindEvent() } @@ -214,7 +216,7 @@ export default class TextEdit { this.registerTmpShortcut() if (!this.textEditNode) { this.textEditNode = document.createElement('div') - this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;` + this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: ${this.textNodePaddingY}px ${this.textNodePaddingX}px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;` this.textEditNode.setAttribute('contenteditable', true) this.textEditNode.addEventListener('keyup', e => { e.stopPropagation() @@ -240,6 +242,13 @@ export default class TextEdit { handleInputPasteText(e) } }) + this.textEditNode.addEventListener('input', () => { + this.mindMap.emit('node_text_edit_change', { + node: this.currentNode, + text: this.getEditText(), + richText: false + }) + }) const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body targetNode.appendChild(this.textEditNode) @@ -256,8 +265,10 @@ export default class TextEdit { node.style.domText(this.textEditNode, scale, isMultiLine) this.textEditNode.style.zIndex = nodeTextEditZIndex this.textEditNode.innerHTML = textLines.join('
') - this.textEditNode.style.minWidth = rect.width + 10 + 'px' - this.textEditNode.style.minHeight = rect.height + 6 + 'px' + this.textEditNode.style.minWidth = + rect.width + this.textNodePaddingX * 2 + 'px' + this.textEditNode.style.minHeight = + rect.height + this.textNodePaddingY * 2 + 'px' this.textEditNode.style.left = rect.left + 'px' this.textEditNode.style.top = rect.top + 'px' this.textEditNode.style.display = 'block' @@ -280,6 +291,24 @@ export default class TextEdit { this.cacheEditingText = '' } + // 更新文本编辑框的大小和位置 + updateTextEditNode() { + if (this.mindMap.richText) { + this.mindMap.richText.updateTextEditNode() + return + } + if (!this.showTextEdit || !this.currentNode) { + return + } + const rect = this.currentNode._textData.node.node.getBoundingClientRect() + this.textEditNode.style.minWidth = + rect.width + this.textNodePaddingX * 2 + 'px' + this.textEditNode.style.minHeight = + rect.height + this.textNodePaddingY * 2 + 'px' + this.textEditNode.style.left = rect.left + 'px' + this.textEditNode.style.top = rect.top + 'px' + } + // 删除文本编辑元素 removeTextEditEl() { if (this.mindMap.richText) { @@ -313,12 +342,7 @@ export default class TextEdit { } this.mindMap.render() }) - this.mindMap.emit( - 'hide_text_edit', - this.textEditNode, - this.renderer.activeNodeList, - this.currentNode - ) + const currentNode = this.currentNode this.currentNode = null this.textEditNode.style.display = 'none' this.textEditNode.innerHTML = '' @@ -327,6 +351,12 @@ export default class TextEdit { this.textEditNode.style.fontWeight = 'normal' this.textEditNode.style.transform = 'translateY(0)' this.showTextEdit = false + this.mindMap.emit( + 'hide_text_edit', + this.textEditNode, + this.renderer.activeNodeList, + currentNode + ) } // 获取当前正在编辑中的节点实例 diff --git a/simple-mind-map/src/core/render/node/MindMapNode.js b/simple-mind-map/src/core/render/node/MindMapNode.js index 5b3c7174..b1654cd3 100644 --- a/simple-mind-map/src/core/render/node/MindMapNode.js +++ b/simple-mind-map/src/core/render/node/MindMapNode.js @@ -34,6 +34,8 @@ class MindMapNode { this.lineDraw = this.mindMap.lineDraw // 样式实例 this.style = new Style(this) + // 节点当前生效的全部样式 + this.effectiveStyles = {} // 形状实例 this.shapeInstance = new Shape(this) this.shapePadding = { diff --git a/simple-mind-map/src/core/render/node/Style.js b/simple-mind-map/src/core/render/node/Style.js index 1248fbb7..d28d3977 100644 --- a/simple-mind-map/src/core/render/node/Style.js +++ b/simple-mind-map/src/core/render/node/Style.js @@ -64,8 +64,10 @@ class Style { let themeConfig = this.ctx.mindMap.themeConfig // 三级及以下节点 let defaultConfig = themeConfig.node + let useRoot = false if (root || rootProp.includes(prop)) { // 直接使用最外层样式 + useRoot = true defaultConfig = themeConfig } else if (this.ctx.isGeneralization) { // 概要节点 @@ -78,9 +80,16 @@ class Style { defaultConfig = themeConfig.second } // 优先使用节点本身的样式 - return this.getSelfStyle(prop) !== undefined - ? this.getSelfStyle(prop) - : defaultConfig[prop] + const value = + this.getSelfStyle(prop) !== undefined + ? this.getSelfStyle(prop) + : defaultConfig[prop] + if (!useRoot) { + this.addToEffectiveStyles({ + [prop]: value + }) + } + return value } // 获取某个样式值 @@ -93,6 +102,14 @@ class Style { return this.ctx.getData(prop) } + // 更新当前节点生效的样式数据 + addToEffectiveStyles(styles) { + this.ctx.effectiveStyles = { + ...this.ctx.effectiveStyles, + ...styles + } + } + // 矩形 rect(node) { this.shape(node) @@ -101,18 +118,30 @@ class Style { // 形状 shape(node) { - if (this.merge('gradientStyle')) { + const styles = { + gradientStyle: this.merge('gradientStyle'), + startColor: this.merge('startColor'), + endColor: this.merge('endColor'), + startDir: this.merge('startDir'), + endDir: this.merge('endDir'), + fillColor: this.merge('fillColor'), + borderColor: this.merge('borderColor'), + borderWidth: this.merge('borderWidth'), + borderDasharray: this.merge('borderDasharray') + } + if (styles.gradientStyle) { if (!this._gradient) { this._gradient = this.ctx.nodeDraw.gradient('linear') } this._gradient.update(add => { - add.stop(0, this.merge('startColor')) - add.stop(1, this.merge('endColor')) + add.stop(0, styles.startColor) + add.stop(1, styles.endColor) }) + this._gradient.from(...styles.startDir).to(...styles.endDir) node.fill(this._gradient) } else { node.fill({ - color: this.merge('fillColor') + color: styles.fillColor }) } // 节点使用横线样式,不需要渲染非激活状态的边框样式 @@ -125,56 +154,89 @@ class Style { // return // } node.stroke({ - color: this.merge('borderColor'), - width: this.merge('borderWidth'), - dasharray: this.merge('borderDasharray') + color: styles.borderColor, + width: styles.borderWidth, + dasharray: styles.borderDasharray }) } // 文字 text(node) { + const styles = { + color: this.merge('color'), + fontFamily: this.merge('fontFamily'), + fontSize: this.merge('fontSize'), + fontWeight: this.merge('fontWeight'), + fontStyle: this.merge('fontStyle'), + textDecoration: this.merge('textDecoration') + } node .fill({ - color: this.merge('color') + color: styles.color }) .css({ - 'font-family': this.merge('fontFamily'), - 'font-size': this.merge('fontSize'), - 'font-weight': this.merge('fontWeight'), - 'font-style': this.merge('fontStyle'), - 'text-decoration': this.merge('textDecoration') + 'font-family': styles.fontFamily, + 'font-size': styles.fontSize, + 'font-weight': styles.fontWeight, + 'font-style': styles.fontStyle, + 'text-decoration': styles.textDecoration }) } // 生成内联样式 createStyleText() { + const styles = { + color: this.merge('color'), + fontFamily: this.merge('fontFamily'), + fontSize: this.merge('fontSize'), + fontWeight: this.merge('fontWeight'), + fontStyle: this.merge('fontStyle'), + textDecoration: this.merge('textDecoration') + } return ` - color: ${this.merge('color')}; - font-family: ${this.merge('fontFamily')}; - font-size: ${this.merge('fontSize') + 'px'}; - font-weight: ${this.merge('fontWeight')}; - font-style: ${this.merge('fontStyle')}; - text-decoration: ${this.merge('textDecoration')} + color: ${styles.color}; + font-family: ${styles.fontFamily}; + font-size: ${styles.fontSize + 'px'}; + font-weight: ${styles.fontWeight}; + font-style: ${styles.fontStyle}; + text-decoration: ${styles.textDecoration} ` } // 获取文本样式 getTextFontStyle() { - return { - italic: this.merge('fontStyle') === 'italic', - bold: this.merge('fontWeight'), + const styles = { + color: this.merge('color'), + fontFamily: this.merge('fontFamily'), fontSize: this.merge('fontSize'), - fontFamily: this.merge('fontFamily') + fontWeight: this.merge('fontWeight'), + fontStyle: this.merge('fontStyle'), + textDecoration: this.merge('textDecoration') + } + return { + italic: styles.fontStyle === 'italic', + bold: styles.fontWeight, + fontSize: styles.fontSize, + fontFamily: styles.fontFamily } } // html文字节点 domText(node, fontSizeScale = 1, isMultiLine) { - node.style.fontFamily = this.merge('fontFamily') - node.style.fontSize = this.merge('fontSize') * fontSizeScale + 'px' - node.style.fontWeight = this.merge('fontWeight') || 'normal' - node.style.lineHeight = !isMultiLine ? 'normal' : this.merge('lineHeight') - node.style.fontStyle = this.merge('fontStyle') + const styles = { + color: this.merge('color'), + fontFamily: this.merge('fontFamily'), + fontSize: this.merge('fontSize'), + fontWeight: this.merge('fontWeight'), + fontStyle: this.merge('fontStyle'), + textDecoration: this.merge('textDecoration'), + lineHeight: this.merge('lineHeight') + } + node.style.fontFamily = styles.fontFamily + node.style.fontSize = styles.fontSize * fontSizeScale + 'px' + node.style.fontWeight = styles.fontWeight || 'normal' + node.style.lineHeight = !isMultiLine ? 'normal' : styles.lineHeight + node.style.fontStyle = styles.fontStyle } // 标签文字 @@ -286,7 +348,7 @@ class Style { // hover和激活节点 hoverNode(node) { - const { hoverRectColor } = this.ctx.mindMap.opt + const hoverRectColor = this.merge('hoverRectColor') || this.ctx.mindMap.opt.hoverRectColor node.radius(5).fill('none').stroke({ color: hoverRectColor }) diff --git a/simple-mind-map/src/core/render/node/nodeCreateContents.js b/simple-mind-map/src/core/render/node/nodeCreateContents.js index a2c0ec6e..5bc5b2a9 100644 --- a/simple-mind-map/src/core/render/node/nodeCreateContents.js +++ b/simple-mind-map/src/core/render/node/nodeCreateContents.js @@ -114,8 +114,10 @@ function createIconNode() { } // 创建富文本节点 -function createRichTextNode() { - const { textAutoWrapWidth } = this.mindMap.opt +function createRichTextNode(specifyText) { + let text = + typeof specifyText === 'string' ? specifyText : this.getData('text') + const { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt let g = new G() // 重新设置富文本节点内容 let recoverText = false @@ -129,7 +131,6 @@ function createRichTextNode() { recoverText = true } } - let text = this.getData('text') if (recoverText && !isUndef(text)) { // 判断节点内容是否是富文本 let isRichText = checkIsRichText(text) @@ -153,7 +154,7 @@ function createRichTextNode() { text: text }) } - let html = `
${this.getData('text')}
` + let html = `
${text}
` if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) { this.mindMap.commonCaches.measureRichtextNodeTextSizeEl = document.createElement('div') @@ -174,7 +175,7 @@ function createRichTextNode() { let { width, height } = el.getBoundingClientRect() // 如果文本为空,那么需要计算一个默认高度 if (height <= 0) { - div.innerHTML = '

abc123我和你

' + div.innerHTML = `

${emptyTextMeasureHeightText}

` let elTmp = div.children[0] elTmp.classList.add('smm-richtext-node-wrap') height = elTmp.getBoundingClientRect().height @@ -199,10 +200,12 @@ function createRichTextNode() { } // 创建文本节点 -function createTextNode() { +function createTextNode(specifyText) { if (this.getData('richText')) { - return this.createRichTextNode() + return this.createRichTextNode(specifyText) } + const text = + typeof specifyText === 'string' ? specifyText : this.getData('text') if (this.getData('resetRichText')) { delete this.nodeData.data.resetRichText } @@ -212,10 +215,11 @@ function createTextNode() { // 文本超长自动换行 let textStyle = this.style.getTextFontStyle() let textArr = [] - if (!isUndef(this.getData('text'))) { - textArr = String(this.getData('text')).split(/\n/gim) + if (!isUndef(text)) { + textArr = String(text).split(/\n/gim) } - let maxWidth = this.mindMap.opt.textAutoWrapWidth + const { textAutoWrapWidth: maxWidth, emptyTextMeasureHeightText } = + this.mindMap.opt let isMultiLine = false textArr.forEach((item, index) => { let arr = item.split('') @@ -247,6 +251,13 @@ function createTextNode() { g.add(node) }) let { width, height } = g.bbox() + // 如果文本为空,那么需要计算一个默认高度 + if (height <= 0) { + const tmpNode = new Text().text(emptyTextMeasureHeightText) + this.style.text(tmpNode) + const tmpBbox = tmpNode.bbox() + height = tmpBbox.height + } width = Math.min(Math.ceil(width), maxWidth) height = Math.ceil(height) g.attr('data-width', width) diff --git a/simple-mind-map/src/core/render/node/nodeExpandBtn.js b/simple-mind-map/src/core/render/node/nodeExpandBtn.js index 87941fde..baf26b36 100644 --- a/simple-mind-map/src/core/render/node/nodeExpandBtn.js +++ b/simple-mind-map/src/core/render/node/nodeExpandBtn.js @@ -1,5 +1,6 @@ import btnsSvg from '../../../svg/btns' import { SVG, Circle, G, Text } from '@svgdotjs/svg.js' +import { isUndef } from '../../../utils' // 创建展开收起按钮的内容节点 function createExpandNodeContent() { @@ -78,7 +79,12 @@ function updateExpandBtnNode() { }) // 计算子节点数量 let count = this.sumNode(this.nodeData.children) - count = expandBtnNumHandler(count) + if (typeof expandBtnNumHandler === 'function') { + const res = expandBtnNumHandler(count, this) + if (!isUndef(res)) { + count = res + } + } node.text(String(count)) } else { this._fillExpandNode.stroke('none') diff --git a/simple-mind-map/src/core/render/node/nodeGeneralization.js b/simple-mind-map/src/core/render/node/nodeGeneralization.js index 34e6908e..3bc735d5 100644 --- a/simple-mind-map/src/core/render/node/nodeGeneralization.js +++ b/simple-mind-map/src/core/render/node/nodeGeneralization.js @@ -186,15 +186,29 @@ function handleGeneralizationMouseenter() { const list = belongNode.formatGetGeneralization() const index = belongNode.getGeneralizationNodeIndex(this) const generalizationData = list[index] + // 如果主题中设置了hoverRectColor颜色,那么使用该颜色 + // 否则使用hoverRectColor实例化选项的颜色 + // 兜底使用highlightNode方法的默认颜色 + const hoverRectColor = this.getStyle('hoverRectColor') + const color = hoverRectColor || this.mindMap.opt.hoverRectColor + const style = color + ? { + stroke: color + } + : null // 区间概要,框子节点 if ( Array.isArray(generalizationData.range) && generalizationData.range.length > 0 ) { - this.mindMap.renderer.highlightNode(belongNode, generalizationData.range) + this.mindMap.renderer.highlightNode( + belongNode, + generalizationData.range, + style + ) } else { // 否则框自己 - this.mindMap.renderer.highlightNode(belongNode) + this.mindMap.renderer.highlightNode(belongNode, null, style) } } diff --git a/simple-mind-map/src/layouts/Base.js b/simple-mind-map/src/layouts/Base.js index 63eb4e48..6e2084dc 100644 --- a/simple-mind-map/src/layouts/Base.js +++ b/simple-mind-map/src/layouts/Base.js @@ -128,8 +128,9 @@ class Base { ) } // 主题或主题配置改变了、节点层级改变了,需要重新渲染节点文本等情况需要重新计算节点大小和布局 + const isNeedResizeSources = this.checkIsNeedResizeSources() if ( - this.checkIsNeedResizeSources() || + isNeedResizeSources || isLayerTypeChange || newNode.getData('resetRichText') || isNumberChange @@ -137,7 +138,7 @@ class Base { newNode.getSize() newNode.needLayout = true } - this.checkGetGeneralizationChange(newNode) + this.checkGetGeneralizationChange(newNode, isNeedResizeSources) } else if ( (this.lru.has(uid) || this.renderer.lastNodeCache[uid]) && !this.renderer.reRender @@ -186,7 +187,7 @@ class Base { newNode.getSize() newNode.needLayout = true } - this.checkGetGeneralizationChange(newNode) + this.checkGetGeneralizationChange(newNode, isResizeSource) } else { // 创建新节点 const newUid = uid || createUid() @@ -228,7 +229,7 @@ class Base { } // 检查概要节点是否需要更新 - checkGetGeneralizationChange(node) { + checkGetGeneralizationChange(node, isResizeSource) { const generalizationList = node.getData('generalization') if ( generalizationList && @@ -239,7 +240,10 @@ class Base { const gNode = item.generalizationNode const oldData = gNode.getData() const newData = generalizationList[index] - if (newData && JSON.stringify(oldData) !== JSON.stringify(newData)) { + if ( + isResizeSource || + (newData && JSON.stringify(oldData) !== JSON.stringify(newData)) + ) { gNode.nodeData.data = newData gNode.getSize() gNode.needLayout = true @@ -371,18 +375,32 @@ class Base { } // 二次贝塞尔曲线 - quadraticCurvePath(x1, y1, x2, y2) { - let cx = x1 + (x2 - x1) * 0.2 - let cy = y1 + (y2 - y1) * 0.8 + quadraticCurvePath(x1, y1, x2, y2, v = false) { + let cx, cy + if (v) { + cx = x1 + (x2 - x1) * 0.8 + cy = y1 + (y2 - y1) * 0.2 + } else { + cx = x1 + (x2 - x1) * 0.2 + cy = y1 + (y2 - y1) * 0.8 + } return `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}` } // 三次贝塞尔曲线 - cubicBezierPath(x1, y1, x2, y2) { - let cx1 = x1 + (x2 - x1) / 2 - let cy1 = y1 - let cx2 = cx1 - let cy2 = y2 + cubicBezierPath(x1, y1, x2, y2, v = false) { + let cx1, cy1, cx2, cy2 + if (v) { + cx1 = x1 + cy1 = y1 + (y2 - y1) / 2 + cx2 = x2 + cy2 = cy1 + } else { + cx1 = x1 + (x2 - x1) / 2 + cy1 = y1 + cx2 = cx1 + cy2 = y2 + } return `M ${x1},${y1} C ${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}` } diff --git a/simple-mind-map/src/layouts/OrganizationStructure.js b/simple-mind-map/src/layouts/OrganizationStructure.js index 475bf7c6..ff5d0730 100644 --- a/simple-mind-map/src/layouts/OrganizationStructure.js +++ b/simple-mind-map/src/layouts/OrganizationStructure.js @@ -34,7 +34,14 @@ class OrganizationStructure extends Base { this.renderer.renderTree, null, (cur, parent, isRoot, layerIndex, index, ancestors) => { - let newNode = this.createNode(cur, parent, isRoot, layerIndex, index, ancestors) + let newNode = this.createNode( + cur, + parent, + isRoot, + layerIndex, + index, + ancestors + ) // 根节点定位在画布中心位置 if (isRoot) { this.setNodeCenter(newNode) @@ -148,13 +155,56 @@ class OrganizationStructure extends Base { // 绘制连线,连接该节点到其子节点 renderLine(node, lines, style, lineStyle) { - if (lineStyle === 'direct') { + if (lineStyle === 'curve') { + this.renderLineCurve(node, lines, style) + } else if (lineStyle === 'direct') { this.renderLineDirect(node, lines, style) } else { this.renderLineStraight(node, lines, style) } } + // 曲线风格连线 + renderLineCurve(node, lines, style) { + if (node.children.length <= 0) { + return [] + } + let { left, top, width, height, expandBtnSize } = node + const { alwaysShowExpandBtn, notShowExpandBtn } = this.mindMap.opt + if (!alwaysShowExpandBtn || notShowExpandBtn) { + expandBtnSize = 0 + } + const { + nodeUseLineStyle, + rootLineStartPositionKeepSameInCurve, + rootLineKeepSameInCurve + } = this.mindMap.themeConfig + node.children.forEach((item, index) => { + if (node.layerIndex === 0) { + expandBtnSize = 0 + } + let x1 = left + width / 2 + let y1 = + node.layerIndex === 0 && !rootLineStartPositionKeepSameInCurve + ? top + height / 2 + : top + height + expandBtnSize + let x2 = item.left + item.width / 2 + let y2 = item.top + let path = '' + // 节点使用横线风格,需要额外渲染横线 + let nodeUseLineStylePath = nodeUseLineStyle + ? ` L ${item.left},${y2} L ${item.left + item.width},${y2}` + : '' + if (node.isRoot && !rootLineKeepSameInCurve) { + path = + this.quadraticCurvePath(x1, y1, x2, y2, true) + nodeUseLineStylePath + } else { + path = this.cubicBezierPath(x1, y1, x2, y2, true) + nodeUseLineStylePath + } + this.setLineStyle(style, lines[index], path, item) + }) + } + // 直连风格 renderLineDirect(node, lines, style) { if (node.children.length <= 0) { diff --git a/simple-mind-map/src/plugins/Formula.js b/simple-mind-map/src/plugins/Formula.js index b28926a8..af7e49fb 100644 --- a/simple-mind-map/src/plugins/Formula.js +++ b/simple-mind-map/src/plugins/Formula.js @@ -1,6 +1,6 @@ import katex from 'katex' import Quill from 'quill' -import { getChromeVersion } from '../utils/index' +import { getChromeVersion, htmlEscape } from '../utils/index' import { getBaseStyleText, getFontStyleText } from './FormulaStyle' // 数学公式支持插件 @@ -58,7 +58,7 @@ class Formula { let node = super.create(value) if (typeof value === 'string') { katex.render(value, node, self.config) - node.setAttribute('data-value', value) + node.setAttribute('data-value', htmlEscape(value)) } return node } @@ -110,11 +110,7 @@ class Formula { for (const el of els) nodeText = nodeText.replace( el.outerHTML, - `\$${el - .getAttribute('data-value') - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>')}\$` + `\$${el.getAttribute('data-value')}\$` ) } return nodeText diff --git a/simple-mind-map/src/plugins/NodeImgAdjust.js b/simple-mind-map/src/plugins/NodeImgAdjust.js index c21987d1..fd7dbb2c 100644 --- a/simple-mind-map/src/plugins/NodeImgAdjust.js +++ b/simple-mind-map/src/plugins/NodeImgAdjust.js @@ -192,8 +192,14 @@ class NodeImgAdjust { if (this.isMousedown) return this.hideHandleEl() }) - btnRemove.addEventListener('click', e => { - this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null }) + btnRemove.addEventListener('click', async e => { + let stop = false + if (typeof this.mindMap.opt.beforeDeleteNodeImg === 'function') { + stop = await this.mindMap.opt.beforeDeleteNodeImg(this.node) + } + if (!stop) { + this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null }) + } }) // 添加元素到页面 const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body diff --git a/simple-mind-map/src/plugins/Painter.js b/simple-mind-map/src/plugins/Painter.js index 8dcf90e8..3b7be755 100644 --- a/simple-mind-map/src/plugins/Painter.js +++ b/simple-mind-map/src/plugins/Painter.js @@ -53,17 +53,22 @@ class Painter { node.uid === this.painterNode.uid ) return - const style = {} + let style = {} + // 格式刷节点所有生效的样式 + if (!this.mindMap.opt.onlyPainterNodeCustomStyles) { + style = { + ...this.painterNode.effectiveStyles + } + } const painterNodeData = this.painterNode.getData() Object.keys(painterNodeData).forEach(key => { if (checkIsNodeStyleDataKey(key)) { style[key] = painterNodeData[key] } }) + // 先去除目标节点的样式 + this.mindMap.renderer._handleRemoveCustomStyles(node.getData()) node.setStyles(style) - if (painterNodeData.activeStyle) { - node.setStyles(painterNodeData.activeStyle, true) - } } // 插件被移除前做的事情 diff --git a/simple-mind-map/src/plugins/RichText.js b/simple-mind-map/src/plugins/RichText.js index 2388c33f..e32c96cc 100644 --- a/simple-mind-map/src/plugins/RichText.js +++ b/simple-mind-map/src/plugins/RichText.js @@ -57,6 +57,8 @@ class RichText { this.cacheEditingText = '' this.lostStyle = false this.isCompositing = false + this.textNodePaddingX = 6 + this.textNodePaddingY = 4 this.initOpt() this.extendQuill() this.appendCss() @@ -71,14 +73,17 @@ class RichText { // 绑定事件 bindEvent() { this.onCompositionStart = this.onCompositionStart.bind(this) + this.onCompositionUpdate = this.onCompositionUpdate.bind(this) this.onCompositionEnd = this.onCompositionEnd.bind(this) window.addEventListener('compositionstart', this.onCompositionStart) + window.addEventListener('compositionupdate', this.onCompositionUpdate) window.addEventListener('compositionend', this.onCompositionEnd) } // 解绑事件 unbindEvent() { window.removeEventListener('compositionstart', this.onCompositionStart) + window.removeEventListener('compositionupdate', this.onCompositionUpdate) window.removeEventListener('compositionend', this.onCompositionEnd) } @@ -198,8 +203,8 @@ class RichText { let scaleX = rect.width / originWidth let scaleY = rect.height / originHeight // 内边距 - let paddingX = 6 - let paddingY = 4 + let paddingX = this.textNodePaddingX + let paddingY = this.textNodePaddingY if (richTextEditFakeInPlace) { let paddingValue = node.getPaddingVale() paddingX = paddingValue.paddingX @@ -287,6 +292,20 @@ class RichText { this.cacheEditingText = '' } + // 更新文本编辑框的大小和位置 + updateTextEditNode() { + if (!this.node) return + const rect = this.node._textData.node.node.getBoundingClientRect() + const g = this.node._textData.node + const originWidth = g.attr('data-width') + const originHeight = g.attr('data-height') + this.textEditNode.style.minWidth = + originWidth + this.textNodePaddingX * 2 + 'px' + this.textEditNode.style.minHeight = originHeight + 'px' + this.textEditNode.style.left = rect.left + 'px' + this.textEditNode.style.top = rect.top + 'px' + } + // 删除文本编辑框元素 removeTextEditEl() { if (!this.textEditNode) return @@ -340,6 +359,18 @@ class RichText { return html.replace(/


<\/p>$/, '') } + // 给html字符串中的节点样式按样式名首字母排序 + sortHtmlNodeStyles(html) { + return html.replace(/(<[^<>]+\s+style=")([^"]+)("\s*>)/g, (_, a, b, c) => { + let arr = b.match(/[^:]+:[^:]+;/g) || [] + arr = arr.map(item => { + return item.trim() + }) + arr.sort() + return a + arr.join('') + c + }) + } + // 隐藏文本编辑控件,即完成编辑 hideEditText(nodes) { if (!this.showTextEdit) { @@ -350,6 +381,7 @@ class RichText { beforeHideRichTextEdit(this) } let html = this.getEditText() + html = this.sortHtmlNodeStyles(html) let list = nodes && nodes.length > 0 ? nodes : this.mindMap.renderer.activeNodeList list.forEach(node => { @@ -360,12 +392,13 @@ class RichText { // } this.mindMap.render() }) - this.mindMap.emit('hide_text_edit', this.textEditNode, list, this.node) + const node = this.node this.textEditNode.style.display = 'none' this.showTextEdit = false this.mindMap.emit('rich_text_selection_change', false) this.node = null this.isInserting = false + this.mindMap.emit('hide_text_edit', this.textEditNode, list, node) } // 初始化Quill富文本编辑器 @@ -490,6 +523,11 @@ class RichText { this.setTextStyleIfNotRichText(this.node) this.lostStyle = false } + this.mindMap.emit('node_text_edit_change', { + node: this.node, + text: this.getEditText(), + richText: true + }) }) // 拦截粘贴,只允许粘贴纯文本 // this.quill.clipboard.addMatcher(Node.TEXT_NODE, node => { @@ -544,6 +582,16 @@ class RichText { this.isCompositing = true } + // 中文输入中 + onCompositionUpdate() { + if (!this.showTextEdit || !this.node) return + this.mindMap.emit('node_text_edit_change', { + node: this.node, + text: this.getEditText(), + richText: true + }) + } + // 中文输入结束 onCompositionEnd() { if (!this.showTextEdit) { diff --git a/simple-mind-map/src/themes/default.js b/simple-mind-map/src/themes/default.js index d0baa737..fb2a6b8e 100644 --- a/simple-mind-map/src/themes/default.js +++ b/simple-mind-map/src/themes/default.js @@ -82,8 +82,12 @@ export default { gradientStyle: false, startColor: '#549688', endColor: '#fff', + startDir: [0, 0], + endDir: [1, 0], // 连线标记的位置,start(头部)、end(尾部),该配置在showLineMarker配置为true时生效 - lineMarkerDir: 'end' + lineMarkerDir: 'end', + // 节点鼠标hover和激活时显示的矩形边框的颜色,主题里不设置,默认会取hoverRectColor实例化选项的值 + hoverRectColor: '' }, // 二级节点样式 second: { @@ -94,7 +98,7 @@ export default { fontFamily: '微软雅黑, Microsoft YaHei', color: '#565656', fontSize: 16, - fontWeight: 'noraml', + fontWeight: 'normal', fontStyle: 'normal', lineHeight: 1.5, borderColor: '#549688', @@ -105,7 +109,10 @@ export default { gradientStyle: false, startColor: '#549688', endColor: '#fff', - lineMarkerDir: 'end' + startDir: [0, 0], + endDir: [1, 0], + lineMarkerDir: 'end', + hoverRectColor: '' }, // 三级及以下节点样式 node: { @@ -116,7 +123,7 @@ export default { fontFamily: '微软雅黑, Microsoft YaHei', color: '#6a6d6c', fontSize: 14, - fontWeight: 'noraml', + fontWeight: 'normal', fontStyle: 'normal', lineHeight: 1.5, borderColor: 'transparent', @@ -127,7 +134,10 @@ export default { gradientStyle: false, startColor: '#549688', endColor: '#fff', - lineMarkerDir: 'end' + startDir: [0, 0], + endDir: [1, 0], + lineMarkerDir: 'end', + hoverRectColor: '' }, // 概要节点样式 generalization: { @@ -138,7 +148,7 @@ export default { fontFamily: '微软雅黑, Microsoft YaHei', color: '#565656', fontSize: 16, - fontWeight: 'noraml', + fontWeight: 'normal', fontStyle: 'normal', lineHeight: 1.5, borderColor: '#549688', @@ -148,7 +158,10 @@ export default { textDecoration: 'none', gradientStyle: false, startColor: '#549688', - endColor: '#fff' + endColor: '#fff', + startDir: [0, 0], + endDir: [1, 0], + hoverRectColor: '' } } @@ -179,7 +192,10 @@ const nodeSizeIndependenceList = [ 'gradientStyle', 'lineRadius', 'startColor', - 'endColor' + 'endColor', + 'startDir', + 'endDir', + 'hoverRectColor' ] export const checkIsNodeSizeIndependenceConfig = config => { let keys = Object.keys(config) diff --git a/simple-mind-map/src/utils/index.js b/simple-mind-map/src/utils/index.js index 5f0cbf8c..132ca77c 100644 --- a/simple-mind-map/src/utils/index.js +++ b/simple-mind-map/src/utils/index.js @@ -1184,9 +1184,18 @@ export const handleInputPasteText = (e, text) => { // 去除格式 text = getTextFromHtml(text) // 去除换行 - text = text.replaceAll(/\n/g, '') - const node = document.createTextNode(text) - selection.getRangeAt(0).insertNode(node) + // text = text.replaceAll(/\n/g, '') + const textArr = text.split(/\n/g) + const fragment = document.createDocumentFragment() + textArr.forEach((item, index) => { + const node = document.createTextNode(item) + fragment.appendChild(node) + if (index < textArr.length - 1) { + const br = document.createElement('br') + fragment.appendChild(br) + } + }) + selection.getRangeAt(0).insertNode(fragment) selection.collapseToEnd() } diff --git a/web/scripts/transformMdToVue.js b/web/scripts/transformMdToVue.js deleted file mode 100644 index cb36f3ee..00000000 --- a/web/scripts/transformMdToVue.js +++ /dev/null @@ -1,33 +0,0 @@ -const path = require('path') -const fs = require('fs') -const hljs = require('highlight.js') -const md = require('markdown-it')({ - html: true, - xhtmlOut: true, - highlight: function(str, lang) { - if (lang && hljs.getLanguage(lang)) { - try { - return ( - '

' +
-          hljs.highlight(str, {
-            language: lang, 
-            ignoreIllegals: true
-          }).value +
-          '
' - ) - } catch (__) {} - } - - return ( - '
' + md.utils.escapeHtml(str) + '
' - ) - } -}).use(require('markdown-it-checkbox')) - -const templatePath = path.join(__dirname, '../src/pages/Doc/Template.vue') - -exports.transformMdToVue = (content) => { - let result = md.render(content) - let template = fs.readFileSync(templatePath, 'utf-8') - return template.replace('$$$$', result) -} \ No newline at end of file diff --git a/web/src/assets/avatar/Lawliet.jpg b/web/src/assets/avatar/Lawliet.jpg new file mode 100644 index 00000000..4d8260d3 Binary files /dev/null and b/web/src/assets/avatar/Lawliet.jpg differ diff --git a/web/src/assets/avatar/一叶孤舟.jpg b/web/src/assets/avatar/一叶孤舟.jpg new file mode 100644 index 00000000..68c32e89 Binary files /dev/null and b/web/src/assets/avatar/一叶孤舟.jpg differ diff --git a/web/src/assets/icon-font/iconfont.css b/web/src/assets/icon-font/iconfont.css index dadf7589..ff910e7e 100644 --- a/web/src/assets/icon-font/iconfont.css +++ b/web/src/assets/icon-font/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 2479351 */ - src: url('iconfont.woff2?t=1719815803051') format('woff2'), - url('iconfont.woff?t=1719815803051') format('woff'), - url('iconfont.ttf?t=1719815803051') format('truetype'); + src: url('iconfont.woff2?t=1726022313538') format('woff2'), + url('iconfont.woff?t=1726022313538') format('woff'), + url('iconfont.ttf?t=1726022313538') format('truetype'); } .iconfont { @@ -13,6 +13,14 @@ -moz-osx-font-smoothing: grayscale; } +.iconfile-excel:before { + content: "\e7b7"; +} + +.iconfreemind:before { + content: "\e97d"; +} + .iconwaikuang:before { content: "\e640"; } diff --git a/web/src/assets/icon-font/iconfont.ttf b/web/src/assets/icon-font/iconfont.ttf index 9b4dcc24..be64d2e5 100644 Binary files a/web/src/assets/icon-font/iconfont.ttf and b/web/src/assets/icon-font/iconfont.ttf differ diff --git a/web/src/assets/icon-font/iconfont.woff b/web/src/assets/icon-font/iconfont.woff index ae10beee..c7c1342e 100644 Binary files a/web/src/assets/icon-font/iconfont.woff and b/web/src/assets/icon-font/iconfont.woff differ diff --git a/web/src/assets/icon-font/iconfont.woff2 b/web/src/assets/icon-font/iconfont.woff2 index a21e2a92..d711ab4d 100644 Binary files a/web/src/assets/icon-font/iconfont.woff2 and b/web/src/assets/icon-font/iconfont.woff2 differ diff --git a/web/src/config/constant.js b/web/src/config/constant.js index 3189e33a..d85420f7 100644 --- a/web/src/config/constant.js +++ b/web/src/config/constant.js @@ -85,12 +85,14 @@ export const formulaList = [ '\\begin{cases}3x + 5y + z \\\\7x - 2y + 4z \\\\-6x + 3y + 2z\\end{cases}' ] +// 支持某种连线类型的结构 export const supportLineStyleLayoutsMap = { curve: [ 'logicalStructure', 'logicalStructureLeft', 'mindMap', - 'verticalTimeline' + 'verticalTimeline', + 'organizationStructure' ], direct: [ 'logicalStructure', @@ -101,6 +103,7 @@ export const supportLineStyleLayoutsMap = { ] } +// 直线模式支持设置圆角的结构 export const supportLineRadiusLayouts = [ 'logicalStructure', 'logicalStructureLeft', @@ -108,6 +111,7 @@ export const supportLineRadiusLayouts = [ 'verticalTimeline' ] +// 支持只显示底边直线风格的结构 export const supportNodeUseLineStyleLayouts = [ 'logicalStructure', 'logicalStructureLeft', @@ -116,10 +120,12 @@ export const supportNodeUseLineStyleLayouts = [ 'organizationStructure' ] +// 支持曲线模式下,根节点样式和其他节点样式保持一致的结构 export const supportRootLineKeepSameInCurveLayouts = [ 'logicalStructure', 'logicalStructureLeft', - 'mindMap' + 'mindMap', + 'organizationStructure' ] // 彩虹线条配置 diff --git a/web/src/config/en.js b/web/src/config/en.js index 5a5f0968..d3e12f6b 100644 --- a/web/src/config/en.js +++ b/web/src/config/en.js @@ -495,6 +495,18 @@ export const downTypeList = [ type: 'txt', icon: 'iconTXT', desc: 'Plain text file' + }, + { + name: 'FreeMind', + type: 'mm', + icon: 'iconfreemind', + desc: 'FreeMind software format' + }, + { + name: 'Excel', + type: 'xlsx', + icon: 'iconfile-excel', + desc: 'Excel software format' } ] @@ -557,3 +569,55 @@ export const numberLevelList = [ value: 0 } ] + +// 背景渐变方向 +export const linearGradientDirList = [ + { + name: 'Left to right', + value: '1', + start: [0, 0], + end: [1, 0] + }, + { + name: 'Right to left', + value: '2', + start: [1, 0], + end: [0, 0] + }, + { + name: 'Top to bottom', + value: '3', + start: [0, 0], + end: [0, 1] + }, + { + name: 'Bottom to top', + value: '4', + start: [0, 1], + end: [0, 0] + }, + { + name: 'Left top to right bottom', + value: '5', + start: [0, 0], + end: [1, 1] + }, + { + name: 'Left bottom to right top', + value: '6', + start: [0, 1], + end: [1, 0] + }, + { + name: 'Right top to left bottom', + value: '7', + start: [1, 0], + end: [0, 1] + }, + { + name: 'Right bottom to left top', + value: '8', + start: [1, 1], + end: [0, 0] + } +] diff --git a/web/src/config/index.js b/web/src/config/index.js index 32affc86..b8ceff17 100644 --- a/web/src/config/index.js +++ b/web/src/config/index.js @@ -21,7 +21,8 @@ import { shapeListMap as shapeListMapZh, lineStyleMap as lineStyleMapZh, numberTypeList as numberTypeListZh, - numberLevelList as numberLevelListZh + numberLevelList as numberLevelListZh, + linearGradientDirList as linearGradientDirListZh } from './zh' import { fontFamilyList as fontFamilyListEn, @@ -36,7 +37,8 @@ import { backgroundSizeList as backgroundSizeListEn, downTypeList as downTypeListEn, numberTypeList as numberTypeListEn, - numberLevelList as numberLevelListEn + numberLevelList as numberLevelListEn, + linearGradientDirList as linearGradientDirListEn } from './en' const fontFamilyList = { @@ -114,6 +116,11 @@ const numberLevelList = { en: numberLevelListEn } +const linearGradientDirList = { + zh: linearGradientDirListZh, + en: linearGradientDirListEn +} + export { fontSizeList, lineHeightList, @@ -137,5 +144,6 @@ export { sidebarTriggerList, downTypeList, numberTypeList, - numberLevelList + numberLevelList, + linearGradientDirList } diff --git a/web/src/config/zh.js b/web/src/config/zh.js index 704f2611..bfddedfc 100644 --- a/web/src/config/zh.js +++ b/web/src/config/zh.js @@ -593,6 +593,18 @@ export const downTypeList = [ type: 'txt', icon: 'iconTXT', desc: '纯文本文件' + }, + { + name: 'FreeMind', + type: 'mm', + icon: 'iconfreemind', + desc: 'FreeMind软件格式' + }, + { + name: 'Excel', + type: 'xlsx', + icon: 'iconfile-excel', + desc: 'Excel软件格式' } ] @@ -655,3 +667,55 @@ export const numberLevelList = [ value: 0 } ] + +// 背景渐变方向 +export const linearGradientDirList = [ + { + name: '从左到右', + value: '1', + start: [0, 0], + end: [1, 0] + }, + { + name: '从右到左', + value: '2', + start: [1, 0], + end: [0, 0] + }, + { + name: '从上到下', + value: '3', + start: [0, 0], + end: [0, 1] + }, + { + name: '从下到上', + value: '4', + start: [0, 1], + end: [0, 0] + }, + { + name: '从左上到右下', + value: '5', + start: [0, 0], + end: [1, 1] + }, + { + name: '从左下到右上', + value: '6', + start: [0, 1], + end: [1, 0] + }, + { + name: '从右上到左下', + value: '7', + start: [1, 0], + end: [0, 1] + }, + { + name: '从右下到左上', + value: '8', + start: [1, 1], + end: [0, 0] + } +] diff --git a/web/src/lang/en_us.js b/web/src/lang/en_us.js index d313a98e..930081a4 100644 --- a/web/src/lang/en_us.js +++ b/web/src/lang/en_us.js @@ -157,8 +157,9 @@ export default { import: { title: 'Import', selectFile: 'Select file', - supportFile: 'Support .smm、.json、.xmind、.xlsx、.md file', - enableFileTip: 'Please select .smm、.json、.xmind、.xlsx、.md file', + support: 'Support', + file: 'file', + pleaseSelect: 'Please select', maxFileNum: 'At most one file can be selected', notSelectTip: 'Please select the file to import', fileContentError: 'The file content is incorrect', @@ -238,7 +239,8 @@ export default { endColor: 'End', arrowDir: 'Arrow dir', arrowDirStart: 'Start', - arrowDirEnd: 'End' + arrowDirEnd: 'End', + direction: 'Direction' }, theme: { title: 'Theme', @@ -305,7 +307,8 @@ export default { yes: 'Yes', no: 'No', exportError: 'Export failed', - dragTip: 'Release here to import the file' + dragTip: 'Release here to import the file', + deleteNodeImgTip: 'Are you sure to delete the node image?' }, mouseAction: { tip1: diff --git a/web/src/lang/zh_cn.js b/web/src/lang/zh_cn.js index cb351c11..dda55c7d 100644 --- a/web/src/lang/zh_cn.js +++ b/web/src/lang/zh_cn.js @@ -155,8 +155,9 @@ export default { import: { title: '导入', selectFile: '选取文件', - supportFile: '支持.smm、.json、.xmind、.xlsx、.md文件', - enableFileTip: '请选择.smm、.json、.xmind、.xlsx、.md文件', + support: '支持', + file: '文件', + pleaseSelect: '请选择', maxFileNum: '最多只能选择一个文件', notSelectTip: '请选择要导入的文件', fileContentError: '文件内容有误', @@ -236,7 +237,8 @@ export default { endColor: '结束', arrowDir: '箭头位置', arrowDirStart: '头部', - arrowDirEnd: '尾部' + arrowDirEnd: '尾部', + direction: '方向' }, theme: { title: '主题', @@ -299,7 +301,8 @@ export default { yes: '是', no: '否', exportError: '导出失败', - dragTip: '在此释放以导入该文件' + dragTip: '在此释放以导入该文件', + deleteNodeImgTip: '是否确认删除该节点图片?' }, mouseAction: { tip1: '当前:左键拖动画布,右键框选节点', diff --git a/web/src/pages/Edit/components/Edit.vue b/web/src/pages/Edit/components/Edit.vue index ef9d9ec0..3f2f2244 100644 --- a/web/src/pages/Edit/components/Edit.vue +++ b/web/src/pages/Edit/components/Edit.vue @@ -79,6 +79,11 @@ import OuterFrame from 'simple-mind-map/src/plugins/OuterFrame.js' // import Notation from 'simple-mind-map-plugin-notation' // 编号插件,该插件为付费插件,详情请查看开发文档 // import Numbers from 'simple-mind-map-plugin-numbers' +// Freemind软件格式导入导出插件,该插件为付费插件,详情请查看开发文档 +// import Freemind from 'simple-mind-map-plugin-freemind' +// Excel软件格式导入导出插件,该插件为付费插件,详情请查看开发文档 +// import Excel from 'simple-mind-map-plugin-excel' +// npm link simple-mind-map-plugin-excel simple-mind-map-plugin-freemind simple-mind-map-plugin-numbers simple-mind-map-plugin-notation simple-mind-map-plugin-handdrawnlikestyle simple-mind-map import OutlineSidebar from './OutlineSidebar' import Style from './Style' import BaseStyle from './BaseStyle' @@ -418,6 +423,25 @@ export default { }, expandBtnNumHandler: num => { return num >= 100 ? '…' : num + }, + beforeDeleteNodeImg: node => { + return new Promise(resolve => { + this.$confirm( + this.$t('edit.deleteNodeImgTip'), + this.$t('edit.tip'), + { + confirmButtonText: this.$t('edit.yes'), + cancelButtonText: this.$t('edit.no'), + type: 'warning' + } + ) + .then(() => { + resolve(false) + }) + .catch(() => { + resolve(true) + }) + }) } // createNodePrefixContent: (node) => { // const el = document.createElement('div') @@ -544,6 +568,16 @@ export default { this.mindMap.addPlugin(Numbers) this.$store.commit('setSupportNumbers', true) } + if (typeof Freemind !== 'undefined') { + this.mindMap.addPlugin(Freemind) + this.$store.commit('setSupportFreemind', true) + Vue.prototype.Freemind = Freemind + } + if (typeof Excel !== 'undefined') { + this.mindMap.addPlugin(Excel) + this.$store.commit('setSupportExcel', true) + Vue.prototype.Excel = Excel + } this.mindMap.keyCommand.addShortcut('Control+s', () => { this.manualSave() }) diff --git a/web/src/pages/Edit/components/Export.vue b/web/src/pages/Edit/components/Export.vue index aa96e22c..f7994256 100644 --- a/web/src/pages/Edit/components/Export.vue +++ b/web/src/pages/Edit/components/Export.vue @@ -8,7 +8,7 @@ element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.8)" :width="isMobile ? '90%' : '50%'" - :top="isMobile? '20px' : '15vh'" + :top="isMobile ? '20px' : '15vh'" >
@@ -98,12 +98,14 @@ import { mapState, mapMutations } from 'vuex' import { downTypeList } from '@/config' import { isMobile } from 'simple-mind-map/src/utils/index' +import MarkdownIt from 'markdown-it' /** * @Author: 王林 * @Date: 2021-06-24 22:53:54 * @Desc: 导出 */ +let md = null export default { name: 'Export', data() { @@ -124,11 +126,23 @@ export default { computed: { ...mapState({ openNodeRichText: state => state.localConfig.openNodeRichText, - isDark: state => state.localConfig.isDark + isDark: state => state.localConfig.isDark, + supportFreemind: state => state.supportFreemind, + supportExcel: state => state.supportExcel }), downTypeList() { - return downTypeList[this.$i18n.locale] || downTypeList.zh + const list = downTypeList[this.$i18n.locale] || downTypeList.zh + return list.filter(item => { + if (item.type === 'mm') { + return this.supportFreemind + } + if (item.type === 'xlsx') { + return this.supportExcel + } else { + return true + } + }) } }, created() { @@ -203,6 +217,22 @@ export default { this.fileName, this.isTransparent ) + } else if (this.exportType === 'mm') { + this.$bus.$emit('export', this.exportType, true, this.fileName, { + transformNote: note => { + if (!md) { + md = new MarkdownIt() + } + return md.render(note) + }, + transformImage: img => { + if (/^https?:\/\//.test(img)) { + return img + } else { + return '' + } + } + }) } else { this.$bus.$emit('export', this.exportType, true, this.fileName) } diff --git a/web/src/pages/Edit/components/Import.vue b/web/src/pages/Edit/components/Import.vue index a19f76ab..de5dfe19 100644 --- a/web/src/pages/Edit/components/Import.vue +++ b/web/src/pages/Edit/components/Import.vue @@ -9,7 +9,7 @@
- {{ $t('import.supportFile') }} + {{ $t('import.support') }}{{ supportFileStr }}{{ $t('import.file') }}
@@ -40,9 +40,12 @@ :show-close="false" > - {{ - item.title - }} + {{ item.title }} {{ @@ -56,9 +59,8 @@