BreakChange:重构富文本渲染逻辑

This commit is contained in:
街角小林
2024-12-20 16:45:35 +08:00
parent 71f92c985f
commit f8f126e8de
8 changed files with 170 additions and 281 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.12.2",
"version": "0.13.0",
"description": "一个简单的web在线思维导图",
"authors": [
{

View File

@@ -3,7 +3,6 @@ export const CONSTANTS = {
CHANGE_THEME: 'changeTheme',
CHANGE_LAYOUT: 'changeLayout',
SET_DATA: 'setData',
TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode',
MODE: {
READONLY: 'readonly',
EDIT: 'edit'
@@ -157,7 +156,7 @@ export const nodeDataNoStylePropList = [
'isActive',
'generalization',
'richText',
'resetRichText',
'resetRichText',// 重新创建富文本内容,去掉原有样式
'uid',
'activeStyle',
'associativeLineTargets',
@@ -174,7 +173,8 @@ export const nodeDataNoStylePropList = [
'customTop',
'customTextWidth',
'checkbox',
'dir'
'dir',
'needUpdate'// 重新创建节点内容
]
// 错误类型
@@ -226,3 +226,13 @@ export const selfCloseTagList = [
// 非富文本模式下的节点文本行高
export const noneRichTextNodeLineHeight = 1.2
// 富文本支持的样式列表
export const richTextSupportStyleList = [
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
]

View File

@@ -30,7 +30,6 @@ import {
createSmmFormatData,
checkSmmFormatData,
checkIsNodeStyleDataKey,
removeRichTextStyes,
formatGetNodeGeneralization,
sortNodeList,
throttle,
@@ -547,13 +546,6 @@ class Render {
if (this.reRender) {
this.reRender = false
}
// 触发一次保存,因为修改了渲染树的数据
if (
this.hasRichTextPlugin() &&
[CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)
) {
this.mindMap.command.addHistory()
}
}
this.mindMap.emit('node_tree_render_end')
})
@@ -561,13 +553,14 @@ class Render {
this.emitNodeActiveEvent()
}
// 给当前被收起来的节点数据添加文本复位标志
// 给当前被收起来的节点数据添加更新标志
resetUnExpandNodeStyle() {
if (!this.renderTree || !this.hasRichTextPlugin()) return
if (!this.renderTree) return
walk(this.renderTree, null, node => {
if (!node.data.expand) {
walk(node, null, node2 => {
node2.data.resetRichText = true
// 主要是触发数据新旧对比,不一样则会重新创建节点
node2.data['needUpdate'] = true
})
return true
}
@@ -750,11 +743,10 @@ class Render {
richText: isRichText,
isActive: focusNewNode // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
}
if (isRichText) params.resetRichText = isRichText
if (isRichText) params.resetRichText = true
// 动态指定的子节点数据也需要添加相关属性
appointChildren = addDataToAppointNodes(appointChildren, {
...params
})
appointChildren = addDataToAppointNodes(appointChildren, params)
const alreadyIsRichText = appointData && appointData.richText
list.forEach(node => {
if (node.isGeneralization || node.isRoot) {
return
@@ -767,6 +759,10 @@ class Render {
: defaultInsertBelowSecondLevelNodeText
// 计算插入位置
const index = getNodeDataIndex(node)
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNodeData = {
inserting,
data: {
@@ -802,7 +798,7 @@ class Render {
richText: isRichText,
isActive: focusNewNode
}
if (isRichText) params.resetRichText = isRichText
if (isRichText) params.resetRichText = true
nodeList = addDataToAppointNodes(nodeList, params)
list.forEach(node => {
if (node.isGeneralization || node.isRoot) {
@@ -848,11 +844,10 @@ class Render {
richText: isRichText,
isActive: focusNewNode
}
if (isRichText) params.resetRichText = isRichText
if (isRichText) params.resetRichText = true
// 动态指定的子节点数据也需要添加相关属性
appointChildren = addDataToAppointNodes(appointChildren, {
...params
})
appointChildren = addDataToAppointNodes(appointChildren, params)
const alreadyIsRichText = appointData && appointData.richText
list.forEach(node => {
if (node.isGeneralization) {
return
@@ -863,6 +858,10 @@ class Render {
const text = node.isRoot
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNode = {
inserting,
data: {
@@ -902,7 +901,7 @@ class Render {
richText: isRichText,
isActive: focusNewNode
}
if (isRichText) params.resetRichText = isRichText
if (isRichText) params.resetRichText = true
childList = addDataToAppointNodes(childList, params)
list.forEach(node => {
if (node.isGeneralization) {
@@ -947,7 +946,8 @@ class Render {
richText: isRichText,
isActive: focusNewNode
}
if (isRichText) params.resetRichText = isRichText
if (isRichText) params.resetRichText = true
const alreadyIsRichText = appointData && appointData.richText
list.forEach(node => {
if (node.isGeneralization || node.isRoot) {
return
@@ -956,6 +956,10 @@ class Render {
node.layerIndex === 1
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNode = {
inserting,
data: {
@@ -966,11 +970,6 @@ class Render {
},
children: [node.nodeData]
}
if (isRichText) {
node.setData({
resetRichText: true
})
}
const parent = node.parent
// 获取当前节点所在位置
const index = getNodeDataIndex(node)
@@ -1046,7 +1045,6 @@ class Render {
const index = getNodeIndexInNodeList(node, parent.children)
const parentIndex = getNodeIndexInNodeList(parent, grandpa.children)
// 节点数据
this.checkNodeLayerChange(node, parent)
parent.nodeData.children.splice(index, 1)
grandpa.nodeData.children.splice(parentIndex + 1, 0, node.nodeData)
this.mindMap.render()
@@ -1061,10 +1059,10 @@ class Render {
delete nodeData[key]
}
})
// 如果是富文本,那么还要处理富文本内容
if (hasCustomStyles && this.hasRichTextPlugin()) {
// 如果是富文本,那么直接全部重新创建,因为有些样式是通过标签来渲染的
if (this.hasRichTextPlugin()) {
hasCustomStyles = true
nodeData.resetRichText = true
nodeData.text = removeRichTextStyes(nodeData.text)
}
return hasCustomStyles
}
@@ -1302,7 +1300,6 @@ class Render {
nodeList.reverse()
}
nodeList.forEach(item => {
this.checkNodeLayerChange(item, exist)
// 移动节点
let nodeParent = item.parent
let nodeBorthers = nodeParent.children
@@ -1329,25 +1326,6 @@ class Render {
this.mindMap.render()
}
// 如果是富文本模式,那么某些层级变化需要更新样式
checkNodeLayerChange(node, toNode, toNodeIsParent = false) {
if (this.hasRichTextPlugin()) {
// 如果设置了自定义样式那么不需要更新
if (this.mindMap.richText.checkNodeHasCustomRichTextStyle(node)) {
return
}
const toIndex = toNodeIsParent ? toNode.layerIndex + 1 : toNode.layerIndex
let nodeLayerChanged =
(node.layerIndex === 1 && toIndex !== 1) ||
(node.layerIndex !== 1 && toIndex === 1)
if (nodeLayerChanged) {
node.setData({
resetRichText: true
})
}
}
}
// 移除节点
removeNode(appointNodes = []) {
appointNodes = formatDataToArray(appointNodes)
@@ -1531,7 +1509,6 @@ class Render {
return !item.isRoot
})
nodeList.forEach(item => {
this.checkNodeLayerChange(item, toNode, true)
this.removeNodeFromActiveList(item)
removeFromParentNodeData(item)
toNode.setData({
@@ -1558,18 +1535,7 @@ class Render {
node.nodeData.children.push(
...data.map(item => {
const newData = simpleDeepClone(item)
createUidForAppointNodes([newData], true, node => {
// 可能跨层级复制,那么富文本样式需要更新
if (this.hasRichTextPlugin()) {
// 如果设置了自定义样式那么不需要更新
if (
this.mindMap.richText.checkNodeHasCustomRichTextStyle(node.data)
) {
return
}
node.data.resetRichText = true
}
})
createUidForAppointNodes([newData], true)
return newData
})
)
@@ -1582,13 +1548,6 @@ class Render {
const data = {
[prop]: value
}
// 如果开启了富文本,则需要应用到富文本上
if (
this.hasRichTextPlugin() &&
this.mindMap.richText.isHasRichTextStyle(data)
) {
data.resetRichText = true
}
this.setNodeDataRender(node, data)
// 更新了连线的样式
if (lineStyleProps.includes(prop)) {
@@ -1599,13 +1558,6 @@ class Render {
// 设置节点多个样式
setNodeStyles(node, style) {
const data = { ...style }
// 如果开启了富文本,则需要应用到富文本上
if (
this.hasRichTextPlugin() &&
this.mindMap.richText.isHasRichTextStyle(data)
) {
data.resetRichText = true
}
this.setNodeDataRender(node, data)
// 更新了连线的样式
let props = Object.keys(style)
@@ -1826,6 +1778,7 @@ class Render {
list.length > 1
)
let needRender = false
const alreadyIsRichText = data && data.richText
list.forEach(item => {
const newData = {
inserting,
@@ -1837,7 +1790,7 @@ class Render {
richText: isRichText,
isActive: focusNewNode
}
if (isRichText) newData.resetRichText = isRichText
if (isRichText && !alreadyIsRichText) newData.resetRichText = isRichText
let generalization = item.node.getData('generalization')
generalization = generalization
? Array.isArray(generalization)

View File

@@ -1,19 +1,17 @@
import {
resizeImgSize,
removeHtmlStyle,
addHtmlStyle,
removeRichTextStyes,
checkIsRichText,
isUndef,
createForeignObjectNode,
addXmlns,
generateColorByContent
generateColorByContent,
camelCaseToHyphen,
getNodeRichTextStyles
} 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'
import { noneRichTextNodeLineHeight } from '../../../constants/constant'
// 测量svg文本宽高
const measureText = (text, style) => {
@@ -124,20 +122,6 @@ function createIconNode() {
})
}
// 尝试给html指定标签添加内联样式
function tryAddHtmlStyle(text, style) {
const tagList = ['span', 'strong', 's', 'em', 'u']
// let _text = text
// for (let i = 0; i < tagList.length; i++) {
// text = addHtmlStyle(text, tagList[i], style)
// if (text !== _text) {
// break
// }
// }
// return text
return addHtmlStyle(text, tagList, style)
}
// 创建富文本节点
function createRichTextNode(specifyText) {
const hasCustomWidth = this.hasCustomWidth()
@@ -145,40 +129,32 @@ function createRichTextNode(specifyText) {
typeof specifyText === 'string' ? specifyText : this.getData('text')
let { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt
textAutoWrapWidth = hasCustomWidth ? this.customTextWidth : textAutoWrapWidth
let g = new G()
// 重新设置富文本节点内容
const 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)) {
// 判断节点内容是否是富文本
const isRichText = checkIsRichText(text)
// 获取自定义样式
const customStyle = this.style.getCustomStyle()
// 样式字符串
const style = this.style.createStyleText(customStyle)
if (isRichText) {
// 如果是富文本那么线移除内联样式
text = removeHtmlStyle(text)
// 再添加新的内联样式
text = this.tryAddHtmlStyle(text, style)
if (checkIsRichText(text)) {
// 如果是富文本那么移除内联样式
text = removeRichTextStyes(text)
} else {
// 非富文本
text = `<p><span style="${style}">${text}</span></p>`
// 非富文本则改为富文本结构
text = `<p>${text}</p>`
}
this.setData({
text: text
text
})
}
let html = `<div>${text}</div>`
// 节点的富文本样式数据
const nodeTextStyleList = []
const nodeRichTextStyles = getNodeRichTextStyles(this)
Object.keys(nodeRichTextStyles).forEach(prop => {
nodeTextStyleList.push([prop, nodeRichTextStyles[prop]])
})
// 测量文本大小
if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) {
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl =
document.createElement('div')
@@ -190,9 +166,15 @@ function createRichTextNode(specifyText) {
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
)
}
let div = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
const div = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
// 应用节点的文本样式
nodeTextStyleList.forEach(([prop, value]) => {
div.style[prop] = value
})
div.style.lineHeight = 1.2
const html = `<div>${text}</div>`
div.innerHTML = html
let el = div.children[0]
const el = div.children[0]
el.classList.add('smm-richtext-node-wrap')
addXmlns(el)
el.style.maxWidth = textAutoWrapWidth + 'px'
@@ -219,6 +201,15 @@ function createRichTextNode(specifyText) {
width,
height
})
// 应用节点文本样式
// 进入文本编辑时,这个样式也会同样添加到文本编辑框的元素上
const foreignObjectStyle = {
'line-height': 1.2
}
nodeTextStyleList.forEach(([prop, value]) => {
foreignObjectStyle[camelCaseToHyphen(prop)] = value
})
foreignObject.css(foreignObjectStyle)
g.add(foreignObject)
return {
node: g,
@@ -230,6 +221,10 @@ function createRichTextNode(specifyText) {
// 创建文本节点
function createTextNode(specifyText) {
if (this.getData('needUpdate')) {
delete this.nodeData.data.needUpdate
}
// 如果是富文本内容,那么转给富文本函数
if (this.getData('richText')) {
return this.createRichTextNode(specifyText)
}
@@ -556,7 +551,6 @@ export default {
createImgNode,
getImgShowSize,
createIconNode,
tryAddHtmlStyle,
createRichTextNode,
createTextNode,
createHyperlinkNode,

View File

@@ -49,10 +49,7 @@ class Base {
// 检查当前来源是否需要重新计算节点大小
checkIsNeedResizeSources() {
return [
CONSTANTS.CHANGE_THEME,
CONSTANTS.TRANSFORM_TO_NORMAL_NODE
].includes(this.renderer.renderSource)
return [CONSTANTS.CHANGE_THEME].includes(this.renderer.renderSource)
}
// 层级类型改变
@@ -140,6 +137,7 @@ class Base {
isNodeDataChange ||
isLayerTypeChange ||
newNode.getData('resetRichText') ||
newNode.getData('needUpdate') ||
isNodeInnerPrefixChange
) {
newNode.getSize()
@@ -193,6 +191,7 @@ class Base {
isNodeDataChange ||
isLayerTypeChange ||
newNode.getData('resetRichText') ||
newNode.getData('needUpdate') ||
isNodeInnerPrefixChange
) {
newNode.getSize()

View File

@@ -6,11 +6,11 @@ import {
getTextFromHtml,
isUndef,
checkSmmFormatData,
removeHtmlNodeByClass,
formatGetNodeGeneralization,
nodeRichTextToTextWithWrap
nodeRichTextToTextWithWrap,
getNodeRichTextStyles
} from '../utils'
import { CONSTANTS } from '../constants/constant'
import { CONSTANTS, richTextSupportStyleList } from '../constants/constant'
import MindMapNode from '../core/render/node/MindMapNode'
import { Scope } from 'parchment'
@@ -53,27 +53,15 @@ class RichText {
this.isInserting = false
this.styleEl = null
this.cacheEditingText = ''
this.lostStyle = false
this.isCompositing = false
this.textNodePaddingX = 6
this.textNodePaddingY = 4
this.supportStyleProps = [
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
]
this.initOpt()
this.extendQuill()
this.appendCss()
this.bindEvent()
// 处理数据,转成富文本格式
if (this.mindMap.opt.data) {
this.mindMap.opt.data = this.handleSetData(this.mindMap.opt.data)
}
this.handleDataToRichTextOnInit()
}
// 绑定事件
@@ -107,10 +95,6 @@ class RichText {
word-break: break-all;
user-select: none;
}
.smm-richtext-node-wrap p {
font-family: auto;
}
`
)
let cssText = `
@@ -118,7 +102,7 @@ class RichText {
overflow: hidden;
padding: 0;
height: auto;
line-height: normal;
line-height: 1.2;
-webkit-user-select: text;
}
@@ -130,10 +114,6 @@ class RichText {
.ql-container.ql-snow {
border: none;
}
.smm-richtext-node-edit-wrap p {
font-family: auto;
}
`
this.styleEl = document.createElement('style')
this.styleEl.type = 'text/css'
@@ -238,6 +218,7 @@ class RichText {
outline: none;
word-break: break-all;
padding: ${paddingY}px ${paddingX}px;
line-height: 1.2;
`
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
@@ -253,6 +234,7 @@ class RichText {
const targetNode = customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
this.addNodeTextStyleToTextEditNode(node)
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
this.textEditNode.style.zIndex = nodeTextEditZIndex
@@ -277,13 +259,8 @@ class RichText {
const isEmptyText = isUndef(nodeText)
// 是否是非空的非富文本
const noneEmptyNoneRichText = !node.getData('richText') && !isEmptyText
// 如果是空文本,那么设置为丢失样式状态,否则输入不会带上样式
if (isEmptyText) {
this.lostStyle = true
}
if (isFromKeyDown && autoEmptyTextWhenKeydownEnterEdit) {
this.textEditNode.innerHTML = ''
this.lostStyle = true
} else if (noneEmptyNoneRichText) {
// 还不是富文本
let text = String(nodeText).split(/\n/gim).join('<br>')
@@ -303,10 +280,6 @@ class RichText {
this.focus(
isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null
)
if (noneEmptyNoneRichText) {
// 如果是非富文本的情况,需要手动应用文本样式
this.setTextStyleIfNotRichText(node)
}
this.cacheEditingText = ''
}
@@ -325,6 +298,14 @@ class RichText {
: '0 0 20px rgba(0,0,0,.5)'
}
// 将指定节点的文本样式添加到编辑框元素上
addNodeTextStyleToTextEditNode(node) {
const style = getNodeRichTextStyles(node)
Object.keys(style).forEach(prop => {
this.textEditNode.style[prop] = style[prop]
})
}
// 更新文本编辑框的大小和位置
updateTextEditNode() {
if (!this.node) return
@@ -346,20 +327,6 @@ class RichText {
targetNode.removeChild(this.textEditNode)
}
// 如果是非富文本的情况,需要手动应用文本样式
setTextStyleIfNotRichText(node) {
let style = {
font: node.style.merge('fontFamily'),
color: node.style.merge('color'),
italic: node.style.merge('fontStyle') === 'italic',
bold: node.style.merge('fontWeight') === 'bold',
size: node.style.merge('fontSize') + 'px',
underline: node.style.merge('textDecoration') === 'underline',
strike: node.style.merge('textDecoration') === 'line-through'
}
this.pureFormatAllText(style)
}
// 获取当前正在编辑的内容
getEditText() {
// https://github.com/slab/quill/issues/4509
@@ -374,18 +341,6 @@ class RichText {
// return html.replace(/<p><br><\/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) {
@@ -395,8 +350,7 @@ class RichText {
if (typeof beforeHideRichTextEdit === 'function') {
beforeHideRichTextEdit(this)
}
let html = this.getEditText()
html = this.sortHtmlNodeStyles(html)
const html = this.getEditText()
const list = nodes && nodes.length > 0 ? nodes : [this.node]
const node = this.node
this.textEditNode.style.display = 'none'
@@ -536,18 +490,6 @@ class RichText {
}
})
this.quill.on('text-change', () => {
let contents = this.quill.getContents()
let len = contents.ops.length
// 如果编辑过程中删除所有字符,那么会丢失主题的样式
if (len <= 0 || (len === 1 && contents.ops[0].insert === '\n')) {
this.lostStyle = true
// 需要删除节点的样式数据
this.syncFormatToNodeConfig(null, true)
} else if (this.lostStyle && !this.isCompositing) {
// 如果处于样式丢失状态,那么需要进行格式化加回样式
this.setTextStyleIfNotRichText(this.node)
this.lostStyle = false
}
this.mindMap.emit('node_text_edit_change', {
node: this.node,
text: this.getEditText(),
@@ -638,10 +580,6 @@ class RichText {
return
}
this.isCompositing = false
if (!this.lostStyle) {
return
}
this.setTextStyleIfNotRichText(this.node)
}
// 选中全部
@@ -651,16 +589,15 @@ class RichText {
// 聚焦
focus(start) {
let len = this.quill.getLength()
const len = this.quill.getLength()
this.quill.setSelection(typeof start === 'number' ? start : len, len)
}
// 格式化当前选中的文本
formatText(config = {}, clear = false, pure = false) {
formatText(config = {}, clear = false) {
if (!this.range && !this.lastRange) return
if (!pure) this.syncFormatToNodeConfig(config, clear)
let rangeLost = !this.range
let range = rangeLost ? this.lastRange : this.range
const rangeLost = !this.range
const range = rangeLost ? this.lastRange : this.range
clear
? this.quill.removeFormat(range.index, range.length)
: this.quill.formatText(range.index, range.length, config)
@@ -671,56 +608,25 @@ class RichText {
// 清除当前选中文本的样式
removeFormat() {
// 先移除全部样式
this.formatText({}, true)
// 再将样式恢复为当前主题改节点的默认样式
const style = {}
if (this.node) {
this.supportStyleProps.forEach(key => {
style[key] = this.node.style.merge(key)
})
}
const config = this.normalStyleToRichTextStyle(style)
this.formatText(config, false, true)
}
// 格式化指定范围的文本
formatRangeText(range, config = {}) {
if (!range) return
this.syncFormatToNodeConfig(config)
this.quill.formatText(range.index, range.length, config)
}
// 格式化所有文本
formatAllText(config = {}) {
this.syncFormatToNodeConfig(config)
this.pureFormatAllText(config)
}
// 纯粹的格式化所有文本
pureFormatAllText(config = {}) {
this.quill.formatText(0, this.quill.getLength(), config)
}
// 同步格式化到节点样式配置
syncFormatToNodeConfig(config, clear) {
if (!this.node) return
if (clear) {
// 清除文本样式
this.supportStyleProps.forEach(prop => {
delete this.node.nodeData.data[prop]
})
} else {
let data = this.richTextStyleToNormalStyle(config)
this.mindMap.execCommand('SET_NODE_DATA', this.node, data)
}
}
// 将普通节点样式对象转换成富文本样式对象
normalStyleToRichTextStyle(style) {
let config = {}
const config = {}
Object.keys(style).forEach(prop => {
let value = style[prop]
const value = style[prop]
switch (prop) {
case 'fontFamily':
config.font = value
@@ -750,9 +656,9 @@ class RichText {
// 将富文本样式对象转换成普通节点样式对象
richTextStyleToNormalStyle(config) {
let data = {}
const data = {}
Object.keys(config).forEach(prop => {
let value = config[prop]
const value = config[prop]
switch (prop) {
case 'font':
data.fontFamily = value
@@ -787,39 +693,50 @@ class RichText {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (this.supportStyleProps.includes(key)) {
if (richTextSupportStyleList.includes(key)) {
return true
}
}
return false
}
// 给未激活的节点设置富文本样式
setNotActiveNodeStyle(node, style) {
const config = this.normalStyleToRichTextStyle(style)
if (Object.keys(config).length > 0) {
this.showEditText({ node })
this.formatAllText(config)
this.hideEditText([node])
}
}
// 检查指定节点是否存在自定义的富文本样式
checkNodeHasCustomRichTextStyle(node) {
const nodeData = node instanceof MindMapNode ? node.getData() : node
for (let i = 0; i < this.supportStyleProps.length; i++) {
if (nodeData[this.supportStyleProps[i]] !== undefined) {
for (let i = 0; i < richTextSupportStyleList.length; i++) {
if (nodeData[richTextSupportStyleList[i]] !== undefined) {
return true
}
}
return false
}
// 转换数据后的渲染操作
afterHandleData() {
// 清空历史数据,并且触发数据变化
this.mindMap.command.clearHistory()
this.mindMap.command.addHistory()
this.mindMap.render()
}
// 插件实例化时处理思维导图数据,转换为富文本数据
handleDataToRichTextOnInit() {
// 处理数据,转成富文本格式
if (this.mindMap.renderer.renderTree) {
// 如果已经存在渲染树了,那么直接更新渲染树,并且触发重新渲染
this.handleSetData(this.mindMap.renderer.renderTree)
this.afterHandleData()
} else if (this.mindMap.opt.data) {
this.handleSetData(this.mindMap.opt.data)
}
}
// 将所有节点转换成非富文本节点
transformAllNodesToNormalNode() {
if (!this.mindMap.renderer.renderTree) return
const renderTree = this.mindMap.renderer.renderTree
if (!renderTree) return
walk(
this.mindMap.renderer.renderTree,
renderTree,
null,
node => {
if (node.data.richText) {
@@ -840,15 +757,12 @@ class RichText {
0,
0
)
// 清空历史数据,并且触发数据变化
this.mindMap.command.clearHistory()
this.mindMap.command.addHistory()
this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE)
this.afterHandleData()
}
// 处理导入数据
handleSetData(data) {
let walk = root => {
const walk = root => {
if (root.data && !root.data.richText) {
root.data.richText = true
root.data.resetRichText = true
@@ -857,8 +771,10 @@ class RichText {
if (root.data) {
const generalizationList = formatGetNodeGeneralization(root.data)
generalizationList.forEach(item => {
item.richText = true
item.resetRichText = true
if (!item.richText) {
item.richText = true
item.resetRichText = true
}
})
}
if (root.children && root.children.length > 0) {

View File

@@ -228,7 +228,7 @@ class Search {
const keep = replaceText.includes(this.searchText)
const text = this.getReplacedText(currentNode, this.searchText, replaceText)
this.notResetSearchText = true
currentNode.setText(text, currentNode.getData('richText'), true)
currentNode.setText(text, currentNode.getData('richText'))
if (keep) {
this.updateMatchNodeList(this.matchNodeList)
return
@@ -258,18 +258,15 @@ class Search {
// 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的
const keep = replaceText.includes(this.searchText)
this.notResetSearchText = true
const hasRichTextPlugin = this.mindMap.renderer.hasRichTextPlugin()
this.matchNodeList.forEach(node => {
const text = this.getReplacedText(node, this.searchText, replaceText)
if (this.isNodeInstance(node)) {
const data = {
text
}
if (hasRichTextPlugin) data.resetRichText = !!node.getData('richText')
this.mindMap.renderer.setNodeDataRender(node, data, true)
} else {
node.data.text = text
if (hasRichTextPlugin) node.data.resetRichText = !!node.data.richText
}
})
this.mindMap.render()

View File

@@ -1,7 +1,8 @@
import { v4 as uuidv4 } from 'uuid'
import {
nodeDataNoStylePropList,
selfCloseTagList
selfCloseTagList,
richTextSupportStyleList
} from '../constants/constant'
import MersenneTwister from './mersenneTwister'
import { ForeignObject } from '@svgdotjs/svg.js'
@@ -481,7 +482,7 @@ export const loadImage = imgFile => {
// 移除字符串中的html实体
export const removeHTMLEntities = str => {
;[['&nbsp;', '&#160;']].forEach(item => {
[['&nbsp;', '&#160;']].forEach(item => {
str = str.replaceAll(item[0], item[1])
})
return str
@@ -949,6 +950,12 @@ export const selectAllInput = el => {
// 给指定的节点列表树数据添加附加数据,会修改原数据
export const addDataToAppointNodes = (appointNodes, data = {}) => {
data = { ...data }
const alreadyIsRichText = data && data.richText
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && data.resetRichText) {
delete data.resetRichText
}
const walk = list => {
list.forEach(node => {
node.data = {
@@ -1027,7 +1034,7 @@ export const generateColorByContent = str => {
// html转义
export const htmlEscape = str => {
;[
[
['&', '&amp;'],
['<', '&lt;'],
['>', '&gt;']
@@ -1639,3 +1646,16 @@ export const mergeTheme = (dest, source) => {
}
})
}
// 获取节点实例的文本样式数据
export const getNodeRichTextStyles = node => {
const res = {}
richTextSupportStyleList.forEach(prop => {
let value = node.style.merge(prop)
if (prop === 'fontSize') {
value = value + 'px'
}
res[prop] = value
})
return res
}