diff --git a/simple-mind-map/src/core/render/node/MindMapNode.js b/simple-mind-map/src/core/render/node/MindMapNode.js index 7bcc08c9..aa1812fa 100644 --- a/simple-mind-map/src/core/render/node/MindMapNode.js +++ b/simple-mind-map/src/core/render/node/MindMapNode.js @@ -8,14 +8,10 @@ import nodeCreateContentsMethods from './nodeCreateContents' import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect' import nodeModifyWidthMethods from './nodeModifyWidth' import nodeCooperateMethods from './nodeCooperate' -import QuickCreateChildBtnMethods from './QuickCreateChildBtn' +import quickCreateChildBtnMethods from './quickCreateChildBtn' +import nodeLayoutMethods from './nodeLayout' import { CONSTANTS } from '../../../constants/constant' -import { - copyNodeTree, - createForeignObjectNode, - createUid, - addXmlns -} from '../../../utils/index' +import { copyNodeTree, createUid, addXmlns } from '../../../utils/index' // 节点类 class MindMapNode { @@ -105,18 +101,13 @@ class MindMapNode { this._isMouseenter = false // 尺寸信息 this._rectInfo = { - imgContentWidth: 0, - imgContentHeight: 0, textContentWidth: 0, - textContentHeight: 0 + textContentHeight: 0, + textContentWidthWithoutTag: 0 } // 概要节点的宽高 this._generalizationNodeWidth = 0 this._generalizationNodeHeight = 0 - // 各种文字信息的间距 - this.textContentItemMargin = this.mindMap.opt.textContentMargin - // 图片和文字节点的间距 - this.blockContentMargin = this.mindMap.opt.imgTextMargin // 展开收缩按钮尺寸 this.expandBtnSize = this.mindMap.opt.expandBtnSize // 是否是多选节点 @@ -127,6 +118,10 @@ class MindMapNode { this.isHide = false const proto = Object.getPrototypeOf(this) if (!proto.bindEvent) { + // 节点尺寸计算和布局相关方法 + Object.keys(nodeLayoutMethods).forEach(item => { + proto[item] = nodeLayoutMethods[item] + }) // 概要相关方法 Object.keys(nodeGeneralizationMethods).forEach(item => { proto[item] = nodeGeneralizationMethods[item] @@ -159,8 +154,8 @@ class MindMapNode { }) // 快捷创建子节点按钮 if (this.mindMap.opt.isShowCreateChildBtnIcon) { - Object.keys(QuickCreateChildBtnMethods).forEach(item => { - proto[item] = QuickCreateChildBtnMethods[item] + Object.keys(quickCreateChildBtnMethods).forEach(item => { + proto[item] = quickCreateChildBtnMethods[item] }) this.initQuickCreateChildBtn() } @@ -315,334 +310,6 @@ class MindMapNode { return changed } - // 计算节点尺寸信息 - getNodeRect() { - // 自定义节点内容 - if (this.isUseCustomNodeContent()) { - const rect = this.measureCustomNodeContentSize(this._customNodeContent) - return { - width: this.hasCustomWidth() ? this.customTextWidth : rect.width, - height: rect.height - } - } - const { tagPosition } = this.mindMap.opt - const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM - // 宽高 - let imgContentWidth = 0 - let imgContentHeight = 0 - let textContentWidth = 0 - let textContentHeight = 0 - let tagContentWidth = 0 - let tagContentHeight = 0 - // 存在图片 - if (this._imgData) { - this._rectInfo.imgContentWidth = imgContentWidth = this._imgData.width - this._rectInfo.imgContentHeight = imgContentHeight = this._imgData.height - } - // 库前置内容 - this.mindMap.nodeInnerPrefixList.forEach(item => { - const itemData = this[`_${item.name}Data`] - if (itemData) { - textContentWidth += itemData.width - textContentHeight = Math.max(textContentHeight, itemData.height) - } - }) - // 自定义前置内容 - if (this._prefixData) { - textContentWidth += this._prefixData.width - textContentHeight = Math.max(textContentHeight, this._prefixData.height) - } - // 图标 - if (this._iconData.length > 0) { - textContentWidth += this._iconData.reduce((sum, cur) => { - textContentHeight = Math.max(textContentHeight, cur.height) - return (sum += cur.width + this.textContentItemMargin) - }, 0) - } - // 文字 - if (this._textData) { - textContentWidth += this._textData.width - textContentHeight = Math.max(textContentHeight, this._textData.height) - } - // 超链接 - if (this._hyperlinkData) { - textContentWidth += this._hyperlinkData.width - textContentHeight = Math.max( - textContentHeight, - this._hyperlinkData.height - ) - } - // 标签 - if (this._tagData.length > 0) { - let maxTagHeight = 0 - const totalTagWidth = this._tagData.reduce((sum, cur) => { - maxTagHeight = Math.max(maxTagHeight, cur.height) - return (sum += cur.width + this.textContentItemMargin) - }, 0) - if (tagIsBottom) { - // 文字下方 - tagContentWidth = totalTagWidth - tagContentHeight = maxTagHeight - } else { - // 否则在右侧 - textContentWidth += totalTagWidth - textContentHeight = Math.max(textContentHeight, maxTagHeight) - } - } - // 备注 - if (this._noteData) { - textContentWidth += this._noteData.width - textContentHeight = Math.max(textContentHeight, this._noteData.height) - } - // 附件 - if (this._attachmentData) { - textContentWidth += this._attachmentData.width - textContentHeight = Math.max( - textContentHeight, - this._attachmentData.height - ) - } - // 自定义后置内容 - if (this._postfixData) { - textContentWidth += this._postfixData.width - textContentHeight = Math.max(textContentHeight, this._postfixData.height) - } - // 文字内容部分的尺寸 - this._rectInfo.textContentWidth = textContentWidth - this._rectInfo.textContentHeight = textContentHeight - // 间距 - let margin = - imgContentHeight > 0 && textContentHeight > 0 - ? this.blockContentMargin - : 0 - const { paddingX, paddingY } = this.getPaddingVale() - // 纯内容宽高 - let _width = Math.max(imgContentWidth, textContentWidth) - let _height = imgContentHeight + textContentHeight - // 如果标签在文字下方 - if (tagIsBottom && tagContentHeight > 0 && textContentHeight > 0) { - // 那么文字和标签之间也需要间距 - margin += this.blockContentMargin - // 整体高度要考虑标签宽度 - _width = Math.max(_width, tagContentWidth) - // 整体高度要加上标签的高度 - _height += tagContentHeight - } - // 计算节点形状需要的附加内边距 - const { paddingX: shapePaddingX, paddingY: shapePaddingY } = - this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY) - this.shapePadding.paddingX = shapePaddingX - this.shapePadding.paddingY = shapePaddingY - // 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点 - const borderWidth = this.getBorderWidth() - return { - width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth, - height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth - } - } - - // 定位节点内容 - layout() { - if (!this.group) return - // 清除之前的内容 - this.group.clear() - const { hoverRectPadding, tagPosition, openRealtimeRenderOnNodeTextEdit } = - this.mindMap.opt - // 避免编辑过程中展开收起按钮闪烁的问题 - if (openRealtimeRenderOnNodeTextEdit && this._expandBtn) { - this.group.add(this._expandBtn) - } - let { width, height, textContentItemMargin } = this - let { paddingY } = this.getPaddingVale() - const halfBorderWidth = this.getBorderWidth() / 2 - paddingY += this.shapePadding.paddingY + halfBorderWidth - // 节点形状 - this.shapeNode = this.shapeInstance.createShape() - this.shapeNode.addClass('smm-node-shape') - this.shapeNode.translate(halfBorderWidth, halfBorderWidth) - this.style.shape(this.shapeNode) - this.group.add(this.shapeNode) - // 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示 - this.renderExpandBtnPlaceholderRect() - // 创建协同头像节点 - if (this.createUserListNode) this.createUserListNode() - // 概要节点添加一个带所属节点id的类名 - if (this.isGeneralization && this.generalizationBelongNode) { - this.group.addClass('generalization_' + this.generalizationBelongNode.uid) - } - // 激活hover和激活边框 - const addHoverNode = () => { - this.hoverNode = new Rect() - .size(width + hoverRectPadding * 2, height + hoverRectPadding * 2) - .x(-hoverRectPadding) - .y(-hoverRectPadding) - this.hoverNode.addClass('smm-hover-node') - this.style.hoverNode(this.hoverNode, width, height) - this.group.add(this.hoverNode) - } - // 如果存在自定义节点内容,那么使用自定义节点内容 - if (this.isUseCustomNodeContent()) { - const foreignObject = createForeignObjectNode({ - el: this._customNodeContent, - width, - height - }) - this.group.add(foreignObject) - addHoverNode() - return - } - const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM - const { textContentHeight } = this._rectInfo - // 图片节点 - let imgHeight = 0 - if (this._imgData) { - imgHeight = this._imgData.height - this.group.add(this._imgData.node) - this._imgData.node.cx(width / 2).y(paddingY) - } - // 内容节点 - let textContentNested = new G() - let textContentOffsetX = 0 - // 库前置内容 - this.mindMap.nodeInnerPrefixList.forEach(item => { - const itemData = this[`_${item.name}Data`] - if (itemData) { - itemData.node - .x(textContentOffsetX) - .y((textContentHeight - itemData.height) / 2) - textContentNested.add(itemData.node) - textContentOffsetX += itemData.width + textContentItemMargin - } - }) - // 自定义前置内容 - if (this._prefixData) { - const foreignObject = createForeignObjectNode({ - el: this._prefixData.el, - width: this._prefixData.width, - height: this._prefixData.height - }) - foreignObject - .x(textContentOffsetX) - .y((textContentHeight - this._prefixData.height) / 2) - textContentNested.add(foreignObject) - textContentOffsetX += this._prefixData.width + textContentItemMargin - } - // icon - let iconNested = new G() - if (this._iconData && this._iconData.length > 0) { - let iconLeft = 0 - this._iconData.forEach(item => { - item.node - .x(textContentOffsetX + iconLeft) - .y((textContentHeight - item.height) / 2) - iconNested.add(item.node) - iconLeft += item.width + textContentItemMargin - }) - textContentNested.add(iconNested) - textContentOffsetX += iconLeft - } - // 文字 - if (this._textData) { - const oldX = this._textData.node.attr('data-offsetx') || 0 - this._textData.node.attr('data-offsetx', textContentOffsetX) - // 修复safari浏览器节点存在图标时文字位置不正确的问题 - ;(this._textData.nodeContent || this._textData.node) - .x(-oldX) // 修复非富文本模式下同时存在图标和换行的文本时,被收起和展开时图标与文字距离会逐渐拉大的问题 - .x(textContentOffsetX) - .y((textContentHeight - this._textData.height) / 2) - // 如果开启了文本编辑实时渲染,需要判断当前渲染的节点是否是正在编辑的节点,是的话将透明度设置为0不显示 - if (openRealtimeRenderOnNodeTextEdit) { - this._textData.node.opacity( - this.mindMap.renderer.textEdit.getCurrentEditNode() === this ? 0 : 1 - ) - } - textContentNested.add(this._textData.node) - textContentOffsetX += this._textData.width + textContentItemMargin - } - // 超链接 - if (this._hyperlinkData) { - this._hyperlinkData.node - .x(textContentOffsetX) - .y((textContentHeight - this._hyperlinkData.height) / 2) - textContentNested.add(this._hyperlinkData.node) - textContentOffsetX += this._hyperlinkData.width + textContentItemMargin - } - // 标签 - let tagNested = new G() - if (this._tagData && this._tagData.length > 0) { - if (tagIsBottom) { - // 标签显示在文字下方 - let tagLeft = 0 - this._tagData.forEach(item => { - item.node.x(tagLeft).y(0) - tagNested.add(item.node) - tagLeft += item.width + textContentItemMargin - }) - tagNested.cx(width / 2).y( - paddingY + // 内边距 - imgHeight + // 图片高度 - textContentHeight + // 文本区域高度 - (imgHeight > 0 && textContentHeight > 0 - ? this.blockContentMargin - : 0) + // 图片和文本之间的间距 - this.blockContentMargin // 标签和文本之间的间距 - ) - this.group.add(tagNested) - } else { - // 标签显示在文字右侧 - let tagLeft = 0 - this._tagData.forEach(item => { - item.node - .x(textContentOffsetX + tagLeft) - .y((textContentHeight - item.height) / 2) - tagNested.add(item.node) - tagLeft += item.width + textContentItemMargin - }) - textContentNested.add(tagNested) - textContentOffsetX += tagLeft - } - } - // 备注 - if (this._noteData) { - this._noteData.node - .x(textContentOffsetX) - .y((textContentHeight - this._noteData.height) / 2) - textContentNested.add(this._noteData.node) - textContentOffsetX += this._noteData.width - } - // 附件 - if (this._attachmentData) { - this._attachmentData.node - .x(textContentOffsetX) - .y((textContentHeight - this._attachmentData.height) / 2) - textContentNested.add(this._attachmentData.node) - textContentOffsetX += this._attachmentData.width - } - // 自定义后置内容 - if (this._postfixData) { - const foreignObject = createForeignObjectNode({ - el: this._postfixData.el, - width: this._postfixData.width, - height: this._postfixData.height - }) - foreignObject - .x(textContentOffsetX) - .y((textContentHeight - this._postfixData.height) / 2) - textContentNested.add(foreignObject) - textContentOffsetX += this._postfixData.width - } - this.group.add(textContentNested) - // 文字内容整体 - textContentNested.translate( - width / 2 - textContentNested.bbox().width / 2, - paddingY + // 内边距 - imgHeight + // 图片高度 - (imgHeight > 0 && textContentHeight > 0 ? this.blockContentMargin : 0) // 和图片的间距 - ) - addHoverNode() - this.mindMap.emit('node_layout_end', this) - } - // 给节点绑定事件 bindGroupEvent() { // 单击事件,选中节点 diff --git a/simple-mind-map/src/core/render/node/QuickCreateChildBtn.js b/simple-mind-map/src/core/render/node/QuickCreateChildBtn.js index ed21d346..257cb883 100644 --- a/simple-mind-map/src/core/render/node/QuickCreateChildBtn.js +++ b/simple-mind-map/src/core/render/node/QuickCreateChildBtn.js @@ -13,7 +13,8 @@ function showQuickCreateChildBtn() { if (this._quickCreateChildBtn) { this.group.add(this._quickCreateChildBtn) } else { - const { quickCreateChildBtnIcon, expandBtnStyle } = this.mindMap.opt + const { quickCreateChildBtnIcon, expandBtnStyle, expandBtnSize } = + this.mindMap.opt const { icon, style } = quickCreateChildBtnIcon let { color, fill } = expandBtnStyle || { color: '#808080', @@ -22,17 +23,17 @@ function showQuickCreateChildBtn() { color = style.color || color // 图标节点 const iconNode = SVG(icon || btnsSvg.quickCreateChild).size( - this.expandBtnSize, - this.expandBtnSize + expandBtnSize, + expandBtnSize ) iconNode.css({ cursor: 'pointer' }) - iconNode.x(0).y(-this.expandBtnSize / 2) + iconNode.x(0).y(-expandBtnSize / 2) this.style.iconNode(iconNode, color) // 填充节点 - const fillNode = new Circle().size(this.expandBtnSize) - fillNode.x(0).y(-this.expandBtnSize / 2) + const fillNode = new Circle().size(expandBtnSize) + fillNode.x(0).y(-expandBtnSize / 2) fillNode.fill({ color: fill }).css({ cursor: 'pointer' }) diff --git a/simple-mind-map/src/core/render/node/nodeExpandBtn.js b/simple-mind-map/src/core/render/node/nodeExpandBtn.js index 7d6aa380..7961a18c 100644 --- a/simple-mind-map/src/core/render/node/nodeExpandBtn.js +++ b/simple-mind-map/src/core/render/node/nodeExpandBtn.js @@ -7,9 +7,10 @@ function createExpandNodeContent() { if (this._openExpandNode) { return } - let { close, open } = this.mindMap.opt.expandBtnIcon || {} + const { expandBtnSize, expandBtnIcon, isShowExpandNum } = this.mindMap.opt + let { close, open } = expandBtnIcon || {} // 根据配置判断是否显示数量按钮 - if (this.mindMap.opt.isShowExpandNum) { + if (isShowExpandNum) { // 展开的节点 this._openExpandNode = new Text() this._openExpandNode.addClass('smm-expand-btn-text') @@ -17,25 +18,25 @@ function createExpandNodeContent() { this._openExpandNode.attr({ 'text-anchor': 'middle', 'dominant-baseline': 'middle', - x: this.expandBtnSize / 2, + x: expandBtnSize / 2, y: 2 }) } else { this._openExpandNode = SVG(open || btnsSvg.open).size( - this.expandBtnSize, - this.expandBtnSize + expandBtnSize, + expandBtnSize ) - this._openExpandNode.x(0).y(-this.expandBtnSize / 2) + this._openExpandNode.x(0).y(-expandBtnSize / 2) } // 收起的节点 this._closeExpandNode = SVG(close || btnsSvg.close).size( - this.expandBtnSize, - this.expandBtnSize + expandBtnSize, + expandBtnSize ) - this._closeExpandNode.x(0).y(-this.expandBtnSize / 2) + this._closeExpandNode.x(0).y(-expandBtnSize / 2) // 填充节点 - this._fillExpandNode = new Circle().size(this.expandBtnSize) - this._fillExpandNode.x(0).y(-this.expandBtnSize / 2) + this._fillExpandNode = new Circle().size(expandBtnSize) + this._fillExpandNode.x(0).y(-expandBtnSize / 2) // 设置样式 this.style.iconBtn( diff --git a/simple-mind-map/src/core/render/node/nodeExpandBtnPlaceholderRect.js b/simple-mind-map/src/core/render/node/nodeExpandBtnPlaceholderRect.js index 7a11e55f..40c6d14d 100644 --- a/simple-mind-map/src/core/render/node/nodeExpandBtnPlaceholderRect.js +++ b/simple-mind-map/src/core/render/node/nodeExpandBtnPlaceholderRect.js @@ -7,7 +7,8 @@ function renderExpandBtnPlaceholderRect() { return } // 默认显示展开按钮的情况下或不显示展开收起按钮的情况下不需要渲染 - const { alwaysShowExpandBtn, notShowExpandBtn } = this.mindMap.opt + const { alwaysShowExpandBtn, notShowExpandBtn, expandBtnSize } = + this.mindMap.opt if (!alwaysShowExpandBtn && !notShowExpandBtn) { let { width, height } = this if (!this._unVisibleRectRegionNode) { @@ -19,7 +20,7 @@ function renderExpandBtnPlaceholderRect() { this.group.add(this._unVisibleRectRegionNode) this.renderer.layout.renderExpandBtnRect( this._unVisibleRectRegionNode, - this.expandBtnSize, + expandBtnSize, width, height, this diff --git a/simple-mind-map/src/core/render/node/nodeLayout.js b/simple-mind-map/src/core/render/node/nodeLayout.js new file mode 100644 index 00000000..6e014bef --- /dev/null +++ b/simple-mind-map/src/core/render/node/nodeLayout.js @@ -0,0 +1,436 @@ +import { CONSTANTS } from '../../../constants/constant' +import { G, Rect } from '@svgdotjs/svg.js' +import { createForeignObjectNode } from '../../../utils/index' + +// 根据图片放置位置返回图片和文本的间距值 +function getImgTextMarin(dir, imgWidth, textWidth, imgHeight, textHeight) { + // 图片和文字节点的间距 + const { imgTextMargin } = this.mindMap.opt + if (dir === 'v') { + // 垂直 + return imgHeight > 0 && textHeight > 0 ? imgTextMargin : 0 + } else { + // 水平 + return imgWidth > 0 && textWidth > 0 ? imgTextMargin : 0 + } +} + +// 获取标签内容的大小 +function getTagContentSize(space) { + let maxTagHeight = 0 + let width = this._tagData.reduce((sum, cur) => { + maxTagHeight = Math.max(maxTagHeight, cur.height) + return (sum += cur.width) + }, 0) + width += (this._tagData.length - 1) * space + return { + width, + height: maxTagHeight + } +} + +// 计算节点尺寸信息 +function getNodeRect() { + // 自定义节点内容 + if (this.isUseCustomNodeContent()) { + const rect = this.measureCustomNodeContentSize(this._customNodeContent) + return { + width: this.hasCustomWidth() ? this.customTextWidth : rect.width, + height: rect.height + } + } + const { tagPosition, textContentMargin } = this.mindMap.opt + const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM + const imgPlacement = this.getStyle('imgPlacement') || 'top' + // 宽高 + let imgContentWidth = 0 + let imgContentHeight = 0 + let textContentWidth = 0 + let textContentHeight = 0 + let tagContentWidth = 0 + let tagContentHeight = 0 + let spaceCount = 0 + // 存在图片 + if (this._imgData) { + imgContentWidth = this._imgData.width + imgContentHeight = this._imgData.height + } + // 库前置内容 + this.mindMap.nodeInnerPrefixList.forEach(item => { + const itemData = this[`_${item.name}Data`] + if (itemData) { + textContentWidth += itemData.width + textContentHeight = Math.max(textContentHeight, itemData.height) + spaceCount++ + } + }) + // 自定义前置内容 + if (this._prefixData) { + textContentWidth += this._prefixData.width + textContentHeight = Math.max(textContentHeight, this._prefixData.height) + spaceCount++ + } + // 图标 + if (this._iconData.length > 0) { + textContentWidth += + this._iconData.reduce((sum, cur) => { + textContentHeight = Math.max(textContentHeight, cur.height) + return (sum += cur.width) + }, 0) + + (this._iconData.length - 1) * textContentMargin + spaceCount++ + } + // 文字 + if (this._textData) { + textContentWidth += this._textData.width + textContentHeight = Math.max(textContentHeight, this._textData.height) + spaceCount++ + } + // 超链接 + if (this._hyperlinkData) { + textContentWidth += this._hyperlinkData.width + textContentHeight = Math.max(textContentHeight, this._hyperlinkData.height) + spaceCount++ + } + // 标签 + if (this._tagData.length > 0) { + const { width: totalTagWidth, height: maxTagHeight } = + this.getTagContentSize(textContentMargin) + if (tagIsBottom) { + // 文字下方 + tagContentWidth = totalTagWidth + tagContentHeight = maxTagHeight + } else { + // 否则在右侧 + textContentWidth += totalTagWidth + textContentHeight = Math.max(textContentHeight, maxTagHeight) + spaceCount++ + } + } + // 备注 + if (this._noteData) { + textContentWidth += this._noteData.width + textContentHeight = Math.max(textContentHeight, this._noteData.height) + spaceCount++ + } + // 附件 + if (this._attachmentData) { + textContentWidth += this._attachmentData.width + textContentHeight = Math.max(textContentHeight, this._attachmentData.height) + spaceCount++ + } + // 自定义后置内容 + if (this._postfixData) { + textContentWidth += this._postfixData.width + textContentHeight = Math.max(textContentHeight, this._postfixData.height) + spaceCount++ + } + textContentWidth += (spaceCount - 1) * textContentMargin + // 文字内容部分的尺寸 + if (tagIsBottom && textContentWidth > 0 && tagContentHeight > 0) { + this._rectInfo.textContentWidthWithoutTag = textContentWidth + textContentWidth = Math.max(textContentWidth, tagContentWidth) + textContentHeight = textContentHeight + textContentMargin + tagContentHeight + } + this._rectInfo.textContentWidth = textContentWidth + this._rectInfo.textContentHeight = textContentHeight + + // 纯内容宽高 + let _width = 0 + let _height = 0 + if (['top', 'bottom'].includes(imgPlacement)) { + // 图片在上下 + _width = Math.max(imgContentWidth, textContentWidth) + _height = + imgContentHeight + + textContentHeight + + this.getImgTextMarin('v', 0, 0, imgContentHeight, textContentHeight) + } else { + // 图片在左右 + _width = + imgContentWidth + + textContentWidth + + this.getImgTextMarin('h', imgContentWidth, textContentWidth) + _height = Math.max(imgContentHeight, textContentHeight) + } + const { paddingX, paddingY } = this.getPaddingVale() + // 计算节点形状需要的附加内边距 + const { paddingX: shapePaddingX, paddingY: shapePaddingY } = + this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY) + this.shapePadding.paddingX = shapePaddingX + this.shapePadding.paddingY = shapePaddingY + // 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点 + const borderWidth = this.getBorderWidth() + return { + width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth, + height: _height + paddingY * 2 + shapePaddingY * 2 + borderWidth + } +} + +// 定位节点内容 +function layout() { + if (!this.group) return + // 清除之前的内容 + this.group.clear() + const { + hoverRectPadding, + tagPosition, + openRealtimeRenderOnNodeTextEdit, + textContentMargin + } = this.mindMap.opt + // 避免编辑过程中展开收起按钮闪烁的问题 + if (openRealtimeRenderOnNodeTextEdit && this._expandBtn) { + this.group.add(this._expandBtn) + } + const { width, height } = this + let { paddingX, paddingY } = this.getPaddingVale() + const halfBorderWidth = this.getBorderWidth() / 2 + paddingX += this.shapePadding.paddingX + halfBorderWidth + paddingY += this.shapePadding.paddingY + halfBorderWidth + // 节点形状 + this.shapeNode = this.shapeInstance.createShape() + this.shapeNode.addClass('smm-node-shape') + this.shapeNode.translate(halfBorderWidth, halfBorderWidth) + this.style.shape(this.shapeNode) + this.group.add(this.shapeNode) + // 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示 + this.renderExpandBtnPlaceholderRect() + // 创建协同头像节点 + if (this.createUserListNode) this.createUserListNode() + // 概要节点添加一个带所属节点id的类名 + if (this.isGeneralization && this.generalizationBelongNode) { + this.group.addClass('generalization_' + this.generalizationBelongNode.uid) + } + // 激活hover和激活边框 + const addHoverNode = () => { + this.hoverNode = new Rect() + .size(width + hoverRectPadding * 2, height + hoverRectPadding * 2) + .x(-hoverRectPadding) + .y(-hoverRectPadding) + this.hoverNode.addClass('smm-hover-node') + this.style.hoverNode(this.hoverNode, width, height) + this.group.add(this.hoverNode) + } + // 如果存在自定义节点内容,那么使用自定义节点内容 + if (this.isUseCustomNodeContent()) { + const foreignObject = createForeignObjectNode({ + el: this._customNodeContent, + width, + height + }) + this.group.add(foreignObject) + addHoverNode() + return + } + const imgPlacement = this.getStyle('imgPlacement') || 'top' + const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM + let { textContentWidth, textContentHeight, textContentWidthWithoutTag } = + this._rectInfo + const textContentHeightWithTag = textContentHeight + // 如果存在显示在文本下方的标签,那么非标签内容的整体高度需要减去标签高度 + let totalTagWidth = 0 + let maxTagHeight = 0 + const hasTagContent = this._tagData && this._tagData.length > 0 + if (hasTagContent) { + const res = this.getTagContentSize(textContentMargin) + totalTagWidth = res.width + maxTagHeight = res.height + if (tagIsBottom) { + textContentHeight -= maxTagHeight + textContentMargin + } + } + // 图片节点 + let imgWidth = 0 + let imgHeight = 0 + if (this._imgData) { + imgWidth = this._imgData.width + imgHeight = this._imgData.height + this.group.add(this._imgData.node) + switch (imgPlacement) { + case 'top': + this._imgData.node.cx(width / 2).y(paddingY) + break + case 'bottom': + this._imgData.node.cx(width / 2).y(height - paddingY - imgHeight) + break + case 'left': + this._imgData.node.x(paddingX).cy(height / 2) + break + case 'right': + this._imgData.node.x(width - paddingX - imgWidth).cy(height / 2) + break + default: + break + } + } + // 内容节点 + let textContentNested = new G() + let textContentOffsetX = 0 + if (hasTagContent && tagIsBottom) { + textContentOffsetX = + textContentWidthWithoutTag < textContentWidth + ? (textContentWidth - textContentWidthWithoutTag) / 2 + : 0 + } + // 库前置内容 + this.mindMap.nodeInnerPrefixList.forEach(item => { + const itemData = this[`_${item.name}Data`] + if (itemData) { + itemData.node + .x(textContentOffsetX) + .y((textContentHeight - itemData.height) / 2) + textContentNested.add(itemData.node) + textContentOffsetX += itemData.width + textContentMargin + } + }) + // 自定义前置内容 + if (this._prefixData) { + const foreignObject = createForeignObjectNode({ + el: this._prefixData.el, + width: this._prefixData.width, + height: this._prefixData.height + }) + foreignObject + .x(textContentOffsetX) + .y((textContentHeight - this._prefixData.height) / 2) + textContentNested.add(foreignObject) + textContentOffsetX += this._prefixData.width + textContentMargin + } + // icon + let iconNested = new G() + if (this._iconData && this._iconData.length > 0) { + let iconLeft = 0 + this._iconData.forEach(item => { + item.node + .x(textContentOffsetX + iconLeft) + .y((textContentHeight - item.height) / 2) + iconNested.add(item.node) + iconLeft += item.width + textContentMargin + }) + textContentNested.add(iconNested) + textContentOffsetX += iconLeft + } + // 文字 + if (this._textData) { + const oldX = this._textData.node.attr('data-offsetx') || 0 + this._textData.node.attr('data-offsetx', textContentOffsetX) + // 修复safari浏览器节点存在图标时文字位置不正确的问题 + ;(this._textData.nodeContent || this._textData.node) + .x(-oldX) // 修复非富文本模式下同时存在图标和换行的文本时,被收起和展开时图标与文字距离会逐渐拉大的问题 + .x(textContentOffsetX) + .y((textContentHeight - this._textData.height) / 2) + // 如果开启了文本编辑实时渲染,需要判断当前渲染的节点是否是正在编辑的节点,是的话将透明度设置为0不显示 + if (openRealtimeRenderOnNodeTextEdit) { + this._textData.node.opacity( + this.mindMap.renderer.textEdit.getCurrentEditNode() === this ? 0 : 1 + ) + } + textContentNested.add(this._textData.node) + textContentOffsetX += this._textData.width + textContentMargin + } + // 超链接 + if (this._hyperlinkData) { + this._hyperlinkData.node + .x(textContentOffsetX) + .y((textContentHeight - this._hyperlinkData.height) / 2) + textContentNested.add(this._hyperlinkData.node) + textContentOffsetX += this._hyperlinkData.width + textContentMargin + } + // 标签 + let tagNested = new G() + if (hasTagContent) { + if (tagIsBottom) { + // 标签显示在文字下方 + let tagLeft = 0 + this._tagData.forEach(item => { + item.node.x(tagLeft).y((maxTagHeight - item.height) / 2) + tagNested.add(item.node) + tagLeft += item.width + textContentMargin + }) + tagNested + .x((textContentWidth - totalTagWidth) / 2) + .y(textContentHeightWithTag - maxTagHeight) + textContentNested.add(tagNested) + } else { + // 标签显示在文字右侧 + let tagLeft = 0 + this._tagData.forEach(item => { + item.node + .x(textContentOffsetX + tagLeft) + .y((textContentHeight - item.height) / 2) + tagNested.add(item.node) + tagLeft += item.width + textContentMargin + }) + textContentNested.add(tagNested) + textContentOffsetX += tagLeft + } + } + // 备注 + if (this._noteData) { + this._noteData.node + .x(textContentOffsetX) + .y((textContentHeight - this._noteData.height) / 2) + textContentNested.add(this._noteData.node) + textContentOffsetX += this._noteData.width + textContentMargin + } + // 附件 + if (this._attachmentData) { + this._attachmentData.node + .x(textContentOffsetX) + .y((textContentHeight - this._attachmentData.height) / 2) + textContentNested.add(this._attachmentData.node) + textContentOffsetX += this._attachmentData.width + textContentMargin + } + // 自定义后置内容 + if (this._postfixData) { + const foreignObject = createForeignObjectNode({ + el: this._postfixData.el, + width: this._postfixData.width, + height: this._postfixData.height + }) + foreignObject + .x(textContentOffsetX) + .y((textContentHeight - this._postfixData.height) / 2) + textContentNested.add(foreignObject) + textContentOffsetX += this._postfixData.width + } + this.group.add(textContentNested) + // 文字内容整体 + const { width: bboxWidth, height: bboxHeight } = textContentNested.bbox() + let translateX = 0 + let translateY = 0 + switch (imgPlacement) { + case 'top': + translateX = width / 2 - bboxWidth / 2 + translateY = + paddingY + // 内边距 + imgHeight + // 图片高度 + this.getImgTextMarin('v', 0, 0, imgHeight, textContentHeightWithTag) // 和图片的间距 + break + case 'bottom': + translateX = width / 2 - bboxWidth / 2 + translateY = paddingY + break + case 'left': + translateX = + imgWidth + + paddingX + + this.getImgTextMarin('h', imgWidth, textContentWidth) + translateY = height / 2 - bboxHeight / 2 + break + case 'right': + translateX = paddingX + translateY = height / 2 - bboxHeight / 2 + break + } + textContentNested.translate(translateX, translateY) + addHoverNode() + this.mindMap.emit('node_layout_end', this) +} + +export default { + getImgTextMarin, + getTagContentSize, + getNodeRect, + layout +} diff --git a/simple-mind-map/src/theme/default.js b/simple-mind-map/src/theme/default.js index fbf1aa51..f41cfa08 100644 --- a/simple-mind-map/src/theme/default.js +++ b/simple-mind-map/src/theme/default.js @@ -95,7 +95,9 @@ export default { // 点鼠标hover和激活时显示的矩形边框的圆角大小 hoverRectRadius: 5, // 文本对齐 - align: 'left' + align: 'left', + // 图片放置位置 + imgPlacement: 'top' // 下列样式也支持给节点设置,用于覆盖最外层的设置 // paddingX, // paddingY, @@ -131,7 +133,8 @@ export default { lineMarkerDir: 'end', hoverRectColor: '', hoverRectRadius: 5, - textAlign: 'left' + textAlign: 'left', + imgPlacement: 'top' }, // 三级及以下节点样式 node: { @@ -157,7 +160,8 @@ export default { lineMarkerDir: 'end', hoverRectColor: '', hoverRectRadius: 5, - textAlign: 'left' + textAlign: 'left', + imgPlacement: 'top' }, // 概要节点样式 generalization: { @@ -182,7 +186,8 @@ export default { endDir: [1, 0], hoverRectColor: '', hoverRectRadius: 5, - textAlign: 'left' + textAlign: 'left', + imgPlacement: 'top' } }