import { resizeImgSize, removeHtmlStyle, addHtmlStyle, checkIsRichText, isUndef, createForeignObjectNode, addXmlns, generateColorByContent } from '../../../utils' import { Image as SVGImage, SVG, A, G, Rect, Text } from '@svgdotjs/svg.js' import iconsSvg from '../../../svg/icons' import { CONSTANTS, noneRichTextNodeLineHeight } from '../../../constants/constant' // 测量svg文本宽高 const measureText = (text, style) => { const g = new G() const node = new Text().text(text) style.text(node) g.add(node) return g.bbox() } // 标签默认的样式 const defaultTagStyle = { radius: 3, // 标签矩形的圆角大小 fontSize: 12, // 字号,建议文字高度不要大于height fill: '', // 标签矩形的背景颜色 height: 20, // 标签矩形的高度 paddingX: 8 // 水平内边距,如果设置了width,将忽略该配置 //width: 30 // 标签矩形的宽度,如果不设置,默认以文字的宽度+paddingX*2为宽度 } // 创建图片节点 function createImgNode() { const img = this.getData('image') if (!img) { return } const imgSize = this.getImgShowSize() const node = new SVGImage().load(img).size(...imgSize) // 如果指定了加载失败显示的图片,那么加载一下图片检测是否失败 const { defaultNodeImage } = this.mindMap.opt if (defaultNodeImage) { const imgEl = new Image() imgEl.onerror = () => { node.load(defaultNodeImage) } imgEl.src = img } if (this.getData('imageTitle')) { node.attr('title', this.getData('imageTitle')) } node.on('dblclick', e => { this.mindMap.emit('node_img_dblclick', this, e) }) node.on('mouseenter', e => { this.mindMap.emit('node_img_mouseenter', this, node, e) }) node.on('mouseleave', e => { this.mindMap.emit('node_img_mouseleave', this, node, e) }) node.on('mousemove', e => { this.mindMap.emit('node_img_mousemove', this, node, e) }) return { node, width: imgSize[0], height: imgSize[1] } } // 获取图片显示宽高 function getImgShowSize() { const { custom, width, height } = this.getData('imageSize') // 如果是自定义了图片的宽高,那么不受最大宽高限制 if (custom) return [width, height] return resizeImgSize( width, height, this.mindMap.themeConfig.imgMaxWidth, this.mindMap.themeConfig.imgMaxHeight ) } // 创建icon节点 function createIconNode() { let _data = this.getData() if (!_data.icon || _data.icon.length <= 0) { return [] } let iconSize = this.mindMap.themeConfig.iconSize return _data.icon.map(item => { let src = iconsSvg.getNodeIconListIcon( item, this.mindMap.opt.iconList || [] ) let node = null // svg图标 if (/^ { this.mindMap.emit('node_icon_click', this, item, e, node) }) node.on('mouseenter', e => { this.mindMap.emit('node_icon_mouseenter', this, item, e, node) }) node.on('mouseleave', e => { this.mindMap.emit('node_icon_mouseleave', this, item, e, node) }) return { node, width: iconSize, height: iconSize } }) } // 创建富文本节点 function createRichTextNode(specifyText) { const hasCustomWidth = this.hasCustomWidth() let text = typeof specifyText === 'string' ? specifyText : this.getData('text') let { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt textAutoWrapWidth = hasCustomWidth ? this.customTextWidth : textAutoWrapWidth let g = new G() // 重新设置富文本节点内容 let recoverText = false if (this.getData('resetRichText')) { delete this.nodeData.data.resetRichText recoverText = true } if ([CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) { // // 如果自定义过样式则不允许覆盖 // if (!this.hasCustomStyle() ) { recoverText = true // } } if (recoverText && !isUndef(text)) { // 判断节点内容是否是富文本 let isRichText = checkIsRichText(text) // 获取自定义样式 let customStyle = this.getCustomStyle() // 样式字符串 let style = this.style.createStyleText(customStyle) if (isRichText) { // 如果是富文本那么线移除内联样式 text = removeHtmlStyle(text) // 再添加新的内联样式 let _text = text text = addHtmlStyle(text, 'span', style) // 给span添加样式没有成功,则尝试给strong标签添加样式 if (text === _text) { text = addHtmlStyle(text, 'strong', style) } // 给strong添加样式没有成功,则尝试给s标签添加样式 if (text === _text) { text = addHtmlStyle(text, 's', style) } // 给s添加样式没有成功,则尝试给em标签添加样式 if (text === _text) { text = addHtmlStyle(text, 'em', style) } } else { // 非富文本 text = `

${text}

