Compare commits

...

129 Commits

Author SHA1 Message Date
wanglin2
4ce9533763 打包0.6.17 2023-08-25 17:29:33 +08:00
wanglin2
43ecdafdd1 Doc: update 2023-08-25 17:25:56 +08:00
wanglin2
5ff8704778 Feat:拦截富文本编辑时的粘贴操作,去掉格式,只允许粘贴纯文本 2023-08-25 09:20:12 +08:00
wanglin2
d206d6dd99 Fix:修复mindMap.export方法代码错误 2023-08-24 18:02:58 +08:00
wanglin2
850b9ed936 Fix:修复导入百度脑图导出的xmind文件报错的问题 2023-08-24 17:57:12 +08:00
wanglin2
26508b5a62 Feat:支持配置是否开启双击复位思维导图 2023-08-24 17:40:35 +08:00
街角小林
061c82459e Merge pull request #282 from hyfree/main-1
Create index.d.ts
2023-08-24 17:23:53 +08:00
wanglin2
d2bf5be7c7 更新群二维码 2023-08-24 16:55:35 +08:00
hyfree
b726aa30e5 Create index.d.ts 2023-08-24 15:50:39 +08:00
wanglin2
6546ec090e Merge branch 'feature' into main 2023-08-22 09:58:18 +08:00
wanglin2
4dc5754f1d 打包0.6.16 2023-08-22 09:57:47 +08:00
wanglin2
7f199e6c2f Doc: update 2023-08-22 09:54:41 +08:00
wanglin2
2548ac4eb4 Feat:去除dom-to-image-more库 2023-08-21 15:12:40 +08:00
wanglin2
60e503ab1f Feat:1.去除导出svg方法的第二个参数,改为通过实例化配置;2.导出图片不再使用外部库 2023-08-21 15:10:03 +08:00
wanglin2
c718cbc030 Fix:优化导出图片逻辑,遍历节点转换图片的url时,如果已经是data:URL形式不用再处理 2023-08-21 13:48:45 +08:00
wanglin2
beb2b550f0 Fix:优化富文本测量元素的逻辑,删除样式的重复设置和重复添加 2023-08-21 09:58:33 +08:00
wanglin2
d3e1389f10 Fix:降级dom-to-image-more库版本,解决在firefox浏览器导出图片为空的问题 2023-08-21 09:14:19 +08:00
wanglin2
68cb24c0b6 Merge branch 'feature' into main 2023-08-19 08:04:45 +08:00
wanglin2
e3959107bd Demo:修复单独编辑大纲时能给根节点添加兄弟节点的bug 2023-08-19 08:04:28 +08:00
wanglin2
4b3c81ab91 Merge branch 'feature' into main 2023-08-18 23:11:03 +08:00
wanglin2
76b5d9d11d 打包0.6.15-fix.2 2023-08-18 23:10:29 +08:00
wanglin2
22d0fe5ac4 打包0.6.15-fix.2 2023-08-18 23:10:01 +08:00
wanglin2
8b8d549abd Fix:修复在Firefox浏览器中富文本节点无法显示的问题 2023-08-18 23:06:30 +08:00
wanglin2
731a5a504d Merge branch 'feature' into main 2023-08-18 15:13:42 +08:00
wanglin2
008e697b74 打包0.6.15-fix.1 2023-08-18 15:13:24 +08:00
wanglin2
fe1745e779 Merge branch 'feature' into main 2023-08-18 14:55:15 +08:00
wanglin2
e6ac9be402 打包0.6.15 2023-08-18 14:54:49 +08:00
wanglin2
b427a9ed1b Doc: update 2023-08-18 14:51:42 +08:00
wanglin2
a4dc9210b3 Fix:修复节点边框会重合的问题 2023-08-18 14:25:24 +08:00
wanglin2
fb681de1f5 Feat:增加错误处理 2023-08-18 11:02:24 +08:00
wanglin2
3757622521 Fix:修复画布距浏览器窗口左上角不为0时鼠标缩放时不以鼠标为中心的问题 2023-08-18 10:33:52 +08:00
wanglin2
12265be7d4 Feat:新增禁止鼠标滚轮缩放的配置 2023-08-18 10:16:19 +08:00
wanglin2
df1aed7e04 Fix:优化展开收起按钮的占位元素:1.没有子节点的节点不渲染该元素;2.根据是否存在子节点动态更新该元素 2023-08-18 10:09:23 +08:00
wanglin2
c32c9d1ba1 更新群二维码 2023-08-17 10:06:16 +08:00
wanglin2
26d75c9203 打包 2023-08-17 10:04:49 +08:00
wanglin2
8d1e9fa8e9 Demo:侧边栏涉及图形的选项增加可视化效果 2023-08-17 10:00:33 +08:00
wanglin2
ebc99e97af Feat:导出pdf支持根据长宽比自动调整方向 2023-08-17 08:49:40 +08:00
wanglin2
efe4aa0ec2 Feat:导出pdf支持根据图片大小分页导出 2023-08-16 17:45:34 +08:00
wanglin2
3f659af1e1 Featc:删除重复的exportPadding配置,改为使用exportPaddingX和exportPaddingY配置;去除导出图片时的双重padding设置 2023-08-16 17:20:05 +08:00
wanglin2
64209a6392 Featc:删除重复的exportPadding配置,改为使用exportPaddingX和exportPaddingY配置;去除导出图片时的双重padding设置 2023-08-16 17:19:35 +08:00
wanglin2
8d1e5dd2e9 Feat:导出svg的图形的paddingX和paddingY改为单侧padding 2023-08-16 17:17:18 +08:00
wanglin2
d218122752 '打包' 2023-08-16 09:31:03 +08:00
wanglin2
9af8afca22 Demo:单独编辑大纲不再和画布联动,优化大数据量下的编辑体验 2023-08-16 09:25:33 +08:00
wanglin2
002ec41ba8 Fix:修复节点文本为空时显示异常问题 2023-08-15 09:30:00 +08:00
wanglin2
ffff257f57 Demo:修复打开本地文件右上角的提示无法关闭的问 2023-08-15 09:03:26 +08:00
街角小林
41d0b675a0 Merge pull request #269 from suka233/main
fix: 修复尝试赋值给const常量导致的vite启动dev服务报错
2023-08-15 08:48:43 +08:00
suka233
7601d6730d Merge pull request #1 from suka233/suka233-patch-1
fix: 修复尝试赋值给const常量导致的vite启动dev服务报错
2023-08-14 17:07:29 +08:00
suka233
f0e9b76bb5 fix: 修复尝试赋值给const常量导致的vite启动dev服务报错 2023-08-14 15:13:42 +08:00
wanglin2
4ec33062a4 Merge branch 'feature' into main 2023-08-14 09:39:03 +08:00
wanglin2
edc63b1293 打包 2023-08-14 09:38:49 +08:00
wanglin2
ec83976818 Doc:调整首页文档 2023-08-14 09:36:10 +08:00
wanglin2
393410757b Demo:调整小地图位置,解决被侧边按钮遮挡的问题 2023-08-14 09:29:11 +08:00
wanglin2
f20748744a Demo:右下角支持跳转相关链接 2023-08-14 09:27:59 +08:00
wanglin2
067131f76d Merge branch 'feature' into main 2023-08-13 12:21:16 +08:00
wanglin2
f689d333f9 打包0.6.14 2023-08-13 12:20:31 +08:00
wanglin2
b3059bb6a3 Doc: update 2023-08-13 12:18:25 +08:00
wanglin2
b135f6a61c Demo:支持粘贴知犀思维导图的节点数据 2023-08-13 11:43:44 +08:00
wanglin2
2a8b71497f Feat:自定义处理剪贴板函数支持返回Promise实例 2023-08-13 09:49:29 +08:00
wanglin2
726ebc5e88 Feat:支持自定义处理剪贴板中的文本数据 2023-08-13 08:47:35 +08:00
wanglin2
2c56cd453c Demo:支持从右键菜单删除节点的超链接,备注 2023-08-12 22:45:41 +08:00
wanglin2
bf2f9b2697 Demo:修复退出禅模式后左下角的节点和字数统计未更新的问题 2023-08-12 22:17:38 +08:00
wanglin2
fbd061e8b3 Demo:修复大纲修改一个节点的文本后再点击其他节点时不聚焦的问题 2023-08-12 15:43:10 +08:00
wanglin2
680320ba76 Demo:修复基础样式侧边栏打开状态下,导入思维导图数据后侧边栏数据未更新的问题 2023-08-12 15:10:46 +08:00
wanglin2
0d992bd6b1 Demo:颜色选择器支持选择透明颜色 2023-08-12 14:58:32 +08:00
wanglin2
c80eacc5e7 Demo:修复在节点样式侧边栏里无法去除节点的文本修饰线样式的问题 2023-08-12 14:42:14 +08:00
wanglin2
6d202b4b7d Demo:修复无法在节点图片弹窗里删除节点图片的问题 2023-08-12 14:21:11 +08:00
wanglin2
94478fe9f3 Demo:修复开启输入自动进入文本编辑模式和其他输入框冲突的问题 2023-08-12 11:12:11 +08:00
wanglin2
5745e4567b Feat:修改复制粘贴实现方式,去除创建隐藏输入框,支持跨浏览器粘贴思维导图数据 2023-08-12 10:57:51 +08:00
wanglin2
c6a3f4ac7b Feat:去除创建隐藏输入框,通过navigator.clipboard实现复制粘贴 2023-08-11 17:39:04 +08:00
wanglin2
f6609b3050 更新群二维码 2023-08-11 09:11:30 +08:00
wanglin2
4d69778a50 打包0.6.13 2023-08-11 09:10:34 +08:00
wanglin2
c8dfbd3b87 Doc: update 2023-08-11 09:08:44 +08:00
wanglin2
c0aab1e921 Demo:分类整理图片文件 2023-08-10 16:56:04 +08:00
wanglin2
6efee6e859 打包 2023-08-10 10:09:08 +08:00
wanglin2
dd82bf0879 优化代码 2023-08-10 10:06:02 +08:00
街角小林
2d41b5f9e6 Merge pull request #250 from kalcaddle/main
修复部分小问题
2023-08-10 09:23:02 +08:00
街角小林
7e7b6fae9d Merge branch 'main' into main 2023-08-10 09:22:56 +08:00
街角小林
29020daaf0 Merge pull request #248 from Xbs233/main
fix:手势缩小时 会先放大一次 再缩小
2023-08-10 08:55:25 +08:00
wanglin2
6bcead7784 Demo:修复在大纲中插入新节点报错的问题 2023-08-10 08:50:26 +08:00
wanglin2
9625129691 Demo:完善右键菜单和富文本工具条的暗黑模式 2023-08-10 08:47:16 +08:00
warlee
765d0ee212 还原. 2023-08-09 18:58:52 +08:00
warlee
099d4cd78e 画布背景图为none时处理为空. 2023-08-09 18:56:14 +08:00
wanglin2
d9a6981df4 Fix:修复主题配置中背景图片为none时会发起一个异常请求的问题 2023-08-09 18:43:43 +08:00
wanglin2
bbf424c6d2 Feat:1.非富文本输入框进入编辑状态时取消默认全选;2.存在一个激活节点时,支持按下中文、数字、英文按键时自动进入文本编辑模式 2023-08-09 18:26:50 +08:00
warlee
5cde3b76fe Merge branch 'main' of https://github.com/kalcaddle/mind-map
* 'main' of https://github.com/kalcaddle/mind-map:
  打包
  Demo:组件卸载时解除绑定的事件
  打包
  Demo:一些耗时的操作添加loading
  Feat:去掉异步渲染节点的逻辑
2023-08-09 18:14:46 +08:00
warlee
75af742053 Merge branch 'wanglin2:main' into main 2023-08-09 18:04:00 +08:00
wanglin2
69e192ea9d Fix:修复在移动端激活节点、展开收起时等操作时会拉起输入法的问题 2023-08-09 17:22:23 +08:00
wanglin2
5a32c1d99d Fix:修复在移动端激活节点、展开收起时等操作时会拉起输入法的问题 2023-08-09 17:16:31 +08:00
wanglin2
e81e0a5512 Fix:修复快速拖动节点几次后会概率性报错的问题 2023-08-09 17:12:55 +08:00
wanglin2
2fef76c55c Feat:导出图片由html2canvas库改为dom-to-image-more库 2023-08-09 17:01:35 +08:00
wanglin2
49735950f2 打包 2023-08-09 09:37:20 +08:00
wanglin2
c005455144 Demo:组件卸载时解除绑定的事件 2023-08-09 09:36:08 +08:00
wanglin2
ca4afb5440 打包 2023-08-09 09:13:57 +08:00
wanglin2
885647cedf Demo:一些耗时的操作添加loading 2023-08-09 09:09:43 +08:00
wanglin2
dc8efbe3ef Feat:去掉异步渲染节点的逻辑 2023-08-09 09:08:45 +08:00
Xbs233
888b8e725a fix:手势缩小时 会先放大一次 再缩小
修复初次onTouchmove时 必然是放大的情况
2023-08-08 21:23:12 +08:00
warlee
15d65db19d Merge branch 'main' of https://github.com/kalcaddle/mind-map
* 'main' of https://github.com/kalcaddle/mind-map:
  '打包'
  Demo:保存视图数据的逻辑增加防抖操作,优化性能
