mind-map/simple-mind-map/src/core/render/node/nodeLayout.js

457 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { TAG_PLACEMENT, IMG_PLACEMENT } = CONSTANTS
const { textContentMargin } = this.mindMap.opt
const tagPlacement = this.getStyle('tagPlacement') || TAG_PLACEMENT.RIGHT
const tagIsBottom = tagPlacement === TAG_PLACEMENT.BOTTOM
const imgPlacement = this.getStyle('imgPlacement') || IMG_PLACEMENT.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 ([IMG_PLACEMENT.TOP, IMG_PLACEMENT.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,
openRealtimeRenderOnNodeTextEdit,
textContentMargin,
addCustomContentToNode
} = 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 { IMG_PLACEMENT, TAG_PLACEMENT } = CONSTANTS
const imgPlacement = this.getStyle('imgPlacement') || IMG_PLACEMENT.TOP
const tagPlacement = this.getStyle('tagPlacement') || TAG_PLACEMENT.RIGHT
const tagIsBottom = tagPlacement === TAG_PLACEMENT.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 IMG_PLACEMENT.TOP:
this._imgData.node.cx(width / 2).y(paddingY)
break
case IMG_PLACEMENT.BOTTOM:
this._imgData.node.cx(width / 2).y(height - paddingY - imgHeight)
break
case IMG_PLACEMENT.LEFT:
this._imgData.node.x(paddingX).cy(height / 2)
break
case IMG_PLACEMENT.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 IMG_PLACEMENT.TOP:
translateX = width / 2 - bboxWidth / 2
translateY =
paddingY + // 内边距
imgHeight + // 图片高度
this.getImgTextMarin('v', 0, 0, imgHeight, textContentHeightWithTag) // 和图片的间距
break
case IMG_PLACEMENT.BOTTOM:
translateX = width / 2 - bboxWidth / 2
translateY = paddingY
break
case IMG_PLACEMENT.LEFT:
translateX =
imgWidth +
paddingX +
this.getImgTextMarin('h', imgWidth, textContentWidth)
translateY = height / 2 - bboxHeight / 2
break
case IMG_PLACEMENT.RIGHT:
translateX = paddingX
translateY = height / 2 - bboxHeight / 2
break
}
textContentNested.translate(translateX, translateY)
addHoverNode()
if (this._customContentAddToNodeAdd && this._customContentAddToNodeAdd.el) {
const foreignObject = createForeignObjectNode(
this._customContentAddToNodeAdd
)
this.group.add(foreignObject)
if (
addCustomContentToNode &&
typeof addCustomContentToNode.handle === 'function'
) {
addCustomContentToNode.handle({
content: this._customContentAddToNodeAdd,
element: foreignObject,
node: this
})
}
}
this.mindMap.emit('node_layout_end', this)
}
export default {
getImgTextMarin,
getTagContentSize,
getNodeRect,
layout
}