mirror of
https://github.com/wanglin2/mind-map.git
synced 2026-02-17 14:04:47 +08:00
BreakChange:重构富文本渲染逻辑
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.12.2",
|
||||
"version": "0.13.0",
|
||||
"description": "一个简单的web在线思维导图",
|
||||
"authors": [
|
||||
{
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 => {
|
||||
;[[' ', ' ']].forEach(item => {
|
||||
[[' ', ' ']].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 => {
|
||||
;[
|
||||
[
|
||||
['&', '&'],
|
||||
['<', '<'],
|
||||
['>', '>']
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user