2023-08-08 17:22:00 +08:00
warlee
2ce0d4cd11 no message 2023-08-08 17:21:50 +08:00
warlee
13f3c2c20c - 移动端手势缩放优化: 按线性关系进行缩放,双指位移可以调整画布位置;
- 拖动画布优化: 只读模式拖拽画布时点击在子元素上时无法拖动问题处理(只读模式拖拽,鼠标中键拖拽,移动端拖拽)
2023-08-08 17:21:05 +08:00
wanglin2
d707329526 '打包' 2023-08-08 16:31:15 +08:00
wanglin2
69faa8bb3e Demo:保存视图数据的逻辑增加防抖操作,优化性能 2023-08-08 16:29:29 +08:00
wanglin2
86b184d5c1 Doc: update 2023-08-08 09:51:05 +08:00
wanglin2
2b6263acb4 打包0.6.12 2023-08-07 18:07:58 +08:00
wanglin2
187c940e56 Doc: update 2023-08-07 17:54:56 +08:00
wanglin2
4b59bec01c Demo:优化界面暗黑效果 2023-08-07 17:05:47 +08:00
街角小林
9427ee550c Merge pull request #240 from kalcaddle/main
部分问题修复及细节优化
2023-08-07 16:25:18 +08:00
街角小林
14975e117c Merge branch 'main' into main 2023-08-07 16:25:08 +08:00
wanglin2
d6254c0cc2 Merge branch 'feature' into main 2023-08-07 16:07:12 +08:00
wanglin2
4a81ce9cc2 修复本地开发热更新失效的问题 2023-08-07 10:32:42 +08:00
wanglin2
d17191c890 Demo:优化大纲编辑 2023-08-07 10:09:05 +08:00
wanglin2
ff56fe3e68 Demo:扩展大纲功能,支持拖拽,删除 2023-08-06 22:37:13 +08:00
wanglin2
6bdcec0fca Demo:支持全屏编辑大纲 2023-08-05 18:38:22 +08:00
wanglin2
27885aabe7 Demo:大纲不再使用节点默认样式 2023-08-05 12:04:43 +08:00
wanglin2
e345037f9b Doc:update 2023-08-04 22:38:34 +08:00
warlee
da5290e649 ### 问题修复
- mindMap.setFullData设置文件内容,主题为深色时主界面没有自动切换到深色,要点击主题按钮才切换;
- xmind 部分老版本文件解析报错;(xmind解析导出给外部可用)
- 点击节点图片上的缩放按钮,点击后松开(没有拖拽缩放),后续图片缩放按钮都不显示问题;

### 细节优化
- 右侧边栏展开关闭添加css activeSidebar到container上,方便界面样式自适应调整;
- 节点插入图片后无法删除图片(添加贴纸也如此)
- 双击节点输入时,输入框样式边框覆盖到完整节点的话会更好;
- 双击节点选中文字;
- 鼠标中键按住拖拽画布支持(左键拖拽可能误操作在节点上,右键框选, 中键拖拽画布就很完善了);
2023-08-04 21:19:39 +08:00
wanglin2
20a8934da9 Fix:修复大纲里点击文字编辑时输入框焦点丢失的问题 2023-08-04 16:19:08 +08:00
wanglin2
0075c44b29 Fix:修复富文本模式下,搜索替换后换行会丢失的问题 2023-08-04 15:54:16 +08:00
wanglin2
ec7a8cdd43 Feat:搜索支持连续替换 2023-08-04 15:38:03 +08:00
wanglin2
1629bb7ccf Fix:修复搜索不能替换为空字符串的问题 2023-08-04 14:21:36 +08:00
wanglin2
35bff6ab57 Feat:曲线风格下,根节点的连接线样式支持和其他节点保持一致 2023-08-04 09:39:21 +08:00
wanglin2
ef9d8b0ea4 Fix:修复移动节点时新位置的提示块过大的问题 2023-08-04 09:11:02 +08:00
wanglin2
5d4c5703bb Fix:修复当思维导图距浏览器窗口左上角不为0时,小地图渲染不正确的问题 2023-08-03 17:04:41 +08:00
wanglin2
11bb519db8 Feat:支持格式刷功能 2023-08-03 16:50:07 +08:00
wanglin2
1952280003 Demo:给对话框添加唯一的class,方便开发者调整样式 2023-08-03 08:49:29 +08:00
wanglin2
c845a0b7fa Fix:修复只读模式下按住节点无法拖动画布的问题 2023-08-02 20:00:08 +08:00
wanglin2
f1748e7e42 Fix:修复移动端双指缩放过于灵敏的问题 2023-08-02 19:49:12 +08:00
wanglin2
49063d257b Feat:支持通过配置指定内部一些元素添加到的位置 2023-08-02 19:36:03 +08:00
wanglin2
803c83ac4f Feat:节点移动结束事件node_dragend增加回调参数,可以获取到移动到节点的uid 2023-08-02 19:05:15 +08:00
wanglin2
0bdf9f3add Fix:修复当思维导图全部移除可视区域后小地图中的指示器也会移除小地图区域的问题 2023-08-02 18:48:08 +08:00
202 changed files with 5213 additions and 1119 deletions

View File

