Compare commits

..

85 Commits

Author SHA1 Message Date
街角小林
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
61 changed files with 2723 additions and 1381 deletions

230
README.md
View File

@@ -44,7 +44,7 @@ Github[releases](https://github.com/wanglin2/mind-map/releases)。百度云
官方提供了如下插件,可根据需求按需引入(某个功能不生效大概率是因为你没有引入对应的插件),具体使用方式请查看文档:
> RichText节点富文本插件、Select鼠标多选节点插件、Drag节点拖拽插件、AssociativeLine关联线插件、Export导出插件、KeyboardNavigation键盘导航插件、MiniMap小地图插件、Watermark水印插件、TouchEvent移动端触摸事件支持插件、NodeImgAdjust拖拽调整节点图片大小插件、Search搜索插件、Painter节点格式刷插件、Scrollbar滚动条插件、Formula数学公式插件、Cooperate协同编辑插件、RainbowLines彩虹线条插件、Demonstrate演示模式插件、OuterFrame外框插件、HandDrawnLikeStyle手绘风格插件[收费]、Notation节点标记插件[收费]、Numbers节点编号插件[收费]、FreemindFreemind格式导入导出插件[收费]、ExcelExcel格式导入导出插件[收费]
> RichText节点富文本插件、Select鼠标多选节点插件、Drag节点拖拽插件、AssociativeLine关联线插件、Export导出插件、KeyboardNavigation键盘导航插件、MiniMap小地图插件、Watermark水印插件、TouchEvent移动端触摸事件支持插件、NodeImgAdjust拖拽调整节点图片大小插件、Search搜索插件、Painter节点格式刷插件、Scrollbar滚动条插件、Formula数学公式插件、Cooperate协同编辑插件、RainbowLines彩虹线条插件、Demonstrate演示模式插件、OuterFrame外框插件MindMapLayoutPro思维导图布局插件HandDrawnLikeStyle手绘风格插件[收费]、Notation节点标记插件[收费]、Numbers节点编号插件[收费]、FreemindFreemind格式导入导出插件[收费]、ExcelExcel格式导入导出插件[收费]、Checkbox待办插件[收费]、Lineflow节点连线流动插件[收费]
本项目不会实现的特性:
@@ -93,17 +93,15 @@ const mindMap = new MindMap({
});
```
即可得到一个思维导图。
想要实现更多功能?可以查看[开发文档](https://wanglin2.github.io/mind-map-docs/)。
即可得到一个思维导图。想要实现更多功能?可以查看[开发文档](https://wanglin2.github.io/mind-map-docs/)。
# License
[MIT](./LICENSE)。保留`mind-map`版权声明的情况下可随意商用如不想保留可联系作者。
[MIT](./LICENSE)。保留`mind-map`版权声明的情况下可随意商用如不想保留可联系作者。
# 微信交流群
微信添加`wanglinguanfang`拉你入群。根据过往的经验大部分问题都可以通过查看issue列表或文档解决所以提问前请确保你已经阅读完了所有文档文档里没有的可在群里提问不必私聊作者。
微信添加`wanglinguanfang`拉你入群。根据过往的经验大部分问题都可以通过查看issue列表或文档解决所以提问前请确保你已经阅读完了所有文档文档里没有的可在群里提问不必私聊作者如果你一定要私聊请先发红包¥9.9+每次)
# star
@@ -117,17 +115,119 @@ const mindMap = new MindMap({
# 请作者喝杯咖啡
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡你的支持是开发者持续维护的最大动力~
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡~你的赞助对项目的可持续发展非常重要,是作者持续维护的最大动力
> 推荐使用支付宝,微信获取不到头像。转账请备注【思维导图】。
>
> 也可以通过购买付费插件来支持我们:[付费插件](https://wanglin2.github.io/mind-map-docs/plugins/about.html)。
>
> 赞助等级最强王者¥500+、星耀赞助¥300+、钻石赞助¥150+、黄金赞助¥50+)、青铜赞助
<p>
<img src="./web/src/assets/img/alipay.jpg" style="width: 300px" />
<img src="./web/src/assets/img/wechat.jpg" style="width: 300px" />
</p>
## 钻石赞助
<p>
<span>
<img src="./web/src/assets/avatar/黄智彪@一米一栗科技.png" style="width: 50px;height: 50px;" />
<span>黄智彪@一米一栗科技</span>
</span>
</p>
## 黄金赞助
<p>
<span>
<img src="./web/src/assets/avatar/小土渣的宇宙.jpeg" style="width: 50px;height: 50px;" />
<span>小土渣的宇宙</span>
</span>
<span>
<img src="./web/src/assets/avatar/Chris.jpg" style="width: 50px;height: 50px;" />
<span>Chris</span>
</span>
<span>
<img src="./web/src/assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;" />
<span>仓鼠</span>
</span>
<span>
<img src="./web/src/assets/avatar/风格.jpg" style="width: 50px;height: 50px;" />
<span>风格</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>LiuJL</span>
</span>
<span>
<img src="./web/src/assets/avatar/Kyle.jpg" style="width: 50px;height: 50px;" />
<span>Kyle</span>
</span>
<span>
<img src="./web/src/assets/avatar/秀树因馨雨.jpg" style="width: 50px;height: 50px;" />
<span>秀树因馨雨</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>黄泳</span>
</span>
<span>
<img src="./web/src/assets/avatar/ccccs.jpg" style="width: 50px;height: 50px;" />
<span>ccccs</span>
</span>
<span>
<img src="./web/src/assets/avatar/炫.jpg" style="width: 50px;height: 50px;" />
<span>炫</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>晏江</span>
</span>
<span>
<img src="./web/src/assets/avatar/梁辉.jpg" style="width: 50px;height: 50px;" />
<span>梁辉</span>
</span>
<span>
<img src="./web/src/assets/avatar/千帆.jpg" style="width: 50px;height: 50px;" />
<span>千帆</span>
</span>
<span>
<img src="./web/src/assets/avatar/布林.jpg" style="width: 50px;height: 50px;" />
<span>布林</span>
</span>
<span>
<img src="./web/src/assets/avatar/达仁科技.jpg" style="width: 50px;height: 50px;" />
<span>达仁科技</span>
</span>
<span>
<img src="./web/src/assets/avatar/沐风牧草.jpg" style="width: 50px;height: 50px;" />
<span>沐风牧草</span>
</span>
<span>
<img src="./web/src/assets/avatar/俊奇.jpg" style="width: 50px;height: 50px;" />
<span>俊奇</span>
</span>
<span>
<img src="./web/src/assets/avatar/庆国.jpg" style="width: 50px;height: 50px;" />
<span>庆国</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>Matt</span>
</span>
<span>
<img src="./web/src/assets/avatar/雨馨.jpg" style="width: 50px;height: 50px;" />
<span>雨馨</span>
</span>
<span>
<img src="./web/src/assets/avatar/峰.jpg" style="width: 50px;height: 50px;" />
<span>峰</span>
</span>
</p>
## 青铜赞助
<p>
<span>
<img src="./web/src/assets/avatar/Think.jpg" style="width: 50px;height: 50px;" />
@@ -137,10 +237,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/志斌.jpg" style="width: 50px;height: 50px;" />
<span>志斌</span>
</span>
<span>
<img src="./web/src/assets/avatar/小土渣的宇宙.jpeg" style="width: 50px;height: 50px;" />
<span>小土渣的宇宙</span>
</span>
<span>
<img src="./web/src/assets/avatar/qp.jpg" style="width: 50px;height: 50px;" />
<span>qp</span>
@@ -157,22 +253,10 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/suka.jpg" style="width: 50px;height: 50px;" />
<span>suka</span>
</span>
<span>
<img src="./web/src/assets/avatar/Chris.jpg" style="width: 50px;height: 50px;" />
<span>Chris</span>
</span>
<span>
<img src="./web/src/assets/avatar/水车.jpg" style="width: 50px;height: 50px;" />
<span>水车</span>
</span>
<span>
<img src="./web/src/assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;" />
<span>仓鼠</span>
</span>
<span>
<img src="./web/src/assets/avatar/千帆.jpg" style="width: 50px;height: 50px;" />
<span>千帆</span>
</span>
<span>
<img src="./web/src/assets/avatar/才镇.jpg" style="width: 50px;height: 50px;" />
<span>才镇</span>
@@ -189,10 +273,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>Luke</span>
</span>
<span>
<img src="./web/src/assets/avatar/布林.jpg" style="width: 50px;height: 50px;" />
<span>布林</span>
</span>
<span>
<img src="./web/src/assets/avatar/南风.jpg" style="width: 50px;height: 50px;" />
<span>南风</span>
@@ -209,10 +289,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/敏.jpg" style="width: 50px;height: 50px;" />
<span>敏</span>
</span>
<span>
<img src="./web/src/assets/avatar/沐风牧草.jpg" style="width: 50px;height: 50px;" />
<span>沐风牧草</span>
</span>
<span>
<img src="./web/src/assets/avatar/有希.jpg" style="width: 50px;height: 50px;" />
<span>有希</span>
@@ -221,10 +297,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/樊笼.jpg" style="width: 50px;height: 50px;" />
<span>樊笼</span>
</span>
<span>
<img src="./web/src/assets/avatar/达仁科技.jpg" style="width: 50px;height: 50px;" />
<span>达仁科技</span>
</span>
<span>
<img src="./web/src/assets/avatar/小逗比.png" style="width: 50px;height: 50px;" />
<span>小逗比</span>
@@ -301,10 +373,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/旭东.png" style="width: 50px;height: 50px;" />
<span>旭东</span>
</span>
<span>
<img src="./web/src/assets/avatar/俊奇.jpg" style="width: 50px;height: 50px;" />
<span>俊奇</span>
</span>
<span>
<img src="./web/src/assets/avatar/橘半.jpg" style="width: 50px;height: 50px;" />
<span>橘半</span>
@@ -317,10 +385,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/皇登攀.jpg" style="width: 50px;height: 50px;" />
<span>皇登攀</span>
</span>
<span>
<img src="./web/src/assets/avatar/风格.jpg" style="width: 50px;height: 50px;" />
<span>风格</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>SR</span>
@@ -329,10 +393,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/逆水行舟.jpg" style="width: 50px;height: 50px;" />
<span>逆水行舟</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>LiuJL</span>
</span>
<span>
<img src="./web/src/assets/avatar/L.jpg" style="width: 50px;height: 50px;" />
<span>L</span>
@@ -357,10 +417,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>铁</span>
</span>
<span>
<img src="./web/src/assets/avatar/庆国.jpg" style="width: 50px;height: 50px;" />
<span>庆国</span>
</span>
<span>
<img src="./web/src/assets/avatar/Alex.jpg" style="width: 50px;height: 50px;" />
<span>Alex</span>
@@ -377,18 +433,10 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/最多5个字.jpg" style="width: 50px;height: 50px;" />
<span>最多5个字</span>
</span>
<span>
<img src="./web/src/assets/avatar/雨馨.jpg" style="width: 50px;height: 50px;" />
<span>雨馨</span>
</span>
<span>
<img src="./web/src/assets/avatar/ZX.jpg" style="width: 50px;height: 50px;" />
<span>ZX</span>
</span>
<span>
<img src="./web/src/assets/avatar/峰.jpg" style="width: 50px;height: 50px;" />
<span>峰</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>协成</span>
@@ -401,18 +449,10 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/好名字.jpg" style="width: 50px;height: 50px;" />
<span>好名字</span>
</span>
<span>
<img src="./web/src/assets/avatar/Kyle.jpg" style="width: 50px;height: 50px;" />
<span>Kyle</span>
</span>
<span>
<img src="./web/src/assets/avatar/lsytyrt.jpg" style="width: 50px;height: 50px;" />
<span>lsytyrt</span>
</span>
<span>
<img src="./web/src/assets/avatar/秀树因馨雨.jpg" style="width: 50px;height: 50px;" />
<span>秀树因馨雨</span>
</span>
<span>
<img src="./web/src/assets/avatar/buddy.jpg" style="width: 50px;height: 50px;" />
<span>buddy</span>
@@ -433,14 +473,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/晴空.jpg" style="width: 50px;height: 50px;" />
<span>晴空</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>黄泳</span>
</span>
<span>
<img src="./web/src/assets/avatar/ccccs.jpg" style="width: 50px;height: 50px;" />
<span>ccccs</span>
</span>
<span>
<img src="./web/src/assets/avatar/。.png" style="width: 50px;height: 50px;" />
<span>。</span>
@@ -453,10 +485,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/张文建.jpg" style="width: 50px;height: 50px;" />
<span>张文建</span>
</span>
<span>
<img src="./web/src/assets/avatar/炫.jpg" style="width: 50px;height: 50px;" />
<span>炫</span>
</span>
<span>
<img src="./web/src/assets/avatar/Lawliet.jpg" style="width: 50px;height: 50px;" />
<span>Lawliet</span>
@@ -465,10 +493,6 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/一叶孤舟.jpg" style="width: 50px;height: 50px;" />
<span>一叶孤舟</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>晏江</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>Eric</span>
@@ -481,12 +505,44 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>中文网字计划-江夏尧</span>
</span>
<span>
<img src="./web/src/assets/avatar/梁辉.jpg" style="width: 50px;height: 50px;" />
<span>梁辉</span>
</span>
<span>
<img src="./web/src/assets/avatar/海云.jpg" style="width: 50px;height: 50px;" />
<span>海云</span>
</span>
<span>
<img src="./web/src/assets/avatar/皮老板.jpg" style="width: 50px;height: 50px;" />
<span>皮老板</span>
</span>
<span>
<img src="./web/src/assets/avatar/h.r.w.jpg" style="width: 50px;height: 50px;" />
<span>h.r.w</span>
</span>
<span>
<img src="./web/src/assets/avatar/时光匆匆.png" style="width: 50px;height: 50px;" />
<span>时光匆匆</span>
</span>
<span>
<img src="./web/src/assets/avatar/广兴.jpg" style="width: 50px;height: 50px;" />
<span>广兴</span>
</span>
<span>
<img src="./web/src/assets/avatar/一亩三.jpg" style="width: 50px;height: 50px;" />
<span>一亩三</span>
</span>
<span>
<img src="./web/src/assets/avatar/xbkkjbs0246658.png" style="width: 50px;height: 50px;" />
<span>xbkkjbs0246658</span>
</span>
<span>
<img src="./web/src/assets/avatar/4399行星元帅.jpg" style="width: 50px;height: 50px;" />
<span>4399行星元帅</span>
</span>
<span>
<img src="./web/src/assets/avatar/Xavier.png" style="width: 50px;height: 50px;" />
<span>Xavier</span>
</span>
<span>
<img src="./web/src/assets/avatar/冒号括号.png" style="width: 50px;height: 50px;" />
<span>:)</span>
</span>
</p>

File diff suppressed because one or more lines are too long

2
dist/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

69
dist/js/chunk-9a7bee60.js vendored Normal file

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?f5839763ea0d5c47d80a" rel="stylesheet"><link href="dist/css/app.css?f5839763ea0d5c47d80a" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
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?f5839763ea0d5c47d80a"></script><script src="dist/js/app.js?f5839763ea0d5c47d80a"></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.12.2'
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.12.2",
"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

@@ -75,6 +75,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'
}
}
@@ -167,7 +172,9 @@ export const nodeDataNoStylePropList = [
'range',
'customLeft',
'customTop',
'customTextWidth'
'customTextWidth',
'checkbox',
'dir'
]
// 错误类型
@@ -200,6 +207,10 @@ export const cssContent = `
opacity: 1;
stroke-width: 2;
}
.smm-text-node-wrap {
user-select: none;
}
`
// html自闭合标签列表
@@ -212,3 +223,6 @@ export const selfCloseTagList = [
'meta',
'area'
]
// 非富文本模式下的节点文本行高
export const noneRichTextNodeLineHeight = 1.2

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,11 @@ 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,
// 【Select插件】
// 多选节点时鼠标移动到边缘时的画布移动偏移量
@@ -426,9 +437,6 @@ export const defaultOpt = {
transformRichTextOnEnterEdit: null,
// 可以传递一个函数即将结束富文本编辑前会执行该函数函数接收richText实例所以你可以在此时机更新quill文档数据
beforeHideRichTextEdit: null,
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
richTextEditFakeInPlace: false,
// 【OuterFrame】插件
outerFramePaddingX: 10,

View File

@@ -18,6 +18,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 +61,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

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

@@ -33,7 +33,9 @@ import {
removeRichTextStyes,
formatGetNodeGeneralization,
sortNodeList,
throttle
throttle,
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 = this.onNodeTextEditChange.bind(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', () => {
@@ -981,11 +984,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 +1010,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
}
@@ -1144,8 +1149,6 @@ class Render {
text = clipboardData.getData('text')
}
})
this.pasteData.img = img
this.pasteData.text = text
this.paste()
}
@@ -1158,134 +1161,121 @@ 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 {
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)
}
@@ -1560,6 +1550,8 @@ class Render {
return
}
this.activeNodeList.forEach(node => {
// 概要节点不允许添加下级节点
if (node.isGeneralization) return
node.setData({
expand: true
})
@@ -1587,28 +1579,32 @@ class Render {
// 设置节点样式
setNodeStyle(node, prop, value) {
let data = {
const data = {
[prop]: value
}
// 如果开启了富文本,则需要应用到富文本上
if (this.hasRichTextPlugin()) {
this.mindMap.richText.setNotActiveNodeStyle(node, {
[prop]: value
})
if (
this.hasRichTextPlugin() &&
this.mindMap.richText.isHasRichTextStyle(data)
) {
data.resetRichText = true
}
this.setNodeDataRender(node, data)
// 更新了连线的样式
if (lineStyleProps.includes(prop)) {
;(node.parent || node).renderLine(true)
(node.parent || node).renderLine(true)
}
}
// 设置节点多个样式
setNodeStyles(node, style) {
let data = { ...style }
const data = { ...style }
// 如果开启了富文本,则需要应用到富文本上
if (this.hasRichTextPlugin()) {
this.mindMap.richText.setNotActiveNodeStyle(node, style)
if (
this.hasRichTextPlugin() &&
this.mindMap.richText.isHasRichTextStyle(data)
) {
data.resetRichText = true
}
this.setNodeDataRender(node, data)
// 更新了连线的样式
@@ -1620,7 +1616,7 @@ class Render {
}
})
if (hasLineStyleProps) {
;(node.parent || node).renderLine(true)
(node.parent || node).renderLine(true)
}
}
@@ -1971,6 +1967,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 {
@@ -92,6 +98,32 @@ export default class TextEdit {
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)
}
})
}
// 解绑事件
@@ -101,6 +133,7 @@ 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]
@@ -158,7 +191,8 @@ export default class TextEdit {
if (node.isUseCustomNodeContent()) {
return
}
const { beforeTextEdit } = this.mindMap.opt
const { beforeTextEdit, openRealtimeRenderOnNodeTextEdit } =
this.mindMap.opt
if (typeof beforeTextEdit === 'function') {
let isShow = false
try {
@@ -172,7 +206,12 @@ export default class TextEdit {
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
const rect = g.node.getBoundingClientRect()
// 如果开启了大小实时更新,那么直接隐藏节点原文本
if (openRealtimeRenderOnNodeTextEdit) {
g.hide()
}
const params = {
node,
rect,
@@ -187,6 +226,21 @@ export default class TextEdit {
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 +262,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()
@@ -254,30 +329,38 @@ export default class TextEdit {
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.minHeight = rect.height + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = 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
// 选中文本
@@ -310,6 +393,27 @@ export default class TextEdit {
this.textEditNode.style.top = 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
}
}
// 删除文本编辑元素
removeTextEditEl() {
if (this.mindMap.richText) {
@@ -334,17 +438,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 +448,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
// 图片和文字节点的间距
@@ -207,10 +204,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 +223,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 +263,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 +287,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 +309,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 +329,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 +405,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 +419,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 +436,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 +489,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 +536,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 +674,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 +793,7 @@ class MindMapNode {
this.renderExpandBtn()
}
} else {
let { isActive, expand } = this.getData()
const { isActive, expand } = this.getData()
// 展开状态且非激活状态,且当前鼠标不在它上面,才隐藏
if (childrenLength <= 0) {
this.removeExpandBtn()
@@ -793,7 +811,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 +822,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 +844,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 +995,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 +1017,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 +1263,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 +1328,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 +1373,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
}
@@ -276,6 +278,10 @@ class Style {
// 连线
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,5 +1,4 @@
import {
measureText,
resizeImgSize,
removeHtmlStyle,
addHtmlStyle,
@@ -11,7 +10,19 @@ import {
} 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 {
CONSTANTS,
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 = {
@@ -113,6 +124,20 @@ function createIconNode() {
})
}
// 尝试给html指定标签添加内联样式
function tryAddHtmlStyle(text, style) {
const tagList = ['span', 'strong', 's', 'em', 'u']
// let _text = text
// for (let i = 0; i < tagList.length; i++) {
// text = addHtmlStyle(text, tagList[i], style)
// if (text !== _text) {
// break
// }
// }
// return text
return addHtmlStyle(text, tagList, style)
}
// 创建富文本节点
function createRichTextNode(specifyText) {
const hasCustomWidth = this.hasCustomWidth()
@@ -129,25 +154,22 @@ function createRichTextNode(specifyText) {
}
if ([CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
// 如果自定义过样式则不允许覆盖
if (!this.hasCustomStyle()) {
recoverText = true
}
// if (!this.hasCustomStyle() ) {
recoverText = true
// }
}
if (recoverText && !isUndef(text)) {
// 判断节点内容是否是富文本
let isRichText = checkIsRichText(text)
const isRichText = checkIsRichText(text)
// 获取自定义样式
const customStyle = this.style.getCustomStyle()
// 样式字符串
let style = this.style.createStyleText()
const style = this.style.createStyleText(customStyle)
if (isRichText) {
// 如果是富文本那么线移除内联样式
text = removeHtmlStyle(text)
// 再添加新的内联样式
let _text = text
text = addHtmlStyle(text, 'span', style)
// 给span添加样式没有成功则尝试给strong标签添加样式
if (text === _text) {
text = addHtmlStyle(text, 'strong', style)
}
text = this.tryAddHtmlStyle(text, style)
} else {
// 非富文本
text = `<p><span style="${style}">${text}</span></p>`
@@ -218,16 +240,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 +255,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 +270,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()
@@ -441,6 +470,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,
@@ -524,6 +556,7 @@ export default {
createImgNode,
getImgShowSize,
createIconNode,
tryAddHtmlStyle,
createRichTextNode,
createTextNode,
createHyperlinkNode,

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

@@ -69,28 +69,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 +83,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 +117,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 +140,7 @@ class Base {
isNodeDataChange ||
isLayerTypeChange ||
newNode.getData('resetRichText') ||
isNumberChange
isNodeInnerPrefixChange
) {
newNode.getSize()
newNode.needLayout = true
@@ -190,21 +177,23 @@ 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
isNodeInnerPrefixChange
) {
newNode.getSize()
newNode.needLayout = true
@@ -222,7 +211,7 @@ class Base {
layerIndex,
isRoot,
parent: !isRoot ? parent._node : null,
number: newNumberStr
...nodeInnerPrefixData
})
// uid保存到数据上为了节点复用
data.data.uid = newUid
@@ -463,8 +452,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

@@ -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,8 +4,6 @@ import 'quill/dist/quill.snow.css'
import {
walk,
getTextFromHtml,
isWhite,
getVisibleColorFromTheme,
isUndef,
checkSmmFormatData,
removeHtmlNodeByClass,
@@ -59,6 +57,14 @@ class RichText {
this.isCompositing = false
this.textNodePaddingX = 6
this.textNodePaddingY = 4
this.supportStyleProps = [
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
]
this.initOpt()
this.extendQuill()
this.appendCss()
@@ -75,9 +81,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 +94,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,6 +105,7 @@ class RichText {
`
.smm-richtext-node-wrap {
word-break: break-all;
user-select: none;
}
.smm-richtext-node-wrap p {
@@ -102,7 +114,7 @@ class RichText {
`
)
let cssText = `
.ql-editor {
.${CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP} {
overflow: hidden;
padding: 0;
height: auto;
@@ -184,12 +196,13 @@ class RichText {
return
}
let {
richTextEditFakeInPlace,
customInnerElsAppendTo,
nodeTextEditZIndex,
textAutoWrapWidth,
selectTextOnEnterEditText,
transformRichTextOnEnterEdit
transformRichTextOnEnterEdit,
openRealtimeRenderOnNodeTextEdit,
autoEmptyTextWhenKeydownEnterEdit
} = this.mindMap.opt
textAutoWrapWidth = node.hasCustomWidth()
? node.customTextWidth
@@ -206,26 +219,24 @@ 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;
`
this.textEditNode.addEventListener('click', e => {
@@ -245,7 +256,10 @@ class RichText {
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 +268,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') {
@@ -274,7 +281,10 @@ class RichText {
if (isEmptyText) {
this.lostStyle = true
}
if (noneEmptyNoneRichText) {
if (isFromKeyDown && autoEmptyTextWhenKeydownEnterEdit) {
this.textEditNode.innerHTML = ''
this.lostStyle = true
} else if (noneEmptyNoneRichText) {
// 还不是富文本
let text = String(nodeText).split(/\n/gim).join('<br>')
let html = `<p>${text}</p>`
@@ -284,7 +294,9 @@ class RichText {
this.textEditNode.innerHTML = this.cacheEditingText || nodeText
}
this.initQuillEditor()
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
document.querySelector(
'.' + CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP
).style.minHeight = originHeight + 'px'
this.showTextEdit = true
// 如果是刚创建的节点那么默认全选否则普通激活不全选除非selectTextOnEnterEditText配置为true
// 在selectTextOnEnterEditText时如果是在keydown事件进入的节点编辑也不需要全选
@@ -298,11 +310,26 @@ class RichText {
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)'
}
// 更新文本编辑框的大小和位置
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 =
@@ -319,27 +346,6 @@ 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 = {
@@ -356,13 +362,16 @@ class RichText {
// 获取当前正在编辑的内容
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>$/, '')
// return html.replace(/<p><br><\/p>$/, '')
}
// 给html字符串中的节点样式按样式名首字母排序
@@ -389,6 +398,12 @@ class RichText {
let html = this.getEditText()
html = this.sortHtmlNodeStyles(html)
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 +412,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 +473,17 @@ class RichText {
}
}
},
formats: [
'bold',
'italic',
'underline',
'strike',
'color',
'background',
'font',
'size',
'formula'
], // 明确指定允许的格式,不包含有序列表,无序列表等
theme: 'snow'
})
// 拦截复制事件即Ctrl + c去除多余的空行
@@ -656,14 +676,7 @@ class RichText {
// 再将样式恢复为当前主题改节点的默认样式
const style = {}
if (this.node) {
;[
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
].forEach(key => {
this.supportStyleProps.forEach(key => {
style[key] = this.node.style.merge(key)
})
}
@@ -694,14 +707,7 @@ class RichText {
if (!this.node) return
if (clear) {
// 清除文本样式
;[
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
].forEach(prop => {
this.supportStyleProps.forEach(prop => {
delete this.node.nodeData.data[prop]
})
} else {
@@ -776,6 +782,18 @@ class RichText {
return data
}
// 判断一个对象是否包含了富文本支持的样式字段
isHasRichTextStyle(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (this.supportStyleProps.includes(key)) {
return true
}
}
return false
}
// 给未激活的节点设置富文本样式
setNotActiveNodeStyle(node, style) {
const config = this.normalStyleToRichTextStyle(style)
@@ -788,17 +806,9 @@ class RichText {
// 检查指定节点是否存在自定义的富文本样式
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) {
for (let i = 0; i < this.supportStyleProps.length; i++) {
if (nodeData[this.supportStyleProps[i]] !== undefined) {
return true
}
}

View File

@@ -257,6 +257,7 @@ class Search {
replaceText = String(replaceText)
// 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的
const keep = replaceText.includes(this.searchText)
this.notResetSearchText = true
const hasRichTextPlugin = this.mindMap.renderer.hasRichTextPlugin()
this.matchNodeList.forEach(node => {
const text = this.getReplacedText(node, this.searchText, replaceText)

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

@@ -6,6 +6,7 @@ import {
import MersenneTwister from './mersenneTwister'
import { ForeignObject } from '@svgdotjs/svg.js'
import merge from 'deepmerge'
import { lineStyleProps } from '../theme/default'
// 深度优先遍历树
export const walk = (
@@ -507,13 +508,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 +792,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' }] },
@@ -1069,9 +1083,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 +1099,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) {

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: 2.8 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,11 @@ 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'
},
color: {
moreColor: 'More color'
@@ -112,7 +122,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 +229,6 @@ export default {
text: 'Text',
fontFamily: 'Font family',
fontSize: 'Font size',
lineHeight: 'Line height',
color: 'color',
addFontWeight: 'add font weight',
italic: 'Italic',
@@ -241,7 +253,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,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: '外框内边距'
tagPosition: '节点标签显示的位置',
tagPositionRight: '文本右侧',
tagPositionBottom: '文本下面',
alwaysShowExpandBtn: '是否一直显示展开收起按钮',
enableAutoEnterTextEditWhenKeydown: '键盘输入时自动进入文本编辑'
},
color: {
moreColor: '更多颜色'
@@ -112,7 +120,10 @@ export default {
copySuccess: '复制成功',
copyFail: '复制失败',
number: '编号其子节点',
expandNodeChild: '展开所有下级节点'
expandNodeChild: '展开所有下级节点',
unExpandNodeChild: '收起所有下级节点',
addToDo: '添加待办',
removeToDo: '删除待办'
},
count: {
words: '字数',
@@ -214,7 +225,6 @@ export default {
text: '文字',
fontFamily: '字体',
fontSize: '字号',
lineHeight: '行高',
color: '颜色',
addFontWeight: '加粗',
italic: '斜体',
@@ -239,7 +249,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: '僅在匯出時顯示',
@@ -39,35 +76,7 @@ export default {
watermarkAngle: '旋轉角度',
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: '外框內距'
belowNode: '顯示在節點下方'
},
color: {
moreColor: '更多顏色'
@@ -112,7 +121,10 @@ export default {
copySuccess: '複製成功',
copyFail: '複製失敗',
number: '將其子節點編號',
expandNodeChild: '展開所有下級節點'
expandNodeChild: '展開所有下級節點',
unExpandNodeChild: '收起所有下級節點',
addToDo: '添加待辦',
removeToDo: '刪除待辦'
},
count: {
words: '字數',
@@ -214,7 +226,6 @@ export default {
text: '文字',
fontFamily: '字型',
fontSize: '字型大小',
lineHeight: '行高',
color: '顏色',
addFontWeight: '粗體',
italic: '斜體',
@@ -238,7 +249,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

@@ -0,0 +1,574 @@
<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.mindMap.renderer.textEdit.hideEditTextBox()
this.setLocalConfig({
openNodeRichText: e
})
},
// 本地配置
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)
}