Compare commits

..

114 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
街角小林
2d0310f675 打包0.12.2 2024-12-17 09:12:52 +08:00
街角小林
4ef99958b6 Feat:去除richTextEditFakeInPlace实例化选项 2024-12-17 09:01:25 +08:00
街角小林
ec677c781e Feat:支持调整关联线控制点位置后激活状态不丢失,提升使用体验 2024-12-16 17:20:28 +08:00
街角小林
2822dcc99a Fix:修复关联线设置激活样式时控制线条颜色没同步更新的问题 2024-12-16 17:09:29 +08:00
wanglin2
46e3d85b5f update 2024-12-14 17:54:48 +08:00
wanglin2
024271ec54 update 2024-12-14 17:38:58 +08:00
街角小林
da9fd4c36d 打包0.12.2 2024-12-13 15:19:36 +08:00
街角小林
5a291b4a5f Fix:修复添加数学公式报错的问题 2024-12-12 16:33:24 +08:00
街角小林
3f002ce2ee Feat:增加节点宽度拖拽手柄的大小 2024-12-12 09:18:31 +08:00
街角小林
78a242faff Feat:新增扩展快捷键内部键值映射对象的方法 2024-12-11 18:19:47 +08:00
街角小林
be229a0c04 Demo:支持单独设置每条关联线的样式 2024-12-11 17:54:27 +08:00
街角小林
9a36cd4478 Fix:修复关联线文本编辑时按回车键无法结束编辑的问题 2024-12-11 17:53:47 +08:00
街角小林
732b6b50b0 Feat:1.支持单独定义每条关联线的样式;2.新增取消激活关联线的事件;3.修复关联线文本存在空行时编辑框渲染异常的问题 2024-12-11 17:53:08 +08:00
街角小林
ade7a95f3c Feat:转换富文本节点数据的逻辑由Render类移至RichText插件本身 2024-12-09 18:35:31 +08:00
街角小林
322975528e Feat:新增改变[思维导图]布局结构行为的插件,使之更符合目前主流思维导图的做法 2024-12-09 18:32:54 +08:00
街角小林
2a76f5a0bc Feat:新增抛出命令执行后的事件 2024-12-09 18:10:18 +08:00
街角小林
2a7eaefac5 Feat:updateData和setData方法新增抛出更新数据前的事件 2024-12-09 18:05:39 +08:00
街角小林
68784f3e4d Feat:升级quill版本;Fix:修复富文本模式下文本输入连续多个空格会压缩成一个的问题 2024-12-09 11:38:11 +08:00
街角小林
c29477ed55 Demo:支持双击节点备注图标进入备注编辑 2024-12-06 17:43:24 +08:00
街角小林
88a6442539 Feat:新增节点备注图标双击事件 2024-12-06 17:40:19 +08:00
街角小林
24363d55a4 Feat:非富文本模式支持输入和渲染空行;Fix:修复非富文本模式下节点存在图标时,进入文本编辑时文本编辑框位置不正确的问题 2024-12-06 16:41:27 +08:00
街角小林
7fd4c7504d Feat:去掉文本编辑框元素的某些样式,避免文本模糊的问题 2024-12-06 16:39:50 +08:00
街角小林
efb4dcf236 Fix:修复非富文本模式下文本编辑实时渲染功能在一些结构中会出现异常的问题 2024-12-06 15:17:59 +08:00
街角小林
53c2af0bc0 Fix:修复只读模式实例化后再切换为编辑模式时没有将当前数据入栈的问题 2024-12-06 09:45:07 +08:00
街角小林
74d302639a Feat:新增实例化时是否进行一次历史数据入栈操作的选项 2024-12-06 09:28:20 +08:00
街角小林
0a2e4e7c14 Feat:去除富文本插件中去除最后一行空行的逻辑 2024-12-05 09:56:52 +08:00
街角小林
7d399b436b Feat:上移、下移命令支持指定操作节点 2024-12-04 18:49:23 +08:00
街角小林
f02098f697 Demo:支持配置节点连线的流动效果 2024-12-03 18:35:45 +08:00
街角小林
fbb3b47b7d Feat:修改tryAddHtmlStyle方法的逻辑 2024-12-03 17:52:23 +08:00
街角小林
508d8fe357 Feat:1.优化节点样式设置,如果设置的是连线样式不触发节点重新创建;2.优化富文本模式下同时对大量节点调用setStyle方法修改文本样式非常慢的问题 2024-11-27 19:06:14 +08:00
街角小林
7258ed9ea7 Feat:新增自定义处理节点连线元素的实例化选项 2024-11-22 16:51:56 +08:00
街角小林
6ae5d244f1 Fix:修复http协议下可以在概要节点上粘贴下级节点的问题 2024-11-20 18:31:30 +08:00
街角小林
7213348c12 Feat:调整粘贴逻辑,如果支持操作剪贴板数据,那么以剪贴板数据为准,否则以画布内数据为准 2024-11-20 18:27:38 +08:00
街角小林
93092db49f Fix:修复直线风格连线圆角不为0时某些场景会存在不必要的凸起问题 2024-11-13 09:17:13 +08:00
街角小林
b8df51eb02 Fix:修复代码缺陷和优化代码 2024-11-12 18:44:35 +08:00
街角小林
6cdc2ff526 Merge pull request #1000 from Tarrency/switchtheme-fontcolor
fix: 解决切换主题前后,设置了部分字体样式的节点,其未设置的字体样式没有响应新主题样式的bug
2024-11-12 18:34:53 +08:00
街角小林
b1303ce7a5 Fix:修复搜索内容为全部替换内容的子串时连续点击全部替换搜索结果异常的问题 2024-11-12 17:53:42 +08:00
街角小林
ac5bb1d684 Fix:修复插件文件的类型定义文件没有生成的问题 2024-11-12 16:57:13 +08:00
wangqi01
ad8cf74bba fix: 解决切换主题前后,设置了部分字体样式的节点,其未设置的字体样式没有响应新主题样式的bug 2024-11-12 16:02:58 +08:00
街角小林
4e6688e4e0 Demo:节点右键菜单新增收起所有下级节点按钮 2024-11-08 17:19:05 +08:00
街角小林
15859e76b6 Merge pull request #985 from ZhangMingZhao1/fix-richtext-list
fix: 限定富文本节点的编辑态格式类型 , 避免富文本节点编辑态下会触发渲染问题和结束编辑后的不一致问题,如有序列表(数字加点符号加空格触发)和无序列表(-符号加空格触发)
2024-11-08 09:22:19 +08:00
ZhangMingZhao1
eb89bd71e1 fix: 明确支持的指定的富文本格式渲染,避免富文本节点输入时触发其他渲染错乱问题 2024-11-07 17:16:02 +08:00
街角小林
d8a88f94d7 Feat:阻止非富文本模式时的节点文本可被选中 2024-11-07 09:41:00 +08:00
街角小林
fa7761b5a3 Feat:1.只有当按键事件的事件目标为body或节点文本编辑框元素时才响应快捷键事件;2.新增自定义响应快捷键事件的实例化选项 2024-11-07 09:33:26 +08:00
街角小林
e769c9602b Feat:只有当按键事件的事件目标为body时才允许自动进入节点文本编辑模式 2024-11-07 09:27:18 +08:00
街角小林
2078d38092 update 2024-11-07 09:08:04 +08:00
街角小林
97834086d0 Merge branch 'feature' of https://github.com/wanglin2/mind-map into feature 2024-11-07 09:07:15 +08:00
街角小林
8fe0a53ba1 Merge pull request #973 from ZhangMingZhao1/fix-scale-problem
feat: 解决缩放非整数时编辑浮层dom会出现锯齿问题
2024-11-07 09:07:08 +08:00
街角小林
20fb9c3067 update 2024-11-07 09:03:05 +08:00
街角小林
1573141f2c Merge pull request #941 from ZhangMingZhao1/fix-mousedown-style
fix: 解决设置mousedownEventPreventDefault下框选会选中节点文字闪动问题
2024-11-07 09:02:32 +08:00
街角小林
dba711c9ef Feat:mousedownEventPreventDefault选项默认值改为false、富文本模式节点文字禁止被选中 2024-11-06 19:08:21 +08:00
街角小林
be3faa0aef Feat:新增通过按键进入文本编辑时是否自动清空原有文本 2024-11-06 18:16:02 +08:00
街角小林
d93511eca4 Fix:修复富文本模式的一些情况下节点文本和编辑文本位置存在偏差的问题 2024-11-06 18:03:23 +08:00
街角小林
1d75e8519d update README 2024-11-04 17:08:41 +08:00
zhangmingzhao
02460c0642 feat: 解决缩放非整数时编辑浮层dom会出现锯齿问题 2024-11-01 19:47:05 +08:00
街角小林
a43ad7aa06 update README 2024-10-30 15:49:30 +08:00
街角小林
0041be9892 打包0.12.1 2024-10-25 10:02:17 +08:00
街角小林
7ebab0298b 打包Demo 2024-10-24 18:27:39 +08:00
街角小林
079b963ae3 Fix:修复拖拽调整节点宽度后回退操作时节点宽度没有回退的问题 2024-10-24 18:25:31 +08:00
街角小林
fb6fcd6bd3 Fix:修复当存在滚动条插件,且思维导图被限制在画布内时,演示模式中边缘节点无法正常显示的问题 2024-10-24 09:49:07 +08:00
街角小林
59950b2ba0 Demo:增加是否自动进入文本编辑的配置 2024-10-24 09:35:52 +08:00
街角小林
1f23257917 Demo:增加是否一直显示展开收起按钮的配置 2024-10-24 09:26:25 +08:00
街角小林
4e1db01f44 Demo:增加节点标签显示位置的配置 2024-10-24 09:20:34 +08:00
街角小林
38769c3b55 打包demo 2024-10-23 18:14:06 +08:00
街角小林
a4ef09779d Demo:将设置类的配置从基础样式移到单独的设置栏中 2024-10-23 18:11:48 +08:00
街角小林
4821dd6052 打包Demo 2024-10-23 09:30:19 +08:00
街角小林
f33c886d6a Feat:修复处于文本编辑中切换为只读模式时文本未保存的问题 2024-10-23 09:26:25 +08:00
街角小林
b5b8c2be60 Merge pull request #951 from ZhangMingZhao1/fix-exit-edit-pr
fix: 修复设置readonly如果处于编辑态还能编辑的问题
2024-10-23 09:18:18 +08:00
街角小林
4a7485c58e Demo:节点右键菜单新增添加待办按钮 2024-10-22 17:32:46 +08:00
街角小林
c097d20748 Feat:去除代码中对编号插件的硬编码,新增节点库前置内容的创建逻辑 2024-10-22 17:22:54 +08:00
ZhangMingZhao1
eeeae7d0e2 fix: 修复设置readonly如果处于编辑态还能编辑的问题 2024-10-22 10:49:29 +08:00
街角小林
dd3e169946 Fix:修复在节点文本编辑中调用destroy方法时setBackgroundStyle方法会报错的问题 2024-10-21 09:37:13 +08:00
街角小林
b895a58194 打包Demo 2024-10-17 18:02:25 +08:00
街角小林
2b42b9fafa Demo:支持设置节点文本编辑是否实时更新节点大小,默认开启 2024-10-17 17:56:03 +08:00
街角小林
c2125b07ca Feat:非富文本模式文本编辑框的样式同步节点样式 2024-10-17 17:54:41 +08:00
街角小林
eb342bf69b Feat:当开启openRealtimeRenderOnNodeTextEdit选项后,会去除文本编辑框的背景和阴影,达到类似原地编辑的效果 2024-10-17 13:51:17 +08:00
街角小林
a7eb66a6c9 Feat:after_update_config事件新增上一次配置的返回参数 2024-10-17 11:32:35 +08:00
街角小林
e24fd9bdbb Fix:修复开启maxImgResizeWidthInheritTheme选项后第一次上传图片拖动图片到最大值会导致后续调整按钮无法显示的问题 2024-10-17 09:34:17 +08:00
街角小林
34d7c6fed2 Fix:修复开启openRealtimeRenderOnNodeTextEdit选项后非富文本模式编辑文本时输入框会左右抖动的问题 2024-10-17 09:13:55 +08:00
街角小林
a0f88031c1 Demo:去除节点样式的行高配置 2024-10-16 18:42:38 +08:00
街角小林
889ec13dbf Feat:1.去除主题的行高配置;2.优化非富文本模式下的文本编辑效果 2024-10-16 18:40:47 +08:00
街角小林
4aa5a8c48b Fix:修复给Text文本设置的字号没有加单位导致在一些浏览器上不生效的问题 2024-10-16 17:39:25 +08:00
ZhangMingZhao1
c6a8ec257c fix: 解决设置mousedownEventPreventDefault下框选会选中节点文字闪动问题 2024-10-16 11:07:03 +08:00
街角小林
0ec20b8fa0 update 2024-10-16 10:09:46 +08:00
街角小林
f3285cf4e6 Doc update 2024-10-15 17:08:29 +08:00
68 changed files with 3725 additions and 1982 deletions

1138
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

69
dist/js/chunk-1f155ff8.js vendored Normal file

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?63f5fae2a1a38e74b08f" rel="stylesheet"><link href="dist/css/app.css?63f5fae2a1a38e74b08f" 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?63f5fae2a1a38e74b08f"></script><script src="dist/js/app.js?63f5fae2a1a38e74b08f"></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

@@ -0,0 +1,21 @@
const { exec } = require('child_process')
const fs = require('fs')
const base = './src/plugins/'
const list = fs.readdirSync(base)
const files = []
list.forEach(item => {
const stat = fs.statSync(base + item)
if (stat.isFile()) {
files.push(item)
}
})
const str = files
.map(item => {
return base + item
})
.join(' ')
exec(
`tsc ${str} --declaration --allowJs --emitDeclarationOnly --outDir types/src/ --target es2017 --skipLibCheck `
)

View File

@@ -18,6 +18,7 @@ import Formula from './src/plugins/Formula.js'
import RainbowLines from './src/plugins/RainbowLines.js'
import Demonstrate from './src/plugins/Demonstrate.js'
import OuterFrame from './src/plugins/OuterFrame.js'
import MindMapLayoutPro from './src/plugins/MindMapLayoutPro.js'
import xmind from './src/parse/xmind.js'
import markdown from './src/parse/markdown.js'
import icons from './src/svg/icons.js'
@@ -29,7 +30,7 @@ MindMap.markdown = markdown
MindMap.iconList = icons.nodeIconList
MindMap.constants = constants
MindMap.defaultTheme = defaultTheme
MindMap.version = '0.12.0'
MindMap.version = '0.13.0'
MindMap.usePlugin(MiniMap)
.usePlugin(Watermark)
@@ -50,5 +51,6 @@ MindMap.usePlugin(MiniMap)
.usePlugin(RainbowLines)
.usePlugin(Demonstrate)
.usePlugin(OuterFrame)
.usePlugin(MindMapLayoutPro)
export default MindMap

View File