@@ -97,7 +97,7 @@ const mindMap = new MindMap({
> 厚椰乳一盒 + 纯牛奶半盒 + 冰块 + 咖啡液 = 生椰拿铁 yyds
> 转账请备注【思维导图】。你的头像和名字将会出现在下面和[文档页面](https://wanglin2.github.io/mind-map/#/doc/zh/introduction/%E8%AF%B7%E4%BD%9C%E8%80%85%E5%96%9D%E6%9D%AF%E5%92%96%E5%95%A1)
> 推荐使用支付宝,微信获取不到头像。转账请备注【思维导图】。你的头像和名字将会出现在下面和[文档页面](https://wanglin2.github.io/mind-map/#/doc/zh/introduction/%E8%AF%B7%E4%BD%9C%E8%80%85%E5%96%9D%E6%9D%AF%E5%92%96%E5%95%A1)
<p>
<img src="./web/src/assets/img/alipay.jpg" style="width: 300px" />
@@ -149,4 +149,20 @@ 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>小米bbᯤ²ᴳ</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>Luke</span>
</span>
</p>

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"><link rel="icon" href="dist/logo.ico"><title>思绪思维导图</title><script>// 自定义静态资源的路径
window.externalPublicPath = './dist/'
// 接管应用
window.takeOverApp = false</script><link href="dist/css/chunk-vendors.css?e577492ccbbe7e7070c3" rel="stylesheet"><link href="dist/css/app.css?e577492ccbbe7e7070c3" 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 = () => {
window.takeOverApp = false</script><link href="dist/css/chunk-vendors.css?3f27c97d35a340dfd1c0" rel="stylesheet"><link href="dist/css/app.css?3f27c97d35a340dfd1c0" 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({
@@ -66,4 +66,4 @@
// 可以通过window.$bus.$on()来监听应用的一些事件
// 实例化页面
window.initApp()
}</script><script src="dist/js/chunk-vendors.js?e577492ccbbe7e7070c3"></script><script src="dist/js/app.js?e577492ccbbe7e7070c3"></script></body></html>
}</script><script src="dist/js/chunk-vendors.js?3f27c97d35a340dfd1c0"></script><script src="dist/js/app.js?3f27c97d35a340dfd1c0"></script></body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -2,6 +2,7 @@ import MindMap from './index'
import MiniMap from './src/plugins/MiniMap.js'
import Watermark from './src/plugins/Watermark.js'
import KeyboardNavigation from './src/plugins/KeyboardNavigation.js'
import ExportXMind from './src/plugins/ExportXMind.js'
import ExportPDF from './src/plugins/ExportPDF.js'
import Export from './src/plugins/Export.js'
import Drag from './src/plugins/Drag.js'
@@ -11,6 +12,7 @@ import RichText from './src/plugins/RichText'
import NodeImgAdjust from './src/plugins/NodeImgAdjust.js'
import TouchEvent from './src/plugins/TouchEvent.js'
import Search from './src/plugins/Search.js'
import Painter from './src/plugins/Painter.js'
import xmind from './src/parse/xmind.js'
import markdown from './src/parse/markdown.js'
import icons from './src/svg/icons.js'
@@ -30,6 +32,7 @@ MindMap
.usePlugin(Watermark)
.usePlugin(Drag)
.usePlugin(KeyboardNavigation)
.usePlugin(ExportXMind)
.usePlugin(ExportPDF)
.usePlugin(Export)
.usePlugin(Select)
@@ -38,5 +41,6 @@ MindMap
.usePlugin(TouchEvent)
.usePlugin(NodeImgAdjust)
.usePlugin(Search)
.usePlugin(Painter)
export default MindMap

14
simple-mind-map/index.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
// declare module "simple-mind-map";
declare module 'simple-mind-map'{
class MindMap {
constructor(opt:any);
handleOpt(opt:any):void;
render(callback:any, source:string):void;
reRender(callback:any, source:string):void;
resize():void;
on(event:any, fn:any):void;
setFullData(data:any):void;
getData(withConfig:any):any;
}
export default MindMap;
}

View File

@@ -7,10 +7,17 @@ import Style from './src/core/render/node/Style'
import KeyCommand from './src/core/command/KeyCommand'
import Command from './src/core/command/Command'
import BatchExecution from './src/utils/BatchExecution'
import { layoutValueList, CONSTANTS, commonCaches } from './src/constants/constant'
import {
layoutValueList,
CONSTANTS,
commonCaches,
ERROR_TYPES
} from './src/constants/constant'
import { SVG } from '@svgdotjs/svg.js'
import { simpleDeepClone, getType } from './src/utils'
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
import defaultTheme, {
checkIsNodeSizeIndependenceConfig
} from './src/themes/default'
import { defaultOpt } from './src/constants/defaultOptions'
// 思维导图
@@ -22,11 +29,13 @@ class MindMap {
// 容器元素
this.el = this.opt.el
if (!this.el) throw new Error('缺少容器元素el')
this.elRect = this.el.getBoundingClientRect()
// 画布宽高
this.width = this.elRect.width
this.height = this.elRect.height
if (this.width <= 0 || this.height <= 0) throw new Error('容器元素el的宽高不能为0')
// 画布
this.svg = SVG().addTo(this.el).size(this.width, this.height)
@@ -68,7 +77,7 @@ class MindMap {
this.batchExecution = new BatchExecution()
// 注册插件
MindMap.pluginList.forEach((plugin) => {
MindMap.pluginList.forEach(plugin => {
this.initPlugin(plugin)
})
@@ -136,10 +145,10 @@ class MindMap {
// 初始化缓存数据
initCache() {
Object.keys(commonCaches).forEach((key) => {
Object.keys(commonCaches).forEach(key => {
let type = getType(commonCaches[key])
let value = ''
switch(type) {
switch (type) {
case 'Boolean':
value = false
break
@@ -164,6 +173,7 @@ class MindMap {
this.renderer.clearAllActive()
this.opt.theme = theme
this.render(null, CONSTANTS.CHANGE_THEME)
this.emit('view_theme_change', theme)
}
// 获取当前主题
@@ -277,8 +287,12 @@ class MindMap {
// 导出
async export(...args) {
let result = await this.doExport.export(...args)
return result
try {
let result = await this.doExport.export(...args)
return result
} catch (error) {
this.opt.errorHandler(ERROR_TYPES.EXPORT_ERROR, error)
}
}
// 转换位置
@@ -316,9 +330,9 @@ class MindMap {
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 内边距
rect.width += paddingX
rect.height += paddingY
draw.translate(paddingX / 2, paddingY / 2)
rect.width += paddingX * 2
rect.height += paddingY * 2
draw.translate(paddingX, paddingY)
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
@@ -326,7 +340,11 @@ class MindMap {
// 克隆一份数据
let clone = svg.clone()
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
if ((rect.width > origWidth || rect.height > origHeight) && this.watermark && this.watermark.hasWatermark()) {
if (
(rect.width > origWidth || rect.height > origHeight) &&
this.watermark &&
this.watermark.hasWatermark()
) {
this.width = rect.width
this.height = rect.height
this.watermark.draw()
@@ -387,7 +405,7 @@ class MindMap {
// 销毁
destroy() {
// 移除插件
[...MindMap.pluginList].forEach((plugin) => {
;[...MindMap.pluginList].forEach(plugin => {
this[plugin.instanceName] = null
})
// 解绑事件
@@ -407,8 +425,8 @@ MindMap.usePlugin = (plugin, opt = {}) => {
MindMap.pluginList.push(plugin)
return MindMap
}
MindMap.hasPlugin = (plugin) => {
return MindMap.pluginList.findIndex((item) => {
MindMap.hasPlugin = plugin => {
return MindMap.pluginList.findIndex(item => {
return item === plugin
})
}

View File

@@ -1,17 +1,16 @@
{
"name": "simple-mind-map",
"version": "0.6.7",
"version": "0.6.15-fix.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "0.6.7",
"version": "0.6.15-fix.2",
"license": "MIT",
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",
"deepmerge": "^1.5.2",
"eventemitter3": "^4.0.7",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0",
@@ -255,6 +254,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
@@ -411,6 +411,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
@@ -937,6 +938,7 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
@@ -2142,6 +2144,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
@@ -2206,6 +2209,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
@@ -2461,7 +2465,8 @@
"base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
@@ -2576,6 +2581,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"requires": {
"utrie": "^1.0.2"
}
@@ -2966,6 +2972,7 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"requires": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
@@ -3749,6 +3756,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"requires": {
"utrie": "^1.0.2"
}
@@ -3800,6 +3808,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"requires": {
"base64-arraybuffer": "^1.0.2"
}

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.6.11-fix.1",
"version": "0.6.17",
"description": "一个简单的web在线思维导图",
"authors": [
{
@@ -27,7 +27,6 @@
"@svgdotjs/svg.js": "^3.0.16",
"deepmerge": "^1.5.2",
"eventemitter3": "^4.0.7",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0",

View File

@@ -323,10 +323,23 @@ export const nodeDataNoStylePropList = [
'richText',
'resetRichText',
'uid',
'activeStyle'
'activeStyle',
'associativeLineTargets',
'associativeLineTargetControlOffsets'
]
// 数据缓存
export const commonCaches = {
measureCustomNodeContentSizeEl: null
measureCustomNodeContentSizeEl: null,
measureRichtextNodeTextSizeEl: null
}
// 错误类型
export const ERROR_TYPES = {
READ_CLIPBOARD_ERROR: 'read_clipboard_error',
PARSE_PASTE_DATA_ERROR: 'parse_paste_data_error',
CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR: 'custom_handle_clipboard_text_error',
LOAD_CLIPBOARD_IMAGE_ERROR: 'load_clipboard_image_error',
BEFORE_TEXT_EDIT_ERROR: 'before_text_edit_error',
EXPORT_ERROR: 'export_error'
}

View File

@@ -18,8 +18,6 @@ export const defaultOpt = {
mouseScaleCenterUseMousePosition: true,
// 最多显示几个标签
maxTag: 5,
// 导出图片时的内边距
exportPadding: 20,
// 展开收缩按钮尺寸
expandBtnSize: 20,
// 节点里图片和文字的间距
@@ -81,7 +79,7 @@ export const defaultOpt = {
enableShortcutOnlyWhenMouseInSvg: true,
// 初始根节点的位置
initRootNodePosition: null,
// 导出png、svg、pdf时的图形内边距
// 导出png、svg、pdf时的图形内边距,注意是单侧内边距
exportPaddingX: 10,
exportPaddingY: 10,
// 节点文本编辑框的z-index
@@ -122,5 +120,51 @@ export const defaultOpt = {
// 是否开启自定义节点内容
isUseCustomNodeContent: false,
// 自定义返回节点内容的方法
customCreateNodeContent: null
customCreateNodeContent: null,
// 指定内部一些元素节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素添加到的位置默认添加到document.body下
customInnerElsAppendTo: null,
// 拖拽元素时,指示元素新位置的块的最大高度
nodeDragPlaceholderMaxSize: 20,
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
// 开启该特性后需要给你的输入框绑定keydown事件并禁止冒泡
enableAutoEnterTextEditWhenKeydown: false,
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
richTextEditFakeInPlace: false,
// 自定义对剪贴板文本的处理。当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据如果你想处理其他思维导图的数据比如processon、zhixi等那么可以传递一个函数接受当前剪贴板中的文本为参数返回处理后的数据可以返回两种类型
/*
1.返回一个纯文本,那么会直接以该文本创建一个子节点
2.返回一个节点对象,格式如下:
{
// 代表是simple-mind-map格式的数据
simpleMindMap: true,
// 节点数据同simple-mind-map节点数据格式
data: {
data: {
text: ''
},
children: []
}
}
*/
// 如果你的处理逻辑存在异步逻辑也可以返回一个promise
customHandleClipboardText: null,
// 禁止鼠标滚轮缩放你仍旧可以使用api进行缩放
disableMouseWheelZoom: false,
// 错误处理函数
errorHandler: (code, error) => {
console.error(code, error)
},
// 设置导出图片和svg时针对富文本节点内容也就是嵌入到svg中的html节点的默认样式覆盖
// 如果不覆盖,会发生偏移问题
resetCss: `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
`,
// 开启鼠标双击复位思维导图位置及缩放
enableDblclickReset: true
}

View File

@@ -108,6 +108,11 @@ export default class KeyCommand {
return arr
}
// 判断是否按下了组合键
hasCombinationKey(e) {
return e.ctrlKey || e.metaKey || e.altKey || e.shiftKey
}
// 获取快捷键对应的键值数组
getKeyCodeArr(key) {
let keyArr = key.split(/\s*\+\s*/)

View File

@@ -10,6 +10,7 @@ class Event extends EventEmitter {
this.mindMap = opt.mindMap
this.isLeftMousedown = false
this.isRightMousedown = false
this.isMiddleMousedown = false
this.mousedownPos = {
x: 0,
y: 0
@@ -92,6 +93,8 @@ class Event extends EventEmitter {
this.isLeftMousedown = true
} else if (e.which === 3) {
this.isRightMousedown = true
} else if (e.which === 2) {
this.isMiddleMousedown = true
}
this.mousedownPos.x = e.clientX
this.mousedownPos.y = e.clientY
@@ -107,9 +110,10 @@ class Event extends EventEmitter {
this.mousemoveOffset.y = e.clientY - this.mousedownPos.y
this.emit('mousemove', e, this)
if (
useLeftKeySelectionRightKeyDrag
this.isMiddleMousedown ||
(useLeftKeySelectionRightKeyDrag
? this.isRightMousedown
: this.isLeftMousedown
: this.isLeftMousedown)
) {
e.preventDefault()
this.emit('drag', e, this)
@@ -120,6 +124,7 @@ class Event extends EventEmitter {
onMouseup(e) {
this.isLeftMousedown = false
this.isRightMousedown = false
this.isMiddleMousedown = false
this.emit('mouseup', e, this)
}

View File

@@ -12,11 +12,12 @@ import {
simpleDeepClone,
walk,
bfsWalk,
loadImage
loadImage,
isUndef
} from '../../utils'
import { shapeList } from './node/Shape'
import { lineStyleProps } from '../../themes/default'
import { CONSTANTS } from '../../constants/constant'
import { CONSTANTS, ERROR_TYPES } from '../../constants/constant'
// 布局列表
const layouts = {
@@ -48,7 +49,7 @@ class Render {
this.themeConfig = this.mindMap.themeConfig
this.draw = this.mindMap.draw
// 渲染树,操作过程中修改的都是这里的数据
this.renderTree = merge({},this.mindMap.opt.data || {})
this.renderTree = merge({}, this.mindMap.opt.data || {})
// 是否重新渲染
this.reRender = false
// 是否正在渲染中
@@ -108,10 +109,13 @@ class Render {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
})
// 粘贴事件
this.mindMap.on('paste', data => {
this.onPaste(data)
})
// let timer = null
// this.mindMap.on('view_data_change', () => {
// clearTimeout(timer)
// timer = setTimeout(() => {
// this.render()
// }, 300)
// })
}
// 注册命令
@@ -153,9 +157,12 @@ class Render {
// 剪切节点
this.cutNode = this.cutNode.bind(this)
this.mindMap.command.add('CUT_NODE', this.cutNode)
// 修改节点样式
// 修改节点单个样式
this.setNodeStyle = this.setNodeStyle.bind(this)
this.mindMap.command.add('SET_NODE_STYLE', this.setNodeStyle)
// 修改节点多个样式
this.setNodeStyles = this.setNodeStyles.bind(this)
this.mindMap.command.add('SET_NODE_STYLES', this.setNodeStyles)
// 切换节点是否激活
this.setNodeActive = this.setNodeActive.bind(this)
this.mindMap.command.add('SET_NODE_ACTIVE', this.setNodeActive)
@@ -263,8 +270,7 @@ class Render {
this.copy = this.copy.bind(this)
this.mindMap.keyCommand.addShortcut('Control+c', this.copy)
this.mindMap.keyCommand.addShortcut('Control+v', () => {
// 隐藏输入框可能会失去焦点,所以要重新聚焦
this.textEdit.focusHiddenInput()
this.onPaste()
})
this.cut = this.cut.bind(this)
this.mindMap.keyCommand.addShortcut('Control+x', this.cut)
@@ -439,7 +445,12 @@ class Render {
}
// 插入同级节点,多个节点只会操作第一个节点
insertNode(openEdit = true, appointNodes = [], appointData = null) {
insertNode(
openEdit = true,
appointNodes = [],
appointData = null,
appointChildren = []
) {
appointNodes = this.formatAppointNodes(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
@@ -475,14 +486,19 @@ class Render {
resetRichText: isRichText,
...(appointData || {})
},
children: []
children: [...appointChildren]
})
this.mindMap.render()
}
}
// 插入子节点
insertChildNode(openEdit = true, appointNodes = [], appointData = null) {
insertChildNode(
openEdit = true,
appointNodes = [],
appointData = null,
appointChildren = []
) {
appointNodes = this.formatAppointNodes(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
@@ -513,7 +529,7 @@ class Render {
resetRichText: isRichText,
...(appointData || {})
},
children: []
children: [...appointChildren]
})
// 插入子节点时自动展开子节点
node.nodeData.data.expand = true
@@ -581,15 +597,29 @@ class Render {
// 复制节点
copy() {
this.beingCopyData = this.copyNode()
this.setCoptyDataToClipboard(this.beingCopyData)
}
// 剪切节点
cut() {
this.mindMap.execCommand('CUT_NODE', copyData => {
this.beingCopyData = copyData
this.setCoptyDataToClipboard(copyData)
})
}
// 将粘贴或剪切的数据设置到用户剪切板中
setCoptyDataToClipboard(data) {
if (navigator.clipboard) {
navigator.clipboard.writeText(
JSON.stringify({
simpleMindMap: true,
data
})
)
}
}
// 粘贴节点
paste() {
if (this.beingCopyData) {
@@ -598,7 +628,29 @@ class Render {
}
// 粘贴事件
async onPaste({ text, img }) {
async onPaste() {
const { errorHandler } = this.mindMap.opt
// 读取剪贴板的文字和图片
let text = null
let img = null
if (navigator.clipboard) {
try {
text = await navigator.clipboard.readText()
const items = await navigator.clipboard.read()
if (items && items.length > 0) {
for (const clipboardItem of items) {
for (const type of clipboardItem.types) {
if (/^image\//.test(type)) {
img = await clipboardItem.getType(type)
break
}
}
}
}
} catch (error) {
errorHandler(ERROR_TYPES.READ_CLIPBOARD_ERROR, error)
}
}
// 检查剪切板数据是否有变化
// 通过图片大小来判断图片是否发生变化,可能是不准确的,但是目前没有其他好方法
const imgSize = img ? img.size : 0
@@ -616,9 +668,51 @@ class Render {
if (this.currentBeingPasteType === CONSTANTS.PASTE_TYPE.CLIP_BOARD) {
// 存在文本,则创建子节点
if (text) {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
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
if (typeof res === 'object' && res.simpleMindMap) {
smmData = res.data
} else {
text = String(res)
}
}
} catch (error) {
errorHandler(ERROR_TYPES.CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR, error)
}
}
// 默认处理
if (useDefault) {
try {
const parsedData = JSON.parse(text)
if (parsedData && parsedData.simpleMindMap) {
smmData = parsedData.data
}
} catch (error) {
errorHandler(ERROR_TYPES.PARSE_PASTE_DATA_ERROR, error)
}
}
if (smmData) {
this.mindMap.execCommand(
'INSERT_CHILD_NODE',
false,
[],
{
...smmData.data
},
[...smmData.children]
)
} else {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
}
}
// 存在图片,则添加到当前激活节点
if (img) {
@@ -635,7 +729,7 @@ class Render {
})
}
} catch (error) {
console.log(error)
errorHandler(ERROR_TYPES.LOAD_CLIPBOARD_IMAGE_ERROR, error)
}
}
} else {
@@ -859,6 +953,42 @@ class Render {
}
}
// 设置节点多个样式
setNodeStyles(node, style, isActive) {
let data = {}
if (isActive) {
data = {
activeStyle: {
...(node.nodeData.data.activeStyle || {}),
...style
}
}
} else {
data = style
}
// 如果开启了富文本,则需要应用到富文本上
if (this.mindMap.richText) {
let config = this.mindMap.richText.normalStyleToRichTextStyle(style)
if (Object.keys(config).length > 0) {
this.mindMap.richText.showEditText(node)
this.mindMap.richText.formatAllText(config)
this.mindMap.richText.hideEditText([node])
}
}
this.setNodeDataRender(node, data)
// 更新了连线的样式
let props = Object.keys(style)
let hasLineStyleProps = false
props.forEach(key => {
if (lineStyleProps.includes(key)) {
hasLineStyleProps = true
}
})
if (hasLineStyleProps) {
;(node.parent || node).renderLine(true)
}
}
// 设置节点是否激活
setNodeActive(node, active) {
this.setNodeData(node, {
@@ -981,7 +1111,8 @@ class Render {
}
// 设置节点图片
setNodeImage(node, { url, title, width, height, custom = false }) {
setNodeImage(node, data) {
const { url, title, width, height, custom = false } = data || { url: '', title: '', width: 0, height: 0, custom: false }
this.setNodeDataRender(node, {
image: url,
imageTitle: title || '',
@@ -1132,6 +1263,8 @@ class Render {
node.generalizationBelongNode.updateGeneralization()
}
if (!notRender) this.mindMap.render()
} else {
this.mindMap.emit('node_tree_render_end')
}
}

View File

@@ -1,4 +1,5 @@
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils'
import { ERROR_TYPES } from '../../constants/constant'
// 节点文字编辑类
export default class TextEdit {
@@ -10,14 +11,11 @@ export default class TextEdit {
this.currentNode = null
// 文本编辑框
this.textEditNode = null
// 隐藏的文本输入框
this.hiddenInputEl = null
// 文本编辑框是否显示
this.showTextEdit = false
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
this.cacheEditingText = ''
this.bindEvent()
this.createHiddenInput()
}
// 事件
@@ -49,10 +47,6 @@ export default class TextEdit {
this.mindMap.on('before_node_active', () => {
this.hideEditTextBox()
})
// 节点激活事件
this.mindMap.on('node_active', () => {
this.focusHiddenInput()
})
// 注册编辑快捷键
this.mindMap.keyCommand.addShortcut('F2', () => {
if (this.renderer.activeNodeList.length <= 0) {
@@ -61,43 +55,29 @@ export default class TextEdit {
this.show(this.renderer.activeNodeList[0])
})
this.mindMap.on('scale', this.onScale)
}
// 创建一个隐藏的文本输入框
createHiddenInput() {
if (this.hiddenInputEl) return
this.hiddenInputEl = document.createElement('input')
this.hiddenInputEl.type = 'text'
this.hiddenInputEl.style.cssText = `
position: fixed;
left: -99999px;
top: -99999px;
`
// 监听粘贴事件
this.hiddenInputEl.addEventListener('paste', async event => {
event.preventDefault()
const text = (event.clipboardData || window.clipboardData).getData('text')
const files = event.clipboardData.files
let img = null
if (files.length > 0) {
for (let i = 0; i < files.length; i++) {
if (/^image\//.test(files[i].type)) {
img = files[i]
break
}
// // 监听按键事件,判断是否自动进入文本编辑模式
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
window.addEventListener('keydown', e => {
const activeNodeList = this.mindMap.renderer.activeNodeList
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
const node = activeNodeList[0]
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
if (node && this.checkIsAutoEnterTextEditKey(e)) {
this.show(node)
}
}
this.mindMap.emit('paste', {
text,
img
})
})
document.body.appendChild(this.hiddenInputEl)
}
}
// 让隐藏的文本输入框聚焦
focusHiddenInput() {
if (this.hiddenInputEl) this.hiddenInputEl.focus()
// 判断是否是自动进入文本编模式的按钮
checkIsAutoEnterTextEditKey(e) {
const keyCode = e.keyCode
return (
(keyCode === 229 ||
(keyCode >= 65 && keyCode <= 90) ||
(keyCode >= 48 && keyCode <= 57)) &&
!this.mindMap.keyCommand.hasCombinationKey(e)
)
}
// 注册临时快捷键
@@ -125,6 +105,7 @@ export default class TextEdit {
isShow = await beforeTextEdit(node, isInserting)
} catch (error) {
isShow = false
this.mindMap.opt.errorHandler(ERROR_TYPES.BEFORE_TEXT_EDIT_ERROR, error)
}
if (!isShow) return
}
@@ -136,7 +117,7 @@ export default class TextEdit {
this.mindMap.richText.showEditText(node, rect, isInserting)
return
}
this.showEditTextBox(node, rect)
this.showEditTextBox(node, rect, isInserting)
}
// 处理画布缩放
@@ -154,7 +135,8 @@ export default class TextEdit {
}
// 显示文本编辑框
showEditTextBox(node, rect) {
showEditTextBox(node, rect, isInserting) {
if (this.showTextEdit) return
this.mindMap.emit('before_show_text_edit')
this.registerTmpShortcut()
if (!this.textEditNode) {
@@ -167,7 +149,17 @@ export default class TextEdit {
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
document.body.appendChild(this.textEditNode)
this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('keydown', e => {
if (this.checkIsAutoEnterTextEditKey(e)) {
e.stopPropagation()
}
})
const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
let scale = this.mindMap.view.scale
let lineHeight = node.style.merge('lineHeight')
@@ -193,12 +185,27 @@ export default class TextEdit {
}
this.showTextEdit = true
// 选中文本
if (!this.cacheEditingText) {
// if (!this.cacheEditingText) {
// this.selectNodeText()
// }
if (isInserting) {
this.selectNodeText()
} else {
this.focus()
}
this.cacheEditingText = ''
}
// 聚焦
focus() {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.textEditNode)
range.collapse()
selection.removeAllRanges()
selection.addRange(range)
}
// 选中文本
selectNodeText() {
let selection = window.getSelection()

View File

@@ -1,11 +1,11 @@
import Style from './Style'
import Shape from './Shape'
import { asyncRun, nodeToHTML } from '../../../utils'
import { G, Rect, ForeignObject, SVG } from '@svgdotjs/svg.js'
import { G, ForeignObject, SVG } from '@svgdotjs/svg.js'
import nodeGeneralizationMethods from './nodeGeneralization'
import nodeExpandBtnMethods from './nodeExpandBtn'
import nodeCommandWrapsMethods from './nodeCommandWraps'
import nodeCreateContentsMethods from './nodeCreateContents'
import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect'
import { CONSTANTS } from '../../../constants/constant'
// 节点类
@@ -98,6 +98,8 @@ class Node {
this.isMultipleChoice = false
// 是否需要重新layout
this.needLayout = false
// 当前是否是隐藏状态
this.isHide = false
// 概要相关方法
Object.keys(nodeGeneralizationMethods).forEach(item => {
this[item] = nodeGeneralizationMethods[item].bind(this)
@@ -106,6 +108,10 @@ class Node {
Object.keys(nodeExpandBtnMethods).forEach(item => {
this[item] = nodeExpandBtnMethods[item].bind(this)
})
// 展开收起按钮占位元素相关方法
Object.keys(nodeExpandBtnPlaceholderRectMethods).forEach(item => {
this[item] = nodeExpandBtnPlaceholderRectMethods[item].bind(this)
})
// 命令的相关方法
Object.keys(nodeCommandWrapsMethods).forEach(item => {
this[item] = nodeCommandWrapsMethods[item].bind(this)
@@ -251,9 +257,11 @@ class Node {
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
this.shapePadding.paddingX = shapePaddingX
this.shapePadding.paddingY = shapePaddingY
// 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点
const borderWidth = this.getBorderWidth()
return {
width: _width + paddingX * 2 + shapePaddingX * 2,
height: _height + paddingY * 2 + margin + shapePaddingY * 2
width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth,
height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth
}
}
@@ -263,10 +271,12 @@ class Node {
this.group.clear()
let { width, height, textContentItemMargin } = this
let { paddingY } = this.getPaddingVale()
paddingY += this.shapePadding.paddingY
const halfBorderWidth = this.getBorderWidth() / 2
paddingY += this.shapePadding.paddingY + halfBorderWidth
// 节点形状
this.shapeNode = this.shapeInstance.createShape()
this.shapeNode.addClass('smm-node-shape')
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
this.group.add(this.shapeNode)
this.updateNodeShape()
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
@@ -357,21 +367,6 @@ class Node {
this.group.add(textContentNested)
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnPlaceholderRect() {
if (!this.mindMap.opt.alwaysShowExpandBtn) {
let { width, height } = this
if (!this._unVisibleRectRegionNode) {
this._unVisibleRectRegionNode = new Rect()
this._unVisibleRectRegionNode.fill({
color: 'transparent'
})
}
this.group.add(this._unVisibleRectRegionNode)
this.renderer.layout.renderExpandBtnRect(this._unVisibleRectRegionNode, this.expandBtnSize, width, height, this)
}
}
// 给节点绑定事件
bindGroupEvent() {
// 单击事件,选中节点
@@ -385,10 +380,10 @@ class Node {
this.active(e)
})
this.group.on('mousedown', e => {
if (this.isRoot && e.which === 3) {
if (this.isRoot && e.which === 3 && !this.mindMap.opt.readonly) {
e.stopPropagation()
}
if (!this.isRoot) {
if (!this.isRoot && e.which !== 2 && !this.mindMap.opt.readonly) {
e.stopPropagation()
}
// 多选和取消多选
@@ -414,7 +409,7 @@ class Node {
this.mindMap.emit('node_mousedown', this, e)
})
this.group.on('mouseup', e => {
if (!this.isRoot) {
if (!this.isRoot && e.which !== 2 && !this.mindMap.opt.readonly) {
e.stopPropagation()
}
this.mindMap.emit('node_mouseup', this, e)
@@ -476,9 +471,7 @@ class Node {
if (!this.group) {
return
}
let {
alwaysShowExpandBtn
} = this.mindMap.opt
let { alwaysShowExpandBtn } = this.mindMap.opt
if (alwaysShowExpandBtn) {
// 需要移除展开收缩按钮
if (this._expandBtn && this.nodeData.children.length <= 0) {
@@ -500,11 +493,48 @@ class Node {
this.renderGeneralization()
// 更新节点位置
let t = this.group.transform()
// // 如果上次不在可视区内,且本次也不在,那么直接返回
// let { left: ox, top: oy } = this.getNodePosInClient(
// t.translateX,
// t.translateY
// )
// let oldIsInClient =
// ox > 0 && oy > 0 && ox < this.mindMap.width && oy < this.mindMap.height
// let { left: nx, top: ny } = this.getNodePosInClient(this.left, this.top)
// let newIsNotInClient =
// nx + this.width < 0 ||
// ny + this.height < 0 ||
// nx > this.mindMap.width ||
// ny > this.mindMap.height
// if (!oldIsInClient && newIsNotInClient) {
// if (!this.isHide) {
// this.isHide = true
// this.group.hide()
// }
// return
// }
// // 如果当前是隐藏状态,那么先显示
// if (this.isHide) {
// this.isHide = false
// this.group.show()
// }
// 如果节点位置没有变化,则返回
if (this.left === t.translateX && this.top === t.translateY) return
this.group.translate(this.left - t.translateX, this.top - t.translateY)
}
// 获取节点相当于画布的位置
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
return {
left,
top
}
}
// 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置
reRender() {
let sizeChange = this.getSize()
@@ -546,10 +576,7 @@ class Node {
this.needLayout = false
this.layout()
}
if (this.needRerenderExpandBtnPlaceholderRect) {
this.needRerenderExpandBtnPlaceholderRect = false
this.renderExpandBtnPlaceholderRect()
}
this.updateExpandBtnPlaceholderRect()
this.update()
}
// 子节点
@@ -559,18 +586,14 @@ class Node {
this.nodeData.data.expand !== false
) {
let index = 0
asyncRun(
this.children.map(item => {
return () => {
item.render(() => {
index++
if (index >= this.children.length) {
callback()
}
})
this.children.forEach(item => {
item.render(() => {
index++
if (index >= this.children.length) {
callback()
}
})
)
})
} else {
callback()
}
@@ -592,13 +615,9 @@ class Node {
this.removeLine()
// 子节点
if (this.children && this.children.length) {
asyncRun(
this.children.map(item => {
return () => {
item.remove()
}
})
)
this.children.forEach(item => {
item.remove()
})
}
}
@@ -624,13 +643,9 @@ class Node {
}
// 子节点
if (this.children && this.children.length) {
asyncRun(
this.children.map(item => {
return () => {
item.hide()
}
})
)
this.children.forEach(item => {
item.hide()
})
}
}
@@ -650,13 +665,9 @@ class Node {
}
// 子节点
if (this.children && this.children.length) {
asyncRun(
this.children.map(item => {
return () => {
item.show()
}
})
)
this.children.forEach(item => {
item.show()
})
}
}
@@ -820,6 +831,11 @@ class Node {
) // 父级
}
// 获取节点非节点状态的边框大小
getBorderWidth() {
return this.style.merge('borderWidth', false, false) || 0
}
// 获取数据
getData(key) {
return key ? this.nodeData.data[key] || '' : this.nodeData.data

View File

@@ -62,7 +62,10 @@ export default class Shape {
// 创建形状节点
createShape() {
const shape = this.node.getShape()
const borderWidth = this.node.getBorderWidth()
let { width, height } = this.node
width -= borderWidth
height -= borderWidth
let node = null
// 矩形
if (shape === CONSTANTS.SHAPE.RECTANGLE) {

View File

@@ -17,7 +17,7 @@ class Style {
// 设置新样式
let { backgroundColor, backgroundImage, backgroundRepeat, backgroundPosition, backgroundSize } = themeConfig
el.style.backgroundColor = backgroundColor
if (backgroundImage) {
if (backgroundImage && backgroundImage !== 'none') {
el.style.backgroundImage = `url(${backgroundImage})`
el.style.backgroundRepeat = backgroundRepeat
el.style.backgroundPosition = backgroundPosition

View File

@@ -43,6 +43,11 @@ function setStyle(prop, value, isActive) {
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value, isActive)
}
// 修改多个样式
function setStyles(style, isActive) {
this.mindMap.execCommand('SET_NODE_STYLES', this, style, isActive)
}
export default {
setData,
setText,
@@ -52,5 +57,6 @@ export default {
setNote,
setTag,
setShape,
setStyle
setStyle,
setStyles
}

View File

@@ -1,4 +1,10 @@
import { measureText, resizeImgSize, removeHtmlStyle, addHtmlStyle, checkIsRichText } from '../../../utils'
import {
measureText,
resizeImgSize,
removeHtmlStyle,
addHtmlStyle,
checkIsRichText
} from '../../../utils'
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
import iconsSvg from '../../../svg/icons'
import { CONSTANTS, commonCaches } from '../../../constants/constant'
@@ -54,7 +60,10 @@ function createIconNode() {
}
let iconSize = this.mindMap.themeConfig.iconSize
return _data.icon.map(item => {
let src = iconsSvg.getNodeIconListIcon(item, this.mindMap.opt.iconList || [])
let src = iconsSvg.getNodeIconListIcon(
item,
this.mindMap.opt.iconList || []
)
let node = null
// svg图标
if (/^<svg/.test(src)) {
@@ -77,6 +86,7 @@ function createIconNode() {
// 创建富文本节点
function createRichTextNode() {
const { textAutoWrapWidth } = this.mindMap.opt
let g = new G()
// 重新设置富文本节点内容
let recoverText = false
@@ -108,25 +118,34 @@ function createRichTextNode() {
this.nodeData.data.text = text
}
let html = `<div>${this.nodeData.data.text}</div>`
let div = document.createElement('div')
if (!commonCaches.measureRichtextNodeTextSizeEl) {
commonCaches.measureRichtextNodeTextSizeEl = document.createElement('div')
commonCaches.measureRichtextNodeTextSizeEl.style.position = 'fixed'
commonCaches.measureRichtextNodeTextSizeEl.style.left = '-999999px'
this.mindMap.el.appendChild(commonCaches.measureRichtextNodeTextSizeEl)
}
let div = commonCaches.measureRichtextNodeTextSizeEl
div.innerHTML = html
div.style.cssText = `position: fixed; left: -999999px;`
let el = div.children[0]
el.classList.add('smm-richtext-node-wrap')
el.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px'
this.mindMap.el.appendChild(div)
el.style.maxWidth = textAutoWrapWidth + 'px'
let { width, height } = el.getBoundingClientRect()
width = Math.ceil(width) + 1// 修复getBoundingClientRect方法对实际宽度是小数的元素获取到的值是整数导致宽度不够文本发生换行的问题
// 如果文本为空,那么需要计算一个默认高度
if (height <= 0) {
div.innerHTML = '<p>abc123我和你</p>'
let elTmp = div.children[0]
elTmp.classList.add('smm-richtext-node-wrap')
height = elTmp.getBoundingClientRect().height
}
width = Math.ceil(width) + 1 // 修复getBoundingClientRect方法对实际宽度是小数的元素获取到的值是整数导致宽度不够文本发生换行的问题
height = Math.ceil(height)
g.attr('data-width', width)
g.attr('data-height', height)
html = div.innerHTML
this.mindMap.el.removeChild(div)
let foreignObject = new ForeignObject()
foreignObject.width(width)
foreignObject.height(height)
foreignObject.add(SVG(html))
foreignObject.add(div.children[0])
g.add(foreignObject)
return {
node: g,
@@ -268,15 +287,17 @@ function createNoteNode() {
if (!this.noteEl) {
this.noteEl = document.createElement('div')
this.noteEl.style.cssText = `
position: absolute;
position: fixed;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
display: none;
background-color: #fff;
z-index: ${ this.mindMap.opt.nodeNoteTooltipZIndex }
z-index: ${this.mindMap.opt.nodeNoteTooltipZIndex}
`
document.body.appendChild(this.noteEl)
const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.noteEl)
}
this.noteEl.innerText = this.nodeData.data.note
}
@@ -309,7 +330,7 @@ function createNoteNode() {
}
// 测量自定义节点内容元素的宽高
function measureCustomNodeContentSize (content) {
function measureCustomNodeContentSize(content) {
if (!commonCaches.measureCustomNodeContentSizeEl) {
commonCaches.measureCustomNodeContentSizeEl = document.createElement('div')
commonCaches.measureCustomNodeContentSizeEl.style.cssText = `
@@ -329,19 +350,19 @@ function measureCustomNodeContentSize (content) {
}
// 是否使用的是自定义节点内容
function isUseCustomNodeContent() {
function isUseCustomNodeContent() {
return !!this._customNodeContent
}
export default {
createImgNode,
getImgShowSize,
createIconNode,
createRichTextNode,
createTextNode,
createHyperlinkNode,
createTagNode,
createNoteNode,
measureCustomNodeContentSize,
isUseCustomNodeContent
}
createImgNode,
getImgShowSize,
createIconNode,
createRichTextNode,
createTextNode,
createHyperlinkNode,
createTagNode,
createNoteNode,
measureCustomNodeContentSize,
isUseCustomNodeContent
}

View File

@@ -0,0 +1,66 @@
import { Rect } from '@svgdotjs/svg.js'
// 渲染展开收起按钮的隐藏占位元素
function renderExpandBtnPlaceholderRect() {
// 根节点或没有子节点不需要渲染
if (
!this.nodeData.children ||
this.nodeData.children.length <= 0 ||
this.isRoot
) {
return
}
// 默认显示展开按钮的情况下也不需要渲染
if (!this.mindMap.opt.alwaysShowExpandBtn) {
let { width, height } = this
if (!this._unVisibleRectRegionNode) {
this._unVisibleRectRegionNode = new Rect()
this._unVisibleRectRegionNode.fill({
color: 'transparent'
})
}
this.group.add(this._unVisibleRectRegionNode)
this.renderer.layout.renderExpandBtnRect(
this._unVisibleRectRegionNode,
this.expandBtnSize,
width,
height,
this
)
}
}
// 删除展开收起按钮的隐藏占位元素
function clearExpandBtnPlaceholderRect() {
if (!this._unVisibleRectRegionNode) {
return
}
this._unVisibleRectRegionNode.remove()
this._unVisibleRectRegionNode = null
}
// 更新展开收起按钮的隐藏占位元素
function updateExpandBtnPlaceholderRect() {
// 布局改变需要重新渲染
if (this.needRerenderExpandBtnPlaceholderRect) {
this.needRerenderExpandBtnPlaceholderRect = false
this.renderExpandBtnPlaceholderRect()
}
// 没有子节点到有子节点需要渲染
if (this.nodeData.children && this.nodeData.children.length > 0) {
if (!this._unVisibleRectRegionNode) {
this.renderExpandBtnPlaceholderRect()
}
} else {
// 有子节点到没子节点,需要删除
if (this._unVisibleRectRegionNode) {
this.clearExpandBtnPlaceholderRect()
}
}
}
export default {
renderExpandBtnPlaceholderRect,
clearExpandBtnPlaceholderRect,
updateExpandBtnPlaceholderRect
}

View File

@@ -32,6 +32,7 @@ class View {
this.fit()
})
this.mindMap.svg.on('dblclick', () => {
if (!this.mindMap.opt.enableDblclickReset) return
this.reset()
})
// 拖动视图
@@ -65,7 +66,8 @@ class View {
mousewheelAction,
mouseScaleCenterUseMousePosition,
mousewheelMoveStep,
mousewheelZoomActionReverse
mousewheelZoomActionReverse,
disableMouseWheelZoom
} = this.mindMap.opt
// 是否自定义鼠标滚轮事件
if (
@@ -76,21 +78,28 @@ class View {
}
// 鼠标滚轮事件控制缩放
if (mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
let cx = mouseScaleCenterUseMousePosition ? e.clientX : undefined
let cy = mouseScaleCenterUseMousePosition ? e.clientY : undefined
if (disableMouseWheelZoom) return
const { x: clientX, y: clientY } = this.mindMap.toPos(e.clientX, e.clientY)
let cx = mouseScaleCenterUseMousePosition ? clientX : undefined
let cy = mouseScaleCenterUseMousePosition ? clientY : undefined
switch (dir) {
// 鼠标滚轮,向上和向左,都是缩小
case CONSTANTS.DIR.UP:
case CONSTANTS.DIR.LEFT:
mousewheelZoomActionReverse ? this.enlarge(cx, cy, isTouchPad) : this.narrow(cx, cy, isTouchPad)
mousewheelZoomActionReverse
? this.enlarge(cx, cy, isTouchPad)
: this.narrow(cx, cy, isTouchPad)
break
// 鼠标滚轮,向下和向右,都是放大
case CONSTANTS.DIR.DOWN:
case CONSTANTS.DIR.RIGHT:
mousewheelZoomActionReverse ? this.narrow(cx, cy, isTouchPad) : this.enlarge(cx, cy, isTouchPad)
mousewheelZoomActionReverse
? this.narrow(cx, cy, isTouchPad)
: this.enlarge(cx, cy, isTouchPad)
break
}
} else {// 鼠标滚轮事件控制画布移动
} else {
// 鼠标滚轮事件控制画布移动
let step = mousewheelMoveStep
if (isTouchPad) {
step = 5
@@ -147,6 +156,7 @@ class View {
// 平移x,y方向
translateXY(x, y) {
if (x === 0 && y === 0) return
this.x += x
this.y += y
this.transform()
@@ -154,24 +164,28 @@ class View {
// 平移x方向
translateX(step) {
if (step === 0) return
this.x += step
this.transform()
}
// 平移x方式到
translateXTo(x) {
if (x === 0) return
this.x = x
this.transform()
}
// 平移y方向
translateY(step) {
if (step === 0) return
this.y += step
this.transform()
}
// 平移y方向到
translateYTo(y) {
if (y === 0) return
this.y = y
this.transform()
}

View File

@@ -234,7 +234,7 @@ class LogicalStructure extends Base {
let nodeUseLineStylePath = nodeUseLineStyle
? ` L ${item.left + item.width},${y2}`
: ''
if (node.isRoot) {
if (node.isRoot && !this.mindMap.themeConfig.rootLineKeepSameInCurve) {
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
} else {
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath

View File

@@ -315,7 +315,7 @@ class MindMap extends Base {
nodeUseLineStylePath = ` L ${item.left + item.width},${y2}`
}
}
if (node.isRoot) {
if (node.isRoot && !this.mindMap.themeConfig.rootLineKeepSameInCurve) {
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
} else {
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath

View File

@@ -28,7 +28,7 @@ const handleList = node => {
}
// 将markdown转换成节点树
export const transformMarkdownTo = async md => {
export const transformMarkdownTo = md => {
const tree = fromMarkdown(md)
let root = {
children: []

View File

@@ -14,11 +14,13 @@ const parseXmindFile = file => {
async zip => {
try {
let content = ''
if (zip.files['content.json']) {
let json = await zip.files['content.json'].async('string')
let jsonFile = zip.files['content.json']
let xmlFile = zip.files['content.xml'] || zip.files['/content.xml']
if (jsonFile) {
let json = await jsonFile.async('string')
content = await transformXmind(json, zip.files)
} else if (zip.files['content.xml']) {
let xml = await zip.files['content.xml'].async('string')
} else if (xmlFile) {
let xml = await xmlFile.async('string')
let json = xmlConvert.xml2json(xml)
content = transformOldXmind(json)
}
@@ -64,13 +66,13 @@ const transformXmind = async (content, files) => {
}
// 图片
if (node.image && /\.(jpg|jpeg|png|gif|webp)$/.test(node.image.src)) {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
try {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
// 读取图片
let imageType = /\.([^.]+)$/.exec(node.image.src)[1]
let imageBase64 =
@@ -123,6 +125,7 @@ const transformOldXmind = content => {
let elements = data.elements
let root = null
let getRoot = arr => {
if (!arr) return
for (let i = 0; i < arr.length; i++) {
if (!root && arr[i].name === 'topic') {
root = arr[i]
@@ -142,9 +145,10 @@ const transformOldXmind = content => {
}
let walk = (node, newNode) => {
let nodeElements = node.elements
let nodeTitle = getItemByName(nodeElements, 'title')
newNode.data = {
// 节点内容
text: getItemByName(nodeElements, 'title').elements[0].text
text: nodeTitle && nodeTitle.elements && nodeTitle.elements[0].text
}
try {
// 节点备注
@@ -185,7 +189,7 @@ const transformOldXmind = content => {
if (_children && _children.elements && _children.elements.length > 0) {
_children.elements.forEach(item => {
if (item.name === 'topics') {
item.elements.forEach(item2 => {
(item.elements || []).forEach(item2 => {
let newChild = {}
newNode.children.push(newChild)
walk(item2, newChild)

View File

@@ -44,6 +44,7 @@ class Drag extends Base {
this.mouseMoveY = 0
// 鼠标移动的距离距鼠标按下的位置距离多少以上才认为是拖动事件
this.checkDragOffset = 10
this.minOffset = 10
}
// 绑定事件
@@ -105,6 +106,9 @@ class Drag extends Base {
this.node.isDrag = false
this.node.show()
this.removeCloneNode()
let overlapNodeUid = this.overlapNode ? this.overlapNode.nodeData.data.uid : ''
let prevNodeUid = this.prevNode ? this.prevNode.nodeData.data.uid : ''
let nextNodeUid = this.nextNode ? this.nextNode.nodeData.data.uid : ''
// 存在重叠子节点,则移动作为其子节点
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
@@ -134,7 +138,11 @@ class Drag extends Base {
this.mindMap.render()
}
this.reset()
this.mindMap.emit('node_dragend')
this.mindMap.emit('node_dragend', {
overlapNodeUid,
prevNodeUid,
nextNodeUid
})
}
// 创建克隆节点
@@ -196,9 +204,10 @@ class Drag extends Base {
// 检测重叠节点
checkOverlapNode() {
if (!this.drawTransform) {
if (!this.drawTransform || !this.placeholder) {
return
}
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt
let x = this.mouseMoveX
let y = this.mouseMoveY
this.overlapNode = null
@@ -240,19 +249,19 @@ class Drag extends Base {
let prevNodeRect = this.getNodeRect(prevBrother)
prevBrotherOffset = nodeRect.top - prevNodeRect.bottom
// 间距小于10就当它不存在
prevBrotherOffset = prevBrotherOffset >= 10 ? prevBrotherOffset / 2 : 0
prevBrotherOffset = prevBrotherOffset >= this.minOffset ? prevBrotherOffset / 2 : 0
} else {
// 没有前一个兄弟节点那么假设和前一个节点的距离为20
prevBrotherOffset = 10
prevBrotherOffset = this.minOffset
}
// 和后一个兄弟节点的距离
let nextBrotherOffset = 0
if (nextBrother) {
let nextNodeRect = this.getNodeRect(nextBrother)
nextBrotherOffset = nextNodeRect.top - nodeRect.bottom
nextBrotherOffset = nextBrotherOffset >= 10 ? nextBrotherOffset / 2 : 0
nextBrotherOffset = nextBrotherOffset >= this.minOffset ? nextBrotherOffset / 2 : 0
} else {
nextBrotherOffset = 10
nextBrotherOffset = this.minOffset
}
if (nodeRect.left <= x && nodeRect.right >= x) {
// 检测兄弟节点位置
@@ -265,11 +274,11 @@ class Drag extends Base {
y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
if (checkIsPrevNode) {
this.prevNode = node
let size = nextBrotherOffset > 0 ? nextBrotherOffset : 5
let size = nextBrotherOffset > 0 ? Math.min(nextBrotherOffset, nodeDragPlaceholderMaxSize) : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom)
} else if (checkIsNextNode) {
this.nextNode = node
let size = prevBrotherOffset > 0 ? prevBrotherOffset : 5
let size = prevBrotherOffset > 0 ? Math.min(prevBrotherOffset, nodeDragPlaceholderMaxSize) : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originTop - size)
}
}

View File

@@ -1,4 +1,9 @@
import { imgToDataUrl, downloadFile, readBlob, removeHTMLEntities } from '../utils'
import {
imgToDataUrl,
downloadFile,
readBlob,
removeHTMLEntities
} from '../utils'
import { SVG } from '@svgdotjs/svg.js'
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
import { transformToMarkdown } from '../parse/toMarkdown'
@@ -8,7 +13,6 @@ class Export {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
this.exportPadding = this.mindMap.opt.exportPadding
}
// 导出
@@ -35,6 +39,10 @@ class Export {
let imageList = svg.find('image')
let task = imageList.map(async item => {
let imgUlr = item.attr('href') || item.attr('xlink:href')
// 已经是data:URL形式不用转换
if (/^data:/.test(imgUlr)) {
return
}
let imgData = await imgToDataUrl(imgUlr)
item.attr('href', imgData)
})
@@ -49,7 +57,7 @@ class Export {
}
// svg转png
svgToPng(svgSrc, transparent) {
svgToPng(svgSrc, transparent, rotateWhenWidthLongerThenHeight = false) {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
@@ -57,12 +65,24 @@ class Export {
img.onload = async () => {
try {
let canvas = document.createElement('canvas')
canvas.width = img.width + this.exportPadding * 2
canvas.height = img.height + this.exportPadding * 2
// 如果宽比高长那么旋转90度
let needRotate =
rotateWhenWidthLongerThenHeight && img.width / img.height > 1
if (needRotate) {
canvas.width = img.height
canvas.height = img.width
} else {
canvas.width = img.width
canvas.height = img.height
}
let ctx = canvas.getContext('2d')
if (needRotate) {
ctx.rotate(0.5 * Math.PI)
ctx.translate(0, -img.height)
}
// 绘制背景
if (!transparent) {
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
await this.drawBackgroundToCanvas(ctx, img.width, img.height)
}
// 图片绘制到canvas里
ctx.drawImage(
@@ -71,8 +91,8 @@ class Export {
0,
img.width,
img.height,
this.exportPadding,
this.exportPadding,
0,
0,
img.width,
img.height
)
@@ -96,7 +116,7 @@ class Export {
backgroundImage,
backgroundRepeat = 'no-repeat',
backgroundPosition = 'center center',
backgroundSize = 'cover',
backgroundSize = 'cover'
} = this.mindMap.themeConfig
// 背景颜色
ctx.save()
@@ -107,18 +127,25 @@ class Export {
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
ctx.save()
drawBackgroundImageToCanvas(ctx, width, height, backgroundImage, {
backgroundRepeat,
backgroundPosition,
backgroundSize
}, (err) => {
if (err) {
reject(err)
} else {
resolve()
drawBackgroundImageToCanvas(
ctx,
width,
height,
backgroundImage,
{
backgroundRepeat,
backgroundPosition,
backgroundSize
},
err => {
if (err) {
reject(err)
} else {
resolve()
}
ctx.restore()
}
ctx.restore()
})
)
} else {
resolve()
}
@@ -152,14 +179,25 @@ class Export {
* 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png(name, transparent = false) {
async png(name, transparent = false, rotateWhenWidthLongerThenHeight) {
let { node, str } = await this.getSvgData()
str = removeHTMLEntities(str)
// 如果开启了富文本则使用htmltocanvas转换为图片
if (this.mindMap.richText) {
let res = await this.mindMap.richText.handleExportPng(node.node)
let imgDataUrl = await this.svgToPng(res, transparent)
return imgDataUrl
// 覆盖html默认的样式
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>${ this.mindMap.opt.resetCss }</style>`))
}
str = node.svg()
// 使用其他库html2canvas、dom-to-image-more等来完成导出
// let res = await this.mindMap.richText.handleExportPng(node.node)
// let imgDataUrl = await this.svgToPng(
// res,
// transparent,
// rotateWhenWidthLongerThenHeight
// )
// return imgDataUrl
}
// 转换成blob数据
let blob = new Blob([str], {
@@ -168,17 +206,21 @@ class Export {
// 转换成data:url数据
let svgUrl = await readBlob(blob)
// 绘制到canvas上
let res = await this.svgToPng(svgUrl, transparent)
let res = await this.svgToPng(
svgUrl,
transparent,
rotateWhenWidthLongerThenHeight
)
return res
}
// 导出为pdf
async pdf(name) {
async pdf(name, useMultiPageExport) {
if (!this.mindMap.doExportPDF) {
throw new Error('请注册ExportPDF插件')
}
let img = await this.png()
this.mindMap.doExportPDF.pdf(name, img)
let img = await this.png('', false, true)
this.mindMap.doExportPDF.pdf(name, img, useMultiPageExport)
}
// 导出为xmind
@@ -194,15 +236,13 @@ class Export {
// 导出为svg
// plusCssText附加的css样式如果svg中存在dom节点想要设置一些针对节点的样式可以通过这个参数传入
async svg(name, plusCssText) {
async svg(name) {
let { node } = await this.getSvgData()
// 开启了节点富文本编辑
if (this.mindMap.richText) {
if (plusCssText) {
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>${plusCssText}</style>`))
}
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>${ this.mindMap.opt.resetCss }</style>`))
}
}
node.first().before(SVG(`<title>${name}</title>`))

View File

@@ -8,7 +8,16 @@ class ExportPDF {
}
// 导出为pdf
pdf(name, img) {
pdf(name, img, useMultiPageExport = false) {
if (useMultiPageExport) {
this.multiPageExport(name, img)
} else {
this.onePageExport(name, img)
}
}
// 单页导出
onePageExport(name, img) {
let pdf = new JsPDF('', 'pt', 'a4')
let a4Width = 595
let a4Height = 841
@@ -37,6 +46,52 @@ class ExportPDF {
}
image.src = img
}
// 多页导出
multiPageExport(name, img) {
let image = new Image()
const a4Width = 592.28
const a4Height = 841.89
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
// 一页pdf显示高度
let pageHeight = (imageWidth / a4Width) * a4Height
// 未生成pdf的高度
let leftHeight = imageHeight
// 偏移
let position = 0
// a4纸的尺寸[595.28,841.89]图片在pdf中图片的宽高
let imgWidth = a4Width
let imgHeight = (a4Width / imageWidth) * imageHeight
let pdf = new JsPDF('', 'pt', 'a4')
// 有两个高度需要区分一个是图片的实际高度和生成pdf的页面高度(841.89)
// 当内容未超过pdf一页显示的范围无需分页
if (leftHeight < pageHeight) {
pdf.addImage(
img,
'PNG',
(a4Width - imgWidth) / 2,
(a4Height - imgHeight) / 2,
imgWidth,
imgHeight
)
} else {
// 分页
while (leftHeight > 0) {
pdf.addImage(img, 'PNG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight
position -= a4Height
// 避免添加空白页
if (leftHeight > 0) {
pdf.addPage()
}
}
}
pdf.save(name)
}
image.src = img
}
}
ExportPDF.instanceName = 'doExportPDF'

View File

@@ -12,6 +12,11 @@ class ExportXMind {
const zipData = await xmind.transformToXmind(data, name)
return zipData
}
// 获取解析器
getXmind() {
return xmind
}
}
ExportXMind.instanceName = 'doExportXMind'

View File

@@ -25,6 +25,11 @@ class MiniMap {
let { svg, rect, origWidth, origHeight, scaleX, scaleY } =
this.mindMap.getSvgData()
// 计算数据
const elRect = this.mindMap.elRect
rect.x -= elRect.left
rect.x2 -= elRect.left
rect.y -= elRect.top
rect.y2 -= elRect.top
let boxRatio = boxWidth / boxHeight
let actWidth = 0
let actHeight = 0
@@ -55,19 +60,28 @@ class MiniMap {
bottom: 0
}
viewBoxStyle.left =
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft + 'px'
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft
viewBoxStyle.right =
Math.max(0, ((_rectX2 - origWidth) / _rectWidth) * actWidth) +
miniMapBoxLeft +
'px'
miniMapBoxLeft
viewBoxStyle.top =
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop + 'px'
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop
viewBoxStyle.bottom =
Math.max(0, ((_rectY2 - origHeight) / _rectHeight) * actHeight) +
miniMapBoxTop +
'px'
miniMapBoxTop
if (viewBoxStyle.top > miniMapBoxTop + actHeight) {
viewBoxStyle.top = miniMapBoxTop + actHeight
}
if (viewBoxStyle.left > miniMapBoxLeft + actWidth) {
viewBoxStyle.left = miniMapBoxLeft + actWidth
}
Object.keys(viewBoxStyle).forEach((key) => {
viewBoxStyle[key] = viewBoxStyle[key] + 'px'
})
this.removeNodeContent(svg)
return {
svgHTML: svg.svg(), // 小地图html

View File

@@ -82,7 +82,7 @@ class NodeImgAdjust {
this.createResizeBtnEl()
}
this.setHandleElRect()
document.body.appendChild(this.handleEl)
this.handleEl.style.display = 'block'
this.isShowHandleEl = true
}
@@ -90,7 +90,7 @@ class NodeImgAdjust {
hideHandleEl() {
if (!this.isShowHandleEl) return
this.isShowHandleEl = false
document.body.removeChild(this.handleEl)
this.handleEl.style.display = 'none'
this.handleEl.style.backgroundImage = ``
this.handleEl.style.width = 0
this.handleEl.style.height = 0
@@ -121,8 +121,10 @@ class NodeImgAdjust {
this.handleEl.style.cssText = `
pointer-events: none;
position: fixed;
display:none;
background-size: cover;
`
this.handleEl.className = 'node-img-handle'
// 调整按钮元素
const btnEl = document.createElement('div')
btnEl.innerHTML = btnsSvg.imgAdjust
@@ -139,7 +141,7 @@ class NodeImgAdjust {
align-items: center;
cursor: nwse-resize;
`
this.handleEl.appendChild(btnEl)
btnEl.className = 'node-image-resize'
// 给按钮元素绑定事件
btnEl.addEventListener('mouseenter', () => {
// 移入按钮,会触发节点图片的移出事件,所以需要再次显示按钮
@@ -151,8 +153,50 @@ class NodeImgAdjust {
this.hideHandleEl()
})
btnEl.addEventListener('mousedown', e => {
e.stopPropagation()
this.onMousedown(e)
})
btnEl.addEventListener('mouseup', e => {
setTimeout(() => {
//点击后直接松开异常处理; 其他事件响应之后处理
this.hideHandleEl()
this.isAdjusted = false
}, 0)
})
btnEl.addEventListener('click', e => {
e.stopPropagation()
})
this.handleEl.appendChild(btnEl)
// 删除按钮
const btnRemove = document.createElement('div')
this.handleEl.prepend(btnRemove)
btnRemove.className = 'node-image-remove'
btnRemove.innerHTML = btnsSvg.remove
btnRemove.style.cssText = `
position: absolute;
right: 0;top:0;color:#fff;
pointer-events: auto;
background-color: rgba(0, 0, 0, 0.3);
width: ${this.resizeBtnSize}px;
height: ${this.resizeBtnSize}px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
`
btnRemove.addEventListener('mouseenter', e => {
this.showHandleEl()
})
btnRemove.addEventListener('mouseleave', e => {
if (this.isMousedown) return
this.hideHandleEl()
})
btnRemove.addEventListener('click', e => {
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null })
})
// 添加元素到页面
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.handleEl)
}
// 鼠标按钮按下事件

View File

@@ -0,0 +1,76 @@
import { nodeDataNoStylePropList } from '../constants/constant'
// 格式刷插件
class Painter {
constructor({ mindMap }) {
this.mindMap = mindMap
this.isInPainter = false
this.painterNode = null
this.bindEvent()
}
bindEvent() {
this.painterOneNode = this.painterOneNode.bind(this)
this.onEndPainter = this.onEndPainter.bind(this)
this.mindMap.on('node_click', this.painterOneNode)
this.mindMap.on('draw_click', this.onEndPainter)
}
unBindEvent() {
this.mindMap.off('node_click', this.painterOneNode)
this.mindMap.off('draw_click', this.onEndPainter)
}
// 开始格式刷
startPainter() {
if (this.mindMap.opt.readonly) return
let activeNodeList = this.mindMap.renderer.activeNodeList
if (activeNodeList.length <= 0) return
this.painterNode = activeNodeList[0]
this.isInPainter = true
this.mindMap.emit('painter_start')
}
// 结束格式刷
endPainter() {
this.painterNode = null
this.isInPainter = false
}
onEndPainter() {
this.endPainter()
this.mindMap.emit('painter_end')
}
// 格式刷某个节点
painterOneNode(node) {
if (
!node ||
!this.isInPainter ||
!this.painterNode ||
!node ||
node === this.painterNode
)
return
const style = {}
const painterNodeData = this.painterNode.nodeData.data
Object.keys(painterNodeData).forEach(key => {
if (!nodeDataNoStylePropList.includes(key)) {
style[key] = painterNodeData[key]
}
})
node.setStyles(style)
if (painterNodeData.activeStyle) {
node.setStyles(painterNodeData.activeStyle, true)
}
}
// 插件被移除前做的事情
beforePluginRemove() {
this.unBindEvent()
}
}
Painter.instanceName = 'painter'
export default Painter

View File

@@ -1,7 +1,12 @@
import Quill from 'quill'
import Delta from 'quill-delta'
import 'quill/dist/quill.snow.css'
import html2canvas from 'html2canvas'
import { walk, getTextFromHtml, isWhite, getVisibleColorFromTheme } from '../utils'
import {
walk,
getTextFromHtml,
isWhite,
getVisibleColorFromTheme
} from '../utils'
import { CONSTANTS } from '../constants/constant'
let extended = false
@@ -38,6 +43,7 @@ class RichText {
this.quill = null
this.range = null
this.lastRange = null
this.pasteUseRange = null
this.node = null
this.isInserting = false
this.styleEl = null
@@ -150,6 +156,12 @@ class RichText {
if (this.showTextEdit) {
return
}
const {
richTextEditFakeInPlace,
customInnerElsAppendTo,
nodeTextEditZIndex,
textAutoWrapWidth
} = this.mindMap.opt
this.node = node
this.isInserting = isInserting
if (!rect) rect = node._textData.node.node.getBoundingClientRect()
@@ -163,34 +175,65 @@ class RichText {
let scaleX = rect.width / originWidth
let scaleY = rect.height / originHeight
// 内边距
const paddingX = 6
const paddingY = 4
let paddingX = 6
let paddingY = 4
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;padding: ${paddingY}px ${paddingX}px;`
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;padding: ${paddingY}px ${paddingX}px;
`
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
document.body.appendChild(this.textEditNode)
this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('keydown', e => {
if (this.mindMap.renderer.textEdit.checkIsAutoEnterTextEditKey(e)) {
e.stopPropagation()
}
})
const targetNode = customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
// 使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
let bgColor = node.style.merge('fillColor')
let color = node.style.merge('color')
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.style.zIndex = nodeTextEditZIndex
this.textEditNode.style.backgroundColor =
bgColor === 'transparent' ? isWhite(color) ? getVisibleColorFromTheme(this.mindMap.themeConfig) : '#fff' : bgColor
bgColor === 'transparent'
? isWhite(color)
? getVisibleColorFromTheme(this.mindMap.themeConfig)
: '#fff'
: bgColor
this.textEditNode.style.minWidth = originWidth + paddingX * 2 + 'px'
this.textEditNode.style.minHeight = originHeight + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
this.textEditNode.style.display = 'block'
this.textEditNode.style.maxWidth =
this.mindMap.opt.textAutoWrapWidth + paddingX * 2 + 'px'
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'
}
}
if (!node.nodeData.data.richText) {
// 还不是富文本的情况
let text = node.nodeData.data.text.split(/\n/gim).join('<br>')
@@ -287,6 +330,7 @@ class RichText {
this.lastRange = this.range
this.range = null
if (range) {
this.pasteUseRange = range
let bounds = this.quill.getBounds(range.index, range.length)
let rect = this.textEditNode.getBoundingClientRect()
let rectInfo = {
@@ -326,6 +370,38 @@ class RichText {
this.lostStyle = false
}
})
// 拦截粘贴,只允许粘贴纯文本
this.quill.clipboard.addMatcher(Node.TEXT_NODE, (node) => {
let style = this.getPasteTextStyle()
return new Delta().insert(node.data, style)
})
this.quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
let ops = []
let style = this.getPasteTextStyle()
delta.ops.forEach(op => {
// 过滤出文本内容,过滤掉换行
if (op.insert && typeof op.insert === 'string' && op.insert !== '\n') {
ops.push({
attributes: { ...style },
insert: op.insert
})
}
})
delta.ops = ops
return delta
})
}
// 获取粘贴的文本的样式
getPasteTextStyle() {
// 粘贴的数据使用当前光标位置处的文本样式
if (this.pasteUseRange) {
return this.quill.getFormat(
this.pasteUseRange.index,
this.pasteUseRange.length
)
}
return {}
}
// 正则输入中文
@@ -498,11 +574,16 @@ class RichText {
}
}
walk(node)
let canvas = await html2canvas(el, {
backgroundColor: null
})
// 如果使用html2canvas
// let canvas = await html2canvas(el, {
// backgroundColor: null
// })
// return canvas.toDataURL()
const res = await domtoimage.toPng(el)
this.mindMap.el.removeChild(el)
return canvas.toDataURL()
return res
}
// 将所有节点转换成非富文本节点

View File

@@ -1,4 +1,4 @@
import { bfsWalk, getTextFromHtml, isUndef } from '../utils/index'
import { bfsWalk, getTextFromHtml, isUndef, replaceHtmlText } from '../utils/index'
// 搜索插件
class Search {
@@ -15,12 +15,19 @@ class Search {
this.currentIndex = -1
// 不要复位搜索文本
this.notResetSearchText = false
// 是否自动跳转下一个匹配节点
this.isJumpNext = false
this.onDataChange = this.onDataChange.bind(this)
this.mindMap.on('data_change', this.onDataChange)
}
// 节点数据改变了,需要重新搜索
onDataChange() {
if (this.isJumpNext) {
this.isJumpNext = false
this.search(this.searchText)
return
}
if (this.notResetSearchText) {
this.notResetSearchText = false
return
@@ -29,7 +36,7 @@ class Search {
}
// 搜索
search(text, callback) {
search(text, callback = () => {}) {
if (isUndef(text)) return this.endSearch()
text = String(text)
this.isSearching = true
@@ -88,13 +95,16 @@ class Search {
}
// 替换当前节点
replace(replaceText) {
replace(replaceText, jumpNext = false) {
if (
isUndef(replaceText) ||
replaceText === null ||
replaceText === undefined ||
!this.isSearching ||
this.matchNodeList.length <= 0
)
return
// 自动跳转下一个匹配节点
this.isJumpNext = jumpNext
replaceText = String(replaceText)
let currentNode = this.matchNodeList[this.currentIndex]
if (!currentNode) return
@@ -115,7 +125,8 @@ class Search {
// 替换所有
replaceAll(replaceText) {
if (
isUndef(replaceText) ||
replaceText === null ||
replaceText === undefined ||
!this.isSearching ||
this.matchNodeList.length <= 0
)
@@ -141,9 +152,10 @@ class Search {
getReplacedText(node, searchText, replaceText) {
let { richText, text } = node.nodeData.data
if (richText) {
text = getTextFromHtml(text)
return replaceHtmlText(text, searchText, replaceText)
} else {
return text.replaceAll(searchText, replaceText)
}
return text.replaceAll(searchText, replaceText)
}
// 发送事件

View File

@@ -6,7 +6,7 @@ class TouchEvent {
this.touchesNum = 0
this.singleTouchstartEvent = null
this.clickNum = 0
this.doubleTouchmoveDistance = 0
this.touchStartScaleView = null
this.bindEvent()
}
@@ -33,6 +33,7 @@ class TouchEvent {
// 手指按下事件
onTouchstart(e) {
this.touchesNum = e.touches.length
this.touchStartScaleView = null
if (this.touchesNum === 1) {
let touch = e.touches[0]
this.singleTouchstartEvent = touch
@@ -53,18 +54,46 @@ class TouchEvent {
let oy = touch1.clientY - touch2.clientY
let distance = Math.sqrt(Math.pow(ox, 2) + Math.pow(oy, 2))
// 以两指中心点进行缩放
let { x: touch1ClientX, y: touch1ClientY } = this.mindMap.toPos(touch1.clientX, touch1.clientY)
let { x: touch2ClientX, y: touch2ClientY } = this.mindMap.toPos(touch2.clientX, touch2.clientY)
let { x: touch1ClientX, y: touch1ClientY } = this.mindMap.toPos(
touch1.clientX,
touch1.clientY
)
let { x: touch2ClientX, y: touch2ClientY } = this.mindMap.toPos(
touch2.clientX,
touch2.clientY
)
let cx = (touch1ClientX + touch2ClientX) / 2
let cy = (touch1ClientY + touch2ClientY) / 2
if (distance > this.doubleTouchmoveDistance) {
// 放大
this.mindMap.view.enlarge(cx, cy)
} else {
// 缩小
this.mindMap.view.narrow(cx, cy)
// 手势缩放,基于最开始的位置进行缩放(基于前一个位置缩放不是线性关系); 缩放同时支持位置拖动
const view = this.mindMap.view
if (!this.touchStartScaleView) {
this.touchStartScaleView = {
distance: distance,
scale: view.scale,
x: view.x,
y: view.y,
cx: cx,
cy: cy
}
return
}
this.doubleTouchmoveDistance = distance
const viewBefore = this.touchStartScaleView
let scale = viewBefore.scale * (distance / viewBefore.distance)
if (Math.abs(distance - viewBefore.distance) <= 10) {
scale = viewBefore.scale
}
const ratio = 1 - scale / viewBefore.scale
view.scale = scale < 0.1 ? 0.1 : scale
view.x =
viewBefore.x +
(cx - viewBefore.x) * ratio +
(cx - viewBefore.cx) * scale
view.y =
viewBefore.y +
(cy - viewBefore.y) * ratio +
(cy - viewBefore.cy) * scale
view.transform()
this.mindMap.emit('scale', scale)
}
}
@@ -91,7 +120,7 @@ class TouchEvent {
}
this.touchesNum = 0
this.singleTouchstartEvent = null
this.doubleTouchmoveDistance = 0
this.touchStartScaleView = null
}
// 发送鼠标事件

View File

@@ -47,7 +47,8 @@ function showEditTextBox(g) {
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
document.body.appendChild(this.textEditNode)
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
}
let {
associativeLineTextFontSize,

View File

@@ -4,11 +4,15 @@ const open = `<svg t="1618141562310" class="icon" viewBox="0 0 1024 1024" versio
// 收缩按钮
const close = `<svg t="1618141589243" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13611" width="200" height="200"><path d="M512 105.472c225.28 0 407.04 181.76 407.04 407.04s-181.76 407.04-407.04 407.04-407.04-181.76-407.04-407.04 181.76-407.04 407.04-407.04z m0-74.24c-265.216 0-480.768 215.552-480.768 480.768s215.552 480.768 480.768 480.768 480.768-215.552 480.768-480.768-215.552-480.768-480.768-480.768z" p-id="13612"></path><path d="M252.928 474.624h518.144v74.24h-518.144z" p-id="13613"></path></svg>`
// 删除按钮
const remove = `<svg width="14px" height="14px" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13611" width="200" height="200"><path fill="#ffffff" d="M512 105.472c225.28 0 407.04 181.76 407.04 407.04s-181.76 407.04-407.04 407.04-407.04-181.76-407.04-407.04 181.76-407.04 407.04-407.04z m0-74.24c-265.216 0-480.768 215.552-480.768 480.768s215.552 480.768 480.768 480.768 480.768-215.552 480.768-480.768-215.552-480.768-480.768-480.768z" p-id="13612"></path><path fill="#ffffff" d="M252.928 474.624h518.144v74.24h-518.144z" p-id="13613"></path></svg>`
// 图片调整按钮
const imgAdjust = `<svg width="12px" height="12px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M1008.128 614.4a25.6 25.6 0 0 0-27.648 5.632l-142.848 142.848L259.072 186.88 401.92 43.52A25.6 25.6 0 0 0 384 0h-358.4a25.6 25.6 0 0 0-25.6 25.6v358.4a25.6 25.6 0 0 0 43.52 17.92l143.36-142.848 578.048 578.048-142.848 142.848a25.6 25.6 0 0 0 17.92 43.52h358.4a25.6 25.6 0 0 0 25.6-25.6v-358.4a25.6 25.6 0 0 0-15.872-25.088z" /></svg>`
export default {
open,
close,
remove,
imgAdjust
}

View File

@@ -18,6 +18,8 @@ export default {
lineDasharray: 'none',
// 连线风格
lineStyle: 'straight', // 针对logicalStructure、mindMap两种结构。曲线curve、直线straight、直连direct
// 曲线连接时,根节点和其他节点的连接线样式保持统一,默认根节点为 ( 型,其他节点为 { 型设为true后都为 { 型
rootLineKeepSameInCurve: true,
// 概要连线的粗细
generalizationLineWidth: 1,
// 概要连线的颜色

View File

@@ -455,25 +455,25 @@ export const loadImage = imgFile => {
}
// 移除字符串中的html实体
export const removeHTMLEntities = (str) => {
[['&nbsp;', '&#160;']].forEach((item) => {
export const removeHTMLEntities = str => {
;[['&nbsp;', '&#160;']].forEach(item => {
str = str.replaceAll(item[0], item[1])
})
return str
}
// 获取一个数据的类型
export const getType = (data) => {
export const getType = data => {
return Object.prototype.toString.call(data).slice(7, -1)
}
// 判断一个数据是否是null和undefined和空字符串
export const isUndef = (data) => {
export const isUndef = data => {
return data === null || data === undefined || data === ''
}
// 移除html字符串中节点的内联样式
export const removeHtmlStyle = (html) => {
export const removeHtmlStyle = html => {
return html.replaceAll(/(<[^\s]+)\s+style=["'][^'"]+["']\s*(>)/g, '$1$2')
}
@@ -485,12 +485,12 @@ export const addHtmlStyle = (html, tag, style) => {
// 检查一个字符串是否是富文本字符
let checkIsRichTextEl = null
export const checkIsRichText = (str) => {
export const checkIsRichText = str => {
if (!checkIsRichTextEl) {
checkIsRichTextEl = document.createElement('div')
}
checkIsRichTextEl.innerHTML = str
for (let c = checkIsRichTextEl.childNodes, i = c.length; i--;) {
for (let c = checkIsRichTextEl.childNodes, i = c.length; i--; ) {
if (c[i].nodeType == 1) return true
}
return false
@@ -503,13 +503,20 @@ export const replaceHtmlText = (html, searchText, replaceText) => {
replaceHtmlTextEl = document.createElement('div')
}
replaceHtmlTextEl.innerHTML = html
let walk = (root) => {
let walk = root => {
let childNodes = root.childNodes
childNodes.forEach((node) => {
if (node.nodeType === 1) {// 元素节点
childNodes.forEach(node => {
if (node.nodeType === 1) {
// 元素节点
walk(node)
} else if (node.nodeType === 3) {// 文本节点
root.replaceChild(document.createTextNode(node.nodeValue.replaceAll(searchText, replaceText)), node)
} else if (node.nodeType === 3) {
// 文本节点
root.replaceChild(
document.createTextNode(
node.nodeValue.replaceAll(searchText, replaceText)
),
node
)
}
})
}
@@ -518,25 +525,110 @@ export const replaceHtmlText = (html, searchText, replaceText) => {
}
// 判断一个颜色是否是白色
export const isWhite = (color) => {
export const isWhite = color => {
color = String(color).replaceAll(/\s+/g, '')
return ['#fff', '#ffffff', '#FFF', '#FFFFFF', 'rgb(255,255,255)'].includes(color) || /rgba\(255,255,255,[^)]+\)/.test(color)
return (
['#fff', '#ffffff', '#FFF', '#FFFFFF', 'rgb(255,255,255)'].includes(
color
) || /rgba\(255,255,255,[^)]+\)/.test(color)
)
}
// 判断一个颜色是否是透明
export const isTransparent = (color) => {
export const isTransparent = color => {
color = String(color).replaceAll(/\s+/g, '')
return ['', 'transparent'].includes(color) || /rgba\(\d+,\d+,\d+,0\)/.test(color)
return (
['', 'transparent'].includes(color) || /rgba\(\d+,\d+,\d+,0\)/.test(color)
)
}
// 从当前主题里获取一个非透明非白色的颜色
export const getVisibleColorFromTheme = (themeConfig) => {
export const getVisibleColorFromTheme = themeConfig => {
let { lineColor, root, second, node } = themeConfig
let list = [lineColor, root.fillColor, root.color, second.fillColor, second.color, node.fillColor, node.color, root.borderColor, second.borderColor, node.borderColor]
for(let i = 0; i < list.length; i++) {
let list = [
lineColor,
root.fillColor,
root.color,
second.fillColor,
second.color,
node.fillColor,
node.color,
root.borderColor,
second.borderColor,
node.borderColor
]
for (let i = 0; i < list.length; i++) {
let color = list[i]
if (!isTransparent(color) && !isWhite(color)) {
return color
}
}
}
}
// 将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本
let nodeRichTextToTextWithWrapEl = null
export const nodeRichTextToTextWithWrap = html => {
if (!nodeRichTextToTextWithWrapEl) {
nodeRichTextToTextWithWrapEl = document.createElement('div')
}
nodeRichTextToTextWithWrapEl.innerHTML = html
const childNodes = nodeRichTextToTextWithWrapEl.childNodes
let res = ''
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i]
if (node.nodeType === 1) {
// 元素节点
if (node.tagName.toLowerCase() === 'p') {
res += node.textContent + '\n'
} else {
res += node.textContent
}
} else if (node.nodeType === 3) {
// 文本节点
res += node.nodeValue
}
}
return res.replace(/\n$/, '')
}
// 将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容
let textToNodeRichTextWithWrapEl = null
export const textToNodeRichTextWithWrap = html => {
if (!textToNodeRichTextWithWrapEl) {
textToNodeRichTextWithWrapEl = document.createElement('div')
}
textToNodeRichTextWithWrapEl.innerHTML = html
const childNodes = textToNodeRichTextWithWrapEl.childNodes
let list = []
let str = ''
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i]
if (node.nodeType === 1) {
// 元素节点
if (node.tagName.toLowerCase() === 'br') {
list.push(str)
str = ''
} else {
str += node.textContent
}
} else if (node.nodeType === 3) {
// 文本节点
str += node.nodeValue
}
}
if (str) {
list.push(str)
}
return list
.map(item => {
return `<p><span>${item}</span></p>`
})
.join('')
}
// 判断是否是移动端环境
export const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
}

1
web/.env.library Normal file
View File

@@ -0,0 +1 @@
NODE_ENV=library

View File

@@ -6,7 +6,7 @@
"serve": "vue-cli-service serve",
"build": "vue-cli-service build && node ../copy.js",
"lint": "vue-cli-service lint",
"buildLibrary": "vue-cli-service build --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js && esbuild ../simple-mind-map/full.js --bundle --minify --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.min.js",
"buildLibrary": "vue-cli-service build --mode library --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js && esbuild ../simple-mind-map/full.js --bundle --minify --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.min.js",
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
"buildDoc": "node ./scripts/buildDoc.js",
"autoBuildDoc": "node ./scripts/autoBuildDoc.js",

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2479351 */
src: url('iconfont.woff2?t=1690537337895') format('woff2'),
url('iconfont.woff?t=1690537337895') format('woff'),
url('iconfont.ttf?t=1690537337895') format('truetype');
src: url('iconfont.woff2?t=1691822758372') format('woff2'),
url('iconfont.woff?t=1691822758372') format('woff'),
url('iconfont.ttf?t=1691822758372') format('truetype');
}
.iconfont {
@@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icontouming:before {
content: "\e60c";
}
.iconlieri:before {
content: "\e60b";
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Some files were not shown because too many files have changed in this diff Show More