mirror of
https://github.com/wanglin2/mind-map.git
synced 2026-02-22 00:50:43 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5688bb6821 | ||
|
|
c9d0b6c916 | ||
|
|
5a116d952a | ||
|
|
5717b4fa1d | ||
|
|
e04a5d4a6f | ||
|
|
a7d97065c6 | ||
|
|
4332abce4d | ||
|
|
f71b47b215 | ||
|
|
656cfa50c6 | ||
|
|
f10f8e0610 | ||
|
|
7533599cac | ||
|
|
f34de3acd9 | ||
|
|
4a5501f7a3 | ||
|
|
f52fd2ff48 | ||
|
|
628a6b72a2 | ||
|
|
3642763301 | ||
|
|
11c6fa3e45 | ||
|
|
d85210372d | ||
|
|
62e02ae956 | ||
|
|
799b46c68e | ||
|
|
6479841dee | ||
|
|
f1622e1a15 | ||
|
|
f490ac6f8d | ||
|
|
c342fbbe75 | ||
|
|
dde085b54e | ||
|
|
f8f126e8de | ||
|
|
71f92c985f | ||
|
|
3f2b5be4aa | ||
|
|
07712e7ac3 |
File diff suppressed because one or more lines are too long
2
dist/js/app.js
vendored
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
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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'// 重新创建节点内容
|
||||
]
|
||||
|
||||
// 错误类型
|
||||
@@ -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'
|
||||
]
|
||||
|
||||
@@ -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插件】
|
||||
// 多选节点时鼠标移动到边缘时的画布移动偏移量
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
// 获取编辑区域的背景填充
|
||||
|
||||
@@ -160,6 +160,8 @@ class MindMapNode {
|
||||
}
|
||||
// 初始化
|
||||
this.getSize()
|
||||
// 初始需要计算一下概要节点的大小,否则计算布局时获取不到概要的大小
|
||||
this.updateGeneralization()
|
||||
this.initDragHandle()
|
||||
}
|
||||
|
||||
|
||||
@@ -270,9 +270,9 @@ class Style {
|
||||
}
|
||||
|
||||
// 内置图标
|
||||
iconNode(node) {
|
||||
iconNode(node, color) {
|
||||
node.attr({
|
||||
fill: this.merge('color')
|
||||
fill: color || this.merge('color')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 => {
|
||||
;[[' ', ' ']].forEach(item => {
|
||||
[[' ', ' ']].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 => {
|
||||
;[
|
||||
[
|
||||
['&', '&'],
|
||||
['<', '<'],
|
||||
['>', '>']
|
||||
@@ -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 '='
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
BIN
web/src/assets/avatar/御风.jpg
Normal file
BIN
web/src/assets/avatar/御风.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
web/src/assets/avatar/桌案.jpg
Normal file
BIN
web/src/assets/avatar/桌案.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -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'
|
||||
|
||||
@@ -75,7 +75,12 @@ export default {
|
||||
tagPositionRight: '文本右侧',
|
||||
tagPositionBottom: '文本下面',
|
||||
alwaysShowExpandBtn: '是否一直显示展开收起按钮',
|
||||
enableAutoEnterTextEditWhenKeydown: '键盘输入时自动进入文本编辑'
|
||||
enableAutoEnterTextEditWhenKeydown: '键盘输入时自动进入文本编辑',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
changeRichTextTip: '该操作会清空所有历史修改记录,并且修改思维导图数据,是否继续?',
|
||||
changeRichTextTip2: '是否切换为富文本模式?',
|
||||
changeRichTextTip3: '是否切换为非富文本模式?'
|
||||
},
|
||||
color: {
|
||||
moreColor: '更多颜色'
|
||||
|
||||
@@ -76,7 +76,12 @@ export default {
|
||||
watermarkAngle: '旋轉角度',
|
||||
watermarkTextOpacity: '文字透明度',
|
||||
watermarkTextFontSize: '字型大小',
|
||||
belowNode: '顯示在節點下方'
|
||||
belowNode: '顯示在節點下方',
|
||||
confirm: '確定',
|
||||
cancel: '取消',
|
||||
changeRichTextTip: '該操作會清空所有曆史修改記錄,並且修改思維導圖數據,是否繼續?',
|
||||
changeRichTextTip2: '是否切換爲富文本模式?',
|
||||
changeRichTextTip3: '是否切換爲非富文本模式?'
|
||||
},
|
||||
color: {
|
||||
moreColor: '更多顏色'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
},
|
||||
|
||||
// 本地配置
|
||||
|
||||
Reference in New Issue
Block a user