diff --git a/simple-mind-map/src/constants/constant.js b/simple-mind-map/src/constants/constant.js index 818b2ffe..e3aab596 100644 --- a/simple-mind-map/src/constants/constant.js +++ b/simple-mind-map/src/constants/constant.js @@ -330,7 +330,8 @@ export const nodeDataNoStylePropList = [ 'number', 'range', 'customLeft', - 'customTop' + 'customTop', + 'customTextWidth' ] // 错误类型 diff --git a/simple-mind-map/src/constants/defaultOptions.js b/simple-mind-map/src/constants/defaultOptions.js index 7afe3914..99777253 100644 --- a/simple-mind-map/src/constants/defaultOptions.js +++ b/simple-mind-map/src/constants/defaultOptions.js @@ -251,6 +251,12 @@ export const defaultOpt = { mousedownEventPreventDefault: true, // 在激活上粘贴用户剪贴板中的数据时,如果同时存在文本和图片,那么只粘贴文本,忽略图片 onlyPasteTextWhenHasImgAndText: true, + // 是否允许拖拽调整节点的宽度,实际上压缩的是节点里面文本内容的宽度,当节点文本内容宽度压缩到最小时无法继续压缩。如果节点存在图片,那么最小值以图片宽度和文本内容最小宽度的最大值为准(目前该特性仅在两种情况下可用:1.开启了富文本模式,即注册了RichText插件;2.自定义节点内容) + enableDragModifyNodeWidth: true, + // 当允许拖拽调整节点的宽度时,可以通过该选项设置节点文本内容允许压缩的最小宽度 + minNodeTextModifyWidth: 20, + // 同minNodeTextModifyWidth,最大值,传-1代表不限制 + maxNodeTextModifyWidth: -1, // 【Select插件】 // 多选节点时鼠标移动到边缘时的画布移动偏移量 diff --git a/simple-mind-map/src/core/render/node/MindMapNode.js b/simple-mind-map/src/core/render/node/MindMapNode.js index ad8dafe8..69f8f482 100644 --- a/simple-mind-map/src/core/render/node/MindMapNode.js +++ b/simple-mind-map/src/core/render/node/MindMapNode.js @@ -6,6 +6,7 @@ import nodeExpandBtnMethods from './nodeExpandBtn' import nodeCommandWrapsMethods from './nodeCommandWraps' import nodeCreateContentsMethods from './nodeCreateContents' import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect' +import nodeModifyWidthMethods from './nodeModifyWidth' import nodeCooperateMethods from './nodeCooperate' import { CONSTANTS } from '../../../constants/constant' import { @@ -54,6 +55,8 @@ class MindMapNode { this.width = opt.width || 0 // 节点高 this.height = opt.height || 0 + // 自定义文本的宽度 + this.customTextWidth = opt.data.data.customTextWidth || undefined // left this._left = opt.left || 0 // top @@ -150,10 +153,15 @@ class MindMapNode { proto[item] = nodeCooperateMethods[item] }) } + // 拖拽调整节点宽度 + Object.keys(nodeModifyWidthMethods).forEach(item => { + proto[item] = nodeModifyWidthMethods[item] + }) proto.bindEvent = true } // 初始化 this.getSize() + this.initDragHandle() } // 支持自定义位置 @@ -197,7 +205,8 @@ class MindMapNode { } // 创建节点的各个内容对象数据 - createNodeData() { + // recreateTypes:[] custom、image、icon、text、hyperlink、tag、note、attachment、numbers、prefix、postfix + createNodeData(recreateTypes) { // 自定义节点内容 let { isUseCustomNodeContent, @@ -205,7 +214,39 @@ class MindMapNode { createNodePrefixContent, createNodePostfixContent } = this.mindMap.opt - if (isUseCustomNodeContent && customCreateNodeContent) { + // 需要创建的内容类型 + const typeList = [ + 'custom', + 'image', + 'icon', + 'text', + 'hyperlink', + 'tag', + 'note', + 'attachment', + 'numbers', + 'prefix', + 'postfix' + ] + const createTypes = {} + if (Array.isArray(recreateTypes)) { + // 重新创建指定的内容类型 + typeList.forEach(item => { + if (recreateTypes.includes(item)) { + createTypes[item] = true + } + }) + } else { + // 创建所有类型 + typeList.forEach(item => { + createTypes[item] = true + }) + } + if ( + isUseCustomNodeContent && + customCreateNodeContent && + createTypes.custom + ) { this._customNodeContent = customCreateNodeContent(this) } // 如果没有返回内容,那么还是使用内置的节点内容 @@ -213,37 +254,42 @@ class MindMapNode { addXmlns(this._customNodeContent) return } - this._imgData = this.createImgNode() - this._iconData = this.createIconNode() - this._textData = this.createTextNode() - this._hyperlinkData = this.createHyperlinkNode() - this._tagData = this.createTagNode() - this._noteData = this.createNoteNode() - this._attachmentData = this.createAttachmentNode() - if (this.mindMap.numbers) { + if (createTypes.image) this._imgData = this.createImgNode() + if (createTypes.icon) this._iconData = this.createIconNode() + if (createTypes.text) this._textData = this.createTextNode() + if (createTypes.hyperlink) this._hyperlinkData = this.createHyperlinkNode() + if (createTypes.tag) this._tagData = this.createTagNode() + if (createTypes.note) this._noteData = this.createNoteNode() + if (createTypes.attachment) + this._attachmentData = this.createAttachmentNode() + if (this.mindMap.numbers && createTypes.numbers) { this._numberData = this.mindMap.numbers.createNumberContent(this) } - this._prefixData = createNodePrefixContent - ? createNodePrefixContent(this) - : null - if (this._prefixData && this._prefixData.el) { - addXmlns(this._prefixData.el) + if (createTypes.prefix) { + this._prefixData = createNodePrefixContent + ? createNodePrefixContent(this) + : null + if (this._prefixData && this._prefixData.el) { + addXmlns(this._prefixData.el) + } } - this._postfixData = createNodePostfixContent - ? createNodePostfixContent(this) - : null - if (this._postfixData && this._postfixData.el) { - addXmlns(this._postfixData.el) + if (createTypes.postfix) { + this._postfixData = createNodePostfixContent + ? createNodePostfixContent(this) + : null + if (this._postfixData && this._postfixData.el) { + addXmlns(this._postfixData.el) + } } } // 计算节点的宽高 - getSize() { + getSize(recreateTypes) { this.customLeft = this.getData('customLeft') || undefined this.customTop = this.getData('customTop') || undefined // 这里不要更新概要,不然即使概要没修改,每次也会重新渲染 // this.updateGeneralization() - this.createNodeData() + this.createNodeData(recreateTypes) let { width, height } = this.getNodeRect() // 判断节点尺寸是否有变化 let changed = this.width !== width || this.height !== height @@ -258,7 +304,7 @@ class MindMapNode { if (this.isUseCustomNodeContent()) { let rect = this.measureCustomNodeContentSize(this._customNodeContent) return { - width: rect.width, + width: this.hasCustomWidth() ? this.customTextWidth : rect.width, height: rect.height } } @@ -735,6 +781,8 @@ class MindMapNode { } } } + // 更新拖拽手柄的显示与否 + this.updateDragHandle() // 更新概要 this.renderGeneralization(forceRender) // 更新协同头像 @@ -770,8 +818,8 @@ class MindMapNode { } // 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置 - reRender() { - let sizeChange = this.getSize() + reRender(recreateTypes) { + let sizeChange = this.getSize(recreateTypes) this.layout() this.update() return sizeChange @@ -794,6 +842,7 @@ class MindMapNode { this.hideExpandBtn() } this.updateNodeActiveClass() + this.updateDragHandle() } } @@ -1297,6 +1346,30 @@ class MindMapNode { createSvgTextNode(text = '') { return new Text().text(text) } + + // 检查是否支持拖拽调整宽度 + // 1.富文本模式 + // 2.自定义节点内容 + checkEnableDragModifyNodeWidth() { + const { + enableDragModifyNodeWidth, + isUseCustomNodeContent, + customCreateNodeContent + } = this.mindMap.opt + return ( + enableDragModifyNodeWidth && + (this.mindMap.richText || + (isUseCustomNodeContent && customCreateNodeContent)) + ) + } + + // 是否存在自定义宽度 + hasCustomWidth() { + return ( + this.checkEnableDragModifyNodeWidth() && + this.customTextWidth !== undefined + ) + } } export default MindMapNode diff --git a/simple-mind-map/src/core/render/node/nodeCreateContents.js b/simple-mind-map/src/core/render/node/nodeCreateContents.js index 5bc5b2a9..bd6aa408 100644 --- a/simple-mind-map/src/core/render/node/nodeCreateContents.js +++ b/simple-mind-map/src/core/render/node/nodeCreateContents.js @@ -115,9 +115,11 @@ function createIconNode() { // 创建富文本节点 function createRichTextNode(specifyText) { + const hasCustomWidth = this.hasCustomWidth() let text = typeof specifyText === 'string' ? specifyText : this.getData('text') - const { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt + let { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt + textAutoWrapWidth = hasCustomWidth ? this.customTextWidth : textAutoWrapWidth let g = new G() // 重新设置富文本节点内容 let recoverText = false @@ -172,6 +174,11 @@ function createRichTextNode(specifyText) { el.classList.add('smm-richtext-node-wrap') addXmlns(el) el.style.maxWidth = textAutoWrapWidth + 'px' + if (hasCustomWidth) { + el.style.width = this.customTextWidth + 'px' + } else { + el.style.width = '' + } let { width, height } = el.getBoundingClientRect() // 如果文本为空,那么需要计算一个默认高度 if (height <= 0) { diff --git a/simple-mind-map/src/core/render/node/nodeModifyWidth.js b/simple-mind-map/src/core/render/node/nodeModifyWidth.js new file mode 100644 index 00000000..af54efa6 --- /dev/null +++ b/simple-mind-map/src/core/render/node/nodeModifyWidth.js @@ -0,0 +1,151 @@ +import { Rect } from '@svgdotjs/svg.js' + +// 初始化拖拽 +function initDragHandle() { + if (!this.checkEnableDragModifyNodeWidth()) { + return + } + // 拖拽手柄元素 + this._dragHandleNodes = null + // 手柄元素的宽度 + this.dragHandleWidth = 2 + // 鼠标按下时的x坐标 + this.dragHandleMousedownX = 0 + // 鼠标是否处于按下状态 + this.isDragHandleMousedown = false + // 当前拖拽的手柄序号 + this.dragHandleIndex = 0 + // 鼠标按下时记录当前的customTextWidth值 + this.dragHandleMousedownCustomTextWidth = 0 + // 鼠标按下时记录当前的手型样式 + this.dragHandleMousedownBodyCursor = '' + // 鼠标按下时记录当前节点的left值 + this.dragHandleMousedownLeft = 0 + + this.onDragMousemoveHandle = this.onDragMousemoveHandle.bind(this) + window.addEventListener('mousemove', this.onDragMousemoveHandle) + this.onDragMouseupHandle = this.onDragMouseupHandle.bind(this) + window.addEventListener('mouseup', this.onDragMouseupHandle) + this.mindMap.on('node_mouseup', this.onDragMouseupHandle) +} + +// 鼠标移动事件 +function onDragMousemoveHandle(e) { + if (!this.isDragHandleMousedown) return + e.stopPropagation() + e.preventDefault() + let { + minNodeTextModifyWidth, + maxNodeTextModifyWidth, + isUseCustomNodeContent, + customCreateNodeContent + } = this.mindMap.opt + const useCustomContent = + isUseCustomNodeContent && customCreateNodeContent && this._customNodeContent + document.body.style.cursor = 'ew-resize' + this.group.css({ + cursor: 'ew-resize' + }) + const { scaleX } = this.mindMap.draw.transform() + const ox = e.clientX - this.dragHandleMousedownX + let newWidth = + this.dragHandleMousedownCustomTextWidth + + (this.dragHandleIndex === 0 ? -ox : ox) / scaleX + newWidth = Math.max(newWidth, minNodeTextModifyWidth) + if (maxNodeTextModifyWidth !== -1) { + newWidth = Math.min(newWidth, maxNodeTextModifyWidth) + } + // 如果存在图片,那么最小值需要考虑图片宽度 + if (!useCustomContent && this.getData('image')) { + const imgSize = this.getImgShowSize() + if ( + this._rectInfo.textContentWidth - this.customTextWidth + newWidth <= + imgSize[0] + ) { + newWidth = + imgSize[0] + this.customTextWidth - this._rectInfo.textContentWidth + } + } + this.customTextWidth = newWidth + if (this.dragHandleIndex === 0) { + this.left = this.dragHandleMousedownLeft + ox / scaleX + } + // 自定义内容不重新渲染,交给开发者 + this.reRender(useCustomContent ? [] : ['text']) +} + +// 鼠标松开事件 +function onDragMouseupHandle() { + if (!this.isDragHandleMousedown) return + document.body.style.cursor = this.dragHandleMousedownBodyCursor + this.group.css({ + cursor: 'default' + }) + this.isDragHandleMousedown = false + this.dragHandleMousedownX = 0 + this.dragHandleIndex = 0 + this.dragHandleMousedownCustomTextWidth = 0 + this.setData({ + customTextWidth: this.customTextWidth + }) + this.mindMap.render() + this.mindMap.emit('dragModifyNodeWidthEnd', this) +} + +// 插件拖拽手柄元素 +function createDragHandleNode() { + const list = [new Rect(), new Rect()] + list.forEach((node, index) => { + node + .size(this.dragHandleWidth, this.height) + .fill({ + color: 'transparent' + }) + .css({ + cursor: 'ew-resize' + }) + node.on('mousedown', e => { + e.stopPropagation() + e.preventDefault() + this.dragHandleMousedownX = e.clientX + this.dragHandleIndex = index + this.dragHandleMousedownCustomTextWidth = + this.customTextWidth === undefined + ? this._textData + ? this._textData.width + : this.width + : this.customTextWidth + this.dragHandleMousedownBodyCursor = document.body.style.cursor + this.dragHandleMousedownLeft = this.left + this.isDragHandleMousedown = true + }) + }) + return list +} + +// 更新拖拽按钮的显隐和位置尺寸 +function updateDragHandle() { + if (!this.checkEnableDragModifyNodeWidth()) return + if (!this._dragHandleNodes) { + this._dragHandleNodes = this.createDragHandleNode() + } + if (this.getData('isActive')) { + this._dragHandleNodes.forEach(node => { + node.height(this.height) + this.group.add(node) + }) + this._dragHandleNodes[1].x(this.width - this.dragHandleWidth) + } else { + this._dragHandleNodes.forEach(node => { + node.remove() + }) + } +} + +export default { + initDragHandle, + onDragMousemoveHandle, + onDragMouseupHandle, + createDragHandleNode, + updateDragHandle +} diff --git a/simple-mind-map/src/plugins/RichText.js b/simple-mind-map/src/plugins/RichText.js index d4916d54..6a46023d 100644 --- a/simple-mind-map/src/plugins/RichText.js +++ b/simple-mind-map/src/plugins/RichText.js @@ -180,7 +180,7 @@ class RichText { if (this.showTextEdit) { return } - const { + let { richTextEditFakeInPlace, customInnerElsAppendTo, nodeTextEditZIndex, @@ -188,6 +188,9 @@ class RichText { selectTextOnEnterEditText, transformRichTextOnEnterEdit } = this.mindMap.opt + textAutoWrapWidth = node.hasCustomWidth() + ? node.customTextWidth + : textAutoWrapWidth this.node = node this.isInserting = isInserting if (!rect) rect = node._textData.node.node.getBoundingClientRect()