Compare commits

..

29 Commits

Author SHA1 Message Date
街角小林
5688bb6821 update 2024-12-31 13:38:52 +08:00
街角小林
c9d0b6c916 Doc: update 2024-12-31 11:36:34 +08:00
街角小林
5a116d952a 打包0.13.0 2024-12-30 18:01:44 +08:00
街角小林
5717b4fa1d Doc: update 2024-12-30 17:49:23 +08:00
街角小林
e04a5d4a6f Fix:修复富文本模式下编辑时减少文字,编辑器高度没有改变的问题 2024-12-30 09:30:40 +08:00
街角小林
a7d97065c6 Fix:修复非富文本模式编辑时粘贴文本不会触发node_text_edit_change事件的问题 2024-12-30 09:25:14 +08:00
街角小林
4332abce4d Fix:优化开启原地编辑模式下且不使用富文本text编辑时输入框定位会抖动的问题 2024-12-27 16:53:06 +08:00
街角小林
f71b47b215 Feat:新增自定义节点备注、超链接、附件图标样式的实例化选项 2024-12-27 09:30:51 +08:00
街角小林
656cfa50c6 Fix:修复复制多个节点然后连续粘贴会导致布局混乱的问题 2024-12-26 19:19:16 +08:00
街角小林
f10f8e0610 Fix:修改http和https协议下的粘贴行为不一致的问题 2024-12-26 18:55:04 +08:00
街角小林
7533599cac Fix:修复copyNodeTree工具方法克隆节点对象的数据时会把节点实例的属性也克隆的问题 2024-12-26 18:25:16 +08:00
街角小林
f34de3acd9 Feat:编辑文本实时更新增加防抖操作,避免快速输入时不必要的计算 2024-12-26 17:54:12 +08:00
街角小林
4a5501f7a3 Fix:修复初始渲染时概要会重叠的问题 2024-12-26 17:52:54 +08:00
街角小林
f52fd2ff48 Fix:禁止展开收起按钮的文字可被选中 2024-12-25 18:50:14 +08:00
街角小林
628a6b72a2 Feat:富文本插件增加对老版本数据的处理 2024-12-25 18:28:00 +08:00
街角小林
3642763301 Feat:节点数据增加字段,保存当前库的版本号 2024-12-25 09:57:50 +08:00
街角小林
11c6fa3e45 Feat:copyRenderTree和copyNodeTree两个工具方法支持保留节点data和children以外的其他字段 2024-12-25 09:57:04 +08:00
街角小林
d85210372d Demo:去除不必要的代码 2024-12-25 09:17:15 +08:00
街角小林
62e02ae956 Feat:在中文输入法时激活节点,直接输入进入编辑时忽略第一个按键的值 2024-12-24 09:44:39 +08:00
街角小林
799b46c68e Fix:修复节点处于编辑状态时给节点添加图标等内容时会导致图标和编辑框重叠的问题 2024-12-23 18:12:40 +08:00
街角小林
6479841dee Fix:修复代码错误,注册了富文本插件后进入文本编辑时不应在原有文本编辑类中保存节点实例 2024-12-23 18:02:52 +08:00
街角小林
f1622e1a15 Fix:修复多选节点时双击其中某个节点进入编辑,再双击其中另一个节点时会导致该节点显示空白的问题 2024-12-23 17:42:13 +08:00
街角小林
f490ac6f8d Fix:修复只读模式下连续输入不同文字搜索,上一个搜索到的高亮节点没有清除的问题 2024-12-23 09:33:00 +08:00
街角小林
c342fbbe75 Fix:修复粘贴<a格式的文本生成节点后,文本显示&lt;a的问题 2024-12-20 17:25:12 +08:00
街角小林
dde085b54e Fix:修复非富文本模式下文本编辑时不能粘贴<a格式的文本的问题 2024-12-20 17:02:25 +08:00
街角小林
f8f126e8de BreakChange:重构富文本渲染逻辑 2024-12-20 16:45:35 +08:00
街角小林
71f92c985f Demo:切换是否开启富文本的设置增加二次提示 2024-12-20 15:47:57 +08:00
街角小林
3f2b5be4aa Fix:修复开启实时渲染特性时节点存在数学公式的时候进入文本编辑,节点大小没有适应的问题 2024-12-20 15:44:07 +08:00
街角小林
07712e7ac3 Fix:修复富文本模式下,处于节点文本编辑中时通过鼠标滚轮缩放画布时,节点文本会消失的问题 2024-12-20 15:42:46 +08:00
30 changed files with 1217 additions and 816 deletions

1166
README.md

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