@@ -56,6 +56,26 @@ class MindMap {
this.cssEl = null
this.cssTextMap = {} // 该样式在实例化时会动态添加到页面同时导出为svg时也会添加到svg源码中
// 节点前置内容列表
/*
{
name: '',// 一个唯一的类型标识
// 创建节点的显示内容:节点元素、宽高
createContent: (node) => {
return {
node: null,
width: 0,
height: 0
}
},
// 创建保存到节点实例的opt对象中的数据
createNodeData: () => {},
// 更新节点实例的opt数据返回数据是否改变了
updateNodeData: () => {},
}
*/
this.nodeInnerPrefixList = []
// 画布
this.initContainer()
@@ -103,9 +123,11 @@ class MindMap {
// 初始渲染
this.render(this.opt.fit ? () => this.view.fit() : () => {})
setTimeout(() => {
if (this.opt.data) this.command.addHistory()
}, 0)
// 将初始数据添加到历史记录堆栈中
if (this.opt.addHistoryOnInit && this.opt.data) {
this.command.addHistory()
}
}
// 配置参数处理
@@ -316,7 +338,7 @@ class MindMap {
this.opt.themeConfig = config
if (!notRender) {
// 检查改变的是否是节点大小无关的主题属性
let res = checkIsNodeSizeIndependenceConfig(changedConfig)
const res = checkIsNodeSizeIndependenceConfig(changedConfig)
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
}
}
@@ -339,8 +361,11 @@ class MindMap {
// 更新配置
updateConfig(opt = {}) {
this.emit('before_update_config', this.opt)
const lastOpt = {
...this.opt
}
this.opt = this.handleOpt(merge.all([defaultOpt, this.opt, opt]))
this.emit('after_update_config', this.opt)
this.emit('after_update_config', this.opt, lastOpt)
}
// 获取当前布局结构
@@ -370,14 +395,17 @@ class MindMap {
// 更新画布数据,如果新的数据是在当前画布节点数据基础上增删改查后形成的,那么可以使用该方法来更新画布数据
updateData(data) {
this.emit('before_update_data', data)
this.renderer.setData(data)
this.render()
this.command.addHistory()
this.emit('update_data', data)
}
// 动态设置思维导图数据,纯节点数据
setData(data) {
data = this.handleData(data)
this.emit('before_set_data', data)
this.opt.data = data
this.execCommand('CLEAR_ACTIVE_NODE')
this.command.clearHistory()
@@ -456,11 +484,20 @@ class MindMap {
}
const isReadonly = mode === CONSTANTS.MODE.READONLY
if (isReadonly === this.opt.readonly) return
this.opt.readonly = isReadonly
if (this.opt.readonly) {
if (isReadonly) {
// 如果处于编辑态,要隐藏所有的编辑框
if (this.renderer.textEdit.isShowTextEdit()) {
this.renderer.textEdit.hideEditTextBox()
this.command.originAddHistory()
}
// 取消当前激活的元素
this.execCommand('CLEAR_ACTIVE_NODE')
}
this.opt.readonly = isReadonly
// 切换为编辑模式时,如果历史记录堆栈是空的,那么进行一次入栈操作
if (!isReadonly && this.command.history.length <= 0) {
this.command.originAddHistory()
}
this.emit('mode_change', mode)
}
@@ -546,7 +583,7 @@ class MindMap {
this.watermark.isInExport = false
}
// 添加必要的样式
;[this.joinCss(), ...cssTextList].forEach(s => {
[this.joinCss(), ...cssTextList].forEach(s => {
clone.add(SVG(`<style>${s}</style>`))
})
// 附加内容

View File

@@ -1,11 +1,11 @@
{
"name": "simple-mind-map",
"version": "0.10.6",
"version": "0.12.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "0.10.6",
"version": "0.12.1",
"license": "MIT",
"dependencies": {
"@svgdotjs/svg.js": "3.2.0",
@@ -15,7 +15,7 @@
"katex": "^0.16.8",
"mdast-util-from-markdown": "^1.3.0",
"pdf-lib": "^1.17.1",
"quill": "^2.0.2",
"quill": "^2.0.3",
"tern": "^0.24.3",
"uuid": "^9.0.0",
"ws": "^7.5.9",
@@ -1822,9 +1822,9 @@
]
},
"node_modules/quill": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz",
"integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
"integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
"dependencies": {
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
@@ -3553,9 +3553,9 @@
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
},
"quill": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz",
"integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
"integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
"requires": {
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.12.0",
"version": "0.13.0",
"description": "一个简单的web在线思维导图",
"authors": [
{
@@ -22,7 +22,7 @@
"scripts": {
"lint": "eslint src/",
"format": "prettier --write .",
"types": "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir types --target es2017 --skipLibCheck",
"types": "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir types --target es2017 --skipLibCheck & node ./bin/createPluginsTypeFiles.js",
"wsServe": "node ./bin/wsServer.mjs"
},
"module": "index.js",
@@ -35,7 +35,7 @@
"katex": "^0.16.8",
"mdast-util-from-markdown": "^1.3.0",
"pdf-lib": "^1.17.1",
"quill": "^2.0.2",
"quill": "^2.0.3",
"tern": "^0.24.3",
"uuid": "^9.0.0",
"ws": "^7.5.9",

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'
@@ -75,6 +74,11 @@ export const CONSTANTS = {
TAG_POSITION: {
RIGHT: 'right',
BOTTOM: 'bottom'
},
EDIT_NODE_CLASS: {
SMM_NODE_EDIT_WRAP: 'smm-node-edit-wrap',
RICH_TEXT_EDIT_WRAP: 'ql-editor',
ASSOCIATIVE_LINE_TEXT_EDIT_WRAP: 'associative-line-text-edit-warp'
}
}
@@ -152,7 +156,7 @@ export const nodeDataNoStylePropList = [
'isActive',
'generalization',
'richText',
'resetRichText',
'resetRichText',// 重新创建富文本内容,去掉原有样式
'uid',
'activeStyle',
'associativeLineTargets',
@@ -167,7 +171,10 @@ export const nodeDataNoStylePropList = [
'range',
'customLeft',
'customTop',
'customTextWidth'
'customTextWidth',
'checkbox',
'dir',
'needUpdate'// 重新创建节点内容
]
// 错误类型
@@ -200,6 +207,10 @@ export const cssContent = `
opacity: 1;
stroke-width: 2;
}
.smm-text-node-wrap, .smm-expand-btn-text {
user-select: none;
}
`
// html自闭合标签列表
@@ -212,3 +223,16 @@ export const selfCloseTagList = [
'meta',
'area'
]
// 非富文本模式下的节点文本行高
export const noneRichTextNodeLineHeight = 1.2
// 富文本支持的样式列表
export const richTextSupportStyleList = [
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
]

View File

@@ -84,6 +84,9 @@ export const defaultOpt = {
isShowExpandNum: true,
// 是否只有当鼠标在画布内才响应快捷键事件
enableShortcutOnlyWhenMouseInSvg: true,
// 自定义判断是否响应快捷键事件优先级比enableShortcutOnlyWhenMouseInSvg选项高
// 可以传递一个函数接收事件对象e为参数需要返回true或false返回true代表允许响应快捷键事件反之不允许库默认当事件目标为body或为文本编辑框元素普通文本编辑框、富文本编辑框、关联线文本编辑框时响应快捷键其他不响应
customCheckEnableShortcut: null,
// 初始根节点的位置
initRootNodePosition: null,
// 节点文本编辑框的z-index
@@ -130,6 +133,8 @@ export const defaultOpt = {
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
// 开启该特性后需要给你的输入框绑定keydown事件并禁止冒泡
enableAutoEnterTextEditWhenKeydown: false,
// 当enableAutoEnterTextEditWhenKeydown选项开启时生效当通过按键进入文本编辑时是否自动清空原有文本
autoEmptyTextWhenKeydownEnterEdit: false,
// 自定义对剪贴板文本的处理。当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据如果你想处理其他思维导图的数据比如processon、zhixi等那么可以传递一个函数接受当前剪贴板中的文本为参数返回处理后的数据可以返回两种类型
/*
1.返回一个纯文本,那么会直接以该文本创建一个子节点
@@ -226,7 +231,7 @@ export const defaultOpt = {
// 移动节点到画布中心、回到根节点等操作时是否将缩放层级复位为100%
// 该选项实际影响的是render.moveNodeToCenter方法moveNodeToCenter方法本身也存在第二个参数resetScale来设置是否复位如果resetScale参数没有传递那么使用resetScaleOnMoveNodeToCenter配置否则使用resetScale配置
resetScaleOnMoveNodeToCenter: false,
// 添加附加的节点前置内容,前置内容指和文本同一行的区域中的前置内容,不包括节点图片部分
// 添加附加的节点前置内容,前置内容指和文本同一行的区域中的前置内容,不包括节点图片部分。如果存在编号、任务勾选框内容,这里添加的前置内容会在这两者之后
createNodePrefixContent: null,
// 添加附加的节点后置内容,后置内容指和文本同一行的区域中的后置内容,不包括节点图片部分
createNodePostfixContent: null,
@@ -247,8 +252,9 @@ export const defaultOpt = {
emptyTextMeasureHeightText: 'abc123我和你',
// 是否在进行节点文本编辑时实时更新节点大小和节点位置,开启后当节点数量比较多时可能会造成卡顿
openRealtimeRenderOnNodeTextEdit: false,
// 默认会给容器元素el绑定mousedown事件并且会阻止其默认事件,这会带来一定问题,比如你聚焦在思维导图外的其他输入框,点击画布就不会触发其失焦,可以通过该选项关闭阻止。关闭后也会带来一定问题,比如鼠标框选节点时可能会选中节点文字,看你如何取舍
mousedownEventPreventDefault: true,
// 默认会给容器元素el绑定mousedown事件可通过该选项设置是否阻止其默认事件
// 如果设置为true会带来一定问题比如你聚焦在思维导图外的其他输入框点击画布就不会触发其失焦
mousedownEventPreventDefault: false,
// 在激活上粘贴用户剪贴板中的数据时,如果同时存在文本和图片,那么只粘贴文本,忽略图片
onlyPasteTextWhenHasImgAndText: true,
// 是否允许拖拽调整节点的宽度实际上压缩的是节点里面文本内容的宽度当节点文本内容宽度压缩到最小时无法继续压缩。如果节点存在图片那么最小值以图片宽度和文本内容最小宽度的最大值为准目前该特性仅在两种情况下可用1.开启了富文本模式即注册了RichText插件2.自定义节点内容)
@@ -257,6 +263,35 @@ export const defaultOpt = {
minNodeTextModifyWidth: 20,
// 同minNodeTextModifyWidth最大值传-1代表不限制
maxNodeTextModifyWidth: -1,
// 自定义处理节点的连线方法可以传递一个函数函数接收三个参数node节点实例、line节点的某条连线@svgjs库的path对象, { width, color, dasharray }dasharray该条连线的虚线样式为none代表实线你可以修改line对象来达到修改节点连线样式的效果比如增加流动效果
customHandleLine: null,
// 实例化完后是否立刻进行一次历史数据入栈操作
// 即调用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插件】
// 多选节点时鼠标移动到边缘时的画布移动偏移量
@@ -426,9 +461,6 @@ export const defaultOpt = {
transformRichTextOnEnterEdit: null,
// 可以传递一个函数即将结束富文本编辑前会执行该函数函数接收richText实例所以你可以在此时机更新quill文档数据
beforeHideRichTextEdit: null,
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
richTextEditFakeInPlace: false,
// 【OuterFrame】插件
outerFramePaddingX: 10,

View File

@@ -6,6 +6,7 @@ import {
transformTreeDataToObject
} from '../../utils'
import { ERROR_TYPES } from '../../constants/constant'
import pkg from '../../../package.json'
// 命令类
class Command {
@@ -18,6 +19,7 @@ class Command {
this.activeHistoryIndex = 0
// 注册快捷键
this.registerShortcutKeys()
this.originAddHistory = this.addHistory.bind(this)
this.addHistory = throttle(
this.addHistory,
this.mindMap.opt.addHistoryTime,
@@ -60,6 +62,7 @@ class Command {
this.commands[name].forEach(fn => {
fn(...args)
})
this.mindMap.emit('afterExecCommand', name, ...args)
if (
['BACK', 'FORWARD', 'SET_NODE_ACTIVE', 'CLEAR_ACTIVE_NODE'].includes(
name
@@ -170,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

@@ -1,4 +1,5 @@
import { keyMap } from './keyMap'
import { CONSTANTS } from '../../constants/constant'
// 快捷按键、命令处理类
export default class KeyCommand {
@@ -15,6 +16,18 @@ export default class KeyCommand {
this.bindEvent()
}
// 扩展按键映射
extendKeyMap(key, code) {
keyMap[key] = code
}
// 从按键映射中删除某个键
removeKeyMap(key) {
if (typeof keyMap[key] !== 'undefined') {
delete keyMap[key]
}
}
// 暂停快捷键响应
pause() {
this.isPause = true
@@ -73,10 +86,29 @@ export default class KeyCommand {
window.removeEventListener('keydown', this.onKeydown)
}
// 根据事件目标判断是否响应快捷键事件
defaultEnableCheck(e) {
const target = e.target
return (
target === document.body ||
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.SMM_NODE_EDIT_WRAP) ||
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP) ||
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
)
}
// 按键事件
onKeydown(e) {
const { enableShortcutOnlyWhenMouseInSvg, beforeShortcutRun } =
this.mindMap.opt
const {
enableShortcutOnlyWhenMouseInSvg,
beforeShortcutRun,
customCheckEnableShortcut
} = this.mindMap.opt
const checkFn =
typeof customCheckEnableShortcut === 'function'
? customCheckEnableShortcut
: this.defaultEnableCheck
if (!checkFn(e)) return
if (this.isPause || (enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) {
return
}

View File

@@ -30,10 +30,12 @@ import {
createSmmFormatData,
checkSmmFormatData,
checkIsNodeStyleDataKey,
removeRichTextStyes,
formatGetNodeGeneralization,
sortNodeList,
throttle
throttle,
debounce,
checkClipboardReadEnable,
isNodeNotNeedRenderData
} from '../../utils'
import { shapeList } from './node/Shape'
import { lineStyleProps } from '../../theme/default'
@@ -92,12 +94,7 @@ class Render {
// 文本编辑框需要再bindEvent之前实例化否则单击事件只能触发隐藏文本编辑框而无法保存文本修改
this.textEdit = new TextEdit(this)
// 当前复制的数据
this.lastBeingCopyData = null
this.beingCopyData = null
this.beingPasteText = ''
this.beingPasteImgSize = 0
this.currentBeingPasteType = ''
this.pasteData = { text: null, img: null }
// 节点高亮框
this.highlightBoxNode = null
this.highlightBoxNodeStyle = null
@@ -115,24 +112,26 @@ class Render {
// 设置布局结构
setLayout() {
const { layout } = this.mindMap.opt
this.layout = new (
layouts[this.mindMap.opt.layout]
? layouts[this.mindMap.opt.layout]
layouts[layout]
? layouts[layout]
: layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE]
)(this, this.mindMap.opt.layout)
)(this, layout)
}
// 重新设置思维导图数据
setData(data) {
if (this.hasRichTextPlugin()) {
this.renderTree = data ? this.mindMap.richText.handleSetData(data) : null
} else {
this.renderTree = data
}
this.renderTree = data || null
}
// 绑定事件
bindEvent() {
const {
openPerformance,
performanceConfig,
openRealtimeRenderOnNodeTextEdit
} = this.mindMap.opt
// 画布点击事件清除当前激活节点列表
this.mindMap.on('draw_click', e => {
this.clearActiveNodeListOnDrawClick(e, 'click')
@@ -147,34 +146,6 @@ class Render {
this.setRootNodeCenter()
})
// 性能模式
this.performanceMode()
// 实时渲染当节点文本编辑时
if (this.mindMap.opt.openRealtimeRenderOnNodeTextEdit) {
this.mindMap.on('node_text_edit_change', ({ node, text }) => {
node._textData = node.createTextNode(text)
const { width, height } = node.getNodeRect()
node.width = width
node.height = height
node.layout()
this.mindMap.render(() => {
this.textEdit.updateTextEditNode()
})
})
}
// 处理非https下的复制黏贴问题
// 暂时不启用,因为给页面的其他输入框(比如节点文本编辑框)粘贴内容也会触发,冲突问题暂时没有想到好的解决方法,不可能要求所有输入框都阻止冒泡
// if (!navigator.clipboard) {
// this.handlePaste = this.handlePaste.bind(this)
// window.addEventListener('paste', this.handlePaste)
// this.mindMap.on('beforeDestroy', () => {
// window.removeEventListener('paste', this.handlePaste)
// })
// }
}
// 性能模式,懒加载节点
performanceMode() {
const { openPerformance, performanceConfig } = this.mindMap.opt
const onViewDataChange = throttle(() => {
if (this.root) {
this.mindMap.emit('node_tree_render_start')
@@ -187,24 +158,56 @@ class Render {
)
}
}, performanceConfig.time)
let lastOpen = false
this.mindMap.on('before_update_config', opt => {
lastOpen = opt.openPerformance
})
this.mindMap.on('after_update_config', opt => {
if (opt.openPerformance && !lastOpen) {
// 动态开启性能模式
this.mindMap.on('view_data_change', onViewDataChange)
if (openPerformance) {
this.mindMap.on('view_data_change', onViewDataChange)
}
// 文本编辑时实时更新节点大小
this.onNodeTextEditChange = debounce(this.onNodeTextEditChange, 100, this)
if (openRealtimeRenderOnNodeTextEdit) {
this.mindMap.on('node_text_edit_change', this.onNodeTextEditChange)
}
// 监听配置改变事件
this.mindMap.on('after_update_config', (opt, lastOpt) => {
// 更新openPerformance配置
if (opt.openPerformance !== lastOpt.openPerformance) {
this.mindMap[opt.openPerformance ? 'on' : 'off'](
'view_data_change',
onViewDataChange
)
this.forceLoadNode()
}
if (!opt.openPerformance && lastOpen) {
// 动态关闭性能模式
this.mindMap.off('view_data_change', onViewDataChange)
this.forceLoadNode()
// 更新openRealtimeRenderOnNodeTextEdit配置
if (
opt.openRealtimeRenderOnNodeTextEdit !==
lastOpt.openRealtimeRenderOnNodeTextEdit
) {
this.mindMap[opt.openRealtimeRenderOnNodeTextEdit ? 'on' : 'off'](
'node_text_edit_change',
this.onNodeTextEditChange
)
}
})
if (!openPerformance) return
this.mindMap.on('view_data_change', onViewDataChange)
// 处理非https下的复制黏贴问题
// 暂时不启用,因为给页面的其他输入框(比如节点文本编辑框)粘贴内容也会触发,冲突问题暂时没有想到好的解决方法,不可能要求所有输入框都阻止冒泡
// if (!checkClipboardReadEnable()) {
// this.handlePaste = this.handlePaste.bind(this)
// window.addEventListener('paste', this.handlePaste)
// this.mindMap.on('beforeDestroy', () => {
// window.removeEventListener('paste', this.handlePaste)
// })
// }
}
// 监听文本编辑事件,实时更新节点大小
onNodeTextEditChange({ node, text }) {
node._textData = node.createTextNode(text)
const { width, height } = node.getNodeRect()
node.width = width
node.height = height
node.layout()
this.mindMap.render(() => {
this.textEdit.updateTextEditNode()
})
}
// 强制渲染节点,不考虑是否在画布可视区域内
@@ -424,7 +427,7 @@ class Render {
})
// 粘贴节点
this.mindMap.keyCommand.addShortcut('Control+v', () => {
if (navigator.clipboard) this.paste()
this.paste()
})
// 根节点居中显示
this.mindMap.keyCommand.addShortcut('Control+Enter', () => {
@@ -544,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')
})
@@ -558,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
}
@@ -747,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
// 新插入节点的默认文本
@@ -764,6 +762,10 @@ class Render {
: defaultInsertBelowSecondLevelNodeText
// 计算插入位置
const index = getNodeDataIndex(node)
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNodeData = {
inserting,
data: {
@@ -772,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)
})
// 如果同时对多个节点插入子节点,需要清除原来激活的节点
@@ -799,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) {
@@ -845,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: {
@@ -868,8 +879,9 @@ class Render {
...params,
...(appointData || {})
},
children: [...createUidForAppointNodes(appointChildren)]
children: [...createUidForAppointNodes(appointChildren, createNewId)]
}
createNewId = true
node.nodeData.children.push(newNode)
// 插入子节点时自动展开子节点
node.setData({
@@ -899,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({
@@ -944,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
@@ -953,6 +970,10 @@ class Render {
node.layerIndex === 1
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNode = {
inserting,
data: {
@@ -963,11 +984,6 @@ class Render {
},
children: [node.nodeData]
}
if (isRichText) {
node.setData({
resetRichText: true
})
}
const parent = node.parent
// 获取当前节点所在位置
const index = getNodeDataIndex(node)
@@ -981,11 +997,12 @@ class Render {
}
// 上移节点,多个节点只会操作第一个节点
upNode() {
if (this.activeNodeList.length <= 0) {
upNode(appointNode) {
if (this.activeNodeList.length <= 0 && !appointNode) {
return
}
let node = this.activeNodeList[0]
const list = appointNode ? [appointNode] : this.activeNodeList
const node = list[0]
if (node.isRoot) {
return
}
@@ -1006,11 +1023,12 @@ class Render {
}
// 下移节点,多个节点只会操作第一个节点
downNode() {
if (this.activeNodeList.length <= 0) {
downNode(appointNode) {
if (this.activeNodeList.length <= 0 && !appointNode) {
return
}
let node = this.activeNodeList[0]
const list = appointNode ? [appointNode] : this.activeNodeList
const node = list[0]
if (node.isRoot) {
return
}
@@ -1041,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()
@@ -1056,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
}
@@ -1144,8 +1161,6 @@ class Render {
text = clipboardData.getData('text')
}
})
this.pasteData.img = img
this.pasteData.text = text
this.paste()
}
@@ -1158,134 +1173,124 @@ class Render {
disabledClipboard,
onlyPasteTextWhenHasImgAndText
} = this.mindMap.opt
// 读取剪贴板的文字和图片
let text = ''
let img = null
if (!disabledClipboard) {
// 如果支持剪贴板操作,那么以剪贴板数据为准
if (!disabledClipboard && checkClipboardReadEnable()) {
try {
const res = navigator.clipboard
? await getDataFromClipboard()
: this.pasteData
text = res.text || ''
img = res.img || null
const res = await getDataFromClipboard()
let text = res.text || ''
let img = res.img || null
// 存在文本,则创建子节点
if (text) {
// 判断粘贴的是否是simple-mind-map的数据
let smmData = null
let useDefault = true
// 用户自定义处理
if (this.mindMap.opt.customHandleClipboardText) {
try {
const res = await this.mindMap.opt.customHandleClipboardText(text)
if (!isUndef(res)) {
useDefault = false
const checkRes = checkSmmFormatData(res)
if (checkRes.isSmm) {
smmData = checkRes.data
} else {
text = checkRes.data
}
}
} catch (error) {
errorHandler(
ERROR_TYPES.CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR,
error
)
}
}
// 默认处理
if (useDefault) {
const checkRes = checkSmmFormatData(text)
if (checkRes.isSmm) {
smmData = checkRes.data
} else {
text = checkRes.data
}
}
if (smmData) {
this.mindMap.execCommand(
'INSERT_MULTI_CHILD_NODE',
[],
Array.isArray(smmData) ? smmData : [smmData]
)
} else {
// 如果是富文本模式,那么需要转义特殊字符
if (this.hasRichTextPlugin()) {
text = htmlEscape(text)
}
const textArr = text
.split(new RegExp('\r?\n|(?<!\n)\r', 'g'))
.filter(item => {
return !!item
})
// 判断是否需要根据换行自动分割节点
if (textArr.length > 1 && handleIsSplitByWrapOnPasteCreateNewNode) {
handleIsSplitByWrapOnPasteCreateNewNode()
.then(() => {
this.mindMap.execCommand(
'INSERT_MULTI_CHILD_NODE',
[],
textArr.map(item => {
return {
data: {
text: item
},
children: []
}
})
)
})
.catch(() => {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
})
} else {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
}
}
}
// 存在图片,则添加到当前激活节点
if (img && (!text || !onlyPasteTextWhenHasImgAndText)) {
try {
let imgData = null
// 自定义图片处理函数
if (
handleNodePasteImg &&
typeof handleNodePasteImg === 'function'
) {
imgData = await handleNodePasteImg(img)
} else {
imgData = await loadImage(img)
}
if (this.activeNodeList.length > 0) {
this.activeNodeList.forEach(node => {
this.mindMap.execCommand('SET_NODE_IMAGE', node, {
url: imgData.url,
title: '',
width: imgData.size.width,
height: imgData.size.height
})
})
}
} catch (error) {
errorHandler(ERROR_TYPES.LOAD_CLIPBOARD_IMAGE_ERROR, error)
}
}
} catch (error) {
errorHandler(ERROR_TYPES.READ_CLIPBOARD_ERROR, error)
}
}
// 检查剪切板数据是否有变化
// 通过图片大小来判断图片是否发生变化,可能是不准确的,但是目前没有其他好方法
const imgSize = img ? img.size : 0
if (this.beingPasteText !== text || this.beingPasteImgSize !== imgSize) {
this.currentBeingPasteType = CONSTANTS.PASTE_TYPE.CLIP_BOARD
this.beingPasteText = text
this.beingPasteImgSize = imgSize
}
// 检查要粘贴的节点数据是否有变化,节点优先级高于剪切板
if (this.lastBeingCopyData !== this.beingCopyData) {
this.lastBeingCopyData = this.beingCopyData
this.currentBeingPasteType = CONSTANTS.PASTE_TYPE.CANVAS
}
// 粘贴剪切板的数据
if (this.currentBeingPasteType === CONSTANTS.PASTE_TYPE.CLIP_BOARD) {
// 存在文本,则创建子节点
if (text) {
// 判断粘贴的是否是simple-mind-map的数据
let smmData = null
let useDefault = true
// 用户自定义处理
if (this.mindMap.opt.customHandleClipboardText) {
try {
const res = await this.mindMap.opt.customHandleClipboardText(text)
if (!isUndef(res)) {
useDefault = false
const checkRes = checkSmmFormatData(res)
if (checkRes.isSmm) {
smmData = checkRes.data
} else {
text = checkRes.data
}
}
} catch (error) {
errorHandler(ERROR_TYPES.CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR, error)
}
}
// 默认处理
if (useDefault) {
const checkRes = checkSmmFormatData(text)
if (checkRes.isSmm) {
smmData = checkRes.data
} else {
text = checkRes.data
}
}
if (smmData) {
this.mindMap.execCommand(
'INSERT_MULTI_CHILD_NODE',
[],
Array.isArray(smmData) ? smmData : [smmData]
)
} else {
text = htmlEscape(text)
const textArr = text
.split(new RegExp('\r?\n|(?<!\n)\r', 'g'))
.filter(item => {
return !!item
})
// 判断是否需要根据换行自动分割节点
if (textArr.length > 1 && handleIsSplitByWrapOnPasteCreateNewNode) {
handleIsSplitByWrapOnPasteCreateNewNode()
.then(() => {
this.mindMap.execCommand(
'INSERT_MULTI_CHILD_NODE',
[],
textArr.map(item => {
return {
data: {
text: item
},
children: []
}
})
)
})
.catch(() => {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
})
} else {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
}
}
}
// 存在图片,则添加到当前激活节点
if (img && (!text || !onlyPasteTextWhenHasImgAndText)) {
try {
let imgData = null
// 自定义图片处理函数
if (handleNodePasteImg && typeof handleNodePasteImg === 'function') {
imgData = await handleNodePasteImg(img)
} else {
imgData = await loadImage(img)
}
if (this.activeNodeList.length > 0) {
this.activeNodeList.forEach(node => {
this.mindMap.execCommand('SET_NODE_IMAGE', node, {
url: imgData.url,
title: '',
width: imgData.size.width,
height: imgData.size.height
})
})
}
} catch (error) {
errorHandler(ERROR_TYPES.LOAD_CLIPBOARD_IMAGE_ERROR, error)
}
}
} else {
// 粘贴节点数据
// 禁用剪贴板或不支持剪贴板时
// 粘贴画布内的节点数据
if (this.beingCopyData) {
this.mindMap.execCommand('PASTE_NODE', this.beingCopyData)
}
@@ -1312,7 +1317,6 @@ class Render {
nodeList.reverse()
}
nodeList.forEach(item => {
this.checkNodeLayerChange(item, exist)
// 移动节点
let nodeParent = item.parent
let nodeBorthers = nodeParent.children
@@ -1339,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)
@@ -1541,7 +1526,6 @@ class Render {
return !item.isRoot
})
nodeList.forEach(item => {
this.checkNodeLayerChange(item, toNode, true)
this.removeNodeFromActiveList(item)
removeFromParentNodeData(item)
toNode.setData({
@@ -1556,60 +1540,24 @@ class Render {
// 粘贴节点到节点
pasteNode(data) {
data = formatDataToArray(data)
if (this.activeNodeList.length <= 0 || data.length <= 0) {
return
}
this.activeNodeList.forEach(node => {
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)
}
// 设置节点样式
setNodeStyle(node, prop, value) {
let data = {
const data = {
[prop]: value
}
// 如果开启了富文本,则需要应用到富文本上
if (this.hasRichTextPlugin()) {
this.mindMap.richText.setNotActiveNodeStyle(node, {
[prop]: value
})
}
this.setNodeDataRender(node, data)
// 更新了连线的样式
if (lineStyleProps.includes(prop)) {
;(node.parent || node).renderLine(true)
(node.parent || node).renderLine(true)
}
}
// 设置节点多个样式
setNodeStyles(node, style) {
let data = { ...style }
// 如果开启了富文本,则需要应用到富文本上
if (this.hasRichTextPlugin()) {
this.mindMap.richText.setNotActiveNodeStyle(node, style)
}
const data = { ...style }
this.setNodeDataRender(node, data)
// 更新了连线的样式
let props = Object.keys(style)
@@ -1620,7 +1568,7 @@ class Render {
}
})
if (hasLineStyleProps) {
;(node.parent || node).renderLine(true)
(node.parent || node).renderLine(true)
}
}
@@ -1830,6 +1778,7 @@ class Render {
list.length > 1
)
let needRender = false
const alreadyIsRichText = data && data.richText
list.forEach(item => {
const newData = {
inserting,
@@ -1841,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)
@@ -1971,6 +1920,10 @@ class Render {
// 设置节点数据,并判断是否渲染
setNodeDataRender(node, data, notRender = false) {
this.mindMap.execCommand('SET_NODE_DATA', node, data)
if (isNodeNotNeedRenderData(data)) {
this.mindMap.emit('node_tree_render_end')
return
}
this.reRenderNodeCheckChange(node, notRender)
}

View File

@@ -6,9 +6,15 @@ import {
htmlEscape,
handleInputPasteText,
checkSmmFormatData,
getTextFromHtml
getTextFromHtml,
isWhite,
getVisibleColorFromTheme
} from '../../utils'
import { ERROR_TYPES, CONSTANTS } from '../../constants/constant'
import {
ERROR_TYPES,
CONSTANTS,
noneRichTextNodeLineHeight
} from '../../constants/constant'
// 节点文字编辑类
export default class TextEdit {
@@ -27,6 +33,7 @@ export default class TextEdit {
this.hasBodyMousedown = false
this.textNodePaddingX = 5
this.textNodePaddingY = 3
this.isNeedUpdateTextEditNode = false
this.bindEvent()
}
@@ -85,13 +92,51 @@ export default class TextEdit {
})
})
this.mindMap.on('scale', this.onScale)
// // 监听按键事件,判断是否自动进入文本编辑模式
// 监听按键事件,判断是否自动进入文本编辑模式
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
window.addEventListener('keydown', this.onKeydown)
}
this.mindMap.on('beforeDestroy', () => {
this.unBindEvent()
})
this.mindMap.on('after_update_config', (opt, lastOpt) => {
if (
opt.openRealtimeRenderOnNodeTextEdit !==
lastOpt.openRealtimeRenderOnNodeTextEdit
) {
if (this.mindMap.richText) {
this.mindMap.richText.onOpenRealtimeRenderOnNodeTextEditConfigUpdate(
opt.openRealtimeRenderOnNodeTextEdit
)
} else {
this.onOpenRealtimeRenderOnNodeTextEditConfigUpdate(
opt.openRealtimeRenderOnNodeTextEdit
)
}
}
if (
opt.enableAutoEnterTextEditWhenKeydown !==
lastOpt.enableAutoEnterTextEditWhenKeydown
) {
window[
opt.enableAutoEnterTextEditWhenKeydown
? 'addEventListener'
: 'removeEventListener'
]('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()
}
})
}
// 解绑事件
@@ -101,11 +146,15 @@ export default class TextEdit {
// 按键事件
onKeydown(e) {
if (e.target !== document.body) return
const activeNodeList = this.mindMap.renderer.activeNodeList
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
const node = activeNodeList[0]
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
if (node && this.checkIsAutoEnterTextEditKey(e)) {
// 忽略第一个键值,避免中文输入法时进入编辑会导致第一个键值变成字母的问题
// 带来的问题是按的第一下纯粹是进入文本编辑,但没有变成输入
e.preventDefault()
this.show({
node,
e,
@@ -128,7 +177,6 @@ export default class TextEdit {
// 注册临时快捷键
registerTmpShortcut() {
// 注册回车快捷键
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
@@ -145,7 +193,7 @@ export default class TextEdit {
return this.showTextEdit
}
// 显示文本编辑框
// 显示文本编辑框
// isInserting是否是刚创建的节点
// isFromKeyDown是否是在按键事件进入的编辑
async show({
@@ -158,7 +206,13 @@ export default class TextEdit {
if (node.isUseCustomNodeContent()) {
return
}
const { beforeTextEdit } = this.mindMap.opt
// 如果有正在编辑中的节点,那么先结束它
const currentEditNode = this.getCurrentEditNode()
if (currentEditNode) {
this.hideEditTextBox()
}
const { beforeTextEdit, openRealtimeRenderOnNodeTextEdit } =
this.mindMap.opt
if (typeof beforeTextEdit === 'function') {
let isShow = false
try {
@@ -169,10 +223,18 @@ export default class TextEdit {
}
if (!isShow) return
}
this.currentNode = node
const { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
this.mindMap.view.translateXY(offsetLeft, offsetTop)
const rect = node._textData.node.node.getBoundingClientRect()
const g = node._textData.node
// 需要先显示不然宽高获取到的可能是0
if (openRealtimeRenderOnNodeTextEdit) {
g.show()
}
const rect = g.node.getBoundingClientRect()
// 如果开启了大小实时更新,那么直接隐藏节点原文本
if (openRealtimeRenderOnNodeTextEdit) {
g.hide()
}
const params = {
node,
rect,
@@ -184,9 +246,25 @@ export default class TextEdit {
this.mindMap.richText.showEditText(params)
return
}
this.currentNode = node
this.showEditTextBox(params)
}
// 当openRealtimeRenderOnNodeTextEdit配置更新后需要更新编辑框样式
onOpenRealtimeRenderOnNodeTextEditConfigUpdate(
openRealtimeRenderOnNodeTextEdit
) {
if (!this.textEditNode) return
this.textEditNode.style.background = openRealtimeRenderOnNodeTextEdit
? 'transparent'
: this.currentNode
? this.getBackground(this.currentNode)
: ''
this.textEditNode.style.boxShadow = openRealtimeRenderOnNodeTextEdit
? 'none'
: '0 0 20px rgba(0,0,0,.5)'
}
// 处理画布缩放
onScale() {
const node = this.getCurrentEditNode()
@@ -208,16 +286,37 @@ export default class TextEdit {
// 显示文本编辑框
showEditTextBox({ node, rect, isInserting, isFromKeyDown, isFromScale }) {
if (this.showTextEdit) return
const { nodeTextEditZIndex, textAutoWrapWidth, selectTextOnEnterEditText } =
this.mindMap.opt
const {
nodeTextEditZIndex,
textAutoWrapWidth,
selectTextOnEnterEditText,
openRealtimeRenderOnNodeTextEdit,
autoEmptyTextWhenKeydownEnterEdit
} = this.mindMap.opt
if (!isFromScale) {
this.mindMap.emit('before_show_text_edit')
}
this.registerTmpShortcut()
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.classList.add('smm-node-edit-wrap')
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: ${this.textNodePaddingY}px ${this.textNodePaddingX}px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
this.textEditNode.classList.add(
CONSTANTS.EDIT_NODE_CLASS.SMM_NODE_EDIT_WRAP
)
this.textEditNode.style.cssText = `
position: fixed;
box-sizing: border-box;
${
openRealtimeRenderOnNodeTextEdit
? ''
: `box-shadow: 0 0 20px rgba(0,0,0,.5);`
}
padding: ${this.textNodePaddingY}px ${this.textNodePaddingX}px;
margin-left: -${this.textNodePaddingX}px;
margin-top: -${this.textNodePaddingY}px;
outline: none;
word-break: break-all;
line-break: anywhere;
`
this.textEditNode.setAttribute('contenteditable', true)
this.textEditNode.addEventListener('keyup', e => {
e.stopPropagation()
@@ -242,42 +341,47 @@ 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
targetNode.appendChild(this.textEditNode)
}
let scale = this.mindMap.view.scale
let lineHeight = node.style.merge('lineHeight')
let fontSize = node.style.merge('fontSize')
let textLines = (this.cacheEditingText || node.getData('text'))
const scale = this.mindMap.view.scale
const fontSize = node.style.merge('fontSize')
const textLines = (this.cacheEditingText || node.getData('text'))
.split(/\n/gim)
.map(item => {
return htmlEscape(item)
})
let isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
node.style.domText(this.textEditNode, scale, isMultiLine)
const isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
node.style.domText(this.textEditNode, scale)
if (!openRealtimeRenderOnNodeTextEdit) {
this.textEditNode.style.background = this.getBackground(node)
}
this.textEditNode.style.zIndex = nodeTextEditZIndex
this.textEditNode.innerHTML = textLines.join('<br>')
if (isFromKeyDown && autoEmptyTextWhenKeydownEnterEdit) {
this.textEditNode.innerHTML = ''
} else {
this.textEditNode.innerHTML = textLines.join('<br>')
}
this.textEditNode.style.minWidth =
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.minHeight = rect.height + '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 && lineHeight !== 1) {
if (isMultiLine) {
this.textEditNode.style.lineHeight = noneRichTextNodeLineHeight
this.textEditNode.style.transform = `translateY(${
-((lineHeight * fontSize - fontSize) / 2) * scale
(((noneRichTextNodeLineHeight - 1) * fontSize) / 2) * scale
}px)`
} else {
this.textEditNode.style.lineHeight = 'normal'
}
this.showTextEdit = true
// 选中文本
@@ -292,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) {
@@ -306,8 +419,29 @@ 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'
}
// 获取编辑区域的背景填充
getBackground(node) {
const gradientStyle = node.style.merge('gradientStyle')
// 当前使用的是渐变色背景
if (gradientStyle) {
const startColor = node.style.merge('startColor')
const endColor = node.style.merge('endColor')
return `linear-gradient(to right, ${startColor}, ${endColor})`
} else {
// 单色背景
const bgColor = node.style.merge('fillColor')
const color = node.style.merge('color')
// 默认使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
return bgColor === 'transparent'
? isWhite(color)
? getVisibleColorFromTheme(this.mindMap.themeConfig)
: '#fff'
: bgColor
}
}
// 删除文本编辑元素
@@ -334,17 +468,8 @@ export default class TextEdit {
if (!this.showTextEdit) {
return
}
this.mindMap.execCommand(
'SET_NODE_TEXT',
this.currentNode,
this.getEditText()
)
if (this.currentNode.isGeneralization) {
// 概要节点
this.currentNode.generalizationBelongNode.updateGeneralization()
}
this.mindMap.render()
const currentNode = this.currentNode
const text = this.getEditText()
this.currentNode = null
this.textEditNode.style.display = 'none'
this.textEditNode.innerHTML = ''
@@ -353,6 +478,12 @@ export default class TextEdit {
this.textEditNode.style.fontWeight = 'normal'
this.textEditNode.style.transform = 'translateY(0)'
this.showTextEdit = false
this.mindMap.execCommand('SET_NODE_TEXT', currentNode, text)
// if (currentNode.isGeneralization) {
// // 概要节点
// currentNode.generalizationBelongNode.updateGeneralization()
// }
this.mindMap.render()
this.mindMap.emit(
'hide_text_edit',
this.textEditNode,

View File

@@ -1,6 +1,6 @@
import Style from './Style'
import Shape from './Shape'
import { G, Rect, Text } from '@svgdotjs/svg.js'
import { G, Rect, Text, SVG } from '@svgdotjs/svg.js'
import nodeGeneralizationMethods from './nodeGeneralization'
import nodeExpandBtnMethods from './nodeExpandBtn'
import nodeCommandWrapsMethods from './nodeCommandWraps'
@@ -89,7 +89,6 @@ class MindMapNode {
this.noteEl = null
this.noteContentIsShow = false
this._attachmentData = null
this._numberData = null
this._prefixData = null
this._postfixData = null
this._expandBtn = null
@@ -113,8 +112,6 @@ class MindMapNode {
// 概要节点的宽高
this._generalizationNodeWidth = 0
this._generalizationNodeHeight = 0
// 编号字符
this.number = opt.number || ''
// 各种文字信息的间距
this.textContentItemMargin = this.mindMap.opt.textContentMargin
// 图片和文字节点的间距
@@ -163,6 +160,8 @@ class MindMapNode {
}
// 初始化
this.getSize()
// 初始需要计算一下概要节点的大小,否则计算布局时获取不到概要的大小
this.updateGeneralization()
this.initDragHandle()
}
@@ -207,10 +206,10 @@ class MindMapNode {
}
// 创建节点的各个内容对象数据
// recreateTypes[] custom、image、icon、text、hyperlink、tag、note、attachment、numbers、prefix、postfix
// recreateTypes[] custom、image、icon、text、hyperlink、tag、note、attachment、numbers、prefix、postfix、checkbox
createNodeData(recreateTypes) {
// 自定义节点内容
let {
const {
isUseCustomNodeContent,
customCreateNodeContent,
createNodePrefixContent,
@@ -226,9 +225,11 @@ class MindMapNode {
'tag',
'note',
'attachment',
'numbers',
'prefix',
'postfix'
'postfix',
...this.mindMap.nodeInnerPrefixList.map(item => {
return item.name
})
]
const createTypes = {}
if (Array.isArray(recreateTypes)) {
@@ -264,9 +265,11 @@ class MindMapNode {
if (createTypes.note) this._noteData = this.createNoteNode()
if (createTypes.attachment)
this._attachmentData = this.createAttachmentNode()
if (this.mindMap.numbers && createTypes.numbers) {
this._numberData = this.mindMap.numbers.createNumberContent(this)
}
this.mindMap.nodeInnerPrefixList.forEach(item => {
if (createTypes[item.name]) {
this[`_${item.name}Data`] = item.createContent(this)
}
})
if (createTypes.prefix) {
this._prefixData = createNodePrefixContent
? createNodePrefixContent(this)
@@ -286,15 +289,19 @@ class MindMapNode {
}
// 计算节点的宽高
getSize(recreateTypes) {
getSize(recreateTypes, opt = {}) {
const ignoreUpdateCustomTextWidth = opt.ignoreUpdateCustomTextWidth || false
if (!ignoreUpdateCustomTextWidth) {
this.customTextWidth = this.getData('customTextWidth') || undefined
}
this.customLeft = this.getData('customLeft') || undefined
this.customTop = this.getData('customTop') || undefined
// 这里不要更新概要,不然即使概要没修改,每次也会重新渲染
// this.updateGeneralization()
this.createNodeData(recreateTypes)
let { width, height } = this.getNodeRect()
const { width, height } = this.getNodeRect()
// 判断节点尺寸是否有变化
let changed = this.width !== width || this.height !== height
const changed = this.width !== width || this.height !== height
this.width = width
this.height = height
return changed
@@ -304,7 +311,7 @@ class MindMapNode {
getNodeRect() {
// 自定义节点内容
if (this.isUseCustomNodeContent()) {
let rect = this.measureCustomNodeContentSize(this._customNodeContent)
const rect = this.measureCustomNodeContentSize(this._customNodeContent)
return {
width: this.hasCustomWidth() ? this.customTextWidth : rect.width,
height: rect.height
@@ -324,11 +331,14 @@ class MindMapNode {
this._rectInfo.imgContentWidth = imgContentWidth = this._imgData.width
this._rectInfo.imgContentHeight = imgContentHeight = this._imgData.height
}
// 编号内容
if (this._numberData) {
textContentWidth += this._numberData.width
textContentHeight = Math.max(textContentHeight, this._numberData.height)
}
// 库前置内容
this.mindMap.nodeInnerPrefixList.forEach(item => {
const itemData = this[`_${item.name}Data`]
if (itemData) {
textContentWidth += itemData.width
textContentHeight = Math.max(textContentHeight, itemData.height)
}
})
// 自定义前置内容
if (this._prefixData) {
textContentWidth += this._prefixData.width
@@ -397,7 +407,7 @@ class MindMapNode {
imgContentHeight > 0 && textContentHeight > 0
? this.blockContentMargin
: 0
let { paddingX, paddingY } = this.getPaddingVale()
const { paddingX, paddingY } = this.getPaddingVale()
// 纯内容宽高
let _width = Math.max(imgContentWidth, textContentWidth)
let _height = imgContentHeight + textContentHeight
@@ -411,7 +421,7 @@ class MindMapNode {
_height += tagContentHeight
}
// 计算节点形状需要的附加内边距
let { paddingX: shapePaddingX, paddingY: shapePaddingY } =
const { paddingX: shapePaddingX, paddingY: shapePaddingY } =
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
this.shapePadding.paddingX = shapePaddingX
this.shapePadding.paddingY = shapePaddingY
@@ -428,7 +438,8 @@ class MindMapNode {
if (!this.group) return
// 清除之前的内容
this.group.clear()
const { hoverRectPadding, tagPosition } = this.mindMap.opt
const { hoverRectPadding, tagPosition, openRealtimeRenderOnNodeTextEdit } =
this.mindMap.opt
let { width, height, textContentItemMargin } = this
let { paddingY } = this.getPaddingVale()
const halfBorderWidth = this.getBorderWidth() / 2
@@ -480,14 +491,17 @@ class MindMapNode {
// 内容节点
let textContentNested = new G()
let textContentOffsetX = 0
// 编号内容
if (this._numberData) {
this._numberData.node
.x(textContentOffsetX)
.y((textContentHeight - this._numberData.height) / 2)
textContentNested.add(this._numberData.node)
textContentOffsetX += this._numberData.width + textContentItemMargin
}
// 库前置内容
this.mindMap.nodeInnerPrefixList.forEach(item => {
const itemData = this[`_${item.name}Data`]
if (itemData) {
itemData.node
.x(textContentOffsetX)
.y((textContentHeight - itemData.height) / 2)
textContentNested.add(itemData.node)
textContentOffsetX += itemData.width + textContentItemMargin
}
})
// 自定义前置内容
if (this._prefixData) {
const foreignObject = createForeignObjectNode({
@@ -524,6 +538,12 @@ class MindMapNode {
.x(-oldX) // 修复非富文本模式下同时存在图标和换行的文本时,被收起和展开时图标与文字距离会逐渐拉大的问题
.x(textContentOffsetX)
.y((textContentHeight - this._textData.height) / 2)
// 如果开启了文本编辑实时渲染需要判断当前渲染的节点是否是正在编辑的节点是的话将透明度设置为0不显示
if (openRealtimeRenderOnNodeTextEdit) {
this._textData.node.opacity(
this.mindMap.renderer.textEdit.getCurrentEditNode() === this ? 0 : 1
)
}
textContentNested.add(this._textData.node)
textContentOffsetX += this._textData.width + textContentItemMargin
}
@@ -656,7 +676,7 @@ class MindMapNode {
// 多选和取消多选
if (!readonly && (e.ctrlKey || e.metaKey) && enableCtrlKeyNodeSelection) {
this.isMultipleChoice = true
let isActive = this.getData('isActive')
const isActive = this.getData('isActive')
if (!isActive)
this.mindMap.emit(
'before_node_active',
@@ -775,7 +795,7 @@ class MindMapNode {
this.renderExpandBtn()
}
} else {
let { isActive, expand } = this.getData()
const { isActive, expand } = this.getData()
// 展开状态且非激活状态,且当前鼠标不在它上面,才隐藏
if (childrenLength <= 0) {
this.removeExpandBtn()
@@ -793,7 +813,7 @@ class MindMapNode {
// 更新协同头像
if (this.updateUserListNode) this.updateUserListNode()
// 更新节点位置
let t = this.group.transform()
const t = this.group.transform()
// 保存一份当前节点数据快照
this.nodeDataSnapshot = JSON.stringify(this.getData())
// 节点位置变化才更新,因为即使值没有变化属性设置操作也是耗时的
@@ -804,10 +824,10 @@ class MindMapNode {
// 获取节点相当于画布的位置
getNodePosInClient(_left, _top) {
let drawTransform = this.mindMap.draw.transform()
let { scaleX, scaleY, translateX, translateY } = drawTransform
let left = _left * scaleX + translateX
let top = _top * scaleY + translateY
const drawTransform = this.mindMap.draw.transform()
const { scaleX, scaleY, translateX, translateY } = drawTransform
const left = _left * scaleX + translateX
const top = _top * scaleY + translateY
return {
left,
top
@@ -826,8 +846,8 @@ class MindMapNode {
}
// 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置
reRender(recreateTypes) {
let sizeChange = this.getSize(recreateTypes)
reRender(recreateTypes, opt) {
const sizeChange = this.getSize(recreateTypes, opt)
this.layout()
this.update()
return sizeChange
@@ -977,7 +997,7 @@ class MindMapNode {
if (this.group) this.group.hide()
this.hideGeneralization()
if (this.parent) {
let index = this.parent.children.indexOf(this)
const index = this.parent.children.indexOf(this)
this.parent._lines[index] && this.parent._lines[index].hide()
this._lines.forEach(item => {
item.hide()
@@ -999,7 +1019,7 @@ class MindMapNode {
this.group.show()
this.showGeneralization()
if (this.parent) {
let index = this.parent.children.indexOf(this)
const index = this.parent.children.indexOf(this)
this.parent._lines[index] && this.parent._lines[index].show()
this._lines.forEach(item => {
item.show()
@@ -1245,7 +1265,7 @@ class MindMapNode {
// 获取某个样式
getStyle(prop, root) {
let v = this.style.merge(prop, root)
const v = this.style.merge(prop, root)
return v === undefined ? '' : v
}
@@ -1310,11 +1330,11 @@ class MindMapNode {
// 获取节点的尺寸和位置信息,宽高是应用了缩放效果后的实际宽高,位置信息相对于画布
getRectInSvg() {
let { scaleX, scaleY, translateX, translateY } =
const { scaleX, scaleY, translateX, translateY } =
this.mindMap.draw.transform()
let { left, top, width, height } = this
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
const right = (left + width) * scaleX + translateX
const bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
return {
@@ -1355,6 +1375,15 @@ class MindMapNode {
return new Text().text(text)
}
// 获取SVG.js库的一些对象
getSvgObjects() {
return {
SVG,
G,
Rect
}
}
// 检查是否支持拖拽调整宽度
// 1.富文本模式
// 2.自定义节点内容

View File

@@ -12,6 +12,7 @@ const backgroundStyleProps = [
class Style {
// 设置背景样式
static setBackgroundStyle(el, themeConfig) {
if (!el) return
// 缓存容器元素原本的样式
if (!Style.cacheStyle) {
Style.cacheStyle = {}
@@ -183,7 +184,7 @@ class Style {
})
.css({
'font-family': styles.fontFamily,
'font-size': styles.fontSize,
'font-size': styles.fontSize + 'px',
'font-weight': styles.fontWeight,
'font-style': styles.fontStyle,
'text-decoration': styles.textDecoration
@@ -191,14 +192,15 @@ class Style {
}
// 生成内联样式
createStyleText() {
createStyleText(customStyle = {}) {
const styles = {
color: this.merge('color'),
fontFamily: this.merge('fontFamily'),
fontSize: this.merge('fontSize'),
fontWeight: this.merge('fontWeight'),
fontStyle: this.merge('fontStyle'),
textDecoration: this.merge('textDecoration')
textDecoration: this.merge('textDecoration'),
...customStyle
}
return `
color: ${styles.color};
@@ -229,20 +231,20 @@ class Style {
}
// html文字节点
domText(node, fontSizeScale = 1, isMultiLine) {
domText(node, fontSizeScale = 1) {
const styles = {
color: this.merge('color'),
fontFamily: this.merge('fontFamily'),
fontSize: this.merge('fontSize'),
fontWeight: this.merge('fontWeight'),
fontStyle: this.merge('fontStyle'),
textDecoration: this.merge('textDecoration'),
lineHeight: this.merge('lineHeight')
textDecoration: this.merge('textDecoration')
}
node.style.color = styles.color
node.style.textDecoration = styles.textDecoration
node.style.fontFamily = styles.fontFamily
node.style.fontSize = styles.fontSize * fontSizeScale + 'px'
node.style.fontWeight = styles.fontWeight || 'normal'
node.style.lineHeight = !isMultiLine ? 'normal' : styles.lineHeight
node.style.fontStyle = styles.fontStyle
}
@@ -268,14 +270,18 @@ class Style {
}
// 内置图标
iconNode(node) {
iconNode(node, color) {
node.attr({
fill: this.merge('color')
fill: color || this.merge('color')
})
}
// 连线
line(line, { width, color, dasharray } = {}, enableMarker, childNode) {
const { customHandleLine } = this.ctx.mindMap.opt
if (typeof customHandleLine === 'function') {
customHandleLine(this.ctx, line, { width, color, dasharray })
}
line.stroke({ color, dasharray, width }).fill({ color: 'none' })
// 可以显示箭头
if (enableMarker) {
@@ -338,7 +344,7 @@ class Style {
node2.fill({ color: color })
fillNode.fill({ color: fill })
if (this.ctx.mindMap.opt.isShowExpandNum) {
node.attr({ 'font-size': fontSize, 'font-color': fontColor })
node.attr({ 'font-size': fontSize + 'px', 'font-color': fontColor })
}
}
@@ -353,6 +359,17 @@ class Style {
return res
}
// 获取自定义的样式
getCustomStyle() {
const customStyle = {}
Object.keys(this.ctx.getData()).forEach(item => {
if (checkIsNodeStyleDataKey(item)) {
customStyle[item] = this.ctx.getData(item)
}
})
return customStyle
}
// hover和激活节点
hoverNode(node) {
const hoverRectColor =

View File

@@ -28,7 +28,7 @@ function createTextAvatar(item) {
color: '#fff'
})
.css({
'font-size': fontSize
'font-size': fontSize + 'px'
})
.dx(-fontSize / 2)
.dy((avatarSize - fontSize) / 2)

View File

@@ -1,17 +1,26 @@
import {
measureText,
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 } from '../../../constants/constant'
import { noneRichTextNodeLineHeight } from '../../../constants/constant'
// 测量svg文本宽高
const measureText = (text, style) => {
const g = new G()
const node = new Text().text(text)
style.text(node)
g.add(node)
return g.bbox()
}
// 标签默认的样式
const defaultTagStyle = {
@@ -120,43 +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)) {
// 判断节点内容是否是富文本
let isRichText = checkIsRichText(text)
// 样式字符串
let style = this.style.createStyleText()
if (isRichText) {
// 如果是富文本那么线移除内联样式
text = removeHtmlStyle(text)
// 再添加新的内联样式
let _text = text
text = addHtmlStyle(text, 'span', style)
// 给span添加样式没有成功则尝试给strong标签添加样式
if (text === _text) {
text = addHtmlStyle(text, 'strong', 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')
@@ -168,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'
@@ -197,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,
@@ -208,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)
}
@@ -218,16 +235,14 @@ function createTextNode(specifyText) {
}
let g = new G()
let fontSize = this.getStyle('fontSize', false)
let lineHeight = this.getStyle('lineHeight', false)
// 文本超长自动换行
let textStyle = this.style.getTextFontStyle()
let textArr = []
if (!isUndef(text)) {
textArr = String(text).split(/\n/gim)
}
const { textAutoWrapWidth: maxWidth, emptyTextMeasureHeightText } =
this.mindMap.opt
let isMultiLine = false
let isMultiLine = textArr.length > 1
textArr.forEach((item, index) => {
let arr = item.split('')
let lines = []
@@ -235,7 +250,7 @@ function createTextNode(specifyText) {
while (arr.length) {
let str = arr.shift()
let text = [...line, str].join('')
if (measureText(text, textStyle).width <= maxWidth) {
if (measureText(text, this.style).width <= maxWidth) {
line.push(str)
} else {
lines.push(line.join(''))
@@ -250,11 +265,20 @@ function createTextNode(specifyText) {
}
textArr[index] = lines.join('\n')
})
textArr = textArr.join('\n').split(/\n/gim)
textArr = textArr.join('\n').replace(/\n$/g, '').split(/\n/gim)
textArr.forEach((item, index) => {
let node = new Text().text(item)
// 避免尾部的空行不占宽度
// 同时解决该问题https://github.com/wanglin2/mind-map/issues/1037
if (item === '') {
item = ''
}
const node = new Text().text(item)
node.addClass('smm-text-node-wrap')
this.style.text(node)
node.y(fontSize * lineHeight * index)
node.y(
fontSize * noneRichTextNodeLineHeight * index +
((noneRichTextNodeLineHeight - 1) * fontSize) / 2
)
g.add(node)
})
let { width, height } = g.bbox()
@@ -279,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()
@@ -300,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 {
@@ -386,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) {
@@ -441,6 +467,9 @@ function createNoteNode() {
node.on('click', e => {
this.mindMap.emit('node_note_click', this, e, node)
})
node.on('dblclick', e => {
this.mindMap.emit('node_note_dblclick', this, e, node)
})
return {
node,
width: iconSize,
@@ -454,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>`))
@@ -462,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)
@@ -478,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()
@@ -531,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

@@ -8,7 +8,7 @@ function initDragHandle() {
// 拖拽手柄元素
this._dragHandleNodes = null
// 手柄元素的宽度
this.dragHandleWidth = 2
this.dragHandleWidth = 4
// 鼠标按下时的x坐标
this.dragHandleMousedownX = 0
// 鼠标是否处于按下状态
@@ -71,7 +71,9 @@ function onDragMousemoveHandle(e) {
this.left = this.dragHandleMousedownLeft + ox / scaleX
}
// 自定义内容不重新渲染,交给开发者
this.reRender(useCustomContent ? [] : ['text'])
this.reRender(useCustomContent ? [] : ['text'], {
ignoreUpdateCustomTextWidth: true
})
}
// 鼠标松开事件

View File

@@ -354,6 +354,10 @@ class View {
// 判断是否需要将思维导图限制在画布内
checkNeedMindMapInCanvas() {
// 如果当前在演示模式,那么不需要限制
if (this.mindMap.demonstrate && this.mindMap.demonstrate.isInDemonstrate) {
return false
}
const { isLimitMindMapInCanvasWhenHasScrollbar, isLimitMindMapInCanvas } =
this.mindMap.opt
// 如果注册了滚动条插件那么使用isLimitMindMapInCanvasWhenHasScrollbar配置

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)
}
// 层级类型改变
@@ -69,28 +66,6 @@ class Base {
}
}
// 获取节点编号信息
getNumberInfo({ parent, ancestors, layerIndex, index }) {
// 编号
const hasNumberPlugin = !!this.mindMap.numbers
const parentNumberStr =
hasNumberPlugin && parent && parent._node.number
? parent._node.number
: ''
const newNumberStr = hasNumberPlugin
? this.mindMap.numbers.getNodeNumberStr({
ancestors,
layerIndex,
num: index + 1,
parentNumberStr
})
: ''
return {
hasNumberPlugin,
newNumberStr
}
}
// 节点节点数据是否发生了改变
checkIsNodeDataChange(lastData, curData) {
if (lastData) {
@@ -105,14 +80,21 @@ class Base {
// 创建节点实例
createNode(data, parent, isRoot, layerIndex, index, ancestors) {
// 编号
const { hasNumberPlugin, newNumberStr } = this.getNumberInfo({
parent,
ancestors,
layerIndex,
index
})
// 创建节点
// 库前置内容数据
const nodeInnerPrefixData = {}
this.mindMap.nodeInnerPrefixList.forEach(item => {
if (item.createNodeData) {
const [key, value] = item.createNodeData({
data,
parent,
ancestors,
layerIndex,
index
})
nodeInnerPrefixData[key] = value
}
})
const uid = data.data.uid
let newNode = null
// 数据上保存了节点引用,那么直接复用节点
@@ -132,14 +114,16 @@ class Base {
}
this.cacheNode(data._node.uid, newNode)
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
// 判断编号是否改变
let isNumberChange = false
if (hasNumberPlugin) {
isNumberChange = this.mindMap.numbers.updateNumber(
newNode,
newNumberStr
)
}
// 库前置内容是否改变
let isNodeInnerPrefixChange = false
this.mindMap.nodeInnerPrefixList.forEach(item => {
if (item.updateNodeData) {
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData)
if (isChange) {
isNodeInnerPrefixChange = isChange
}
}
})
// 主题或主题配置改变了
const isResizeSource = this.checkIsNeedResizeSources()
// 节点数据改变了
@@ -153,7 +137,8 @@ class Base {
isNodeDataChange ||
isLayerTypeChange ||
newNode.getData('resetRichText') ||
isNumberChange
newNode.getData('needUpdate') ||
isNodeInnerPrefixChange
) {
newNode.getSize()
newNode.needLayout = true
@@ -190,21 +175,24 @@ class Base {
const isResizeSource = this.checkIsNeedResizeSources()
// 点数据改变了
const isNodeDataChange = this.checkIsNodeDataChange(lastData, data.data)
// 判断编号是否改变
let isNumberChange = false
if (hasNumberPlugin) {
isNumberChange = this.mindMap.numbers.updateNumber(
newNode,
newNumberStr
)
}
// 库前置内容是否改变
let isNodeInnerPrefixChange = false
this.mindMap.nodeInnerPrefixList.forEach(item => {
if (item.updateNodeData) {
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData)
if (isChange) {
isNodeInnerPrefixChange = isChange
}
}
})
// 重新计算节点大小和布局
if (
isResizeSource ||
isNodeDataChange ||
isLayerTypeChange ||
newNode.getData('resetRichText') ||
isNumberChange
newNode.getData('needUpdate') ||
isNodeInnerPrefixChange
) {
newNode.getSize()
newNode.needLayout = true
@@ -222,7 +210,7 @@ class Base {
layerIndex,
isRoot,
parent: !isRoot ? parent._node : null,
number: newNumberStr
...nodeInnerPrefixData
})
// uid保存到数据上为了节点复用
data.data.uid = newUid
@@ -463,8 +451,10 @@ class Base {
const end = list[len - 1]
// 如果三点在一条直线,那么不用处理
const isOneLine =
(start[0] === center[0] && center[0] === end[0]) ||
(start[1] === center[1] && center[1] === end[1])
(start[0].toFixed(0) === center[0].toFixed(0) &&
center[0].toFixed(0) === end[0].toFixed(0)) ||
(start[1].toFixed(0) === center[1].toFixed(0) &&
center[1].toFixed(0) === end[1].toFixed(0))
if (!isOneLine) {
const cStart = this.computeNewPoint(start, center, lineRadius)
const cEnd = this.computeNewPoint(end, center, lineRadius)

View File

@@ -35,7 +35,14 @@ class MindMap extends Base {
this.renderer.renderTree,
null,
(cur, parent, isRoot, layerIndex, index, ancestors) => {
let newNode = this.createNode(cur, parent, isRoot, layerIndex, index, ancestors)
let newNode = this.createNode(
cur,
parent,
isRoot,
layerIndex,
index,
ancestors
)
// 根节点定位在画布中心位置
if (isRoot) {
this.setNodeCenter(newNode)
@@ -47,9 +54,10 @@ class MindMap extends Base {
} else {
// 节点生长方向
newNode.dir =
index % 2 === 0
newNode.getData('dir') ||
(index % 2 === 0
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
: CONSTANTS.LAYOUT_GROW_DIR.LEFT)
}
// 根据生长方向定位到父节点的左侧或右侧
newNode.left =

View File

@@ -11,11 +11,25 @@ import {
import associativeLineControlsMethods from './associativeLine/associativeLineControls'
import associativeLineTextMethods from './associativeLine/associativeLineText'
const styleProps = [
'associativeLineWidth',
'associativeLineColor',
'associativeLineActiveWidth',
'associativeLineActiveColor',
'associativeLineDasharray',
'associativeLineTextColor',
'associativeLineTextFontSize',
'associativeLineTextLineHeight',
'associativeLineTextFontFamily'
]
// 关联线插件
class AssociativeLine {
constructor(opt = {}) {
this.mindMap = opt.mindMap
this.associativeLineDraw = this.mindMap.associativeLineDraw
// 本次不要重新渲染连线
this.isNotRenderAllLines = false
// 当前所有连接线
this.lineList = []
// 当前激活的连接线
@@ -27,9 +41,6 @@ class AssociativeLine {
this.overlapNode = null // 创建过程中的目标节点
// 是否有节点正在被拖拽
this.isNodeDragging = false
// 箭头图标
this.markerPath = null
this.marker = this.createMarker()
// 控制点
this.controlLine1 = null
this.controlLine2 = null
@@ -112,6 +123,25 @@ class AssociativeLine {
this.mindMap.off('beforeDestroy', this.onBeforeDestroy)
}
// 获取关联线的样式配置
// 优先级:关联线自定义样式、节点自定义样式、主题的节点层级样式、主题的最外层样式
getStyleConfig(node, toNode) {
let lineStyle = {}
if (toNode) {
const associativeLineStyle = node.getData('associativeLineStyle') || {}
lineStyle = associativeLineStyle[toNode.getData('uid')] || {}
}
const res = {}
styleProps.forEach(prop => {
if (typeof lineStyle[prop] !== 'undefined') {
res[prop] = lineStyle[prop]
} else {
res[prop] = node.getStyle(prop)
}
})
return res
}
// 实例销毁时清除关联线文字编辑框
onBeforeDestroy() {
this.hideEditTextBox()
@@ -140,12 +170,12 @@ class AssociativeLine {
}
// 创建箭头
createMarker() {
createMarker(callback = () => {}) {
return this.associativeLineDraw.marker(20, 20, add => {
add.ref(12, 5)
add.size(10, 10)
add.attr('orient', 'auto-start-reverse')
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
callback(add.path('M0,0 L2,5 L0,10 L10,5 Z'))
})
}
@@ -172,6 +202,10 @@ class AssociativeLine {
// 渲染所有连线
renderAllLines() {
if (this.isNotRenderAllLines) {
this.isNotRenderAllLines = false
return
}
// 先移除
this.removeAllLines()
this.removeControls()
@@ -223,11 +257,14 @@ class AssociativeLine {
associativeLineWidth,
associativeLineColor,
associativeLineActiveWidth,
associativeLineActiveColor,
associativeLineDasharray
} = this.mindMap.themeConfig
} = this.getStyleConfig(node, toNode)
// 箭头
this.markerPath
let markerPath = null
const marker = this.createMarker(p => {
markerPath = p
})
markerPath
.stroke({ color: associativeLineColor })
.fill({ color: associativeLineColor })
// 路径
@@ -247,7 +284,7 @@ class AssociativeLine {
})
.fill({ color: 'none' })
path.plot(pathStr)
path.marker('end', this.marker)
path.marker('end', marker)
// 不可见的点击线
let clickPath = this.associativeLineDraw.path()
clickPath
@@ -258,6 +295,7 @@ class AssociativeLine {
let text = this.createText({
path,
clickPath,
markerPath,
node,
toNode,
startPoint,
@@ -270,6 +308,7 @@ class AssociativeLine {
this.setActiveLine({
path,
clickPath,
markerPath,
text,
node,
toNode,
@@ -284,14 +323,72 @@ class AssociativeLine {
this.showEditTextBox(text)
})
// 渲染关联线文字
this.renderText(this.getText(node, toNode), path, text)
this.renderText(this.getText(node, toNode), path, text, node, toNode)
this.lineList.push([path, clickPath, text, node, toNode])
}
// 更新当前激活连线的样式,一般在自定义了节点关联线的样式后调用
// 直接调用node.setStyle方法更新样式会直接触发关联线更新但是关联线的激活状态会丢失
// 所以可以调用node.setData方法更新数据然后再调用该方法更新样式这样关联线激活状态不会丢失
updateActiveLineStyle() {
if (!this.activeLine) return
this.isNotRenderAllLines = true
const [path, clickPath, text, node, toNode, markerPath] = this.activeLine
const {
associativeLineWidth,
associativeLineColor,
associativeLineDasharray,
associativeLineActiveWidth,
associativeLineActiveColor,
associativeLineTextColor,
associativeLineTextFontFamily,
associativeLineTextFontSize
} = this.getStyleConfig(node, toNode)
path
.stroke({
width: associativeLineWidth,
color: associativeLineColor,
dasharray: associativeLineDasharray || [6, 4]
})
.fill({ color: 'none' })
clickPath
.stroke({
width: associativeLineActiveWidth,
color: associativeLineActiveColor
})
.fill({ color: 'none' })
markerPath
.stroke({ color: associativeLineColor })
.fill({ color: associativeLineColor })
text.find('text').forEach(textNode => {
textNode
.fill({
color: associativeLineTextColor
})
.css({
'font-family': associativeLineTextFontFamily,
'font-size': associativeLineTextFontSize + 'px'
})
})
if (this.controlLine1) {
this.controlLine1.stroke({ color: associativeLineActiveColor })
}
if (this.controlLine2) {
this.controlLine2.stroke({ color: associativeLineActiveColor })
}
if (this.controlPoint1) {
this.controlPoint1.stroke({ color: associativeLineActiveColor })
}
if (this.controlPoint2) {
this.controlPoint2.stroke({ color: associativeLineActiveColor })
}
}
// 激活某根关联线
setActiveLine({
path,
clickPath,
markerPath,
text,
node,
toNode,
@@ -299,25 +396,33 @@ class AssociativeLine {
endPoint,
controlPoints
}) {
let { associativeLineActiveColor } = this.mindMap.themeConfig
let { associativeLineActiveColor } = this.getStyleConfig(node, toNode)
// 如果当前存在激活节点,那么取消激活节点
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
// 否则清除当前的关联线的激活状态,如果有的话
this.clearActiveLine()
// 保存当前激活的关联线信息
this.activeLine = [path, clickPath, text, node, toNode]
this.activeLine = [path, clickPath, text, node, toNode, markerPath]
// 让不可见的点击线显示
clickPath.stroke({ color: associativeLineActiveColor })
// 如果没有输入过关联线文字,那么显示默认文字
if (!this.getText(node, toNode)) {
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text)
this.renderText(
this.mindMap.opt.defaultAssociativeLineText,
path,
text,
node,
toNode
)
}
// 渲染控制点和连线
this.renderControls(
startPoint,
endPoint,
controlPoints[0],
controlPoints[1]
controlPoints[1],
node,
toNode
)
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
this.front()
@@ -346,7 +451,7 @@ class AssociativeLine {
associativeLineWidth,
associativeLineColor,
associativeLineDasharray
} = this.mindMap.themeConfig
} = this.getStyleConfig(fromNode)
if (this.isCreatingLine || !fromNode) return
this.front()
this.isCreatingLine = true
@@ -360,10 +465,14 @@ class AssociativeLine {
})
.fill({ color: 'none' })
// 箭头
this.markerPath
let markerPath = null
const marker = this.createMarker(p => {
markerPath = p
})
markerPath
.stroke({ color: associativeLineColor })
.fill({ color: associativeLineColor })
this.creatingLine.marker('end', this.marker)
this.creatingLine.marker('end', marker)
}
// 取消创建关联线
@@ -529,7 +638,8 @@ class AssociativeLine {
associativeLineTargets,
associativeLinePoint,
associativeLineTargetControlOffsets,
associativeLineText
associativeLineText,
associativeLineStyle
} = node.getData()
associativeLinePoint = associativeLinePoint || []
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
@@ -542,6 +652,15 @@ class AssociativeLine {
}
})
}
// 更新关联线样式数据
let newAssociativeLineStyle = {}
if (associativeLineStyle) {
Object.keys(associativeLineStyle).forEach(item => {
if (item !== toNode.getData('uid')) {
newAssociativeLineStyle[item] = associativeLineStyle[item]
}
})
}
this.mindMap.execCommand('SET_NODE_DATA', node, {
// 目标
associativeLineTargets: associativeLineTargets.filter((_, index) => {
@@ -558,7 +677,9 @@ class AssociativeLine {
})
: [],
// 文本
associativeLineText: newAssociativeLineText
associativeLineText: newAssociativeLineText,
// 样式
associativeLineStyle: newAssociativeLineStyle
})
}
@@ -578,6 +699,7 @@ class AssociativeLine {
this.activeLine = null
this.removeControls()
this.back()
this.mindMap.emit('associative_line_deactivate')
}
}

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

@@ -0,0 +1,117 @@
import { CONSTANTS } from '../constants/constant'
// 该插件会向节点数据的data中添加dir字段
/*
需要更新数据的情况:
1.实例化时的数据
2.调用setData和updateData方法
3.执行完命令
4.切换结构
*/
class MindMapLayoutPro {
constructor(opt) {
this.opt = opt
this.mindMap = opt.mindMap
this.init()
}
init() {
this.updateNodeTree = this.updateNodeTree.bind(this)
this.afterExecCommand = this.afterExecCommand.bind(this)
this.layoutChange = this.layoutChange.bind(this)
// 处理实例化时传入的数据
if (this.mindMap.opt.data && this.isMindMapLayout()) {
this.updateNodeTree(this.mindMap.opt.data)
}
this.mindMap.on('layout_change', this.layoutChange)
this.mindMap.on('afterExecCommand', this.afterExecCommand)
this.mindMap.on('before_update_data', this.updateNodeTree)
this.mindMap.on('before_set_data', this.updateNodeTree)
}
restore() {
this.mindMap.off('layout_change', this.layoutChange)
this.mindMap.off('afterExecCommand', this.afterExecCommand)
this.mindMap.off('before_update_data', this.updateNodeTree)
this.mindMap.off('before_set_data', this.updateNodeTree)
}
// 监听命令执行后的事件
afterExecCommand(name) {
if (!this.isMindMapLayout()) return
if (
![
'BACK',
'FORWARD',
'INSERT_NODE',
'INSERT_MULTI_NODE',
'INSERT_CHILD_NODE',
'INSERT_MULTI_CHILD_NODE',
'INSERT_PARENT_NODE',
'UP_NODE',
'DOWN_NODE',
'MOVE_UP_ONE_LEVEL',
'INSERT_AFTER',
'INSERT_BEFORE',
'MOVE_NODE_TO',
'REMOVE_NODE',
'REMOVE_CURRENT_NODE',
'PASTE_NODE',
'CUT_NODE'
].includes(name)
)
return
this.updateRenderTree()
}
// 更新布局结构
layoutChange(layout) {
if (layout === CONSTANTS.LAYOUT.MIND_MAP) {
this.updateRenderTree()
}
}
// 更新当前的渲染树
updateRenderTree() {
this.updateNodeTree(this.mindMap.renderer.renderTree)
}
// 更新节点树,修改二级节点的排列位置
updateNodeTree(tree) {
if (!this.isMindMapLayout()) return
const root = tree
const childrenLength = root.children.length
if (childrenLength <= 0) return
const center = Math.ceil(childrenLength / 2)
root.children.forEach((item, index) => {
if (index + 1 <= center) {
item.data.dir = CONSTANTS.LAYOUT_GROW_DIR.RIGHT
} else {
item.data.dir = CONSTANTS.LAYOUT_GROW_DIR.LEFT
}
})
}
// 判断当前是否是思维导图布局结构
isMindMapLayout() {
return this.mindMap.opt.layout === CONSTANTS.LAYOUT.MIND_MAP
}
// 插件被移除前做的事情
beforePluginRemove() {
this.restore()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.restore()
}
}
MindMapLayoutPro.instanceName = 'mindMapLayoutPro'
export default MindMapLayoutPro

View File

@@ -289,11 +289,14 @@ class NodeImgAdjust {
// 隐藏自定义元素
this.hideHandleEl()
// 更新节点图片为新的大小
const { image, imageTitle, imageSize } = this.node.getData()
const { image, imageTitle } = this.node.getData()
const { scaleX, scaleY } = this.mousedownDrawTransform
const newWidth = this.currentImgWidth / scaleX
const newHeight = this.currentImgHeight / scaleY
if (newWidth !== imageSize.width || newHeight !== imageSize.height) {
if (
Math.abs(newWidth - this.rect.width) > 1 ||
Math.abs(newHeight - this.rect.height) > 1
) {
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, {
url: image,
title: imageTitle,

View File

@@ -4,15 +4,15 @@ import 'quill/dist/quill.snow.css'
import {
walk,
getTextFromHtml,
isWhite,
getVisibleColorFromTheme,
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'
@@ -55,7 +55,6 @@ class RichText {
this.isInserting = false
this.styleEl = null
this.cacheEditingText = ''
this.lostStyle = false
this.isCompositing = false
this.textNodePaddingX = 6
this.textNodePaddingY = 4
@@ -64,10 +63,7 @@ class RichText {
this.appendCss()
this.bindEvent()
// 处理数据,转成富文本格式
if (this.mindMap.opt.data) {
this.mindMap.opt.data = this.handleSetData(this.mindMap.opt.data)
}
this.handleDataToRichTextOnInit()
}
// 绑定事件
@@ -75,9 +71,12 @@ class RichText {
this.onCompositionStart = this.onCompositionStart.bind(this)
this.onCompositionUpdate = this.onCompositionUpdate.bind(this)
this.onCompositionEnd = this.onCompositionEnd.bind(this)
this.handleSetData = this.handleSetData.bind(this)
window.addEventListener('compositionstart', this.onCompositionStart)
window.addEventListener('compositionupdate', this.onCompositionUpdate)
window.addEventListener('compositionend', this.onCompositionEnd)
this.mindMap.on('before_update_data', this.handleSetData)
this.mindMap.on('before_set_data', this.handleSetData)
}
// 解绑事件
@@ -85,6 +84,8 @@ class RichText {
window.removeEventListener('compositionstart', this.onCompositionStart)
window.removeEventListener('compositionupdate', this.onCompositionUpdate)
window.removeEventListener('compositionend', this.onCompositionEnd)
this.mindMap.off('before_update_data', this.handleSetData)
this.mindMap.off('before_set_data', this.handleSetData)
}
// 插入样式
@@ -94,19 +95,16 @@ class RichText {
`
.smm-richtext-node-wrap {
word-break: break-all;
}
.smm-richtext-node-wrap p {
font-family: auto;
user-select: none;
}
`
)
let cssText = `
.ql-editor {
.${CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP} {
overflow: hidden;
padding: 0;
height: auto;
line-height: normal;
line-height: 1.2;
-webkit-user-select: text;
}
@@ -118,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'
@@ -184,12 +178,13 @@ class RichText {
return
}
let {
richTextEditFakeInPlace,
customInnerElsAppendTo,
nodeTextEditZIndex,
textAutoWrapWidth,
selectTextOnEnterEditText,
transformRichTextOnEnterEdit
transformRichTextOnEnterEdit,
openRealtimeRenderOnNodeTextEdit,
autoEmptyTextWhenKeydownEnterEdit
} = this.mindMap.opt
textAutoWrapWidth = node.hasCustomWidth()
? node.customTextWidth
@@ -206,27 +201,26 @@ class RichText {
let originWidth = g.attr('data-width')
let originHeight = g.attr('data-height')
// 缩放值
let scaleX = rect.width / originWidth
let scaleY = rect.height / originHeight
const scaleX = Math.ceil(rect.width) / originWidth
const scaleY = Math.ceil(rect.height) / originHeight
// 内边距
let paddingX = this.textNodePaddingX
let paddingY = this.textNodePaddingY
if (richTextEditFakeInPlace) {
let paddingValue = node.getPaddingVale()
paddingX = paddingValue.paddingX
paddingY = paddingValue.paddingY
}
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.classList.add('smm-richtext-node-edit-wrap')
this.textEditNode.style.cssText = `
position:fixed;
box-sizing: border-box;
box-shadow: 0 0 20px rgba(0,0,0,.5);
outline: none;
word-break:
break-all;
position:fixed;
box-sizing: border-box;
${
openRealtimeRenderOnNodeTextEdit
? ''
: 'box-shadow: 0 0 20px rgba(0,0,0,.5);'
}
outline: none;
word-break: break-all;
padding: ${paddingY}px ${paddingX}px;
line-height: 1.2;
`
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
@@ -242,10 +236,14 @@ 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
this.textEditNode.style.background = this.getBackground(node)
if (!openRealtimeRenderOnNodeTextEdit) {
this.textEditNode.style.background =
this.mindMap.renderer.textEdit.getBackground(node)
}
this.textEditNode.style.minWidth = originWidth + paddingX * 2 + 'px'
this.textEditNode.style.minHeight = originHeight + 'px'
this.textEditNode.style.left = rect.left + 'px'
@@ -254,13 +252,6 @@ class RichText {
this.textEditNode.style.maxWidth = textAutoWrapWidth + paddingX * 2 + 'px'
this.textEditNode.style.transform = `scale(${scaleX}, ${scaleY})`
this.textEditNode.style.transformOrigin = 'left top'
if (richTextEditFakeInPlace) {
this.textEditNode.style.borderRadius =
(node.style.merge('borderRadius') || 5) + 'px'
if (node.style.merge('shape') == 'roundedRectangle') {
this.textEditNode.style.borderRadius = (node.height || 50) + 'px'
}
}
// 节点文本内容
let nodeText = node.getData('text')
if (typeof transformRichTextOnEnterEdit === 'function') {
@@ -270,11 +261,9 @@ class RichText {
const isEmptyText = isUndef(nodeText)
// 是否是非空的非富文本
const noneEmptyNoneRichText = !node.getData('richText') && !isEmptyText
// 如果是空文本,那么设置为丢失样式状态,否则输入不会带上样式
if (isEmptyText) {
this.lostStyle = true
}
if (noneEmptyNoneRichText) {
if (isFromKeyDown && autoEmptyTextWhenKeydownEnterEdit) {
this.textEditNode.innerHTML = ''
} else if (noneEmptyNoneRichText) {
// 还不是富文本
let text = String(nodeText).split(/\n/gim).join('<br>')
let html = `<p>${text}</p>`
@@ -284,25 +273,51 @@ class RichText {
this.textEditNode.innerHTML = this.cacheEditingText || nodeText
}
this.initQuillEditor()
document.querySelector('.ql-editor').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 = ''
}
// 当openRealtimeRenderOnNodeTextEdit配置更新后需要更新编辑框样式
onOpenRealtimeRenderOnNodeTextEditConfigUpdate(
openRealtimeRenderOnNodeTextEdit
) {
if (!this.textEditNode) return
this.textEditNode.style.background = openRealtimeRenderOnNodeTextEdit
? 'transparent'
: this.node
? this.mindMap.renderer.textEdit.getBackground(this.node)
: ''
this.textEditNode.style.boxShadow = openRealtimeRenderOnNodeTextEdit
? 'none'
: '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
const rect = this.node._textData.node.node.getBoundingClientRect()
const g = this.node._textData.node
const rect = g.node.getBoundingClientRect()
const originWidth = g.attr('data-width')
const originHeight = g.attr('data-height')
this.textEditNode.style.minWidth =
@@ -310,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)
}
// 删除文本编辑框元素
@@ -319,62 +335,18 @@ class RichText {
targetNode.removeChild(this.textEditNode)
}
// 获取编辑区域的背景填充
getBackground(node) {
const gradientStyle = node.style.merge('gradientStyle')
// 当前使用的是渐变色背景
if (gradientStyle) {
const startColor = node.style.merge('startColor')
const endColor = node.style.merge('endColor')
return `linear-gradient(to right, ${startColor}, ${endColor})`
} else {
// 单色背景
const bgColor = node.style.merge('fillColor')
const color = node.style.merge('color')
// 默认使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
return bgColor === 'transparent'
? isWhite(color)
? getVisibleColorFromTheme(this.mindMap.themeConfig)
: '#fff'
: bgColor
}
}
// 如果是非富文本的情况,需要手动应用文本样式
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() {
let html = this.quill.container.firstChild.innerHTML
// https://github.com/slab/quill/issues/4509
return this.quill.container.firstChild.innerHTML.replaceAll(/ +/g, match =>
'&nbsp;'.repeat(match.length)
)
// 去除ql-cursor节点
// https://github.com/wanglin2/mind-map/commit/138cc4b3e824671143f0bf70e5c46796f48520d0
// https://github.com/wanglin2/mind-map/commit/0760500cebe8ec4e8ad84ab63f877b8b2a193aa1
// html = removeHtmlNodeByClass(html, '.ql-cursor')
// 去除最后的空行
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
})
// return html.replace(/<p><br><\/p>$/, '')
}
// 隐藏文本编辑控件,即完成编辑
@@ -386,9 +358,14 @@ 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'
this.showTextEdit = false
this.mindMap.emit('rich_text_selection_change', false)
this.node = null
this.isInserting = false
list.forEach(node => {
this.mindMap.execCommand('SET_NODE_TEXT', node, html, true)
// if (node.isGeneralization) {
@@ -397,12 +374,6 @@ class RichText {
// }
this.mindMap.render()
})
const node = this.node
this.textEditNode.style.display = 'none'
this.showTextEdit = false
this.mindMap.emit('rich_text_selection_change', false)
this.node = null
this.isInserting = false
this.mindMap.emit('hide_text_edit', this.textEditNode, list, node)
}
@@ -464,6 +435,17 @@ class RichText {
}
}
},
formats: [
'bold',
'italic',
'underline',
'strike',
'color',
'background',
'font',
'size',
'formula'
], // 明确指定允许的格式,不包含有序列表,无序列表等
theme: 'snow'
})
// 拦截复制事件即Ctrl + c去除多余的空行
@@ -516,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(),
@@ -618,10 +588,6 @@ class RichText {
return
}
this.isCompositing = false
if (!this.lostStyle) {
return
}
this.setTextStyleIfNotRichText(this.node)
}
// 选中全部
@@ -631,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)
@@ -651,70 +616,25 @@ class RichText {
// 清除当前选中文本的样式
removeFormat() {
// 先移除全部样式
this.formatText({}, true)
// 再将样式恢复为当前主题改节点的默认样式
const style = {}
if (this.node) {
;[
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
].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) {
// 清除文本样式
;[
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
].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
@@ -744,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
@@ -776,40 +696,55 @@ class RichText {
return data
}
// 给未激活的节点设置富文本样式
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 list = [
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
]
const nodeData = node instanceof MindMapNode ? node.getData() : node
for (let i = 0; i < list.length; i++) {
if (nodeData[list[i]] !== undefined) {
// 判断一个对象是否包含了富文本支持的样式字段
isHasRichTextStyle(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (richTextSupportStyleList.includes(key)) {
return true
}
}
return false
}
// 检查指定节点是否存在自定义的富文本样式
checkNodeHasCustomRichTextStyle(node) {
const nodeData = node instanceof MindMapNode ? node.getData() : node
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) {
@@ -830,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
@@ -257,18 +264,16 @@ class Search {
replaceText = String(replaceText)
// 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的
const keep = replaceText.includes(this.searchText)
const hasRichTextPlugin = this.mindMap.renderer.hasRichTextPlugin()
this.notResetSearchText = true
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

@@ -6,8 +6,8 @@ import {
} from './associativeLineUtils'
// 创建控制点、连线节点
function createControlNodes() {
let { associativeLineActiveColor } = this.mindMap.themeConfig
function createControlNodes(node, toNode) {
let { associativeLineActiveColor } = this.getStyleConfig(node, toNode)
// 连线
this.controlLine1 = this.associativeLineDraw
.line()
@@ -16,13 +16,13 @@ function createControlNodes() {
.line()
.stroke({ color: associativeLineActiveColor, width: 2 })
// 控制点
this.controlPoint1 = this.createOneControlNode('controlPoint1')
this.controlPoint2 = this.createOneControlNode('controlPoint2')
this.controlPoint1 = this.createOneControlNode('controlPoint1', node, toNode)
this.controlPoint2 = this.createOneControlNode('controlPoint2', node, toNode)
}
// 创建控制点
function createOneControlNode(pointKey) {
let { associativeLineActiveColor } = this.mindMap.themeConfig
function createOneControlNode(pointKey, node, toNode) {
let { associativeLineActiveColor } = this.getStyleConfig(node, toNode)
return this.associativeLineDraw
.circle(this.controlPointDiameter)
.stroke({ color: associativeLineActiveColor })
@@ -202,6 +202,7 @@ function onControlPointMouseup(e) {
associativeLineTargetControlOffsets: offsetList,
associativeLinePoint
})
this.isNotRenderAllLines = true
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚所以重置isControlPointMousedown需要等draw_click事件触发完以后
setTimeout(() => {
this.resetControlPoint()
@@ -221,10 +222,10 @@ function resetControlPoint() {
}
// 渲染控制点
function renderControls(startPoint, endPoint, point1, point2) {
function renderControls(startPoint, endPoint, point1, point2, node, toNode) {
if (!this.mindMap.opt.enableAdjustAssociativeLinePoints) return
if (!this.controlLine1) {
this.createControlNodes()
this.createControlNodes(node, toNode)
}
let radius = this.controlPointDiameter / 2
// 控制点和起终点的连线

View File

@@ -43,6 +43,7 @@ function showEditTextBox(g) {
// 输入框元素没有创建过,则先创建
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.className = 'associative-line-text-edit-warp'
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
this.textEditNode.setAttribute('contenteditable', true)
this.textEditNode.addEventListener('keyup', e => {
@@ -54,14 +55,14 @@ function showEditTextBox(g) {
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
let [, , , node, toNode] = this.activeLine
let {
associativeLineTextFontSize,
associativeLineTextFontFamily,
associativeLineTextLineHeight
} = this.mindMap.themeConfig
} = this.getStyleConfig(node, toNode)
let { defaultAssociativeLineText, nodeTextEditZIndex } = this.mindMap.opt
let scale = this.mindMap.view.scale
let [, , , node, toNode] = this.activeLine
let text = this.getText(node, toNode)
let textLines = (text || defaultAssociativeLineText).split(/\n/gim)
this.textEditNode.style.fontFamily = associativeLineTextFontFamily
@@ -124,7 +125,7 @@ function hideEditTextBox() {
this.textEditNode.style.display = 'none'
this.textEditNode.innerHTML = ''
this.showTextEdit = false
this.renderText(str, path, text)
this.renderText(str, path, text, node, toNode)
this.mindMap.emit('hide_text_edit')
}
@@ -138,35 +139,41 @@ function getText(node, toNode) {
}
// 渲染关联线文字
function renderText(str, path, text) {
function renderText(str, path, text, node, toNode) {
if (!str) return
let { associativeLineTextFontSize, associativeLineTextLineHeight } =
this.mindMap.themeConfig
this.getStyleConfig(node, toNode)
text.clear()
let textArr = str.split(/\n/gim)
let textArr = str.replace(/\n$/g, '').split(/\n/gim)
textArr.forEach((item, index) => {
let node = new Text().text(item)
node.y(associativeLineTextFontSize * associativeLineTextLineHeight * index)
this.styleText(node)
text.add(node)
// 避免尾部的空行不占宽度,导致文本编辑框定位异常的问题
if (item === '') {
item = ''
}
let textNode = new Text().text(item)
textNode.y(
associativeLineTextFontSize * associativeLineTextLineHeight * index
)
this.styleText(textNode, node, toNode)
text.add(textNode)
})
updateTextPos(path, text)
}
// 给文本设置样式
function styleText(node) {
function styleText(textNode, node, toNode) {
let {
associativeLineTextColor,
associativeLineTextFontSize,
associativeLineTextFontFamily
} = this.mindMap.themeConfig
node
} = this.getStyleConfig(node, toNode)
textNode
.fill({
color: associativeLineTextColor
})
.css({
'font-family': associativeLineTextFontFamily,
'font-size': associativeLineTextFontSize
'font-size': associativeLineTextFontSize + 'px'
})
}

View File

@@ -15,6 +15,12 @@ export default {
lineColor: '#549688',
// 连线样式
lineDasharray: 'none',
// 连线是否开启流动效果仅在虚线时有效需要注册LineFlow插件
lineFlow: false,
// 流动效果一个周期的时间单位s
lineFlowDuration: 1,
// 流动方向是否是从父节点到子节点
lineFlowForward: true,
// 连线风格
lineStyle: 'straight', // 曲线curve【仅支持logicalStructure、mindMap、verticalTimeline三种结构】、直线straight、直连direct【仅支持logicalStructure、mindMap、organizationStructure、verticalTimeline四种结构】
// 曲线连接时,根节点和其他节点的连接线样式保持统一,默认根节点为 ( 型,其他节点为 { 型设为true后都为 { 型。仅支持logicalStructure、mindMap两种结构
@@ -72,7 +78,6 @@ export default {
fontSize: 16,
fontWeight: 'bold',
fontStyle: 'normal',
lineHeight: 1.5,
borderColor: 'transparent',
borderWidth: 0,
borderDasharray: 'none',
@@ -89,8 +94,16 @@ export default {
hoverRectColor: '',
// 点鼠标hover和激活时显示的矩形边框的圆角大小
hoverRectRadius: 5
// paddingX: 15,
// paddingY: 5
// 下列样式也支持给节点设置,用于覆盖最外层的设置
// paddingX,
// paddingY,
// lineWidth,
// lineColor,
// lineDasharray,
// lineFlow,
// lineFlowDuration,
// lineFlowForward
// 关联线的所有样式
},
// 二级节点样式
second: {
@@ -103,7 +116,6 @@ export default {
fontSize: 16,
fontWeight: 'normal',
fontStyle: 'normal',
lineHeight: 1.5,
borderColor: '#549688',
borderWidth: 1,
borderDasharray: 'none',
@@ -117,8 +129,6 @@ export default {
lineMarkerDir: 'end',
hoverRectColor: '',
hoverRectRadius: 5
// paddingX: 15,
// paddingY: 5
},
// 三级及以下节点样式
node: {
@@ -131,7 +141,6 @@ export default {
fontSize: 14,
fontWeight: 'normal',
fontStyle: 'normal',
lineHeight: 1.5,
borderColor: 'transparent',
borderWidth: 0,
borderRadius: 5,
@@ -145,8 +154,6 @@ export default {
lineMarkerDir: 'end',
hoverRectColor: '',
hoverRectRadius: 5
// paddingX: 15,
// paddingY: 5
},
// 概要节点样式
generalization: {
@@ -159,7 +166,6 @@ export default {
fontSize: 16,
fontWeight: 'normal',
fontStyle: 'normal',
lineHeight: 1.5,
borderColor: '#549688',
borderWidth: 1,
borderDasharray: 'none',
@@ -172,8 +178,6 @@ export default {
endDir: [1, 0],
hoverRectColor: '',
hoverRectRadius: 5
// paddingX: 15,
// paddingY: 5
}
}
@@ -201,14 +205,12 @@ const nodeSizeIndependenceList = [
'rootLineKeepSameInCurve',
'rootLineStartPositionKeepSameInCurve',
'showLineMarker',
'gradientStyle',
'lineRadius',
'startColor',
'endColor',
'startDir',
'endDir',
'hoverRectColor',
'hoverRectRadius'
'hoverRectRadius',
'lineFlow',
'lineFlowDuration',
'lineFlowForward'
]
export const checkIsNodeSizeIndependenceConfig = config => {
let keys = Object.keys(config)
@@ -224,9 +226,13 @@ export const checkIsNodeSizeIndependenceConfig = config => {
return true
}
// 连线的样式
export const lineStyleProps = [
'lineColor',
'lineDasharray',
'lineWidth',
'lineMarkerDir'
'lineMarkerDir',
'lineFlow',
'lineFlowDuration',
'lineFlowForward'
]

View File

@@ -1,11 +1,13 @@
import { v4 as uuidv4 } from 'uuid'
import {
nodeDataNoStylePropList,
selfCloseTagList
selfCloseTagList,
richTextSupportStyleList
} from '../constants/constant'
import MersenneTwister from './mersenneTwister'
import { ForeignObject } from '@svgdotjs/svg.js'
import merge from 'deepmerge'
import { lineStyleProps } from '../theme/default'
// 深度优先遍历树
export const walk = (
@@ -172,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
}
@@ -182,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
@@ -207,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
}
@@ -276,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
@@ -480,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
@@ -507,13 +537,14 @@ export const addHtmlStyle = (html, tag, style) => {
if (!addHtmlStyleEl) {
addHtmlStyleEl = document.createElement('div')
}
const tags = Array.isArray(tag) ? tag : [tag]
addHtmlStyleEl.innerHTML = html
let walk = root => {
let childNodes = root.childNodes
childNodes.forEach(node => {
if (node.nodeType === 1) {
// 元素节点
if (node.tagName.toLowerCase() === tag) {
if (tags.includes(node.tagName.toLowerCase())) {
node.style.cssText = style
} else {
walk(node)
@@ -790,6 +821,18 @@ export const checkIsNodeStyleDataKey = key => {
return false
}
// 判断一个对象是否不需要触发节点重新创建
export const isNodeNotNeedRenderData = config => {
const list = [...lineStyleProps] // 节点连线样式
const keys = Object.keys(config)
for (let i = 0; i < keys.length; i++) {
if (!list.includes(keys[i])) {
return false
}
}
return true
}
// 合并图标数组
// const data = [
// { type: 'priority', name: '优先级图标', list: [{ name: '1', icon: 'a' }, { name: 2, icon: 'b' }] },
@@ -935,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 = {
@@ -1013,7 +1062,7 @@ export const generateColorByContent = str => {
// html转义
export const htmlEscape = str => {
;[
[
['&', '&amp;'],
['<', '&lt;'],
['>', '&gt;']
@@ -1069,9 +1118,14 @@ export const isSameObject = (a, b) => {
}
}
// 检查navigator.clipboard对象的读取是否可用
export const checkClipboardReadEnable = () => {
return navigator.clipboard && typeof navigator.clipboard.read === 'function'
}
// 将数据设置到用户剪切板中
export const setDataToClipboard = data => {
if (navigator.clipboard) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(JSON.stringify(data))
}
}
@@ -1080,7 +1134,7 @@ export const setDataToClipboard = data => {
export const getDataFromClipboard = async () => {
let text = null
let img = null
if (navigator.clipboard) {
if (checkClipboardReadEnable()) {
const items = await navigator.clipboard.read()
if (items && items.length > 0) {
for (const clipboardItem of items) {
@@ -1182,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)
// 去除换行
@@ -1620,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: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -439,6 +439,11 @@ export const sidebarTriggerList = [
value: 'outline',
icon: 'iconfuhao-dagangshu'
},
{
name: 'Setting',
value: 'setting',
icon: 'iconshezhi'
},
{
name: 'ShortcutKey',
value: 'shortcutKey',

View File

@@ -1,6 +1,5 @@
import {
fontSizeList,
lineHeightList,
colorList,
borderWidthList,
borderRadiusList,
@@ -155,7 +154,6 @@ const linearGradientDirList = {
export {
fontSizeList,
lineHeightList,
borderWidthList,
borderRadiusList,
lineWidthList,

View File

@@ -57,9 +57,6 @@ export const fontFamilyList = [
// 字号
export const fontSizeList = [10, 12, 16, 18, 24, 32, 48]
// 行高
export const lineHeightList = [1, 1.5, 2, 2.5, 3]
// 颜色
export const colorList = [
'#4D4D4D',
@@ -537,6 +534,11 @@ export const sidebarTriggerList = [
value: 'outline',
icon: 'iconfuhao-dagangshu'
},
{
name: '设置',
value: 'setting',
icon: 'iconshezhi'
},
{
name: '快捷键',
value: 'shortcutKey',

View File

@@ -439,6 +439,11 @@ export const sidebarTriggerList = [
value: 'outline',
icon: 'iconfuhao-dagangshu'
},
{
name: '設置',
value: 'setting',
icon: 'iconshezhi'
},
{
name: '快捷鍵',
value: 'shortcutKey',

View File

@@ -26,8 +26,41 @@ export default {
nodeBorderType: 'Node border style',
nodeUseLineStyle: 'Use only has bottom border style',
otherConfig: 'Other config',
enableFreeDrag: 'Enable node free drag(Beta)',
associativeLine: 'Associative line',
associativeLineWidth: 'Width',
associativeLineColor: 'Color',
associativeLineActiveWidth: 'Active width',
associativeLineActiveColor: 'Active color',
rootStyle: 'Root Node',
associativeLineText: 'Associative line text',
fontFamily: 'Font family',
fontSize: 'Font size',
rootLineStartPos: 'Root line start pos',
center: 'Center',
edge: 'Edge',
rainbowLines: 'Rainbow lines',
notUseRainbowLines: 'Not use rainbow lines',
outerFramePadding: 'Outer frame padding'
},
setting: {
title: 'Setting',
openPerformance: 'Enable performance mode',
enableFreeDrag: 'Enable node free drag(Beta)',
isEnableNodeRichText: 'Enable node rich text editing',
mousewheelAction: 'Mouse wheel behavior',
zoomView: 'Zoom view',
moveViewUpDown: 'Move view up and down',
mousewheelZoomActionReverse: 'Mouse Wheel Zoom',
mousewheelZoomActionReverse1: 'Zoom out forward and zoom in back',
mousewheelZoomActionReverse2: 'Zoom in forward and zoom out back',
createNewNodeBehavior: 'Behavior of creating new node',
default: 'Active new node and editing',
notActive: 'Not active new node',
activeOnly: 'Only active new node but not editing',
openRealtimeRenderOnNodeTextEdit:
'Enable real-time rendering effect for text editing',
isShowScrollbar: 'Is show scrollbar',
isUseHandDrawnLikeStyle: 'Is use hand drawn like style',
watermark: 'Watermark',
showWatermark: 'Is show watermark',
onlyExport: 'Only export',
@@ -40,34 +73,16 @@ export default {
watermarkTextOpacity: 'Text opacity',
watermarkTextFontSize: 'Font size',
belowNode: 'Display below nodes',
isEnableNodeRichText: 'Enable node rich text editing',
mousewheelAction: 'Mouse wheel behavior',
zoomView: 'Zoom view',
moveViewUpDown: 'Move view up and down',
associativeLine: 'Associative line',
associativeLineWidth: 'Width',
associativeLineColor: 'Color',
associativeLineActiveWidth: 'Active width',
associativeLineActiveColor: 'Active color',
mousewheelZoomActionReverse: 'Mouse Wheel Zoom',
mousewheelZoomActionReverse1: 'Zoom out forward and zoom in back',
mousewheelZoomActionReverse2: 'Zoom in forward and zoom out back',
createNewNodeBehavior: 'Behavior of creating new node',
default: 'Active new node and editing',
notActive: 'Not active new node',
activeOnly: 'Only active new node but not editing',
rootStyle: 'Root Node',
associativeLineText: 'Associative line text',
fontFamily: 'Font family',
fontSize: 'Font size',
isShowScrollbar: 'Is show scrollbar',
isUseHandDrawnLikeStyle: 'Is use hand drawn like style',
rootLineStartPos: 'Root line start pos',
center: 'Center',
edge: 'Edge',
rainbowLines: 'Rainbow lines',
notUseRainbowLines: 'Not use rainbow lines',
outerFramePadding: 'Outer frame padding'
tagPosition: 'Node tag position',
tagPositionRight: 'Text right',
tagPositionBottom: 'Text bottom',
alwaysShowExpandBtn: 'Always show expand btn',
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'
@@ -112,7 +127,10 @@ export default {
copySuccess: 'Copy success',
copyFail: 'Copy fail',
number: 'Number child nodes',
expandNodeChild: 'Expand all sub nodes'
expandNodeChild: 'Expand all sub nodes',
unExpandNodeChild: 'Un expand all sub nodes',
addToDo: 'Add toDo',
removeToDo: 'Remove toDo'
},
count: {
words: 'Words',
@@ -216,7 +234,6 @@ export default {
text: 'Text',
fontFamily: 'Font family',
fontSize: 'Font size',
lineHeight: 'Line height',
color: 'color',
addFontWeight: 'add font weight',
italic: 'Italic',
@@ -241,7 +258,12 @@ export default {
arrowDir: 'Arrow dir',
arrowDirStart: 'Start',
arrowDirEnd: 'End',
direction: 'Direction'
direction: 'Direction',
selectNodeTip: 'Please select a node',
openLineFlow: 'Open line flow',
lineFlowDuration: 'Line flow duration',
forward: 'Forward',
reverse: 'Reverse'
},
theme: {
title: 'Theme',

View File

@@ -25,9 +25,40 @@ export default {
belowLevel2Node: '三级及以下节点',
nodeBorderType: '节点边框风格',
nodeUseLineStyle: '是否使用只有底边框的风格',
otherConfig: '其他配置',
enableFreeDrag: '是否开启节点自由拖拽',
associativeLine: '关联线',
associativeLineWidth: '粗细',
associativeLineColor: '颜色',
associativeLineActiveWidth: '激活粗细',
associativeLineActiveColor: '激活颜色',
rootStyle: '根节点',
associativeLineText: '关联线文字',
fontFamily: '字体',
fontSize: '字号',
rootLineStartPos: '根节点连线起始位置',
center: '中心',
edge: '边缘',
rainbowLines: '彩虹线条',
notUseRainbowLines: '不使用彩虹线条',
outerFramePadding: '外框内边距'
},
setting: {
title: '设置',
openPerformance: '开启性能模式(Beta)',
enableFreeDrag: '是否开启节点自由拖拽',
isEnableNodeRichText: '是否开启节点富文本编辑',
mousewheelAction: '鼠标滚轮行为',
zoomView: '缩放视图',
moveViewUpDown: '上下移动视图',
mousewheelZoomActionReverse: '鼠标滚轮缩放',
mousewheelZoomActionReverse1: '向前缩小向后放大',
mousewheelZoomActionReverse2: '向前放大向后缩小',
createNewNodeBehavior: '创建新节点的行为',
default: '激活新节点及进入编辑',
notActive: '不激活新节点',
activeOnly: '只激活新节点,不进入编辑',
openRealtimeRenderOnNodeTextEdit: '开启文本编辑实时渲染效果',
isShowScrollbar: '是否显示滚动条',
isUseHandDrawnLikeStyle: '是否开启手绘风格',
watermark: '水印',
showWatermark: '是否显示水印',
watermarkDefaultText: '水印文字',
@@ -40,34 +71,16 @@ export default {
watermarkTextOpacity: '文字透明度',
watermarkTextFontSize: '文字字号',
belowNode: '显示在节点下方',
isEnableNodeRichText: '是否开启节点富文本编辑',
mousewheelAction: '鼠标滚轮行为',
zoomView: '缩放视图',
moveViewUpDown: '上下移动视图',
associativeLine: '关联线',
associativeLineWidth: '粗细',
associativeLineColor: '颜色',
associativeLineActiveWidth: '激活粗细',
associativeLineActiveColor: '激活颜色',
mousewheelZoomActionReverse: '鼠标滚轮缩放',
mousewheelZoomActionReverse1: '向前缩小向后放大',
mousewheelZoomActionReverse2: '向前放大向后缩小',
createNewNodeBehavior: '创建新节点的行为',
default: '激活新节点及进入编辑',
notActive: '不激活新节点',
activeOnly: '只激活新节点,不进入编辑',
rootStyle: '根节点',
associativeLineText: '关联线文字',
fontFamily: '字体',
fontSize: '字号',
isShowScrollbar: '是否显示滚动条',
isUseHandDrawnLikeStyle: '是否开启手绘风格',
rootLineStartPos: '根节点连线起始位置',
center: '中心',
edge: '边缘',
rainbowLines: '彩虹线条',
notUseRainbowLines: '不使用彩虹线条',
outerFramePadding: '外框内边距'
tagPosition: '节点标签显示的位置',
tagPositionRight: '文本右侧',
tagPositionBottom: '文本下面',
alwaysShowExpandBtn: '是否一直显示展开收起按钮',
enableAutoEnterTextEditWhenKeydown: '键盘输入时自动进入文本编辑',
confirm: '确定',
cancel: '取消',
changeRichTextTip: '该操作会清空所有历史修改记录,并且修改思维导图数据,是否继续?',
changeRichTextTip2: '是否切换为富文本模式?',
changeRichTextTip3: '是否切换为非富文本模式?'
},
color: {
moreColor: '更多颜色'
@@ -112,7 +125,10 @@ export default {
copySuccess: '复制成功',
copyFail: '复制失败',
number: '编号其子节点',
expandNodeChild: '展开所有下级节点'
expandNodeChild: '展开所有下级节点',
unExpandNodeChild: '收起所有下级节点',
addToDo: '添加待办',
removeToDo: '删除待办'
},
count: {
words: '字数',
@@ -214,7 +230,6 @@ export default {
text: '文字',
fontFamily: '字体',
fontSize: '字号',
lineHeight: '行高',
color: '颜色',
addFontWeight: '加粗',
italic: '斜体',
@@ -239,7 +254,12 @@ export default {
arrowDir: '箭头位置',
arrowDirStart: '头部',
arrowDirEnd: '尾部',
direction: '方向'
direction: '方向',
selectNodeTip: '请选择一个节点',
openLineFlow: '开启流动效果',
lineFlowDuration: '一个流动周期的时间',
forward: '正向',
reverse: '反向'
},
theme: {
title: '主题',

View File

@@ -26,8 +26,45 @@ export default {
nodeBorderType: '節點邊框樣式',
nodeUseLineStyle: '僅使用底邊框樣式',
otherConfig: '其他設定',
enableFreeDrag: '啟用節點自由拖曳 (Beta)',
associativeLine: '關聯線',
associativeLineWidth: '寬度',
associativeLineColor: '顏色',
associativeLineActiveWidth: '啟用時寬度',
associativeLineActiveColor: '啟用時顏色',
rootStyle: '根節點',
associativeLineText: '關聯線文字',
fontFamily: '字型',
fontSize: '字型大小',
rootLineStartPos: '根節點連線起始位置',
center: '中心',
edge: '邊緣',
rainbowLines: '彩虹線條',
notUseRainbowLines: '不使用彩虹線條',
outerFramePadding: '外框內距',
tagPosition: '節點標簽顯示的位置',
tagPositionRight: '文本右側',
tagPositionBottom: '文本下面',
alwaysShowExpandBtn: '是否壹直顯示展開收起按鈕',
enableAutoEnterTextEditWhenKeydown: '鍵盤輸入時自動進入文本編輯'
},
setting: {
title: '設置',
openPerformance: '啟用效能模式',
enableFreeDrag: '啟用節點自由拖曳 (Beta)',
isEnableNodeRichText: '啟用節點豐富文字編輯',
mousewheelAction: '滑鼠滾輪行為',
zoomView: '縮放檢視',
moveViewUpDown: '上下移動檢視',
mousewheelZoomActionReverse: '滑鼠滾輪縮放',
mousewheelZoomActionReverse1: '向前縮小,向後放大',
mousewheelZoomActionReverse2: '向前放大,向後縮小',
createNewNodeBehavior: '建立新節點行為',
default: '啟用新節點並進入編輯',
notActive: '不啟用新節點',
activeOnly: '僅啟用新節點,不進入編輯',
openRealtimeRenderOnNodeTextEdit: '開啟文本編輯實時渲染效果',
isShowScrollbar: '顯示捲軸',
isUseHandDrawnLikeStyle: '使用手繪風格',
watermark: '浮水印',
showWatermark: '顯示浮水印',
onlyExport: '僅在匯出時顯示',
@@ -40,34 +77,11 @@ export default {
watermarkTextOpacity: '文字透明度',
watermarkTextFontSize: '字型大小',
belowNode: '顯示在節點下方',
isEnableNodeRichText: '啟用節點豐富文字編輯',
mousewheelAction: '滑鼠滾輪行為',
zoomView: '縮放檢視',
moveViewUpDown: '上下移動檢視',
associativeLine: '關聯線',
associativeLineWidth: '寬度',
associativeLineColor: '顏色',
associativeLineActiveWidth: '啟用時寬度',
associativeLineActiveColor: '啟用時顏色',
mousewheelZoomActionReverse: '滑鼠滾輪縮放',
mousewheelZoomActionReverse1: '向前縮小,向後放大',
mousewheelZoomActionReverse2: '向前放大,向後縮小',
createNewNodeBehavior: '建立新節點行為',
default: '啟用新節點並進入編輯',
notActive: '不啟用新節點',
activeOnly: '僅啟用新節點,不進入編輯',
rootStyle: '根節點',
associativeLineText: '關聯線文字',
fontFamily: '字型',
fontSize: '字型大小',
isShowScrollbar: '顯示捲軸',
isUseHandDrawnLikeStyle: '使用手繪風格',
rootLineStartPos: '根節點連線起始位置',
center: '中心',
edge: '邊緣',
rainbowLines: '彩虹線條',
notUseRainbowLines: '不使用彩虹線條',
outerFramePadding: '外框內距'
confirm: '確定',
cancel: '取消',
changeRichTextTip: '該操作會清空所有曆史修改記錄,並且修改思維導圖數據,是否繼續?',
changeRichTextTip2: '是否切換爲富文本模式?',
changeRichTextTip3: '是否切換爲非富文本模式?'
},
color: {
moreColor: '更多顏色'
@@ -112,7 +126,10 @@ export default {
copySuccess: '複製成功',
copyFail: '複製失敗',
number: '將其子節點編號',
expandNodeChild: '展開所有下級節點'
expandNodeChild: '展開所有下級節點',
unExpandNodeChild: '收起所有下級節點',
addToDo: '添加待辦',
removeToDo: '刪除待辦'
},
count: {
words: '字數',
@@ -214,7 +231,6 @@ export default {
text: '文字',
fontFamily: '字型',
fontSize: '字型大小',
lineHeight: '行高',
color: '顏色',
addFontWeight: '粗體',
italic: '斜體',
@@ -238,7 +254,12 @@ export default {
endColor: '結束',
arrowDir: '箭頭位置',
arrowDirStart: '頭部',
arrowDirEnd: '尾部'
arrowDirEnd: '尾部',
selectNodeTip: '請選擇壹個節點',
openLineFlow: '開啓流動效果',
lineFlowDuration: '一個流動周期的時間',
forward: '正向',
reverse: '反向'
},
theme: {
title: '主題',

View File

@@ -0,0 +1,420 @@
<template>
<Sidebar ref="sidebar" :title="'关联线样式'">
<div class="sidebarContent" :class="{ isDark: isDark }">
<div class="title noTop">{{ $t('baseStyle.associativeLine') }}</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.associativeLineColor') }}</span>
<span
class="block"
v-popover:popover4
:style="{ backgroundColor: style.associativeLineColor }"
></span>
<el-popover ref="popover4" placement="bottom" trigger="click">
<Color
:color="style.associativeLineColor"
@change="
color => {
update('associativeLineColor', color)
}
"
></Color>
</el-popover>
</div>
<div class="rowItem">
<span class="name">{{ $t('baseStyle.associativeLineWidth') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.associativeLineWidth"
placeholder=""
@change="
value => {
update('associativeLineWidth', value)
}
"
>
<el-option
v-for="item in lineWidthList"
:key="item"
:label="item"
:value="item"
>
<span
v-if="item > 0"
class="borderLine"
:class="{ isDark: isDark }"
:style="{ height: item + 'px' }"
></span>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">{{
$t('baseStyle.associativeLineActiveColor')
}}</span>
<span
class="block"
v-popover:popover5
:style="{ backgroundColor: style.associativeLineActiveColor }"
></span>
<el-popover ref="popover5" placement="bottom" trigger="click">
<Color
:color="style.associativeLineActiveColor"
@change="
color => {
update('associativeLineActiveColor', color)
}
"
></Color>
</el-popover>
</div>
<div class="rowItem">
<span class="name">{{
$t('baseStyle.associativeLineActiveWidth')
}}</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.associativeLineActiveWidth"
placeholder=""
@change="
value => {
update('associativeLineActiveWidth', value)
}
"
>
<el-option
v-for="item in lineWidthList"
:key="item"
:label="item"
:value="item"
>
<span
v-if="item > 0"
class="borderLine"
:class="{ isDark: isDark }"
:style="{ height: item + 'px' }"
></span>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('style.style') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.associativeLineDasharray"
placeholder=""
@change="
value => {
update('associativeLineDasharray', value)
}
"
>
<el-option
v-for="item in borderDasharrayList"
:key="item.value"
:label="item.name"
:value="item.value"
>
<svg width="120" height="34">
<line
x1="10"
y1="17"
x2="110"
y2="17"
stroke-width="2"
:stroke="
style.associativeLineDasharray === item.value
? '#409eff'
: isDark
? '#fff'
: '#000'
"
:stroke-dasharray="item.value"
></line>
</svg>
</el-option>
</el-select>
</div>
</div>
<!-- 关联线文字 -->
<div class="title noTop">{{ $t('baseStyle.associativeLineText') }}</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.fontFamily') }}</span>
<el-select
size="mini"
v-model="style.associativeLineTextFontFamily"
placeholder=""
@change="update('associativeLineTextFontFamily', $event)"
>
<el-option
v-for="item in fontFamilyList"
:key="item.value"
:label="item.name"
:value="item.value"
:style="{ fontFamily: item.value }"
>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.color') }}</span>
<span
class="block"
v-popover:popover6
:style="{ backgroundColor: style.associativeLineTextColor }"
></span>
<el-popover ref="popover6" placement="bottom" trigger="click">
<Color
:color="style.associativeLineTextColor"
@change="
color => {
update('associativeLineTextColor', color)
}
"
></Color>
</el-popover>
</div>
<div class="rowItem">
<span class="name">{{ $t('baseStyle.fontSize') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.associativeLineTextFontSize"
placeholder=""
@change="update('associativeLineTextFontSize', $event)"
>
<el-option
v-for="item in fontSizeList"
:key="item"
:label="item"
:value="item"
:style="{ fontSize: item + 'px' }"
>
</el-option>
</el-select>
</div>
</div>
</div>
</Sidebar>
</template>
<script>
import Sidebar from './Sidebar'
import Color from './Color'
import {
lineWidthList,
lineStyleList,
backgroundRepeatList,
backgroundPositionList,
backgroundSizeList,
fontFamilyList,
fontSizeList,
rootLineKeepSameInCurveList,
lineStyleMap,
borderDasharrayList
} from '@/config'
import { mapState, mapMutations } from 'vuex'
import {
supportLineStyleLayoutsMap,
supportLineRadiusLayouts,
supportNodeUseLineStyleLayouts,
supportRootLineKeepSameInCurveLayouts,
rainbowLinesOptions
} from '@/config/constant'
const defaultStyle = {
associativeLineColor: '',
associativeLineWidth: 0,
associativeLineActiveWidth: 0,
associativeLineDasharray: '',
associativeLineActiveColor: '',
associativeLineTextFontSize: 0,
associativeLineTextColor: '',
associativeLineTextFontFamily: ''
}
export default {
name: 'BaseStyle',
components: {
Sidebar,
Color
},
props: {
mindMap: {
type: Object
}
},
data() {
return {
lineWidthList,
fontSizeList,
activeLineNode: null,
activeLineToNode: null,
style: {
...defaultStyle
}
}
},
computed: {
...mapState({
activeSidebar: state => state.activeSidebar,
isDark: state => state.localConfig.isDark
}),
fontFamilyList() {
return fontFamilyList[this.$i18n.locale] || fontFamilyList.zh
},
borderDasharrayList() {
return borderDasharrayList[this.$i18n.locale] || borderDasharrayList.zh
}
},
watch: {
activeSidebar(val) {
if (val === 'associativeLineStyle') {
this.$refs.sidebar.show = true
} else {
this.$refs.sidebar.show = false
}
}
},
created() {
this.mindMap.on('associative_line_click', this.onAssociativeLineClick)
this.mindMap.on(
'associative_line_deactivate',
this.associativeLineDeactivate
)
},
methods: {
...mapMutations(['setActiveSidebar']),
onAssociativeLineClick(a, b, node, toNode) {
this.activeLineNode = node
this.activeLineToNode = toNode
const styleConfig = this.mindMap.associativeLine.getStyleConfig(
node,
toNode
)
Object.keys(this.style).forEach(item => {
this.style[item] = styleConfig[item]
})
this.setActiveSidebar('associativeLineStyle')
},
associativeLineDeactivate() {
this.setActiveSidebar('')
this.activeLineNode = null
this.activeLineToNode = null
this.style = {
...defaultStyle
}
},
update(prop, value) {
this.style[prop] = value
const associativeLineStyle =
this.activeLineNode.getData('associativeLineStyle') || {}
const toNodeUid = this.activeLineToNode.getData('uid')
const lineStyle = associativeLineStyle[toNodeUid] || {}
this.activeLineNode.setData({
associativeLineStyle: {
...associativeLineStyle,
[toNodeUid]: {
...lineStyle,
...this.style
}
}
})
this.mindMap.associativeLine.updateActiveLineStyle()
}
}
}
</script>
<style lang="less" scoped>
.sidebarContent {
padding: 20px;
padding-top: 10px;
&.isDark {
.title {
color: #fff;
}
.row {
.rowItem {
.name {
color: hsla(0, 0%, 100%, 0.6);
}
}
}
}
.title {
font-size: 16px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: rgba(26, 26, 26, 0.9);
margin-bottom: 10px;
margin-top: 20px;
&.noTop {
margin-top: 0;
}
}
.row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
.rowItem {
display: flex;
align-items: center;
margin-bottom: 5px;
.name {
font-size: 12px;
margin-right: 10px;
white-space: nowrap;
}
.block {
display: inline-block;
width: 30px;
height: 30px;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
}
}
}
}
.borderLine {
display: inline-block;
width: 100%;
background-color: #000;
&.isDark {
background-color: #fff;
}
}
</style>
<style lang="less">
.el-select-dropdown__item.selected {
.borderLine {
background-color: #409eff;
}
}
</style>

View File

@@ -273,6 +273,61 @@
>
</div>
</div>
<!-- 流动效果 -->
<div class="row" v-if="supportLineFlow">
<div class="rowItem">
<span class="name">{{ $t('style.openLineFlow') }}</span>
<el-checkbox
v-model="style.lineFlow"
@change="
value => {
update('lineFlow', value)
}
"
></el-checkbox>
</div>
<div class="rowItem">
<span class="name">{{ $t('style.direction') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.lineFlowForward"
placeholder=""
@change="
value => {
update('lineFlowForward', value)
}
"
>
<el-option
key="1"
:label="$t('style.forward')"
:value="true"
></el-option>
<el-option
key="2"
:label="$t('style.reverse')"
:value="false"
></el-option>
</el-select>
</div>
</div>
<div class="row" v-if="supportLineFlow">
<div class="rowItem">
<span class="name">{{ $t('style.lineFlowDuration') }}</span>
<el-input-number
v-model="style.lineFlowDuration"
@change="
value => {
update('lineFlowDuration', value)
}
"
:min="0.1"
size="mini"
:step="0.5"
></el-input-number>
</div>
</div>
<!-- 彩虹线条 -->
<div class="title noTop">{{ $t('baseStyle.rainbowLines') }}</div>
<div class="row">
@@ -714,146 +769,6 @@
></el-slider>
</div>
</div>
<!-- 水印 -->
<div class="title noTop">{{ $t('baseStyle.watermark') }}</div>
<div class="row">
<!-- 是否显示水印 -->
<div class="rowItem">
<el-checkbox
v-model="watermarkConfig.show"
@change="watermarkShowChange"
>{{ $t('baseStyle.showWatermark') }}</el-checkbox
>
</div>
</div>
<template v-if="watermarkConfig.show">
<!-- 是否仅在导出时显示 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="watermarkConfig.onlyExport"
@change="updateWatermarkConfig"
>{{ $t('baseStyle.onlyExport') }}</el-checkbox
>
</div>
</div>
<!-- 是否在节点下方 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="watermarkConfig.belowNode"
@change="updateWatermarkConfig"
>{{ $t('baseStyle.belowNode') }}</el-checkbox
>
</div>
</div>
<!-- 水印文字 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkText') }}</span>
<el-input
v-model="watermarkConfig.text"
size="small"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input>
</div>
</div>
<!-- 水印文字颜色 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkTextColor') }}</span>
<span
class="block"
v-popover:popover3
:style="{ backgroundColor: watermarkConfig.textStyle.color }"
></span>
<el-popover ref="popover3" placement="bottom" trigger="click">
<Color
:color="watermarkConfig.textStyle.color"
@change="
value => {
watermarkConfig.textStyle.color = value
updateWatermarkConfig()
}
"
></Color>
</el-popover>
</div>
</div>
<!-- 水印文字透明度 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkTextOpacity') }}</span>
<el-slider
v-model="watermarkConfig.textStyle.opacity"
style="width: 170px"
:min="0"
:max="1"
:step="0.1"
@change="updateWatermarkConfig"
></el-slider>
</div>
</div>
<!-- 水印文字字号 -->
<div class="row">
<div class="rowItem">
<span class="name">{{
$t('baseStyle.watermarkTextFontSize')
}}</span>
<el-input-number
v-model="watermarkConfig.textStyle.fontSize"
size="small"
:min="0"
:max="50"
:step="1"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
<!-- 旋转角度 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkAngle') }}</span>
<el-input-number
v-model="watermarkConfig.angle"
size="small"
:min="0"
:max="90"
:step="10"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
<!-- 水印行间距 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkLineSpacing') }}</span>
<el-input-number
v-model="watermarkConfig.lineSpacing"
size="small"
:step="10"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
<!-- 水印文字间距 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.watermarkTextSpacing') }}</span>
<el-input-number
v-model="watermarkConfig.textSpacing"
size="small"
:step="10"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
</template>
<!-- 外框内边距 -->
<div class="title noTop">{{ $t('baseStyle.outerFramePadding') }}</div>
<div class="row">
@@ -884,150 +799,6 @@
></el-slider>
</div>
</div>
<!-- 其他配置 -->
<div class="title noTop">{{ $t('baseStyle.otherConfig') }}</div>
<!-- 配置性能模式 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="config.openPerformance"
@change="
value => {
updateOtherConfig('openPerformance', value)
}
"
>{{ $t('baseStyle.openPerformance') }}</el-checkbox
>
</div>
</div>
<!-- 配置开启自由拖拽 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="config.enableFreeDrag"
@change="
value => {
updateOtherConfig('enableFreeDrag', value)
}
"
>{{ $t('baseStyle.enableFreeDrag') }}</el-checkbox
>
</div>
</div>
<!-- 配置是否启用富文本编辑 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="enableNodeRichText"
@change="enableNodeRichTextChange"
>{{ $t('baseStyle.isEnableNodeRichText') }}</el-checkbox
>
</div>
</div>
<!-- 配置鼠标滚轮行为 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.mousewheelAction') }}</span>
<el-select
size="mini"
style="width: 120px"
v-model="config.mousewheelAction"
placeholder=""
@change="
value => {
updateOtherConfig('mousewheelAction', value)
}
"
>
<el-option
:label="$t('baseStyle.zoomView')"
value="zoom"
></el-option>
<el-option
:label="$t('baseStyle.moveViewUpDown')"
value="move"
></el-option>
</el-select>
</div>
</div>
<!-- 配置鼠标缩放行为 -->
<div class="row" v-if="config.mousewheelAction === 'zoom'">
<div class="rowItem">
<span class="name">{{
$t('baseStyle.mousewheelZoomActionReverse')
}}</span>
<el-select
size="mini"
style="width: 120px"
v-model="config.mousewheelZoomActionReverse"
placeholder=""
@change="
value => {
updateOtherConfig('mousewheelZoomActionReverse', value)
}
"
>
<el-option
:label="$t('baseStyle.mousewheelZoomActionReverse1')"
:value="false"
></el-option>
<el-option
:label="$t('baseStyle.mousewheelZoomActionReverse2')"
:value="true"
></el-option>
</el-select>
</div>
</div>
<!-- 配置创建新节点时的行为 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('baseStyle.createNewNodeBehavior') }}</span>
<el-select
size="mini"
style="width: 120px"
v-model="config.createNewNodeBehavior"
placeholder=""
@change="
value => {
updateOtherConfig('createNewNodeBehavior', value)
}
"
>
<el-option
:label="$t('baseStyle.default')"
value="default"
></el-option>
<el-option
:label="$t('baseStyle.notActive')"
value="notActive"
></el-option>
<el-option
:label="$t('baseStyle.activeOnly')"
value="activeOnly"
></el-option>
</el-select>
</div>
</div>
<!-- 是否显示滚动条 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="localConfigs.isShowScrollbar"
@change="updateLocalConfig('isShowScrollbar', $event)"
>{{ $t('baseStyle.isShowScrollbar') }}</el-checkbox
>
</div>
</div>
<!-- 是否开启手绘风格 -->
<div class="row" v-if="supportHandDrawnLikeStyle">
<div class="rowItem">
<el-checkbox
v-model="localConfigs.isUseHandDrawnLikeStyle"
@change="updateLocalConfig('isUseHandDrawnLikeStyle', $event)"
>{{ $t('baseStyle.isUseHandDrawnLikeStyle') }}</el-checkbox
>
</div>
</div>
</div>
</Sidebar>
</template>
@@ -1095,6 +866,9 @@ export default {
rootLineKeepSameInCurve: '',
rootLineStartPositionKeepSameInCurve: '',
lineRadius: 0,
lineFlow: false,
lineFlowForward: true,
lineFlowDuration: 1,
generalizationLineWidth: '',
generalizationLineColor: '',
associativeLineColor: '',
@@ -1118,34 +892,8 @@ export default {
marginY: 0,
nodeUseLineStyle: false
},
config: {
openPerformance: false,
enableFreeDrag: false,
mousewheelAction: 'zoom',
mousewheelZoomActionReverse: false,
createNewNodeBehavior: 'default'
},
watermarkConfig: {
show: false,
onlyExport: false,
text: '',
lineSpacing: 100,
textSpacing: 100,
angle: 30,
textStyle: {
color: '',
opacity: 0,
fontSize: 1
}
},
rainbowLinesPopoverVisible: false,
curRainbowLineColorList: null,
updateWatermarkTimer: null,
enableNodeRichText: true,
localConfigs: {
isShowScrollbar: false,
isUseHandDrawnLikeStyle: false
},
currentLayout: '', // 当前结构
outerFramePadding: {
outerFramePaddingX: 0,
@@ -1158,7 +906,7 @@ export default {
activeSidebar: state => state.activeSidebar,
localConfig: state => state.localConfig,
isDark: state => state.localConfig.isDark,
supportHandDrawnLikeStyle: state => state.supportHandDrawnLikeStyle
supportLineFlow: state => state.supportLineFlow
}),
lineStyleList() {
return lineStyleList[this.$i18n.locale] || lineStyleList.zh
@@ -1221,8 +969,6 @@ export default {
if (val === 'baseStyle') {
this.$refs.sidebar.show = true
this.initStyle()
this.initConfig()
this.initWatermark()
this.initRainbowLines()
this.initOuterFramePadding()
this.currentLayout = this.mindMap.getLayout()
@@ -1243,7 +989,6 @@ export default {
}
},
created() {
this.initLoacalConfig()
this.$bus.$on('setData', this.onSetData)
},
beforeDestroy() {
@@ -1259,42 +1004,9 @@ export default {
}, 0)
},
/**
* @Author: 王林
* @Date: 2021-05-05 14:02:12
* @Desc: 初始样式
*/
// 初始样式
initStyle() {
;[
'backgroundColor',
'lineWidth',
'lineStyle',
'showLineMarker',
'rootLineKeepSameInCurve',
'rootLineStartPositionKeepSameInCurve',
'lineRadius',
'lineColor',
'generalizationLineWidth',
'generalizationLineColor',
'associativeLineColor',
'associativeLineWidth',
'associativeLineActiveWidth',
'associativeLineDasharray',
'associativeLineActiveColor',
'associativeLineTextFontSize',
'associativeLineTextColor',
'associativeLineTextFontFamily',
'paddingX',
'paddingY',
'imgMaxWidth',
'imgMaxHeight',
'iconSize',
'backgroundImage',
'backgroundRepeat',
'backgroundPosition',
'backgroundSize',
'nodeUseLineStyle'
].forEach(key => {
Object.keys(this.style).forEach(key => {
this.style[key] = this.mindMap.getThemeConfig(key)
if (key === 'backgroundImage' && this.style[key] === 'none') {
this.style[key] = ''
@@ -1303,41 +1015,6 @@ export default {
this.initMarginStyle()
},
// 初始化其他配置
initConfig() {
;[
'openPerformance',
'enableFreeDrag',
'mousewheelAction',
'mousewheelZoomActionReverse',
'createNewNodeBehavior'
].forEach(key => {
this.config[key] = this.mindMap.getConfig(key)
})
},
// 初始化本地配置
initLoacalConfig() {
this.enableNodeRichText = this.localConfig.openNodeRichText
this.mousewheelAction = this.localConfig.mousewheelAction
this.mousewheelZoomActionReverse = this.localConfig.mousewheelZoomActionReverse
;['isShowScrollbar', 'isUseHandDrawnLikeStyle'].forEach(key => {
this.localConfigs[key] = this.localConfig[key]
})
},
// 初始化水印配置
initWatermark() {
let config = this.mindMap.getConfig('watermarkConfig')
;['text', 'lineSpacing', 'textSpacing', 'angle', 'onlyExport'].forEach(
key => {
this.watermarkConfig[key] = config[key]
}
)
this.watermarkConfig.show = !!config.text
this.watermarkConfig.textStyle = { ...config.textStyle }
},
// 初始化彩虹线条配置
initRainbowLines() {
const config = this.mindMap.getConfig('rainbowLinesConfig') || {}
@@ -1358,11 +1035,7 @@ export default {
)
},
/**
* @Author: 王林
* @Date: 2021-07-03 22:27:32
* @Desc: margin初始值
*/
// margin初始值
initMarginStyle() {
;['marginX', 'marginY'].forEach(key => {
this.style[key] = this.mindMap.getThemeConfig()[this.marginActiveTab][
@@ -1371,11 +1044,7 @@ export default {
})
},
/**
* @Author: 王林
* @Date: 2021-05-05 14:05:40
* @Desc: 更新配置
*/
// 更新配置
update(key, value) {
if (key === 'backgroundImage' && value === 'none') {
this.style[key] = ''
@@ -1393,36 +1062,6 @@ export default {
})
},
// 更新其他配置
updateOtherConfig(key, value) {
this.mindMap.updateConfig({
[key]: value
})
this.data.config = this.data.config || {}
this.data.config[key] = value
storeConfig({
config: this.data.config
})
},
// 更新水印配置
updateWatermarkConfig() {
clearTimeout(this.updateWatermarkTimer)
this.updateWatermarkTimer = setTimeout(() => {
let { show, ...config } = this.watermarkConfig
this.mindMap.watermark.updateWatermark({
...config
})
this.data.config = this.data.config || {}
this.data.config.watermarkConfig = this.mindMap.getConfig(
'watermarkConfig'
)
storeConfig({
config: this.data.config
})
}, 300)
},
// 更新彩虹线条配置
updateRainbowLinesConfig(item) {
this.rainbowLinesPopoverVisible = false
@@ -1474,33 +1113,6 @@ export default {
config: this.data.theme.config
}
})
},
// 切换显示水印与否
watermarkShowChange(value) {
if (value) {
let text =
this.watermarkConfig.text || this.$t('baseStyle.watermarkDefaultText')
this.watermarkConfig.text = text
} else {
this.watermarkConfig.text = ''
}
this.updateWatermarkConfig()
},
// 切换是否开启节点富文本编辑
enableNodeRichTextChange(e) {
this.mindMap.renderer.textEdit.hideEditTextBox()
this.setLocalConfig({
openNodeRichText: e
})
},
// 本地配置
updateLocalConfig(key, value) {
this.setLocalConfig({
[key]: value
})
}
}
}

View File

@@ -56,6 +56,9 @@
<span class="name">{{ $t('contextmenu.moveDownNode') }}</span>
<span class="desc">Ctrl + </span>
</div>
<div class="item" @click="exec('UNEXPAND_ALL')">
<span class="name">{{ $t('contextmenu.unExpandNodeChild') }}</span>
</div>
<div class="item" @click="exec('EXPAND_ALL')">
<span class="name">{{ $t('contextmenu.expandNodeChild') }}</span>
</div>
@@ -89,6 +92,11 @@
</div>
</div>
</div>
<div class="item" @click="setCheckbox" v-if="supportCheckbox">
<span class="name">{{
hasCheckbox ? $t('contextmenu.removeToDo') : $t('contextmenu.addToDo')
}}</span>
</div>
<div class="splitLine"></div>
<div class="item danger" @click="exec('REMOVE_NODE')">
<span class="name">{{ $t('contextmenu.deleteNode') }}</span>
@@ -244,7 +252,8 @@ export default {
...mapState({
isZenMode: state => state.localConfig.isZenMode,
isDark: state => state.localConfig.isDark,
supportNumbers: state => state.supportNumbers
supportNumbers: state => state.supportNumbers,
supportCheckbox: state => state.supportCheckbox
}),
expandList() {
return [
@@ -322,6 +331,9 @@ export default {
},
numberLevelList() {
return numberLevelList[this.$i18n.locale] || numberLevelList.zh
},
hasCheckbox() {
return !!this.node.getData('checkbox')
}
},
created() {
@@ -464,8 +476,12 @@ export default {
this.node
)
break
case 'UNEXPAND_ALL':
const uid = this.node ? this.node.uid : ''
this.$bus.$emit('execCommand', key, !uid, uid)
break
case 'EXPAND_ALL':
this.$bus.$emit('execCommand', key, this.node.uid)
this.$bus.$emit('execCommand', key, this.node ? this.node.uid : '')
break
default:
this.$bus.$emit('execCommand', key, ...args)
@@ -496,6 +512,21 @@ export default {
this.mindMap.execCommand('SET_NUMBER', [], {
[prop]: value
})
this.hide()
},
// 设置待办
setCheckbox() {
this.mindMap.execCommand(
'SET_CHECKBOX',
[],
this.hasCheckbox
? null
: {
done: false
}
)
this.hide()
},
// 复制到剪贴板

View File

@@ -17,6 +17,10 @@
<OutlineSidebar :mindMap="mindMap"></OutlineSidebar>
<Style v-if="!isZenMode"></Style>
<BaseStyle :data="mindMapData" :mindMap="mindMap"></BaseStyle>
<AssociativeLineStyle
v-if="mindMap"
:mindMap="mindMap"
></AssociativeLineStyle>
<Theme v-if="mindMap" :data="mindMapData" :mindMap="mindMap"></Theme>
<Structure :mindMap="mindMap"></Structure>
<ShortcutKey></ShortcutKey>
@@ -38,6 +42,7 @@
<SourceCodeEdit v-if="mindMap" :mindMap="mindMap"></SourceCodeEdit>
<NodeOuterFrame v-if="mindMap" :mindMap="mindMap"></NodeOuterFrame>
<NodeTagStyle v-if="mindMap" :mindMap="mindMap"></NodeTagStyle>
<Setting :data="mindMapData" :mindMap="mindMap"></Setting>
<div
class="dragMask"
v-if="showDragMask"
@@ -71,20 +76,19 @@ import Formula from 'simple-mind-map/src/plugins/Formula.js'
import RainbowLines from 'simple-mind-map/src/plugins/RainbowLines.js'
import Demonstrate from 'simple-mind-map/src/plugins/Demonstrate.js'
import OuterFrame from 'simple-mind-map/src/plugins/OuterFrame.js'
import MindMapLayoutPro from 'simple-mind-map/src/plugins/MindMapLayoutPro.js'
import Themes from 'simple-mind-map-plugin-themes'
// 协同编辑插件
// import Cooperate from 'simple-mind-map/src/plugins/Cooperate.js'
// 手绘风格插件,该插件为付费插件,详情请查看开发文档
// 以下插件为付费插件,详情请查看开发文档。依次为手绘风格插件、标记插件、编号插件、Freemind软件格式导入导出插件、Excel软件格式导入导出插件、待办插件、节点连线流动效果插件
// import HandDrawnLikeStyle from 'simple-mind-map-plugin-handdrawnlikestyle'
// 标记插件,该插件为付费插件,详情请查看开发文档
// import Notation from 'simple-mind-map-plugin-notation'
// 编号插件,该插件为付费插件,详情请查看开发文档
// import Numbers from 'simple-mind-map-plugin-numbers'
// Freemind软件格式导入导出插件该插件为付费插件详情请查看开发文档
// import Freemind from 'simple-mind-map-plugin-freemind'
// Excel软件格式导入导出插件该插件为付费插件详情请查看开发文档
// import Excel from 'simple-mind-map-plugin-excel'
// npm link simple-mind-map-plugin-excel simple-mind-map-plugin-freemind simple-mind-map-plugin-numbers simple-mind-map-plugin-notation simple-mind-map-plugin-handdrawnlikestyle simple-mind-map simple-mind-map-plugin-themes
// import Checkbox from 'simple-mind-map-plugin-checkbox'
// import LineFlow from 'simple-mind-map-plugin-lineflow'
// npm link simple-mind-map-plugin-excel simple-mind-map-plugin-freemind simple-mind-map-plugin-numbers simple-mind-map-plugin-notation simple-mind-map-plugin-handdrawnlikestyle simple-mind-map-plugin-checkbox simple-mind-map simple-mind-map-plugin-themes simple-mind-map-plugin-lineflow
import OutlineSidebar from './OutlineSidebar'
import Style from './Style'
import BaseStyle from './BaseStyle'
@@ -121,6 +125,8 @@ import SourceCodeEdit from './SourceCodeEdit.vue'
import NodeAttachment from './NodeAttachment.vue'
import NodeOuterFrame from './NodeOuterFrame.vue'
import NodeTagStyle from './NodeTagStyle.vue'
import Setting from './Setting.vue'
import AssociativeLineStyle from './AssociativeLineStyle.vue'
// 注册插件
MindMap.usePlugin(MiniMap)
@@ -140,16 +146,12 @@ MindMap.usePlugin(MiniMap)
.usePlugin(RainbowLines)
.usePlugin(Demonstrate)
.usePlugin(OuterFrame)
.usePlugin(MindMapLayoutPro)
// .usePlugin(Cooperate) // 协同插件
// 注册主题
Themes.init(MindMap)
/**
* @Author: 王林
* @Date: 2021-06-24 22:56:17
* @Desc: 编辑区域
*/
export default {
name: 'Edit',
components: {
@@ -176,7 +178,9 @@ export default {
SourceCodeEdit,
NodeAttachment,
NodeOuterFrame,
NodeTagStyle
NodeTagStyle,
Setting,
AssociativeLineStyle
},
data() {
return {
@@ -290,21 +294,13 @@ export default {
}
},
/**
* @Author: 王林
* @Date: 2021-07-03 22:11:37
* @Desc: 获取思维导图数据,实际应该调接口获取
*/
// 获取思维导图数据,实际应该调接口获取
getData() {
let storeData = getData()
this.mindMapData = storeData
},
/**
* @Author: 王林
* @Date: 2021-08-01 10:19:07
* @Desc: 存储数据当数据有变时
*/
// 存储数据当数据有变时
bindSaveEvent() {
this.$bus.$on('data_change', data => {
storeData(data)
@@ -319,21 +315,13 @@ export default {
})
},
/**
* @Author: 王林
* @Date: 2021-08-02 23:19:52
* @Desc: 手动保存
*/
// 手动保存
manualSave() {
let data = this.mindMap.getData(true)
storeConfig(data)
},
/**
* @Author: 王林
* @Date: 2021-04-10 15:01:01
* @Desc: 初始化
*/
// 初始化
init() {
let hasFileURL = this.hasFileURL()
let { root, layout, theme, view, config } = this.mindMapData
@@ -367,11 +355,12 @@ export default {
// this.$bus.$emit('hideNoteContent')
}
},
openRealtimeRenderOnNodeTextEdit: true,
enableAutoEnterTextEditWhenKeydown: true,
...(config || {}),
iconList: [...icon],
useLeftKeySelectionRightKeyDrag: this.useLeftKeySelectionRightKeyDrag,
customInnerElsAppendTo: null,
enableAutoEnterTextEditWhenKeydown: true,
customHandleClipboardText: handleClipboardText,
defaultNodeImage: require('../../../assets/img/图片加载失败.svg'),
initRootNodePosition: ['center', 'center'],
@@ -576,6 +565,14 @@ export default {
this.$store.commit('setSupportExcel', true)
Vue.prototype.Excel = Excel
}
if (typeof Checkbox !== 'undefined') {
this.mindMap.addPlugin(Checkbox)
this.$store.commit('setSupportCheckbox', true)
}
if (typeof LineFlow !== 'undefined') {
this.mindMap.addPlugin(LineFlow)
this.$store.commit('setSupportLineFlow', true)
}
this.mindMap.keyCommand.addShortcut('Control+s', () => {
this.manualSave()
})
@@ -604,7 +601,8 @@ export default {
'node_attachmentClick',
'node_attachmentContextmenu',
'demonstrate_jump',
'exit_demonstrate'
'exit_demonstrate',
'node_note_dblclick'
].forEach(event => {
this.mindMap.on(event, (...args) => {
this.$bus.$emit(event, ...args)
@@ -653,11 +651,7 @@ export default {
return /\.(smm|json|xmind|md|xlsx)$/.test(fileURL)
},
/**
* @Author: 王林
* @Date: 2021-08-03 23:01:13
* @Desc: 动态设置思维导图数据
*/
// 动态设置思维导图数据
setData(data) {
this.handleShowLoading()
if (data.root) {
@@ -669,29 +663,17 @@ export default {
this.manualSave()
},
/**
* @Author: 王林
* @Date: 2021-05-05 13:32:11
* @Desc: 重新渲染
*/
// 重新渲染
reRender() {
this.mindMap.reRender()
},
/**
* @Author: 王林
* @Date: 2021-05-04 13:08:28
* @Desc: 执行命令
*/
// 执行命令
execCommand(...args) {
this.mindMap.execCommand(...args)
},
/**
* @Author: 王林
* @Date: 2021-07-01 22:33:02
* @Desc: 导出
*/
// 导出
async export(...args) {
try {
showLoading()

View File

@@ -42,7 +42,8 @@ export default {
note: '',
activeNodes: [],
editor: null,
isMobile: isMobile()
isMobile: isMobile(),
appointNode: null
}
},
watch: {
@@ -63,6 +64,10 @@ export default {
methods: {
handleNodeActive(...args) {
this.activeNodes = [...args[1]]
this.updateNoteInfo()
},
updateNoteInfo() {
if (this.activeNodes.length > 0) {
let firstNode = this.activeNodes[0]
this.note = firstNode.getData('note') || ''
@@ -71,19 +76,18 @@ export default {
}
},
handleShowNodeNote() {
handleShowNodeNote(node) {
this.$bus.$emit('startTextEdit')
if (node) {
this.appointNode = node
this.note = node.getData('note') || ''
}
this.dialogVisible = true
this.$nextTick(() => {
this.initEditor()
})
},
/**
* @Author: 王林25
* @Date: 2022-05-09 11:37:05
* @Desc: 初始化编辑器
*/
initEditor() {
if (!this.editor) {
this.editor = new Editor({
@@ -96,25 +100,24 @@ export default {
this.editor.setMarkdown(this.note)
},
/**
* @Author: 王林
* @Date: 2021-06-22 22:08:11
* @Desc: 取消
*/
cancel() {
this.dialogVisible = false
if (this.appointNode) {
this.appointNode = null
this.updateNoteInfo()
}
},
/**
* @Author: 王林
* @Date: 2021-06-06 22:28:20
* @Desc: 确定
*/
confirm() {
this.note = this.editor.getMarkdown()
this.activeNodes.forEach(node => {
node.setNote(this.note)
})
if (this.appointNode) {
this.appointNode.setNote(this.note)
} else {
this.activeNodes.forEach(node => {
node.setNote(this.note)
})
}
this.cancel()
}
}

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

@@ -0,0 +1,592 @@
<template>
<Sidebar ref="sidebar" :title="$t('setting.title')">
<div class="sidebarContent" :class="{ isDark: isDark }" v-if="data">
<!-- 水印 -->
<div class="title noTop">{{ $t('setting.watermark') }}</div>
<div class="row">
<!-- 是否显示水印 -->
<div class="rowItem">
<el-checkbox
v-model="watermarkConfig.show"
@change="watermarkShowChange"
>{{ $t('setting.showWatermark') }}</el-checkbox
>
</div>
</div>
<template v-if="watermarkConfig.show">
<!-- 是否仅在导出时显示 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="watermarkConfig.onlyExport"
@change="updateWatermarkConfig"
>{{ $t('setting.onlyExport') }}</el-checkbox
>
</div>
</div>
<!-- 是否在节点下方 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="watermarkConfig.belowNode"
@change="updateWatermarkConfig"
>{{ $t('setting.belowNode') }}</el-checkbox
>
</div>
</div>
<!-- 水印文字 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('setting.watermarkText') }}</span>
<el-input
v-model="watermarkConfig.text"
size="small"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input>
</div>
</div>
<!-- 水印文字颜色 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('setting.watermarkTextColor') }}</span>
<span
class="block"
v-popover:popover3
:style="{ backgroundColor: watermarkConfig.textStyle.color }"
></span>
<el-popover ref="popover3" placement="bottom" trigger="click">
<Color
:color="watermarkConfig.textStyle.color"
@change="
value => {
watermarkConfig.textStyle.color = value
updateWatermarkConfig()
}
"
></Color>
</el-popover>
</div>
</div>
<!-- 水印文字透明度 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('setting.watermarkTextOpacity') }}</span>
<el-slider
v-model="watermarkConfig.textStyle.opacity"
style="width: 170px"
:min="0"
:max="1"
:step="0.1"
@change="updateWatermarkConfig"
></el-slider>
</div>
</div>
<!-- 水印文字字号 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('setting.watermarkTextFontSize') }}</span>
<el-input-number
v-model="watermarkConfig.textStyle.fontSize"
size="small"
:min="0"
:max="50"
:step="1"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
<!-- 旋转角度 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('setting.watermarkAngle') }}</span>
<el-input-number
v-model="watermarkConfig.angle"
size="small"
:min="0"
:max="90"
:step="10"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
<!-- 水印行间距 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('setting.watermarkLineSpacing') }}</span>
<el-input-number
v-model="watermarkConfig.lineSpacing"
size="small"
:step="10"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
<!-- 水印文字间距 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('setting.watermarkTextSpacing') }}</span>
<el-input-number
v-model="watermarkConfig.textSpacing"
size="small"
:step="10"
@change="updateWatermarkConfig"
@keydown.native.stop
></el-input-number>
</div>
</div>
</template>
<!-- 配置性能模式 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="config.openPerformance"
@change="
value => {
updateOtherConfig('openPerformance', value)
}
"
>{{ $t('setting.openPerformance') }}</el-checkbox
>
</div>
</div>
<!-- 配置开启自由拖拽 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="config.enableFreeDrag"
@change="
value => {
updateOtherConfig('enableFreeDrag', value)
}
"
>{{ $t('setting.enableFreeDrag') }}</el-checkbox
>
</div>
</div>
<!-- 配置是否启用富文本编辑 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="enableNodeRichText"
@change="enableNodeRichTextChange"
>{{ $t('setting.isEnableNodeRichText') }}</el-checkbox
>
</div>
</div>
<!-- 是否开启文本编辑时实时更新节点大小 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="config.openRealtimeRenderOnNodeTextEdit"
@change="
updateOtherConfig('openRealtimeRenderOnNodeTextEdit', $event)
"
>{{ $t('setting.openRealtimeRenderOnNodeTextEdit') }}</el-checkbox
>
</div>
</div>
<!-- 是否显示滚动条 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="localConfigs.isShowScrollbar"
@change="updateLocalConfig('isShowScrollbar', $event)"
>{{ $t('setting.isShowScrollbar') }}</el-checkbox
>
</div>
</div>
<!-- 是否一直显示展开收起按钮 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="config.alwaysShowExpandBtn"
@change="updateOtherConfig('alwaysShowExpandBtn', $event)"
>{{ $t('setting.alwaysShowExpandBtn') }}</el-checkbox
>
</div>
</div>
<!-- 是否在键盘输入时自动进入节点文本编辑模式 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="config.enableAutoEnterTextEditWhenKeydown"
@change="
updateOtherConfig('enableAutoEnterTextEditWhenKeydown', $event)
"
>{{ $t('setting.enableAutoEnterTextEditWhenKeydown') }}</el-checkbox
>
</div>
</div>
<!-- 是否开启手绘风格 -->
<div class="row" v-if="supportHandDrawnLikeStyle">
<div class="rowItem">
<el-checkbox
v-model="localConfigs.isUseHandDrawnLikeStyle"
@change="updateLocalConfig('isUseHandDrawnLikeStyle', $event)"
>{{ $t('setting.isUseHandDrawnLikeStyle') }}</el-checkbox
>
</div>
</div>
<!-- 配置鼠标滚轮行为 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('setting.mousewheelAction') }}</span>
<el-select
size="mini"
style="width: 120px"
v-model="config.mousewheelAction"
placeholder=""
@change="
value => {
updateOtherConfig('mousewheelAction', value)
}
"
>
<el-option :label="$t('setting.zoomView')" value="zoom"></el-option>
<el-option
:label="$t('setting.moveViewUpDown')"
value="move"
></el-option>
</el-select>
</div>
</div>
<!-- 配置鼠标缩放行为 -->
<div class="row" v-if="config.mousewheelAction === 'zoom'">
<div class="rowItem">
<span class="name">{{
$t('setting.mousewheelZoomActionReverse')
}}</span>
<el-select
size="mini"
style="width: 120px"
v-model="config.mousewheelZoomActionReverse"
placeholder=""
@change="
value => {
updateOtherConfig('mousewheelZoomActionReverse', value)
}
"
>
<el-option
:label="$t('setting.mousewheelZoomActionReverse1')"
:value="false"
></el-option>
<el-option
:label="$t('setting.mousewheelZoomActionReverse2')"
:value="true"
></el-option>
</el-select>
</div>
</div>
<!-- 配置创建新节点时的行为 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('setting.createNewNodeBehavior') }}</span>
<el-select
size="mini"
style="width: 120px"
v-model="config.createNewNodeBehavior"
placeholder=""
@change="
value => {
updateOtherConfig('createNewNodeBehavior', value)
}
"
>
<el-option
:label="$t('setting.default')"
value="default"
></el-option>
<el-option
:label="$t('setting.notActive')"
value="notActive"
></el-option>
<el-option
:label="$t('setting.activeOnly')"
value="activeOnly"
></el-option>
</el-select>
</div>
</div>
<!-- 标签显示的位置 -->
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('setting.tagPosition') }}</span>
<el-select
size="mini"
style="width: 120px"
v-model="config.tagPosition"
placeholder=""
@change="
value => {
updateOtherConfig('tagPosition', value)
}
"
>
<el-option
:label="$t('setting.tagPositionRight')"
value="right"
></el-option>
<el-option
:label="$t('setting.tagPositionBottom')"
value="bottom"
></el-option>
</el-select>
</div>
</div>
</div>
</Sidebar>
</template>
<script>
import Sidebar from './Sidebar'
import { storeConfig } from '@/api'
import { mapState, mapMutations } from 'vuex'
import Color from './Color'
export default {
components: {
Sidebar,
Color
},
props: {
data: {
type: [Object, null],
default: null
},
mindMap: {
type: Object
}
},
data() {
return {
config: {
openPerformance: false,
enableFreeDrag: false,
mousewheelAction: 'zoom',
mousewheelZoomActionReverse: false,
createNewNodeBehavior: 'default',
tagPosition: 'right',
openRealtimeRenderOnNodeTextEdit: true,
alwaysShowExpandBtn: false,
enableAutoEnterTextEditWhenKeydown: true
},
watermarkConfig: {
show: false,
onlyExport: false,
text: '',
lineSpacing: 100,
textSpacing: 100,
angle: 30,
textStyle: {
color: '',
opacity: 0,
fontSize: 1
}
},
updateWatermarkTimer: null,
enableNodeRichText: true,
localConfigs: {
isShowScrollbar: false,
isUseHandDrawnLikeStyle: false
}
}
},
computed: {
...mapState({
activeSidebar: state => state.activeSidebar,
localConfig: state => state.localConfig,
isDark: state => state.localConfig.isDark,
supportHandDrawnLikeStyle: state => state.supportHandDrawnLikeStyle
})
},
watch: {
activeSidebar(val) {
if (val === 'setting') {
this.$refs.sidebar.show = true
this.initConfig()
this.initWatermark()
} else {
this.$refs.sidebar.show = false
}
}
},
created() {
this.initLoacalConfig()
},
beforeDestroy() {},
methods: {
...mapMutations(['setLocalConfig']),
// 初始化其他配置
initConfig() {
Object.keys(this.config).forEach(key => {
this.config[key] = this.mindMap.getConfig(key)
})
},
// 初始化本地配置
initLoacalConfig() {
this.enableNodeRichText = this.localConfig.openNodeRichText
this.mousewheelAction = this.localConfig.mousewheelAction
this.mousewheelZoomActionReverse = this.localConfig.mousewheelZoomActionReverse
;['isShowScrollbar', 'isUseHandDrawnLikeStyle'].forEach(key => {
this.localConfigs[key] = this.localConfig[key]
})
},
// 初始化水印配置
initWatermark() {
const config = this.mindMap.getConfig('watermarkConfig')
;['text', 'lineSpacing', 'textSpacing', 'angle', 'onlyExport'].forEach(
key => {
this.watermarkConfig[key] = config[key]
}
)
this.watermarkConfig.show = !!config.text
this.watermarkConfig.textStyle = { ...config.textStyle }
},
// 更新其他配置
updateOtherConfig(key, value) {
this.mindMap.updateConfig({
[key]: value
})
this.data.config = this.data.config || {}
this.data.config[key] = value
storeConfig({
config: this.data.config
})
if (['tagPosition', 'alwaysShowExpandBtn'].includes(key)) {
this.mindMap.reRender()
}
},
// 更新水印配置
updateWatermarkConfig() {
clearTimeout(this.updateWatermarkTimer)
this.updateWatermarkTimer = setTimeout(() => {
let { show, ...config } = this.watermarkConfig
this.mindMap.watermark.updateWatermark({
...config
})
this.data.config = this.data.config || {}
this.data.config.watermarkConfig = this.mindMap.getConfig(
'watermarkConfig'
)
storeConfig({
config: this.data.config
})
}, 300)
},
// 切换显示水印与否
watermarkShowChange(value) {
if (value) {
let text =
this.watermarkConfig.text || this.$t('setting.watermarkDefaultText')
this.watermarkConfig.text = text
} else {
this.watermarkConfig.text = ''
}
this.updateWatermarkConfig()
},
// 切换是否开启节点富文本编辑
enableNodeRichTextChange(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
})
},
// 本地配置
updateLocalConfig(key, value) {
this.setLocalConfig({
[key]: value
})
}
}
}
</script>
<style lang="less" scoped>
.sidebarContent {
padding: 20px;
padding-top: 10px;
&.isDark {
.title {
color: #fff;
}
.row {
.rowItem {
.name {
color: hsla(0, 0%, 100%, 0.6);
}
}
}
}
.title {
font-size: 16px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: rgba(26, 26, 26, 0.9);
margin-bottom: 10px;
margin-top: 20px;
&.noTop {
margin-top: 0;
}
}
.row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
.rowItem {
display: flex;
align-items: center;
margin-bottom: 5px;
.name {
font-size: 12px;
margin-right: 10px;
white-space: nowrap;
}
.block {
display: inline-block;
width: 30px;
height: 30px;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
}
}
}
}
</style>

View File

@@ -13,6 +13,7 @@
<span class="name">{{ $t('style.fontFamily') }}</span>
<el-select
size="mini"
style="width: 100px"
v-model="style.fontFamily"
placeholder=""
@change="update('fontFamily')"
@@ -27,8 +28,6 @@
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">{{ $t('style.fontSize') }}</span>
<el-select
@@ -48,24 +47,6 @@
</el-option>
</el-select>
</div>
<div class="rowItem">
<span class="name">{{ $t('style.lineHeight') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.lineHeight"
placeholder=""
@change="update('lineHeight')"
>
<el-option
v-for="item in lineHeightList"
:key="item"
:label="item"
:value="item"
>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="btnGroup">
@@ -434,6 +415,49 @@
</el-select>
</div>
</div>
<!-- 流动效果 -->
<div class="row" v-if="supportLineFlow">
<div class="rowItem">
<span class="name">{{ $t('style.openLineFlow') }}</span>
<el-checkbox
v-model="style.lineFlow"
@change="update('lineFlow')"
></el-checkbox>
</div>
<div class="rowItem">
<span class="name">{{ $t('style.direction') }}</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.lineFlowForward"
placeholder=""
@change="update('lineFlowForward')"
>
<el-option
key="1"
:label="$t('style.forward')"
:value="true"
></el-option>
<el-option
key="2"
:label="$t('style.reverse')"
:value="false"
></el-option>
</el-select>
</div>
</div>
<div class="row" v-if="supportLineFlow">
<div class="rowItem">
<span class="name">{{ $t('style.lineFlowDuration') }}</span>
<el-input-number
v-model="style.lineFlowDuration"
@change="update('lineFlowDuration')"
:min="0.1"
size="mini"
:step="0.5"
></el-input-number>
</div>
</div>
<!-- 节点内边距 -->
<div class="title noTop">{{ $t('style.nodePadding') }}</div>
<div class="row">
@@ -460,7 +484,7 @@
</div>
<div class="tipBox" v-else>
<div class="tipIcon iconfont icontianjiazijiedian"></div>
<div class="tipText">请选择一个节点</div>
<div class="tipText">{{ $t('style.selectNodeTip') }}</div>
</div>
</Sidebar>
</template>
@@ -474,7 +498,6 @@ import {
borderWidthList,
borderDasharrayList,
borderRadiusList,
lineHeightList,
shapeList,
shapeListMap,
linearGradientDirList
@@ -497,7 +520,6 @@ export default {
fontSizeList,
borderWidthList,
borderRadiusList,
lineHeightList,
activeNodes: [],
style: {
shape: '',
@@ -506,7 +528,6 @@ export default {
color: '',
fontFamily: '',
fontSize: '',
lineHeight: '',
textDecoration: '',
fontWeight: '',
fontStyle: '',
@@ -522,14 +543,18 @@ export default {
gradientStyle: false,
startColor: '',
endColor: '',
linearGradientDir: ''
linearGradientDir: '',
lineFlow: false,
lineFlowForward: true,
lineFlowDuration: 1
}
}
},
computed: {
...mapState({
isDark: state => state.localConfig.isDark,
activeSidebar: state => state.activeSidebar
activeSidebar: state => state.activeSidebar,
supportLineFlow: state => state.supportLineFlow
}),
fontFamilyList() {
return fontFamilyList[this.$i18n.locale] || fontFamilyList.zh
@@ -586,30 +611,7 @@ export default {
if (this.activeNodes.length <= 0) {
return
}
;[
'shape',
'paddingX',
'paddingY',
'color',
'fontFamily',
'fontSize',
'lineHeight',
'textDecoration',
'fontWeight',
'fontStyle',
'borderWidth',
'borderColor',
'fillColor',
'borderDasharray',
'borderRadius',
'lineColor',
'lineDasharray',
'lineWidth',
'lineMarkerDir',
'gradientStyle',
'startColor',
'endColor'
].forEach(item => {
Object.keys(this.style).forEach(item => {
this.style[item] = this.activeNodes[0].getStyle(item, false)
})
this.initLinearGradientDir()

View File

@@ -234,12 +234,14 @@ export default {
window.addEventListener('resize', this.computeToolbarShowThrottle)
this.$bus.$on('lang_change', this.computeToolbarShowThrottle)
window.addEventListener('beforeunload', this.onUnload)
this.$bus.$on('node_note_dblclick', this.onNodeNoteDblclick)
},
beforeDestroy() {
this.$bus.$off('write_local_file', this.onWriteLocalFile)
window.removeEventListener('resize', this.computeToolbarShowThrottle)
this.$bus.$off('lang_change', this.computeToolbarShowThrottle)
window.removeEventListener('beforeunload', this.onUnload)
this.$bus.$off('node_note_dblclick', this.onNodeNoteDblclick)
},
methods: {
// 计算工具按钮如何显示
@@ -501,6 +503,11 @@ export default {
}
this.$message.warning(this.$t('toolbar.notSupportTip'))
}
},
onNodeNoteDblclick(node, e) {
e.stopPropagation()
this.$bus.$emit('showNodeNote', node)
}
}
}

View File

@@ -33,6 +33,8 @@ const store = new Vuex.Store({
supportNumbers: false, // 是否支持编号
supportFreemind: false, // 是否支持Freemind插件
supportExcel: false, // 是否支持Excel插件
supportCheckbox: false, // 是否支持Checkbox插件
supportLineFlow: false, // 是否支持LineFlow插件
isDragOutlineTreeNode: false // 当前是否正在拖拽大纲树的节点
},
mutations: {
@@ -105,6 +107,16 @@ const store = new Vuex.Store({
state.supportExcel = data
},
// 设置是否支持Checkbox插件
setSupportCheckbox(state, data) {
state.supportCheckbox = data
},
// 设置是否支持Lineflow插件
setSupportLineFlow(state, data) {
state.supportLineFlow = data
},
// 设置树节点拖拽
setIsDragOutlineTreeNode(state, data) {
state.isDragOutlineTreeNode = data

View File

@@ -62,14 +62,14 @@ export const copy = text => {
// 复制文本到剪贴板
export const setDataToClipboard = data => {
if (navigator.clipboard) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(data)
}
}
// 复制图片到剪贴板
export const setImgToClipboard = img => {
if (navigator.clipboard) {
if (navigator.clipboard && navigator.clipboard.write) {
const data = [new ClipboardItem({ ['image/png']: img })]
navigator.clipboard.write(data)
}