` } this.setData({ text: text }) } let html = `
${text}
` if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) { this.mindMap.commonCaches.measureRichtextNodeTextSizeEl = document.createElement('div') this.mindMap.commonCaches.measureRichtextNodeTextSizeEl.style.position = 'fixed' this.mindMap.commonCaches.measureRichtextNodeTextSizeEl.style.left = '-999999px' this.mindMap.el.appendChild( this.mindMap.commonCaches.measureRichtextNodeTextSizeEl ) } let div = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl div.innerHTML = html let el = div.children[0] 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) { div.innerHTML = `

${emptyTextMeasureHeightText}

` let elTmp = div.children[0] elTmp.classList.add('smm-richtext-node-wrap') height = elTmp.getBoundingClientRect().height div.innerHTML = html } width = Math.min(Math.ceil(width) + 1, textAutoWrapWidth) // 修复getBoundingClientRect方法对实际宽度是小数的元素获取到的值是整数,导致宽度不够文本发生换行的问题 height = Math.ceil(height) g.attr('data-width', width) g.attr('data-height', height) const foreignObject = createForeignObjectNode({ el: div.children[0], width, height }) g.add(foreignObject) return { node: g, nodeContent: foreignObject, width, height } } // 创建文本节点 function createTextNode(specifyText) { if (this.getData('richText')) { return this.createRichTextNode(specifyText) } const text = typeof specifyText === 'string' ? specifyText : this.getData('text') if (this.getData('resetRichText')) { delete this.nodeData.data.resetRichText } let g = new G() let fontSize = this.getStyle('fontSize', false) // 文本超长自动换行 let textArr = [] if (!isUndef(text)) { textArr = String(text).split(/\n/gim) } const { textAutoWrapWidth: maxWidth, emptyTextMeasureHeightText } = this.mindMap.opt let isMultiLine = textArr.length > 1 textArr.forEach((item, index) => { let arr = item.split('') let lines = [] let line = [] while (arr.length) { let str = arr.shift() let text = [...line, str].join('') if (measureText(text, this.style).width <= maxWidth) { line.push(str) } else { lines.push(line.join('')) line = [str] } } if (line.length > 0) { lines.push(line.join('')) } if (lines.length > 1) { isMultiLine = true } textArr[index] = lines.join('\n') }) textArr = textArr.join('\n').split(/\n/gim) textArr.forEach((item, index) => { const node = new Text().text(item) node.addClass('smm-text-node-wrap') this.style.text(node) node.y( fontSize * noneRichTextNodeLineHeight * index + ((noneRichTextNodeLineHeight - 1) * fontSize) / 2 ) 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) g.attr('data-height', height) g.attr('data-ismultiLine', isMultiLine || textArr.length > 1) return { node: g, width, height } } // 创建超链接节点 function createHyperlinkNode() { let { hyperlink, hyperlinkTitle } = this.getData() if (!hyperlink) { return } const { customHyperlinkJump } = this.mindMap.opt let iconSize = this.mindMap.themeConfig.iconSize let node = new SVG().size(iconSize, iconSize) // 超链接节点 let a = new A().to(hyperlink).target('_blank') a.node.addEventListener('click', e => { if (typeof customHyperlinkJump === 'function') { e.preventDefault() customHyperlinkJump(hyperlink, this) } }) if (hyperlinkTitle) { node.add(SVG(`${hyperlinkTitle}`)) } // 添加一个透明的层,作为鼠标区域 a.rect(iconSize, iconSize).fill({ color: 'transparent' }) // 超链接图标 let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize) this.style.iconNode(iconNode) a.add(iconNode) node.add(a) return { node, width: iconSize, height: iconSize } } // 创建标签节点 function createTagNode() { const tagData = this.getData('tag') if (!tagData || tagData.length <= 0) { return [] } let { maxTag, tagsColorMap } = this.mindMap.opt tagsColorMap = tagsColorMap || {} const nodes = [] tagData.slice(0, maxTag).forEach((item, index) => { let str = '' let style = { ...defaultTagStyle } // 旧版只支持字符串类型 if (typeof item === 'string') { str = item } else { // v0.10.3+版本支持对象类型 str = item.text style = { ...defaultTagStyle, ...item.style } } // 是否手动设置了标签宽度 const hasCustomWidth = typeof style.width !== 'undefined' // 创建容器节点 const tag = new G() tag.on('click', () => { this.mindMap.emit('node_tag_click', this, item, index, tag) }) // 标签文本 const text = new Text().text(str) this.style.tagText(text, style) // 获取文本宽高 const { width: textWidth, height: textHeight } = text.bbox() // 矩形宽度 const rectWidth = hasCustomWidth ? style.width : textWidth + style.paddingX * 2 // 取文本和矩形最大宽高作为标签宽高 const maxWidth = hasCustomWidth ? Math.max(rectWidth, textWidth) : rectWidth const maxHeight = Math.max(style.height, textHeight) // 文本居中 if (hasCustomWidth) { text.x((maxWidth - textWidth) / 2) } else { text.x(hasCustomWidth ? 0 : style.paddingX) } text.cy(-maxHeight / 2) // 标签矩形 const rect = new Rect().size(rectWidth, style.height).cy(-maxHeight / 2) if (hasCustomWidth) { rect.x((maxWidth - rectWidth) / 2) } this.style.tagRect(rect, { ...style, fill: style.fill || // 优先节点自身配置 tagsColorMap[text.node.textContent] || // 否则尝试从实例化选项tagsColorMap映射中获取颜色 generateColorByContent(text.node.textContent) // 否则按照标签内容生成 }) tag.add(rect).add(text) nodes.push({ node: tag, width: maxWidth, height: maxHeight }) }) return nodes } // 创建备注节点 function createNoteNode() { if (!this.getData('note')) { return null } let iconSize = this.mindMap.themeConfig.iconSize let node = new SVG() .attr('cursor', 'pointer') .addClass('smm-node-note') .size(iconSize, iconSize) // 透明的层,用来作为鼠标区域 node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' })) // 备注图标 let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize) this.style.iconNode(iconNode) node.add(iconNode) // 备注tooltip if (!this.mindMap.opt.customNoteContentShow) { if (!this.noteEl) { this.noteEl = document.createElement('div') this.noteEl.style.cssText = ` position: fixed; padding: 10px; border-radius: 5px; box-shadow: 0 2px 5px rgb(0 0 0 / 10%); display: none; background-color: #fff; z-index: ${this.mindMap.opt.nodeNoteTooltipZIndex} ` const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body targetNode.appendChild(this.noteEl) } this.noteEl.innerText = this.getData('note') } node.on('mouseover', () => { const { left, top } = this.getNoteContentPosition() if (!this.mindMap.opt.customNoteContentShow) { this.noteEl.style.left = left + 'px' this.noteEl.style.top = top + 'px' this.noteEl.style.display = 'block' } else { this.mindMap.opt.customNoteContentShow.show( this.getData('note'), left, top, this ) } }) node.on('mouseout', () => { if (!this.mindMap.opt.customNoteContentShow) { this.noteEl.style.display = 'none' } else { this.mindMap.opt.customNoteContentShow.hide() } }) node.on('click', e => { this.mindMap.emit('node_note_click', this, e, node) }) return { node, width: iconSize, height: iconSize } } // 创建附件节点 function createAttachmentNode() { const { attachmentUrl, attachmentName } = this.getData() if (!attachmentUrl) { return } const iconSize = this.mindMap.themeConfig.iconSize const node = new SVG().attr('cursor', 'pointer').size(iconSize, iconSize) if (attachmentName) { node.add(SVG(`${attachmentName}`)) } // 透明的层,用来作为鼠标区域 node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' })) // 备注图标 const iconNode = SVG(iconsSvg.attachment).size(iconSize, iconSize) this.style.iconNode(iconNode) node.add(iconNode) node.on('click', e => { this.mindMap.emit('node_attachmentClick', this, e, node) }) node.on('contextmenu', e => { this.mindMap.emit('node_attachmentContextmenu', this, e, node) }) return { node, width: iconSize, height: iconSize } } // 获取节点备注显示位置 function getNoteContentPosition() { const iconSize = this.mindMap.themeConfig.iconSize const { scaleY } = this.mindMap.view.getTransformData().transform const iconSizeAddScale = iconSize * scaleY let { left, top } = this._noteData.node.node.getBoundingClientRect() top += iconSizeAddScale return { left, top } } // 测量自定义节点内容元素的宽高 function measureCustomNodeContentSize(content) { if (!this.mindMap.commonCaches.measureCustomNodeContentSizeEl) { this.mindMap.commonCaches.measureCustomNodeContentSizeEl = document.createElement('div') this.mindMap.commonCaches.measureCustomNodeContentSizeEl.style.cssText = ` position: fixed; left: -99999px; top: -99999px; ` this.mindMap.el.appendChild( this.mindMap.commonCaches.measureCustomNodeContentSizeEl ) } this.mindMap.commonCaches.measureCustomNodeContentSizeEl.innerHTML = '' this.mindMap.commonCaches.measureCustomNodeContentSizeEl.appendChild(content) let rect = this.mindMap.commonCaches.measureCustomNodeContentSizeEl.getBoundingClientRect() return { width: rect.width, height: rect.height } } // 是否使用的是自定义节点内容 function isUseCustomNodeContent() { return !!this._customNodeContent } export default { createImgNode, getImgShowSize, createIconNode, createRichTextNode, createTextNode, createHyperlinkNode, createTagNode, createNoteNode, createAttachmentNode, getNoteContentPosition, measureCustomNodeContentSize, isUseCustomNodeContent }