2
dist/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,7 @@
})
} catch (error) {
console.log(error)
}</script><link href="dist/css/chunk-vendors.css?f5839763ea0d5c47d80a" rel="stylesheet"><link href="dist/css/app.css?f5839763ea0d5c47d80a" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
}</script><link href="dist/css/chunk-vendors.css?1cdf74fc7be543987c91" rel="stylesheet"><link href="dist/css/app.css?1cdf74fc7be543987c91" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
@@ -74,4 +74,4 @@
// 可以通过window.$bus.$on()来监听应用的一些事件
// 实例化页面
window.initApp()
}</script><script src="dist/js/chunk-vendors.js?f5839763ea0d5c47d80a"></script><script src="dist/js/app.js?f5839763ea0d5c47d80a"></script></body></html>
}</script><script src="dist/js/chunk-vendors.js?1cdf74fc7be543987c91"></script><script src="dist/js/app.js?1cdf74fc7be543987c91"></script></body></html>

View File

@@ -30,7 +30,7 @@ MindMap.markdown = markdown
MindMap.iconList = icons.nodeIconList
MindMap.constants = constants
MindMap.defaultTheme = defaultTheme
MindMap.version = '0.12.2'
MindMap.version = '0.13.0'
MindMap.usePlugin(MiniMap)
.usePlugin(Watermark)

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'// 重新创建节点内容
]
// 错误类型
@@ -208,7 +208,7 @@ export const cssContent = `
stroke-width: 2;
}
.smm-text-node-wrap {
.smm-text-node-wrap, .smm-expand-btn-text {
user-select: none;
}
`
@@ -226,3 +226,13 @@ export const selfCloseTagList = [
// 非富文本模式下的节点文本行高
export const noneRichTextNodeLineHeight = 1.2
// 富文本支持的样式列表
export const richTextSupportStyleList = [
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
]

View File

@@ -134,7 +134,7 @@ export const defaultOpt = {
// 开启该特性后需要给你的输入框绑定keydown事件并禁止冒泡
enableAutoEnterTextEditWhenKeydown: false,
// 当enableAutoEnterTextEditWhenKeydown选项开启时生效当通过按键进入文本编辑时是否自动清空原有文本
autoEmptyTextWhenKeydownEnterEdit: false,
autoEmptyTextWhenKeydownEnterEdit: false,
// 自定义对剪贴板文本的处理。当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据如果你想处理其他思维导图的数据比如processon、zhixi等那么可以传递一个函数接受当前剪贴板中的文本为参数返回处理后的数据可以返回两种类型
/*
1.返回一个纯文本,那么会直接以该文本创建一个子节点
@@ -268,6 +268,30 @@ export const defaultOpt = {
// 实例化完后是否立刻进行一次历史数据入栈操作
// 即调用mindMap.command.addHistory方法
addHistoryOnInit: true,
// 自定义节点备注图标
noteIcon: {
icon: '', // svg字符串如果不是确定要使用svg自带的样式否则请去除其中的fill等样式属性
style: {
// size: 20,// 图标大小不手动设置则会使用主题的iconSize配置
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
}
},
// 自定义节点超链接图标
hyperlinkIcon: {
icon: '', // svg字符串如果不是确定要使用svg自带的样式否则请去除其中的fill等样式属性
style: {
// size: 20,// 图标大小不手动设置则会使用主题的iconSize配置
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
}
},
// 自定义节点附件图标
attachmentIcon: {
icon: '', // svg字符串如果不是确定要使用svg自带的样式否则请去除其中的fill等样式属性
style: {
// size: 20,// 图标大小不手动设置则会使用主题的iconSize配置
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
}
},
// 【Select插件】
// 多选节点时鼠标移动到边缘时的画布移动偏移量

View File

@@ -6,6 +6,7 @@ import {
transformTreeDataToObject
} from '../../utils'
import { ERROR_TYPES } from '../../constants/constant'
import pkg from '../../../package.json'
// 命令类
class Command {
@@ -172,7 +173,9 @@ class Command {
// 获取渲染树数据副本
getCopyData() {
if (!this.mindMap.renderer.renderTree) return null
return copyRenderTree({}, this.mindMap.renderer.renderTree, true)
const res = copyRenderTree({}, this.mindMap.renderer.renderTree, true)
res.smmVersion = pkg.version
return res
}
// 移除节点数据中的uid

View File

@@ -30,10 +30,10 @@ import {
createSmmFormatData,
checkSmmFormatData,
checkIsNodeStyleDataKey,
removeRichTextStyes,
formatGetNodeGeneralization,
sortNodeList,
throttle,
debounce,
checkClipboardReadEnable,
isNodeNotNeedRenderData
} from '../../utils'
@@ -162,7 +162,7 @@ class Render {
this.mindMap.on('view_data_change', onViewDataChange)
}
// 文本编辑时实时更新节点大小
this.onNodeTextEditChange = this.onNodeTextEditChange.bind(this)
this.onNodeTextEditChange = debounce(this.onNodeTextEditChange, 100, this)
if (openRealtimeRenderOnNodeTextEdit) {
this.mindMap.on('node_text_edit_change', this.onNodeTextEditChange)
}
@@ -547,13 +547,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 +554,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,15 +744,16 @@ 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
let createNewId = false
list.forEach(node => {
if (node.isGeneralization || node.isRoot) {
return
}
appointChildren = simpleDeepClone(appointChildren)
const parent = node.parent
const isOneLayer = node.layerIndex === 1
// 新插入节点的默认文本
@@ -767,6 +762,10 @@ class Render {
: defaultInsertBelowSecondLevelNodeText
// 计算插入位置
const index = getNodeDataIndex(node)
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNodeData = {
inserting,
data: {
@@ -775,8 +774,9 @@ class Render {
uid: createUid(),
...(appointData || {})
},
children: [...createUidForAppointNodes(appointChildren)]
children: [...createUidForAppointNodes(appointChildren, createNewId)]
}
createNewId = true
parent.nodeData.children.splice(index + 1, 0, newNodeData)
})
// 如果同时对多个节点插入子节点,需要清除原来激活的节点
@@ -802,16 +802,19 @@ class Render {
richText: isRichText,
isActive: focusNewNode
}
if (isRichText) params.resetRichText = isRichText
if (isRichText) params.resetRichText = true
nodeList = addDataToAppointNodes(nodeList, params)
let createNewId = false
list.forEach(node => {
if (node.isGeneralization || node.isRoot) {
return
}
nodeList = simpleDeepClone(nodeList)
const parent = node.parent
// 计算插入位置
const index = getNodeDataIndex(node)
const newNodeList = createUidForAppointNodes(simpleDeepClone(nodeList))
const newNodeList = createUidForAppointNodes(nodeList, createNewId)
createNewId = true
parent.nodeData.children.splice(index + 1, 0, ...newNodeList)
})
if (focusNewNode) {
@@ -848,21 +851,26 @@ 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
let createNewId = false
list.forEach(node => {
if (node.isGeneralization) {
return
}
appointChildren = simpleDeepClone(appointChildren)
if (!node.nodeData.children) {
node.nodeData.children = []
}
const text = node.isRoot
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNode = {
inserting,
data: {
@@ -871,8 +879,9 @@ class Render {
...params,
...(appointData || {})
},
children: [...createUidForAppointNodes(appointChildren)]
children: [...createUidForAppointNodes(appointChildren, createNewId)]
}
createNewId = true
node.nodeData.children.push(newNode)
// 插入子节点时自动展开子节点
node.setData({
@@ -902,16 +911,20 @@ class Render {
richText: isRichText,
isActive: focusNewNode
}
if (isRichText) params.resetRichText = isRichText
if (isRichText) params.resetRichText = true
childList = addDataToAppointNodes(childList, params)
let createNewId = false
list.forEach(node => {
if (node.isGeneralization) {
return
}
childList = simpleDeepClone(childList)
if (!node.nodeData.children) {
node.nodeData.children = []
}
childList = createUidForAppointNodes(childList)
childList = createUidForAppointNodes(childList, createNewId)
// 第一个引用不需要重新创建uid后面的需要重新创建否则id会重复
createNewId = true
node.nodeData.children.push(...childList)
// 插入子节点时自动展开子节点
node.setData({
@@ -947,7 +960,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 +970,10 @@ class Render {
node.layerIndex === 1
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNode = {
inserting,
data: {
@@ -966,11 +984,6 @@ class Render {
},
children: [node.nodeData]
}
if (isRichText) {
node.setData({
resetRichText: true
})
}
const parent = node.parent
// 获取当前节点所在位置
const index = getNodeDataIndex(node)
@@ -1046,7 +1059,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 +1073,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
}
@@ -1208,7 +1220,10 @@ class Render {
Array.isArray(smmData) ? smmData : [smmData]
)
} else {
text = htmlEscape(text)
// 如果是富文本模式,那么需要转义特殊字符
if (this.hasRichTextPlugin()) {
text = htmlEscape(text)
}
const textArr = text
.split(new RegExp('\r?\n|(?<!\n)\r', 'g'))
.filter(item => {
@@ -1302,7 +1317,6 @@ class Render {
nodeList.reverse()
}
nodeList.forEach(item => {
this.checkNodeLayerChange(item, exist)
// 移动节点
let nodeParent = item.parent
let nodeBorthers = nodeParent.children
@@ -1329,25 +1343,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 +1526,6 @@ class Render {
return !item.isRoot
})
nodeList.forEach(item => {
this.checkNodeLayerChange(item, toNode, true)
this.removeNodeFromActiveList(item)
removeFromParentNodeData(item)
toNode.setData({
@@ -1546,35 +1540,7 @@ class Render {
// 粘贴节点到节点
pasteNode(data) {
data = formatDataToArray(data)
if (this.activeNodeList.length <= 0 || data.length <= 0) {
return
}
this.activeNodeList.forEach(node => {
// 概要节点不允许添加下级节点
if (node.isGeneralization) return
node.setData({
expand: true
})
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
}
})
return newData
})
)
})
this.mindMap.render()
this.mindMap.execCommand('INSERT_MULTI_CHILD_NODE', [], data)
}
// 设置节点样式
@@ -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

@@ -33,6 +33,7 @@ export default class TextEdit {
this.hasBodyMousedown = false
this.textNodePaddingX = 5
this.textNodePaddingY = 3
this.isNeedUpdateTextEditNode = false
this.bindEvent()
}
@@ -91,7 +92,7 @@ export default class TextEdit {
})
})
this.mindMap.on('scale', this.onScale)
// // 监听按键事件,判断是否自动进入文本编辑模式
// 监听按键事件,判断是否自动进入文本编辑模式
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
window.addEventListener('keydown', this.onKeydown)
}
@@ -124,6 +125,18 @@ export default class TextEdit {
]('keydown', this.onKeydown)
}
})
// 正在编辑文本时,给节点添加了图标等其他内容时需要更新编辑框的位置
this.mindMap.on('afterExecCommand', () => {
if (!this.isShowTextEdit()) return
this.isNeedUpdateTextEditNode = true
})
this.mindMap.on('node_tree_render_end', () => {
if (!this.isShowTextEdit()) return
if (this.isNeedUpdateTextEditNode) {
this.isNeedUpdateTextEditNode = false
this.updateTextEditNode()
}
})
}
// 解绑事件
@@ -139,6 +152,9 @@ export default class TextEdit {
const node = activeNodeList[0]
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
if (node && this.checkIsAutoEnterTextEditKey(e)) {
// 忽略第一个键值,避免中文输入法时进入编辑会导致第一个键值变成字母的问题
// 带来的问题是按的第一下纯粹是进入文本编辑,但没有变成输入
e.preventDefault()
this.show({
node,
e,
@@ -161,7 +177,6 @@ export default class TextEdit {
// 注册临时快捷键
registerTmpShortcut() {
// 注册回车快捷键
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
@@ -178,7 +193,7 @@ export default class TextEdit {
return this.showTextEdit
}
// 显示文本编辑框
// 显示文本编辑框
// isInserting是否是刚创建的节点
// isFromKeyDown是否是在按键事件进入的编辑
async show({
@@ -191,6 +206,11 @@ export default class TextEdit {
if (node.isUseCustomNodeContent()) {
return
}
// 如果有正在编辑中的节点,那么先结束它
const currentEditNode = this.getCurrentEditNode()
if (currentEditNode) {
this.hideEditTextBox()
}
const { beforeTextEdit, openRealtimeRenderOnNodeTextEdit } =
this.mindMap.opt
if (typeof beforeTextEdit === 'function') {
@@ -203,10 +223,13 @@ export default class TextEdit {
}
if (!isShow) return
}
this.currentNode = node
const { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
this.mindMap.view.translateXY(offsetLeft, offsetTop)
const g = node._textData.node
// 需要先显示不然宽高获取到的可能是0
if (openRealtimeRenderOnNodeTextEdit) {
g.show()
}
const rect = g.node.getBoundingClientRect()
// 如果开启了大小实时更新,那么直接隐藏节点原文本
if (openRealtimeRenderOnNodeTextEdit) {
@@ -223,6 +246,7 @@ export default class TextEdit {
this.mindMap.richText.showEditText(params)
return
}
this.currentNode = node
this.showEditTextBox(params)
}
@@ -317,13 +341,10 @@ export default class TextEdit {
} else {
handleInputPasteText(e)
}
this.emitTextChangeEvent()
})
this.textEditNode.addEventListener('input', () => {
this.mindMap.emit('node_text_edit_change', {
node: this.currentNode,
text: this.getEditText(),
richText: false
})
this.emitTextChangeEvent()
})
const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
@@ -350,8 +371,8 @@ export default class TextEdit {
this.textEditNode.style.minWidth =
rect.width + this.textNodePaddingX * 2 + 'px'
this.textEditNode.style.minHeight = rect.height + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
this.textEditNode.style.left = Math.floor(rect.left) + 'px'
this.textEditNode.style.top = Math.floor(rect.top) + 'px'
this.textEditNode.style.display = 'block'
this.textEditNode.style.maxWidth = textAutoWrapWidth * scale + 'px'
if (isMultiLine) {
@@ -375,6 +396,15 @@ export default class TextEdit {
this.cacheEditingText = ''
}
// 派发节点文本编辑事件
emitTextChangeEvent() {
this.mindMap.emit('node_text_edit_change', {
node: this.currentNode,
text: this.getEditText(),
richText: false
})
}
// 更新文本编辑框的大小和位置
updateTextEditNode() {
if (this.mindMap.richText) {
@@ -389,8 +419,8 @@ export default class TextEdit {
rect.width + this.textNodePaddingX * 2 + 'px'
this.textEditNode.style.minHeight =
rect.height + this.textNodePaddingY * 2 + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
this.textEditNode.style.left = Math.floor(rect.left) + 'px'
this.textEditNode.style.top = Math.floor(rect.top) + 'px'
}
// 获取编辑区域的背景填充

View File

@@ -160,6 +160,8 @@ class MindMapNode {
}
// 初始化
this.getSize()
// 初始需要计算一下概要节点的大小,否则计算布局时获取不到概要的大小
this.updateGeneralization()
this.initDragHandle()
}

View File

@@ -270,9 +270,9 @@ class Style {
}
// 内置图标
iconNode(node) {
iconNode(node, color) {
node.attr({
fill: this.merge('color')
fill: color || this.merge('color')
})
}

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)
}
@@ -308,15 +303,16 @@ function createTextNode(specifyText) {
// 创建超链接节点
function createHyperlinkNode() {
let { hyperlink, hyperlinkTitle } = this.getData()
const { 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)
const { customHyperlinkJump, hyperlinkIcon } = this.mindMap.opt
const { icon, style } = hyperlinkIcon
const iconSize = this.getNodeIconSize('hyperlinkIcon')
const node = new SVG().size(iconSize, iconSize)
// 超链接节点
let a = new A().to(hyperlink).target('_blank')
const a = new A().to(hyperlink).target('_blank')
a.node.addEventListener('click', e => {
if (typeof customHyperlinkJump === 'function') {
e.preventDefault()
@@ -329,8 +325,8 @@ function createHyperlinkNode() {
// 添加一个透明的层,作为鼠标区域
a.rect(iconSize, iconSize).fill({ color: 'transparent' })
// 超链接图标
let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize)
this.style.iconNode(iconNode)
const iconNode = SVG(icon || iconsSvg.hyperlink).size(iconSize, iconSize)
this.style.iconNode(iconNode, style.color)
a.add(iconNode)
node.add(a)
return {
@@ -415,16 +411,17 @@ function createNoteNode() {
if (!this.getData('note')) {
return null
}
let iconSize = this.mindMap.themeConfig.iconSize
let node = new SVG()
const { icon, style } = this.mindMap.opt.noteIcon
const iconSize = this.getNodeIconSize('noteIcon')
const 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)
const iconNode = SVG(icon || iconsSvg.note).size(iconSize, iconSize)
this.style.iconNode(iconNode, style.color)
node.add(iconNode)
// 备注tooltip
if (!this.mindMap.opt.customNoteContentShow) {
@@ -486,7 +483,8 @@ function createAttachmentNode() {
if (!attachmentUrl) {
return
}
const iconSize = this.mindMap.themeConfig.iconSize
const iconSize = this.getNodeIconSize('attachmentIcon')
const { icon, style } = this.mindMap.opt.attachmentIcon
const node = new SVG().attr('cursor', 'pointer').size(iconSize, iconSize)
if (attachmentName) {
node.add(SVG(`<title>${attachmentName}</title>`))
@@ -494,8 +492,8 @@ function createAttachmentNode() {
// 透明的层,用来作为鼠标区域
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
// 备注图标
const iconNode = SVG(iconsSvg.attachment).size(iconSize, iconSize)
this.style.iconNode(iconNode)
const iconNode = SVG(icon || iconsSvg.attachment).size(iconSize, iconSize)
this.style.iconNode(iconNode, style.color)
node.add(iconNode)
node.on('click', e => {
this.mindMap.emit('node_attachmentClick', this, e, node)
@@ -510,9 +508,15 @@ function createAttachmentNode() {
}
}
// 获取节点图标大小
function getNodeIconSize(prop) {
const { style } = this.mindMap.opt[prop]
return isUndef(style.size) ? this.mindMap.themeConfig.iconSize : style.size
}
// 获取节点备注显示位置
function getNoteContentPosition() {
const iconSize = this.mindMap.themeConfig.iconSize
const iconSize = this.getNodeIconSize('noteIcon')
const { scaleY } = this.mindMap.view.getTransformData().transform
const iconSizeAddScale = iconSize * scaleY
let { left, top } = this._noteData.node.node.getBoundingClientRect()
@@ -556,7 +560,6 @@ export default {
createImgNode,
getImgShowSize,
createIconNode,
tryAddHtmlStyle,
createRichTextNode,
createTextNode,
createHyperlinkNode,
@@ -564,6 +567,7 @@ export default {
createNoteNode,
createAttachmentNode,
getNoteContentPosition,
getNodeIconSize,
measureCustomNodeContentSize,
isUseCustomNodeContent
}

View File

@@ -12,6 +12,7 @@ function createExpandNodeContent() {
if (this.mindMap.opt.isShowExpandNum) {
// 展开的节点
this._openExpandNode = new Text()
this._openExpandNode.addClass('smm-expand-btn-text')
// 文本垂直居中
this._openExpandNode.attr({
'text-anchor': 'middle',

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

@@ -107,14 +107,13 @@ class Formula {
// 给指定的节点插入指定公式
insertFormulaToNode(node, formula) {
let richTextPlugin = this.mindMap.richText
const richTextPlugin = this.mindMap.richText
richTextPlugin.showEditText({ node })
richTextPlugin.quill.insertEmbed(
richTextPlugin.quill.getLength() - 1,
'formula',
formula
)
richTextPlugin.setTextStyleIfNotRichText(richTextPlugin.node)
richTextPlugin.hideEditText([node])
}
@@ -127,8 +126,18 @@ class Formula {
for (const el of els)
nodeText = nodeText.replace(
el.outerHTML,
`\$${el.getAttribute('data-value')}\$`
`$${el.getAttribute('data-value')}$`
)
// 如果开启了实时渲染,那么意味公式转换为源码时会影响节点尺寸,需要派发事件触发渲染
if (this.mindMap.opt.openRealtimeRenderOnNodeTextEdit) {
setTimeout(() => {
this.mindMap.emit('node_text_edit_change', {
node: this.mindMap.richText.node,
text: this.mindMap.richText.getEditText(),
richText: true
})
}, 0)
}
}
return nodeText
}

View File

@@ -6,11 +6,13 @@ import {
getTextFromHtml,
isUndef,
checkSmmFormatData,
removeHtmlNodeByClass,
formatGetNodeGeneralization,
nodeRichTextToTextWithWrap
nodeRichTextToTextWithWrap,
getNodeRichTextStyles,
htmlEscape,
compareVersion
} 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 +55,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 +97,6 @@ class RichText {
word-break: break-all;
user-select: none;
}
.smm-richtext-node-wrap p {
font-family: auto;
}
`
)
let cssText = `
@@ -118,7 +104,7 @@ class RichText {
overflow: hidden;
padding: 0;
height: auto;
line-height: normal;
line-height: 1.2;
-webkit-user-select: text;
}
@@ -130,10 +116,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 +220,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 +236,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 +261,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>')
@@ -294,19 +273,13 @@ class RichText {
this.textEditNode.innerHTML = this.cacheEditingText || nodeText
}
this.initQuillEditor()
document.querySelector(
'.' + CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP
).style.minHeight = originHeight + 'px'
this.setQuillContainerMinHeight(originHeight)
this.showTextEdit = true
// 如果是刚创建的节点那么默认全选否则普通激活不全选除非selectTextOnEnterEditText配置为true
// 在selectTextOnEnterEditText时如果是在keydown事件进入的节点编辑也不需要全选
this.focus(
isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null
)
if (noneEmptyNoneRichText) {
// 如果是非富文本的情况,需要手动应用文本样式
this.setTextStyleIfNotRichText(node)
}
this.cacheEditingText = ''
}
@@ -325,6 +298,21 @@ 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]
})
}
// 设置quill编辑器容器的最小高度
setQuillContainerMinHeight(minHeight) {
document.querySelector(
'.' + CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP
).style.minHeight = minHeight + 'px'
}
// 更新文本编辑框的大小和位置
updateTextEditNode() {
if (!this.node) return
@@ -337,6 +325,7 @@ class RichText {
this.textEditNode.style.minHeight = originHeight + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
this.setQuillContainerMinHeight(originHeight)
}
// 删除文本编辑框元素
@@ -346,20 +335,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 +349,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 +358,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 +498,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 +588,6 @@ class RichText {
return
}
this.isCompositing = false
if (!this.lostStyle) {
return
}
this.setTextStyleIfNotRichText(this.node)
}
// 选中全部
@@ -651,16 +597,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 +616,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 +664,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 +701,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,25 +765,35 @@ class RichText {
0,
0
)
// 清空历史数据,并且触发数据变化
this.mindMap.command.clearHistory()
this.mindMap.command.addHistory()
this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE)
this.afterHandleData()
}
handleDataToRichText(data) {
const oldIsRichText = data.richText
data.richText = true
data.resetRichText = true
// 如果原本就是富文本,那么不能转换
if (!oldIsRichText) {
data.text = htmlEscape(data.text)
}
}
// 处理导入数据
handleSetData(data) {
let walk = root => {
if (root.data && !root.data.richText) {
root.data.richText = true
root.data.resetRichText = true
// 短期处理,为了兼容老数据,长期会去除
const isOldRichTextVersion =
!data.smmVersion || compareVersion(data.smmVersion, '0.13.0') === '<'
const walk = root => {
if (root.data && (!root.data.richText || isOldRichTextVersion)) {
this.handleDataToRichText(root.data)
}
// 概要
if (root.data) {
const generalizationList = formatGetNodeGeneralization(root.data)
generalizationList.forEach(item => {
item.richText = true
item.resetRichText = true
if (!item.richText || isOldRichTextVersion) {
this.handleDataToRichText(item)
}
})
}
if (root.children && root.children.length > 0) {

View File

@@ -107,6 +107,7 @@ class Search {
// 搜索匹配的节点
doSearch() {
this.clearHighlightOnReadonly()
this.updateMatchNodeList([])
this.currentIndex = -1
const { isOnlySearchCurrentRenderNodes } = this.mindMap.opt
@@ -174,14 +175,8 @@ class Search {
}
}
const { readonly } = this.mindMap.opt
// 只读模式下需要激活之前节点的高亮
if (readonly) {
this.matchNodeList.forEach(node => {
if (this.isNodeInstance(node)) {
node.closeHighlight()
}
})
}
// 只读模式下需要清除之前节点的高亮
this.clearHighlightOnReadonly()
const currentNode = this.matchNodeList[this.currentIndex]
this.notResetSearchText = true
const uid = this.isNodeInstance(currentNode)
@@ -205,6 +200,18 @@ class Search {
})
}
// 只读模式下清除现有匹配节点的高亮
clearHighlightOnReadonly() {
const { readonly } = this.mindMap.opt
if (readonly) {
this.matchNodeList.forEach(node => {
if (this.isNodeInstance(node)) {
node.closeHighlight()
}
})
}
}
// 定位到指定搜索结果索引的节点
jump(index, callback = () => {}) {
this.searchNext(callback, index)
@@ -228,7 +235,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 +265,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'
@@ -173,6 +174,12 @@ export const copyRenderTree = (tree, root, removeActiveState = false) => {
tree.children[index] = copyRenderTree({}, item, removeActiveState)
})
}
// data、children外的其他字段
Object.keys(root).forEach(key => {
if (!['data', 'children'].includes(key) && !/^_/.test(key)) {
tree[key] = root[key]
}
})
return tree
}
@@ -183,7 +190,8 @@ export const copyNodeTree = (
removeActiveState = false,
removeId = true
) => {
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data)
const rootData = root.nodeData ? root.nodeData : root
tree.data = simpleDeepClone(rootData.data)
// 移除节点uid
if (removeId) {
delete tree.data.uid
@@ -208,6 +216,12 @@ export const copyNodeTree = (
tree.children[index] = copyNodeTree({}, item, removeActiveState, removeId)
})
}
// data、children外的其他字段
Object.keys(rootData).forEach(key => {
if (!['data', 'children'].includes(key) && !/^_/.test(key)) {
tree[key] = rootData[key]
}
})
return tree
}
@@ -277,6 +291,21 @@ export const throttle = (fn, time = 300, ctx) => {
}
}
// 防抖函数
export const debounce = (fn, wait = 300, ctx) => {
let timeout = null
return (...args) => {
if (timeout) clearTimeout(timeout)
const callNow = !timeout
timeout = setTimeout(() => {
timeout = null
fn.apply(ctx, args)
}, wait)
if (callNow) fn.apply(ctx, args)
}
}
// 异步执行任务队列
export const asyncRun = (taskList, callback = () => {}) => {
let index = 0
@@ -481,7 +510,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 +978,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 +1062,7 @@ export const generateColorByContent = str => {
// html转义
export const htmlEscape = str => {
;[
[
['&', '&amp;'],
['<', '&lt;'],
['>', '&gt;']
@@ -1201,6 +1236,8 @@ export const handleInputPasteText = (e, text) => {
if (!selection.rangeCount) return
selection.deleteFromDocument()
text = text || e.clipboardData.getData('text')
// 转义特殊字符
text = htmlEscape(text)
// 去除格式
text = getTextFromHtml(text)
// 去除换行
@@ -1639,3 +1676,38 @@ 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
}
// 判断两个版本号的关系
/*
a > b 返回 >
a < b 返回 <
a = b 返回 =
*/
export const compareVersion = (a, b) => {
const aArr = String(a).split('.')
const bArr = String(b).split('.')
const max = Math.max(aArr.length, bArr.length)
for (let i = 0; i < max; i++) {
const ai = aArr[i] || 0
const bi = bArr[i] || 0
if (ai > bi) {
return '>'
} else if (ai < bi) {
return '<'
}
}
return '='
}

View File

@@ -9,24 +9,6 @@ const SIMPLE_MIND_MAP_LOCAL_CONFIG = 'SIMPLE_MIND_MAP_LOCAL_CONFIG'
let mindMapData = null
/**
* @Author: 王林
* @Date: 2021-08-02 22:36:48
* @Desc: 克隆思维导图数据,去除激活状态
*/
const copyMindMapTreeData = (tree, root) => {
if (!root) return null
tree.data = simpleDeepClone(root.data)
// tree.data.isActive = false
tree.children = []
if (root.children && root.children.length > 0) {
root.children.forEach((item, index) => {
tree.children[index] = copyMindMapTreeData({}, item)
})
}
return tree
}
/**
* @Author: 王林
* @Date: 2021-08-01 10:10:49
@@ -65,7 +47,7 @@ export const storeData = data => {
} else {
originData = getData()
}
originData.root = copyMindMapTreeData({}, data)
originData.root = data
if (window.takeOverApp) {
mindMapData = originData
window.takeOverAppMethods.saveMindMapData(originData)

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -77,7 +77,12 @@ export default {
tagPositionRight: 'Text right',
tagPositionBottom: 'Text bottom',
alwaysShowExpandBtn: 'Always show expand btn',
enableAutoEnterTextEditWhenKeydown: 'Auto enter text edit when keydown'
enableAutoEnterTextEditWhenKeydown: 'Auto enter text edit when keydown',
confirm: 'Confirm',
cancel: 'Cancel',
changeRichTextTip: 'This operation will clear all historical modification records and modify the mind map data. Do you want to continue?',
changeRichTextTip2: 'Do you want to switch to rich text mode?',
changeRichTextTip3: 'Do you want to switch to non rich text mode?'
},
color: {
moreColor: 'More color'

View File

@@ -75,7 +75,12 @@ export default {
tagPositionRight: '文本右侧',
tagPositionBottom: '文本下面',
alwaysShowExpandBtn: '是否一直显示展开收起按钮',
enableAutoEnterTextEditWhenKeydown: '键盘输入时自动进入文本编辑'
enableAutoEnterTextEditWhenKeydown: '键盘输入时自动进入文本编辑',
confirm: '确定',
cancel: '取消',
changeRichTextTip: '该操作会清空所有历史修改记录,并且修改思维导图数据,是否继续?',
changeRichTextTip2: '是否切换为富文本模式?',
changeRichTextTip3: '是否切换为非富文本模式?'
},
color: {
moreColor: '更多颜色'

View File

@@ -76,7 +76,12 @@ export default {
watermarkAngle: '旋轉角度',
watermarkTextOpacity: '文字透明度',
watermarkTextFontSize: '字型大小',
belowNode: '顯示在節點下方'
belowNode: '顯示在節點下方',
confirm: '確定',
cancel: '取消',
changeRichTextTip: '該操作會清空所有曆史修改記錄,並且修改思維導圖數據,是否繼續?',
changeRichTextTip2: '是否切換爲富文本模式?',
changeRichTextTip3: '是否切換爲非富文本模式?'
},
color: {
moreColor: '更多顏色'

View File

@@ -212,7 +212,7 @@ export default {
if (!targetNode) return
this.notHandleDataChange = true
if (richText) {
targetNode.setText(textToNodeRichTextWithWrap(text), true, true)
targetNode.setText(textToNodeRichTextWithWrap(text), true)
} else {
targetNode.setText(text)
}

View File

@@ -149,7 +149,6 @@ export default {
const richText = node.data.data.richText
const text = richText ? e.target.innerHTML : e.target.innerText
node.data.data.text = richText ? textToNodeRichTextWithWrap(text) : text
if (richText) node.data.data.resetRichText = true
node.data.textCache = e.target.innerHTML
this.save()
},
@@ -170,9 +169,6 @@ export default {
},
children: []
}
if (richText) {
data.data.resetRichText = true
}
if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault()
if (node.data.root) {

View File

@@ -214,7 +214,9 @@
<div class="rowItem">
<el-checkbox
v-model="config.enableAutoEnterTextEditWhenKeydown"
@change="updateOtherConfig('enableAutoEnterTextEditWhenKeydown', $event)"
@change="
updateOtherConfig('enableAutoEnterTextEditWhenKeydown', $event)
"
>{{ $t('setting.enableAutoEnterTextEditWhenKeydown') }}</el-checkbox
>
</div>
@@ -496,10 +498,26 @@ export default {
// 切换是否开启节点富文本编辑
enableNodeRichTextChange(e) {
this.mindMap.renderer.textEdit.hideEditTextBox()
this.setLocalConfig({
openNodeRichText: e
})
this.$confirm(
this.$t('setting.changeRichTextTip'),
e
? this.$t('setting.changeRichTextTip2')
: this.$t('setting.changeRichTextTip3'),
{
confirmButtonText: this.$t('setting.confirm'),
cancelButtonText: this.$t('setting.cancel'),
type: 'warning'
}
)
.then(() => {
this.mindMap.renderer.textEdit.hideEditTextBox()
this.setLocalConfig({
openNodeRichText: e
})
})
.catch(() => {
this.enableNodeRichText = !this.enableNodeRichText
})
},
// 本地配置