Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c5a243c2f | ||
|
|
3541b7df40 | ||
|
|
9eab537b2e | ||
|
|
31d71db611 | ||
|
|
cfe07aa32e | ||
|
|
b8ac079009 | ||
|
|
040ce6601b | ||
|
|
77dd62477e | ||
|
|
b831d95063 | ||
|
|
bff6024e2e | ||
|
|
919b1517d9 | ||
|
|
2cbd08c532 | ||
|
|
c26cc5af83 | ||
|
|
5e300c0320 | ||
|
|
714567a733 | ||
|
|
28d6bb7d90 | ||
|
|
ecb2fbab48 | ||
|
|
a8c13b8f9a | ||
|
|
475dd4754a | ||
|
|
8a59185156 | ||
|
|
80ca74e477 | ||
|
|
c67bebb384 | ||
|
|
3a9002821c | ||
|
|
d0b289ed28 | ||
|
|
84782f924b | ||
|
|
2dd3db4c9d | ||
|
|
eb61e24746 | ||
|
|
abb332fd46 | ||
|
|
0c4fadb211 | ||
|
|
cd361c1f6e | ||
|
|
e0dc13c9f8 | ||
|
|
670114d8d8 | ||
|
|
c5b5fd86de | ||
|
|
493e0da7ae | ||
|
|
896121f6b6 | ||
|
|
b79076baa3 | ||
|
|
715627727e | ||
|
|
5ed5f0ff0d | ||
|
|
c12189ca87 | ||
|
|
be38eb2ca6 | ||
|
|
e80890aa7e | ||
|
|
e0ca3a5d12 | ||
|
|
30404721fa | ||
|
|
c4565143e8 | ||
|
|
2b25e28cb8 | ||
|
|
b543cfde38 | ||
|
|
f36bcbe39a | ||
|
|
328aef5308 | ||
|
|
697cee0b46 | ||
|
|
0f9ae45784 | ||
|
|
dcf4234ce2 | ||
|
|
f9406011e2 | ||
|
|
7d4acd15d0 | ||
|
|
6d729c53ab | ||
|
|
08df73aec4 | ||
|
|
f9eff11a27 | ||
|
|
0146e43815 | ||
|
|
cd2d5943c2 | ||
|
|
121eba1799 | ||
|
|
978c088d95 | ||
|
|
bb4a07b151 | ||
|
|
7f0368c2c8 | ||
|
|
402e0908b0 | ||
|
|
428ac15499 | ||
|
|
0f9f057d65 | ||
|
|
a4fe5e7765 | ||
|
|
1b7aad3de2 | ||
|
|
866287402f | ||
|
|
3b10b2b229 | ||
|
|
9661aa55c5 | ||
|
|
94ed53b31f | ||
|
|
bd2cfda905 | ||
|
|
a0e54870b5 | ||
|
|
bb9dd123f1 | ||
|
|
e5ee2f19d1 | ||
|
|
87d1b95dd9 | ||
|
|
6f0face378 | ||
|
|
37eab5f084 | ||
|
|
92d01e510b | ||
|
|
324652b1ba | ||
|
|
43c41e7ed2 | ||
|
|
a55afdd252 | ||
|
|
8414d39c4c | ||
|
|
e53e41dadc | ||
|
|
0321946b41 | ||
|
|
6071c7e021 | ||
|
|
17e9c29f1d | ||
|
|
d486cbd157 | ||
|
|
70cc88efbd | ||
|
|
0f6f714303 | ||
|
|
45418d803c | ||
|
|
e5648728c4 | ||
|
|
c387d78bfe | ||
|
|
469f5b26cd | ||
|
|
53fecc062f | ||
|
|
ec9f55e068 | ||
|
|
76b5f7d22a | ||
|
|
3b9cced7ea | ||
|
|
c7cd19f956 | ||
|
|
2a8959fb3b | ||
|
|
b836ca87b2 | ||
|
|
f1a97e4ced | ||
|
|
3cb035e365 | ||
|
|
2001bdd3ff | ||
|
|
10e9fa3f22 | ||
|
|
cbd57d2f36 | ||
|
|
cef586cc5c | ||
|
|
b0c0c58bec | ||
|
|
ed2fed78f7 | ||
|
|
75322ddc20 | ||
|
|
2577da10d0 | ||
|
|
4f2d4f8e36 | ||
|
|
6ec552d9fb | ||
|
|
0ecae72fff | ||
|
|
91cdb24a62 | ||
|
|
f8149ce383 | ||
|
|
b834b6fdd7 | ||
|
|
fe11d1152e | ||
|
|
c27ca12489 | ||
|
|
e05df4e92b | ||
|
|
ad63b4c72c | ||
|
|
09e393b174 | ||
|
|
43c7f0551a | ||
|
|
d0253ecf6c | ||
|
|
063742cb9b | ||
|
|
0a8b14ddd8 | ||
|
|
1bdbe0881e | ||
|
|
bd04516a7c | ||
|
|
75742bef27 | ||
|
|
31070c95fb | ||
|
|
193ef7f776 | ||
|
|
3d80c8698c | ||
|
|
88cea27d1e | ||
|
|
86c84cba0b | ||
|
|
8c7d11c629 | ||
|
|
84cb4e9420 | ||
|
|
119ff12339 | ||
|
|
f754bd538b | ||
|
|
26e956ad44 | ||
|
|
aa41d23505 | ||
|
|
19bffea87c | ||
|
|
3128a546e0 | ||
|
|
68f99d5236 | ||
|
|
0991315422 | ||
|
|
361edea91a | ||
|
|
9666f06631 | ||
|
|
24365a22c3 | ||
|
|
c95919a1a0 | ||
|
|
b0a5c8c12a | ||
|
|
1770cb98aa | ||
|
|
b3705712f2 | ||
|
|
f8c71321e6 | ||
|
|
9a289e19b3 | ||
|
|
c774bf01ef | ||
|
|
774609f209 | ||
|
|
4ba82cd7f0 | ||
|
|
0c23ff6527 | ||
|
|
99ff9bdff2 | ||
|
|
c97d92af25 | ||
|
|
01d332009c | ||
|
|
b5209118b5 | ||
|
|
d3353d50c5 | ||
|
|
9b2c9ad1d2 | ||
|
|
34f6fdd8e2 | ||
|
|
407b86c5ee | ||
|
|
e228386222 | ||
|
|
0cb12dcf9f | ||
|
|
fdecf8a308 | ||
|
|
4435feb014 | ||
|
|
57f4fb923c | ||
|
|
281902d962 | ||
|
|
e6a075d9a0 | ||
|
|
db468770ce | ||
|
|
3ef6097ee5 | ||
|
|
586d4b74e0 | ||
|
|
333e5cc878 | ||
|
|
84b08d410a | ||
|
|
d5d01c5f19 | ||
|
|
5688bb6821 | ||
|
|
c9d0b6c916 | ||
|
|
5a116d952a | ||
|
|
5717b4fa1d | ||
|
|
e04a5d4a6f | ||
|
|
a7d97065c6 | ||
|
|
4332abce4d | ||
|
|
f71b47b215 | ||
|
|
656cfa50c6 | ||
|
|
f10f8e0610 | ||
|
|
7533599cac | ||
|
|
f34de3acd9 | ||
|
|
4a5501f7a3 | ||
|
|
f52fd2ff48 | ||
|
|
628a6b72a2 | ||
|
|
3642763301 | ||
|
|
11c6fa3e45 | ||
|
|
d85210372d | ||
|
|
62e02ae956 | ||
|
|
799b46c68e | ||
|
|
6479841dee | ||
|
|
f1622e1a15 | ||
|
|
f490ac6f8d | ||
|
|
c342fbbe75 | ||
|
|
dde085b54e | ||
|
|
f8f126e8de | ||
|
|
71f92c985f | ||
|
|
3f2b5be4aa | ||
|
|
07712e7ac3 |
1
.gitignore
vendored
@ -3,3 +3,4 @@ node_modules
|
|||||||
dist_electron
|
dist_electron
|
||||||
simple-mind-map/dist
|
simple-mind-map/dist
|
||||||
simple-mind-map/types
|
simple-mind-map/types
|
||||||
|
utools/dist
|
||||||
550
README.md
@ -7,542 +7,78 @@
|
|||||||
[](https://github.com/wanglin2/mind-map/stargazers)
|
[](https://github.com/wanglin2/mind-map/stargazers)
|
||||||
[](https://github.com/wanglin2/mind-map/network/members)
|
[](https://github.com/wanglin2/mind-map/network/members)
|
||||||
|
|
||||||
> 中文名:思绪思维导图。一个简单&强大的 Web 思维导图。
|
[English](./README_EN.md) | 中文
|
||||||
|
|
||||||
本项目包含两部分:
|
> 中文名:思绪思维导图。一个简单&强大的 Web 思维导图库和思维导图软件。
|
||||||
|
|
||||||
1.一个 js 思维导图库,不依赖任何框架,可以使用它来快速完成 Web 思维导图产品的开发。
|
本项目包含两部分:开源的JavaScript库和闭源的客户端软件。
|
||||||
|
|
||||||
开发文档:[https://wanglin2.github.io/mind-map-docs/](https://wanglin2.github.io/mind-map-docs/)。
|
# 库、Web
|
||||||
|
|
||||||
2.一个 Web 思维导图,基于思维导图库、Vue2.x、ElementUI 开发,可以操作电脑本地文件,可以当做一个在线版思维导图应用使用,也可以自部署和二次开发。
|
> 即本仓库中的代码,目前已进入低维护状态。
|
||||||
|
|
||||||
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)。
|
- 一个 `js` 思维导图库,不依赖任何框架,可以用来快速完成 Web 思维导图产品的开发。
|
||||||
|
|
||||||
此外也提供了客户端可供下载使用,支持`Windows`、`Mac`及`Linux`,下载地址:
|
> 开发文档:[https://wanglin2.github.io/mind-map-docs/](https://wanglin2.github.io/mind-map-docs/)
|
||||||
|
|
||||||
Github:[releases](https://github.com/wanglin2/mind-map/releases)。百度云盘:[地址](https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3)。
|
- 一个 Web 思维导图,基于思维导图库、`Vue2.x`、`ElementUI` 开发,支持操作电脑本地文件,可以当做一个在线版思维导图应用使用,也可以自部署和二次开发。
|
||||||
|
|
||||||
> 客户端版本会落后于在线版本,尝试最新功能请优先使用在线版。
|
> 在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
|
||||||
|
|
||||||
【云存储版本】如果你需要带后端的云存储版本,可以尝试我们开发的另一个项目[理想文档](https://github.com/wanglin2/lx-doc)。
|
了解更多信息:[README](./README_MORE_ZH.md)。
|
||||||
|
|
||||||
# 特性
|
# 客户端、插件
|
||||||
|
|
||||||
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
|
> 客户端和插件代码不开源,正在积极开发维护中。
|
||||||
- [x] 支持逻辑结构图(向左、向右逻辑结构图)、思维导图、组织结构图、目录组织图、时间轴(横向、竖向)、鱼骨图等结构
|
|
||||||
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
|
|
||||||
- [x] 节点内容支持文本(普通文本、富文本)、图片、图标、超链接、备注、标签、概要、数学公式
|
|
||||||
- [x] 节点支持拖拽(拖拽移动、自由调整)、多种节点形状;支持扩展节点内容、支持使用 DDM 完全自定义节点内容
|
|
||||||
- [x] 支持画布拖动、缩放
|
|
||||||
- [x] 支持鼠标按键拖动选择和 Ctrl+左键两种多选节点方式
|
|
||||||
- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`、`xmind`、`txt`,支持从`json`、`xmind`、`markdown`导入
|
|
||||||
- [x] 支持快捷键、前进后退、关联线、搜索替换、小地图、水印、滚动条、手绘风格、彩虹线条、标记、外框
|
|
||||||
- [x] 提供丰富的配置,满足各种场景各种使用习惯
|
|
||||||
- [x] 支持协同编辑
|
|
||||||
- [x] 支持演示模式
|
|
||||||
|
|
||||||
官方提供了如下插件,可根据需求按需引入(某个功能不生效大概率是因为你没有引入对应的插件),具体使用方式请查看文档:
|
- 思绪思维导图客户端
|
||||||
|
|
||||||
> RichText(节点富文本插件)、Select(鼠标多选节点插件)、Drag(节点拖拽插件)、AssociativeLine(关联线插件)、Export(导出插件)、KeyboardNavigation(键盘导航插件)、MiniMap(小地图插件)、Watermark(水印插件)、TouchEvent(移动端触摸事件支持插件)、NodeImgAdjust(拖拽调整节点图片大小插件)、Search(搜索插件)、Painter(节点格式刷插件)、Scrollbar(滚动条插件)、Formula(数学公式插件)、Cooperate(协同编辑插件)、RainbowLines(彩虹线条插件)、Demonstrate(演示模式插件)、OuterFrame(外框插件)、MindMapLayoutPro(思维导图布局插件)、HandDrawnLikeStyle(手绘风格插件)[收费]、Notation(节点标记插件)[收费]、Numbers(节点编号插件)[收费]、Freemind(Freemind格式导入导出插件)[收费]、Excel(Excel格式导入导出插件)[收费]、Checkbox(待办插件)[收费]、Lineflow(节点连线流动插件)[收费]
|
本地化存储,隐私优先,数据安全,软件无需联网即可使用!
|
||||||
|
|
||||||
本项目不会实现的特性:
|
- [x] 1.支持创建无限数量的文件、节点(自由节点);支持创建使用模板;
|
||||||
|
- [x] 2.提供丰富的设置:基础设置、自定义字体/快捷键/右键菜单/图标、图床配置、AI配置、webdav云同步配置等等,可玩性很高;
|
||||||
|
- [x] 3.支持思维导图、逻辑结构图、目录组织图、组织结构图、时间轴、鱼骨图、表格等多种结构类型;
|
||||||
|
- [x] 4.内置上百个丰富好看的主题,也支持自定义主题及AI生成主题;
|
||||||
|
- [x] 5.节点支持添加文本、图片、链接、图标、备注、附件、标签、概要节点、关联线、外框、标记、待办、描述、编号、数学公式等丰富内容;
|
||||||
|
- [x] 6.支持导入XMind、FreeMind、Markdown、Txt、Xlsx等格式文件;支持导出为PNG、XMind、SVG、PDF、Markdown、Txt、Xlsx、FreeMind、Mermaid、Html等格式;
|
||||||
|
- [x] 7.丰富的样式设置:文字、边框、背景、形状、线条、内外边距、图片标签布局等等;
|
||||||
|
- [x] 8.支持历史版本管理、演示模式、AI生成、手绘风格、大纲编辑、水印、滚动条、同级节点对齐、小地图、进入指定节点、彩虹线条、节点双向链接、搜索替换等等实用有趣的功能;
|
||||||
|
|
||||||
> 1.自由节点,即多个根节点;
|
支持Windows、Mac及Linux系统;支持中文、英文、中文繁体、越南语、俄语语言。
|
||||||
>
|
|
||||||
> 2.概要节点后面继续添加节点;
|
|
||||||
>
|
|
||||||
> 如果你需要以上特性,那么本库可能无法满足你的需求。
|
|
||||||
|
|
||||||
# 安装
|
下载地址:[Github](https://github.com/wanglin2/mind-map/releases)、[百度网盘](https://pan.baidu.com/s/1C8phEJ5pagAAa-o1tU42Uw?pwd=jqfb)、[夸克网盘](https://pan.quark.cn/s/2733982f1976)
|
||||||
|
|
||||||
```bash
|
> 如果在macOS上安装后无法打开,报错**不受信任**或者**移到垃圾箱**,执行下面命令后再启动即可:
|
||||||
npm i simple-mind-map
|
> ``` shell
|
||||||
```
|
> sudo xattr -d com.apple.quarantine /Applications/思绪思维导图.app
|
||||||
|
> ```
|
||||||
|
|
||||||
# 使用
|

|
||||||
|
|
||||||
提供一个宽高不为 0 的容器元素:
|

|
||||||
|
|
||||||
```html
|

|
||||||
<div id="mindMapContainer"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
另外再设置一下`css`样式:
|

|
||||||
|
|
||||||
```css
|

|
||||||
#mindMapContainer * {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
然后创建一个实例:
|

|
||||||
|
|
||||||
```js
|
- Obsidian插件
|
||||||
import MindMap from "simple-mind-map";
|
|
||||||
|
|
||||||
const mindMap = new MindMap({
|
下载地址:[Github](https://github.com/wanglin2/obsidian-simplemindmap/releases)
|
||||||
el: document.getElementById("mindMapContainer"),
|
|
||||||
data: {
|
|
||||||
data: {
|
|
||||||
text: "根节点",
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
即可得到一个思维导图。想要实现更多功能?可以查看[开发文档](https://wanglin2.github.io/mind-map-docs/)。
|

|
||||||
|
|
||||||
# License
|

|
||||||
|
|
||||||
[MIT](./LICENSE)。保留`mind-map`版权声明的情况下可随意商用,如不想保留可联系作者。
|

|
||||||
|
|
||||||
# 微信交流群
|

|
||||||
|
|
||||||
微信添加`wanglinguanfang`拉你入群。根据过往的经验,大部分问题都可以通过查看issue列表或文档解决,所以提问前请确保你已经阅读完了所有文档,文档里没有的可在群里提问,不必私聊作者,如果你一定要私聊,请先发红包(¥9.9+每次)。
|

|
||||||
|
|
||||||
# star
|
- UTools插件
|
||||||
|
|
||||||
如果喜欢本项目,欢迎点个 star,这对我们很重要。
|
已上架[uTools](https://www.u.tools/)插件应用市场,可直接在`uTools`插件应用市场中搜索`思绪`进行安装,也可以直接访问该地址:[主页](https://www.u-tools.cn/plugins/detail/%E6%80%9D%E7%BB%AA%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE/),点击右侧的【启动】按钮进行安装。
|
||||||
|
|
||||||
[](https://star-history.com/#wanglin2/mind-map&Date)
|
|
||||||
|
|
||||||
# 关于定制
|
|
||||||
|
|
||||||
如果你有个性化的商用定制需求,可以联系我们,我们提供付费开发服务,无论前端、后端、还是部署,都可以帮你一站式搞定。
|
|
||||||
|
|
||||||
# 请作者喝杯咖啡
|
|
||||||
|
|
||||||
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡~你的赞助对项目的可持续发展非常重要,是作者持续维护的最大动力。
|
|
||||||
|
|
||||||
> 推荐使用支付宝,微信获取不到头像。转账请备注【思维导图】。
|
|
||||||
>
|
|
||||||
> 也可以通过购买付费插件来支持我们:[付费插件](https://wanglin2.github.io/mind-map-docs/plugins/about.html)。
|
|
||||||
>
|
|
||||||
> 赞助等级:最强王者(¥500+)、星耀赞助(¥300+)、钻石赞助(¥150+)、黄金赞助(¥50+)、青铜赞助
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<img src="./web/src/assets/img/alipay.jpg" style="width: 300px" />
|
|
||||||
<img src="./web/src/assets/img/wechat.jpg" style="width: 300px" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## 钻石赞助
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/黄智彪@一米一栗科技.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>黄智彪@一米一栗科技</span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## 黄金赞助
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/小土渣的宇宙.jpeg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>小土渣的宇宙</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/Chris.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>Chris</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>仓鼠</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/风格.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>风格</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>LiuJL</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/Kyle.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>Kyle</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/秀树因馨雨.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>秀树因馨雨</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>黄泳</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/ccccs.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>ccccs</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/炫.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>炫</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>晏江</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/梁辉.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>梁辉</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/千帆.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>千帆</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/布林.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>布林</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/达仁科技.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>达仁科技</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/沐风牧草.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>沐风牧草</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/俊奇.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>俊奇</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/庆国.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>庆国</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>Matt</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/雨馨.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>雨馨</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/峰.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>峰</span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## 青铜赞助
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/Think.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>Think</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/qp.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>qp</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/ZXR.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>ZXR</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/suka.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>suka</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/水车.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>水车</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/才镇.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>才镇</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/小米.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>小米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>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/南风.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>南风</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/蜉蝣撼大叔.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>蜉蝣撼大叔</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/乙.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>乙</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/敏.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>敏</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/有希.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>有希</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/樊笼.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>樊笼</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/小逗比.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>小逗比</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/天清如愿.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>天清如愿</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/敬明朗.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>敬明朗</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>飞箭</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/戚永峰.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>戚永峰</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/moom.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>moom</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/张扬.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>张扬</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/长沙利奥软件.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>长沙利奥软件</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/HaHN.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>HaHN</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/继龙.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>继龙</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/欣.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>欣</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>易空小易</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/国发.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>国发</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>建明</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/汪津合.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>汪津合</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>博文</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/慕智打印-兰兰.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>慕智打印-兰兰</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>锦冰</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/旭东.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>旭东</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/橘半.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>橘半</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/pluvet.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>pluvet</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/皇登攀.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>皇登攀</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>SR</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/逆水行舟.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>逆水行舟</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/L.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>L</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>sunniberg</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/在下青铜五.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>在下青铜五</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/木星二号.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>木星二号</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/阿晨.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>阿晨</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>铁</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/Alex.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>Alex</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/子豪.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>子豪</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/宏涛.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>宏涛</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/最多5个字.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>最多5个字</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/ZX.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>ZX</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>协成</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/木木.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>木木</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/好名字.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>好名字</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/lsytyrt.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>lsytyrt</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/buddy.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>buddy</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>小川</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/Tobin.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>Tobin</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/夏虫不语冰.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>夏虫不语冰</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/晴空.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>晴空</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/。.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>。</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/Jeffrey.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>Jeffrey</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/张文建.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>张文建</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/Lawliet.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>Lawliet</span>
|
|
||||||
</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>Eric</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/Joe.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>Joe</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>中文网字计划-江夏尧</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/海云.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>海云</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/皮老板.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>皮老板</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/h.r.w.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>h.r.w</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/时光匆匆.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>时光匆匆</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/广兴.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>广兴</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/一亩三.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>一亩三</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/xbkkjbs0246658.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>xbkkjbs0246658</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/4399行星元帅.jpg" style="width: 50px;height: 50px;" />
|
|
||||||
<span>4399行星元帅</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/Xavier.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>Xavier</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<img src="./web/src/assets/avatar/冒号括号.png" style="width: 50px;height: 50px;" />
|
|
||||||
<span>:)</span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|||||||
84
README_EN.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<h1 align="center">Simple mind map</h1>
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/simple-mind-map)
|
||||||
|

|
||||||
|
[](https://github.com/wanglin2/mind-map/issues)
|
||||||
|

|
||||||
|
[](https://github.com/wanglin2/mind-map/stargazers)
|
||||||
|
[](https://github.com/wanglin2/mind-map/network/members)
|
||||||
|
|
||||||
|
English | [中文](./README.md)
|
||||||
|
|
||||||
|
> Chinese name: 思绪思维导图. A simple & powerful web mind map library and mind map software.
|
||||||
|
|
||||||
|
This project consists of two parts: an open-source JavaScript library and closed-source client software.
|
||||||
|
|
||||||
|
# Library, Web
|
||||||
|
|
||||||
|
> Refers to the code in this repository, currently in low-maintenance status.
|
||||||
|
|
||||||
|
- A `js` mind map library, independent of any framework, which can be used to quickly develop web-based mind map products.
|
||||||
|
|
||||||
|
> Documentation: [https://wanglin2.github.io/mind-map-docs/](https://wanglin2.github.io/mind-map-docs/)
|
||||||
|
|
||||||
|
- A web-based mind map application, developed using the mind map library, `Vue2.x`, and `ElementUI`. It supports operations on local computer files, can be used as an online mind map application, and is open for self-deployment and secondary development.
|
||||||
|
|
||||||
|
> Online address: [https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
|
||||||
|
|
||||||
|
Learn more: [README](./README_MORE_EN.md).
|
||||||
|
|
||||||
|
# Client, Plugins
|
||||||
|
|
||||||
|
> The client and plugin code are not open source and are under active development and maintenance.
|
||||||
|
|
||||||
|
- 思绪思维导图 Client
|
||||||
|
|
||||||
|
Local storage, privacy-first, data security. The software can be used without an internet connection!
|
||||||
|
|
||||||
|
- [x] 1. Supports creating unlimited files and nodes (free nodes); supports creating and using templates.
|
||||||
|
- [x] 2. Offers rich settings: basic settings, custom fonts/shortcuts/right-click menus/icons, image hosting configuration, AI configuration, WebDAV cloud sync configuration, etc., highly customizable.
|
||||||
|
- [x] 3. Supports various structure types: mind maps, logical structure diagrams, directory organization charts, organizational charts, timelines, fishbone diagrams, tables, etc.
|
||||||
|
- [x] 4. Built-in hundreds of rich and beautiful themes, also supports custom themes and AI-generated themes.
|
||||||
|
- [x] 5. Nodes support adding rich content: text, images, links, icons, notes, attachments, tags, summary nodes, association lines, borders, markers, to-dos, descriptions, numbering, mathematical formulas, etc.
|
||||||
|
- [x] 6. Supports importing files in XMind, FreeMind, Markdown, Txt, Xlsx, etc.; supports exporting to PNG, XMind, SVG, PDF, Markdown, Txt, Xlsx, FreeMind, Mermaid, Html, etc.
|
||||||
|
- [x] 7. Rich style settings: text, borders, background, shape, lines, inner/outer margins, image tag layout, etc.
|
||||||
|
- [x] 8. Supports practical and interesting features: historical version management, presentation mode, AI generation, hand-drawn style, outline editing, watermark, scrollbars, sibling node alignment, minimap, entering specific nodes, rainbow lines, bidirectional node linking, search and replace, etc.
|
||||||
|
|
||||||
|
Supports Windows, Mac, and Linux systems; supports Chinese, English, Traditional Chinese, Vietnamese, and Russian languages.
|
||||||
|
|
||||||
|
Download links: [Github](https://github.com/wanglin2/mind-map/releases), [Baidu Netdisk](https://pan.baidu.com/s/1C8phEJ5pagAAa-o1tU42Uw?pwd=jqfb), [Quark Netdisk](https://pan.quark.cn/s/2733982f1976)
|
||||||
|
|
||||||
|
> If the software fails to open after installation on macOS, showing an error like **untrusted** or **moved to trash**, execute the following command and then restart:
|
||||||
|
> ``` shell
|
||||||
|
> sudo xattr -d com.apple.quarantine /Applications/思绪思维导图.app
|
||||||
|
> ```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Obsidian Plugin
|
||||||
|
|
||||||
|
Download link: [Github](https://github.com/wanglin2/obsidian-simplemindmap/releases)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- UTools Plugin
|
||||||
|
|
||||||
|
Available in the [uTools](https://www.u.tools/) plugin market. You can search for `思绪` directly in the uTools plugin market to install it, or visit this address directly: [Homepage](https://www.u-tools.cn/plugins/detail/%E6%80%9D%E7%BB%AA%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE/), and click the 【Launch】 button on the right to install.
|
||||||
91
README_MORE_EN.md
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Features
|
||||||
|
|
||||||
|
- [x] Plugin-based architecture. Apart from core functionalities, other features are provided as plugins, allowing on-demand use to reduce bundle size.
|
||||||
|
- [x] Supports various structures: Logical Structure Diagrams (left, right), Mind Maps, Organizational Charts, Directory Organization Charts, Timelines (horizontal, vertical), Fishbone Diagrams, etc.
|
||||||
|
- [x] Built-in multiple themes, allows high customization of styles, supports registering new themes.
|
||||||
|
- [x] Node content supports text (plain text, rich text), images, icons, hyperlinks, notes, tags, summaries, mathematical formulas.
|
||||||
|
- [x] Nodes support drag-and-drop (move, free resize), multiple node shapes; supports extending node content, supports using DDM for fully custom node content.
|
||||||
|
- [x] Supports canvas dragging and zooming.
|
||||||
|
- [x] Supports two methods for multi-selecting nodes: mouse button drag selection and Ctrl+left click.
|
||||||
|
- [x] Supports export to `json`, `png`, `svg`, `pdf`, `markdown`, `xmind`, `txt`; supports import from `json`, `xmind`, `markdown`.
|
||||||
|
- [x] Supports shortcuts, undo/redo, associative lines, search/replace, mini-map, watermark, scrollbars, hand-drawn style, rainbow lines, markers, outer frames.
|
||||||
|
- [x] Provides rich configuration options to meet various scenarios and usage habits.
|
||||||
|
- [x] Supports collaborative editing.
|
||||||
|
- [x] Supports presentation mode.
|
||||||
|
- [x] More features await your discovery.
|
||||||
|
|
||||||
|
The following plugins are officially provided and can be imported as needed (if a feature doesn't work, it's likely because the corresponding plugin hasn't been imported). Please refer to the documentation for specific usage:
|
||||||
|
|
||||||
|
| RichText (Node Rich Text Plugin) | Select (Mouse Multi-Select Node Plugin) | Drag (Node Drag Plugin) | AssociativeLine (Associative Line Plugin) |
|
||||||
|
| ------------------------------------- | ----------------------------------------- | ------------------------------------- | ----------------------------------------- |
|
||||||
|
| Export (Export Plugin) | KeyboardNavigation (Keyboard Navigation Plugin) | MiniMap (Mini-Map Plugin) | Watermark (Watermark Plugin) |
|
||||||
|
| TouchEvent (Mobile Touch Event Support Plugin) | NodeImgAdjust (Drag to Adjust Node Image Size Plugin) | Search (Search Plugin) | Painter (Node Format Painter Plugin) |
|
||||||
|
| Scrollbar (Scrollbar Plugin) | Formula (Mathematical Formula Plugin) | Cooperate (Collaborative Editing Plugin) | RainbowLines (Rainbow Lines Plugin) |
|
||||||
|
| Demonstrate (Presentation Mode Plugin) | OuterFrame (Outer Frame Plugin) | MindMapLayoutPro (Mind Map Layout Plugin) | |
|
||||||
|
|
||||||
|
Features that will **not** be implemented in this project:
|
||||||
|
|
||||||
|
> 1. Free nodes, i.e., multiple root nodes.
|
||||||
|
>
|
||||||
|
> 2. Adding nodes after a summary node.
|
||||||
|
>
|
||||||
|
> If you need the above features, this library may not meet your requirements.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i simple-mind-map
|
||||||
|
```
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Provide a container element with non-zero width and height:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="mindMapContainer"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Also, set the following CSS styles:
|
||||||
|
|
||||||
|
```css
|
||||||
|
#mindMapContainer * {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then create an instance:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import MindMap from "simple-mind-map";
|
||||||
|
|
||||||
|
const mindMap = new MindMap({
|
||||||
|
el: document.getElementById("mindMapContainer"),
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
text: "Root Node",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
You will get a mind map. Want to implement more features? Check the [Development Documentation](https://wanglin2.github.io/mind-map-docs/).
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
[MIT](./LICENSE). Commercial use is permitted freely as long as the `simple-mind-map` copyright notice and attribution are retained. If you have questions or wish to remove these requirements, please contact the author (WeChat: wanglinguanfang) for a paid option to remove them.
|
||||||
|
|
||||||
|
> Example: You can add the following content on any page of your application, such as the About page, Help page, Documentation page, Open Source Notice, etc.:
|
||||||
|
>
|
||||||
|
> The mind map feature of this product is developed based on the SimpleMindMap project. The copyright belongs to the original project. [Open Source License](https://github.com/wanglin2/mind-map/blob/main/LICENSE).
|
||||||
|
|
||||||
|
# Development Help / Technical Support / Consulting
|
||||||
|
|
||||||
|
Due to limited time and a shift in focus, we currently do not provide any development support (including paid support). Thank you for your understanding!
|
||||||
|
|
||||||
|
# Star
|
||||||
|
|
||||||
|
If you like this project, welcome to give it a star. It means a lot to us.
|
||||||
|
|
||||||
|
[](https://star-history.com/#wanglin2/mind-map&Date)
|
||||||
986
README_MORE_ZH.md
Normal file
@ -0,0 +1,986 @@
|
|||||||
|
# 特性
|
||||||
|
|
||||||
|
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
|
||||||
|
- [x] 支持逻辑结构图(向左、向右逻辑结构图)、思维导图、组织结构图、目录组织图、时间轴(横向、竖向)、鱼骨图等结构
|
||||||
|
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
|
||||||
|
- [x] 节点内容支持文本(普通文本、富文本)、图片、图标、超链接、备注、标签、概要、数学公式
|
||||||
|
- [x] 节点支持拖拽(拖拽移动、自由调整)、多种节点形状;支持扩展节点内容、支持使用 DDM 完全自定义节点内容
|
||||||
|
- [x] 支持画布拖动、缩放
|
||||||
|
- [x] 支持鼠标按键拖动选择和 Ctrl+左键两种多选节点方式
|
||||||
|
- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`、`xmind`、`txt`,支持从`json`、`xmind`、`markdown`导入
|
||||||
|
- [x] 支持快捷键、前进后退、关联线、搜索替换、小地图、水印、滚动条、手绘风格、彩虹线条、标记、外框
|
||||||
|
- [x] 提供丰富的配置,满足各种场景各种使用习惯
|
||||||
|
- [x] 支持协同编辑
|
||||||
|
- [x] 支持演示模式
|
||||||
|
- [x] 更多功能等你来发现
|
||||||
|
|
||||||
|
官方提供了如下插件,可根据需求按需引入(某个功能不生效大概率是因为你没有引入对应的插件),具体使用方式请查看文档:
|
||||||
|
|
||||||
|
| RichText(节点富文本插件) | Select(鼠标多选节点插件) | Drag(节点拖拽插件) | AssociativeLine(关联线插件) |
|
||||||
|
| ------------------------------------ | ----------------------------------------- | ------------------------------------ | ------------------------------------ |
|
||||||
|
| Export(导出插件) | KeyboardNavigation(键盘导航插件) | MiniMap(小地图插件) | Watermark(水印插件) |
|
||||||
|
| TouchEvent(移动端触摸事件支持插件) | NodeImgAdjust(拖拽调整节点图片大小插件) | Search(搜索插件) | Painter(节点格式刷插件) |
|
||||||
|
| Scrollbar(滚动条插件) | Formula(数学公式插件) | Cooperate(协同编辑插件) | RainbowLines(彩虹线条插件) |
|
||||||
|
| Demonstrate(演示模式插件) | OuterFrame(外框插件) | MindMapLayoutPro(思维导图布局插件) | |
|
||||||
|
|
||||||
|
|
||||||
|
本项目不会实现的特性:
|
||||||
|
|
||||||
|
> 1.自由节点,即多个根节点;
|
||||||
|
>
|
||||||
|
> 2.概要节点后面继续添加节点;
|
||||||
|
>
|
||||||
|
> 如果你需要以上特性,那么本库可能无法满足你的需求。
|
||||||
|
|
||||||
|
# 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i simple-mind-map
|
||||||
|
```
|
||||||
|
|
||||||
|
# 使用
|
||||||
|
|
||||||
|
提供一个宽高不为 0 的容器元素:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="mindMapContainer"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
另外再设置一下`css`样式:
|
||||||
|
|
||||||
|
```css
|
||||||
|
#mindMapContainer * {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后创建一个实例:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import MindMap from "simple-mind-map";
|
||||||
|
|
||||||
|
const mindMap = new MindMap({
|
||||||
|
el: document.getElementById("mindMapContainer"),
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
text: "根节点",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
即可得到一个思维导图。想要实现更多功能?可以查看[开发文档](https://wanglin2.github.io/mind-map-docs/)。
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
[MIT](./LICENSE)。保留`simple-mind-map`版权声明和注明来源的情况下可随意商用,如有疑问或不想保留可联系作者(微信:wanglinguanfang)通过付费的方式去除。
|
||||||
|
|
||||||
|
> 示例:可以在你应用中的关于页面、帮助页面、文档页面、开源声明等任何页面添加以下内容:
|
||||||
|
>
|
||||||
|
> 本产品思维导图基于SimpleMindMap项目开发,版权归源项目所有,[开源协议](https://github.com/wanglin2/mind-map/blob/main/LICENSE)。
|
||||||
|
|
||||||
|
# 开发帮助/技术支持/咨询等
|
||||||
|
|
||||||
|
因精力有限,及重心转变,暂不提供任何开发支持(包括有偿),请见谅!
|
||||||
|
|
||||||
|
# star
|
||||||
|
|
||||||
|
如果喜欢本项目,欢迎点个 star,这对我们很重要。
|
||||||
|
|
||||||
|
[](https://star-history.com/#wanglin2/mind-map&Date)
|
||||||
|
|
||||||
|
# 关于定制
|
||||||
|
|
||||||
|
如果你有个性化的商用定制需求,可以联系我们,我们提供付费开发服务,无论前端、后端、还是部署,都可以帮你一站式搞定。
|
||||||
|
|
||||||
|
# 谁在使用
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="http://drawon.cn/">
|
||||||
|
<img src="./web/src/assets/avatar/桌案.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>drawon.cn(桌案)</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
# 感谢赞赏过本项目的人
|
||||||
|
|
||||||
|
## 最强王者
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/hi.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>hi</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## 钻石赞助
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/黄智彪@一米一栗科技.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>黄智彪@一米一栗科技</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/沨沄.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>沨沄</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/行.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>行</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## 黄金赞助
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/小土渣的宇宙.jpeg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>小土渣的宇宙</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/Chris.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Chris</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/仓鼠.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>仓鼠</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/风格.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>风格</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>LiuJL</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/Kyle.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Kyle</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/秀树因馨雨.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>秀树因馨雨</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>黄泳</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/ccccs.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>ccccs</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/炫.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>炫</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>晏江</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/梁辉.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>梁辉</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/千帆.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>千帆</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/布林.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>布林</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/达仁科技.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>达仁科技</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/沐风牧草.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>沐风牧草</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/俊奇.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>俊奇</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/庆国.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>庆国</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Matt</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/雨馨.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>雨馨</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/峰.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>峰</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/御风.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>御风</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/兔子快跑.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>兔子快跑</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>LSHM</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>newplayer</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## 青铜赞助
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/Think.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Think</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/志斌.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>志斌</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/qp.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>qp</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/ZXR.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>ZXR</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/花儿朵朵.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>花儿朵朵</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/suka.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>suka</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/水车.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>水车</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/才镇.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>才镇</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/小米.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>小米bbᯤ²ᴳ</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/棐.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>*棐</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/南风.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>南风</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/蜉蝣撼大叔.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>蜉蝣撼大叔</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/乙.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>乙</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/敏.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>敏</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/有希.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>有希</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/樊笼.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>樊笼</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/小逗比.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>小逗比</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/天清如愿.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>天清如愿</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/敬明朗.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>敬明朗</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>飞箭</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/戚永峰.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>戚永峰</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/moom.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>moom</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/张扬.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>张扬</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/长沙利奥软件.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>长沙利奥软件</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/HaHN.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>HaHN</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/继龙.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>继龙</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/欣.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>欣</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>易空小易</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/国发.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>国发</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>建明</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/汪津合.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>汪津合</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>博文</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/慕智打印-兰兰.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>慕智打印-兰兰</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>锦冰</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/旭东.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>旭东</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/橘半.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>橘半</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/pluvet.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>pluvet</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/皇登攀.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>皇登攀</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>SR</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/逆水行舟.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>逆水行舟</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/L.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>L</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>sunniberg</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/在下青铜五.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>sunniberg</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/在下青铜五.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>在下青铜五</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/木星二号.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>木星二号</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/阿晨.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>阿晨</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>铁</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/Alex.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Alex</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/子豪.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>子豪</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/宏涛.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>宏涛</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/最多5个字.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>最多5个字</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/ZX.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>ZX</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>协成</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/木木.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>木木</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/好名字.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>好名字</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/lsytyrt.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>lsytyrt</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/buddy.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>buddy</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>小川</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/Tobin.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Tobin</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/夏虫不语冰.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>夏虫不语冰</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/晴空.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>晴空</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/。.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>。</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/Jeffrey.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Jeffrey</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/张文建.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>张文建</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/Lawliet.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Lawliet</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/一叶孤舟.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>一叶孤舟</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Eric</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/Joe.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Joe</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>中文网字计划-江夏尧</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/海云.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>海云</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/皮老板.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>皮老板</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/h.r.w.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>h.r.w</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/时光匆匆.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>时光匆匆</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/广兴.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>广兴</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/一亩三.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>一亩三</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/xbkkjbs0246658.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>xbkkjbs0246658</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/4399行星元帅.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>4399行星元帅</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/Xavier.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Xavier</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/冒号括号.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>:)</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/可米阳光.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>可米阳光</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/MrFujing.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>MrFujing</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/Sword.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Sword</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/好好先生Ervin.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>好好先生Ervin</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/胡永刚.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>胡永刚</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/旋风.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>旋风</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/星夜寒.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>星夜寒</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/神话.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>神话</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Towards the future</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
|
||||||
|
<a href="#">
|
||||||
|
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>安嘉</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
BIN
assets/client/client1.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
assets/client/client2.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/client/client3.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
assets/client/client4.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
assets/client/client5.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
assets/client/client6.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
assets/client/clienten1.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
assets/client/clienten2.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
assets/client/clienten3.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/client/clienten4.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
assets/client/clienten5.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
assets/client/clienten6.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
assets/ob/ob1.png
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
assets/ob/ob2.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
assets/ob/ob3.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
assets/ob/ob4.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
assets/ob/ob5.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
assets/ob/oben1.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
assets/ob/oben2.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
assets/ob/oben3.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
assets/ob/oben4.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
assets/ob/oben5.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
2
copy.js
@ -13,4 +13,4 @@ if (fs.existsSync(src)) {
|
|||||||
fs.unlinkSync(src)
|
fs.unlinkSync(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn('请检查付费插件是否启用!!!')
|
// console.warn('请检查付费插件是否启用!!!')
|
||||||
2
dist/css/app.css
vendored
@ -1 +1 @@
|
|||||||
*{margin:0;padding:0;box-sizing:border-box}#app{font-family:Avenir,Helvetica,Arial,sans-serif;color:#2c3e50}@font-face{font-family:iconfont;src:url(../fonts/iconfont.woff2) format("woff2"),url(../fonts/iconfont.woff) format("woff"),url(../fonts/iconfont.ttf) format("truetype")}#app,.iconfont{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal}.iconfile-excel:before{content:"\e7b7"}.iconfreemind:before{content:"\e97d"}.iconwaikuang:before{content:"\e640"}.iconhighlight:before{content:"\e6b8"}.iconyanshibofang:before{content:"\e648"}.iconfujian:before{content:"\e88a"}.icongeshihua:before{content:"\e7a3"}.iconyuanma:before{content:"\e658"}.icongundongtiao:before{content:"\e670"}.iconxietongwendang:before{content:"\e60d"}.iconTXT:before{content:"\e6e1"}.iconwenjian1:before{content:"\e69f"}.icondodeparent:before{content:"\e70f"}.icongongshi:before{content:"\e617"}.icontouming:before{content:"\e60c"}.iconlieri:before{content:"\e60b"}.iconmoon_line:before{content:"\e745"}.iconsousuo:before{content:"\e693"}.iconjiantouyou:before{content:"\e62d"}.iconbianji1:before{content:"\e60a"}.icondaohang1:before{content:"\e632"}.iconyanjing:before{content:"\e8bf"}.iconwangzhan:before{content:"\e628"}.iconcsdn:before{content:"\e608"}.iconshejiaotubiao-10:before{content:"\e644"}.iconstar:before{content:"\e7df"}.iconfork:before{content:"\e641"}.iconxiazai:before{content:"\e613"}.iconteamwork:before{content:"\e870"}.iconshuiyin:before{content:"\e67a"}.iconxmind:before{content:"\ea57"}.iconmouseR:before{content:"\e6bd"}.iconmouseL:before{content:"\e6c0"}.iconwenjian:before{content:"\e607"}.iconpdf:before{content:"\e740"}.iconPNG:before{content:"\ec18"}.iconSVG:before{content:"\e621"}.iconmarkdown:before{content:"\ec04"}.iconjson:before{content:"\ea42"}.iconlianjiexian:before{content:"\e75b"}.iconbangzhu:before{content:"\e620"}.iconshezhi:before{content:"\e8b7"}.iconwushuju:before{content:"\e643"}.iconzuijinliulan:before{content:"\e62f"}.icon3zuidahua-3:before{content:"\e692"}.iconzuixiaohua:before{content:"\e650"}.iconzuidahua:before{content:"\e651"}.iconguanbi:before{content:"\e652"}.icondiannao:before{content:"\eac0"}.iconzhuye:before{content:"\e65c"}.iconbendi1x:before{content:"\e606"}.iconbeijingyanse:before{content:"\e6f8"}.iconqingchu:before{content:"\e605"}.iconcase:before{content:"\e6c6"}.iconxingzhuang-wenzi:before{content:"\eb99"}.iconzitijiacu:before{content:"\ec83"}.iconzitixiahuaxian:before{content:"\ec85"}.iconzitixieti:before{content:"\ec86"}.iconshanchuxian:before{content:"\e612"}.iconzitiyanse:before{content:"\e854"}.icongithub:before{content:"\e64f"}.iconchoose1:before{content:"\e6c5"}.iconzhuti:before{content:"\e7aa"}.icondaochu1:before{content:"\e63e"}.iconlingcunwei:before{content:"\e657"}.iconexport:before{content:"\e642"}.icondakai:before{content:"\ebdf"}.iconxinjian:before{content:"\e64e"}.iconjianqie:before{content:"\e601"}.iconzhengli:before{content:"\e83b"}.iconfuzhi:before{content:"\e604"}.iconniantie:before{content:"\e63f"}.iconshangyi:before{content:"\e6be"}.iconxiayi:before{content:"\e6bf"}.icongaikuozonglan:before{content:"\e609"}.iconquanxuan:before{content:"\f199"}.icondaoru:before{content:"\e6a3"}.iconhoutui-shi:before{content:"\e656"}.iconqianjin1:before{content:"\e654"}.iconwithdraw:before{content:"\e603"}.iconqianjin:before{content:"\e600"}.iconhuifumoren:before{content:"\e60e"}.iconhuanhang:before{content:"\e61e"}.iconsuoxiao:before{content:"\ec13"}.iconbianji:before{content:"\e626"}.iconfangda:before{content:"\e663"}.iconquanping1:before{content:"\e664"}.icondingwei:before{content:"\e616"}.icondaohang:before{content:"\e611"}.iconjianpan:before{content:"\e64d"}.iconquanping:before{content:"\e602"}.icondaochu:before{content:"\e63d"}.iconbiaoqian:before{content:"\e63c"}.iconflow-Mark:before{content:"\e65b"}.iconchaolianjie:before{content:"\e6f4"}.iconjingzi:before{content:"\e610"}.iconxiaolian:before{content:"\e60f"}.iconimage:before{content:"\e629"}.iconjiegou:before{content:"\e61d"}.iconyangshi:before{content:"\e631"}.iconfuhao-dagangshu:before{content:"\e71f"}.icontianjiazijiedian:before{content:"\e622"}.iconjiedian:before{content:"\e655"}.iconshanchu:before{content:"\e696"}.iconzhankai:before{content:"\e64c"}.iconzhankai1:before{content:"\e673"}
|
*{margin:0;padding:0;box-sizing:border-box}#app{font-family:Avenir,Helvetica,Arial,sans-serif;color:#2c3e50}.customScrollbar::-webkit-scrollbar{width:7px;height:7px}.customScrollbar::-webkit-scrollbar-thumb{border-radius:7px;background-color:rgba(0,0,0,.3);cursor:pointer}.customScrollbar::-webkit-scrollbar-track{box-shadow:none;background:transparent;display:none}.el-dialog{border-radius:10px}@font-face{font-family:iconfont;src:url(../fonts/iconfont.woff2) format("woff2"),url(../fonts/iconfont.woff) format("woff"),url(../fonts/iconfont.ttf) format("truetype")}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.iconAIshengcheng:before{content:"\e6b5"}.iconprinting:before{content:"\ea28"}.iconwenjianjia:before{content:"\e614"}.iconcontentleft:before{content:"\e8c9"}.iconjuzhongduiqi:before{content:"\ec80"}.iconfile-excel:before{content:"\e7b7"}.iconfreemind:before{content:"\e97d"}.iconwaikuang:before{content:"\e640"}.iconhighlight:before{content:"\e6b8"}.iconyanshibofang:before{content:"\e648"}.iconfujian:before{content:"\e88a"}.icongeshihua:before{content:"\e7a3"}.iconyuanma:before{content:"\e658"}.icongundongtiao:before{content:"\e670"}.iconxietongwendang:before{content:"\e60d"}.iconTXT:before{content:"\e6e1"}.iconwenjian1:before{content:"\e69f"}.icondodeparent:before{content:"\e70f"}.icongongshi:before{content:"\e617"}.icontouming:before{content:"\e60c"}.iconlieri:before{content:"\e60b"}.iconmoon_line:before{content:"\e745"}.iconsousuo:before{content:"\e693"}.iconjiantouyou:before{content:"\e62d"}.iconbianji1:before{content:"\e60a"}.icondaohang1:before{content:"\e632"}.iconyanjing:before{content:"\e8bf"}.iconwangzhan:before{content:"\e628"}.iconcsdn:before{content:"\e608"}.iconshejiaotubiao-10:before{content:"\e644"}.iconstar:before{content:"\e7df"}.iconfork:before{content:"\e641"}.iconxiazai:before{content:"\e613"}.iconteamwork:before{content:"\e870"}.iconshuiyin:before{content:"\e67a"}.iconxmind:before{content:"\ea57"}.iconmouseR:before{content:"\e6bd"}.iconmouseL:before{content:"\e6c0"}.iconwenjian:before{content:"\e607"}.iconpdf:before{content:"\e740"}.iconPNG:before{content:"\ec18"}.iconSVG:before{content:"\e621"}.iconmarkdown:before{content:"\ec04"}.iconjson:before{content:"\ea42"}.iconlianjiexian:before{content:"\e75b"}.iconbangzhu:before{content:"\e620"}.iconshezhi:before{content:"\e8b7"}.iconwushuju:before{content:"\e643"}.iconzuijinliulan:before{content:"\e62f"}.icon3zuidahua-3:before{content:"\e692"}.iconzuixiaohua:before{content:"\e650"}.iconzuidahua:before{content:"\e651"}.iconguanbi:before{content:"\e652"}.icondiannao:before{content:"\eac0"}.iconzhuye:before{content:"\e65c"}.iconbendi1x:before{content:"\e606"}.iconbeijingyanse:before{content:"\e6f8"}.iconqingchu:before{content:"\e605"}.iconcase:before{content:"\e6c6"}.iconxingzhuang-wenzi:before{content:"\eb99"}.iconzitijiacu:before{content:"\ec83"}.iconzitixiahuaxian:before{content:"\ec85"}.iconzitixieti:before{content:"\ec86"}.iconshanchuxian:before{content:"\e612"}.iconzitiyanse:before{content:"\e854"}.icongithub:before{content:"\e64f"}.iconchoose1:before{content:"\e6c5"}.iconzhuti:before{content:"\e7aa"}.icondaochu1:before{content:"\e63e"}.iconlingcunwei:before{content:"\e657"}.iconexport:before{content:"\e642"}.icondakai:before{content:"\ebdf"}.iconxinjian:before{content:"\e64e"}.iconjianqie:before{content:"\e601"}.iconzhengli:before{content:"\e83b"}.iconfuzhi:before{content:"\e604"}.iconniantie:before{content:"\e63f"}.iconshangyi:before{content:"\e6be"}.iconxiayi:before{content:"\e6bf"}.icongaikuozonglan:before{content:"\e609"}.iconquanxuan:before{content:"\f199"}.icondaoru:before{content:"\e6a3"}.iconhoutui-shi:before{content:"\e656"}.iconqianjin1:before{content:"\e654"}.iconwithdraw:before{content:"\e603"}.iconqianjin:before{content:"\e600"}.iconhuifumoren:before{content:"\e60e"}.iconhuanhang:before{content:"\e61e"}.iconsuoxiao:before{content:"\ec13"}.iconbianji:before{content:"\e626"}.iconfangda:before{content:"\e663"}.iconquanping1:before{content:"\e664"}.icondingwei:before{content:"\e616"}.icondaohang:before{content:"\e611"}.iconjianpan:before{content:"\e64d"}.iconquanping:before{content:"\e602"}.icondaochu:before{content:"\e63d"}.iconbiaoqian:before{content:"\e63c"}.iconflow-Mark:before{content:"\e65b"}.iconchaolianjie:before{content:"\e6f4"}.iconjingzi:before{content:"\e610"}.iconxiaolian:before{content:"\e60f"}.iconimage:before{content:"\e629"}.iconjiegou:before{content:"\e61d"}.iconyangshi:before{content:"\e631"}.iconfuhao-dagangshu:before{content:"\e71f"}.icontianjiazijiedian:before{content:"\e622"}.iconjiedian:before{content:"\e655"}.iconshanchu:before{content:"\e696"}.iconzhankai:before{content:"\e64c"}.iconzhankai1:before{content:"\e673"}
|
||||||
BIN
dist/fonts/iconfont.ttf
vendored
BIN
dist/fonts/iconfont.woff
vendored
BIN
dist/fonts/iconfont.woff2
vendored
BIN
dist/img/catalogOrganization.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
dist/img/catalogOrganization.png
vendored
|
Before Width: | Height: | Size: 6.6 KiB |
BIN
dist/img/fishbone.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
dist/img/fishbone.png
vendored
|
Before Width: | Height: | Size: 7.1 KiB |
BIN
dist/img/fishbone2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
dist/img/logicalStructure.jpg
vendored
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
dist/img/logicalStructure.png
vendored
|
Before Width: | Height: | Size: 5.9 KiB |
BIN
dist/img/logicalStructureLeft.jpg
vendored
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
dist/img/mindMap.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
dist/img/mindMap.png
vendored
|
Before Width: | Height: | Size: 7.0 KiB |
BIN
dist/img/organizationStructure.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
dist/img/organizationStructure.png
vendored
|
Before Width: | Height: | Size: 7.1 KiB |
BIN
dist/img/rightFishbone.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
dist/img/rightFishbone2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
dist/img/timeline.jpg
vendored
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
dist/img/timeline.png
vendored
|
Before Width: | Height: | Size: 6.2 KiB |
BIN
dist/img/timeline2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
dist/img/timeline2.png
vendored
|
Before Width: | Height: | Size: 6.6 KiB |
BIN
dist/img/verticalTimeline.jpg
vendored
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
dist/img/verticalTimeline.png
vendored
|
Before Width: | Height: | Size: 7.2 KiB |
BIN
dist/img/verticalTimeline2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
dist/img/verticalTimeline3.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
2
dist/js/app.js
vendored
65
dist/js/chunk-183b683c.js
vendored
Normal file
69
dist/js/chunk-9a7bee60.js
vendored
14
dist/js/chunk-vendors.js
vendored
13
index.html
@ -9,7 +9,7 @@
|
|||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}</script><link href="dist/css/chunk-vendors.css?f5839763ea0d5c47d80a" rel="stylesheet"><link href="dist/css/app.css?f5839763ea0d5c47d80a" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
|
}</script><link href="dist/css/chunk-vendors.css?227f61428db154a5d9bc" rel="stylesheet"><link href="dist/css/app.css?227f61428db154a5d9bc" 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve({
|
resolve({
|
||||||
@ -28,6 +28,7 @@
|
|||||||
config: {},
|
config: {},
|
||||||
view: null
|
view: null
|
||||||
},
|
},
|
||||||
|
mindMapConfig: {},
|
||||||
lang: 'zh',
|
lang: 'zh',
|
||||||
localConfig: null
|
localConfig: null
|
||||||
})
|
})
|
||||||
@ -44,6 +45,14 @@
|
|||||||
window.takeOverAppMethods.saveMindMapData = data => {
|
window.takeOverAppMethods.saveMindMapData = data => {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
}
|
}
|
||||||
|
// 获取思维导图配置,也就是实例化时会传入的选项
|
||||||
|
window.takeOverAppMethods.getMindMapConfig = () => {
|
||||||
|
return data.mindMapConfig
|
||||||
|
}
|
||||||
|
// 保存思维导图配置
|
||||||
|
window.takeOverAppMethods.saveMindMapConfig = config => {
|
||||||
|
console.log(config)
|
||||||
|
}
|
||||||
// 获取语言的函数
|
// 获取语言的函数
|
||||||
window.takeOverAppMethods.getLanguage = () => {
|
window.takeOverAppMethods.getLanguage = () => {
|
||||||
return data.lang
|
return data.lang
|
||||||
@ -74,4 +83,4 @@
|
|||||||
// 可以通过window.$bus.$on()来监听应用的一些事件
|
// 可以通过window.$bus.$on()来监听应用的一些事件
|
||||||
// 实例化页面
|
// 实例化页面
|
||||||
window.initApp()
|
window.initApp()
|
||||||
}</script><script src="dist/js/chunk-vendors.js?f5839763ea0d5c47d80a"></script><script src="dist/js/app.js?f5839763ea0d5c47d80a"></script></body></html>
|
}</script><script src="dist/js/chunk-vendors.js?227f61428db154a5d9bc"></script><script src="dist/js/app.js?227f61428db154a5d9bc"></script></body></html>
|
||||||
BIN
qrcode.jpg
|
Before Width: | Height: | Size: 41 KiB |
@ -17,11 +17,7 @@ const createFullData = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 节点较多示例数据
|
||||||
* @Author: 王林
|
|
||||||
* @Date: 2021-04-15 22:23:24
|
|
||||||
* @Desc: 节点较多示例数据
|
|
||||||
*/
|
|
||||||
const data1 = {
|
const data1 = {
|
||||||
"root": {
|
"root": {
|
||||||
"data": {
|
"data": {
|
||||||
@ -936,6 +932,5 @@ export default {
|
|||||||
"layout": "logicalStructure",
|
"layout": "logicalStructure",
|
||||||
// "layout": "mindMap",
|
// "layout": "mindMap",
|
||||||
// "layout": "catalogOrganization"
|
// "layout": "catalogOrganization"
|
||||||
// "layout": "organizationStructure",
|
// "layout": "organizationStructure"
|
||||||
"config": {}
|
|
||||||
}
|
}
|
||||||
@ -19,6 +19,7 @@ import RainbowLines from './src/plugins/RainbowLines.js'
|
|||||||
import Demonstrate from './src/plugins/Demonstrate.js'
|
import Demonstrate from './src/plugins/Demonstrate.js'
|
||||||
import OuterFrame from './src/plugins/OuterFrame.js'
|
import OuterFrame from './src/plugins/OuterFrame.js'
|
||||||
import MindMapLayoutPro from './src/plugins/MindMapLayoutPro.js'
|
import MindMapLayoutPro from './src/plugins/MindMapLayoutPro.js'
|
||||||
|
import NodeBase64ImageStorage from './src/plugins/NodeBase64ImageStorage.js'
|
||||||
import xmind from './src/parse/xmind.js'
|
import xmind from './src/parse/xmind.js'
|
||||||
import markdown from './src/parse/markdown.js'
|
import markdown from './src/parse/markdown.js'
|
||||||
import icons from './src/svg/icons.js'
|
import icons from './src/svg/icons.js'
|
||||||
@ -30,7 +31,7 @@ MindMap.markdown = markdown
|
|||||||
MindMap.iconList = icons.nodeIconList
|
MindMap.iconList = icons.nodeIconList
|
||||||
MindMap.constants = constants
|
MindMap.constants = constants
|
||||||
MindMap.defaultTheme = defaultTheme
|
MindMap.defaultTheme = defaultTheme
|
||||||
MindMap.version = '0.12.2'
|
MindMap.version = '0.14.0-fix.1'
|
||||||
|
|
||||||
MindMap.usePlugin(MiniMap)
|
MindMap.usePlugin(MiniMap)
|
||||||
.usePlugin(Watermark)
|
.usePlugin(Watermark)
|
||||||
@ -52,5 +53,6 @@ MindMap.usePlugin(MiniMap)
|
|||||||
.usePlugin(Demonstrate)
|
.usePlugin(Demonstrate)
|
||||||
.usePlugin(OuterFrame)
|
.usePlugin(OuterFrame)
|
||||||
.usePlugin(MindMapLayoutPro)
|
.usePlugin(MindMapLayoutPro)
|
||||||
|
.usePlugin(NodeBase64ImageStorage)
|
||||||
|
|
||||||
export default MindMap
|
export default MindMap
|
||||||
|
|||||||
@ -11,16 +11,18 @@ import {
|
|||||||
layoutValueList,
|
layoutValueList,
|
||||||
CONSTANTS,
|
CONSTANTS,
|
||||||
ERROR_TYPES,
|
ERROR_TYPES,
|
||||||
cssContent
|
cssContent,
|
||||||
|
nodeDataNoStylePropList
|
||||||
} from './src/constants/constant'
|
} from './src/constants/constant'
|
||||||
import { SVG } from '@svgdotjs/svg.js'
|
import { SVG, G, Rect } from '@svgdotjs/svg.js'
|
||||||
import {
|
import {
|
||||||
simpleDeepClone,
|
simpleDeepClone,
|
||||||
getObjectChangedProps,
|
getObjectChangedProps,
|
||||||
isUndef,
|
isUndef,
|
||||||
handleGetSvgDataExtraContent,
|
handleGetSvgDataExtraContent,
|
||||||
getNodeTreeBoundingRect,
|
getNodeTreeBoundingRect,
|
||||||
mergeTheme
|
mergeTheme,
|
||||||
|
createUidForAppointNodes
|
||||||
} from './src/utils'
|
} from './src/utils'
|
||||||
import defaultTheme, {
|
import defaultTheme, {
|
||||||
checkIsNodeSizeIndependenceConfig
|
checkIsNodeSizeIndependenceConfig
|
||||||
@ -56,7 +58,7 @@ class MindMap {
|
|||||||
this.cssEl = null
|
this.cssEl = null
|
||||||
this.cssTextMap = {} // 该样式在实例化时会动态添加到页面,同时导出为svg时也会添加到svg源码中
|
this.cssTextMap = {} // 该样式在实例化时会动态添加到页面,同时导出为svg时也会添加到svg源码中
|
||||||
|
|
||||||
// 节点前置内容列表
|
// 节点前置/后置内容列表
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
name: '',// 一个唯一的类型标识
|
name: '',// 一个唯一的类型标识
|
||||||
@ -75,6 +77,27 @@ class MindMap {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
this.nodeInnerPrefixList = []
|
this.nodeInnerPrefixList = []
|
||||||
|
this.nodeInnerPostfixList = []
|
||||||
|
|
||||||
|
// 编辑节点的类名列表,快捷键响应会检查事件目标是否是body或该列表中的元素,是的话才会响应
|
||||||
|
// 该检查可以通过customCheckEnableShortcut选项来覆盖
|
||||||
|
this.editNodeClassList = []
|
||||||
|
|
||||||
|
// 扩展的节点形状列表
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
createShape: (node) => {
|
||||||
|
return path
|
||||||
|
},
|
||||||
|
getPadding: ({ node, width, height, paddingX, paddingY }) => {
|
||||||
|
return {
|
||||||
|
paddingX: 0,
|
||||||
|
paddingY: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
this.extendShapeList = []
|
||||||
|
|
||||||
// 画布
|
// 画布
|
||||||
this.initContainer()
|
this.initContainer()
|
||||||
@ -85,6 +108,15 @@ class MindMap {
|
|||||||
// 初始化缓存数据
|
// 初始化缓存数据
|
||||||
this.initCache()
|
this.initCache()
|
||||||
|
|
||||||
|
// 注册插件
|
||||||
|
MindMap.pluginList
|
||||||
|
.filter(plugin => {
|
||||||
|
return plugin.preload
|
||||||
|
})
|
||||||
|
.forEach(plugin => {
|
||||||
|
this.initPlugin(plugin)
|
||||||
|
})
|
||||||
|
|
||||||
// 事件类
|
// 事件类
|
||||||
this.event = new Event({
|
this.event = new Event({
|
||||||
mindMap: this
|
mindMap: this
|
||||||
@ -114,7 +146,11 @@ class MindMap {
|
|||||||
this.batchExecution = new BatchExecution()
|
this.batchExecution = new BatchExecution()
|
||||||
|
|
||||||
// 注册插件
|
// 注册插件
|
||||||
MindMap.pluginList.forEach(plugin => {
|
MindMap.pluginList
|
||||||
|
.filter(plugin => {
|
||||||
|
return !plugin.preload
|
||||||
|
})
|
||||||
|
.forEach(plugin => {
|
||||||
this.initPlugin(plugin)
|
this.initPlugin(plugin)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -149,6 +185,8 @@ class MindMap {
|
|||||||
if (data.data && !data.data.expand) {
|
if (data.data && !data.data.expand) {
|
||||||
data.data.expand = true
|
data.data.expand = true
|
||||||
}
|
}
|
||||||
|
// 给没有uid的节点添加uid
|
||||||
|
createUidForAppointNodes([data], false, null, true)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,12 +275,33 @@ class MindMap {
|
|||||||
if (this.cssEl) document.head.removeChild(this.cssEl)
|
if (this.cssEl) document.head.removeChild(this.cssEl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查某个编辑节点类名是否存在,返回索引
|
||||||
|
checkEditNodeClassIndex(className) {
|
||||||
|
return this.editNodeClassList.findIndex(item => {
|
||||||
|
return item === className
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加一个编辑节点类名
|
||||||
|
addEditNodeClass(className) {
|
||||||
|
const index = this.checkEditNodeClassIndex(className)
|
||||||
|
if (index === -1) {
|
||||||
|
this.editNodeClassList.push(className)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除一个编辑节点类名
|
||||||
|
deleteEditNodeClass(className) {
|
||||||
|
const index = this.checkEditNodeClassIndex(className)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.editNodeClassList.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染,部分渲染
|
// 渲染,部分渲染
|
||||||
render(callback, source = '') {
|
render(callback, source = '') {
|
||||||
this.batchExecution.push('render', () => {
|
|
||||||
this.initTheme()
|
this.initTheme()
|
||||||
this.renderer.render(callback, source)
|
this.renderer.render(callback, source)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新渲染
|
// 重新渲染
|
||||||
@ -395,6 +454,7 @@ class MindMap {
|
|||||||
|
|
||||||
// 更新画布数据,如果新的数据是在当前画布节点数据基础上增删改查后形成的,那么可以使用该方法来更新画布数据
|
// 更新画布数据,如果新的数据是在当前画布节点数据基础上增删改查后形成的,那么可以使用该方法来更新画布数据
|
||||||
updateData(data) {
|
updateData(data) {
|
||||||
|
data = this.handleData(data)
|
||||||
this.emit('before_update_data', data)
|
this.emit('before_update_data', data)
|
||||||
this.renderer.setData(data)
|
this.renderer.setData(data)
|
||||||
this.render()
|
this.render()
|
||||||
@ -411,7 +471,7 @@ class MindMap {
|
|||||||
this.command.clearHistory()
|
this.command.clearHistory()
|
||||||
this.command.addHistory()
|
this.command.addHistory()
|
||||||
this.renderer.setData(data)
|
this.renderer.setData(data)
|
||||||
this.reRender(() => {}, CONSTANTS.SET_DATA)
|
this.reRender()
|
||||||
this.emit('set_data', data)
|
this.emit('set_data', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -632,6 +692,35 @@ class MindMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 扩展节点形状
|
||||||
|
addShape(shape) {
|
||||||
|
if (!shape) return
|
||||||
|
const exist = this.extendShapeList.find(item => {
|
||||||
|
return item.name === shape.name
|
||||||
|
})
|
||||||
|
if (exist) return
|
||||||
|
this.extendShapeList.push(shape)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除扩展的形状
|
||||||
|
removeShape(name) {
|
||||||
|
const index = this.extendShapeList.findIndex(item => {
|
||||||
|
return item.name === name
|
||||||
|
})
|
||||||
|
if (index !== -1) {
|
||||||
|
this.extendShapeList.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取SVG.js库的一些对象
|
||||||
|
getSvgObjects() {
|
||||||
|
return {
|
||||||
|
SVG,
|
||||||
|
G,
|
||||||
|
Rect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 添加插件
|
// 添加插件
|
||||||
addPlugin(plugin, opt) {
|
addPlugin(plugin, opt) {
|
||||||
let index = MindMap.hasPlugin(plugin)
|
let index = MindMap.hasPlugin(plugin)
|
||||||
@ -695,6 +784,39 @@ class MindMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 扩展节点数据中非样式的字段列表
|
||||||
|
// 内部会根据这个列表判断,如果不在这个列表里的字段都会认为是样式字段
|
||||||
|
/*
|
||||||
|
比如一个节点的数据为:
|
||||||
|
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
text: '',
|
||||||
|
note: '',
|
||||||
|
color: ''
|
||||||
|
},
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
|
||||||
|
color字段不在nodeDataNoStylePropList列表中,所以是样式,内部一些操作的方法会用到,所以如果你新增了自定义的节点数据,并且不是`_`开头的,那么需要通过该方法扩展
|
||||||
|
*/
|
||||||
|
let _extendNodeDataNoStylePropList = []
|
||||||
|
MindMap.extendNodeDataNoStylePropList = (list = []) => {
|
||||||
|
_extendNodeDataNoStylePropList.push(...list)
|
||||||
|
nodeDataNoStylePropList.push(...list)
|
||||||
|
}
|
||||||
|
MindMap.resetNodeDataNoStylePropList = () => {
|
||||||
|
_extendNodeDataNoStylePropList.forEach(item => {
|
||||||
|
const index = nodeDataNoStylePropList.findIndex(item2 => {
|
||||||
|
return item2 === item
|
||||||
|
})
|
||||||
|
if (index !== -1) {
|
||||||
|
nodeDataNoStylePropList.splice(index, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
_extendNodeDataNoStylePropList = []
|
||||||
|
}
|
||||||
|
|
||||||
// 插件列表
|
// 插件列表
|
||||||
MindMap.pluginList = []
|
MindMap.pluginList = []
|
||||||
MindMap.usePlugin = (plugin, opt = {}) => {
|
MindMap.usePlugin = (plugin, opt = {}) => {
|
||||||
|
|||||||
4
simple-mind-map/package-lock.json
generated
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "simple-mind-map",
|
"name": "simple-mind-map",
|
||||||
"version": "0.12.1",
|
"version": "0.14.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"version": "0.12.1",
|
"version": "0.14.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@svgdotjs/svg.js": "3.2.0",
|
"@svgdotjs/svg.js": "3.2.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "simple-mind-map",
|
"name": "simple-mind-map",
|
||||||
"version": "0.12.2",
|
"version": "0.14.0-fix.1",
|
||||||
"description": "一个简单的web在线思维导图",
|
"description": "一个简单的web在线思维导图",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
export const CONSTANTS = {
|
export const CONSTANTS = {
|
||||||
CHANGE_THEME: 'changeTheme',
|
CHANGE_THEME: 'changeTheme',
|
||||||
CHANGE_LAYOUT: 'changeLayout',
|
CHANGE_LAYOUT: 'changeLayout',
|
||||||
SET_DATA: 'setData',
|
|
||||||
TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode',
|
|
||||||
MODE: {
|
MODE: {
|
||||||
READONLY: 'readonly',
|
READONLY: 'readonly',
|
||||||
EDIT: 'edit'
|
EDIT: 'edit'
|
||||||
@ -17,7 +15,12 @@ export const CONSTANTS = {
|
|||||||
TIMELINE: 'timeline',
|
TIMELINE: 'timeline',
|
||||||
TIMELINE2: 'timeline2',
|
TIMELINE2: 'timeline2',
|
||||||
FISHBONE: 'fishbone',
|
FISHBONE: 'fishbone',
|
||||||
VERTICAL_TIMELINE: 'verticalTimeline'
|
FISHBONE2: 'fishbone2',
|
||||||
|
RIGHT_FISHBONE: 'rightFishbone',
|
||||||
|
RIGHT_FISHBONE2: 'rightFishbone2',
|
||||||
|
VERTICAL_TIMELINE: 'verticalTimeline',
|
||||||
|
VERTICAL_TIMELINE2: 'verticalTimeline2',
|
||||||
|
VERTICAL_TIMELINE3: 'verticalTimeline3'
|
||||||
},
|
},
|
||||||
DIR: {
|
DIR: {
|
||||||
UP: 'up',
|
UP: 'up',
|
||||||
@ -72,14 +75,15 @@ export const CONSTANTS = {
|
|||||||
NOT_ACTIVE: 'notActive',
|
NOT_ACTIVE: 'notActive',
|
||||||
ACTIVE_ONLY: 'activeOnly'
|
ACTIVE_ONLY: 'activeOnly'
|
||||||
},
|
},
|
||||||
TAG_POSITION: {
|
TAG_PLACEMENT: {
|
||||||
RIGHT: 'right',
|
RIGHT: 'right',
|
||||||
BOTTOM: 'bottom'
|
BOTTOM: 'bottom'
|
||||||
},
|
},
|
||||||
EDIT_NODE_CLASS: {
|
IMG_PLACEMENT: {
|
||||||
SMM_NODE_EDIT_WRAP: 'smm-node-edit-wrap',
|
LEFT: 'left',
|
||||||
RICH_TEXT_EDIT_WRAP: 'ql-editor',
|
TOP: 'top',
|
||||||
ASSOCIATIVE_LINE_TEXT_EDIT_WRAP: 'associative-line-text-edit-warp'
|
RIGHT: 'right',
|
||||||
|
BOTTOM: 'bottom'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,9 +129,29 @@ export const layoutList = [
|
|||||||
name: '竖向时间轴',
|
name: '竖向时间轴',
|
||||||
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE
|
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '竖向时间轴2',
|
||||||
|
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '竖向时间轴3',
|
||||||
|
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE3
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '鱼骨图',
|
name: '鱼骨图',
|
||||||
value: CONSTANTS.LAYOUT.FISHBONE
|
value: CONSTANTS.LAYOUT.FISHBONE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '鱼骨图2',
|
||||||
|
value: CONSTANTS.LAYOUT.FISHBONE2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '向右鱼骨图',
|
||||||
|
value: CONSTANTS.LAYOUT.RIGHT_FISHBONE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '向右鱼骨图2',
|
||||||
|
value: CONSTANTS.LAYOUT.RIGHT_FISHBONE2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
export const layoutValueList = [
|
export const layoutValueList = [
|
||||||
@ -139,7 +163,12 @@ export const layoutValueList = [
|
|||||||
CONSTANTS.LAYOUT.TIMELINE,
|
CONSTANTS.LAYOUT.TIMELINE,
|
||||||
CONSTANTS.LAYOUT.TIMELINE2,
|
CONSTANTS.LAYOUT.TIMELINE2,
|
||||||
CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
|
CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
|
||||||
CONSTANTS.LAYOUT.FISHBONE
|
CONSTANTS.LAYOUT.VERTICAL_TIMELINE2,
|
||||||
|
CONSTANTS.LAYOUT.VERTICAL_TIMELINE3,
|
||||||
|
CONSTANTS.LAYOUT.FISHBONE,
|
||||||
|
CONSTANTS.LAYOUT.FISHBONE2,
|
||||||
|
CONSTANTS.LAYOUT.RIGHT_FISHBONE,
|
||||||
|
CONSTANTS.LAYOUT.RIGHT_FISHBONE2
|
||||||
]
|
]
|
||||||
|
|
||||||
// 节点数据中非样式的字段
|
// 节点数据中非样式的字段
|
||||||
@ -157,7 +186,7 @@ export const nodeDataNoStylePropList = [
|
|||||||
'isActive',
|
'isActive',
|
||||||
'generalization',
|
'generalization',
|
||||||
'richText',
|
'richText',
|
||||||
'resetRichText',
|
'resetRichText', // 重新创建富文本内容,去掉原有样式
|
||||||
'uid',
|
'uid',
|
||||||
'activeStyle',
|
'activeStyle',
|
||||||
'associativeLineTargets',
|
'associativeLineTargets',
|
||||||
@ -174,7 +203,10 @@ export const nodeDataNoStylePropList = [
|
|||||||
'customTop',
|
'customTop',
|
||||||
'customTextWidth',
|
'customTextWidth',
|
||||||
'checkbox',
|
'checkbox',
|
||||||
'dir'
|
'dir',
|
||||||
|
'needUpdate', // 重新创建节点内容
|
||||||
|
'imgMap',
|
||||||
|
'nodeLink'
|
||||||
]
|
]
|
||||||
|
|
||||||
// 错误类型
|
// 错误类型
|
||||||
@ -208,7 +240,7 @@ export const cssContent = `
|
|||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.smm-text-node-wrap {
|
.smm-text-node-wrap, .smm-expand-btn-text {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -226,3 +258,14 @@ export const selfCloseTagList = [
|
|||||||
|
|
||||||
// 非富文本模式下的节点文本行高
|
// 非富文本模式下的节点文本行高
|
||||||
export const noneRichTextNodeLineHeight = 1.2
|
export const noneRichTextNodeLineHeight = 1.2
|
||||||
|
|
||||||
|
// 富文本支持的样式列表
|
||||||
|
export const richTextSupportStyleList = [
|
||||||
|
'fontFamily',
|
||||||
|
'fontSize',
|
||||||
|
'fontWeight',
|
||||||
|
'fontStyle',
|
||||||
|
'textDecoration',
|
||||||
|
'color',
|
||||||
|
'textAlign'
|
||||||
|
]
|
||||||
|
|||||||
@ -35,8 +35,6 @@ export const defaultOpt = {
|
|||||||
mouseScaleCenterUseMousePosition: true,
|
mouseScaleCenterUseMousePosition: true,
|
||||||
// 最多显示几个标签
|
// 最多显示几个标签
|
||||||
maxTag: 5,
|
maxTag: 5,
|
||||||
// 标签显示的位置,相对于节点文本,bottom(下方)、right(右侧)
|
|
||||||
tagPosition: CONSTANTS.TAG_POSITION.RIGHT,
|
|
||||||
// 展开收缩按钮尺寸
|
// 展开收缩按钮尺寸
|
||||||
expandBtnSize: 20,
|
expandBtnSize: 20,
|
||||||
// 节点里图片和文字的间距
|
// 节点里图片和文字的间距
|
||||||
@ -268,6 +266,63 @@ export const defaultOpt = {
|
|||||||
// 实例化完后是否立刻进行一次历史数据入栈操作
|
// 实例化完后是否立刻进行一次历史数据入栈操作
|
||||||
// 即调用mindMap.command.addHistory方法
|
// 即调用mindMap.command.addHistory方法
|
||||||
addHistoryOnInit: true,
|
addHistoryOnInit: true,
|
||||||
|
// 自定义节点备注图标
|
||||||
|
noteIcon: {
|
||||||
|
icon: '', // svg字符串,如果不是确定要使用svg自带的样式,否则请去除其中的fill等样式属性
|
||||||
|
style: {
|
||||||
|
// size: 20,// 图标大小,不手动设置则会使用主题的iconSize配置
|
||||||
|
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 自定义节点超链接图标
|
||||||
|
hyperlinkIcon: {
|
||||||
|
icon: '', // svg字符串,如果不是确定要使用svg自带的样式,否则请去除其中的fill等样式属性
|
||||||
|
style: {
|
||||||
|
// size: 20,// 图标大小,不手动设置则会使用主题的iconSize配置
|
||||||
|
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 自定义节点附件图标
|
||||||
|
attachmentIcon: {
|
||||||
|
icon: '', // svg字符串,如果不是确定要使用svg自带的样式,否则请去除其中的fill等样式属性
|
||||||
|
style: {
|
||||||
|
// size: 20,// 图标大小,不手动设置则会使用主题的iconSize配置
|
||||||
|
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 是否显示快捷创建子节点按钮
|
||||||
|
isShowCreateChildBtnIcon: true,
|
||||||
|
// 自定义快捷创建子节点按钮图标
|
||||||
|
quickCreateChildBtnIcon: {
|
||||||
|
icon: '', // svg字符串,如果不是确定要使用svg自带的样式,否则请去除其中的fill等样式属性
|
||||||
|
style: {
|
||||||
|
// 图标大小使用的是expandBtnSize选项
|
||||||
|
// color: '',// 图标颜色,不手动设置则会使用expandBtnStyle选项的color字段
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 自定义快捷创建子节点按钮的点击操作,
|
||||||
|
customQuickCreateChildBtnClick: null,
|
||||||
|
// 添加自定义的节点内容
|
||||||
|
// 可传递一个对象,格式如下:
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
// 返回要添加的DOM元素详细
|
||||||
|
create: (node) => {
|
||||||
|
return {
|
||||||
|
el, // DOM节点
|
||||||
|
width: 20, // 宽高
|
||||||
|
height: 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 处理生成的@svgdotjs/svg.js库的ForeignObject节点实例,可以设置其在节点内的位置
|
||||||
|
handle: ({ content, element, node }) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
addCustomContentToNode: null,
|
||||||
|
// 节点连线样式是否允许继承祖先的连线样式
|
||||||
|
enableInheritAncestorLineStyle: true,
|
||||||
|
|
||||||
// 【Select插件】
|
// 【Select插件】
|
||||||
// 多选节点时鼠标移动到边缘时的画布移动偏移量
|
// 多选节点时鼠标移动到边缘时的画布移动偏移量
|
||||||
@ -441,6 +496,7 @@ export const defaultOpt = {
|
|||||||
// 【OuterFrame】插件
|
// 【OuterFrame】插件
|
||||||
outerFramePaddingX: 10,
|
outerFramePaddingX: 10,
|
||||||
outerFramePaddingY: 10,
|
outerFramePaddingY: 10,
|
||||||
|
defaultOuterFrameText: '外框',
|
||||||
|
|
||||||
// 【Painter】插件
|
// 【Painter】插件
|
||||||
// 是否只格式刷节点手动设置的样式,不考虑节点通过主题的应用的样式
|
// 是否只格式刷节点手动设置的样式,不考虑节点通过主题的应用的样式
|
||||||
@ -458,5 +514,10 @@ export const defaultOpt = {
|
|||||||
maxImgResizeWidthInheritTheme: false,
|
maxImgResizeWidthInheritTheme: false,
|
||||||
// 最大允许缩放的尺寸,maxImgResizeWidthInheritTheme选项设置为false时生效,不限制最大值可传递Infinity
|
// 最大允许缩放的尺寸,maxImgResizeWidthInheritTheme选项设置为false时生效,不限制最大值可传递Infinity
|
||||||
maxImgResizeWidth: Infinity,
|
maxImgResizeWidth: Infinity,
|
||||||
maxImgResizeHeight: Infinity
|
maxImgResizeHeight: Infinity,
|
||||||
|
// 自定义删除按钮和尺寸调整按钮的内容
|
||||||
|
// 默认为内置图标,你可以传递一个svg字符串,或者其他的html字符串
|
||||||
|
// 整体大小请使用上面的minImgResizeWidth和minImgResizeHeight选项设置
|
||||||
|
customDeleteBtnInnerHTML: '',
|
||||||
|
customResizeBtnInnerHTML: ''
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
transformTreeDataToObject
|
transformTreeDataToObject
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { ERROR_TYPES } from '../../constants/constant'
|
import { ERROR_TYPES } from '../../constants/constant'
|
||||||
|
import pkg from '../../../package.json'
|
||||||
|
|
||||||
// 命令类
|
// 命令类
|
||||||
class Command {
|
class Command {
|
||||||
@ -14,7 +15,7 @@ class Command {
|
|||||||
this.opt = opt
|
this.opt = opt
|
||||||
this.mindMap = opt.mindMap
|
this.mindMap = opt.mindMap
|
||||||
this.commands = {}
|
this.commands = {}
|
||||||
this.history = []
|
this.history = [] // 字符串形式存储
|
||||||
this.activeHistoryIndex = 0
|
this.activeHistoryIndex = 0
|
||||||
// 注册快捷键
|
// 注册快捷键
|
||||||
this.registerShortcutKeys()
|
this.registerShortcutKeys()
|
||||||
@ -105,18 +106,19 @@ class Command {
|
|||||||
if (this.mindMap.opt.readonly || this.isPause) {
|
if (this.mindMap.opt.readonly || this.isPause) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const lastData =
|
this.mindMap.emit('beforeAddHistory')
|
||||||
|
const lastDataStr =
|
||||||
this.history.length > 0 ? this.history[this.activeHistoryIndex] : null
|
this.history.length > 0 ? this.history[this.activeHistoryIndex] : null
|
||||||
const data = this.getCopyData()
|
const data = this.getCopyData()
|
||||||
|
const dataStr = JSON.stringify(data)
|
||||||
// 此次数据和上次一样则不重复添加
|
// 此次数据和上次一样则不重复添加
|
||||||
if (lastData === data) return
|
if (lastDataStr && lastDataStr === dataStr) {
|
||||||
if (lastData && JSON.stringify(lastData) === JSON.stringify(data)) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.emitDataUpdatesEvent(lastData, data)
|
this.emitDataUpdatesEvent(lastDataStr, dataStr)
|
||||||
// 删除当前历史指针后面的数据
|
// 删除当前历史指针后面的数据
|
||||||
this.history = this.history.slice(0, this.activeHistoryIndex + 1)
|
this.history = this.history.slice(0, this.activeHistoryIndex + 1)
|
||||||
this.history.push(simpleDeepClone(data))
|
this.history.push(dataStr)
|
||||||
// 历史记录数超过最大数量
|
// 历史记录数超过最大数量
|
||||||
if (this.history.length > this.mindMap.opt.maxHistoryCount) {
|
if (this.history.length > this.mindMap.opt.maxHistoryCount) {
|
||||||
this.history.shift()
|
this.history.shift()
|
||||||
@ -136,15 +138,16 @@ class Command {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.activeHistoryIndex - step >= 0) {
|
if (this.activeHistoryIndex - step >= 0) {
|
||||||
const lastData = this.history[this.activeHistoryIndex]
|
const lastDataStr = this.history[this.activeHistoryIndex]
|
||||||
this.activeHistoryIndex -= step
|
this.activeHistoryIndex -= step
|
||||||
this.mindMap.emit(
|
this.mindMap.emit(
|
||||||
'back_forward',
|
'back_forward',
|
||||||
this.activeHistoryIndex,
|
this.activeHistoryIndex,
|
||||||
this.history.length
|
this.history.length
|
||||||
)
|
)
|
||||||
const data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
const dataStr = this.history[this.activeHistoryIndex]
|
||||||
this.emitDataUpdatesEvent(lastData, data)
|
const data = JSON.parse(dataStr)
|
||||||
|
this.emitDataUpdatesEvent(lastDataStr, dataStr)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,15 +159,16 @@ class Command {
|
|||||||
}
|
}
|
||||||
let len = this.history.length
|
let len = this.history.length
|
||||||
if (this.activeHistoryIndex + step <= len - 1) {
|
if (this.activeHistoryIndex + step <= len - 1) {
|
||||||
const lastData = this.history[this.activeHistoryIndex]
|
const lastDataStr = this.history[this.activeHistoryIndex]
|
||||||
this.activeHistoryIndex += step
|
this.activeHistoryIndex += step
|
||||||
this.mindMap.emit(
|
this.mindMap.emit(
|
||||||
'back_forward',
|
'back_forward',
|
||||||
this.activeHistoryIndex,
|
this.activeHistoryIndex,
|
||||||
this.history.length
|
this.history.length
|
||||||
)
|
)
|
||||||
const data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
const dataStr = this.history[this.activeHistoryIndex]
|
||||||
this.emitDataUpdatesEvent(lastData, data)
|
const data = JSON.parse(dataStr)
|
||||||
|
this.emitDataUpdatesEvent(lastDataStr, dataStr)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -172,7 +176,9 @@ class Command {
|
|||||||
// 获取渲染树数据副本
|
// 获取渲染树数据副本
|
||||||
getCopyData() {
|
getCopyData() {
|
||||||
if (!this.mindMap.renderer.renderTree) return null
|
if (!this.mindMap.renderer.renderTree) return null
|
||||||
return copyRenderTree({}, this.mindMap.renderer.renderTree, true)
|
const res = copyRenderTree({}, this.mindMap.renderer.renderTree, true)
|
||||||
|
res.smmVersion = pkg.version
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除节点数据中的uid
|
// 移除节点数据中的uid
|
||||||
@ -191,12 +197,14 @@ class Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 派发思维导图更新明细事件
|
// 派发思维导图更新明细事件
|
||||||
emitDataUpdatesEvent(lastData, data) {
|
emitDataUpdatesEvent(lastDataStr, dataStr) {
|
||||||
try {
|
try {
|
||||||
// 如果data_change_detail没有监听者,那么不进行计算,节省性能
|
// 如果data_change_detail没有监听者,那么不进行计算,节省性能
|
||||||
const eventName = 'data_change_detail'
|
const eventName = 'data_change_detail'
|
||||||
const count = this.mindMap.event.listenerCount(eventName)
|
const count = this.mindMap.event.listenerCount(eventName)
|
||||||
if (count > 0 && lastData && data) {
|
if (count > 0 && lastDataStr && dataStr) {
|
||||||
|
const lastData = JSON.parse(lastDataStr)
|
||||||
|
const data = JSON.parse(dataStr)
|
||||||
const lastDataObj = simpleDeepClone(transformTreeDataToObject(lastData))
|
const lastDataObj = simpleDeepClone(transformTreeDataToObject(lastData))
|
||||||
const dataObj = simpleDeepClone(transformTreeDataToObject(data))
|
const dataObj = simpleDeepClone(transformTreeDataToObject(data))
|
||||||
const res = []
|
const res = []
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { keyMap } from './keyMap'
|
import { keyMap } from './keyMap'
|
||||||
import { CONSTANTS } from '../../constants/constant'
|
|
||||||
|
|
||||||
// 快捷按键、命令处理类
|
// 快捷按键、命令处理类
|
||||||
export default class KeyCommand {
|
export default class KeyCommand {
|
||||||
@ -13,6 +12,8 @@ export default class KeyCommand {
|
|||||||
this.shortcutMapCache = {}
|
this.shortcutMapCache = {}
|
||||||
this.isPause = false
|
this.isPause = false
|
||||||
this.isInSvg = false
|
this.isInSvg = false
|
||||||
|
this.isStopCheckInSvg = false
|
||||||
|
this.defaultEnableCheck = this.defaultEnableCheck.bind(this)
|
||||||
this.bindEvent()
|
this.bindEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +59,22 @@ export default class KeyCommand {
|
|||||||
this.shortcutMapCache = {}
|
this.shortcutMapCache = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 停止对鼠标是否在画布内的检查,前提是开启了enableShortcutOnlyWhenMouseInSvg选项
|
||||||
|
// 库内部节点文本编辑、关联线文本编辑、外框文本编辑前都会暂停检查,否则无法响应回车快捷键用于结束编辑
|
||||||
|
// 如果你新增了额外的文本编辑,也可以在编辑前调用此方法
|
||||||
|
stopCheckInSvg() {
|
||||||
|
const { enableShortcutOnlyWhenMouseInSvg } = this.mindMap.opt
|
||||||
|
if (!enableShortcutOnlyWhenMouseInSvg) return
|
||||||
|
this.isStopCheckInSvg = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复对鼠标是否在画布内的检查
|
||||||
|
recoveryCheckInSvg() {
|
||||||
|
const { enableShortcutOnlyWhenMouseInSvg } = this.mindMap.opt
|
||||||
|
if (!enableShortcutOnlyWhenMouseInSvg) return
|
||||||
|
this.isStopCheckInSvg = true
|
||||||
|
}
|
||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
bindEvent() {
|
bindEvent() {
|
||||||
this.onKeydown = this.onKeydown.bind(this)
|
this.onKeydown = this.onKeydown.bind(this)
|
||||||
@ -66,13 +83,6 @@ export default class KeyCommand {
|
|||||||
this.isInSvg = true
|
this.isInSvg = true
|
||||||
})
|
})
|
||||||
this.mindMap.on('svg_mouseleave', () => {
|
this.mindMap.on('svg_mouseleave', () => {
|
||||||
if (this.mindMap.renderer.textEdit.isShowTextEdit()) return
|
|
||||||
if (
|
|
||||||
this.mindMap.associativeLine &&
|
|
||||||
this.mindMap.associativeLine.showTextEdit
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.isInSvg = false
|
this.isInSvg = false
|
||||||
})
|
})
|
||||||
window.addEventListener('keydown', this.onKeydown)
|
window.addEventListener('keydown', this.onKeydown)
|
||||||
@ -89,12 +99,14 @@ export default class KeyCommand {
|
|||||||
// 根据事件目标判断是否响应快捷键事件
|
// 根据事件目标判断是否响应快捷键事件
|
||||||
defaultEnableCheck(e) {
|
defaultEnableCheck(e) {
|
||||||
const target = e.target
|
const target = e.target
|
||||||
return (
|
if (target === document.body) return true
|
||||||
target === document.body ||
|
for (let i = 0; i < this.mindMap.editNodeClassList.length; i++) {
|
||||||
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.SMM_NODE_EDIT_WRAP) ||
|
const cur = this.mindMap.editNodeClassList[i]
|
||||||
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP) ||
|
if (target.classList.contains(cur)) {
|
||||||
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
return true
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按键事件
|
// 按键事件
|
||||||
@ -109,7 +121,12 @@ export default class KeyCommand {
|
|||||||
? customCheckEnableShortcut
|
? customCheckEnableShortcut
|
||||||
: this.defaultEnableCheck
|
: this.defaultEnableCheck
|
||||||
if (!checkFn(e)) return
|
if (!checkFn(e)) return
|
||||||
if (this.isPause || (enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) {
|
if (
|
||||||
|
this.isPause ||
|
||||||
|
(enableShortcutOnlyWhenMouseInSvg &&
|
||||||
|
!this.isStopCheckInSvg &&
|
||||||
|
!this.isInSvg)
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Object.keys(this.shortcutMap).forEach(key => {
|
Object.keys(this.shortcutMap).forEach(key => {
|
||||||
|
|||||||
@ -30,10 +30,10 @@ import {
|
|||||||
createSmmFormatData,
|
createSmmFormatData,
|
||||||
checkSmmFormatData,
|
checkSmmFormatData,
|
||||||
checkIsNodeStyleDataKey,
|
checkIsNodeStyleDataKey,
|
||||||
removeRichTextStyes,
|
|
||||||
formatGetNodeGeneralization,
|
formatGetNodeGeneralization,
|
||||||
sortNodeList,
|
sortNodeList,
|
||||||
throttle,
|
throttle,
|
||||||
|
debounce,
|
||||||
checkClipboardReadEnable,
|
checkClipboardReadEnable,
|
||||||
isNodeNotNeedRenderData
|
isNodeNotNeedRenderData
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@ -60,8 +60,14 @@ const layouts = {
|
|||||||
[CONSTANTS.LAYOUT.TIMELINE2]: Timeline,
|
[CONSTANTS.LAYOUT.TIMELINE2]: Timeline,
|
||||||
// 竖向时间轴
|
// 竖向时间轴
|
||||||
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline,
|
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline,
|
||||||
|
// 竖向时间轴2
|
||||||
|
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE2]: VerticalTimeline,
|
||||||
|
// 竖向时间轴3
|
||||||
|
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE3]: VerticalTimeline,
|
||||||
// 鱼骨图
|
// 鱼骨图
|
||||||
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone
|
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone,
|
||||||
|
// 鱼骨图2
|
||||||
|
[CONSTANTS.LAYOUT.FISHBONE2]: Fishbone
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染
|
// 渲染
|
||||||
@ -81,14 +87,18 @@ class Render {
|
|||||||
this.isRendering = false
|
this.isRendering = false
|
||||||
// 是否存在等待渲染
|
// 是否存在等待渲染
|
||||||
this.hasWaitRendering = false
|
this.hasWaitRendering = false
|
||||||
this.waitRenderingParams = []
|
|
||||||
// 用于缓存节点
|
// 用于缓存节点
|
||||||
this.nodeCache = {}
|
this.nodeCache = {}
|
||||||
this.lastNodeCache = {}
|
this.lastNodeCache = {}
|
||||||
// 触发render的来源
|
// 收集触发render的来源
|
||||||
this.renderSource = ''
|
this.renderSourceList = []
|
||||||
|
// 收集render的回调函数
|
||||||
|
this.renderCallbackList = []
|
||||||
// 当前激活的节点列表
|
// 当前激活的节点列表
|
||||||
this.activeNodeList = []
|
this.activeNodeList = []
|
||||||
|
// 防抖定时器
|
||||||
|
this.emitNodeActiveEventTimer = null
|
||||||
|
this.renderTimer = null
|
||||||
// 根节点
|
// 根节点
|
||||||
this.root = null
|
this.root = null
|
||||||
// 文本编辑框,需要再bindEvent之前实例化,否则单击事件只能触发隐藏文本编辑框,而无法保存文本修改
|
// 文本编辑框,需要再bindEvent之前实例化,否则单击事件只能触发隐藏文本编辑框,而无法保存文本修改
|
||||||
@ -112,12 +122,16 @@ class Render {
|
|||||||
|
|
||||||
// 设置布局结构
|
// 设置布局结构
|
||||||
setLayout() {
|
setLayout() {
|
||||||
|
if (this.layout && this.layout.beforeChange) {
|
||||||
|
this.layout.beforeChange()
|
||||||
|
}
|
||||||
const { layout } = this.mindMap.opt
|
const { layout } = this.mindMap.opt
|
||||||
this.layout = new (
|
let L = layouts[layout] || this.mindMap[layout]
|
||||||
layouts[layout]
|
if (!L) {
|
||||||
? layouts[layout]
|
L = layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE]
|
||||||
: layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE]
|
this.mindMap.opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||||
)(this, layout)
|
}
|
||||||
|
this.layout = new L(this, layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新设置思维导图数据
|
// 重新设置思维导图数据
|
||||||
@ -147,6 +161,9 @@ class Render {
|
|||||||
})
|
})
|
||||||
// 性能模式
|
// 性能模式
|
||||||
const onViewDataChange = throttle(() => {
|
const onViewDataChange = throttle(() => {
|
||||||
|
if (!this.renderTree) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (this.root) {
|
if (this.root) {
|
||||||
this.mindMap.emit('node_tree_render_start')
|
this.mindMap.emit('node_tree_render_start')
|
||||||
this.root.render(
|
this.root.render(
|
||||||
@ -162,7 +179,7 @@ class Render {
|
|||||||
this.mindMap.on('view_data_change', onViewDataChange)
|
this.mindMap.on('view_data_change', onViewDataChange)
|
||||||
}
|
}
|
||||||
// 文本编辑时实时更新节点大小
|
// 文本编辑时实时更新节点大小
|
||||||
this.onNodeTextEditChange = this.onNodeTextEditChange.bind(this)
|
this.onNodeTextEditChange = debounce(this.onNodeTextEditChange, 100, this)
|
||||||
if (openRealtimeRenderOnNodeTextEdit) {
|
if (openRealtimeRenderOnNodeTextEdit) {
|
||||||
this.mindMap.on('node_text_edit_change', this.onNodeTextEditChange)
|
this.mindMap.on('node_text_edit_change', this.onNodeTextEditChange)
|
||||||
}
|
}
|
||||||
@ -443,9 +460,10 @@ class Render {
|
|||||||
)
|
)
|
||||||
if (!isChange) return
|
if (!isChange) return
|
||||||
this.lastActiveNodeList = [...activeNodeList]
|
this.lastActiveNodeList = [...activeNodeList]
|
||||||
this.mindMap.batchExecution.push('emitNodeActiveEvent', () => {
|
clearTimeout(this.emitNodeActiveEventTimer)
|
||||||
|
this.emitNodeActiveEventTimer = setTimeout(() => {
|
||||||
this.mindMap.emit('node_active', node, activeNodeList)
|
this.mindMap.emit('node_active', node, activeNodeList)
|
||||||
})
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鼠标点击画布时清空当前激活节点列表
|
// 鼠标点击画布时清空当前激活节点列表
|
||||||
@ -488,22 +506,71 @@ class Render {
|
|||||||
this.lastNodeCache = {}
|
this.lastNodeCache = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存触发渲染的参数
|
||||||
|
addRenderParams(callback, source) {
|
||||||
|
if (callback) {
|
||||||
|
const index = this.renderCallbackList.findIndex(fn => {
|
||||||
|
return fn === callback
|
||||||
|
})
|
||||||
|
if (index === -1) {
|
||||||
|
this.renderCallbackList.push(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (source) {
|
||||||
|
const index = this.renderSourceList.findIndex(s => {
|
||||||
|
return s === source
|
||||||
|
})
|
||||||
|
if (index === -1) {
|
||||||
|
this.renderSourceList.push(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否包含某种触发渲染源
|
||||||
|
checkHasRenderSource(val) {
|
||||||
|
val = Array.isArray(val) ? val : [val]
|
||||||
|
for (let i = 0; i < this.renderSourceList.length; i++) {
|
||||||
|
if (val.includes(this.renderSourceList[i])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染完毕的操作
|
||||||
|
onRenderEnd() {
|
||||||
|
this.renderCallbackList.forEach(fn => {
|
||||||
|
fn()
|
||||||
|
})
|
||||||
|
this.isRendering = false
|
||||||
|
this.reRender = false
|
||||||
|
this.renderCallbackList = []
|
||||||
|
this.renderSourceList = []
|
||||||
|
this.mindMap.emit('node_tree_render_end')
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染
|
// 渲染
|
||||||
render(callback = () => {}, source) {
|
render(callback, source) {
|
||||||
|
this.addRenderParams(callback, source)
|
||||||
|
clearTimeout(this.renderTimer)
|
||||||
|
this.renderTimer = setTimeout(() => {
|
||||||
|
this._render()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 真正的渲染
|
||||||
|
_render() {
|
||||||
// 切换主题时,被收起的节点需要添加样式复位的标注
|
// 切换主题时,被收起的节点需要添加样式复位的标注
|
||||||
if (source === CONSTANTS.CHANGE_THEME) {
|
if (this.checkHasRenderSource(CONSTANTS.CHANGE_THEME)) {
|
||||||
this.resetUnExpandNodeStyle()
|
this.resetUnExpandNodeStyle()
|
||||||
}
|
}
|
||||||
// 如果当前还没有渲染完毕,不再触发渲染
|
// 如果当前还没有渲染完毕,不再触发渲染
|
||||||
if (this.isRendering) {
|
if (this.isRendering) {
|
||||||
// 等待当前渲染完毕后再进行一次渲染
|
// 等待当前渲染完毕后再进行一次渲染
|
||||||
this.hasWaitRendering = true
|
this.hasWaitRendering = true
|
||||||
this.waitRenderingParams = [callback, source]
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isRendering = true
|
this.isRendering = true
|
||||||
// 触发当前重新渲染的来源
|
|
||||||
this.renderSource = source
|
|
||||||
// 节点缓存
|
// 节点缓存
|
||||||
this.lastNodeCache = this.nodeCache
|
this.lastNodeCache = this.nodeCache
|
||||||
this.nodeCache = {}
|
this.nodeCache = {}
|
||||||
@ -513,8 +580,7 @@ class Render {
|
|||||||
}
|
}
|
||||||
// 如果没有节点数据
|
// 如果没有节点数据
|
||||||
if (!this.renderTree) {
|
if (!this.renderTree) {
|
||||||
this.isRendering = false
|
this.onRenderEnd()
|
||||||
this.mindMap.emit('node_tree_render_end')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.mindMap.emit('node_tree_render_start')
|
this.mindMap.emit('node_tree_render_start')
|
||||||
@ -536,38 +602,32 @@ class Render {
|
|||||||
// 渲染节点
|
// 渲染节点
|
||||||
this.root.render(() => {
|
this.root.render(() => {
|
||||||
this.isRendering = false
|
this.isRendering = false
|
||||||
callback && callback()
|
|
||||||
if (this.hasWaitRendering) {
|
if (this.hasWaitRendering) {
|
||||||
const params = this.waitRenderingParams
|
|
||||||
this.hasWaitRendering = false
|
this.hasWaitRendering = false
|
||||||
this.waitRenderingParams = []
|
this.render()
|
||||||
this.render(...params)
|
return
|
||||||
} else {
|
|
||||||
this.renderSource = ''
|
|
||||||
if (this.reRender) {
|
|
||||||
this.reRender = false
|
|
||||||
}
|
}
|
||||||
// 触发一次保存,因为修改了渲染树的数据
|
this.onRenderEnd()
|
||||||
if (
|
|
||||||
this.hasRichTextPlugin() &&
|
|
||||||
[CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)
|
|
||||||
) {
|
|
||||||
this.mindMap.command.addHistory()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.mindMap.emit('node_tree_render_end')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
this.emitNodeActiveEvent()
|
this.emitNodeActiveEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 给当前被收起来的节点数据添加文本复位标志
|
// 当某个自定义节点内容改变后,可以调用该方法实时更新该节点大小和整体节点的定位
|
||||||
|
renderByCustomNodeContentNode(node) {
|
||||||
|
node.getSize()
|
||||||
|
node.customNodeContentRealtimeLayout()
|
||||||
|
this.mindMap.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给当前被收起来的节点数据添加更新标志
|
||||||
resetUnExpandNodeStyle() {
|
resetUnExpandNodeStyle() {
|
||||||
if (!this.renderTree || !this.hasRichTextPlugin()) return
|
if (!this.renderTree) return
|
||||||
walk(this.renderTree, null, node => {
|
walk(this.renderTree, null, node => {
|
||||||
if (!node.data.expand) {
|
if (!node.data.expand) {
|
||||||
walk(node, null, node2 => {
|
walk(node, null, node2 => {
|
||||||
node2.data.resetRichText = true
|
// 主要是触发数据新旧对比,不一样则会重新创建节点
|
||||||
|
node2.data['needUpdate'] = true
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -750,15 +810,16 @@ class Render {
|
|||||||
richText: isRichText,
|
richText: isRichText,
|
||||||
isActive: focusNewNode // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
|
isActive: focusNewNode // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
|
||||||
}
|
}
|
||||||
if (isRichText) params.resetRichText = isRichText
|
if (isRichText) params.resetRichText = true
|
||||||
// 动态指定的子节点数据也需要添加相关属性
|
// 动态指定的子节点数据也需要添加相关属性
|
||||||
appointChildren = addDataToAppointNodes(appointChildren, {
|
appointChildren = addDataToAppointNodes(appointChildren, params)
|
||||||
...params
|
const alreadyIsRichText = appointData && appointData.richText
|
||||||
})
|
let createNewId = false
|
||||||
list.forEach(node => {
|
list.forEach(node => {
|
||||||
if (node.isGeneralization || node.isRoot) {
|
if (node.isGeneralization || node.isRoot) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
appointChildren = simpleDeepClone(appointChildren)
|
||||||
const parent = node.parent
|
const parent = node.parent
|
||||||
const isOneLayer = node.layerIndex === 1
|
const isOneLayer = node.layerIndex === 1
|
||||||
// 新插入节点的默认文本
|
// 新插入节点的默认文本
|
||||||
@ -767,6 +828,10 @@ class Render {
|
|||||||
: defaultInsertBelowSecondLevelNodeText
|
: defaultInsertBelowSecondLevelNodeText
|
||||||
// 计算插入位置
|
// 计算插入位置
|
||||||
const index = getNodeDataIndex(node)
|
const index = getNodeDataIndex(node)
|
||||||
|
// 如果指定的数据就是富文本格式,那么不需要重新创建
|
||||||
|
if (alreadyIsRichText && params.resetRichText) {
|
||||||
|
delete params.resetRichText
|
||||||
|
}
|
||||||
const newNodeData = {
|
const newNodeData = {
|
||||||
inserting,
|
inserting,
|
||||||
data: {
|
data: {
|
||||||
@ -775,8 +840,9 @@ class Render {
|
|||||||
uid: createUid(),
|
uid: createUid(),
|
||||||
...(appointData || {})
|
...(appointData || {})
|
||||||
},
|
},
|
||||||
children: [...createUidForAppointNodes(appointChildren)]
|
children: [...createUidForAppointNodes(appointChildren, createNewId)]
|
||||||
}
|
}
|
||||||
|
createNewId = true
|
||||||
parent.nodeData.children.splice(index + 1, 0, newNodeData)
|
parent.nodeData.children.splice(index + 1, 0, newNodeData)
|
||||||
})
|
})
|
||||||
// 如果同时对多个节点插入子节点,需要清除原来激活的节点
|
// 如果同时对多个节点插入子节点,需要清除原来激活的节点
|
||||||
@ -802,16 +868,19 @@ class Render {
|
|||||||
richText: isRichText,
|
richText: isRichText,
|
||||||
isActive: focusNewNode
|
isActive: focusNewNode
|
||||||
}
|
}
|
||||||
if (isRichText) params.resetRichText = isRichText
|
if (isRichText) params.resetRichText = true
|
||||||
nodeList = addDataToAppointNodes(nodeList, params)
|
nodeList = addDataToAppointNodes(nodeList, params)
|
||||||
|
let createNewId = false
|
||||||
list.forEach(node => {
|
list.forEach(node => {
|
||||||
if (node.isGeneralization || node.isRoot) {
|
if (node.isGeneralization || node.isRoot) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
nodeList = simpleDeepClone(nodeList)
|
||||||
const parent = node.parent
|
const parent = node.parent
|
||||||
// 计算插入位置
|
// 计算插入位置
|
||||||
const index = getNodeDataIndex(node)
|
const index = getNodeDataIndex(node)
|
||||||
const newNodeList = createUidForAppointNodes(simpleDeepClone(nodeList))
|
const newNodeList = createUidForAppointNodes(nodeList, createNewId)
|
||||||
|
createNewId = true
|
||||||
parent.nodeData.children.splice(index + 1, 0, ...newNodeList)
|
parent.nodeData.children.splice(index + 1, 0, ...newNodeList)
|
||||||
})
|
})
|
||||||
if (focusNewNode) {
|
if (focusNewNode) {
|
||||||
@ -848,21 +917,26 @@ class Render {
|
|||||||
richText: isRichText,
|
richText: isRichText,
|
||||||
isActive: focusNewNode
|
isActive: focusNewNode
|
||||||
}
|
}
|
||||||
if (isRichText) params.resetRichText = isRichText
|
if (isRichText) params.resetRichText = true
|
||||||
// 动态指定的子节点数据也需要添加相关属性
|
// 动态指定的子节点数据也需要添加相关属性
|
||||||
appointChildren = addDataToAppointNodes(appointChildren, {
|
appointChildren = addDataToAppointNodes(appointChildren, params)
|
||||||
...params
|
const alreadyIsRichText = appointData && appointData.richText
|
||||||
})
|
let createNewId = false
|
||||||
list.forEach(node => {
|
list.forEach(node => {
|
||||||
if (node.isGeneralization) {
|
if (node.isGeneralization) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
appointChildren = simpleDeepClone(appointChildren)
|
||||||
if (!node.nodeData.children) {
|
if (!node.nodeData.children) {
|
||||||
node.nodeData.children = []
|
node.nodeData.children = []
|
||||||
}
|
}
|
||||||
const text = node.isRoot
|
const text = node.isRoot
|
||||||
? defaultInsertSecondLevelNodeText
|
? defaultInsertSecondLevelNodeText
|
||||||
: defaultInsertBelowSecondLevelNodeText
|
: defaultInsertBelowSecondLevelNodeText
|
||||||
|
// 如果指定的数据就是富文本格式,那么不需要重新创建
|
||||||
|
if (alreadyIsRichText && params.resetRichText) {
|
||||||
|
delete params.resetRichText
|
||||||
|
}
|
||||||
const newNode = {
|
const newNode = {
|
||||||
inserting,
|
inserting,
|
||||||
data: {
|
data: {
|
||||||
@ -871,8 +945,9 @@ class Render {
|
|||||||
...params,
|
...params,
|
||||||
...(appointData || {})
|
...(appointData || {})
|
||||||
},
|
},
|
||||||
children: [...createUidForAppointNodes(appointChildren)]
|
children: [...createUidForAppointNodes(appointChildren, createNewId)]
|
||||||
}
|
}
|
||||||
|
createNewId = true
|
||||||
node.nodeData.children.push(newNode)
|
node.nodeData.children.push(newNode)
|
||||||
// 插入子节点时自动展开子节点
|
// 插入子节点时自动展开子节点
|
||||||
node.setData({
|
node.setData({
|
||||||
@ -902,16 +977,20 @@ class Render {
|
|||||||
richText: isRichText,
|
richText: isRichText,
|
||||||
isActive: focusNewNode
|
isActive: focusNewNode
|
||||||
}
|
}
|
||||||
if (isRichText) params.resetRichText = isRichText
|
if (isRichText) params.resetRichText = true
|
||||||
childList = addDataToAppointNodes(childList, params)
|
childList = addDataToAppointNodes(childList, params)
|
||||||
|
let createNewId = false
|
||||||
list.forEach(node => {
|
list.forEach(node => {
|
||||||
if (node.isGeneralization) {
|
if (node.isGeneralization) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
childList = simpleDeepClone(childList)
|
||||||
if (!node.nodeData.children) {
|
if (!node.nodeData.children) {
|
||||||
node.nodeData.children = []
|
node.nodeData.children = []
|
||||||
}
|
}
|
||||||
childList = createUidForAppointNodes(childList)
|
childList = createUidForAppointNodes(childList, createNewId)
|
||||||
|
// 第一个引用不需要重新创建uid,后面的需要重新创建,否则id会重复
|
||||||
|
createNewId = true
|
||||||
node.nodeData.children.push(...childList)
|
node.nodeData.children.push(...childList)
|
||||||
// 插入子节点时自动展开子节点
|
// 插入子节点时自动展开子节点
|
||||||
node.setData({
|
node.setData({
|
||||||
@ -947,7 +1026,8 @@ class Render {
|
|||||||
richText: isRichText,
|
richText: isRichText,
|
||||||
isActive: focusNewNode
|
isActive: focusNewNode
|
||||||
}
|
}
|
||||||
if (isRichText) params.resetRichText = isRichText
|
if (isRichText) params.resetRichText = true
|
||||||
|
const alreadyIsRichText = appointData && appointData.richText
|
||||||
list.forEach(node => {
|
list.forEach(node => {
|
||||||
if (node.isGeneralization || node.isRoot) {
|
if (node.isGeneralization || node.isRoot) {
|
||||||
return
|
return
|
||||||
@ -956,6 +1036,10 @@ class Render {
|
|||||||
node.layerIndex === 1
|
node.layerIndex === 1
|
||||||
? defaultInsertSecondLevelNodeText
|
? defaultInsertSecondLevelNodeText
|
||||||
: defaultInsertBelowSecondLevelNodeText
|
: defaultInsertBelowSecondLevelNodeText
|
||||||
|
// 如果指定的数据就是富文本格式,那么不需要重新创建
|
||||||
|
if (alreadyIsRichText && params.resetRichText) {
|
||||||
|
delete params.resetRichText
|
||||||
|
}
|
||||||
const newNode = {
|
const newNode = {
|
||||||
inserting,
|
inserting,
|
||||||
data: {
|
data: {
|
||||||
@ -966,11 +1050,6 @@ class Render {
|
|||||||
},
|
},
|
||||||
children: [node.nodeData]
|
children: [node.nodeData]
|
||||||
}
|
}
|
||||||
if (isRichText) {
|
|
||||||
node.setData({
|
|
||||||
resetRichText: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const parent = node.parent
|
const parent = node.parent
|
||||||
// 获取当前节点所在位置
|
// 获取当前节点所在位置
|
||||||
const index = getNodeDataIndex(node)
|
const index = getNodeDataIndex(node)
|
||||||
@ -1046,7 +1125,6 @@ class Render {
|
|||||||
const index = getNodeIndexInNodeList(node, parent.children)
|
const index = getNodeIndexInNodeList(node, parent.children)
|
||||||
const parentIndex = getNodeIndexInNodeList(parent, grandpa.children)
|
const parentIndex = getNodeIndexInNodeList(parent, grandpa.children)
|
||||||
// 节点数据
|
// 节点数据
|
||||||
this.checkNodeLayerChange(node, parent)
|
|
||||||
parent.nodeData.children.splice(index, 1)
|
parent.nodeData.children.splice(index, 1)
|
||||||
grandpa.nodeData.children.splice(parentIndex + 1, 0, node.nodeData)
|
grandpa.nodeData.children.splice(parentIndex + 1, 0, node.nodeData)
|
||||||
this.mindMap.render()
|
this.mindMap.render()
|
||||||
@ -1061,10 +1139,10 @@ class Render {
|
|||||||
delete nodeData[key]
|
delete nodeData[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// 如果是富文本,那么还要处理富文本内容
|
// 如果是富文本,那么直接全部重新创建,因为有些样式是通过标签来渲染的
|
||||||
if (hasCustomStyles && this.hasRichTextPlugin()) {
|
if (this.hasRichTextPlugin()) {
|
||||||
|
hasCustomStyles = true
|
||||||
nodeData.resetRichText = true
|
nodeData.resetRichText = true
|
||||||
nodeData.text = removeRichTextStyes(nodeData.text)
|
|
||||||
}
|
}
|
||||||
return hasCustomStyles
|
return hasCustomStyles
|
||||||
}
|
}
|
||||||
@ -1208,7 +1286,10 @@ class Render {
|
|||||||
Array.isArray(smmData) ? smmData : [smmData]
|
Array.isArray(smmData) ? smmData : [smmData]
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
// 如果是富文本模式,那么需要转义特殊字符
|
||||||
|
if (this.hasRichTextPlugin()) {
|
||||||
text = htmlEscape(text)
|
text = htmlEscape(text)
|
||||||
|
}
|
||||||
const textArr = text
|
const textArr = text
|
||||||
.split(new RegExp('\r?\n|(?<!\n)\r', 'g'))
|
.split(new RegExp('\r?\n|(?<!\n)\r', 'g'))
|
||||||
.filter(item => {
|
.filter(item => {
|
||||||
@ -1302,7 +1383,6 @@ class Render {
|
|||||||
nodeList.reverse()
|
nodeList.reverse()
|
||||||
}
|
}
|
||||||
nodeList.forEach(item => {
|
nodeList.forEach(item => {
|
||||||
this.checkNodeLayerChange(item, exist)
|
|
||||||
// 移动节点
|
// 移动节点
|
||||||
let nodeParent = item.parent
|
let nodeParent = item.parent
|
||||||
let nodeBorthers = nodeParent.children
|
let nodeBorthers = nodeParent.children
|
||||||
@ -1329,25 +1409,6 @@ class Render {
|
|||||||
this.mindMap.render()
|
this.mindMap.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是富文本模式,那么某些层级变化需要更新样式
|
|
||||||
checkNodeLayerChange(node, toNode, toNodeIsParent = false) {
|
|
||||||
if (this.hasRichTextPlugin()) {
|
|
||||||
// 如果设置了自定义样式那么不需要更新
|
|
||||||
if (this.mindMap.richText.checkNodeHasCustomRichTextStyle(node)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const toIndex = toNodeIsParent ? toNode.layerIndex + 1 : toNode.layerIndex
|
|
||||||
let nodeLayerChanged =
|
|
||||||
(node.layerIndex === 1 && toIndex !== 1) ||
|
|
||||||
(node.layerIndex !== 1 && toIndex === 1)
|
|
||||||
if (nodeLayerChanged) {
|
|
||||||
node.setData({
|
|
||||||
resetRichText: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除节点
|
// 移除节点
|
||||||
removeNode(appointNodes = []) {
|
removeNode(appointNodes = []) {
|
||||||
appointNodes = formatDataToArray(appointNodes)
|
appointNodes = formatDataToArray(appointNodes)
|
||||||
@ -1531,7 +1592,6 @@ class Render {
|
|||||||
return !item.isRoot
|
return !item.isRoot
|
||||||
})
|
})
|
||||||
nodeList.forEach(item => {
|
nodeList.forEach(item => {
|
||||||
this.checkNodeLayerChange(item, toNode, true)
|
|
||||||
this.removeNodeFromActiveList(item)
|
this.removeNodeFromActiveList(item)
|
||||||
removeFromParentNodeData(item)
|
removeFromParentNodeData(item)
|
||||||
toNode.setData({
|
toNode.setData({
|
||||||
@ -1546,35 +1606,7 @@ class Render {
|
|||||||
// 粘贴节点到节点
|
// 粘贴节点到节点
|
||||||
pasteNode(data) {
|
pasteNode(data) {
|
||||||
data = formatDataToArray(data)
|
data = formatDataToArray(data)
|
||||||
if (this.activeNodeList.length <= 0 || data.length <= 0) {
|
this.mindMap.execCommand('INSERT_MULTI_CHILD_NODE', [], data)
|
||||||
return
|
|
||||||
}
|
|
||||||
this.activeNodeList.forEach(node => {
|
|
||||||
// 概要节点不允许添加下级节点
|
|
||||||
if (node.isGeneralization) return
|
|
||||||
node.setData({
|
|
||||||
expand: true
|
|
||||||
})
|
|
||||||
node.nodeData.children.push(
|
|
||||||
...data.map(item => {
|
|
||||||
const newData = simpleDeepClone(item)
|
|
||||||
createUidForAppointNodes([newData], true, node => {
|
|
||||||
// 可能跨层级复制,那么富文本样式需要更新
|
|
||||||
if (this.hasRichTextPlugin()) {
|
|
||||||
// 如果设置了自定义样式那么不需要更新
|
|
||||||
if (
|
|
||||||
this.mindMap.richText.checkNodeHasCustomRichTextStyle(node.data)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
node.data.resetRichText = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return newData
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
this.mindMap.render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置节点样式
|
// 设置节点样式
|
||||||
@ -1582,13 +1614,6 @@ class Render {
|
|||||||
const data = {
|
const data = {
|
||||||
[prop]: value
|
[prop]: value
|
||||||
}
|
}
|
||||||
// 如果开启了富文本,则需要应用到富文本上
|
|
||||||
if (
|
|
||||||
this.hasRichTextPlugin() &&
|
|
||||||
this.mindMap.richText.isHasRichTextStyle(data)
|
|
||||||
) {
|
|
||||||
data.resetRichText = true
|
|
||||||
}
|
|
||||||
this.setNodeDataRender(node, data)
|
this.setNodeDataRender(node, data)
|
||||||
// 更新了连线的样式
|
// 更新了连线的样式
|
||||||
if (lineStyleProps.includes(prop)) {
|
if (lineStyleProps.includes(prop)) {
|
||||||
@ -1599,13 +1624,6 @@ class Render {
|
|||||||
// 设置节点多个样式
|
// 设置节点多个样式
|
||||||
setNodeStyles(node, style) {
|
setNodeStyles(node, style) {
|
||||||
const data = { ...style }
|
const data = { ...style }
|
||||||
// 如果开启了富文本,则需要应用到富文本上
|
|
||||||
if (
|
|
||||||
this.hasRichTextPlugin() &&
|
|
||||||
this.mindMap.richText.isHasRichTextStyle(data)
|
|
||||||
) {
|
|
||||||
data.resetRichText = true
|
|
||||||
}
|
|
||||||
this.setNodeDataRender(node, data)
|
this.setNodeDataRender(node, data)
|
||||||
// 更新了连线的样式
|
// 更新了连线的样式
|
||||||
let props = Object.keys(style)
|
let props = Object.keys(style)
|
||||||
@ -1826,6 +1844,7 @@ class Render {
|
|||||||
list.length > 1
|
list.length > 1
|
||||||
)
|
)
|
||||||
let needRender = false
|
let needRender = false
|
||||||
|
const alreadyIsRichText = data && data.richText
|
||||||
list.forEach(item => {
|
list.forEach(item => {
|
||||||
const newData = {
|
const newData = {
|
||||||
inserting,
|
inserting,
|
||||||
@ -1837,7 +1856,7 @@ class Render {
|
|||||||
richText: isRichText,
|
richText: isRichText,
|
||||||
isActive: focusNewNode
|
isActive: focusNewNode
|
||||||
}
|
}
|
||||||
if (isRichText) newData.resetRichText = isRichText
|
if (isRichText && !alreadyIsRichText) newData.resetRichText = isRichText
|
||||||
let generalization = item.node.getData('generalization')
|
let generalization = item.node.getData('generalization')
|
||||||
generalization = generalization
|
generalization = generalization
|
||||||
? Array.isArray(generalization)
|
? Array.isArray(generalization)
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import {
|
|||||||
noneRichTextNodeLineHeight
|
noneRichTextNodeLineHeight
|
||||||
} from '../../constants/constant'
|
} from '../../constants/constant'
|
||||||
|
|
||||||
|
const SMM_NODE_EDIT_WRAP = 'smm-node-edit-wrap'
|
||||||
|
|
||||||
// 节点文字编辑类
|
// 节点文字编辑类
|
||||||
export default class TextEdit {
|
export default class TextEdit {
|
||||||
// 构造函数
|
// 构造函数
|
||||||
@ -33,6 +35,8 @@ export default class TextEdit {
|
|||||||
this.hasBodyMousedown = false
|
this.hasBodyMousedown = false
|
||||||
this.textNodePaddingX = 5
|
this.textNodePaddingX = 5
|
||||||
this.textNodePaddingY = 3
|
this.textNodePaddingY = 3
|
||||||
|
this.isNeedUpdateTextEditNode = false
|
||||||
|
this.mindMap.addEditNodeClass(SMM_NODE_EDIT_WRAP)
|
||||||
this.bindEvent()
|
this.bindEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +95,7 @@ export default class TextEdit {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
this.mindMap.on('scale', this.onScale)
|
this.mindMap.on('scale', this.onScale)
|
||||||
// // 监听按键事件,判断是否自动进入文本编辑模式
|
// 监听按键事件,判断是否自动进入文本编辑模式
|
||||||
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
|
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
|
||||||
window.addEventListener('keydown', this.onKeydown)
|
window.addEventListener('keydown', this.onKeydown)
|
||||||
}
|
}
|
||||||
@ -124,6 +128,18 @@ export default class TextEdit {
|
|||||||
]('keydown', this.onKeydown)
|
]('keydown', this.onKeydown)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// 正在编辑文本时,给节点添加了图标等其他内容时需要更新编辑框的位置
|
||||||
|
this.mindMap.on('afterExecCommand', () => {
|
||||||
|
if (!this.isShowTextEdit()) return
|
||||||
|
this.isNeedUpdateTextEditNode = true
|
||||||
|
})
|
||||||
|
this.mindMap.on('node_tree_render_end', () => {
|
||||||
|
if (!this.isShowTextEdit()) return
|
||||||
|
if (this.isNeedUpdateTextEditNode) {
|
||||||
|
this.isNeedUpdateTextEditNode = false
|
||||||
|
this.updateTextEditNode()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解绑事件
|
// 解绑事件
|
||||||
@ -139,6 +155,9 @@ export default class TextEdit {
|
|||||||
const node = activeNodeList[0]
|
const node = activeNodeList[0]
|
||||||
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
|
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
|
||||||
if (node && this.checkIsAutoEnterTextEditKey(e)) {
|
if (node && this.checkIsAutoEnterTextEditKey(e)) {
|
||||||
|
// 忽略第一个键值,避免中文输入法时进入编辑会导致第一个键值变成字母的问题
|
||||||
|
// 带来的问题是按的第一下纯粹是进入文本编辑,但没有变成输入
|
||||||
|
e.preventDefault()
|
||||||
this.show({
|
this.show({
|
||||||
node,
|
node,
|
||||||
e,
|
e,
|
||||||
@ -161,7 +180,6 @@ export default class TextEdit {
|
|||||||
|
|
||||||
// 注册临时快捷键
|
// 注册临时快捷键
|
||||||
registerTmpShortcut() {
|
registerTmpShortcut() {
|
||||||
// 注册回车快捷键
|
|
||||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||||
this.hideEditTextBox()
|
this.hideEditTextBox()
|
||||||
})
|
})
|
||||||
@ -178,6 +196,16 @@ export default class TextEdit {
|
|||||||
return this.showTextEdit
|
return this.showTextEdit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置文本编辑框是否处于显示状态
|
||||||
|
setIsShowTextEdit(val) {
|
||||||
|
this.showTextEdit = val
|
||||||
|
if (val) {
|
||||||
|
this.mindMap.keyCommand.stopCheckInSvg()
|
||||||
|
} else {
|
||||||
|
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 显示文本编辑框
|
// 显示文本编辑框
|
||||||
// isInserting:是否是刚创建的节点
|
// isInserting:是否是刚创建的节点
|
||||||
// isFromKeyDown:是否是在按键事件进入的编辑
|
// isFromKeyDown:是否是在按键事件进入的编辑
|
||||||
@ -191,6 +219,11 @@ export default class TextEdit {
|
|||||||
if (node.isUseCustomNodeContent()) {
|
if (node.isUseCustomNodeContent()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 如果有正在编辑中的节点,那么先结束它
|
||||||
|
const currentEditNode = this.getCurrentEditNode()
|
||||||
|
if (currentEditNode) {
|
||||||
|
this.hideEditTextBox()
|
||||||
|
}
|
||||||
const { beforeTextEdit, openRealtimeRenderOnNodeTextEdit } =
|
const { beforeTextEdit, openRealtimeRenderOnNodeTextEdit } =
|
||||||
this.mindMap.opt
|
this.mindMap.opt
|
||||||
if (typeof beforeTextEdit === 'function') {
|
if (typeof beforeTextEdit === 'function') {
|
||||||
@ -203,10 +236,13 @@ export default class TextEdit {
|
|||||||
}
|
}
|
||||||
if (!isShow) return
|
if (!isShow) return
|
||||||
}
|
}
|
||||||
this.currentNode = node
|
|
||||||
const { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
|
const { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
|
||||||
this.mindMap.view.translateXY(offsetLeft, offsetTop)
|
this.mindMap.view.translateXY(offsetLeft, offsetTop)
|
||||||
const g = node._textData.node
|
const g = node._textData.node
|
||||||
|
// 需要先显示,不然宽高获取到的可能是0
|
||||||
|
if (openRealtimeRenderOnNodeTextEdit) {
|
||||||
|
g.show()
|
||||||
|
}
|
||||||
const rect = g.node.getBoundingClientRect()
|
const rect = g.node.getBoundingClientRect()
|
||||||
// 如果开启了大小实时更新,那么直接隐藏节点原文本
|
// 如果开启了大小实时更新,那么直接隐藏节点原文本
|
||||||
if (openRealtimeRenderOnNodeTextEdit) {
|
if (openRealtimeRenderOnNodeTextEdit) {
|
||||||
@ -223,6 +259,7 @@ export default class TextEdit {
|
|||||||
this.mindMap.richText.showEditText(params)
|
this.mindMap.richText.showEditText(params)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.currentNode = node
|
||||||
this.showEditTextBox(params)
|
this.showEditTextBox(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +288,7 @@ export default class TextEdit {
|
|||||||
this.mindMap.richText.showTextEdit = false
|
this.mindMap.richText.showTextEdit = false
|
||||||
} else {
|
} else {
|
||||||
this.cacheEditingText = this.getEditText()
|
this.cacheEditingText = this.getEditText()
|
||||||
this.showTextEdit = false
|
this.setIsShowTextEdit(false)
|
||||||
}
|
}
|
||||||
this.show({
|
this.show({
|
||||||
node,
|
node,
|
||||||
@ -275,9 +312,7 @@ export default class TextEdit {
|
|||||||
this.registerTmpShortcut()
|
this.registerTmpShortcut()
|
||||||
if (!this.textEditNode) {
|
if (!this.textEditNode) {
|
||||||
this.textEditNode = document.createElement('div')
|
this.textEditNode = document.createElement('div')
|
||||||
this.textEditNode.classList.add(
|
this.textEditNode.classList.add(SMM_NODE_EDIT_WRAP)
|
||||||
CONSTANTS.EDIT_NODE_CLASS.SMM_NODE_EDIT_WRAP
|
|
||||||
)
|
|
||||||
this.textEditNode.style.cssText = `
|
this.textEditNode.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -317,13 +352,10 @@ export default class TextEdit {
|
|||||||
} else {
|
} else {
|
||||||
handleInputPasteText(e)
|
handleInputPasteText(e)
|
||||||
}
|
}
|
||||||
|
this.emitTextChangeEvent()
|
||||||
})
|
})
|
||||||
this.textEditNode.addEventListener('input', () => {
|
this.textEditNode.addEventListener('input', () => {
|
||||||
this.mindMap.emit('node_text_edit_change', {
|
this.emitTextChangeEvent()
|
||||||
node: this.currentNode,
|
|
||||||
text: this.getEditText(),
|
|
||||||
richText: false
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
const targetNode =
|
const targetNode =
|
||||||
this.mindMap.opt.customInnerElsAppendTo || document.body
|
this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||||
@ -350,8 +382,8 @@ export default class TextEdit {
|
|||||||
this.textEditNode.style.minWidth =
|
this.textEditNode.style.minWidth =
|
||||||
rect.width + this.textNodePaddingX * 2 + 'px'
|
rect.width + this.textNodePaddingX * 2 + 'px'
|
||||||
this.textEditNode.style.minHeight = rect.height + 'px'
|
this.textEditNode.style.minHeight = rect.height + 'px'
|
||||||
this.textEditNode.style.left = rect.left + 'px'
|
this.textEditNode.style.left = Math.floor(rect.left) + 'px'
|
||||||
this.textEditNode.style.top = rect.top + 'px'
|
this.textEditNode.style.top = Math.floor(rect.top) + 'px'
|
||||||
this.textEditNode.style.display = 'block'
|
this.textEditNode.style.display = 'block'
|
||||||
this.textEditNode.style.maxWidth = textAutoWrapWidth * scale + 'px'
|
this.textEditNode.style.maxWidth = textAutoWrapWidth * scale + 'px'
|
||||||
if (isMultiLine) {
|
if (isMultiLine) {
|
||||||
@ -362,7 +394,7 @@ export default class TextEdit {
|
|||||||
} else {
|
} else {
|
||||||
this.textEditNode.style.lineHeight = 'normal'
|
this.textEditNode.style.lineHeight = 'normal'
|
||||||
}
|
}
|
||||||
this.showTextEdit = true
|
this.setIsShowTextEdit(true)
|
||||||
// 选中文本
|
// 选中文本
|
||||||
// if (!this.cacheEditingText) {
|
// if (!this.cacheEditingText) {
|
||||||
// selectAllInput(this.textEditNode)
|
// selectAllInput(this.textEditNode)
|
||||||
@ -375,6 +407,15 @@ export default class TextEdit {
|
|||||||
this.cacheEditingText = ''
|
this.cacheEditingText = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 派发节点文本编辑事件
|
||||||
|
emitTextChangeEvent() {
|
||||||
|
this.mindMap.emit('node_text_edit_change', {
|
||||||
|
node: this.currentNode,
|
||||||
|
text: this.getEditText(),
|
||||||
|
richText: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 更新文本编辑框的大小和位置
|
// 更新文本编辑框的大小和位置
|
||||||
updateTextEditNode() {
|
updateTextEditNode() {
|
||||||
if (this.mindMap.richText) {
|
if (this.mindMap.richText) {
|
||||||
@ -389,8 +430,8 @@ export default class TextEdit {
|
|||||||
rect.width + this.textNodePaddingX * 2 + 'px'
|
rect.width + this.textNodePaddingX * 2 + 'px'
|
||||||
this.textEditNode.style.minHeight =
|
this.textEditNode.style.minHeight =
|
||||||
rect.height + this.textNodePaddingY * 2 + 'px'
|
rect.height + this.textNodePaddingY * 2 + 'px'
|
||||||
this.textEditNode.style.left = rect.left + 'px'
|
this.textEditNode.style.left = Math.floor(rect.left) + 'px'
|
||||||
this.textEditNode.style.top = rect.top + 'px'
|
this.textEditNode.style.top = Math.floor(rect.top) + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取编辑区域的背景填充
|
// 获取编辑区域的背景填充
|
||||||
@ -447,7 +488,7 @@ export default class TextEdit {
|
|||||||
this.textEditNode.style.fontSize = 'inherit'
|
this.textEditNode.style.fontSize = 'inherit'
|
||||||
this.textEditNode.style.fontWeight = 'normal'
|
this.textEditNode.style.fontWeight = 'normal'
|
||||||
this.textEditNode.style.transform = 'translateY(0)'
|
this.textEditNode.style.transform = 'translateY(0)'
|
||||||
this.showTextEdit = false
|
this.setIsShowTextEdit(false)
|
||||||
this.mindMap.execCommand('SET_NODE_TEXT', currentNode, text)
|
this.mindMap.execCommand('SET_NODE_TEXT', currentNode, text)
|
||||||
// if (currentNode.isGeneralization) {
|
// if (currentNode.isGeneralization) {
|
||||||
// // 概要节点
|
// // 概要节点
|
||||||
|
|||||||
@ -8,13 +8,10 @@ import nodeCreateContentsMethods from './nodeCreateContents'
|
|||||||
import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect'
|
import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect'
|
||||||
import nodeModifyWidthMethods from './nodeModifyWidth'
|
import nodeModifyWidthMethods from './nodeModifyWidth'
|
||||||
import nodeCooperateMethods from './nodeCooperate'
|
import nodeCooperateMethods from './nodeCooperate'
|
||||||
|
import quickCreateChildBtnMethods from './quickCreateChildBtn'
|
||||||
|
import nodeLayoutMethods from './nodeLayout'
|
||||||
import { CONSTANTS } from '../../../constants/constant'
|
import { CONSTANTS } from '../../../constants/constant'
|
||||||
import {
|
import { copyNodeTree, createUid, addXmlns } from '../../../utils/index'
|
||||||
copyNodeTree,
|
|
||||||
createForeignObjectNode,
|
|
||||||
createUid,
|
|
||||||
addXmlns
|
|
||||||
} from '../../../utils/index'
|
|
||||||
|
|
||||||
// 节点类
|
// 节点类
|
||||||
class MindMapNode {
|
class MindMapNode {
|
||||||
@ -102,20 +99,16 @@ class MindMapNode {
|
|||||||
this._generalizationList = []
|
this._generalizationList = []
|
||||||
this._unVisibleRectRegionNode = null
|
this._unVisibleRectRegionNode = null
|
||||||
this._isMouseenter = false
|
this._isMouseenter = false
|
||||||
|
this._customContentAddToNodeAdd = null
|
||||||
// 尺寸信息
|
// 尺寸信息
|
||||||
this._rectInfo = {
|
this._rectInfo = {
|
||||||
imgContentWidth: 0,
|
|
||||||
imgContentHeight: 0,
|
|
||||||
textContentWidth: 0,
|
textContentWidth: 0,
|
||||||
textContentHeight: 0
|
textContentHeight: 0,
|
||||||
|
textContentWidthWithoutTag: 0
|
||||||
}
|
}
|
||||||
// 概要节点的宽高
|
// 概要节点的宽高
|
||||||
this._generalizationNodeWidth = 0
|
this._generalizationNodeWidth = 0
|
||||||
this._generalizationNodeHeight = 0
|
this._generalizationNodeHeight = 0
|
||||||
// 各种文字信息的间距
|
|
||||||
this.textContentItemMargin = this.mindMap.opt.textContentMargin
|
|
||||||
// 图片和文字节点的间距
|
|
||||||
this.blockContentMargin = this.mindMap.opt.imgTextMargin
|
|
||||||
// 展开收缩按钮尺寸
|
// 展开收缩按钮尺寸
|
||||||
this.expandBtnSize = this.mindMap.opt.expandBtnSize
|
this.expandBtnSize = this.mindMap.opt.expandBtnSize
|
||||||
// 是否是多选节点
|
// 是否是多选节点
|
||||||
@ -126,6 +119,10 @@ class MindMapNode {
|
|||||||
this.isHide = false
|
this.isHide = false
|
||||||
const proto = Object.getPrototypeOf(this)
|
const proto = Object.getPrototypeOf(this)
|
||||||
if (!proto.bindEvent) {
|
if (!proto.bindEvent) {
|
||||||
|
// 节点尺寸计算和布局相关方法
|
||||||
|
Object.keys(nodeLayoutMethods).forEach(item => {
|
||||||
|
proto[item] = nodeLayoutMethods[item]
|
||||||
|
})
|
||||||
// 概要相关方法
|
// 概要相关方法
|
||||||
Object.keys(nodeGeneralizationMethods).forEach(item => {
|
Object.keys(nodeGeneralizationMethods).forEach(item => {
|
||||||
proto[item] = nodeGeneralizationMethods[item]
|
proto[item] = nodeGeneralizationMethods[item]
|
||||||
@ -156,10 +153,19 @@ class MindMapNode {
|
|||||||
Object.keys(nodeModifyWidthMethods).forEach(item => {
|
Object.keys(nodeModifyWidthMethods).forEach(item => {
|
||||||
proto[item] = nodeModifyWidthMethods[item]
|
proto[item] = nodeModifyWidthMethods[item]
|
||||||
})
|
})
|
||||||
|
// 快捷创建子节点按钮
|
||||||
|
if (this.mindMap.opt.isShowCreateChildBtnIcon) {
|
||||||
|
Object.keys(quickCreateChildBtnMethods).forEach(item => {
|
||||||
|
proto[item] = quickCreateChildBtnMethods[item]
|
||||||
|
})
|
||||||
|
this.initQuickCreateChildBtn()
|
||||||
|
}
|
||||||
proto.bindEvent = true
|
proto.bindEvent = true
|
||||||
}
|
}
|
||||||
// 初始化
|
// 初始化
|
||||||
this.getSize()
|
this.getSize()
|
||||||
|
// 初始需要计算一下概要节点的大小,否则计算布局时获取不到概要的大小
|
||||||
|
this.updateGeneralization()
|
||||||
this.initDragHandle()
|
this.initDragHandle()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +217,8 @@ class MindMapNode {
|
|||||||
isUseCustomNodeContent,
|
isUseCustomNodeContent,
|
||||||
customCreateNodeContent,
|
customCreateNodeContent,
|
||||||
createNodePrefixContent,
|
createNodePrefixContent,
|
||||||
createNodePostfixContent
|
createNodePostfixContent,
|
||||||
|
addCustomContentToNode
|
||||||
} = this.mindMap.opt
|
} = this.mindMap.opt
|
||||||
// 需要创建的内容类型
|
// 需要创建的内容类型
|
||||||
const typeList = [
|
const typeList = [
|
||||||
@ -227,6 +234,9 @@ class MindMapNode {
|
|||||||
'postfix',
|
'postfix',
|
||||||
...this.mindMap.nodeInnerPrefixList.map(item => {
|
...this.mindMap.nodeInnerPrefixList.map(item => {
|
||||||
return item.name
|
return item.name
|
||||||
|
}),
|
||||||
|
...this.mindMap.nodeInnerPostfixList.map(item => {
|
||||||
|
return item.name
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
const createTypes = {}
|
const createTypes = {}
|
||||||
@ -284,6 +294,23 @@ class MindMapNode {
|
|||||||
addXmlns(this._postfixData.el)
|
addXmlns(this._postfixData.el)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.mindMap.nodeInnerPostfixList.forEach(item => {
|
||||||
|
if (createTypes[item.name]) {
|
||||||
|
this[`_${item.name}Data`] = item.createContent(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (
|
||||||
|
addCustomContentToNode &&
|
||||||
|
typeof addCustomContentToNode.create === 'function'
|
||||||
|
) {
|
||||||
|
this._customContentAddToNodeAdd = addCustomContentToNode.create(this)
|
||||||
|
if (
|
||||||
|
this._customContentAddToNodeAdd &&
|
||||||
|
this._customContentAddToNodeAdd.el
|
||||||
|
) {
|
||||||
|
addXmlns(this._customContentAddToNodeAdd.el)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算节点的宽高
|
// 计算节点的宽高
|
||||||
@ -305,330 +332,6 @@ class MindMapNode {
|
|||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算节点尺寸信息
|
|
||||||
getNodeRect() {
|
|
||||||
// 自定义节点内容
|
|
||||||
if (this.isUseCustomNodeContent()) {
|
|
||||||
const rect = this.measureCustomNodeContentSize(this._customNodeContent)
|
|
||||||
return {
|
|
||||||
width: this.hasCustomWidth() ? this.customTextWidth : rect.width,
|
|
||||||
height: rect.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { tagPosition } = this.mindMap.opt
|
|
||||||
const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM
|
|
||||||
// 宽高
|
|
||||||
let imgContentWidth = 0
|
|
||||||
let imgContentHeight = 0
|
|
||||||
let textContentWidth = 0
|
|
||||||
let textContentHeight = 0
|
|
||||||
let tagContentWidth = 0
|
|
||||||
let tagContentHeight = 0
|
|
||||||
// 存在图片
|
|
||||||
if (this._imgData) {
|
|
||||||
this._rectInfo.imgContentWidth = imgContentWidth = this._imgData.width
|
|
||||||
this._rectInfo.imgContentHeight = imgContentHeight = this._imgData.height
|
|
||||||
}
|
|
||||||
// 库前置内容
|
|
||||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
|
||||||
const itemData = this[`_${item.name}Data`]
|
|
||||||
if (itemData) {
|
|
||||||
textContentWidth += itemData.width
|
|
||||||
textContentHeight = Math.max(textContentHeight, itemData.height)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 自定义前置内容
|
|
||||||
if (this._prefixData) {
|
|
||||||
textContentWidth += this._prefixData.width
|
|
||||||
textContentHeight = Math.max(textContentHeight, this._prefixData.height)
|
|
||||||
}
|
|
||||||
// 图标
|
|
||||||
if (this._iconData.length > 0) {
|
|
||||||
textContentWidth += this._iconData.reduce((sum, cur) => {
|
|
||||||
textContentHeight = Math.max(textContentHeight, cur.height)
|
|
||||||
return (sum += cur.width + this.textContentItemMargin)
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
// 文字
|
|
||||||
if (this._textData) {
|
|
||||||
textContentWidth += this._textData.width
|
|
||||||
textContentHeight = Math.max(textContentHeight, this._textData.height)
|
|
||||||
}
|
|
||||||
// 超链接
|
|
||||||
if (this._hyperlinkData) {
|
|
||||||
textContentWidth += this._hyperlinkData.width
|
|
||||||
textContentHeight = Math.max(
|
|
||||||
textContentHeight,
|
|
||||||
this._hyperlinkData.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// 标签
|
|
||||||
if (this._tagData.length > 0) {
|
|
||||||
let maxTagHeight = 0
|
|
||||||
const totalTagWidth = this._tagData.reduce((sum, cur) => {
|
|
||||||
maxTagHeight = Math.max(maxTagHeight, cur.height)
|
|
||||||
return (sum += cur.width + this.textContentItemMargin)
|
|
||||||
}, 0)
|
|
||||||
if (tagIsBottom) {
|
|
||||||
// 文字下方
|
|
||||||
tagContentWidth = totalTagWidth
|
|
||||||
tagContentHeight = maxTagHeight
|
|
||||||
} else {
|
|
||||||
// 否则在右侧
|
|
||||||
textContentWidth += totalTagWidth
|
|
||||||
textContentHeight = Math.max(textContentHeight, maxTagHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 备注
|
|
||||||
if (this._noteData) {
|
|
||||||
textContentWidth += this._noteData.width
|
|
||||||
textContentHeight = Math.max(textContentHeight, this._noteData.height)
|
|
||||||
}
|
|
||||||
// 附件
|
|
||||||
if (this._attachmentData) {
|
|
||||||
textContentWidth += this._attachmentData.width
|
|
||||||
textContentHeight = Math.max(
|
|
||||||
textContentHeight,
|
|
||||||
this._attachmentData.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// 自定义后置内容
|
|
||||||
if (this._postfixData) {
|
|
||||||
textContentWidth += this._postfixData.width
|
|
||||||
textContentHeight = Math.max(textContentHeight, this._postfixData.height)
|
|
||||||
}
|
|
||||||
// 文字内容部分的尺寸
|
|
||||||
this._rectInfo.textContentWidth = textContentWidth
|
|
||||||
this._rectInfo.textContentHeight = textContentHeight
|
|
||||||
// 间距
|
|
||||||
let margin =
|
|
||||||
imgContentHeight > 0 && textContentHeight > 0
|
|
||||||
? this.blockContentMargin
|
|
||||||
: 0
|
|
||||||
const { paddingX, paddingY } = this.getPaddingVale()
|
|
||||||
// 纯内容宽高
|
|
||||||
let _width = Math.max(imgContentWidth, textContentWidth)
|
|
||||||
let _height = imgContentHeight + textContentHeight
|
|
||||||
// 如果标签在文字下方
|
|
||||||
if (tagIsBottom && tagContentHeight > 0 && textContentHeight > 0) {
|
|
||||||
// 那么文字和标签之间也需要间距
|
|
||||||
margin += this.blockContentMargin
|
|
||||||
// 整体高度要考虑标签宽度
|
|
||||||
_width = Math.max(_width, tagContentWidth)
|
|
||||||
// 整体高度要加上标签的高度
|
|
||||||
_height += tagContentHeight
|
|
||||||
}
|
|
||||||
// 计算节点形状需要的附加内边距
|
|
||||||
const { paddingX: shapePaddingX, paddingY: shapePaddingY } =
|
|
||||||
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 + borderWidth,
|
|
||||||
height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定位节点内容
|
|
||||||
layout() {
|
|
||||||
if (!this.group) return
|
|
||||||
// 清除之前的内容
|
|
||||||
this.group.clear()
|
|
||||||
const { hoverRectPadding, tagPosition, openRealtimeRenderOnNodeTextEdit } =
|
|
||||||
this.mindMap.opt
|
|
||||||
let { width, height, textContentItemMargin } = this
|
|
||||||
let { paddingY } = this.getPaddingVale()
|
|
||||||
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.style.shape(this.shapeNode)
|
|
||||||
this.group.add(this.shapeNode)
|
|
||||||
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
|
|
||||||
this.renderExpandBtnPlaceholderRect()
|
|
||||||
// 创建协同头像节点
|
|
||||||
if (this.createUserListNode) this.createUserListNode()
|
|
||||||
// 概要节点添加一个带所属节点id的类名
|
|
||||||
if (this.isGeneralization && this.generalizationBelongNode) {
|
|
||||||
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
|
|
||||||
}
|
|
||||||
// 激活hover和激活边框
|
|
||||||
const addHoverNode = () => {
|
|
||||||
this.hoverNode = new Rect()
|
|
||||||
.size(width + hoverRectPadding * 2, height + hoverRectPadding * 2)
|
|
||||||
.x(-hoverRectPadding)
|
|
||||||
.y(-hoverRectPadding)
|
|
||||||
this.hoverNode.addClass('smm-hover-node')
|
|
||||||
this.style.hoverNode(this.hoverNode, width, height)
|
|
||||||
this.group.add(this.hoverNode)
|
|
||||||
}
|
|
||||||
// 如果存在自定义节点内容,那么使用自定义节点内容
|
|
||||||
if (this.isUseCustomNodeContent()) {
|
|
||||||
const foreignObject = createForeignObjectNode({
|
|
||||||
el: this._customNodeContent,
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
})
|
|
||||||
this.group.add(foreignObject)
|
|
||||||
addHoverNode()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM
|
|
||||||
const { textContentHeight } = this._rectInfo
|
|
||||||
// 图片节点
|
|
||||||
let imgHeight = 0
|
|
||||||
if (this._imgData) {
|
|
||||||
imgHeight = this._imgData.height
|
|
||||||
this.group.add(this._imgData.node)
|
|
||||||
this._imgData.node.cx(width / 2).y(paddingY)
|
|
||||||
}
|
|
||||||
// 内容节点
|
|
||||||
let textContentNested = new G()
|
|
||||||
let textContentOffsetX = 0
|
|
||||||
// 库前置内容
|
|
||||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
|
||||||
const itemData = this[`_${item.name}Data`]
|
|
||||||
if (itemData) {
|
|
||||||
itemData.node
|
|
||||||
.x(textContentOffsetX)
|
|
||||||
.y((textContentHeight - itemData.height) / 2)
|
|
||||||
textContentNested.add(itemData.node)
|
|
||||||
textContentOffsetX += itemData.width + textContentItemMargin
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 自定义前置内容
|
|
||||||
if (this._prefixData) {
|
|
||||||
const foreignObject = createForeignObjectNode({
|
|
||||||
el: this._prefixData.el,
|
|
||||||
width: this._prefixData.width,
|
|
||||||
height: this._prefixData.height
|
|
||||||
})
|
|
||||||
foreignObject
|
|
||||||
.x(textContentOffsetX)
|
|
||||||
.y((textContentHeight - this._prefixData.height) / 2)
|
|
||||||
textContentNested.add(foreignObject)
|
|
||||||
textContentOffsetX += this._prefixData.width + textContentItemMargin
|
|
||||||
}
|
|
||||||
// icon
|
|
||||||
let iconNested = new G()
|
|
||||||
if (this._iconData && this._iconData.length > 0) {
|
|
||||||
let iconLeft = 0
|
|
||||||
this._iconData.forEach(item => {
|
|
||||||
item.node
|
|
||||||
.x(textContentOffsetX + iconLeft)
|
|
||||||
.y((textContentHeight - item.height) / 2)
|
|
||||||
iconNested.add(item.node)
|
|
||||||
iconLeft += item.width + textContentItemMargin
|
|
||||||
})
|
|
||||||
textContentNested.add(iconNested)
|
|
||||||
textContentOffsetX += iconLeft
|
|
||||||
}
|
|
||||||
// 文字
|
|
||||||
if (this._textData) {
|
|
||||||
const oldX = this._textData.node.attr('data-offsetx') || 0
|
|
||||||
this._textData.node.attr('data-offsetx', textContentOffsetX)
|
|
||||||
// 修复safari浏览器节点存在图标时文字位置不正确的问题
|
|
||||||
;(this._textData.nodeContent || this._textData.node)
|
|
||||||
.x(-oldX) // 修复非富文本模式下同时存在图标和换行的文本时,被收起和展开时图标与文字距离会逐渐拉大的问题
|
|
||||||
.x(textContentOffsetX)
|
|
||||||
.y((textContentHeight - this._textData.height) / 2)
|
|
||||||
// 如果开启了文本编辑实时渲染,需要判断当前渲染的节点是否是正在编辑的节点,是的话将透明度设置为0不显示
|
|
||||||
if (openRealtimeRenderOnNodeTextEdit) {
|
|
||||||
this._textData.node.opacity(
|
|
||||||
this.mindMap.renderer.textEdit.getCurrentEditNode() === this ? 0 : 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
textContentNested.add(this._textData.node)
|
|
||||||
textContentOffsetX += this._textData.width + textContentItemMargin
|
|
||||||
}
|
|
||||||
// 超链接
|
|
||||||
if (this._hyperlinkData) {
|
|
||||||
this._hyperlinkData.node
|
|
||||||
.x(textContentOffsetX)
|
|
||||||
.y((textContentHeight - this._hyperlinkData.height) / 2)
|
|
||||||
textContentNested.add(this._hyperlinkData.node)
|
|
||||||
textContentOffsetX += this._hyperlinkData.width + textContentItemMargin
|
|
||||||
}
|
|
||||||
// 标签
|
|
||||||
let tagNested = new G()
|
|
||||||
if (this._tagData && this._tagData.length > 0) {
|
|
||||||
if (tagIsBottom) {
|
|
||||||
// 标签显示在文字下方
|
|
||||||
let tagLeft = 0
|
|
||||||
this._tagData.forEach(item => {
|
|
||||||
item.node.x(tagLeft).y(0)
|
|
||||||
tagNested.add(item.node)
|
|
||||||
tagLeft += item.width + textContentItemMargin
|
|
||||||
})
|
|
||||||
tagNested.cx(width / 2).y(
|
|
||||||
paddingY + // 内边距
|
|
||||||
imgHeight + // 图片高度
|
|
||||||
textContentHeight + // 文本区域高度
|
|
||||||
(imgHeight > 0 && textContentHeight > 0
|
|
||||||
? this.blockContentMargin
|
|
||||||
: 0) + // 图片和文本之间的间距
|
|
||||||
this.blockContentMargin // 标签和文本之间的间距
|
|
||||||
)
|
|
||||||
this.group.add(tagNested)
|
|
||||||
} else {
|
|
||||||
// 标签显示在文字右侧
|
|
||||||
let tagLeft = 0
|
|
||||||
this._tagData.forEach(item => {
|
|
||||||
item.node
|
|
||||||
.x(textContentOffsetX + tagLeft)
|
|
||||||
.y((textContentHeight - item.height) / 2)
|
|
||||||
tagNested.add(item.node)
|
|
||||||
tagLeft += item.width + textContentItemMargin
|
|
||||||
})
|
|
||||||
textContentNested.add(tagNested)
|
|
||||||
textContentOffsetX += tagLeft
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 备注
|
|
||||||
if (this._noteData) {
|
|
||||||
this._noteData.node
|
|
||||||
.x(textContentOffsetX)
|
|
||||||
.y((textContentHeight - this._noteData.height) / 2)
|
|
||||||
textContentNested.add(this._noteData.node)
|
|
||||||
textContentOffsetX += this._noteData.width
|
|
||||||
}
|
|
||||||
// 附件
|
|
||||||
if (this._attachmentData) {
|
|
||||||
this._attachmentData.node
|
|
||||||
.x(textContentOffsetX)
|
|
||||||
.y((textContentHeight - this._attachmentData.height) / 2)
|
|
||||||
textContentNested.add(this._attachmentData.node)
|
|
||||||
textContentOffsetX += this._attachmentData.width
|
|
||||||
}
|
|
||||||
// 自定义后置内容
|
|
||||||
if (this._postfixData) {
|
|
||||||
const foreignObject = createForeignObjectNode({
|
|
||||||
el: this._postfixData.el,
|
|
||||||
width: this._postfixData.width,
|
|
||||||
height: this._postfixData.height
|
|
||||||
})
|
|
||||||
foreignObject
|
|
||||||
.x(textContentOffsetX)
|
|
||||||
.y((textContentHeight - this._postfixData.height) / 2)
|
|
||||||
textContentNested.add(foreignObject)
|
|
||||||
textContentOffsetX += this._postfixData.width
|
|
||||||
}
|
|
||||||
this.group.add(textContentNested)
|
|
||||||
// 文字内容整体
|
|
||||||
textContentNested.translate(
|
|
||||||
width / 2 - textContentNested.bbox().width / 2,
|
|
||||||
paddingY + // 内边距
|
|
||||||
imgHeight + // 图片高度
|
|
||||||
(imgHeight > 0 && textContentHeight > 0 ? this.blockContentMargin : 0) // 和图片的间距
|
|
||||||
)
|
|
||||||
addHoverNode()
|
|
||||||
this.mindMap.emit('node_layout_end', this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 给节点绑定事件
|
// 给节点绑定事件
|
||||||
bindGroupEvent() {
|
bindGroupEvent() {
|
||||||
// 单击事件,选中节点
|
// 单击事件,选中节点
|
||||||
@ -780,10 +483,15 @@ class MindMapNode {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.updateNodeActiveClass()
|
this.updateNodeActiveClass()
|
||||||
const { alwaysShowExpandBtn, notShowExpandBtn } = this.mindMap.opt
|
const {
|
||||||
|
alwaysShowExpandBtn,
|
||||||
|
notShowExpandBtn,
|
||||||
|
isShowCreateChildBtnIcon,
|
||||||
|
readonly
|
||||||
|
} = this.mindMap.opt
|
||||||
|
const childrenLength = this.getChildrenLength()
|
||||||
// 不显示展开收起按钮则不需要处理
|
// 不显示展开收起按钮则不需要处理
|
||||||
if (!notShowExpandBtn) {
|
if (!notShowExpandBtn) {
|
||||||
const childrenLength = this.nodeData.children.length
|
|
||||||
if (alwaysShowExpandBtn) {
|
if (alwaysShowExpandBtn) {
|
||||||
// 需要移除展开收缩按钮
|
// 需要移除展开收缩按钮
|
||||||
if (this._expandBtn && childrenLength <= 0) {
|
if (this._expandBtn && childrenLength <= 0) {
|
||||||
@ -804,6 +512,19 @@ class MindMapNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 更新快速创建子节点按钮
|
||||||
|
if (isShowCreateChildBtnIcon) {
|
||||||
|
if (childrenLength > 0) {
|
||||||
|
this.removeQuickCreateChildBtn()
|
||||||
|
} else {
|
||||||
|
const { isActive } = this.getData()
|
||||||
|
if (isActive) {
|
||||||
|
this.showQuickCreateChildBtn()
|
||||||
|
} else {
|
||||||
|
this.hideQuickCreateChildBtn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// 更新拖拽手柄的显示与否
|
// 更新拖拽手柄的显示与否
|
||||||
this.updateDragHandle()
|
this.updateDragHandle()
|
||||||
// 更新概要
|
// 更新概要
|
||||||
@ -813,7 +534,7 @@ class MindMapNode {
|
|||||||
// 更新节点位置
|
// 更新节点位置
|
||||||
const t = this.group.transform()
|
const t = this.group.transform()
|
||||||
// 保存一份当前节点数据快照
|
// 保存一份当前节点数据快照
|
||||||
this.nodeDataSnapshot = JSON.stringify(this.getData())
|
this.nodeDataSnapshot = readonly ? '' : JSON.stringify(this.getData())
|
||||||
// 节点位置变化才更新,因为即使值没有变化属性设置操作也是耗时的
|
// 节点位置变化才更新,因为即使值没有变化属性设置操作也是耗时的
|
||||||
if (this.left !== t.translateX || this.top !== t.translateY) {
|
if (this.left !== t.translateX || this.top !== t.translateY) {
|
||||||
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
||||||
@ -861,11 +582,18 @@ class MindMapNode {
|
|||||||
// 根据是否激活更新节点
|
// 根据是否激活更新节点
|
||||||
updateNodeByActive(active) {
|
updateNodeByActive(active) {
|
||||||
if (this.group) {
|
if (this.group) {
|
||||||
|
const { isShowCreateChildBtnIcon } = this.mindMap.opt
|
||||||
// 切换激活状态,需要切换展开收起按钮的显隐
|
// 切换激活状态,需要切换展开收起按钮的显隐
|
||||||
if (active) {
|
if (active) {
|
||||||
this.showExpandBtn()
|
this.showExpandBtn()
|
||||||
|
if (isShowCreateChildBtnIcon) {
|
||||||
|
this.showQuickCreateChildBtn()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.hideExpandBtn()
|
this.hideExpandBtn()
|
||||||
|
if (isShowCreateChildBtnIcon) {
|
||||||
|
this.hideQuickCreateChildBtn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.updateNodeActiveClass()
|
this.updateNodeActiveClass()
|
||||||
this.updateDragHandle()
|
this.updateDragHandle()
|
||||||
@ -1088,14 +816,13 @@ class MindMapNode {
|
|||||||
if (this.getData('expand') === false) {
|
if (this.getData('expand') === false) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let childrenLen = this.nodeData.children.length
|
let childrenLen = this.getChildrenLength()
|
||||||
// 切换为鱼骨结构时,清空根节点和二级节点的连线
|
// 切换为鱼骨结构时,清空根节点和二级节点的连线
|
||||||
if (
|
if (this.mindMap.renderer.layout.nodeIsRemoveAllLines) {
|
||||||
this.mindMap.opt.layout === CONSTANTS.LAYOUT.FISHBONE &&
|
if (this.mindMap.renderer.layout.nodeIsRemoveAllLines(this)) {
|
||||||
(this.isRoot || this.layerIndex === 1)
|
|
||||||
) {
|
|
||||||
childrenLen = 0
|
childrenLen = 0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (childrenLen > this._lines.length) {
|
if (childrenLen > this._lines.length) {
|
||||||
// 创建缺少的线
|
// 创建缺少的线
|
||||||
new Array(childrenLen - this._lines.length).fill(0).forEach(() => {
|
new Array(childrenLen - this._lines.length).fill(0).forEach(() => {
|
||||||
@ -1170,15 +897,18 @@ class MindMapNode {
|
|||||||
|
|
||||||
// 设置连线样式
|
// 设置连线样式
|
||||||
styleLine(line, childNode, enableMarker) {
|
styleLine(line, childNode, enableMarker) {
|
||||||
|
const { enableInheritAncestorLineStyle } = this.mindMap.opt
|
||||||
|
const getName = enableInheritAncestorLineStyle
|
||||||
|
? 'getSelfInhertStyle'
|
||||||
|
: 'getSelfStyle'
|
||||||
const width =
|
const width =
|
||||||
childNode.getSelfInhertStyle('lineWidth') ||
|
childNode[getName]('lineWidth') || childNode.getStyle('lineWidth', true)
|
||||||
childNode.getStyle('lineWidth', true)
|
|
||||||
const color =
|
const color =
|
||||||
childNode.getSelfInhertStyle('lineColor') ||
|
childNode[getName]('lineColor') ||
|
||||||
this.getRainbowLineColor(childNode) ||
|
this.getRainbowLineColor(childNode) ||
|
||||||
childNode.getStyle('lineColor', true)
|
childNode.getStyle('lineColor', true)
|
||||||
const dasharray =
|
const dasharray =
|
||||||
childNode.getSelfInhertStyle('lineDasharray') ||
|
childNode[getName]('lineDasharray') ||
|
||||||
childNode.getStyle('lineDasharray', true)
|
childNode.getStyle('lineDasharray', true)
|
||||||
this.style.line(
|
this.style.line(
|
||||||
line,
|
line,
|
||||||
@ -1405,6 +1135,11 @@ class MindMapNode {
|
|||||||
this.customTextWidth !== undefined
|
this.customTextWidth !== undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取子节点的数量
|
||||||
|
getChildrenLength() {
|
||||||
|
return this.nodeData.children ? this.nodeData.children.length : 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MindMapNode
|
export default MindMapNode
|
||||||
|
|||||||
@ -52,7 +52,22 @@ export default class Shape {
|
|||||||
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
|
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
|
||||||
paddingY: actHeight < actWidth ? actOffset / 2 : 0
|
paddingY: actHeight < actWidth ? actOffset / 2 : 0
|
||||||
}
|
}
|
||||||
default:
|
}
|
||||||
|
const extendShape = this.getShapeFromExtendList(shape)
|
||||||
|
if (extendShape) {
|
||||||
|
return (
|
||||||
|
extendShape.getPadding({
|
||||||
|
node: this.node,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
paddingX,
|
||||||
|
paddingY
|
||||||
|
}) || {
|
||||||
|
paddingX: 0,
|
||||||
|
paddingY: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
return {
|
return {
|
||||||
paddingX: 0,
|
paddingX: 0,
|
||||||
paddingY: 0
|
paddingY: 0
|
||||||
@ -60,6 +75,13 @@ export default class Shape {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从形状扩展列表里获取指定名称的形状
|
||||||
|
getShapeFromExtendList(shape) {
|
||||||
|
return this.mindMap.extendShapeList.find(item => {
|
||||||
|
return item.name === shape
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 创建形状节点
|
// 创建形状节点
|
||||||
createShape() {
|
createShape() {
|
||||||
const shape = this.node.getShape()
|
const shape = this.node.getShape()
|
||||||
@ -92,7 +114,13 @@ export default class Shape {
|
|||||||
// 圆
|
// 圆
|
||||||
node = this.createCircle()
|
node = this.createCircle()
|
||||||
}
|
}
|
||||||
return node
|
if (!node) {
|
||||||
|
const extendShape = this.getShapeFromExtendList(shape)
|
||||||
|
if (extendShape) {
|
||||||
|
node = extendShape.createShape(this.node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node || this.createRect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取节点减去节点边框宽度、hover节点边框宽度后的尺寸
|
// 获取节点减去节点边框宽度、hover节点边框宽度后的尺寸
|
||||||
|
|||||||
@ -8,6 +8,18 @@ const backgroundStyleProps = [
|
|||||||
'backgroundSize'
|
'backgroundSize'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const shapeStyleProps = [
|
||||||
|
'gradientStyle',
|
||||||
|
'startColor',
|
||||||
|
'endColor',
|
||||||
|
'startDir',
|
||||||
|
'endDir',
|
||||||
|
'fillColor',
|
||||||
|
'borderColor',
|
||||||
|
'borderWidth',
|
||||||
|
'borderDasharray'
|
||||||
|
]
|
||||||
|
|
||||||
// 样式类
|
// 样式类
|
||||||
class Style {
|
class Style {
|
||||||
// 设置背景样式
|
// 设置背景样式
|
||||||
@ -112,6 +124,8 @@ class Style {
|
|||||||
|
|
||||||
// 更新当前节点生效的样式数据
|
// 更新当前节点生效的样式数据
|
||||||
addToEffectiveStyles(styles) {
|
addToEffectiveStyles(styles) {
|
||||||
|
// effectiveStyles目前只提供给格式刷插件使用,所以如果没有注册该插件,那么不需要保存该数据
|
||||||
|
if (!this.ctx.mindMap.painter) return
|
||||||
this.ctx.effectiveStyles = {
|
this.ctx.effectiveStyles = {
|
||||||
...this.ctx.effectiveStyles,
|
...this.ctx.effectiveStyles,
|
||||||
...styles
|
...styles
|
||||||
@ -126,17 +140,10 @@ class Style {
|
|||||||
|
|
||||||
// 形状
|
// 形状
|
||||||
shape(node) {
|
shape(node) {
|
||||||
const styles = {
|
const styles = {}
|
||||||
gradientStyle: this.merge('gradientStyle'),
|
shapeStyleProps.forEach(key => {
|
||||||
startColor: this.merge('startColor'),
|
styles[key] = this.merge(key)
|
||||||
endColor: this.merge('endColor'),
|
})
|
||||||
startDir: this.merge('startDir'),
|
|
||||||
endDir: this.merge('endDir'),
|
|
||||||
fillColor: this.merge('fillColor'),
|
|
||||||
borderColor: this.merge('borderColor'),
|
|
||||||
borderWidth: this.merge('borderWidth'),
|
|
||||||
borderDasharray: this.merge('borderDasharray')
|
|
||||||
}
|
|
||||||
if (styles.gradientStyle) {
|
if (styles.gradientStyle) {
|
||||||
if (!this._gradient) {
|
if (!this._gradient) {
|
||||||
this._gradient = this.ctx.nodeDraw.gradient('linear')
|
this._gradient = this.ctx.nodeDraw.gradient('linear')
|
||||||
@ -191,45 +198,6 @@ class Style {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成内联样式
|
|
||||||
createStyleText(customStyle = {}) {
|
|
||||||
const styles = {
|
|
||||||
color: this.merge('color'),
|
|
||||||
fontFamily: this.merge('fontFamily'),
|
|
||||||
fontSize: this.merge('fontSize'),
|
|
||||||
fontWeight: this.merge('fontWeight'),
|
|
||||||
fontStyle: this.merge('fontStyle'),
|
|
||||||
textDecoration: this.merge('textDecoration'),
|
|
||||||
...customStyle
|
|
||||||
}
|
|
||||||
return `
|
|
||||||
color: ${styles.color};
|
|
||||||
font-family: ${styles.fontFamily};
|
|
||||||
font-size: ${styles.fontSize + 'px'};
|
|
||||||
font-weight: ${styles.fontWeight};
|
|
||||||
font-style: ${styles.fontStyle};
|
|
||||||
text-decoration: ${styles.textDecoration}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文本样式
|
|
||||||
getTextFontStyle() {
|
|
||||||
const styles = {
|
|
||||||
color: this.merge('color'),
|
|
||||||
fontFamily: this.merge('fontFamily'),
|
|
||||||
fontSize: this.merge('fontSize'),
|
|
||||||
fontWeight: this.merge('fontWeight'),
|
|
||||||
fontStyle: this.merge('fontStyle'),
|
|
||||||
textDecoration: this.merge('textDecoration')
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
italic: styles.fontStyle === 'italic',
|
|
||||||
bold: styles.fontWeight,
|
|
||||||
fontSize: styles.fontSize,
|
|
||||||
fontFamily: styles.fontFamily
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// html文字节点
|
// html文字节点
|
||||||
domText(node, fontSizeScale = 1) {
|
domText(node, fontSizeScale = 1) {
|
||||||
const styles = {
|
const styles = {
|
||||||
@ -238,7 +206,8 @@ class Style {
|
|||||||
fontSize: this.merge('fontSize'),
|
fontSize: this.merge('fontSize'),
|
||||||
fontWeight: this.merge('fontWeight'),
|
fontWeight: this.merge('fontWeight'),
|
||||||
fontStyle: this.merge('fontStyle'),
|
fontStyle: this.merge('fontStyle'),
|
||||||
textDecoration: this.merge('textDecoration')
|
textDecoration: this.merge('textDecoration'),
|
||||||
|
textAlign: this.merge('textAlign')
|
||||||
}
|
}
|
||||||
node.style.color = styles.color
|
node.style.color = styles.color
|
||||||
node.style.textDecoration = styles.textDecoration
|
node.style.textDecoration = styles.textDecoration
|
||||||
@ -246,6 +215,7 @@ class Style {
|
|||||||
node.style.fontSize = styles.fontSize * fontSizeScale + 'px'
|
node.style.fontSize = styles.fontSize * fontSizeScale + 'px'
|
||||||
node.style.fontWeight = styles.fontWeight || 'normal'
|
node.style.fontWeight = styles.fontWeight || 'normal'
|
||||||
node.style.fontStyle = styles.fontStyle
|
node.style.fontStyle = styles.fontStyle
|
||||||
|
node.style.textAlign = styles.textAlign
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标签文字
|
// 标签文字
|
||||||
@ -270,9 +240,9 @@ class Style {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 内置图标
|
// 内置图标
|
||||||
iconNode(node) {
|
iconNode(node, color) {
|
||||||
node.attr({
|
node.attr({
|
||||||
fill: this.merge('color')
|
fill: color || this.merge('color')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
resizeImgSize,
|
resizeImgSize,
|
||||||
removeHtmlStyle,
|
removeRichTextStyes,
|
||||||
addHtmlStyle,
|
|
||||||
checkIsRichText,
|
checkIsRichText,
|
||||||
isUndef,
|
isUndef,
|
||||||
createForeignObjectNode,
|
createForeignObjectNode,
|
||||||
addXmlns,
|
addXmlns,
|
||||||
generateColorByContent
|
generateColorByContent,
|
||||||
|
camelCaseToHyphen,
|
||||||
|
getNodeRichTextStyles
|
||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
import { Image as SVGImage, SVG, A, G, Rect, Text } from '@svgdotjs/svg.js'
|
import { Image as SVGImage, SVG, A, G, Rect, Text } from '@svgdotjs/svg.js'
|
||||||
import iconsSvg from '../../../svg/icons'
|
import iconsSvg from '../../../svg/icons'
|
||||||
import {
|
import { noneRichTextNodeLineHeight } from '../../../constants/constant'
|
||||||
CONSTANTS,
|
|
||||||
noneRichTextNodeLineHeight
|
|
||||||
} from '../../../constants/constant'
|
|
||||||
|
|
||||||
// 测量svg文本宽高
|
// 测量svg文本宽高
|
||||||
const measureText = (text, style) => {
|
const measureText = (text, style) => {
|
||||||
@ -34,12 +32,20 @@ const defaultTagStyle = {
|
|||||||
//width: 30 // 标签矩形的宽度,如果不设置,默认以文字的宽度+paddingX*2为宽度
|
//width: 30 // 标签矩形的宽度,如果不设置,默认以文字的宽度+paddingX*2为宽度
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取图片的真实url
|
||||||
|
// 因为如果注册了NodeBase64ImageStorage插件,那么节点图片字段保存的实际是一个id,所以如果要获取图片真实的url可以通过该方法
|
||||||
|
function getImageUrl() {
|
||||||
|
const img = this.getData('image')
|
||||||
|
return (this.mindMap.renderer.renderTree.data.imgMap || {})[img] || img
|
||||||
|
}
|
||||||
|
|
||||||
// 创建图片节点
|
// 创建图片节点
|
||||||
function createImgNode() {
|
function createImgNode() {
|
||||||
const img = this.getData('image')
|
const img = this.getImageUrl()
|
||||||
if (!img) {
|
if (!img) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
img = (this.mindMap.renderer.renderTree.data.imgMap || {})[img] || img
|
||||||
const imgSize = this.getImgShowSize()
|
const imgSize = this.getImgShowSize()
|
||||||
const node = new SVGImage().load(img).size(...imgSize)
|
const node = new SVGImage().load(img).size(...imgSize)
|
||||||
// 如果指定了加载失败显示的图片,那么加载一下图片检测是否失败
|
// 如果指定了加载失败显示的图片,那么加载一下图片检测是否失败
|
||||||
@ -54,8 +60,11 @@ function createImgNode() {
|
|||||||
if (this.getData('imageTitle')) {
|
if (this.getData('imageTitle')) {
|
||||||
node.attr('title', this.getData('imageTitle'))
|
node.attr('title', this.getData('imageTitle'))
|
||||||
}
|
}
|
||||||
|
node.on('click', e => {
|
||||||
|
this.mindMap.emit('node_img_click', this, node, e)
|
||||||
|
})
|
||||||
node.on('dblclick', e => {
|
node.on('dblclick', e => {
|
||||||
this.mindMap.emit('node_img_dblclick', this, e)
|
this.mindMap.emit('node_img_dblclick', this, e, node)
|
||||||
})
|
})
|
||||||
node.on('mouseenter', e => {
|
node.on('mouseenter', e => {
|
||||||
this.mindMap.emit('node_img_mouseenter', this, node, e)
|
this.mindMap.emit('node_img_mouseenter', this, node, e)
|
||||||
@ -124,20 +133,6 @@ function createIconNode() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试给html指定标签添加内联样式
|
|
||||||
function tryAddHtmlStyle(text, style) {
|
|
||||||
const tagList = ['span', 'strong', 's', 'em', 'u']
|
|
||||||
// let _text = text
|
|
||||||
// for (let i = 0; i < tagList.length; i++) {
|
|
||||||
// text = addHtmlStyle(text, tagList[i], style)
|
|
||||||
// if (text !== _text) {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return text
|
|
||||||
return addHtmlStyle(text, tagList, style)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建富文本节点
|
// 创建富文本节点
|
||||||
function createRichTextNode(specifyText) {
|
function createRichTextNode(specifyText) {
|
||||||
const hasCustomWidth = this.hasCustomWidth()
|
const hasCustomWidth = this.hasCustomWidth()
|
||||||
@ -145,40 +140,32 @@ function createRichTextNode(specifyText) {
|
|||||||
typeof specifyText === 'string' ? specifyText : this.getData('text')
|
typeof specifyText === 'string' ? specifyText : this.getData('text')
|
||||||
let { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt
|
let { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt
|
||||||
textAutoWrapWidth = hasCustomWidth ? this.customTextWidth : textAutoWrapWidth
|
textAutoWrapWidth = hasCustomWidth ? this.customTextWidth : textAutoWrapWidth
|
||||||
let g = new G()
|
const g = new G()
|
||||||
// 重新设置富文本节点内容
|
// 创建富文本结构,或复位富文本样式
|
||||||
let recoverText = false
|
let recoverText = false
|
||||||
if (this.getData('resetRichText')) {
|
if (this.getData('resetRichText')) {
|
||||||
delete this.nodeData.data.resetRichText
|
delete this.nodeData.data.resetRichText
|
||||||
recoverText = true
|
recoverText = true
|
||||||
}
|
}
|
||||||
if ([CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
|
|
||||||
// 如果自定义过样式则不允许覆盖
|
|
||||||
// if (!this.hasCustomStyle() ) {
|
|
||||||
recoverText = true
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
if (recoverText && !isUndef(text)) {
|
if (recoverText && !isUndef(text)) {
|
||||||
// 判断节点内容是否是富文本
|
if (checkIsRichText(text)) {
|
||||||
const isRichText = checkIsRichText(text)
|
// 如果是富文本那么移除内联样式
|
||||||
// 获取自定义样式
|
text = removeRichTextStyes(text)
|
||||||
const customStyle = this.style.getCustomStyle()
|
|
||||||
// 样式字符串
|
|
||||||
const style = this.style.createStyleText(customStyle)
|
|
||||||
if (isRichText) {
|
|
||||||
// 如果是富文本那么线移除内联样式
|
|
||||||
text = removeHtmlStyle(text)
|
|
||||||
// 再添加新的内联样式
|
|
||||||
text = this.tryAddHtmlStyle(text, style)
|
|
||||||
} else {
|
} else {
|
||||||
// 非富文本
|
// 非富文本则改为富文本结构
|
||||||
text = `<p><span style="${style}">${text}</span></p>`
|
text = `<p>${text}</p>`
|
||||||
}
|
}
|
||||||
this.setData({
|
this.setData({
|
||||||
text: text
|
text
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let html = `<div>${text}</div>`
|
// 节点的富文本样式数据
|
||||||
|
const nodeTextStyleList = []
|
||||||
|
const nodeRichTextStyles = getNodeRichTextStyles(this)
|
||||||
|
Object.keys(nodeRichTextStyles).forEach(prop => {
|
||||||
|
nodeTextStyleList.push([prop, nodeRichTextStyles[prop]])
|
||||||
|
})
|
||||||
|
// 测量文本大小
|
||||||
if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) {
|
if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) {
|
||||||
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl =
|
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl =
|
||||||
document.createElement('div')
|
document.createElement('div')
|
||||||
@ -190,9 +177,15 @@ function createRichTextNode(specifyText) {
|
|||||||
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
|
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
let div = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
|
const div = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
|
||||||
|
// 应用节点的文本样式
|
||||||
|
nodeTextStyleList.forEach(([prop, value]) => {
|
||||||
|
div.style[prop] = value
|
||||||
|
})
|
||||||
|
div.style.lineHeight = 1.2
|
||||||
|
const html = `<div>${text}</div>`
|
||||||
div.innerHTML = html
|
div.innerHTML = html
|
||||||
let el = div.children[0]
|
const el = div.children[0]
|
||||||
el.classList.add('smm-richtext-node-wrap')
|
el.classList.add('smm-richtext-node-wrap')
|
||||||
addXmlns(el)
|
addXmlns(el)
|
||||||
el.style.maxWidth = textAutoWrapWidth + 'px'
|
el.style.maxWidth = textAutoWrapWidth + 'px'
|
||||||
@ -219,6 +212,15 @@ function createRichTextNode(specifyText) {
|
|||||||
width,
|
width,
|
||||||
height
|
height
|
||||||
})
|
})
|
||||||
|
// 应用节点文本样式
|
||||||
|
// 进入文本编辑时,这个样式也会同样添加到文本编辑框的元素上
|
||||||
|
const foreignObjectStyle = {
|
||||||
|
'line-height': 1.2
|
||||||
|
}
|
||||||
|
nodeTextStyleList.forEach(([prop, value]) => {
|
||||||
|
foreignObjectStyle[camelCaseToHyphen(prop)] = value
|
||||||
|
})
|
||||||
|
foreignObject.css(foreignObjectStyle)
|
||||||
g.add(foreignObject)
|
g.add(foreignObject)
|
||||||
return {
|
return {
|
||||||
node: g,
|
node: g,
|
||||||
@ -230,6 +232,10 @@ function createRichTextNode(specifyText) {
|
|||||||
|
|
||||||
// 创建文本节点
|
// 创建文本节点
|
||||||
function createTextNode(specifyText) {
|
function createTextNode(specifyText) {
|
||||||
|
if (this.getData('needUpdate')) {
|
||||||
|
delete this.nodeData.data.needUpdate
|
||||||
|
}
|
||||||
|
// 如果是富文本内容,那么转给富文本函数
|
||||||
if (this.getData('richText')) {
|
if (this.getData('richText')) {
|
||||||
return this.createRichTextNode(specifyText)
|
return this.createRichTextNode(specifyText)
|
||||||
}
|
}
|
||||||
@ -238,8 +244,9 @@ function createTextNode(specifyText) {
|
|||||||
if (this.getData('resetRichText')) {
|
if (this.getData('resetRichText')) {
|
||||||
delete this.nodeData.data.resetRichText
|
delete this.nodeData.data.resetRichText
|
||||||
}
|
}
|
||||||
let g = new G()
|
const g = new G()
|
||||||
let fontSize = this.getStyle('fontSize', false)
|
const fontSize = this.getStyle('fontSize', false)
|
||||||
|
const textAlign = this.getStyle('textAlign', false)
|
||||||
// 文本超长自动换行
|
// 文本超长自动换行
|
||||||
let textArr = []
|
let textArr = []
|
||||||
if (!isUndef(text)) {
|
if (!isUndef(text)) {
|
||||||
@ -279,6 +286,14 @@ function createTextNode(specifyText) {
|
|||||||
}
|
}
|
||||||
const node = new Text().text(item)
|
const node = new Text().text(item)
|
||||||
node.addClass('smm-text-node-wrap')
|
node.addClass('smm-text-node-wrap')
|
||||||
|
node.attr(
|
||||||
|
'text-anchor',
|
||||||
|
{
|
||||||
|
left: 'start',
|
||||||
|
center: 'middle',
|
||||||
|
right: 'end'
|
||||||
|
}[textAlign] || 'start'
|
||||||
|
)
|
||||||
this.style.text(node)
|
this.style.text(node)
|
||||||
node.y(
|
node.y(
|
||||||
fontSize * noneRichTextNodeLineHeight * index +
|
fontSize * noneRichTextNodeLineHeight * index +
|
||||||
@ -308,15 +323,16 @@ function createTextNode(specifyText) {
|
|||||||
|
|
||||||
// 创建超链接节点
|
// 创建超链接节点
|
||||||
function createHyperlinkNode() {
|
function createHyperlinkNode() {
|
||||||
let { hyperlink, hyperlinkTitle } = this.getData()
|
const { hyperlink, hyperlinkTitle } = this.getData()
|
||||||
if (!hyperlink) {
|
if (!hyperlink) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { customHyperlinkJump } = this.mindMap.opt
|
const { customHyperlinkJump, hyperlinkIcon } = this.mindMap.opt
|
||||||
let iconSize = this.mindMap.themeConfig.iconSize
|
const { icon, style } = hyperlinkIcon
|
||||||
let node = new SVG().size(iconSize, iconSize)
|
const iconSize = this.getNodeIconSize('hyperlinkIcon')
|
||||||
|
const node = new SVG().size(iconSize, iconSize)
|
||||||
// 超链接节点
|
// 超链接节点
|
||||||
let a = new A().to(hyperlink).target('_blank')
|
const a = new A().to(hyperlink).target('_blank')
|
||||||
a.node.addEventListener('click', e => {
|
a.node.addEventListener('click', e => {
|
||||||
if (typeof customHyperlinkJump === 'function') {
|
if (typeof customHyperlinkJump === 'function') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -329,8 +345,8 @@ function createHyperlinkNode() {
|
|||||||
// 添加一个透明的层,作为鼠标区域
|
// 添加一个透明的层,作为鼠标区域
|
||||||
a.rect(iconSize, iconSize).fill({ color: 'transparent' })
|
a.rect(iconSize, iconSize).fill({ color: 'transparent' })
|
||||||
// 超链接图标
|
// 超链接图标
|
||||||
let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize)
|
const iconNode = SVG(icon || iconsSvg.hyperlink).size(iconSize, iconSize)
|
||||||
this.style.iconNode(iconNode)
|
this.style.iconNode(iconNode, style.color)
|
||||||
a.add(iconNode)
|
a.add(iconNode)
|
||||||
node.add(a)
|
node.add(a)
|
||||||
return {
|
return {
|
||||||
@ -415,16 +431,17 @@ function createNoteNode() {
|
|||||||
if (!this.getData('note')) {
|
if (!this.getData('note')) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let iconSize = this.mindMap.themeConfig.iconSize
|
const { icon, style } = this.mindMap.opt.noteIcon
|
||||||
let node = new SVG()
|
const iconSize = this.getNodeIconSize('noteIcon')
|
||||||
|
const node = new SVG()
|
||||||
.attr('cursor', 'pointer')
|
.attr('cursor', 'pointer')
|
||||||
.addClass('smm-node-note')
|
.addClass('smm-node-note')
|
||||||
.size(iconSize, iconSize)
|
.size(iconSize, iconSize)
|
||||||
// 透明的层,用来作为鼠标区域
|
// 透明的层,用来作为鼠标区域
|
||||||
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
|
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
|
||||||
// 备注图标
|
// 备注图标
|
||||||
let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize)
|
const iconNode = SVG(icon || iconsSvg.note).size(iconSize, iconSize)
|
||||||
this.style.iconNode(iconNode)
|
this.style.iconNode(iconNode, style.color)
|
||||||
node.add(iconNode)
|
node.add(iconNode)
|
||||||
// 备注tooltip
|
// 备注tooltip
|
||||||
if (!this.mindMap.opt.customNoteContentShow) {
|
if (!this.mindMap.opt.customNoteContentShow) {
|
||||||
@ -486,7 +503,8 @@ function createAttachmentNode() {
|
|||||||
if (!attachmentUrl) {
|
if (!attachmentUrl) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const iconSize = this.mindMap.themeConfig.iconSize
|
const iconSize = this.getNodeIconSize('attachmentIcon')
|
||||||
|
const { icon, style } = this.mindMap.opt.attachmentIcon
|
||||||
const node = new SVG().attr('cursor', 'pointer').size(iconSize, iconSize)
|
const node = new SVG().attr('cursor', 'pointer').size(iconSize, iconSize)
|
||||||
if (attachmentName) {
|
if (attachmentName) {
|
||||||
node.add(SVG(`<title>${attachmentName}</title>`))
|
node.add(SVG(`<title>${attachmentName}</title>`))
|
||||||
@ -494,8 +512,8 @@ function createAttachmentNode() {
|
|||||||
// 透明的层,用来作为鼠标区域
|
// 透明的层,用来作为鼠标区域
|
||||||
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
|
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
|
||||||
// 备注图标
|
// 备注图标
|
||||||
const iconNode = SVG(iconsSvg.attachment).size(iconSize, iconSize)
|
const iconNode = SVG(icon || iconsSvg.attachment).size(iconSize, iconSize)
|
||||||
this.style.iconNode(iconNode)
|
this.style.iconNode(iconNode, style.color)
|
||||||
node.add(iconNode)
|
node.add(iconNode)
|
||||||
node.on('click', e => {
|
node.on('click', e => {
|
||||||
this.mindMap.emit('node_attachmentClick', this, e, node)
|
this.mindMap.emit('node_attachmentClick', this, e, node)
|
||||||
@ -510,9 +528,15 @@ function createAttachmentNode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取节点图标大小
|
||||||
|
function getNodeIconSize(prop) {
|
||||||
|
const { style } = this.mindMap.opt[prop]
|
||||||
|
return isUndef(style.size) ? this.mindMap.themeConfig.iconSize : style.size
|
||||||
|
}
|
||||||
|
|
||||||
// 获取节点备注显示位置
|
// 获取节点备注显示位置
|
||||||
function getNoteContentPosition() {
|
function getNoteContentPosition() {
|
||||||
const iconSize = this.mindMap.themeConfig.iconSize
|
const iconSize = this.getNodeIconSize('noteIcon')
|
||||||
const { scaleY } = this.mindMap.view.getTransformData().transform
|
const { scaleY } = this.mindMap.view.getTransformData().transform
|
||||||
const iconSizeAddScale = iconSize * scaleY
|
const iconSizeAddScale = iconSize * scaleY
|
||||||
let { left, top } = this._noteData.node.node.getBoundingClientRect()
|
let { left, top } = this._noteData.node.node.getBoundingClientRect()
|
||||||
@ -553,10 +577,10 @@ function isUseCustomNodeContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
getImageUrl,
|
||||||
createImgNode,
|
createImgNode,
|
||||||
getImgShowSize,
|
getImgShowSize,
|
||||||
createIconNode,
|
createIconNode,
|
||||||
tryAddHtmlStyle,
|
|
||||||
createRichTextNode,
|
createRichTextNode,
|
||||||
createTextNode,
|
createTextNode,
|
||||||
createHyperlinkNode,
|
createHyperlinkNode,
|
||||||
@ -564,6 +588,7 @@ export default {
|
|||||||
createNoteNode,
|
createNoteNode,
|
||||||
createAttachmentNode,
|
createAttachmentNode,
|
||||||
getNoteContentPosition,
|
getNoteContentPosition,
|
||||||
|
getNodeIconSize,
|
||||||
measureCustomNodeContentSize,
|
measureCustomNodeContentSize,
|
||||||
isUseCustomNodeContent
|
isUseCustomNodeContent
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,34 +7,36 @@ function createExpandNodeContent() {
|
|||||||
if (this._openExpandNode) {
|
if (this._openExpandNode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let { close, open } = this.mindMap.opt.expandBtnIcon || {}
|
const { expandBtnSize, expandBtnIcon, isShowExpandNum } = this.mindMap.opt
|
||||||
|
let { close, open } = expandBtnIcon || {}
|
||||||
// 根据配置判断是否显示数量按钮
|
// 根据配置判断是否显示数量按钮
|
||||||
if (this.mindMap.opt.isShowExpandNum) {
|
if (isShowExpandNum) {
|
||||||
// 展开的节点
|
// 展开的节点
|
||||||
this._openExpandNode = new Text()
|
this._openExpandNode = new Text()
|
||||||
|
this._openExpandNode.addClass('smm-expand-btn-text')
|
||||||
// 文本垂直居中
|
// 文本垂直居中
|
||||||
this._openExpandNode.attr({
|
this._openExpandNode.attr({
|
||||||
'text-anchor': 'middle',
|
'text-anchor': 'middle',
|
||||||
'dominant-baseline': 'middle',
|
'dominant-baseline': 'middle',
|
||||||
x: this.expandBtnSize / 2,
|
x: expandBtnSize / 2,
|
||||||
y: 2
|
y: 2
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this._openExpandNode = SVG(open || btnsSvg.open).size(
|
this._openExpandNode = SVG(open || btnsSvg.open).size(
|
||||||
this.expandBtnSize,
|
expandBtnSize,
|
||||||
this.expandBtnSize
|
expandBtnSize
|
||||||
)
|
)
|
||||||
this._openExpandNode.x(0).y(-this.expandBtnSize / 2)
|
this._openExpandNode.x(0).y(-expandBtnSize / 2)
|
||||||
}
|
}
|
||||||
// 收起的节点
|
// 收起的节点
|
||||||
this._closeExpandNode = SVG(close || btnsSvg.close).size(
|
this._closeExpandNode = SVG(close || btnsSvg.close).size(
|
||||||
this.expandBtnSize,
|
expandBtnSize,
|
||||||
this.expandBtnSize
|
expandBtnSize
|
||||||
)
|
)
|
||||||
this._closeExpandNode.x(0).y(-this.expandBtnSize / 2)
|
this._closeExpandNode.x(0).y(-expandBtnSize / 2)
|
||||||
// 填充节点
|
// 填充节点
|
||||||
this._fillExpandNode = new Circle().size(this.expandBtnSize)
|
this._fillExpandNode = new Circle().size(expandBtnSize)
|
||||||
this._fillExpandNode.x(0).y(-this.expandBtnSize / 2)
|
this._fillExpandNode.x(0).y(-expandBtnSize / 2)
|
||||||
|
|
||||||
// 设置样式
|
// 设置样式
|
||||||
this.style.iconBtn(
|
this.style.iconBtn(
|
||||||
@ -78,7 +80,7 @@ function updateExpandBtnNode() {
|
|||||||
color: expandBtnStyle.strokeColor
|
color: expandBtnStyle.strokeColor
|
||||||
})
|
})
|
||||||
// 计算子节点数量
|
// 计算子节点数量
|
||||||
let count = this.sumNode(this.nodeData.children)
|
let count = this.sumNode(this.nodeData.children || [])
|
||||||
if (typeof expandBtnNumHandler === 'function') {
|
if (typeof expandBtnNumHandler === 'function') {
|
||||||
const res = expandBtnNumHandler(count, this)
|
const res = expandBtnNumHandler(count, this)
|
||||||
if (!isUndef(res)) {
|
if (!isUndef(res)) {
|
||||||
@ -104,11 +106,7 @@ function updateExpandBtnPos() {
|
|||||||
|
|
||||||
// 创建展开收缩按钮
|
// 创建展开收缩按钮
|
||||||
function renderExpandBtn() {
|
function renderExpandBtn() {
|
||||||
if (
|
if (this.getChildrenLength() <= 0 || this.isRoot) {
|
||||||
!this.nodeData.children ||
|
|
||||||
this.nodeData.children.length <= 0 ||
|
|
||||||
this.isRoot
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this._expandBtn) {
|
if (this._expandBtn) {
|
||||||
|
|||||||
@ -3,15 +3,12 @@ import { Rect } from '@svgdotjs/svg.js'
|
|||||||
// 渲染展开收起按钮的隐藏占位元素
|
// 渲染展开收起按钮的隐藏占位元素
|
||||||
function renderExpandBtnPlaceholderRect() {
|
function renderExpandBtnPlaceholderRect() {
|
||||||
// 根节点或没有子节点不需要渲染
|
// 根节点或没有子节点不需要渲染
|
||||||
if (
|
if (this.getChildrenLength() <= 0 || this.isRoot) {
|
||||||
!this.nodeData.children ||
|
|
||||||
this.nodeData.children.length <= 0 ||
|
|
||||||
this.isRoot
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 默认显示展开按钮的情况下或不显示展开收起按钮的情况下不需要渲染
|
// 默认显示展开按钮的情况下或不显示展开收起按钮的情况下不需要渲染
|
||||||
const { alwaysShowExpandBtn, notShowExpandBtn } = this.mindMap.opt
|
const { alwaysShowExpandBtn, notShowExpandBtn, expandBtnSize } =
|
||||||
|
this.mindMap.opt
|
||||||
if (!alwaysShowExpandBtn && !notShowExpandBtn) {
|
if (!alwaysShowExpandBtn && !notShowExpandBtn) {
|
||||||
let { width, height } = this
|
let { width, height } = this
|
||||||
if (!this._unVisibleRectRegionNode) {
|
if (!this._unVisibleRectRegionNode) {
|
||||||
@ -23,7 +20,7 @@ function renderExpandBtnPlaceholderRect() {
|
|||||||
this.group.add(this._unVisibleRectRegionNode)
|
this.group.add(this._unVisibleRectRegionNode)
|
||||||
this.renderer.layout.renderExpandBtnRect(
|
this.renderer.layout.renderExpandBtnRect(
|
||||||
this._unVisibleRectRegionNode,
|
this._unVisibleRectRegionNode,
|
||||||
this.expandBtnSize,
|
expandBtnSize,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
this
|
this
|
||||||
@ -48,7 +45,7 @@ function updateExpandBtnPlaceholderRect() {
|
|||||||
this.renderExpandBtnPlaceholderRect()
|
this.renderExpandBtnPlaceholderRect()
|
||||||
}
|
}
|
||||||
// 没有子节点到有子节点需要渲染
|
// 没有子节点到有子节点需要渲染
|
||||||
if (this.nodeData.children && this.nodeData.children.length > 0) {
|
if (this.getChildrenLength() > 0) {
|
||||||
if (!this._unVisibleRectRegionNode) {
|
if (!this._unVisibleRectRegionNode) {
|
||||||
this.renderExpandBtnPlaceholderRect()
|
this.renderExpandBtnPlaceholderRect()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,7 +106,7 @@ function renderGeneralization(forceRender) {
|
|||||||
|
|
||||||
// 更新节点概要数据
|
// 更新节点概要数据
|
||||||
function updateGeneralizationData() {
|
function updateGeneralizationData() {
|
||||||
const childrenLength = this.nodeData.children.length
|
const childrenLength = this.getChildrenLength()
|
||||||
const list = this.formatGetGeneralization()
|
const list = this.formatGetGeneralization()
|
||||||
const newList = []
|
const newList = []
|
||||||
list.forEach(item => {
|
list.forEach(item => {
|
||||||
|
|||||||
516
simple-mind-map/src/core/render/node/nodeLayout.js
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
import { CONSTANTS } from '../../../constants/constant'
|
||||||
|
import { G, Rect } from '@svgdotjs/svg.js'
|
||||||
|
import { createForeignObjectNode } from '../../../utils/index'
|
||||||
|
|
||||||
|
// 根据图片放置位置返回图片和文本的间距值
|
||||||
|
function getImgTextMarin(dir, imgWidth, textWidth, imgHeight, textHeight) {
|
||||||
|
// 图片和文字节点的间距
|
||||||
|
const { imgTextMargin } = this.mindMap.opt
|
||||||
|
if (dir === 'v') {
|
||||||
|
// 垂直
|
||||||
|
return imgHeight > 0 && textHeight > 0 ? imgTextMargin : 0
|
||||||
|
} else {
|
||||||
|
// 水平
|
||||||
|
return imgWidth > 0 && textWidth > 0 ? imgTextMargin : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标签内容的大小
|
||||||
|
function getTagContentSize(space) {
|
||||||
|
let maxTagHeight = 0
|
||||||
|
let width = this._tagData.reduce((sum, cur) => {
|
||||||
|
maxTagHeight = Math.max(maxTagHeight, cur.height)
|
||||||
|
return (sum += cur.width)
|
||||||
|
}, 0)
|
||||||
|
width += (this._tagData.length - 1) * space
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height: maxTagHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算节点尺寸信息
|
||||||
|
function getNodeRect() {
|
||||||
|
// 自定义节点内容
|
||||||
|
if (this.isUseCustomNodeContent()) {
|
||||||
|
const rect = this.measureCustomNodeContentSize(
|
||||||
|
this._customNodeContent.cloneNode(true)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
width: this.hasCustomWidth() ? this.customTextWidth : rect.width,
|
||||||
|
height: rect.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { TAG_PLACEMENT, IMG_PLACEMENT } = CONSTANTS
|
||||||
|
const { textContentMargin } = this.mindMap.opt
|
||||||
|
const tagPlacement = this.getStyle('tagPlacement') || TAG_PLACEMENT.RIGHT
|
||||||
|
const tagIsBottom = tagPlacement === TAG_PLACEMENT.BOTTOM
|
||||||
|
const imgPlacement = this.getStyle('imgPlacement') || IMG_PLACEMENT.TOP
|
||||||
|
// 宽高
|
||||||
|
let imgContentWidth = 0
|
||||||
|
let imgContentHeight = 0
|
||||||
|
let textContentWidth = 0
|
||||||
|
let textContentHeight = 0
|
||||||
|
let tagContentWidth = 0
|
||||||
|
let tagContentHeight = 0
|
||||||
|
let spaceCount = 0
|
||||||
|
// 存在图片
|
||||||
|
if (this._imgData) {
|
||||||
|
imgContentWidth = this._imgData.width
|
||||||
|
imgContentHeight = this._imgData.height
|
||||||
|
}
|
||||||
|
// 库前置内容
|
||||||
|
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||||||
|
const itemData = this[`_${item.name}Data`]
|
||||||
|
if (itemData) {
|
||||||
|
textContentWidth += itemData.width
|
||||||
|
textContentHeight = Math.max(textContentHeight, itemData.height)
|
||||||
|
spaceCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 自定义前置内容
|
||||||
|
if (this._prefixData) {
|
||||||
|
textContentWidth += this._prefixData.width
|
||||||
|
textContentHeight = Math.max(textContentHeight, this._prefixData.height)
|
||||||
|
spaceCount++
|
||||||
|
}
|
||||||
|
// 图标
|
||||||
|
if (this._iconData.length > 0) {
|
||||||
|
textContentWidth +=
|
||||||
|
this._iconData.reduce((sum, cur) => {
|
||||||
|
textContentHeight = Math.max(textContentHeight, cur.height)
|
||||||
|
return (sum += cur.width)
|
||||||
|
}, 0) +
|
||||||
|
(this._iconData.length - 1) * textContentMargin
|
||||||
|
spaceCount++
|
||||||
|
}
|
||||||
|
// 文字
|
||||||
|
if (this._textData) {
|
||||||
|
textContentWidth += this._textData.width
|
||||||
|
textContentHeight = Math.max(textContentHeight, this._textData.height)
|
||||||
|
spaceCount++
|
||||||
|
}
|
||||||
|
// 超链接
|
||||||
|
if (this._hyperlinkData) {
|
||||||
|
textContentWidth += this._hyperlinkData.width
|
||||||
|
textContentHeight = Math.max(textContentHeight, this._hyperlinkData.height)
|
||||||
|
spaceCount++
|
||||||
|
}
|
||||||
|
// 标签
|
||||||
|
if (this._tagData.length > 0) {
|
||||||
|
const { width: totalTagWidth, height: maxTagHeight } =
|
||||||
|
this.getTagContentSize(textContentMargin)
|
||||||
|
if (tagIsBottom) {
|
||||||
|
// 文字下方
|
||||||
|
tagContentWidth = totalTagWidth
|
||||||
|
tagContentHeight = maxTagHeight
|
||||||
|
} else {
|
||||||
|
// 否则在右侧
|
||||||
|
textContentWidth += totalTagWidth
|
||||||
|
textContentHeight = Math.max(textContentHeight, maxTagHeight)
|
||||||
|
spaceCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 备注
|
||||||
|
if (this._noteData) {
|
||||||
|
textContentWidth += this._noteData.width
|
||||||
|
textContentHeight = Math.max(textContentHeight, this._noteData.height)
|
||||||
|
spaceCount++
|
||||||
|
}
|
||||||
|
// 附件
|
||||||
|
if (this._attachmentData) {
|
||||||
|
textContentWidth += this._attachmentData.width
|
||||||
|
textContentHeight = Math.max(textContentHeight, this._attachmentData.height)
|
||||||
|
spaceCount++
|
||||||
|
}
|
||||||
|
// 自定义后置内容
|
||||||
|
if (this._postfixData) {
|
||||||
|
textContentWidth += this._postfixData.width
|
||||||
|
textContentHeight = Math.max(textContentHeight, this._postfixData.height)
|
||||||
|
spaceCount++
|
||||||
|
}
|
||||||
|
// 库后置内容
|
||||||
|
this.mindMap.nodeInnerPostfixList.forEach(item => {
|
||||||
|
const itemData = this[`_${item.name}Data`]
|
||||||
|
if (itemData) {
|
||||||
|
textContentWidth += itemData.width
|
||||||
|
textContentHeight = Math.max(textContentHeight, itemData.height)
|
||||||
|
spaceCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
textContentWidth += (spaceCount - 1) * textContentMargin
|
||||||
|
// 文字内容部分的尺寸
|
||||||
|
if (tagIsBottom && textContentWidth > 0 && tagContentHeight > 0) {
|
||||||
|
this._rectInfo.textContentWidthWithoutTag = textContentWidth
|
||||||
|
textContentWidth = Math.max(textContentWidth, tagContentWidth)
|
||||||
|
textContentHeight = textContentHeight + textContentMargin + tagContentHeight
|
||||||
|
}
|
||||||
|
this._rectInfo.textContentWidth = textContentWidth
|
||||||
|
this._rectInfo.textContentHeight = textContentHeight
|
||||||
|
|
||||||
|
// 纯内容宽高
|
||||||
|
let _width = 0
|
||||||
|
let _height = 0
|
||||||
|
if ([IMG_PLACEMENT.TOP, IMG_PLACEMENT.BOTTOM].includes(imgPlacement)) {
|
||||||
|
// 图片在上下
|
||||||
|
_width = Math.max(imgContentWidth, textContentWidth)
|
||||||
|
_height =
|
||||||
|
imgContentHeight +
|
||||||
|
textContentHeight +
|
||||||
|
this.getImgTextMarin('v', 0, 0, imgContentHeight, textContentHeight)
|
||||||
|
} else {
|
||||||
|
// 图片在左右
|
||||||
|
_width =
|
||||||
|
imgContentWidth +
|
||||||
|
textContentWidth +
|
||||||
|
this.getImgTextMarin('h', imgContentWidth, textContentWidth)
|
||||||
|
_height = Math.max(imgContentHeight, textContentHeight)
|
||||||
|
}
|
||||||
|
const { paddingX, paddingY } = this.getPaddingVale()
|
||||||
|
// 计算节点形状需要的附加内边距
|
||||||
|
const { paddingX: shapePaddingX, paddingY: shapePaddingY } =
|
||||||
|
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 + borderWidth,
|
||||||
|
height: _height + paddingY * 2 + shapePaddingY * 2 + borderWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 激活hover和激活边框
|
||||||
|
function addHoverNode(width, height) {
|
||||||
|
const { hoverRectPadding } = this.mindMap.opt
|
||||||
|
this.hoverNode = new Rect()
|
||||||
|
.size(width + hoverRectPadding * 2, height + hoverRectPadding * 2)
|
||||||
|
.x(-hoverRectPadding)
|
||||||
|
.y(-hoverRectPadding)
|
||||||
|
this.hoverNode.addClass('smm-hover-node')
|
||||||
|
this.style.hoverNode(this.hoverNode, width, height)
|
||||||
|
this.group.add(this.hoverNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当使用了完全自定义节点内容后,可以通过该方法实时更新节点大小
|
||||||
|
function customNodeContentRealtimeLayout() {
|
||||||
|
if (!this.group) return
|
||||||
|
if (!this.isUseCustomNodeContent()) return
|
||||||
|
// 删除除foreignObject外的其他元素
|
||||||
|
if (this.shapeNode) this.shapeNode.remove()
|
||||||
|
if (this._unVisibleRectRegionNode) this._unVisibleRectRegionNode.remove()
|
||||||
|
if (this.hoverNode) this.hoverNode.remove()
|
||||||
|
const { width, height } = this
|
||||||
|
const halfBorderWidth = this.getBorderWidth() / 2
|
||||||
|
// 节点形状
|
||||||
|
this.shapeNode = this.shapeInstance.createShape()
|
||||||
|
this.shapeNode.addClass('smm-node-shape')
|
||||||
|
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
|
||||||
|
this.style.shape(this.shapeNode)
|
||||||
|
this.group.add(this.shapeNode)
|
||||||
|
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
|
||||||
|
this.renderExpandBtnPlaceholderRect()
|
||||||
|
// 概要节点添加一个带所属节点id的类名
|
||||||
|
if (this.isGeneralization && this.generalizationBelongNode) {
|
||||||
|
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
|
||||||
|
}
|
||||||
|
// 激活hover和激活边框
|
||||||
|
this.addHoverNode(width, height)
|
||||||
|
// 将形状元素移至底层,避免遮挡foreignObject
|
||||||
|
this.shapeNode.back()
|
||||||
|
// 更新foreignObject元素大小
|
||||||
|
this.group.findOne('foreignObject').size(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定位节点内容
|
||||||
|
function layout() {
|
||||||
|
if (!this.group) return
|
||||||
|
// 清除之前的内容
|
||||||
|
this.group.clear()
|
||||||
|
const {
|
||||||
|
openRealtimeRenderOnNodeTextEdit,
|
||||||
|
textContentMargin,
|
||||||
|
addCustomContentToNode
|
||||||
|
} = this.mindMap.opt
|
||||||
|
// 避免编辑过程中展开收起按钮闪烁的问题
|
||||||
|
// 暂时去掉,带来的问题太多
|
||||||
|
// if (
|
||||||
|
// openRealtimeRenderOnNodeTextEdit &&
|
||||||
|
// this._expandBtn &&
|
||||||
|
// this.getChildrenLength() > 0
|
||||||
|
// ) {
|
||||||
|
// this.group.add(this._expandBtn)
|
||||||
|
// }
|
||||||
|
const { width, height } = this
|
||||||
|
let { paddingX, paddingY } = this.getPaddingVale()
|
||||||
|
const halfBorderWidth = this.getBorderWidth() / 2
|
||||||
|
paddingX += this.shapePadding.paddingX + halfBorderWidth
|
||||||
|
paddingY += this.shapePadding.paddingY + halfBorderWidth
|
||||||
|
// 节点形状
|
||||||
|
this.shapeNode = this.shapeInstance.createShape()
|
||||||
|
this.shapeNode.addClass('smm-node-shape')
|
||||||
|
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
|
||||||
|
this.style.shape(this.shapeNode)
|
||||||
|
this.group.add(this.shapeNode)
|
||||||
|
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
|
||||||
|
this.renderExpandBtnPlaceholderRect()
|
||||||
|
// 创建协同头像节点
|
||||||
|
if (this.createUserListNode) this.createUserListNode()
|
||||||
|
// 概要节点添加一个带所属节点id的类名
|
||||||
|
if (this.isGeneralization && this.generalizationBelongNode) {
|
||||||
|
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
|
||||||
|
}
|
||||||
|
// 如果存在自定义节点内容,那么使用自定义节点内容
|
||||||
|
if (this.isUseCustomNodeContent()) {
|
||||||
|
const foreignObject = createForeignObjectNode({
|
||||||
|
el: this._customNodeContent,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
})
|
||||||
|
this.group.add(foreignObject)
|
||||||
|
this.addHoverNode(width, height)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { IMG_PLACEMENT, TAG_PLACEMENT } = CONSTANTS
|
||||||
|
const imgPlacement = this.getStyle('imgPlacement') || IMG_PLACEMENT.TOP
|
||||||
|
const tagPlacement = this.getStyle('tagPlacement') || TAG_PLACEMENT.RIGHT
|
||||||
|
const tagIsBottom = tagPlacement === TAG_PLACEMENT.BOTTOM
|
||||||
|
let { textContentWidth, textContentHeight, textContentWidthWithoutTag } =
|
||||||
|
this._rectInfo
|
||||||
|
const textContentHeightWithTag = textContentHeight
|
||||||
|
// 如果存在显示在文本下方的标签,那么非标签内容的整体高度需要减去标签高度
|
||||||
|
let totalTagWidth = 0
|
||||||
|
let maxTagHeight = 0
|
||||||
|
const hasTagContent = this._tagData && this._tagData.length > 0
|
||||||
|
if (hasTagContent) {
|
||||||
|
const res = this.getTagContentSize(textContentMargin)
|
||||||
|
totalTagWidth = res.width
|
||||||
|
maxTagHeight = res.height
|
||||||
|
if (tagIsBottom) {
|
||||||
|
textContentHeight -= maxTagHeight + textContentMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 图片节点
|
||||||
|
let imgWidth = 0
|
||||||
|
let imgHeight = 0
|
||||||
|
if (this._imgData) {
|
||||||
|
imgWidth = this._imgData.width
|
||||||
|
imgHeight = this._imgData.height
|
||||||
|
this.group.add(this._imgData.node)
|
||||||
|
switch (imgPlacement) {
|
||||||
|
case IMG_PLACEMENT.TOP:
|
||||||
|
this._imgData.node.cx(width / 2).y(paddingY)
|
||||||
|
break
|
||||||
|
case IMG_PLACEMENT.BOTTOM:
|
||||||
|
this._imgData.node.cx(width / 2).y(height - paddingY - imgHeight)
|
||||||
|
break
|
||||||
|
case IMG_PLACEMENT.LEFT:
|
||||||
|
this._imgData.node.x(paddingX).cy(height / 2)
|
||||||
|
break
|
||||||
|
case IMG_PLACEMENT.RIGHT:
|
||||||
|
this._imgData.node.x(width - paddingX - imgWidth).cy(height / 2)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 内容节点
|
||||||
|
let textContentNested = new G()
|
||||||
|
let textContentOffsetX = 0
|
||||||
|
if (hasTagContent && tagIsBottom) {
|
||||||
|
textContentOffsetX =
|
||||||
|
textContentWidthWithoutTag < textContentWidth
|
||||||
|
? (textContentWidth - textContentWidthWithoutTag) / 2
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
// 库前置内容
|
||||||
|
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||||||
|
const itemData = this[`_${item.name}Data`]
|
||||||
|
if (itemData) {
|
||||||
|
itemData.node
|
||||||
|
.x(textContentOffsetX)
|
||||||
|
.y((textContentHeight - itemData.height) / 2)
|
||||||
|
textContentNested.add(itemData.node)
|
||||||
|
textContentOffsetX += itemData.width + textContentMargin
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 自定义前置内容
|
||||||
|
if (this._prefixData) {
|
||||||
|
const foreignObject = createForeignObjectNode({
|
||||||
|
el: this._prefixData.el,
|
||||||
|
width: this._prefixData.width,
|
||||||
|
height: this._prefixData.height
|
||||||
|
})
|
||||||
|
foreignObject
|
||||||
|
.x(textContentOffsetX)
|
||||||
|
.y((textContentHeight - this._prefixData.height) / 2)
|
||||||
|
textContentNested.add(foreignObject)
|
||||||
|
textContentOffsetX += this._prefixData.width + textContentMargin
|
||||||
|
}
|
||||||
|
// icon
|
||||||
|
let iconNested = new G()
|
||||||
|
if (this._iconData && this._iconData.length > 0) {
|
||||||
|
let iconLeft = 0
|
||||||
|
this._iconData.forEach(item => {
|
||||||
|
item.node
|
||||||
|
.x(textContentOffsetX + iconLeft)
|
||||||
|
.y((textContentHeight - item.height) / 2)
|
||||||
|
iconNested.add(item.node)
|
||||||
|
iconLeft += item.width + textContentMargin
|
||||||
|
})
|
||||||
|
textContentNested.add(iconNested)
|
||||||
|
textContentOffsetX += iconLeft
|
||||||
|
}
|
||||||
|
// 文字
|
||||||
|
if (this._textData) {
|
||||||
|
const oldX = this._textData.node.attr('data-offsetx') || 0
|
||||||
|
this._textData.node.attr('data-offsetx', textContentOffsetX)
|
||||||
|
// 修复safari浏览器节点存在图标时文字位置不正确的问题
|
||||||
|
;(this._textData.nodeContent || this._textData.node)
|
||||||
|
.x(-oldX) // 修复非富文本模式下同时存在图标和换行的文本时,被收起和展开时图标与文字距离会逐渐拉大的问题
|
||||||
|
.x(textContentOffsetX)
|
||||||
|
.y((textContentHeight - this._textData.height) / 2)
|
||||||
|
// 如果开启了文本编辑实时渲染,需要判断当前渲染的节点是否是正在编辑的节点,是的话将透明度设置为0不显示
|
||||||
|
if (openRealtimeRenderOnNodeTextEdit) {
|
||||||
|
this._textData.node.opacity(
|
||||||
|
this.mindMap.renderer.textEdit.getCurrentEditNode() === this ? 0 : 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
textContentNested.add(this._textData.node)
|
||||||
|
textContentOffsetX += this._textData.width + textContentMargin
|
||||||
|
}
|
||||||
|
// 超链接
|
||||||
|
if (this._hyperlinkData) {
|
||||||
|
this._hyperlinkData.node
|
||||||
|
.x(textContentOffsetX)
|
||||||
|
.y((textContentHeight - this._hyperlinkData.height) / 2)
|
||||||
|
textContentNested.add(this._hyperlinkData.node)
|
||||||
|
textContentOffsetX += this._hyperlinkData.width + textContentMargin
|
||||||
|
}
|
||||||
|
// 标签
|
||||||
|
let tagNested = new G()
|
||||||
|
if (hasTagContent) {
|
||||||
|
if (tagIsBottom) {
|
||||||
|
// 标签显示在文字下方
|
||||||
|
let tagLeft = 0
|
||||||
|
this._tagData.forEach(item => {
|
||||||
|
item.node.x(tagLeft).y((maxTagHeight - item.height) / 2)
|
||||||
|
tagNested.add(item.node)
|
||||||
|
tagLeft += item.width + textContentMargin
|
||||||
|
})
|
||||||
|
tagNested
|
||||||
|
.x((textContentWidth - totalTagWidth) / 2)
|
||||||
|
.y(textContentHeightWithTag - maxTagHeight)
|
||||||
|
textContentNested.add(tagNested)
|
||||||
|
} else {
|
||||||
|
// 标签显示在文字右侧
|
||||||
|
let tagLeft = 0
|
||||||
|
this._tagData.forEach(item => {
|
||||||
|
item.node
|
||||||
|
.x(textContentOffsetX + tagLeft)
|
||||||
|
.y((textContentHeight - item.height) / 2)
|
||||||
|
tagNested.add(item.node)
|
||||||
|
tagLeft += item.width + textContentMargin
|
||||||
|
})
|
||||||
|
textContentNested.add(tagNested)
|
||||||
|
textContentOffsetX += tagLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 备注
|
||||||
|
if (this._noteData) {
|
||||||
|
this._noteData.node
|
||||||
|
.x(textContentOffsetX)
|
||||||
|
.y((textContentHeight - this._noteData.height) / 2)
|
||||||
|
textContentNested.add(this._noteData.node)
|
||||||
|
textContentOffsetX += this._noteData.width + textContentMargin
|
||||||
|
}
|
||||||
|
// 附件
|
||||||
|
if (this._attachmentData) {
|
||||||
|
this._attachmentData.node
|
||||||
|
.x(textContentOffsetX)
|
||||||
|
.y((textContentHeight - this._attachmentData.height) / 2)
|
||||||
|
textContentNested.add(this._attachmentData.node)
|
||||||
|
textContentOffsetX += this._attachmentData.width + textContentMargin
|
||||||
|
}
|
||||||
|
// 自定义后置内容
|
||||||
|
if (this._postfixData) {
|
||||||
|
const foreignObject = createForeignObjectNode({
|
||||||
|
el: this._postfixData.el,
|
||||||
|
width: this._postfixData.width,
|
||||||
|
height: this._postfixData.height
|
||||||
|
})
|
||||||
|
foreignObject
|
||||||
|
.x(textContentOffsetX)
|
||||||
|
.y((textContentHeight - this._postfixData.height) / 2)
|
||||||
|
textContentNested.add(foreignObject)
|
||||||
|
textContentOffsetX += this._postfixData.width + textContentMargin
|
||||||
|
}
|
||||||
|
// 库后置内容
|
||||||
|
this.mindMap.nodeInnerPostfixList.forEach(item => {
|
||||||
|
const itemData = this[`_${item.name}Data`]
|
||||||
|
if (itemData) {
|
||||||
|
itemData.node
|
||||||
|
.x(textContentOffsetX)
|
||||||
|
.y((textContentHeight - itemData.height) / 2)
|
||||||
|
textContentNested.add(itemData.node)
|
||||||
|
textContentOffsetX += itemData.width + textContentMargin
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.group.add(textContentNested)
|
||||||
|
// 文字内容整体
|
||||||
|
const { width: bboxWidth, height: bboxHeight } = textContentNested.bbox()
|
||||||
|
let translateX = 0
|
||||||
|
let translateY = 0
|
||||||
|
switch (imgPlacement) {
|
||||||
|
case IMG_PLACEMENT.TOP:
|
||||||
|
translateX = width / 2 - bboxWidth / 2
|
||||||
|
translateY =
|
||||||
|
paddingY + // 内边距
|
||||||
|
imgHeight + // 图片高度
|
||||||
|
this.getImgTextMarin('v', 0, 0, imgHeight, textContentHeightWithTag) // 和图片的间距
|
||||||
|
break
|
||||||
|
case IMG_PLACEMENT.BOTTOM:
|
||||||
|
translateX = width / 2 - bboxWidth / 2
|
||||||
|
translateY = paddingY
|
||||||
|
break
|
||||||
|
case IMG_PLACEMENT.LEFT:
|
||||||
|
translateX =
|
||||||
|
imgWidth +
|
||||||
|
paddingX +
|
||||||
|
this.getImgTextMarin('h', imgWidth, textContentWidth)
|
||||||
|
translateY = height / 2 - bboxHeight / 2
|
||||||
|
break
|
||||||
|
case IMG_PLACEMENT.RIGHT:
|
||||||
|
translateX = paddingX
|
||||||
|
translateY = height / 2 - bboxHeight / 2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
textContentNested.translate(translateX, translateY)
|
||||||
|
this.addHoverNode(width, height)
|
||||||
|
if (this._customContentAddToNodeAdd && this._customContentAddToNodeAdd.el) {
|
||||||
|
const foreignObject = createForeignObjectNode(
|
||||||
|
this._customContentAddToNodeAdd
|
||||||
|
)
|
||||||
|
this.group.add(foreignObject)
|
||||||
|
if (
|
||||||
|
addCustomContentToNode &&
|
||||||
|
typeof addCustomContentToNode.handle === 'function'
|
||||||
|
) {
|
||||||
|
addCustomContentToNode.handle({
|
||||||
|
content: this._customContentAddToNodeAdd,
|
||||||
|
element: foreignObject,
|
||||||
|
node: this
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mindMap.emit('node_layout_end', this)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getImgTextMarin,
|
||||||
|
getTagContentSize,
|
||||||
|
getNodeRect,
|
||||||
|
addHoverNode,
|
||||||
|
layout,
|
||||||
|
customNodeContentRealtimeLayout
|
||||||
|
}
|
||||||
88
simple-mind-map/src/core/render/node/quickCreateChildBtn.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import btnsSvg from '../../../svg/btns'
|
||||||
|
import { SVG, Circle, G } from '@svgdotjs/svg.js'
|
||||||
|
|
||||||
|
function initQuickCreateChildBtn() {
|
||||||
|
if (this.isGeneralization) return
|
||||||
|
this._quickCreateChildBtn = null
|
||||||
|
this._showQuickCreateChildBtn = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示按钮
|
||||||
|
function showQuickCreateChildBtn() {
|
||||||
|
if (this.isGeneralization || this.getChildrenLength() > 0) return
|
||||||
|
// 创建按钮
|
||||||
|
if (this._quickCreateChildBtn) {
|
||||||
|
this.group.add(this._quickCreateChildBtn)
|
||||||
|
} else {
|
||||||
|
const { quickCreateChildBtnIcon, expandBtnStyle, expandBtnSize } =
|
||||||
|
this.mindMap.opt
|
||||||
|
const { icon, style } = quickCreateChildBtnIcon
|
||||||
|
let { color, fill } = expandBtnStyle || {
|
||||||
|
color: '#808080',
|
||||||
|
fill: '#fff'
|
||||||
|
}
|
||||||
|
color = style.color || color
|
||||||
|
// 图标节点
|
||||||
|
const iconNode = SVG(icon || btnsSvg.quickCreateChild).size(
|
||||||
|
expandBtnSize,
|
||||||
|
expandBtnSize
|
||||||
|
)
|
||||||
|
iconNode.css({
|
||||||
|
cursor: 'pointer'
|
||||||
|
})
|
||||||
|
iconNode.x(0).y(-expandBtnSize / 2)
|
||||||
|
this.style.iconNode(iconNode, color)
|
||||||
|
// 填充节点
|
||||||
|
const fillNode = new Circle().size(expandBtnSize)
|
||||||
|
fillNode.x(0).y(-expandBtnSize / 2)
|
||||||
|
fillNode.fill({ color: fill }).css({
|
||||||
|
cursor: 'pointer'
|
||||||
|
})
|
||||||
|
// 容器节点
|
||||||
|
this._quickCreateChildBtn = new G()
|
||||||
|
this._quickCreateChildBtn.add(fillNode).add(iconNode)
|
||||||
|
this._quickCreateChildBtn.on('click', e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
this.mindMap.emit('quick_create_btn_click', this)
|
||||||
|
const { customQuickCreateChildBtnClick } = this.mindMap.opt
|
||||||
|
if (typeof customQuickCreateChildBtnClick === 'function') {
|
||||||
|
customQuickCreateChildBtnClick(this)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.mindMap.execCommand('INSERT_CHILD_NODE', true, [this])
|
||||||
|
})
|
||||||
|
this._quickCreateChildBtn.on('dblclick', e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
})
|
||||||
|
this._quickCreateChildBtn.addClass('smm-quick-create-child-btn')
|
||||||
|
this.group.add(this._quickCreateChildBtn)
|
||||||
|
}
|
||||||
|
this._showQuickCreateChildBtn = true
|
||||||
|
// 更新按钮
|
||||||
|
this.renderer.layout.renderExpandBtn(this, this._quickCreateChildBtn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除按钮
|
||||||
|
function removeQuickCreateChildBtn() {
|
||||||
|
if (this.isGeneralization) return
|
||||||
|
if (this._quickCreateChildBtn && this._showQuickCreateChildBtn) {
|
||||||
|
this._quickCreateChildBtn.remove()
|
||||||
|
this._showQuickCreateChildBtn = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏按钮
|
||||||
|
function hideQuickCreateChildBtn() {
|
||||||
|
if (this.isGeneralization) return
|
||||||
|
const { isActive } = this.getData()
|
||||||
|
if (!isActive) {
|
||||||
|
this.removeQuickCreateChildBtn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initQuickCreateChildBtn,
|
||||||
|
showQuickCreateChildBtn,
|
||||||
|
removeQuickCreateChildBtn,
|
||||||
|
hideQuickCreateChildBtn
|
||||||
|
}
|
||||||
@ -49,10 +49,7 @@ class Base {
|
|||||||
|
|
||||||
// 检查当前来源是否需要重新计算节点大小
|
// 检查当前来源是否需要重新计算节点大小
|
||||||
checkIsNeedResizeSources() {
|
checkIsNeedResizeSources() {
|
||||||
return [
|
return this.renderer.checkHasRenderSource(CONSTANTS.CHANGE_THEME)
|
||||||
CONSTANTS.CHANGE_THEME,
|
|
||||||
CONSTANTS.TRANSFORM_TO_NORMAL_NODE
|
|
||||||
].includes(this.renderer.renderSource)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 层级类型改变
|
// 层级类型改变
|
||||||
@ -64,7 +61,7 @@ class Base {
|
|||||||
|
|
||||||
// 检查是否是结构布局改变重新渲染展开收起按钮占位元素
|
// 检查是否是结构布局改变重新渲染展开收起按钮占位元素
|
||||||
checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) {
|
checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) {
|
||||||
if (this.renderer.renderSource === CONSTANTS.CHANGE_LAYOUT) {
|
if (this.renderer.checkHasRenderSource(CONSTANTS.CHANGE_LAYOUT)) {
|
||||||
node.needRerenderExpandBtnPlaceholderRect = true
|
node.needRerenderExpandBtnPlaceholderRect = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,10 +74,38 @@ class Base {
|
|||||||
lastData.isActive = curData.isActive
|
lastData.isActive = curData.isActive
|
||||||
lastData.expand = curData.expand
|
lastData.expand = curData.expand
|
||||||
lastData = JSON.stringify(lastData)
|
lastData = JSON.stringify(lastData)
|
||||||
|
} else {
|
||||||
|
// 只在都有数据时才进行对比
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return lastData !== JSON.stringify(curData)
|
return lastData !== JSON.stringify(curData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查库前置或后置内容是否改变了
|
||||||
|
checkNodeFixChange(newNode, nodeInnerPrefixData, nodeInnerPostfixData) {
|
||||||
|
// 库前置内容是否改变了
|
||||||
|
let isNodeInnerPrefixChange = false
|
||||||
|
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||||||
|
if (item.updateNodeData) {
|
||||||
|
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData)
|
||||||
|
if (isChange) {
|
||||||
|
isNodeInnerPrefixChange = isChange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 库后置内容是否改变了
|
||||||
|
let isNodeInnerPostfixChange = false
|
||||||
|
this.mindMap.nodeInnerPostfixList.forEach(item => {
|
||||||
|
if (item.updateNodeData) {
|
||||||
|
const isChange = item.updateNodeData(newNode, nodeInnerPostfixData)
|
||||||
|
if (isChange) {
|
||||||
|
isNodeInnerPostfixChange = isChange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return isNodeInnerPrefixChange || isNodeInnerPostfixChange
|
||||||
|
}
|
||||||
|
|
||||||
// 创建节点实例
|
// 创建节点实例
|
||||||
createNode(data, parent, isRoot, layerIndex, index, ancestors) {
|
createNode(data, parent, isRoot, layerIndex, index, ancestors) {
|
||||||
// 创建节点
|
// 创建节点
|
||||||
@ -98,6 +123,20 @@ class Base {
|
|||||||
nodeInnerPrefixData[key] = value
|
nodeInnerPrefixData[key] = value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// 库后置内容数据
|
||||||
|
const nodeInnerPostfixData = {}
|
||||||
|
this.mindMap.nodeInnerPostfixList.forEach(item => {
|
||||||
|
if (item.createNodeData) {
|
||||||
|
const [key, value] = item.createNodeData({
|
||||||
|
data,
|
||||||
|
parent,
|
||||||
|
ancestors,
|
||||||
|
layerIndex,
|
||||||
|
index
|
||||||
|
})
|
||||||
|
nodeInnerPostfixData[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
const uid = data.data.uid
|
const uid = data.data.uid
|
||||||
let newNode = null
|
let newNode = null
|
||||||
// 数据上保存了节点引用,那么直接复用节点
|
// 数据上保存了节点引用,那么直接复用节点
|
||||||
@ -117,16 +156,12 @@ class Base {
|
|||||||
}
|
}
|
||||||
this.cacheNode(data._node.uid, newNode)
|
this.cacheNode(data._node.uid, newNode)
|
||||||
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
|
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
|
||||||
// 库前置内容是否改变了
|
// 库前置或后置内容是否改变了
|
||||||
let isNodeInnerPrefixChange = false
|
const isNodeInnerFixChange = this.checkNodeFixChange(
|
||||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
newNode,
|
||||||
if (item.updateNodeData) {
|
nodeInnerPrefixData,
|
||||||
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData)
|
nodeInnerPostfixData
|
||||||
if (isChange) {
|
)
|
||||||
isNodeInnerPrefixChange = isChange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 主题或主题配置改变了
|
// 主题或主题配置改变了
|
||||||
const isResizeSource = this.checkIsNeedResizeSources()
|
const isResizeSource = this.checkIsNeedResizeSources()
|
||||||
// 节点数据改变了
|
// 节点数据改变了
|
||||||
@ -139,8 +174,10 @@ class Base {
|
|||||||
isResizeSource ||
|
isResizeSource ||
|
||||||
isNodeDataChange ||
|
isNodeDataChange ||
|
||||||
isLayerTypeChange ||
|
isLayerTypeChange ||
|
||||||
newNode.getData('resetRichText') ||
|
(newNode.getData('resetRichText') && // 自定义节点内容可以直接忽略resetRichText
|
||||||
isNodeInnerPrefixChange
|
!newNode.isUseCustomNodeContent()) ||
|
||||||
|
newNode.getData('needUpdate') ||
|
||||||
|
isNodeInnerFixChange
|
||||||
) {
|
) {
|
||||||
newNode.getSize()
|
newNode.getSize()
|
||||||
newNode.needLayout = true
|
newNode.needLayout = true
|
||||||
@ -177,23 +214,21 @@ class Base {
|
|||||||
const isResizeSource = this.checkIsNeedResizeSources()
|
const isResizeSource = this.checkIsNeedResizeSources()
|
||||||
// 点数据改变了
|
// 点数据改变了
|
||||||
const isNodeDataChange = this.checkIsNodeDataChange(lastData, data.data)
|
const isNodeDataChange = this.checkIsNodeDataChange(lastData, data.data)
|
||||||
// 库前置内容是否改变了
|
// 库前置或后置内容是否改变了
|
||||||
let isNodeInnerPrefixChange = false
|
const isNodeInnerFixChange = this.checkNodeFixChange(
|
||||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
newNode,
|
||||||
if (item.updateNodeData) {
|
nodeInnerPrefixData,
|
||||||
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData)
|
nodeInnerPostfixData
|
||||||
if (isChange) {
|
)
|
||||||
isNodeInnerPrefixChange = isChange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 重新计算节点大小和布局
|
// 重新计算节点大小和布局
|
||||||
if (
|
if (
|
||||||
isResizeSource ||
|
isResizeSource ||
|
||||||
isNodeDataChange ||
|
isNodeDataChange ||
|
||||||
isLayerTypeChange ||
|
isLayerTypeChange ||
|
||||||
newNode.getData('resetRichText') ||
|
(newNode.getData('resetRichText') &&
|
||||||
isNodeInnerPrefixChange
|
!newNode.isUseCustomNodeContent()) ||
|
||||||
|
newNode.getData('needUpdate') ||
|
||||||
|
isNodeInnerFixChange
|
||||||
) {
|
) {
|
||||||
newNode.getSize()
|
newNode.getSize()
|
||||||
newNode.needLayout = true
|
newNode.needLayout = true
|
||||||
|
|||||||
@ -2,14 +2,78 @@ import Base from './Base'
|
|||||||
import { walk, asyncRun, degToRad, getNodeIndexInNodeList } from '../utils'
|
import { walk, asyncRun, degToRad, getNodeIndexInNodeList } from '../utils'
|
||||||
import { CONSTANTS } from '../constants/constant'
|
import { CONSTANTS } from '../constants/constant'
|
||||||
import utils from './fishboneUtils'
|
import utils from './fishboneUtils'
|
||||||
|
import { SVG } from '@svgdotjs/svg.js'
|
||||||
|
import { shapeStyleProps } from '../core/render/node/Style'
|
||||||
|
|
||||||
// 鱼骨图
|
// 鱼骨图
|
||||||
class Fishbone extends Base {
|
class Fishbone extends Base {
|
||||||
// 构造函数
|
// 构造函数
|
||||||
constructor(opt = {}) {
|
constructor(opt = {}, layout) {
|
||||||
super(opt)
|
super(opt)
|
||||||
|
this.layout = layout
|
||||||
this.indent = 0.3
|
this.indent = 0.3
|
||||||
this.childIndent = 0.5
|
this.childIndent = 0.5
|
||||||
|
this.fishTail = null
|
||||||
|
this.maxx = 0
|
||||||
|
this.headRatio = 1
|
||||||
|
this.tailRatio = 0.6
|
||||||
|
this.paddingXRatio = 0.3
|
||||||
|
this.fishHeadPathStr =
|
||||||
|
'M4,181 C4,181, 0,177, 4,173 Q 96.09523809523809,0, 288.2857142857143,0 L 288.2857142857143,354 Q 48.047619047619044,354, 8,218.18367346938777 C8,218.18367346938777, 6,214.18367346938777, 8,214.18367346938777 L 41.183673469387756,214.18367346938777 Z'
|
||||||
|
this.fishTailPathStr =
|
||||||
|
'M 606.9342905223708 0 Q 713.1342905223709 -177 819.3342905223708 -177 L 766.2342905223709 0 L 819.3342905223708 177 Q 713.1342905223709 177 606.9342905223708 0 z'
|
||||||
|
this.bindEvent()
|
||||||
|
this.extendShape()
|
||||||
|
this.beforeChange = this.beforeChange.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新渲染时,节点连线是否全部删除
|
||||||
|
// 鱼尾鱼骨图会多渲染一些连线,按需删除无法删除掉,只能全部删除重新创建
|
||||||
|
nodeIsRemoveAllLines(node) {
|
||||||
|
return node.isRoot || node.layerIndex === 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否是带鱼头鱼尾的鱼骨图
|
||||||
|
isFishbone2() {
|
||||||
|
return this.layout === CONSTANTS.LAYOUT.FISHBONE2
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvent() {
|
||||||
|
if (!this.isFishbone2()) return
|
||||||
|
this.onCheckUpdateFishTail = this.onCheckUpdateFishTail.bind(this)
|
||||||
|
this.mindMap.on('afterExecCommand', this.onCheckUpdateFishTail)
|
||||||
|
}
|
||||||
|
|
||||||
|
unBindEvent() {
|
||||||
|
this.mindMap.off('afterExecCommand', this.onCheckUpdateFishTail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扩展节点形状
|
||||||
|
extendShape() {
|
||||||
|
if (!this.isFishbone2()) return
|
||||||
|
// 扩展鱼头形状
|
||||||
|
this.mindMap.addShape({
|
||||||
|
name: 'fishHead',
|
||||||
|
createShape: node => {
|
||||||
|
const rect = SVG(`<path d="${this.fishHeadPathStr}"></path>`)
|
||||||
|
const { width, height } = node.shapeInstance.getNodeSize()
|
||||||
|
rect.size(width, height)
|
||||||
|
return rect
|
||||||
|
},
|
||||||
|
getPadding: ({ width, height, paddingX, paddingY }) => {
|
||||||
|
width += paddingX * 2
|
||||||
|
height += paddingY * 2
|
||||||
|
let shapePaddingX = this.paddingXRatio * width
|
||||||
|
let shapePaddingY = 0
|
||||||
|
width += shapePaddingX * 2
|
||||||
|
const newHeight = width / this.headRatio
|
||||||
|
shapePaddingY = (newHeight - height) / 2
|
||||||
|
return {
|
||||||
|
paddingX: shapePaddingX,
|
||||||
|
paddingY: shapePaddingY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 布局
|
// 布局
|
||||||
@ -17,12 +81,14 @@ class Fishbone extends Base {
|
|||||||
let task = [
|
let task = [
|
||||||
() => {
|
() => {
|
||||||
this.computedBaseValue()
|
this.computedBaseValue()
|
||||||
|
this.addFishTail()
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.computedLeftTopValue()
|
this.computedLeftTopValue()
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.adjustLeftTopValue()
|
this.adjustLeftTopValue()
|
||||||
|
this.updateFishTailPosition()
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
callback(this.root)
|
callback(this.root)
|
||||||
@ -31,14 +97,75 @@ class Fishbone extends Base {
|
|||||||
asyncRun(task)
|
asyncRun(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建鱼尾
|
||||||
|
addFishTail() {
|
||||||
|
if (!this.isFishbone2()) return
|
||||||
|
const exist = this.mindMap.lineDraw.findOne('.smm-layout-fishbone-tail')
|
||||||
|
if (!exist) {
|
||||||
|
this.fishTail = SVG(`<path d="${this.fishTailPathStr}"></path>`)
|
||||||
|
this.fishTail.addClass('smm-layout-fishbone-tail')
|
||||||
|
} else {
|
||||||
|
this.fishTail = exist
|
||||||
|
}
|
||||||
|
const tailHeight = this.root.height
|
||||||
|
const tailWidth = tailHeight * this.tailRatio
|
||||||
|
this.fishTail.size(tailWidth, tailHeight)
|
||||||
|
this.styleFishTail()
|
||||||
|
this.mindMap.lineDraw.add(this.fishTail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果根节点更新了形状样式,那么鱼尾也要更新
|
||||||
|
onCheckUpdateFishTail(name, node, data) {
|
||||||
|
if (name === 'SET_NODE_DATA') {
|
||||||
|
let hasShapeProp = false
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
if (shapeStyleProps.includes(key)) {
|
||||||
|
hasShapeProp = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (hasShapeProp) {
|
||||||
|
this.styleFishTail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
styleFishTail() {
|
||||||
|
this.root.style.shape(this.fishTail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除鱼尾
|
||||||
|
removeFishTail() {
|
||||||
|
const exist = this.mindMap.lineDraw.findOne('.smm-layout-fishbone-tail')
|
||||||
|
if (exist) {
|
||||||
|
exist.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新鱼尾形状位置
|
||||||
|
updateFishTailPosition() {
|
||||||
|
if (!this.isFishbone2()) return
|
||||||
|
this.fishTail.x(this.maxx).cy(this.root.top + this.root.height / 2)
|
||||||
|
}
|
||||||
|
|
||||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||||
computedBaseValue() {
|
computedBaseValue() {
|
||||||
walk(
|
walk(
|
||||||
this.renderer.renderTree,
|
this.renderer.renderTree,
|
||||||
null,
|
null,
|
||||||
(node, parent, isRoot, layerIndex, index, ancestors) => {
|
(node, parent, isRoot, layerIndex, index, ancestors) => {
|
||||||
|
if (isRoot && this.isFishbone2()) {
|
||||||
|
// 将根节点形状强制修改为鱼头
|
||||||
|
node.data.shape = 'fishHead'
|
||||||
|
}
|
||||||
// 创建节点
|
// 创建节点
|
||||||
let newNode = this.createNode(node, parent, isRoot, layerIndex, index, ancestors)
|
let newNode = this.createNode(
|
||||||
|
node,
|
||||||
|
parent,
|
||||||
|
isRoot,
|
||||||
|
layerIndex,
|
||||||
|
index,
|
||||||
|
ancestors
|
||||||
|
)
|
||||||
// 根节点定位在画布中心位置
|
// 根节点定位在画布中心位置
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
this.setNodeCenter(newNode)
|
this.setNodeCenter(newNode)
|
||||||
@ -57,10 +184,14 @@ class Fishbone extends Base {
|
|||||||
// 计算二级节点的top值
|
// 计算二级节点的top值
|
||||||
if (parent._node.isRoot) {
|
if (parent._node.isRoot) {
|
||||||
let marginY = this.getMarginY(layerIndex)
|
let marginY = this.getMarginY(layerIndex)
|
||||||
|
// 带鱼头鱼尾的鱼骨图因为根节点高度比较大,所以二级节点需要向中间靠一点
|
||||||
|
const topOffset = this.isFishbone2() ? parent._node.height / 4 : 0
|
||||||
if (this.checkIsTop(newNode)) {
|
if (this.checkIsTop(newNode)) {
|
||||||
newNode.top = parent._node.top - newNode.height - marginY
|
newNode.top =
|
||||||
|
parent._node.top - newNode.height - marginY + topOffset
|
||||||
} else {
|
} else {
|
||||||
newNode.top = parent._node.top + parent._node.height + marginY
|
newNode.top =
|
||||||
|
parent._node.top + parent._node.height + marginY - topOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,8 +213,11 @@ class Fishbone extends Base {
|
|||||||
(node, parent, isRoot, layerIndex) => {
|
(node, parent, isRoot, layerIndex) => {
|
||||||
if (node.isRoot) {
|
if (node.isRoot) {
|
||||||
let marginX = this.getMarginX(layerIndex + 1)
|
let marginX = this.getMarginX(layerIndex + 1)
|
||||||
let topTotalLeft = node.left + node.width + node.height + marginX
|
const heightOffsetRatio = this.isFishbone2() ? 2 : 1
|
||||||
let bottomTotalLeft = node.left + node.width + node.height + marginX
|
let topTotalLeft =
|
||||||
|
node.left + node.width + node.height / heightOffsetRatio + marginX
|
||||||
|
let bottomTotalLeft =
|
||||||
|
node.left + node.width + node.height / heightOffsetRatio + marginX
|
||||||
node.children.forEach(item => {
|
node.children.forEach(item => {
|
||||||
if (this.checkIsTop(item)) {
|
if (this.checkIsTop(item)) {
|
||||||
item.left = topTotalLeft
|
item.left = topTotalLeft
|
||||||
@ -133,19 +267,27 @@ class Fishbone extends Base {
|
|||||||
if (node.isRoot) {
|
if (node.isRoot) {
|
||||||
let topTotalLeft = 0
|
let topTotalLeft = 0
|
||||||
let bottomTotalLeft = 0
|
let bottomTotalLeft = 0
|
||||||
|
let maxx = -Infinity
|
||||||
node.children.forEach(item => {
|
node.children.forEach(item => {
|
||||||
if (this.checkIsTop(item)) {
|
if (this.checkIsTop(item)) {
|
||||||
item.left += topTotalLeft
|
item.left += topTotalLeft
|
||||||
this.updateChildren(item.children, 'left', topTotalLeft)
|
this.updateChildren(item.children, 'left', topTotalLeft)
|
||||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||||
|
if (right > maxx) {
|
||||||
|
maxx = right
|
||||||
|
}
|
||||||
topTotalLeft += right - left
|
topTotalLeft += right - left
|
||||||
} else {
|
} else {
|
||||||
item.left += bottomTotalLeft
|
item.left += bottomTotalLeft
|
||||||
this.updateChildren(item.children, 'left', bottomTotalLeft)
|
this.updateChildren(item.children, 'left', bottomTotalLeft)
|
||||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||||
|
if (right > maxx) {
|
||||||
|
maxx = right
|
||||||
|
}
|
||||||
bottomTotalLeft += right - left
|
bottomTotalLeft += right - left
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.maxx = maxx
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
@ -249,7 +391,8 @@ class Fishbone extends Base {
|
|||||||
// 水平线段到二级节点的连线
|
// 水平线段到二级节点的连线
|
||||||
let marginY = this.getMarginY(item.layerIndex)
|
let marginY = this.getMarginY(item.layerIndex)
|
||||||
let nodeLineX = item.left
|
let nodeLineX = item.left
|
||||||
let offset = node.height / 2 + marginY
|
let offset =
|
||||||
|
node.height / 2 + marginY - (this.isFishbone2() ? node.height / 4 : 0)
|
||||||
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||||
let line = this.lineDraw.path()
|
let line = this.lineDraw.path()
|
||||||
if (this.checkIsTop(item)) {
|
if (this.checkIsTop(item)) {
|
||||||
@ -277,11 +420,14 @@ class Fishbone extends Base {
|
|||||||
let nodeHalfTop = node.top + node.height / 2
|
let nodeHalfTop = node.top + node.height / 2
|
||||||
let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1)
|
let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1)
|
||||||
let line = this.lineDraw.path()
|
let line = this.lineDraw.path()
|
||||||
|
const lineEndX = this.isFishbone2()
|
||||||
|
? this.maxx
|
||||||
|
: maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||||
line.plot(
|
line.plot(
|
||||||
this.transformPath(
|
this.transformPath(
|
||||||
`M ${node.left + node.width},${nodeHalfTop} L ${
|
`M ${
|
||||||
maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
node.left + node.width
|
||||||
},${nodeHalfTop}`
|
},${nodeHalfTop} L ${lineEndX},${nodeHalfTop}`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
node.style.line(line)
|
node.style.line(line)
|
||||||
@ -406,6 +552,16 @@ class Fishbone extends Base {
|
|||||||
rect.size(width, expandBtnSize).x(0).y(height)
|
rect.size(width, expandBtnSize).x(0).y(height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换切换为其他结构时的处理
|
||||||
|
beforeChange() {
|
||||||
|
// 删除鱼尾
|
||||||
|
if (!this.isFishbone2()) return
|
||||||
|
this.root.nodeData.data.shape = CONSTANTS.SHAPE.RECTANGLE
|
||||||
|
this.removeFishTail()
|
||||||
|
this.unBindEvent()
|
||||||
|
this.mindMap.removeShape('fishHead')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Fishbone
|
export default Fishbone
|
||||||
|
|||||||
@ -1,370 +0,0 @@
|
|||||||
import Base from './Base'
|
|
||||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
|
||||||
import { CONSTANTS } from '../utils/constant'
|
|
||||||
|
|
||||||
const degToRad = deg => {
|
|
||||||
return (Math.PI / 180) * deg
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下方鱼骨图
|
|
||||||
class Fishbone extends Base {
|
|
||||||
// 构造函数
|
|
||||||
constructor(opt = {}) {
|
|
||||||
super(opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 布局
|
|
||||||
doLayout(callback) {
|
|
||||||
let task = [
|
|
||||||
() => {
|
|
||||||
this.computedBaseValue()
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.computedLeftTopValue()
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.adjustLeftTopValue()
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
callback(this.root)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
asyncRun(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
|
||||||
computedBaseValue() {
|
|
||||||
walk(
|
|
||||||
this.renderer.renderTree,
|
|
||||||
null,
|
|
||||||
(node, parent, isRoot, layerIndex, index) => {
|
|
||||||
// 创建节点
|
|
||||||
let newNode = this.createNode(node, parent, isRoot, layerIndex)
|
|
||||||
// 根节点定位在画布中心位置
|
|
||||||
if (isRoot) {
|
|
||||||
this.setNodeCenter(newNode)
|
|
||||||
} else {
|
|
||||||
// 非根节点
|
|
||||||
// 三级及以下节点以上级方向为准
|
|
||||||
if (parent._node.dir) {
|
|
||||||
newNode.dir = parent._node.dir
|
|
||||||
} else {
|
|
||||||
// 节点生长方向
|
|
||||||
newNode.dir =
|
|
||||||
index % 2 === 0
|
|
||||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
|
||||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
|
||||||
}
|
|
||||||
// 计算二级节点的top值
|
|
||||||
if (parent._node.isRoot) {
|
|
||||||
newNode.top = parent._node.top + parent._node.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!node.data.expand) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遍历节点树计算节点的left、top
|
|
||||||
computedLeftTopValue() {
|
|
||||||
walk(
|
|
||||||
this.root,
|
|
||||||
null,
|
|
||||||
(node, parent, isRoot, layerIndex, index) => {
|
|
||||||
if (node.isRoot) {
|
|
||||||
let totalLeft = node.left + node.width
|
|
||||||
node.children.forEach(item => {
|
|
||||||
item.left = totalLeft
|
|
||||||
totalLeft += item.width
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (layerIndex === 1 && node.children) {
|
|
||||||
// 遍历二级节点的子节点
|
|
||||||
let startLeft = node.left + node.width * 0.5
|
|
||||||
let totalTop =
|
|
||||||
node.top +
|
|
||||||
node.height +
|
|
||||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
|
||||||
|
|
||||||
node.children.forEach(item => {
|
|
||||||
item.left = startLeft
|
|
||||||
item.top =
|
|
||||||
totalTop +
|
|
||||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
|
||||||
totalTop +=
|
|
||||||
item.height +
|
|
||||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (layerIndex > 1 && node.children) {
|
|
||||||
// 遍历三级及以下节点的子节点
|
|
||||||
let startLeft = node.left + node.width * 0.5
|
|
||||||
let totalTop =
|
|
||||||
node.top -
|
|
||||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
|
||||||
node.children.forEach(item => {
|
|
||||||
item.left = startLeft
|
|
||||||
item.top = totalTop - item.height
|
|
||||||
totalTop -=
|
|
||||||
item.height +
|
|
||||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调整节点left、top
|
|
||||||
adjustLeftTopValue() {
|
|
||||||
walk(
|
|
||||||
this.root,
|
|
||||||
null,
|
|
||||||
(node, parent, isRoot, layerIndex) => {
|
|
||||||
if (!node.getData('expand')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 调整top
|
|
||||||
let len = node.children.length
|
|
||||||
// 调整三级节点的top
|
|
||||||
// if (layerIndex === 2 && len > 0) {
|
|
||||||
// let totalHeight = node.children.reduce((h, item) => {
|
|
||||||
// return h + item.height
|
|
||||||
// }, 0)
|
|
||||||
// this.updateBrothersTop(node, totalHeight)
|
|
||||||
// }
|
|
||||||
if (layerIndex > 2 && len > 0) {
|
|
||||||
let totalHeight = node.children.reduce((h, item) => {
|
|
||||||
return (
|
|
||||||
h +
|
|
||||||
item.height +
|
|
||||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
|
||||||
)
|
|
||||||
}, 0)
|
|
||||||
this.updateBrothersTop(node, -totalHeight)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(node, parent) => {
|
|
||||||
// 将二级节点的子节点移到上方
|
|
||||||
if (parent && parent.isRoot) {
|
|
||||||
// 遍历二级节点的子节点
|
|
||||||
let totalHeight = 0
|
|
||||||
let totalHeight2 = 0
|
|
||||||
node.children.forEach(item => {
|
|
||||||
// 调整top
|
|
||||||
let hasChildren = this.getNodeActChildrenLength(item) > 0
|
|
||||||
let nodeTotalHeight = this.getNodeAreaHeight(item)
|
|
||||||
let offset =
|
|
||||||
hasChildren > 0
|
|
||||||
? nodeTotalHeight -
|
|
||||||
item.height -
|
|
||||||
(hasChildren ? item.expandBtnSize : 0)
|
|
||||||
: 0
|
|
||||||
let _top = totalHeight + offset
|
|
||||||
item.top += _top
|
|
||||||
// 调整left
|
|
||||||
let offsetLeft =
|
|
||||||
(totalHeight2 + nodeTotalHeight) /
|
|
||||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
|
||||||
item.left += offsetLeft
|
|
||||||
totalHeight += offset
|
|
||||||
totalHeight2 += nodeTotalHeight
|
|
||||||
// 同步更新后代节点
|
|
||||||
this.updateChildrenPro(item.children, {
|
|
||||||
top: _top,
|
|
||||||
left: offsetLeft
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 调整二级节点的子节点的left值
|
|
||||||
if (node.isRoot) {
|
|
||||||
let totalLeft = 0
|
|
||||||
node.children.forEach(item => {
|
|
||||||
item.left += totalLeft
|
|
||||||
this.updateChildren(item.children, 'left', totalLeft)
|
|
||||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
|
||||||
totalLeft += right - left
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归计算节点的宽度
|
|
||||||
getNodeAreaHeight(node) {
|
|
||||||
let totalHeight = 0
|
|
||||||
let loop = node => {
|
|
||||||
totalHeight +=
|
|
||||||
node.height +
|
|
||||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
|
||||||
if (node.children.length) {
|
|
||||||
node.children.forEach(item => {
|
|
||||||
loop(item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loop(node)
|
|
||||||
return totalHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调整兄弟节点的left
|
|
||||||
updateBrothersLeft(node) {
|
|
||||||
let childrenList = node.children
|
|
||||||
let totalAddWidth = 0
|
|
||||||
childrenList.forEach(item => {
|
|
||||||
item.left += totalAddWidth
|
|
||||||
if (item.children && item.children.length) {
|
|
||||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
|
||||||
}
|
|
||||||
// let areaWidth = this.getNodeAreaWidth(item)
|
|
||||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
|
||||||
let areaWidth = right - left
|
|
||||||
let difference = areaWidth - item.width
|
|
||||||
if (difference > 0) {
|
|
||||||
totalAddWidth += difference
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调整兄弟节点的top
|
|
||||||
updateBrothersTop(node, addHeight) {
|
|
||||||
if (node.parent && !node.parent.isRoot) {
|
|
||||||
let childrenList = node.parent.children
|
|
||||||
let index = getNodeIndexInNodeList(node, childrenList)
|
|
||||||
childrenList.forEach((item, _index) => {
|
|
||||||
if (item.hasCustomPosition()) {
|
|
||||||
// 适配自定义位置
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let _offset = 0
|
|
||||||
// 下面的节点往下移
|
|
||||||
if (_index > index) {
|
|
||||||
_offset = addHeight
|
|
||||||
}
|
|
||||||
item.top += _offset
|
|
||||||
// 同步更新子节点的位置
|
|
||||||
if (item.children && item.children.length) {
|
|
||||||
this.updateChildren(item.children, 'top', _offset)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 更新父节点的位置
|
|
||||||
this.updateBrothersTop(node.parent, node.layerIndex === 3 ? 0 : addHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制连线,连接该节点到其子节点
|
|
||||||
renderLine(node, lines, style) {
|
|
||||||
if (node.children.length <= 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
let { left, top, width, height, expandBtnSize } = node
|
|
||||||
let len = node.children.length
|
|
||||||
if (node.isRoot) {
|
|
||||||
// 当前节点是根节点
|
|
||||||
let prevBother = node
|
|
||||||
// 根节点的子节点是和根节点同一水平线排列
|
|
||||||
node.children.forEach((item, index) => {
|
|
||||||
let x1 = prevBother.left + prevBother.width
|
|
||||||
let x2 = item.left
|
|
||||||
let y = node.top + node.height / 2
|
|
||||||
let path = `M ${x1},${y} L ${x2},${y}`
|
|
||||||
lines[index].plot(path)
|
|
||||||
style && style(lines[index], item)
|
|
||||||
prevBother = item
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 当前节点为非根节点
|
|
||||||
let maxy = -Infinity
|
|
||||||
let miny = Infinity
|
|
||||||
let maxx = -Infinity
|
|
||||||
let x = node.left + node.width * 0.3
|
|
||||||
node.children.forEach((item, index) => {
|
|
||||||
if (item.left > maxx) {
|
|
||||||
maxx = item.left
|
|
||||||
}
|
|
||||||
let y = item.top + item.height / 2
|
|
||||||
if (y > maxy) {
|
|
||||||
maxy = y
|
|
||||||
}
|
|
||||||
if (y < miny) {
|
|
||||||
miny = y
|
|
||||||
}
|
|
||||||
// 水平线
|
|
||||||
if (node.layerIndex > 1) {
|
|
||||||
let path = `M ${x},${y} L ${item.left},${y}`
|
|
||||||
lines[index].plot(path)
|
|
||||||
style && style(lines[index], item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 竖线
|
|
||||||
if (len > 0) {
|
|
||||||
let line = this.lineDraw.path()
|
|
||||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
|
||||||
let lineLength = maxx - node.left - node.width * 0.3
|
|
||||||
if (node.parent && node.parent.isRoot) {
|
|
||||||
line.plot(
|
|
||||||
`M ${x},${top + height} L ${x + lineLength},${
|
|
||||||
top +
|
|
||||||
height +
|
|
||||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
line.plot(`M ${x},${top} L ${x},${miny}`)
|
|
||||||
}
|
|
||||||
node.style.line(line)
|
|
||||||
node._lines.push(line)
|
|
||||||
style && style(line, node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染按钮
|
|
||||||
renderExpandBtn(node, btn) {
|
|
||||||
let { width, height, expandBtnSize, isRoot } = node
|
|
||||||
if (!isRoot) {
|
|
||||||
let { translateX, translateY } = btn.transform()
|
|
||||||
if (node.parent && node.parent.isRoot) {
|
|
||||||
btn.translate(
|
|
||||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
|
||||||
height + expandBtnSize / 2 - translateY
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
btn.translate(
|
|
||||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
|
||||||
-expandBtnSize / 2 - translateY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建概要节点
|
|
||||||
renderGeneralization(node, gLine, gNode) {
|
|
||||||
let {
|
|
||||||
top,
|
|
||||||
bottom,
|
|
||||||
right,
|
|
||||||
generalizationLineMargin,
|
|
||||||
generalizationNodeMargin
|
|
||||||
} = this.getNodeBoundaries(node, 'h')
|
|
||||||
let x1 = right + generalizationLineMargin
|
|
||||||
let y1 = top
|
|
||||||
let x2 = right + generalizationLineMargin
|
|
||||||
let y2 = bottom
|
|
||||||
let cx = x1 + 20
|
|
||||||
let cy = y1 + (y2 - y1) / 2
|
|
||||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
|
||||||
gLine.plot(path)
|
|
||||||
gNode.left = right + generalizationNodeMargin
|
|
||||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Fishbone
|
|
||||||
@ -1,351 +0,0 @@
|
|||||||
import Base from './Base'
|
|
||||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
|
||||||
import { CONSTANTS } from '../utils/constant'
|
|
||||||
|
|
||||||
const degToRad = deg => {
|
|
||||||
return (Math.PI / 180) * deg
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上方鱼骨图
|
|
||||||
class Fishbone extends Base {
|
|
||||||
// 构造函数
|
|
||||||
constructor(opt = {}) {
|
|
||||||
super(opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 布局
|
|
||||||
doLayout(callback) {
|
|
||||||
let task = [
|
|
||||||
() => {
|
|
||||||
this.computedBaseValue()
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.computedLeftTopValue()
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.adjustLeftTopValue()
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
callback(this.root)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
asyncRun(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
|
||||||
computedBaseValue() {
|
|
||||||
walk(
|
|
||||||
this.renderer.renderTree,
|
|
||||||
null,
|
|
||||||
(node, parent, isRoot, layerIndex, index) => {
|
|
||||||
// 创建节点
|
|
||||||
let newNode = this.createNode(node, parent, isRoot, layerIndex)
|
|
||||||
// 根节点定位在画布中心位置
|
|
||||||
if (isRoot) {
|
|
||||||
this.setNodeCenter(newNode)
|
|
||||||
} else {
|
|
||||||
// 非根节点
|
|
||||||
// 三级及以下节点以上级方向为准
|
|
||||||
if (parent._node.dir) {
|
|
||||||
newNode.dir = parent._node.dir
|
|
||||||
} else {
|
|
||||||
// 节点生长方向
|
|
||||||
newNode.dir =
|
|
||||||
index % 2 === 0
|
|
||||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
|
||||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
|
||||||
}
|
|
||||||
// 计算二级节点的top值
|
|
||||||
if (parent._node.isRoot) {
|
|
||||||
newNode.top = parent._node.top - newNode.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!node.data.expand) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遍历节点树计算节点的left、top
|
|
||||||
computedLeftTopValue() {
|
|
||||||
walk(
|
|
||||||
this.root,
|
|
||||||
null,
|
|
||||||
(node, parent, isRoot, layerIndex, index) => {
|
|
||||||
if (node.isRoot) {
|
|
||||||
let totalLeft = node.left + node.width
|
|
||||||
node.children.forEach(item => {
|
|
||||||
item.left = totalLeft
|
|
||||||
totalLeft += item.width
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (layerIndex >= 1 && node.children) {
|
|
||||||
// 遍历三级及以下节点的子节点
|
|
||||||
let startLeft = node.left + node.width * 0.5
|
|
||||||
let totalTop =
|
|
||||||
node.top +
|
|
||||||
node.height +
|
|
||||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
|
||||||
node.children.forEach(item => {
|
|
||||||
item.left = startLeft
|
|
||||||
item.top += totalTop
|
|
||||||
totalTop +=
|
|
||||||
item.height +
|
|
||||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调整节点left、top
|
|
||||||
adjustLeftTopValue() {
|
|
||||||
walk(
|
|
||||||
this.root,
|
|
||||||
null,
|
|
||||||
(node, parent, isRoot, layerIndex) => {
|
|
||||||
if (!node.getData('expand')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 调整top
|
|
||||||
let len = node.children.length
|
|
||||||
// 调整三级及以下节点的top
|
|
||||||
if (parent && !parent.isRoot && len > 0) {
|
|
||||||
let totalHeight = node.children.reduce((h, item) => {
|
|
||||||
return (
|
|
||||||
h +
|
|
||||||
item.height +
|
|
||||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
|
||||||
)
|
|
||||||
}, 0)
|
|
||||||
this.updateBrothersTop(node, totalHeight)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(node, parent) => {
|
|
||||||
// 将二级节点的子节点移到上方
|
|
||||||
if (parent && parent.isRoot) {
|
|
||||||
// 遍历二级节点的子节点
|
|
||||||
let totalHeight = 0
|
|
||||||
node.children.forEach(item => {
|
|
||||||
// 调整top
|
|
||||||
let nodeTotalHeight = this.getNodeAreaHeight(item)
|
|
||||||
let _top = item.top
|
|
||||||
item.top =
|
|
||||||
node.top - (item.top - node.top) - nodeTotalHeight + node.height
|
|
||||||
// 调整left
|
|
||||||
let offsetLeft =
|
|
||||||
(nodeTotalHeight + totalHeight) /
|
|
||||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
|
||||||
item.left += offsetLeft
|
|
||||||
totalHeight += nodeTotalHeight
|
|
||||||
// 同步更新后代节点
|
|
||||||
this.updateChildrenPro(item.children, {
|
|
||||||
top: item.top - _top,
|
|
||||||
left: offsetLeft
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 调整二级节点的子节点的left值
|
|
||||||
if (node.isRoot) {
|
|
||||||
let totalLeft = 0
|
|
||||||
node.children.forEach(item => {
|
|
||||||
item.left += totalLeft
|
|
||||||
this.updateChildren(item.children, 'left', totalLeft)
|
|
||||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
|
||||||
totalLeft += right - left
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归计算节点的宽度
|
|
||||||
getNodeAreaHeight(node) {
|
|
||||||
let totalHeight = 0
|
|
||||||
let loop = node => {
|
|
||||||
totalHeight +=
|
|
||||||
node.height +
|
|
||||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
|
||||||
if (node.children.length) {
|
|
||||||
node.children.forEach(item => {
|
|
||||||
loop(item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loop(node)
|
|
||||||
return totalHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调整兄弟节点的left
|
|
||||||
updateBrothersLeft(node) {
|
|
||||||
let childrenList = node.children
|
|
||||||
let totalAddWidth = 0
|
|
||||||
childrenList.forEach(item => {
|
|
||||||
item.left += totalAddWidth
|
|
||||||
if (item.children && item.children.length) {
|
|
||||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
|
||||||
}
|
|
||||||
// let areaWidth = this.getNodeAreaWidth(item)
|
|
||||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
|
||||||
let areaWidth = right - left
|
|
||||||
let difference = areaWidth - item.width
|
|
||||||
if (difference > 0) {
|
|
||||||
totalAddWidth += difference
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调整兄弟节点的top
|
|
||||||
updateBrothersTop(node, addHeight) {
|
|
||||||
if (node.parent && !node.parent.isRoot) {
|
|
||||||
let childrenList = node.parent.children
|
|
||||||
let index = getNodeIndexInNodeList(node, childrenList)
|
|
||||||
childrenList.forEach((item, _index) => {
|
|
||||||
if (item.hasCustomPosition()) {
|
|
||||||
// 适配自定义位置
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let _offset = 0
|
|
||||||
// 下面的节点往下移
|
|
||||||
if (_index > index) {
|
|
||||||
_offset = addHeight
|
|
||||||
}
|
|
||||||
item.top += _offset
|
|
||||||
// 同步更新子节点的位置
|
|
||||||
if (item.children && item.children.length) {
|
|
||||||
this.updateChildren(item.children, 'top', _offset)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 更新父节点的位置
|
|
||||||
this.updateBrothersTop(node.parent, addHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制连线,连接该节点到其子节点
|
|
||||||
renderLine(node, lines, style) {
|
|
||||||
if (node.children.length <= 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
let { left, top, width, height, expandBtnSize } = node
|
|
||||||
let len = node.children.length
|
|
||||||
if (node.isRoot) {
|
|
||||||
// 当前节点是根节点
|
|
||||||
let prevBother = node
|
|
||||||
// 根节点的子节点是和根节点同一水平线排列
|
|
||||||
node.children.forEach((item, index) => {
|
|
||||||
let x1 = prevBother.left + prevBother.width
|
|
||||||
let x2 = item.left
|
|
||||||
let y = node.top + node.height / 2
|
|
||||||
let path = `M ${x1},${y} L ${x2},${y}`
|
|
||||||
lines[index].plot(path)
|
|
||||||
style && style(lines[index], item)
|
|
||||||
prevBother = item
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 当前节点为非根节点
|
|
||||||
let maxy = -Infinity
|
|
||||||
let miny = Infinity
|
|
||||||
let maxx = -Infinity
|
|
||||||
let x = node.left + node.width * 0.3
|
|
||||||
node.children.forEach((item, index) => {
|
|
||||||
if (item.left > maxx) {
|
|
||||||
maxx = item.left
|
|
||||||
}
|
|
||||||
let y = item.top + item.height / 2
|
|
||||||
if (y > maxy) {
|
|
||||||
maxy = y
|
|
||||||
}
|
|
||||||
if (y < miny) {
|
|
||||||
miny = y
|
|
||||||
}
|
|
||||||
// 水平线
|
|
||||||
if (node.layerIndex > 1) {
|
|
||||||
let path = `M ${x},${y} L ${item.left},${y}`
|
|
||||||
lines[index].plot(path)
|
|
||||||
style && style(lines[index], item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 竖线
|
|
||||||
if (len > 0) {
|
|
||||||
let line = this.lineDraw.path()
|
|
||||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
|
||||||
let lineLength = maxx - node.left - node.width * 0.3
|
|
||||||
if (
|
|
||||||
node.parent &&
|
|
||||||
node.parent.isRoot &&
|
|
||||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
|
||||||
) {
|
|
||||||
line.plot(
|
|
||||||
`M ${x},${top} L ${x + lineLength},${
|
|
||||||
top -
|
|
||||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (node.parent && node.parent.isRoot) {
|
|
||||||
line.plot(
|
|
||||||
`M ${x},${top} L ${x + lineLength},${
|
|
||||||
top -
|
|
||||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.style.line(line)
|
|
||||||
node._lines.push(line)
|
|
||||||
style && style(line, node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染按钮
|
|
||||||
renderExpandBtn(node, btn) {
|
|
||||||
let { width, height, expandBtnSize, isRoot } = node
|
|
||||||
if (!isRoot) {
|
|
||||||
let { translateX, translateY } = btn.transform()
|
|
||||||
if (node.parent && node.parent.isRoot) {
|
|
||||||
btn.translate(
|
|
||||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
|
||||||
-expandBtnSize / 2 - translateY
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
btn.translate(
|
|
||||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
|
||||||
height + expandBtnSize / 2 - translateY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建概要节点
|
|
||||||
renderGeneralization(node, gLine, gNode) {
|
|
||||||
let {
|
|
||||||
top,
|
|
||||||
bottom,
|
|
||||||
right,
|
|
||||||
generalizationLineMargin,
|
|
||||||
generalizationNodeMargin
|
|
||||||
} = this.getNodeBoundaries(node, 'h')
|
|
||||||
let x1 = right + generalizationLineMargin
|
|
||||||
let y1 = top
|
|
||||||
let x2 = right + generalizationLineMargin
|
|
||||||
let y2 = bottom
|
|
||||||
let cx = x1 + 20
|
|
||||||
let cy = y1 + (y2 - y1) / 2
|
|
||||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
|
||||||
gLine.plot(path)
|
|
||||||
gNode.left = right + generalizationNodeMargin
|
|
||||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Fishbone
|
|
||||||
@ -35,7 +35,14 @@ class VerticalTimeline extends Base {
|
|||||||
this.renderer.renderTree,
|
this.renderer.renderTree,
|
||||||
null,
|
null,
|
||||||
(cur, parent, isRoot, layerIndex, index, ancestors) => {
|
(cur, parent, isRoot, layerIndex, index, ancestors) => {
|
||||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex, index, ancestors)
|
let newNode = this.createNode(
|
||||||
|
cur,
|
||||||
|
parent,
|
||||||
|
isRoot,
|
||||||
|
layerIndex,
|
||||||
|
index,
|
||||||
|
ancestors
|
||||||
|
)
|
||||||
// 根节点定位在画布中心位置
|
// 根节点定位在画布中心位置
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
this.setNodeCenter(newNode)
|
this.setNodeCenter(newNode)
|
||||||
@ -45,12 +52,18 @@ class VerticalTimeline extends Base {
|
|||||||
// 三级及以下节点以上级为准
|
// 三级及以下节点以上级为准
|
||||||
if (parent._node.dir) {
|
if (parent._node.dir) {
|
||||||
newNode.dir = parent._node.dir
|
newNode.dir = parent._node.dir
|
||||||
|
} else {
|
||||||
|
if (this.layout === CONSTANTS.LAYOUT.VERTICAL_TIMELINE2) {
|
||||||
|
newNode.dir = CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||||
|
} else if (this.layout === CONSTANTS.LAYOUT.VERTICAL_TIMELINE3) {
|
||||||
|
newNode.dir = CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||||
} else {
|
} else {
|
||||||
newNode.dir =
|
newNode.dir =
|
||||||
index % 2 === 0
|
index % 2 === 0
|
||||||
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 定位二级节点的left
|
// 定位二级节点的left
|
||||||
if (parent._node.isRoot) {
|
if (parent._node.isRoot) {
|
||||||
newNode.left =
|
newNode.left =
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||||
|
|
||||||
const getNodeText = node => {
|
const getNodeText = node => {
|
||||||
// 优先找出其中的text类型的子节点
|
if (node.type === 'list') return ''
|
||||||
let textChild = (node.children || []).find(item => {
|
let textStr = ''
|
||||||
return item.type === 'text'
|
|
||||||
|
;(node.children || []).forEach(item => {
|
||||||
|
if (['inlineCode', 'text'].includes(item.type)) {
|
||||||
|
textStr += item.value || ''
|
||||||
|
} else {
|
||||||
|
textStr += getNodeText(item)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
// 没有找到,那么直接使用第一个子节点
|
|
||||||
textChild = textChild || node.children[0]
|
return textStr
|
||||||
if (textChild) {
|
|
||||||
if (textChild.value !== undefined) {
|
|
||||||
return textChild.value
|
|
||||||
}
|
|
||||||
return getNodeText(textChild)
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理list的情况
|
// 处理list的情况
|
||||||
|
|||||||
@ -253,7 +253,9 @@ const transformToXmind = async (data, name) => {
|
|||||||
}
|
}
|
||||||
// 标签
|
// 标签
|
||||||
if (node.data.tag !== undefined) {
|
if (node.data.tag !== undefined) {
|
||||||
newData.labels = node.data.tag || []
|
newData.labels = (node.data.tag || []).map(item => {
|
||||||
|
return typeof item === 'object' && item !== null ? item.text : item
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// 图片
|
// 图片
|
||||||
handleNodeImageToXmind(node, newNode, waitLoadImageList, imageList)
|
handleNodeImageToXmind(node, newNode, waitLoadImageList, imageList)
|
||||||
|
|||||||
@ -23,6 +23,8 @@ const styleProps = [
|
|||||||
'associativeLineTextFontFamily'
|
'associativeLineTextFontFamily'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ASSOCIATIVE_LINE_TEXT_EDIT_WRAP = 'associative-line-text-edit-warp'
|
||||||
|
|
||||||
// 关联线插件
|
// 关联线插件
|
||||||
class AssociativeLine {
|
class AssociativeLine {
|
||||||
constructor(opt = {}) {
|
constructor(opt = {}) {
|
||||||
@ -62,9 +64,11 @@ class AssociativeLine {
|
|||||||
this[item] = associativeLineControlsMethods[item].bind(this)
|
this[item] = associativeLineControlsMethods[item].bind(this)
|
||||||
})
|
})
|
||||||
// 关联线文字相关方法
|
// 关联线文字相关方法
|
||||||
|
this.showTextEdit = false
|
||||||
Object.keys(associativeLineTextMethods).forEach(item => {
|
Object.keys(associativeLineTextMethods).forEach(item => {
|
||||||
this[item] = associativeLineTextMethods[item].bind(this)
|
this[item] = associativeLineTextMethods[item].bind(this)
|
||||||
})
|
})
|
||||||
|
this.mindMap.addEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
||||||
this.bindEvent()
|
this.bindEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,6 +161,7 @@ class AssociativeLine {
|
|||||||
// 取消激活关联线
|
// 取消激活关联线
|
||||||
if (!this.isControlPointMousedown) {
|
if (!this.isControlPointMousedown) {
|
||||||
this.clearActiveLine()
|
this.clearActiveLine()
|
||||||
|
this.renderAllLines()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,6 +171,7 @@ class AssociativeLine {
|
|||||||
this.completeCreateLine(node)
|
this.completeCreateLine(node)
|
||||||
} else {
|
} else {
|
||||||
this.clearActiveLine()
|
this.clearActiveLine()
|
||||||
|
this.renderAllLines()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,7 +286,7 @@ class AssociativeLine {
|
|||||||
.stroke({
|
.stroke({
|
||||||
width: associativeLineWidth,
|
width: associativeLineWidth,
|
||||||
color: associativeLineColor,
|
color: associativeLineColor,
|
||||||
dasharray: associativeLineDasharray || [6, 4]
|
dasharray: associativeLineDasharray || '6,4'
|
||||||
})
|
})
|
||||||
.fill({ color: 'none' })
|
.fill({ color: 'none' })
|
||||||
path.plot(pathStr)
|
path.plot(pathStr)
|
||||||
@ -348,7 +354,7 @@ class AssociativeLine {
|
|||||||
.stroke({
|
.stroke({
|
||||||
width: associativeLineWidth,
|
width: associativeLineWidth,
|
||||||
color: associativeLineColor,
|
color: associativeLineColor,
|
||||||
dasharray: associativeLineDasharray || [6, 4]
|
dasharray: associativeLineDasharray || '6,4'
|
||||||
})
|
})
|
||||||
.fill({ color: 'none' })
|
.fill({ color: 'none' })
|
||||||
clickPath
|
clickPath
|
||||||
@ -382,6 +388,7 @@ class AssociativeLine {
|
|||||||
if (this.controlPoint2) {
|
if (this.controlPoint2) {
|
||||||
this.controlPoint2.stroke({ color: associativeLineActiveColor })
|
this.controlPoint2.stroke({ color: associativeLineActiveColor })
|
||||||
}
|
}
|
||||||
|
this.updateTextPos(path, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 激活某根关联线
|
// 激活某根关联线
|
||||||
@ -461,7 +468,7 @@ class AssociativeLine {
|
|||||||
.stroke({
|
.stroke({
|
||||||
width: associativeLineWidth,
|
width: associativeLineWidth,
|
||||||
color: associativeLineColor,
|
color: associativeLineColor,
|
||||||
dasharray: associativeLineDasharray || [6, 4]
|
dasharray: associativeLineDasharray || '6,4'
|
||||||
})
|
})
|
||||||
.fill({ color: 'none' })
|
.fill({ color: 'none' })
|
||||||
// 箭头
|
// 箭头
|
||||||
@ -742,11 +749,13 @@ class AssociativeLine {
|
|||||||
|
|
||||||
// 插件被移除前做的事情
|
// 插件被移除前做的事情
|
||||||
beforePluginRemove() {
|
beforePluginRemove() {
|
||||||
|
this.mindMap.deleteEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
||||||
this.unBindEvent()
|
this.unBindEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件被卸载前做的事情
|
// 插件被卸载前做的事情
|
||||||
beforePluginDestroy() {
|
beforePluginDestroy() {
|
||||||
|
this.mindMap.deleteEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
||||||
this.unBindEvent()
|
this.unBindEvent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,18 @@ class Demonstrate {
|
|||||||
this.mindMap.opt.demonstrateConfig || {}
|
this.mindMap.opt.demonstrateConfig || {}
|
||||||
)
|
)
|
||||||
this.needRestorePerformanceMode = false
|
this.needRestorePerformanceMode = false
|
||||||
|
this.onConfigUpdate = this.onConfigUpdate.bind(this)
|
||||||
|
this.mindMap.on('after_update_config', this.onConfigUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听配置更新
|
||||||
|
onConfigUpdate(opt) {
|
||||||
|
if (typeof opt.demonstrateConfig !== 'undefined') {
|
||||||
|
this.config = {
|
||||||
|
...this.config,
|
||||||
|
...opt.demonstrateConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进入演示模式
|
// 进入演示模式
|
||||||
@ -417,11 +429,13 @@ class Demonstrate {
|
|||||||
// 插件被移除前做的事情
|
// 插件被移除前做的事情
|
||||||
beforePluginRemove() {
|
beforePluginRemove() {
|
||||||
this.unBindEvent()
|
this.unBindEvent()
|
||||||
|
this.mindMap.off('after_update_config', this.onConfigUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件被卸载前做的事情
|
// 插件被卸载前做的事情
|
||||||
beforePluginDestroy() {
|
beforePluginDestroy() {
|
||||||
this.unBindEvent()
|
this.unBindEvent()
|
||||||
|
this.mindMap.off('after_update_config', this.onConfigUpdate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -407,7 +407,12 @@ class Drag extends Base {
|
|||||||
TIMELINE,
|
TIMELINE,
|
||||||
TIMELINE2,
|
TIMELINE2,
|
||||||
VERTICAL_TIMELINE,
|
VERTICAL_TIMELINE,
|
||||||
FISHBONE
|
VERTICAL_TIMELINE2,
|
||||||
|
VERTICAL_TIMELINE3,
|
||||||
|
FISHBONE,
|
||||||
|
FISHBONE2,
|
||||||
|
RIGHT_FISHBONE,
|
||||||
|
RIGHT_FISHBONE2
|
||||||
} = CONSTANTS.LAYOUT
|
} = CONSTANTS.LAYOUT
|
||||||
this.overlapNode = null
|
this.overlapNode = null
|
||||||
this.prevNode = null
|
this.prevNode = null
|
||||||
@ -443,9 +448,14 @@ class Drag extends Base {
|
|||||||
this.handleTimeLine2(node)
|
this.handleTimeLine2(node)
|
||||||
break
|
break
|
||||||
case VERTICAL_TIMELINE:
|
case VERTICAL_TIMELINE:
|
||||||
|
case VERTICAL_TIMELINE2:
|
||||||
|
case VERTICAL_TIMELINE3:
|
||||||
this.handleLogicalStructure(node)
|
this.handleLogicalStructure(node)
|
||||||
break
|
break
|
||||||
case FISHBONE:
|
case FISHBONE:
|
||||||
|
case FISHBONE2:
|
||||||
|
case RIGHT_FISHBONE:
|
||||||
|
case RIGHT_FISHBONE2:
|
||||||
this.handleFishbone(node)
|
this.handleFishbone(node)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@ -469,7 +479,12 @@ class Drag extends Base {
|
|||||||
TIMELINE,
|
TIMELINE,
|
||||||
TIMELINE2,
|
TIMELINE2,
|
||||||
VERTICAL_TIMELINE,
|
VERTICAL_TIMELINE,
|
||||||
FISHBONE
|
VERTICAL_TIMELINE2,
|
||||||
|
VERTICAL_TIMELINE3,
|
||||||
|
FISHBONE,
|
||||||
|
FISHBONE2,
|
||||||
|
RIGHT_FISHBONE,
|
||||||
|
RIGHT_FISHBONE2
|
||||||
} = CONSTANTS.LAYOUT
|
} = CONSTANTS.LAYOUT
|
||||||
const { LEFT, TOP, RIGHT, BOTTOM } = CONSTANTS.LAYOUT_GROW_DIR
|
const { LEFT, TOP, RIGHT, BOTTOM } = CONSTANTS.LAYOUT_GROW_DIR
|
||||||
const layerIndex = this.overlapNode.layerIndex
|
const layerIndex = this.overlapNode.layerIndex
|
||||||
@ -563,6 +578,8 @@ class Drag extends Base {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
case VERTICAL_TIMELINE:
|
case VERTICAL_TIMELINE:
|
||||||
|
case VERTICAL_TIMELINE2:
|
||||||
|
case VERTICAL_TIMELINE3:
|
||||||
if (layerIndex === 0) {
|
if (layerIndex === 0) {
|
||||||
x =
|
x =
|
||||||
lastNodeRect.originLeft +
|
lastNodeRect.originLeft +
|
||||||
@ -580,6 +597,9 @@ class Drag extends Base {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
case FISHBONE:
|
case FISHBONE:
|
||||||
|
case FISHBONE2:
|
||||||
|
case RIGHT_FISHBONE:
|
||||||
|
case RIGHT_FISHBONE2:
|
||||||
if (layerIndex <= 1) {
|
if (layerIndex <= 1) {
|
||||||
notRenderPlaceholder = true
|
notRenderPlaceholder = true
|
||||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
|
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
|
||||||
@ -655,6 +675,8 @@ class Drag extends Base {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
case VERTICAL_TIMELINE:
|
case VERTICAL_TIMELINE:
|
||||||
|
case VERTICAL_TIMELINE2:
|
||||||
|
case VERTICAL_TIMELINE3:
|
||||||
if (layerIndex === 0) {
|
if (layerIndex === 0) {
|
||||||
rotate = true
|
rotate = true
|
||||||
}
|
}
|
||||||
@ -668,6 +690,9 @@ class Drag extends Base {
|
|||||||
halfPlaceholderHeight
|
halfPlaceholderHeight
|
||||||
break
|
break
|
||||||
case FISHBONE:
|
case FISHBONE:
|
||||||
|
case FISHBONE2:
|
||||||
|
case RIGHT_FISHBONE:
|
||||||
|
case RIGHT_FISHBONE2:
|
||||||
if (layerIndex <= 1) {
|
if (layerIndex <= 1) {
|
||||||
notRenderPlaceholder = true
|
notRenderPlaceholder = true
|
||||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
|
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
|
||||||
@ -703,7 +728,12 @@ class Drag extends Base {
|
|||||||
MIND_MAP,
|
MIND_MAP,
|
||||||
TIMELINE2,
|
TIMELINE2,
|
||||||
VERTICAL_TIMELINE,
|
VERTICAL_TIMELINE,
|
||||||
FISHBONE
|
VERTICAL_TIMELINE2,
|
||||||
|
VERTICAL_TIMELINE3,
|
||||||
|
FISHBONE,
|
||||||
|
FISHBONE2,
|
||||||
|
RIGHT_FISHBONE,
|
||||||
|
RIGHT_FISHBONE2
|
||||||
} = CONSTANTS.LAYOUT
|
} = CONSTANTS.LAYOUT
|
||||||
switch (this.mindMap.opt.layout) {
|
switch (this.mindMap.opt.layout) {
|
||||||
case LOGICAL_STRUCTURE:
|
case LOGICAL_STRUCTURE:
|
||||||
@ -713,7 +743,12 @@ class Drag extends Base {
|
|||||||
case MIND_MAP:
|
case MIND_MAP:
|
||||||
case TIMELINE2:
|
case TIMELINE2:
|
||||||
case VERTICAL_TIMELINE:
|
case VERTICAL_TIMELINE:
|
||||||
|
case VERTICAL_TIMELINE2:
|
||||||
|
case VERTICAL_TIMELINE3:
|
||||||
case FISHBONE:
|
case FISHBONE:
|
||||||
|
case FISHBONE2:
|
||||||
|
case RIGHT_FISHBONE:
|
||||||
|
case RIGHT_FISHBONE2:
|
||||||
return node.dir
|
return node.dir
|
||||||
default:
|
default:
|
||||||
return ''
|
return ''
|
||||||
@ -725,17 +760,22 @@ class Drag extends Base {
|
|||||||
handleVerticalCheck(node, checkList, isReverse = false) {
|
handleVerticalCheck(node, checkList, isReverse = false) {
|
||||||
const { layout } = this.mindMap.opt
|
const { layout } = this.mindMap.opt
|
||||||
const { LAYOUT, LAYOUT_GROW_DIR } = CONSTANTS
|
const { LAYOUT, LAYOUT_GROW_DIR } = CONSTANTS
|
||||||
const { VERTICAL_TIMELINE, FISHBONE } = LAYOUT
|
const {
|
||||||
const { BOTTOM, LEFT } = LAYOUT_GROW_DIR
|
VERTICAL_TIMELINE,
|
||||||
|
VERTICAL_TIMELINE2,
|
||||||
|
VERTICAL_TIMELINE3,
|
||||||
|
FISHBONE,
|
||||||
|
FISHBONE2,
|
||||||
|
RIGHT_FISHBONE,
|
||||||
|
RIGHT_FISHBONE2
|
||||||
|
} = LAYOUT
|
||||||
|
const { LEFT } = LAYOUT_GROW_DIR
|
||||||
const mouseMoveX = this.mouseMoveX
|
const mouseMoveX = this.mouseMoveX
|
||||||
const mouseMoveY = this.mouseMoveY
|
const mouseMoveY = this.mouseMoveY
|
||||||
const nodeRect = this.getNodeRect(node)
|
const nodeRect = this.getNodeRect(node)
|
||||||
const dir = this.getNewChildNodeDir(node)
|
const dir = this.getNewChildNodeDir(node)
|
||||||
const layerIndex = node.layerIndex
|
const layerIndex = node.layerIndex
|
||||||
if (
|
if (isReverse) {
|
||||||
isReverse ||
|
|
||||||
(layout === FISHBONE && dir === BOTTOM && layerIndex >= 3)
|
|
||||||
) {
|
|
||||||
checkList = checkList.reverse()
|
checkList = checkList.reverse()
|
||||||
}
|
}
|
||||||
let oneFourthHeight = nodeRect.originHeight / 4
|
let oneFourthHeight = nodeRect.originHeight / 4
|
||||||
@ -770,6 +810,8 @@ class Drag extends Base {
|
|||||||
let notRenderLine = false
|
let notRenderLine = false
|
||||||
switch (layout) {
|
switch (layout) {
|
||||||
case VERTICAL_TIMELINE:
|
case VERTICAL_TIMELINE:
|
||||||
|
case VERTICAL_TIMELINE2:
|
||||||
|
case VERTICAL_TIMELINE3:
|
||||||
if (layerIndex === 1) {
|
if (layerIndex === 1) {
|
||||||
x =
|
x =
|
||||||
nodeRect.originLeft +
|
nodeRect.originLeft +
|
||||||
@ -777,6 +819,11 @@ class Drag extends Base {
|
|||||||
this.placeholderWidth / 2
|
this.placeholderWidth / 2
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case RIGHT_FISHBONE:
|
||||||
|
case RIGHT_FISHBONE2:
|
||||||
|
x =
|
||||||
|
nodeRect.originLeft + nodeRect.originWidth - this.placeholderWidth
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
if (checkIsPrevNode) {
|
if (checkIsPrevNode) {
|
||||||
@ -791,6 +838,9 @@ class Drag extends Base {
|
|||||||
this.placeholderHeight / 2
|
this.placeholderHeight / 2
|
||||||
switch (layout) {
|
switch (layout) {
|
||||||
case FISHBONE:
|
case FISHBONE:
|
||||||
|
case FISHBONE2:
|
||||||
|
case RIGHT_FISHBONE:
|
||||||
|
case RIGHT_FISHBONE2:
|
||||||
if (layerIndex === 2) {
|
if (layerIndex === 2) {
|
||||||
notRenderLine = true
|
notRenderLine = true
|
||||||
y =
|
y =
|
||||||
@ -820,6 +870,9 @@ class Drag extends Base {
|
|||||||
this.placeholderHeight / 2
|
this.placeholderHeight / 2
|
||||||
switch (layout) {
|
switch (layout) {
|
||||||
case FISHBONE:
|
case FISHBONE:
|
||||||
|
case FISHBONE2:
|
||||||
|
case RIGHT_FISHBONE:
|
||||||
|
case RIGHT_FISHBONE2:
|
||||||
if (layerIndex === 2) {
|
if (layerIndex === 2) {
|
||||||
notRenderLine = true
|
notRenderLine = true
|
||||||
y =
|
y =
|
||||||
@ -856,7 +909,14 @@ class Drag extends Base {
|
|||||||
handleHorizontalCheck(node, checkList) {
|
handleHorizontalCheck(node, checkList) {
|
||||||
const { layout } = this.mindMap.opt
|
const { layout } = this.mindMap.opt
|
||||||
const { LAYOUT } = CONSTANTS
|
const { LAYOUT } = CONSTANTS
|
||||||
const { FISHBONE, TIMELINE, TIMELINE2 } = LAYOUT
|
const {
|
||||||
|
FISHBONE,
|
||||||
|
FISHBONE2,
|
||||||
|
RIGHT_FISHBONE,
|
||||||
|
RIGHT_FISHBONE2,
|
||||||
|
TIMELINE,
|
||||||
|
TIMELINE2
|
||||||
|
} = LAYOUT
|
||||||
let mouseMoveX = this.mouseMoveX
|
let mouseMoveX = this.mouseMoveX
|
||||||
let mouseMoveY = this.mouseMoveY
|
let mouseMoveY = this.mouseMoveY
|
||||||
let nodeRect = this.getNodeRect(node)
|
let nodeRect = this.getNodeRect(node)
|
||||||
@ -896,6 +956,9 @@ class Drag extends Base {
|
|||||||
this.placeholderWidth / 2
|
this.placeholderWidth / 2
|
||||||
break
|
break
|
||||||
case FISHBONE:
|
case FISHBONE:
|
||||||
|
case FISHBONE2:
|
||||||
|
case RIGHT_FISHBONE:
|
||||||
|
case RIGHT_FISHBONE2:
|
||||||
if (layerIndex === 1) {
|
if (layerIndex === 1) {
|
||||||
notRenderLine = true
|
notRenderLine = true
|
||||||
y =
|
y =
|
||||||
@ -907,7 +970,11 @@ class Drag extends Base {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
if (checkIsPrevNode) {
|
if (checkIsPrevNode) {
|
||||||
|
if ([RIGHT_FISHBONE, RIGHT_FISHBONE2].includes(layout)) {
|
||||||
|
this.nextNode = node
|
||||||
|
} else {
|
||||||
this.prevNode = node
|
this.prevNode = node
|
||||||
|
}
|
||||||
this.setPlaceholderRect({
|
this.setPlaceholderRect({
|
||||||
x:
|
x:
|
||||||
nodeRect.originRight +
|
nodeRect.originRight +
|
||||||
@ -918,7 +985,11 @@ class Drag extends Base {
|
|||||||
notRenderLine
|
notRenderLine
|
||||||
})
|
})
|
||||||
} else if (checkIsNextNode) {
|
} else if (checkIsNextNode) {
|
||||||
|
if ([RIGHT_FISHBONE, RIGHT_FISHBONE2].includes(layout)) {
|
||||||
|
this.prevNode = node
|
||||||
|
} else {
|
||||||
this.nextNode = node
|
this.nextNode = node
|
||||||
|
}
|
||||||
this.setPlaceholderRect({
|
this.setPlaceholderRect({
|
||||||
x:
|
x:
|
||||||
nodeRect.originLeft -
|
nodeRect.originLeft -
|
||||||
@ -1142,7 +1213,11 @@ class Drag extends Base {
|
|||||||
this.handleHorizontalCheck(node, checkList)
|
this.handleHorizontalCheck(node, checkList)
|
||||||
} else {
|
} else {
|
||||||
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
|
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
|
||||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP && node.layerIndex === 2) {
|
const is2LayerTop =
|
||||||
|
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP && node.layerIndex === 2
|
||||||
|
const is2MoreLayerBottom =
|
||||||
|
node.dir === CONSTANTS.LAYOUT_GROW_DIR.BOTTOM && node.layerIndex >= 3
|
||||||
|
if (is2LayerTop || is2MoreLayerBottom) {
|
||||||
this.handleVerticalCheck(node, checkList, true)
|
this.handleVerticalCheck(node, checkList, true)
|
||||||
} else {
|
} else {
|
||||||
this.handleVerticalCheck(node, checkList)
|
this.handleVerticalCheck(node, checkList)
|
||||||
|
|||||||
@ -128,7 +128,13 @@ class Export {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// svg转png
|
// svg转png
|
||||||
svgToPng(svgSrc, transparent, clipData = null) {
|
svgToPng(
|
||||||
|
svgSrc,
|
||||||
|
transparent,
|
||||||
|
clipData = null,
|
||||||
|
fitBg = false,
|
||||||
|
format = 'image/png'
|
||||||
|
) {
|
||||||
const { maxCanvasSize, minExportImgCanvasScale } = this.mindMap.opt
|
const { maxCanvasSize, minExportImgCanvasScale } = this.mindMap.opt
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
@ -138,6 +144,7 @@ class Export {
|
|||||||
try {
|
try {
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
const dpr = Math.max(window.devicePixelRatio, minExportImgCanvasScale)
|
const dpr = Math.max(window.devicePixelRatio, minExportImgCanvasScale)
|
||||||
|
// 图片原始大小
|
||||||
let imgWidth = img.width
|
let imgWidth = img.width
|
||||||
let imgHeight = img.height
|
let imgHeight = img.height
|
||||||
// 如果是裁减操作的话,那么需要手动添加内边距,及调整图片大小为实际的裁减区域的大小,不要忘了内边距哦
|
// 如果是裁减操作的话,那么需要手动添加内边距,及调整图片大小为实际的裁减区域的大小,不要忘了内边距哦
|
||||||
@ -149,10 +156,39 @@ class Export {
|
|||||||
imgWidth = clipData.width + paddingX * 2
|
imgWidth = clipData.width + paddingX * 2
|
||||||
imgHeight = clipData.height + paddingY * 2
|
imgHeight = clipData.height + paddingY * 2
|
||||||
}
|
}
|
||||||
|
// 适配背景图片的大小
|
||||||
|
let fitBgImgWidth = 0
|
||||||
|
let fitBgImgHeight = 0
|
||||||
|
const { backgroundImage } = this.mindMap.themeConfig
|
||||||
|
if (fitBg && backgroundImage && !transparent) {
|
||||||
|
const bgImgSize = await new Promise(resolve => {
|
||||||
|
const bgImg = new Image()
|
||||||
|
bgImg.onload = () => {
|
||||||
|
resolve([bgImg.width, bgImg.height])
|
||||||
|
}
|
||||||
|
bgImg.onerror = () => {
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
bgImg.src = backgroundImage
|
||||||
|
})
|
||||||
|
if (bgImgSize) {
|
||||||
|
const imgRatio = imgWidth / imgHeight
|
||||||
|
const bgRatio = bgImgSize[0] / bgImgSize[1]
|
||||||
|
if (imgRatio > bgRatio) {
|
||||||
|
fitBgImgWidth = imgWidth
|
||||||
|
fitBgImgHeight = imgWidth / bgRatio
|
||||||
|
} else {
|
||||||
|
fitBgImgHeight = imgHeight
|
||||||
|
fitBgImgWidth = imgHeight * bgRatio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// 检查是否超出canvas支持的像素上限
|
// 检查是否超出canvas支持的像素上限
|
||||||
// canvas大小需要乘以dpr
|
// canvas大小需要乘以dpr
|
||||||
let canvasWidth = imgWidth * dpr
|
let scaleX = 1
|
||||||
let canvasHeight = imgHeight * dpr
|
let scaleY = 1
|
||||||
|
let canvasWidth = (fitBgImgWidth || imgWidth) * dpr
|
||||||
|
let canvasHeight = (fitBgImgHeight || imgHeight) * dpr
|
||||||
if (canvasWidth > maxCanvasSize || canvasHeight > maxCanvasSize) {
|
if (canvasWidth > maxCanvasSize || canvasHeight > maxCanvasSize) {
|
||||||
let newWidth = null
|
let newWidth = null
|
||||||
let newHeight = null
|
let newHeight = null
|
||||||
@ -170,6 +206,8 @@ class Export {
|
|||||||
newWidth,
|
newWidth,
|
||||||
newHeight
|
newHeight
|
||||||
)
|
)
|
||||||
|
scaleX = res[0] / canvasWidth
|
||||||
|
scaleY = res[1] / canvasHeight
|
||||||
canvasWidth = res[0]
|
canvasWidth = res[0]
|
||||||
canvasHeight = res[1]
|
canvasHeight = res[1]
|
||||||
}
|
}
|
||||||
@ -177,6 +215,7 @@ class Export {
|
|||||||
canvas.height = canvasHeight
|
canvas.height = canvasHeight
|
||||||
const styleWidth = canvasWidth / dpr
|
const styleWidth = canvasWidth / dpr
|
||||||
const styleHeight = canvasHeight / dpr
|
const styleHeight = canvasHeight / dpr
|
||||||
|
// canvas元素实际上的大小
|
||||||
canvas.style.width = styleWidth + 'px'
|
canvas.style.width = styleWidth + 'px'
|
||||||
canvas.style.height = styleHeight + 'px'
|
canvas.style.height = styleHeight + 'px'
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
@ -187,6 +226,10 @@ class Export {
|
|||||||
}
|
}
|
||||||
// 图片绘制到canvas里
|
// 图片绘制到canvas里
|
||||||
// 如果有裁减数据,那么需要进行裁减
|
// 如果有裁减数据,那么需要进行裁减
|
||||||
|
const fitBgLeft =
|
||||||
|
(fitBgImgWidth > 0 ? (fitBgImgWidth - imgWidth) / 2 : 0) * scaleX
|
||||||
|
const fitBgTop =
|
||||||
|
(fitBgImgHeight > 0 ? (fitBgImgHeight - imgHeight) / 2 : 0) * scaleY
|
||||||
if (clipData) {
|
if (clipData) {
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
img,
|
img,
|
||||||
@ -194,15 +237,21 @@ class Export {
|
|||||||
clipData.top,
|
clipData.top,
|
||||||
clipData.width,
|
clipData.width,
|
||||||
clipData.height,
|
clipData.height,
|
||||||
paddingX,
|
paddingX * scaleX + fitBgLeft,
|
||||||
paddingY,
|
paddingY * scaleY + fitBgTop,
|
||||||
clipData.width,
|
clipData.width * scaleX,
|
||||||
clipData.height
|
clipData.height * scaleY
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ctx.drawImage(img, 0, 0, styleWidth, styleHeight)
|
ctx.drawImage(
|
||||||
|
img,
|
||||||
|
fitBgLeft,
|
||||||
|
fitBgTop,
|
||||||
|
imgWidth * scaleX,
|
||||||
|
imgHeight * scaleY
|
||||||
|
)
|
||||||
}
|
}
|
||||||
resolve(canvas.toDataURL())
|
resolve(canvas.toDataURL(format))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error)
|
reject(error)
|
||||||
}
|
}
|
||||||
@ -280,16 +329,35 @@ class Export {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出为指定格式的图片
|
||||||
|
async _image(format, name, transparent = false, node = null, fitBg = false) {
|
||||||
|
this.mindMap.renderer.textEdit.hideEditTextBox()
|
||||||
|
this.handleNodeExport(node)
|
||||||
|
const { str, clipData } = await this.getSvgData(node)
|
||||||
|
const svgUrl = await this.fixSvgStrAndToBlob(str)
|
||||||
|
const res = await this.svgToPng(
|
||||||
|
svgUrl,
|
||||||
|
transparent,
|
||||||
|
clipData,
|
||||||
|
fitBg,
|
||||||
|
format
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// 导出为png
|
// 导出为png
|
||||||
/**
|
/**
|
||||||
* 方法1.把svg的图片都转化成data:url格式,再转换
|
* 方法1.把svg的图片都转化成data:url格式,再转换
|
||||||
* 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换
|
* 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换
|
||||||
*/
|
*/
|
||||||
async png(name, transparent = false, node = null) {
|
async png(...args) {
|
||||||
this.handleNodeExport(node)
|
const res = await this._image('image/png', ...args)
|
||||||
const { str, clipData } = await this.getSvgData(node)
|
return res
|
||||||
const svgUrl = await this.fixSvgStrAndToBlob(str)
|
}
|
||||||
const res = await this.svgToPng(svgUrl, transparent, clipData)
|
|
||||||
|
// 导出为jpg
|
||||||
|
async jpg(...args) {
|
||||||
|
const res = await this._image('image/jpg', ...args)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,11 +373,11 @@ class Export {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出为pdf
|
// 导出为pdf
|
||||||
async pdf(name, transparent = false) {
|
async pdf(name, transparent = false, fitBg = false) {
|
||||||
if (!this.mindMap.doExportPDF) {
|
if (!this.mindMap.doExportPDF) {
|
||||||
throw new Error('请注册ExportPDF插件')
|
throw new Error('请注册ExportPDF插件')
|
||||||
}
|
}
|
||||||
const img = await this.png(name, transparent)
|
const img = await this.png(name, transparent, null, fitBg)
|
||||||
// 使用jspdf库
|
// 使用jspdf库
|
||||||
// await this.mindMap.doExportPDF.pdf(name, img)
|
// await this.mindMap.doExportPDF.pdf(name, img)
|
||||||
// 使用pdf-lib库
|
// 使用pdf-lib库
|
||||||
@ -330,6 +398,7 @@ class Export {
|
|||||||
|
|
||||||
// 导出为svg
|
// 导出为svg
|
||||||
async svg(name) {
|
async svg(name) {
|
||||||
|
this.mindMap.renderer.textEdit.hideEditTextBox()
|
||||||
const { node } = await this.getSvgData()
|
const { node } = await this.getSvgData()
|
||||||
node.first().before(SVG(`<title>${name}</title>`))
|
node.first().before(SVG(`<title>${name}</title>`))
|
||||||
await this.drawBackgroundToSvg(node)
|
await this.drawBackgroundToSvg(node)
|
||||||
|
|||||||
@ -107,14 +107,13 @@ class Formula {
|
|||||||
|
|
||||||
// 给指定的节点插入指定公式
|
// 给指定的节点插入指定公式
|
||||||
insertFormulaToNode(node, formula) {
|
insertFormulaToNode(node, formula) {
|
||||||
let richTextPlugin = this.mindMap.richText
|
const richTextPlugin = this.mindMap.richText
|
||||||
richTextPlugin.showEditText({ node })
|
richTextPlugin.showEditText({ node })
|
||||||
richTextPlugin.quill.insertEmbed(
|
richTextPlugin.quill.insertEmbed(
|
||||||
richTextPlugin.quill.getLength() - 1,
|
richTextPlugin.quill.getLength() - 1,
|
||||||
'formula',
|
'formula',
|
||||||
formula
|
formula
|
||||||
)
|
)
|
||||||
richTextPlugin.setTextStyleIfNotRichText(richTextPlugin.node)
|
|
||||||
richTextPlugin.hideEditText([node])
|
richTextPlugin.hideEditText([node])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,8 +126,18 @@ class Formula {
|
|||||||
for (const el of els)
|
for (const el of els)
|
||||||
nodeText = nodeText.replace(
|
nodeText = nodeText.replace(
|
||||||
el.outerHTML,
|
el.outerHTML,
|
||||||
`\$${el.getAttribute('data-value')}\$`
|
`$${el.getAttribute('data-value')}$`
|
||||||
)
|
)
|
||||||
|
// 如果开启了实时渲染,那么意味公式转换为源码时会影响节点尺寸,需要派发事件触发渲染
|
||||||
|
if (this.mindMap.opt.openRealtimeRenderOnNodeTextEdit) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.mindMap.emit('node_text_edit_change', {
|
||||||
|
node: this.mindMap.richText.node,
|
||||||
|
text: this.mindMap.richText.getEditText(),
|
||||||
|
richText: true
|
||||||
|
})
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nodeText
|
return nodeText
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
isWhite,
|
isWhite,
|
||||||
isTransparent,
|
isTransparent,
|
||||||
getVisibleColorFromTheme,
|
getVisibleColorFromTheme
|
||||||
readBlob
|
|
||||||
} from '../utils/index'
|
} from '../utils/index'
|
||||||
|
|
||||||
// 小地图插件
|
// 小地图插件
|
||||||
|
|||||||
100
simple-mind-map/src/plugins/NodeBase64ImageStorage.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { walk, createUid } from '../utils/index'
|
||||||
|
|
||||||
|
// 修改base64格式的节点图片在数据中的存储方式
|
||||||
|
// 将base64格式的图片以key-map的形式存储在根节点的imgMap字段里,其他节点只保存key,避免不同的节点引用相同的图片重复存储的问题,普通url格式的图片不处理
|
||||||
|
class NodeBase64ImageStorage {
|
||||||
|
constructor(opt) {
|
||||||
|
this.opt = opt
|
||||||
|
this.mindMap = opt.mindMap
|
||||||
|
this.bindEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvent() {
|
||||||
|
this.onBeforeAddHistory = this.onBeforeAddHistory.bind(this)
|
||||||
|
this.mindMap.on('beforeAddHistory', this.onBeforeAddHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
unBindEvent() {
|
||||||
|
this.mindMap.off('beforeAddHistory', this.onBeforeAddHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
isBase64ImgUrl(url) {
|
||||||
|
return /^data:/.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
isImageKey(url) {
|
||||||
|
return /^smm_img_key_/.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
createImageKey() {
|
||||||
|
return 'smm_img_key_' + createUid()
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeAddHistory() {
|
||||||
|
const renderTree = this.mindMap.renderer.renderTree
|
||||||
|
if (!renderTree) return
|
||||||
|
let imgMap = renderTree.data.imgMap
|
||||||
|
if (!imgMap) {
|
||||||
|
imgMap = renderTree.data.imgMap = {}
|
||||||
|
}
|
||||||
|
const useIds = []
|
||||||
|
|
||||||
|
const getImgIds = () => {
|
||||||
|
return Object.keys(imgMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getImgId = image => {
|
||||||
|
return getImgIds().find(id => {
|
||||||
|
return imgMap[id] === image
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(renderTree, null, node => {
|
||||||
|
const image = node.data.image
|
||||||
|
if (image) {
|
||||||
|
// 如果是base64图片url
|
||||||
|
if (this.isBase64ImgUrl(image)) {
|
||||||
|
// 检查该图片是否已存在
|
||||||
|
const hasId = getImgId(image)
|
||||||
|
if (hasId) {
|
||||||
|
// 已存在则直接使用现有的key
|
||||||
|
useIds.push(hasId)
|
||||||
|
node.data.image = hasId
|
||||||
|
} else {
|
||||||
|
// 不存在则生成key,并存储
|
||||||
|
const newId = this.createImageKey()
|
||||||
|
node.data.image = newId
|
||||||
|
imgMap[newId] = image
|
||||||
|
useIds.push(newId)
|
||||||
|
}
|
||||||
|
} else if (this.isImageKey(image)) {
|
||||||
|
// 如果是key,那么收集一下
|
||||||
|
if (getImgIds().includes(image)) {
|
||||||
|
useIds.push(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除已无节点引用的图片
|
||||||
|
getImgIds().forEach(id => {
|
||||||
|
if (!useIds.includes(id)) {
|
||||||
|
delete imgMap[id]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件被移除前做的事情
|
||||||
|
beforePluginRemove() {
|
||||||
|
this.unBindEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件被卸载前做的事情
|
||||||
|
beforePluginDestroy() {
|
||||||
|
this.unBindEvent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeBase64ImageStorage.instanceName = 'nodeBase64ImageStorage'
|
||||||
|
|
||||||
|
export default NodeBase64ImageStorage
|
||||||
@ -31,12 +31,14 @@ class NodeImgAdjust {
|
|||||||
this.onMousemove = this.onMousemove.bind(this)
|
this.onMousemove = this.onMousemove.bind(this)
|
||||||
this.onMouseup = this.onMouseup.bind(this)
|
this.onMouseup = this.onMouseup.bind(this)
|
||||||
this.onRenderEnd = this.onRenderEnd.bind(this)
|
this.onRenderEnd = this.onRenderEnd.bind(this)
|
||||||
|
this.onScale = this.onScale.bind(this)
|
||||||
this.mindMap.on('node_img_mouseleave', this.onNodeImgMouseleave)
|
this.mindMap.on('node_img_mouseleave', this.onNodeImgMouseleave)
|
||||||
this.mindMap.on('node_img_mousemove', this.onNodeImgMousemove)
|
this.mindMap.on('node_img_mousemove', this.onNodeImgMousemove)
|
||||||
this.mindMap.on('mousemove', this.onMousemove)
|
this.mindMap.on('mousemove', this.onMousemove)
|
||||||
this.mindMap.on('mouseup', this.onMouseup)
|
this.mindMap.on('mouseup', this.onMouseup)
|
||||||
this.mindMap.on('node_mouseup', this.onMouseup)
|
this.mindMap.on('node_mouseup', this.onMouseup)
|
||||||
this.mindMap.on('node_tree_render_end', this.onRenderEnd)
|
this.mindMap.on('node_tree_render_end', this.onRenderEnd)
|
||||||
|
this.mindMap.on('scale', this.onScale)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解绑事件
|
// 解绑事件
|
||||||
@ -47,6 +49,15 @@ class NodeImgAdjust {
|
|||||||
this.mindMap.off('mouseup', this.onMouseup)
|
this.mindMap.off('mouseup', this.onMouseup)
|
||||||
this.mindMap.off('node_mouseup', this.onMouseup)
|
this.mindMap.off('node_mouseup', this.onMouseup)
|
||||||
this.mindMap.off('node_tree_render_end', this.onRenderEnd)
|
this.mindMap.off('node_tree_render_end', this.onRenderEnd)
|
||||||
|
this.mindMap.off('scale', this.onScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前操作按钮正在显示时缩放了画布,那么需要更新位置
|
||||||
|
onScale() {
|
||||||
|
if (this.node && this.img && this.isShowHandleEl) {
|
||||||
|
this.rect = this.img.rbox()
|
||||||
|
this.setHandleElRect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 节点图片鼠标移动事件
|
// 节点图片鼠标移动事件
|
||||||
@ -122,7 +133,11 @@ class NodeImgAdjust {
|
|||||||
|
|
||||||
// 创建调整按钮元素
|
// 创建调整按钮元素
|
||||||
createResizeBtnEl() {
|
createResizeBtnEl() {
|
||||||
const { imgResizeBtnSize } = this.mindMap.opt
|
const {
|
||||||
|
imgResizeBtnSize,
|
||||||
|
customResizeBtnInnerHTML,
|
||||||
|
customDeleteBtnInnerHTML
|
||||||
|
} = this.mindMap.opt
|
||||||
// 容器元素
|
// 容器元素
|
||||||
this.handleEl = document.createElement('div')
|
this.handleEl = document.createElement('div')
|
||||||
this.handleEl.style.cssText = `
|
this.handleEl.style.cssText = `
|
||||||
@ -134,7 +149,7 @@ class NodeImgAdjust {
|
|||||||
this.handleEl.className = 'node-img-handle'
|
this.handleEl.className = 'node-img-handle'
|
||||||
// 调整按钮元素
|
// 调整按钮元素
|
||||||
const btnEl = document.createElement('div')
|
const btnEl = document.createElement('div')
|
||||||
btnEl.innerHTML = btnsSvg.imgAdjust
|
btnEl.innerHTML = customResizeBtnInnerHTML || btnsSvg.imgAdjust
|
||||||
btnEl.style.cssText = `
|
btnEl.style.cssText = `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -179,7 +194,7 @@ class NodeImgAdjust {
|
|||||||
const btnRemove = document.createElement('div')
|
const btnRemove = document.createElement('div')
|
||||||
this.handleEl.prepend(btnRemove)
|
this.handleEl.prepend(btnRemove)
|
||||||
btnRemove.className = 'node-image-remove'
|
btnRemove.className = 'node-image-remove'
|
||||||
btnRemove.innerHTML = btnsSvg.remove
|
btnRemove.innerHTML = customDeleteBtnInnerHTML || btnsSvg.remove
|
||||||
btnRemove.style.cssText = `
|
btnRemove.style.cssText = `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;top:0;color:#fff;
|
right: 0;top:0;color:#fff;
|
||||||
@ -206,6 +221,7 @@ class NodeImgAdjust {
|
|||||||
}
|
}
|
||||||
if (!stop) {
|
if (!stop) {
|
||||||
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null })
|
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null })
|
||||||
|
this.mindMap.emit('delete_node_img_from_delete_btn', this.node)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// 添加元素到页面
|
// 添加元素到页面
|
||||||
@ -215,6 +231,7 @@ class NodeImgAdjust {
|
|||||||
|
|
||||||
// 鼠标按钮按下事件
|
// 鼠标按钮按下事件
|
||||||
onMousedown(e) {
|
onMousedown(e) {
|
||||||
|
this.mindMap.emit('node_img_adjust_btn_mousedown', this.node)
|
||||||
this.isMousedown = true
|
this.isMousedown = true
|
||||||
this.mousedownDrawTransform = this.mindMap.draw.transform()
|
this.mousedownDrawTransform = this.mindMap.draw.transform()
|
||||||
// 隐藏节点实际图片
|
// 隐藏节点实际图片
|
||||||
|
|||||||
@ -1,149 +1,68 @@
|
|||||||
import {
|
import {
|
||||||
formatDataToArray,
|
formatDataToArray,
|
||||||
walk,
|
walk,
|
||||||
getTopAncestorsFomNodeList,
|
|
||||||
getNodeListBoundingRect,
|
getNodeListBoundingRect,
|
||||||
createUid
|
createUid
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
import {
|
||||||
// 解析要添加外框的节点实例列表
|
parseAddNodeList,
|
||||||
const parseAddNodeList = list => {
|
getNodeOuterFrameList
|
||||||
// 找出顶层节点
|
} from './outerFrame/outerFrameUtils'
|
||||||
list = getTopAncestorsFomNodeList(list)
|
import outerFrameTextMethods from './outerFrame/outerFrameText'
|
||||||
const cache = {}
|
|
||||||
const uidToParent = {}
|
|
||||||
// 找出列表中节点在兄弟节点中的索引,并和父节点关联起来
|
|
||||||
list.forEach(node => {
|
|
||||||
const parent = node.parent
|
|
||||||
if (parent) {
|
|
||||||
const pUid = parent.uid
|
|
||||||
uidToParent[pUid] = parent
|
|
||||||
const index = node.getIndexInBrothers()
|
|
||||||
const data = {
|
|
||||||
node,
|
|
||||||
index
|
|
||||||
}
|
|
||||||
if (cache[pUid]) {
|
|
||||||
if (
|
|
||||||
!cache[pUid].find(item => {
|
|
||||||
return item.index === data.index
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
cache[pUid].push(data)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cache[pUid] = [data]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const res = []
|
|
||||||
Object.keys(cache).forEach(uid => {
|
|
||||||
const indexList = cache[uid]
|
|
||||||
const parentNode = uidToParent[uid]
|
|
||||||
if (indexList.length > 1) {
|
|
||||||
// 多个节点
|
|
||||||
const rangeList = indexList
|
|
||||||
.map(item => {
|
|
||||||
return item.index
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a - b
|
|
||||||
})
|
|
||||||
const minIndex = rangeList[0]
|
|
||||||
const maxIndex = rangeList[rangeList.length - 1]
|
|
||||||
let curStart = -1
|
|
||||||
let curEnd = -1
|
|
||||||
for (let i = minIndex; i <= maxIndex; i++) {
|
|
||||||
// 连续索引
|
|
||||||
if (rangeList.includes(i)) {
|
|
||||||
if (curStart === -1) {
|
|
||||||
curStart = i
|
|
||||||
}
|
|
||||||
curEnd = i
|
|
||||||
} else {
|
|
||||||
// 连续断开
|
|
||||||
if (curStart !== -1 && curEnd !== -1) {
|
|
||||||
res.push({
|
|
||||||
node: parentNode,
|
|
||||||
range: [curStart, curEnd]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
curStart = -1
|
|
||||||
curEnd = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 不要忘了最后一段索引
|
|
||||||
if (curStart !== -1 && curEnd !== -1) {
|
|
||||||
res.push({
|
|
||||||
node: parentNode,
|
|
||||||
range: [curStart, curEnd]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 单个节点
|
|
||||||
res.push({
|
|
||||||
node: parentNode,
|
|
||||||
range: [indexList[0].index, indexList[0].index]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析获取节点的子节点生成的外框列表
|
|
||||||
const getNodeOuterFrameList = node => {
|
|
||||||
const children = node.children
|
|
||||||
if (!children || children.length <= 0) return
|
|
||||||
const res = []
|
|
||||||
const map = {}
|
|
||||||
children.forEach((item, index) => {
|
|
||||||
const outerFrameData = item.getData('outerFrame')
|
|
||||||
if (!outerFrameData) return
|
|
||||||
const groupId = outerFrameData.groupId
|
|
||||||
if (groupId) {
|
|
||||||
if (!map[groupId]) {
|
|
||||||
map[groupId] = []
|
|
||||||
}
|
|
||||||
map[groupId].push({
|
|
||||||
node: item,
|
|
||||||
index
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
res.push({
|
|
||||||
nodeList: [item],
|
|
||||||
range: [index, index]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Object.keys(map).forEach(id => {
|
|
||||||
const list = map[id]
|
|
||||||
res.push({
|
|
||||||
nodeList: list.map(item => {
|
|
||||||
return item.node
|
|
||||||
}),
|
|
||||||
range: [list[0].index, list[list.length - 1].index]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认外框样式
|
// 默认外框样式
|
||||||
const defaultStyle = {
|
const defaultStyle = {
|
||||||
|
// 外框圆角大小
|
||||||
radius: 5,
|
radius: 5,
|
||||||
|
// 外框边框宽度
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
// 外框边框颜色
|
||||||
strokeColor: '#0984e3',
|
strokeColor: '#0984e3',
|
||||||
|
// 外框边框虚线样式
|
||||||
strokeDasharray: '5,5',
|
strokeDasharray: '5,5',
|
||||||
fill: 'rgba(9,132,227,0.05)'
|
// 外框填充颜色
|
||||||
|
fill: 'rgba(9,132,227,0.05)',
|
||||||
|
// 外框文字字号
|
||||||
|
fontSize: 14,
|
||||||
|
// 外框文字字体
|
||||||
|
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||||
|
// 加粗
|
||||||
|
fontWeight: 'normal', // bold
|
||||||
|
// 斜体
|
||||||
|
fontStyle: 'normal', // italic
|
||||||
|
// 外框文字颜色
|
||||||
|
color: '#fff',
|
||||||
|
// 外框文字行高
|
||||||
|
lineHeight: 1.2,
|
||||||
|
// 外框文字背景
|
||||||
|
textFill: '#0984e3',
|
||||||
|
// 外框文字圆角
|
||||||
|
textFillRadius: 5,
|
||||||
|
// 外框文字矩内边距,左上右下
|
||||||
|
textFillPadding: [5, 5, 5, 5],
|
||||||
|
// 外框文字水平显示位置,相对于外框
|
||||||
|
textAlign: 'left' // left、center、right
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OUTER_FRAME_TEXT_EDIT_WRAP = 'outer-frame-text-edit-warp'
|
||||||
|
|
||||||
// 外框插件
|
// 外框插件
|
||||||
class OuterFrame {
|
class OuterFrame {
|
||||||
constructor(opt = {}) {
|
constructor(opt = {}) {
|
||||||
this.mindMap = opt.mindMap
|
this.mindMap = opt.mindMap
|
||||||
this.draw = null
|
this.draw = null
|
||||||
this.createDrawContainer()
|
this.createDrawContainer()
|
||||||
|
this.isNotRenderOuterFrames = false
|
||||||
|
this.textNodeList = []
|
||||||
this.outerFrameElList = []
|
this.outerFrameElList = []
|
||||||
this.activeOuterFrame = null
|
this.activeOuterFrame = null
|
||||||
|
// 文字相关方法
|
||||||
|
this.textEditNode = null
|
||||||
|
this.showTextEdit = false
|
||||||
|
Object.keys(outerFrameTextMethods).forEach(item => {
|
||||||
|
this[item] = outerFrameTextMethods[item].bind(this)
|
||||||
|
})
|
||||||
|
this.mindMap.addEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||||
this.bindEvent()
|
this.bindEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +83,11 @@ class OuterFrame {
|
|||||||
this.clearActiveOuterFrame = this.clearActiveOuterFrame.bind(this)
|
this.clearActiveOuterFrame = this.clearActiveOuterFrame.bind(this)
|
||||||
this.mindMap.on('draw_click', this.clearActiveOuterFrame)
|
this.mindMap.on('draw_click', this.clearActiveOuterFrame)
|
||||||
this.mindMap.on('node_click', this.clearActiveOuterFrame)
|
this.mindMap.on('node_click', this.clearActiveOuterFrame)
|
||||||
|
// 缩放事件
|
||||||
|
this.mindMap.on('scale', this.onScale)
|
||||||
|
// 实例销毁事件
|
||||||
|
this.onBeforeDestroy = this.onBeforeDestroy.bind(this)
|
||||||
|
this.mindMap.on('beforeDestroy', this.onBeforeDestroy)
|
||||||
|
|
||||||
this.addOuterFrame = this.addOuterFrame.bind(this)
|
this.addOuterFrame = this.addOuterFrame.bind(this)
|
||||||
this.mindMap.command.add('ADD_OUTER_FRAME', this.addOuterFrame)
|
this.mindMap.command.add('ADD_OUTER_FRAME', this.addOuterFrame)
|
||||||
@ -181,6 +105,8 @@ class OuterFrame {
|
|||||||
this.mindMap.off('data_change', this.renderOuterFrames)
|
this.mindMap.off('data_change', this.renderOuterFrames)
|
||||||
this.mindMap.off('draw_click', this.clearActiveOuterFrame)
|
this.mindMap.off('draw_click', this.clearActiveOuterFrame)
|
||||||
this.mindMap.off('node_click', this.clearActiveOuterFrame)
|
this.mindMap.off('node_click', this.clearActiveOuterFrame)
|
||||||
|
this.mindMap.off('scale', this.onScale)
|
||||||
|
this.mindMap.off('beforeDestroy', this.onBeforeDestroy)
|
||||||
this.mindMap.command.remove('ADD_OUTER_FRAME', this.addOuterFrame)
|
this.mindMap.command.remove('ADD_OUTER_FRAME', this.addOuterFrame)
|
||||||
this.mindMap.keyCommand.removeShortcut(
|
this.mindMap.keyCommand.removeShortcut(
|
||||||
'Del|Backspace',
|
'Del|Backspace',
|
||||||
@ -188,6 +114,12 @@ class OuterFrame {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 实例销毁时清除关联线文字编辑框
|
||||||
|
onBeforeDestroy() {
|
||||||
|
this.hideEditTextBox()
|
||||||
|
this.removeTextEditEl()
|
||||||
|
}
|
||||||
|
|
||||||
// 给节点添加外框数据
|
// 给节点添加外框数据
|
||||||
/*
|
/*
|
||||||
config: {
|
config: {
|
||||||
@ -256,20 +188,47 @@ class OuterFrame {
|
|||||||
this.mindMap.emit('outer_frame_delete')
|
this.mindMap.emit('outer_frame_delete')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除当前激活外框的文字
|
||||||
|
removeActiveOuterFrameText() {
|
||||||
|
this.updateActiveOuterFrame({
|
||||||
|
text: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 更新当前激活的外框
|
// 更新当前激活的外框
|
||||||
// 执行了该方法后请立即隐藏你的样式面板,因为会清除当前激活的外框
|
|
||||||
updateActiveOuterFrame(config = {}) {
|
updateActiveOuterFrame(config = {}) {
|
||||||
if (!this.activeOuterFrame) return
|
if (!this.activeOuterFrame) return
|
||||||
const { node, range } = this.activeOuterFrame
|
this.isNotRenderOuterFrames = true
|
||||||
|
const { el, node, range } = this.activeOuterFrame
|
||||||
|
let newStrokeDasharray = ''
|
||||||
this.getRangeNodeList(node, range).forEach(node => {
|
this.getRangeNodeList(node, range).forEach(node => {
|
||||||
const outerFrame = node.getData('outerFrame')
|
const outerFrame = node.getData('outerFrame')
|
||||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
const newData = {
|
||||||
outerFrame: {
|
|
||||||
...outerFrame,
|
...outerFrame,
|
||||||
...config
|
...config
|
||||||
}
|
}
|
||||||
|
newStrokeDasharray = newData.strokeDasharray
|
||||||
|
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||||
|
outerFrame: newData
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
el.cacheStyle = {
|
||||||
|
dasharray: newStrokeDasharray
|
||||||
|
}
|
||||||
|
this.updateOuterFrameStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前激活外框的样式
|
||||||
|
updateOuterFrameStyle() {
|
||||||
|
const { el, node, range, textNode } = this.activeOuterFrame
|
||||||
|
const firstNode = this.getNodeRangeFirstNode(node, range)
|
||||||
|
const styleConfig = this.getStyle(firstNode)
|
||||||
|
this.styleOuterFrame(el, {
|
||||||
|
...styleConfig,
|
||||||
|
strokeDasharray: 'none'
|
||||||
|
})
|
||||||
|
const text = this.getText(firstNode)
|
||||||
|
this.renderText(text, el, textNode, node, range)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取某个节点指定范围的带外框的子节点列表
|
// 获取某个节点指定范围的带外框的子节点列表
|
||||||
@ -279,8 +238,19 @@ class OuterFrame {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取某个节点指定范围的带外框的第一个子节点
|
||||||
|
getNodeRangeFirstNode(node, range) {
|
||||||
|
return node.children[range[0]]
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染外框
|
// 渲染外框
|
||||||
renderOuterFrames() {
|
renderOuterFrames() {
|
||||||
|
if (this.isNotRenderOuterFrames) {
|
||||||
|
this.isNotRenderOuterFrames = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.clearActiveOuterFrame()
|
||||||
|
this.clearTextNodes()
|
||||||
this.clearOuterFrameElList()
|
this.clearOuterFrameElList()
|
||||||
let tree = this.mindMap.renderer.root
|
let tree = this.mindMap.renderer.root
|
||||||
if (!tree) return
|
if (!tree) return
|
||||||
@ -317,11 +287,15 @@ class OuterFrame {
|
|||||||
t.scaleY,
|
t.scaleY,
|
||||||
(width + outerFramePaddingX * 2) / t.scaleX,
|
(width + outerFramePaddingX * 2) / t.scaleX,
|
||||||
(height + outerFramePaddingY * 2) / t.scaleY,
|
(height + outerFramePaddingY * 2) / t.scaleY,
|
||||||
nodeList[0].getData('outerFrame') // 使用第一个节点的外框样式
|
this.getStyle(nodeList[0]) // 使用第一个节点的外框样式
|
||||||
)
|
)
|
||||||
|
// 渲染文字,如果有的话
|
||||||
|
const textNode = this.createText(el, cur, range)
|
||||||
|
this.textNodeList.push(textNode)
|
||||||
|
this.renderText(this.getText(nodeList[0]), el, textNode, cur, range)
|
||||||
el.on('click', e => {
|
el.on('click', e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
this.setActiveOuterFrame(el, cur, range)
|
this.setActiveOuterFrame(el, cur, range, textNode)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -333,37 +307,67 @@ class OuterFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 激活外框
|
// 激活外框
|
||||||
setActiveOuterFrame(el, node, range) {
|
setActiveOuterFrame(el, node, range, textNode) {
|
||||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||||
this.clearActiveOuterFrame()
|
this.clearActiveOuterFrame()
|
||||||
this.activeOuterFrame = {
|
this.activeOuterFrame = {
|
||||||
el,
|
el,
|
||||||
node,
|
node,
|
||||||
range
|
range,
|
||||||
|
textNode
|
||||||
}
|
}
|
||||||
el.stroke({
|
el.stroke({
|
||||||
dasharray: 'none'
|
dasharray: 'none'
|
||||||
})
|
})
|
||||||
|
// 如果没有输入过文字,那么显示默认文字
|
||||||
|
if (!this.getText(this.getNodeRangeFirstNode(node, range))) {
|
||||||
|
this.renderText(
|
||||||
|
this.mindMap.opt.defaultOuterFrameText,
|
||||||
|
el,
|
||||||
|
textNode,
|
||||||
|
node,
|
||||||
|
range
|
||||||
|
)
|
||||||
|
}
|
||||||
this.mindMap.emit('outer_frame_active', el, node, range)
|
this.mindMap.emit('outer_frame_active', el, node, range)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除当前激活的外框
|
// 清除当前激活的外框
|
||||||
clearActiveOuterFrame() {
|
clearActiveOuterFrame() {
|
||||||
if (!this.activeOuterFrame) return
|
if (!this.activeOuterFrame) return
|
||||||
const { el } = this.activeOuterFrame
|
const { el, textNode, node, range } = this.activeOuterFrame
|
||||||
el.stroke({
|
el.stroke({
|
||||||
dasharray: el.cacheStyle.dasharray || defaultStyle.strokeDasharray
|
dasharray: el.cacheStyle.dasharray || defaultStyle.strokeDasharray
|
||||||
})
|
})
|
||||||
|
// 隐藏文本编辑框
|
||||||
|
this.hideEditTextBox()
|
||||||
|
// 如果没有输入过文字,那么隐藏
|
||||||
|
if (!this.getText(this.getNodeRangeFirstNode(node, range))) {
|
||||||
|
textNode.clear()
|
||||||
|
}
|
||||||
this.activeOuterFrame = null
|
this.activeOuterFrame = null
|
||||||
|
this.mindMap.emit('outer_frame_deactivate')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取指定外框的样式
|
||||||
|
getStyle(node) {
|
||||||
|
return { ...defaultStyle, ...(node.getData('outerFrame') || {}) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建外框元素
|
// 创建外框元素
|
||||||
createOuterFrameEl(x, y, width, height, styleConfig = {}) {
|
createOuterFrameEl(x, y, width, height, styleConfig = {}) {
|
||||||
styleConfig = { ...defaultStyle, ...styleConfig }
|
const el = this.draw.rect().size(width, height).x(x).y(y)
|
||||||
const el = this.draw
|
this.styleOuterFrame(el, styleConfig)
|
||||||
.rect()
|
el.cacheStyle = {
|
||||||
.size(width, height)
|
dasharray: styleConfig.strokeDasharray
|
||||||
.radius(styleConfig.radius)
|
}
|
||||||
|
this.outerFrameElList.push(el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置外框样式
|
||||||
|
styleOuterFrame(el, styleConfig) {
|
||||||
|
el.radius(styleConfig.radius)
|
||||||
.stroke({
|
.stroke({
|
||||||
width: styleConfig.strokeWidth,
|
width: styleConfig.strokeWidth,
|
||||||
color: styleConfig.strokeColor,
|
color: styleConfig.strokeColor,
|
||||||
@ -372,13 +376,13 @@ class OuterFrame {
|
|||||||
.fill({
|
.fill({
|
||||||
color: styleConfig.fill
|
color: styleConfig.fill
|
||||||
})
|
})
|
||||||
.x(x)
|
|
||||||
.y(y)
|
|
||||||
el.cacheStyle = {
|
|
||||||
dasharray: styleConfig.strokeDasharray
|
|
||||||
}
|
}
|
||||||
this.outerFrameElList.push(el)
|
|
||||||
return el
|
// 清除文本元素
|
||||||
|
clearTextNodes() {
|
||||||
|
this.textNodeList.forEach(item => {
|
||||||
|
item.remove()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除外框元素
|
// 清除外框元素
|
||||||
@ -392,15 +396,18 @@ class OuterFrame {
|
|||||||
|
|
||||||
// 插件被移除前做的事情
|
// 插件被移除前做的事情
|
||||||
beforePluginRemove() {
|
beforePluginRemove() {
|
||||||
|
this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||||
this.unBindEvent()
|
this.unBindEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件被卸载前做的事情
|
// 插件被卸载前做的事情
|
||||||
beforePluginDestroy() {
|
beforePluginDestroy() {
|
||||||
|
this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||||
this.unBindEvent()
|
this.unBindEvent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OuterFrame.instanceName = 'outerFrame'
|
OuterFrame.instanceName = 'outerFrame'
|
||||||
|
OuterFrame.defaultStyle = defaultStyle
|
||||||
|
|
||||||
export default OuterFrame
|
export default OuterFrame
|
||||||
|
|||||||
@ -6,11 +6,13 @@ import {
|
|||||||
getTextFromHtml,
|
getTextFromHtml,
|
||||||
isUndef,
|
isUndef,
|
||||||
checkSmmFormatData,
|
checkSmmFormatData,
|
||||||
removeHtmlNodeByClass,
|
|
||||||
formatGetNodeGeneralization,
|
formatGetNodeGeneralization,
|
||||||
nodeRichTextToTextWithWrap
|
nodeRichTextToTextWithWrap,
|
||||||
|
getNodeRichTextStyles,
|
||||||
|
htmlEscape,
|
||||||
|
compareVersion
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { CONSTANTS } from '../constants/constant'
|
import { richTextSupportStyleList } from '../constants/constant'
|
||||||
import MindMapNode from '../core/render/node/MindMapNode'
|
import MindMapNode from '../core/render/node/MindMapNode'
|
||||||
import { Scope } from 'parchment'
|
import { Scope } from 'parchment'
|
||||||
|
|
||||||
@ -38,6 +40,8 @@ let fontSizeList = new Array(100).fill(0).map((_, index) => {
|
|||||||
return index + 'px'
|
return index + 'px'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const RICH_TEXT_EDIT_WRAP = 'ql-editor'
|
||||||
|
|
||||||
// 富文本编辑插件
|
// 富文本编辑插件
|
||||||
class RichText {
|
class RichText {
|
||||||
constructor({ mindMap, pluginOpt }) {
|
constructor({ mindMap, pluginOpt }) {
|
||||||
@ -53,27 +57,16 @@ class RichText {
|
|||||||
this.isInserting = false
|
this.isInserting = false
|
||||||
this.styleEl = null
|
this.styleEl = null
|
||||||
this.cacheEditingText = ''
|
this.cacheEditingText = ''
|
||||||
this.lostStyle = false
|
|
||||||
this.isCompositing = false
|
this.isCompositing = false
|
||||||
this.textNodePaddingX = 6
|
this.textNodePaddingX = 6
|
||||||
this.textNodePaddingY = 4
|
this.textNodePaddingY = 4
|
||||||
this.supportStyleProps = [
|
this.mindMap.addEditNodeClass(RICH_TEXT_EDIT_WRAP)
|
||||||
'fontFamily',
|
|
||||||
'fontSize',
|
|
||||||
'fontWeight',
|
|
||||||
'fontStyle',
|
|
||||||
'textDecoration',
|
|
||||||
'color'
|
|
||||||
]
|
|
||||||
this.initOpt()
|
this.initOpt()
|
||||||
this.extendQuill()
|
this.extendQuill()
|
||||||
this.appendCss()
|
this.appendCss()
|
||||||
this.bindEvent()
|
this.bindEvent()
|
||||||
|
|
||||||
// 处理数据,转成富文本格式
|
this.handleDataToRichTextOnInit()
|
||||||
if (this.mindMap.opt.data) {
|
|
||||||
this.mindMap.opt.data = this.handleSetData(this.mindMap.opt.data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
@ -108,18 +101,28 @@ class RichText {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.smm-richtext-node-wrap p {
|
.ql-editor .ql-align-left,
|
||||||
font-family: auto;
|
.smm-richtext-node-wrap .ql-align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smm-richtext-node-wrap .ql-align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smm-richtext-node-wrap .ql-align-center {
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
let cssText = `
|
let cssText = `
|
||||||
.${CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP} {
|
.${RICH_TEXT_EDIT_WRAP} {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: auto;
|
height: auto;
|
||||||
line-height: normal;
|
line-height: 1.2;
|
||||||
-webkit-user-select: text;
|
-webkit-user-select: text;
|
||||||
|
text-align: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ql-container {
|
.ql-container {
|
||||||
@ -130,10 +133,6 @@ class RichText {
|
|||||||
.ql-container.ql-snow {
|
.ql-container.ql-snow {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.smm-richtext-node-edit-wrap p {
|
|
||||||
font-family: auto;
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
this.styleEl = document.createElement('style')
|
this.styleEl = document.createElement('style')
|
||||||
this.styleEl.type = 'text/css'
|
this.styleEl.type = 'text/css'
|
||||||
@ -166,6 +165,8 @@ class RichText {
|
|||||||
|
|
||||||
this.extendFont([])
|
this.extendFont([])
|
||||||
|
|
||||||
|
this.extendAlign()
|
||||||
|
|
||||||
// 扩展quill的字号列表
|
// 扩展quill的字号列表
|
||||||
const SizeAttributor = Quill.import('attributors/class/size')
|
const SizeAttributor = Quill.import('attributors/class/size')
|
||||||
SizeAttributor.whitelist = fontSizeList
|
SizeAttributor.whitelist = fontSizeList
|
||||||
@ -190,6 +191,13 @@ class RichText {
|
|||||||
Quill.register(FontStyle, true)
|
Quill.register(FontStyle, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 扩展文本对齐方式
|
||||||
|
extendAlign() {
|
||||||
|
const AlignFormat = Quill.import('formats/align')
|
||||||
|
AlignFormat.whitelist = ['right', 'center', 'justify', 'left']
|
||||||
|
Quill.register(AlignFormat, true)
|
||||||
|
}
|
||||||
|
|
||||||
// 显示文本编辑控件
|
// 显示文本编辑控件
|
||||||
showEditText({ node, rect, isInserting, isFromKeyDown, isFromScale }) {
|
showEditText({ node, rect, isInserting, isFromKeyDown, isFromScale }) {
|
||||||
if (this.showTextEdit) {
|
if (this.showTextEdit) {
|
||||||
@ -238,6 +246,7 @@ class RichText {
|
|||||||
outline: none;
|
outline: none;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
padding: ${paddingY}px ${paddingX}px;
|
padding: ${paddingY}px ${paddingX}px;
|
||||||
|
line-height: 1.2;
|
||||||
`
|
`
|
||||||
this.textEditNode.addEventListener('click', e => {
|
this.textEditNode.addEventListener('click', e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@ -253,6 +262,7 @@ class RichText {
|
|||||||
const targetNode = customInnerElsAppendTo || document.body
|
const targetNode = customInnerElsAppendTo || document.body
|
||||||
targetNode.appendChild(this.textEditNode)
|
targetNode.appendChild(this.textEditNode)
|
||||||
}
|
}
|
||||||
|
this.addNodeTextStyleToTextEditNode(node)
|
||||||
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
|
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
|
||||||
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
|
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
|
||||||
this.textEditNode.style.zIndex = nodeTextEditZIndex
|
this.textEditNode.style.zIndex = nodeTextEditZIndex
|
||||||
@ -277,13 +287,8 @@ class RichText {
|
|||||||
const isEmptyText = isUndef(nodeText)
|
const isEmptyText = isUndef(nodeText)
|
||||||
// 是否是非空的非富文本
|
// 是否是非空的非富文本
|
||||||
const noneEmptyNoneRichText = !node.getData('richText') && !isEmptyText
|
const noneEmptyNoneRichText = !node.getData('richText') && !isEmptyText
|
||||||
// 如果是空文本,那么设置为丢失样式状态,否则输入不会带上样式
|
|
||||||
if (isEmptyText) {
|
|
||||||
this.lostStyle = true
|
|
||||||
}
|
|
||||||
if (isFromKeyDown && autoEmptyTextWhenKeydownEnterEdit) {
|
if (isFromKeyDown && autoEmptyTextWhenKeydownEnterEdit) {
|
||||||
this.textEditNode.innerHTML = ''
|
this.textEditNode.innerHTML = ''
|
||||||
this.lostStyle = true
|
|
||||||
} else if (noneEmptyNoneRichText) {
|
} else if (noneEmptyNoneRichText) {
|
||||||
// 还不是富文本
|
// 还不是富文本
|
||||||
let text = String(nodeText).split(/\n/gim).join('<br>')
|
let text = String(nodeText).split(/\n/gim).join('<br>')
|
||||||
@ -294,19 +299,13 @@ class RichText {
|
|||||||
this.textEditNode.innerHTML = this.cacheEditingText || nodeText
|
this.textEditNode.innerHTML = this.cacheEditingText || nodeText
|
||||||
}
|
}
|
||||||
this.initQuillEditor()
|
this.initQuillEditor()
|
||||||
document.querySelector(
|
this.setQuillContainerMinHeight(originHeight)
|
||||||
'.' + CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP
|
this.setIsShowTextEdit(true)
|
||||||
).style.minHeight = originHeight + 'px'
|
|
||||||
this.showTextEdit = true
|
|
||||||
// 如果是刚创建的节点,那么默认全选,否则普通激活不全选,除非selectTextOnEnterEditText配置为true
|
// 如果是刚创建的节点,那么默认全选,否则普通激活不全选,除非selectTextOnEnterEditText配置为true
|
||||||
// 在selectTextOnEnterEditText时,如果是在keydown事件进入的节点编辑,也不需要全选
|
// 在selectTextOnEnterEditText时,如果是在keydown事件进入的节点编辑,也不需要全选
|
||||||
this.focus(
|
this.focus(
|
||||||
isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null
|
isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null
|
||||||
)
|
)
|
||||||
if (noneEmptyNoneRichText) {
|
|
||||||
// 如果是非富文本的情况,需要手动应用文本样式
|
|
||||||
this.setTextStyleIfNotRichText(node)
|
|
||||||
}
|
|
||||||
this.cacheEditingText = ''
|
this.cacheEditingText = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,6 +324,20 @@ class RichText {
|
|||||||
: '0 0 20px rgba(0,0,0,.5)'
|
: '0 0 20px rgba(0,0,0,.5)'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将指定节点的文本样式添加到编辑框元素上
|
||||||
|
addNodeTextStyleToTextEditNode(node) {
|
||||||
|
const style = getNodeRichTextStyles(node)
|
||||||
|
Object.keys(style).forEach(prop => {
|
||||||
|
this.textEditNode.style[prop] = style[prop]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置quill编辑器容器的最小高度
|
||||||
|
setQuillContainerMinHeight(minHeight) {
|
||||||
|
document.querySelector('.' + RICH_TEXT_EDIT_WRAP).style.minHeight =
|
||||||
|
minHeight + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
// 更新文本编辑框的大小和位置
|
// 更新文本编辑框的大小和位置
|
||||||
updateTextEditNode() {
|
updateTextEditNode() {
|
||||||
if (!this.node) return
|
if (!this.node) return
|
||||||
@ -337,6 +350,7 @@ class RichText {
|
|||||||
this.textEditNode.style.minHeight = originHeight + 'px'
|
this.textEditNode.style.minHeight = originHeight + 'px'
|
||||||
this.textEditNode.style.left = rect.left + 'px'
|
this.textEditNode.style.left = rect.left + 'px'
|
||||||
this.textEditNode.style.top = rect.top + 'px'
|
this.textEditNode.style.top = rect.top + 'px'
|
||||||
|
this.setQuillContainerMinHeight(originHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除文本编辑框元素
|
// 删除文本编辑框元素
|
||||||
@ -346,24 +360,10 @@ class RichText {
|
|||||||
targetNode.removeChild(this.textEditNode)
|
targetNode.removeChild(this.textEditNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是非富文本的情况,需要手动应用文本样式
|
|
||||||
setTextStyleIfNotRichText(node) {
|
|
||||||
let style = {
|
|
||||||
font: node.style.merge('fontFamily'),
|
|
||||||
color: node.style.merge('color'),
|
|
||||||
italic: node.style.merge('fontStyle') === 'italic',
|
|
||||||
bold: node.style.merge('fontWeight') === 'bold',
|
|
||||||
size: node.style.merge('fontSize') + 'px',
|
|
||||||
underline: node.style.merge('textDecoration') === 'underline',
|
|
||||||
strike: node.style.merge('textDecoration') === 'line-through'
|
|
||||||
}
|
|
||||||
this.pureFormatAllText(style)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前正在编辑的内容
|
// 获取当前正在编辑的内容
|
||||||
getEditText() {
|
getEditText() {
|
||||||
// https://github.com/slab/quill/issues/4509
|
// https://github.com/slab/quill/issues/4509
|
||||||
return this.quill.container.firstChild.innerHTML.replaceAll(/ +/g, match =>
|
return this.quill.container.firstChild.innerHTML.replace(/ +/g, match =>
|
||||||
' '.repeat(match.length)
|
' '.repeat(match.length)
|
||||||
)
|
)
|
||||||
// 去除ql-cursor节点
|
// 去除ql-cursor节点
|
||||||
@ -374,18 +374,6 @@ class RichText {
|
|||||||
// return html.replace(/<p><br><\/p>$/, '')
|
// return html.replace(/<p><br><\/p>$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 给html字符串中的节点样式按样式名首字母排序
|
|
||||||
sortHtmlNodeStyles(html) {
|
|
||||||
return html.replace(/(<[^<>]+\s+style=")([^"]+)("\s*>)/g, (_, a, b, c) => {
|
|
||||||
let arr = b.match(/[^:]+:[^:]+;/g) || []
|
|
||||||
arr = arr.map(item => {
|
|
||||||
return item.trim()
|
|
||||||
})
|
|
||||||
arr.sort()
|
|
||||||
return a + arr.join('') + c
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏文本编辑控件,即完成编辑
|
// 隐藏文本编辑控件,即完成编辑
|
||||||
hideEditText(nodes) {
|
hideEditText(nodes) {
|
||||||
if (!this.showTextEdit) {
|
if (!this.showTextEdit) {
|
||||||
@ -395,12 +383,11 @@ class RichText {
|
|||||||
if (typeof beforeHideRichTextEdit === 'function') {
|
if (typeof beforeHideRichTextEdit === 'function') {
|
||||||
beforeHideRichTextEdit(this)
|
beforeHideRichTextEdit(this)
|
||||||
}
|
}
|
||||||
let html = this.getEditText()
|
const html = this.getEditText()
|
||||||
html = this.sortHtmlNodeStyles(html)
|
|
||||||
const list = nodes && nodes.length > 0 ? nodes : [this.node]
|
const list = nodes && nodes.length > 0 ? nodes : [this.node]
|
||||||
const node = this.node
|
const node = this.node
|
||||||
this.textEditNode.style.display = 'none'
|
this.textEditNode.style.display = 'none'
|
||||||
this.showTextEdit = false
|
this.setIsShowTextEdit(false)
|
||||||
this.mindMap.emit('rich_text_selection_change', false)
|
this.mindMap.emit('rich_text_selection_change', false)
|
||||||
this.node = null
|
this.node = null
|
||||||
this.isInserting = false
|
this.isInserting = false
|
||||||
@ -482,7 +469,8 @@ class RichText {
|
|||||||
'background',
|
'background',
|
||||||
'font',
|
'font',
|
||||||
'size',
|
'size',
|
||||||
'formula'
|
'formula',
|
||||||
|
'align'
|
||||||
], // 明确指定允许的格式,不包含有序列表,无序列表等
|
], // 明确指定允许的格式,不包含有序列表,无序列表等
|
||||||
theme: 'snow'
|
theme: 'snow'
|
||||||
})
|
})
|
||||||
@ -503,7 +491,10 @@ class RichText {
|
|||||||
})
|
})
|
||||||
this.quill.on('selection-change', range => {
|
this.quill.on('selection-change', range => {
|
||||||
// 刚创建的节点全选不需要显示操作条
|
// 刚创建的节点全选不需要显示操作条
|
||||||
if (this.isInserting) return
|
if (this.isInserting) {
|
||||||
|
this.isInserting = false
|
||||||
|
return
|
||||||
|
}
|
||||||
this.lastRange = this.range
|
this.lastRange = this.range
|
||||||
this.range = null
|
this.range = null
|
||||||
if (range) {
|
if (range) {
|
||||||
@ -536,18 +527,6 @@ class RichText {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.quill.on('text-change', () => {
|
this.quill.on('text-change', () => {
|
||||||
let contents = this.quill.getContents()
|
|
||||||
let len = contents.ops.length
|
|
||||||
// 如果编辑过程中删除所有字符,那么会丢失主题的样式
|
|
||||||
if (len <= 0 || (len === 1 && contents.ops[0].insert === '\n')) {
|
|
||||||
this.lostStyle = true
|
|
||||||
// 需要删除节点的样式数据
|
|
||||||
this.syncFormatToNodeConfig(null, true)
|
|
||||||
} else if (this.lostStyle && !this.isCompositing) {
|
|
||||||
// 如果处于样式丢失状态,那么需要进行格式化加回样式
|
|
||||||
this.setTextStyleIfNotRichText(this.node)
|
|
||||||
this.lostStyle = false
|
|
||||||
}
|
|
||||||
this.mindMap.emit('node_text_edit_change', {
|
this.mindMap.emit('node_text_edit_change', {
|
||||||
node: this.node,
|
node: this.node,
|
||||||
text: this.getEditText(),
|
text: this.getEditText(),
|
||||||
@ -638,10 +617,16 @@ class RichText {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isCompositing = false
|
this.isCompositing = false
|
||||||
if (!this.lostStyle) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
this.setTextStyleIfNotRichText(this.node)
|
|
||||||
|
// 设置文本编辑框是否处于显示状态
|
||||||
|
setIsShowTextEdit(val) {
|
||||||
|
this.showTextEdit = val
|
||||||
|
if (val) {
|
||||||
|
this.mindMap.keyCommand.stopCheckInSvg()
|
||||||
|
} else {
|
||||||
|
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选中全部
|
// 选中全部
|
||||||
@ -651,19 +636,28 @@ class RichText {
|
|||||||
|
|
||||||
// 聚焦
|
// 聚焦
|
||||||
focus(start) {
|
focus(start) {
|
||||||
let len = this.quill.getLength()
|
const len = this.quill.getLength()
|
||||||
this.quill.setSelection(typeof start === 'number' ? start : len, len)
|
this.quill.setSelection(typeof start === 'number' ? start : len, len)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化当前选中的文本
|
// 格式化当前选中的文本
|
||||||
formatText(config = {}, clear = false, pure = false) {
|
formatText(config = {}, clear = false) {
|
||||||
if (!this.range && !this.lastRange) return
|
if (!this.range && !this.lastRange) return
|
||||||
if (!pure) this.syncFormatToNodeConfig(config, clear)
|
const rangeLost = !this.range
|
||||||
let rangeLost = !this.range
|
const range = rangeLost ? this.lastRange : this.range
|
||||||
let range = rangeLost ? this.lastRange : this.range
|
if (clear) {
|
||||||
clear
|
this.quill.removeFormat(range.index, range.length)
|
||||||
? this.quill.removeFormat(range.index, range.length)
|
} else {
|
||||||
: this.quill.formatText(range.index, range.length, config)
|
const { align, ...rest } = config
|
||||||
|
// 文本对齐需要对行进行格式化
|
||||||
|
if (align) {
|
||||||
|
this.quill.formatLine(range.index, range.length, 'align', align)
|
||||||
|
}
|
||||||
|
// 其他内容对文本
|
||||||
|
if (Object.keys(rest).length > 0) {
|
||||||
|
this.quill.formatText(range.index, range.length, rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (rangeLost) {
|
if (rangeLost) {
|
||||||
this.quill.setSelection(this.lastRange.index, this.lastRange.length)
|
this.quill.setSelection(this.lastRange.index, this.lastRange.length)
|
||||||
}
|
}
|
||||||
@ -671,56 +665,25 @@ class RichText {
|
|||||||
|
|
||||||
// 清除当前选中文本的样式
|
// 清除当前选中文本的样式
|
||||||
removeFormat() {
|
removeFormat() {
|
||||||
// 先移除全部样式
|
|
||||||
this.formatText({}, true)
|
this.formatText({}, true)
|
||||||
// 再将样式恢复为当前主题改节点的默认样式
|
|
||||||
const style = {}
|
|
||||||
if (this.node) {
|
|
||||||
this.supportStyleProps.forEach(key => {
|
|
||||||
style[key] = this.node.style.merge(key)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const config = this.normalStyleToRichTextStyle(style)
|
|
||||||
this.formatText(config, false, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化指定范围的文本
|
// 格式化指定范围的文本
|
||||||
formatRangeText(range, config = {}) {
|
formatRangeText(range, config = {}) {
|
||||||
if (!range) return
|
if (!range) return
|
||||||
this.syncFormatToNodeConfig(config)
|
|
||||||
this.quill.formatText(range.index, range.length, config)
|
this.quill.formatText(range.index, range.length, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化所有文本
|
// 格式化所有文本
|
||||||
formatAllText(config = {}) {
|
formatAllText(config = {}) {
|
||||||
this.syncFormatToNodeConfig(config)
|
|
||||||
this.pureFormatAllText(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 纯粹的格式化所有文本
|
|
||||||
pureFormatAllText(config = {}) {
|
|
||||||
this.quill.formatText(0, this.quill.getLength(), config)
|
this.quill.formatText(0, this.quill.getLength(), config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步格式化到节点样式配置
|
|
||||||
syncFormatToNodeConfig(config, clear) {
|
|
||||||
if (!this.node) return
|
|
||||||
if (clear) {
|
|
||||||
// 清除文本样式
|
|
||||||
this.supportStyleProps.forEach(prop => {
|
|
||||||
delete this.node.nodeData.data[prop]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let data = this.richTextStyleToNormalStyle(config)
|
|
||||||
this.mindMap.execCommand('SET_NODE_DATA', this.node, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将普通节点样式对象转换成富文本样式对象
|
// 将普通节点样式对象转换成富文本样式对象
|
||||||
normalStyleToRichTextStyle(style) {
|
normalStyleToRichTextStyle(style) {
|
||||||
let config = {}
|
const config = {}
|
||||||
Object.keys(style).forEach(prop => {
|
Object.keys(style).forEach(prop => {
|
||||||
let value = style[prop]
|
const value = style[prop]
|
||||||
switch (prop) {
|
switch (prop) {
|
||||||
case 'fontFamily':
|
case 'fontFamily':
|
||||||
config.font = value
|
config.font = value
|
||||||
@ -741,6 +704,9 @@ class RichText {
|
|||||||
case 'color':
|
case 'color':
|
||||||
config.color = value
|
config.color = value
|
||||||
break
|
break
|
||||||
|
case 'textAlign':
|
||||||
|
config.align = value
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -750,9 +716,9 @@ class RichText {
|
|||||||
|
|
||||||
// 将富文本样式对象转换成普通节点样式对象
|
// 将富文本样式对象转换成普通节点样式对象
|
||||||
richTextStyleToNormalStyle(config) {
|
richTextStyleToNormalStyle(config) {
|
||||||
let data = {}
|
const data = {}
|
||||||
Object.keys(config).forEach(prop => {
|
Object.keys(config).forEach(prop => {
|
||||||
let value = config[prop]
|
const value = config[prop]
|
||||||
switch (prop) {
|
switch (prop) {
|
||||||
case 'font':
|
case 'font':
|
||||||
data.fontFamily = value
|
data.fontFamily = value
|
||||||
@ -775,6 +741,9 @@ class RichText {
|
|||||||
case 'color':
|
case 'color':
|
||||||
data.color = value
|
data.color = value
|
||||||
break
|
break
|
||||||
|
case 'align':
|
||||||
|
data.textAlign = value
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -787,39 +756,50 @@ class RichText {
|
|||||||
const keys = Object.keys(obj)
|
const keys = Object.keys(obj)
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const key = keys[i]
|
const key = keys[i]
|
||||||
if (this.supportStyleProps.includes(key)) {
|
if (richTextSupportStyleList.includes(key)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 给未激活的节点设置富文本样式
|
|
||||||
setNotActiveNodeStyle(node, style) {
|
|
||||||
const config = this.normalStyleToRichTextStyle(style)
|
|
||||||
if (Object.keys(config).length > 0) {
|
|
||||||
this.showEditText({ node })
|
|
||||||
this.formatAllText(config)
|
|
||||||
this.hideEditText([node])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查指定节点是否存在自定义的富文本样式
|
// 检查指定节点是否存在自定义的富文本样式
|
||||||
checkNodeHasCustomRichTextStyle(node) {
|
checkNodeHasCustomRichTextStyle(node) {
|
||||||
const nodeData = node instanceof MindMapNode ? node.getData() : node
|
const nodeData = node instanceof MindMapNode ? node.getData() : node
|
||||||
for (let i = 0; i < this.supportStyleProps.length; i++) {
|
for (let i = 0; i < richTextSupportStyleList.length; i++) {
|
||||||
if (nodeData[this.supportStyleProps[i]] !== undefined) {
|
if (nodeData[richTextSupportStyleList[i]] !== undefined) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 转换数据后的渲染操作
|
||||||
|
afterHandleData() {
|
||||||
|
// 清空历史数据,并且触发数据变化
|
||||||
|
this.mindMap.command.clearHistory()
|
||||||
|
this.mindMap.command.addHistory()
|
||||||
|
this.mindMap.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件实例化时处理思维导图数据,转换为富文本数据
|
||||||
|
handleDataToRichTextOnInit() {
|
||||||
|
// 处理数据,转成富文本格式
|
||||||
|
if (this.mindMap.renderer.renderTree) {
|
||||||
|
// 如果已经存在渲染树了,那么直接更新渲染树,并且触发重新渲染
|
||||||
|
this.handleSetData(this.mindMap.renderer.renderTree)
|
||||||
|
this.afterHandleData()
|
||||||
|
} else if (this.mindMap.opt.data) {
|
||||||
|
this.handleSetData(this.mindMap.opt.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 将所有节点转换成非富文本节点
|
// 将所有节点转换成非富文本节点
|
||||||
transformAllNodesToNormalNode() {
|
transformAllNodesToNormalNode() {
|
||||||
if (!this.mindMap.renderer.renderTree) return
|
const renderTree = this.mindMap.renderer.renderTree
|
||||||
|
if (!renderTree) return
|
||||||
walk(
|
walk(
|
||||||
this.mindMap.renderer.renderTree,
|
renderTree,
|
||||||
null,
|
null,
|
||||||
node => {
|
node => {
|
||||||
if (node.data.richText) {
|
if (node.data.richText) {
|
||||||
@ -840,25 +820,36 @@ class RichText {
|
|||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
// 清空历史数据,并且触发数据变化
|
this.afterHandleData()
|
||||||
this.mindMap.command.clearHistory()
|
}
|
||||||
this.mindMap.command.addHistory()
|
|
||||||
this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE)
|
handleDataToRichText(data) {
|
||||||
|
const oldIsRichText = data.richText
|
||||||
|
data.richText = true
|
||||||
|
data.resetRichText = true
|
||||||
|
// 如果原本就是富文本,那么不能转换
|
||||||
|
if (!oldIsRichText) {
|
||||||
|
data.text = htmlEscape(data.text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理导入数据
|
// 处理导入数据
|
||||||
handleSetData(data) {
|
handleSetData(data) {
|
||||||
let walk = root => {
|
if (!data) return
|
||||||
if (root.data && !root.data.richText) {
|
// 短期处理,为了兼容老数据,长期会去除
|
||||||
root.data.richText = true
|
const isOldRichTextVersion =
|
||||||
root.data.resetRichText = true
|
!data.smmVersion || compareVersion(data.smmVersion, '0.13.0') === '<'
|
||||||
|
const walk = root => {
|
||||||
|
if (root.data && (!root.data.richText || isOldRichTextVersion)) {
|
||||||
|
this.handleDataToRichText(root.data)
|
||||||
}
|
}
|
||||||
// 概要
|
// 概要
|
||||||
if (root.data) {
|
if (root.data) {
|
||||||
const generalizationList = formatGetNodeGeneralization(root.data)
|
const generalizationList = formatGetNodeGeneralization(root.data)
|
||||||
generalizationList.forEach(item => {
|
generalizationList.forEach(item => {
|
||||||
item.richText = true
|
if (!item.richText || isOldRichTextVersion) {
|
||||||
item.resetRichText = true
|
this.handleDataToRichText(item)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (root.children && root.children.length > 0) {
|
if (root.children && root.children.length > 0) {
|
||||||
@ -877,12 +868,14 @@ class RichText {
|
|||||||
document.head.removeChild(this.styleEl)
|
document.head.removeChild(this.styleEl)
|
||||||
this.unbindEvent()
|
this.unbindEvent()
|
||||||
this.mindMap.removeAppendCss('richText')
|
this.mindMap.removeAppendCss('richText')
|
||||||
|
this.mindMap.deleteEditNodeClass(RICH_TEXT_EDIT_WRAP)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件被卸载前做的事情
|
// 插件被卸载前做的事情
|
||||||
beforePluginDestroy() {
|
beforePluginDestroy() {
|
||||||
document.head.removeChild(this.styleEl)
|
document.head.removeChild(this.styleEl)
|
||||||
this.unbindEvent()
|
this.unbindEvent()
|
||||||
|
this.mindMap.deleteEditNodeClass(RICH_TEXT_EDIT_WRAP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,7 @@ class Search {
|
|||||||
|
|
||||||
// 搜索匹配的节点
|
// 搜索匹配的节点
|
||||||
doSearch() {
|
doSearch() {
|
||||||
|
this.clearHighlightOnReadonly()
|
||||||
this.updateMatchNodeList([])
|
this.updateMatchNodeList([])
|
||||||
this.currentIndex = -1
|
this.currentIndex = -1
|
||||||
const { isOnlySearchCurrentRenderNodes } = this.mindMap.opt
|
const { isOnlySearchCurrentRenderNodes } = this.mindMap.opt
|
||||||
@ -174,19 +175,17 @@ class Search {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { readonly } = this.mindMap.opt
|
const { readonly } = this.mindMap.opt
|
||||||
// 只读模式下需要激活之前节点的高亮
|
// 只读模式下需要清除之前节点的高亮
|
||||||
if (readonly) {
|
this.clearHighlightOnReadonly()
|
||||||
this.matchNodeList.forEach(node => {
|
|
||||||
if (this.isNodeInstance(node)) {
|
|
||||||
node.closeHighlight()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const currentNode = this.matchNodeList[this.currentIndex]
|
const currentNode = this.matchNodeList[this.currentIndex]
|
||||||
this.notResetSearchText = true
|
this.notResetSearchText = true
|
||||||
const uid = this.isNodeInstance(currentNode)
|
const uid = this.isNodeInstance(currentNode)
|
||||||
? currentNode.getData('uid')
|
? currentNode.getData('uid')
|
||||||
: currentNode.data.uid
|
: currentNode.data.uid
|
||||||
|
if (!uid) {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
const targetNode = this.mindMap.renderer.findNodeByUid(uid)
|
const targetNode = this.mindMap.renderer.findNodeByUid(uid)
|
||||||
this.mindMap.execCommand('GO_TARGET_NODE', uid, node => {
|
this.mindMap.execCommand('GO_TARGET_NODE', uid, node => {
|
||||||
if (!this.isNodeInstance(currentNode)) {
|
if (!this.isNodeInstance(currentNode)) {
|
||||||
@ -205,6 +204,18 @@ class Search {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只读模式下清除现有匹配节点的高亮
|
||||||
|
clearHighlightOnReadonly() {
|
||||||
|
const { readonly } = this.mindMap.opt
|
||||||
|
if (readonly) {
|
||||||
|
this.matchNodeList.forEach(node => {
|
||||||
|
if (this.isNodeInstance(node)) {
|
||||||
|
node.closeHighlight()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 定位到指定搜索结果索引的节点
|
// 定位到指定搜索结果索引的节点
|
||||||
jump(index, callback = () => {}) {
|
jump(index, callback = () => {}) {
|
||||||
this.searchNext(callback, index)
|
this.searchNext(callback, index)
|
||||||
@ -228,7 +239,7 @@ class Search {
|
|||||||
const keep = replaceText.includes(this.searchText)
|
const keep = replaceText.includes(this.searchText)
|
||||||
const text = this.getReplacedText(currentNode, this.searchText, replaceText)
|
const text = this.getReplacedText(currentNode, this.searchText, replaceText)
|
||||||
this.notResetSearchText = true
|
this.notResetSearchText = true
|
||||||
currentNode.setText(text, currentNode.getData('richText'), true)
|
currentNode.setText(text, currentNode.getData('richText'))
|
||||||
if (keep) {
|
if (keep) {
|
||||||
this.updateMatchNodeList(this.matchNodeList)
|
this.updateMatchNodeList(this.matchNodeList)
|
||||||
return
|
return
|
||||||
@ -258,18 +269,15 @@ class Search {
|
|||||||
// 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的
|
// 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的
|
||||||
const keep = replaceText.includes(this.searchText)
|
const keep = replaceText.includes(this.searchText)
|
||||||
this.notResetSearchText = true
|
this.notResetSearchText = true
|
||||||
const hasRichTextPlugin = this.mindMap.renderer.hasRichTextPlugin()
|
|
||||||
this.matchNodeList.forEach(node => {
|
this.matchNodeList.forEach(node => {
|
||||||
const text = this.getReplacedText(node, this.searchText, replaceText)
|
const text = this.getReplacedText(node, this.searchText, replaceText)
|
||||||
if (this.isNodeInstance(node)) {
|
if (this.isNodeInstance(node)) {
|
||||||
const data = {
|
const data = {
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
if (hasRichTextPlugin) data.resetRichText = !!node.getData('richText')
|
|
||||||
this.mindMap.renderer.setNodeDataRender(node, data, true)
|
this.mindMap.renderer.setNodeDataRender(node, data, true)
|
||||||
} else {
|
} else {
|
||||||
node.data.text = text
|
node.data.text = text
|
||||||
if (hasRichTextPlugin) node.data.resetRichText = !!node.data.richText
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.mindMap.render()
|
this.mindMap.render()
|
||||||
@ -289,7 +297,7 @@ class Search {
|
|||||||
if (richText) {
|
if (richText) {
|
||||||
return replaceHtmlText(text, searchText, replaceText)
|
return replaceHtmlText(text, searchText, replaceText)
|
||||||
} else {
|
} else {
|
||||||
return text.replaceAll(searchText, replaceText)
|
return text.replace(new RegExp(searchText, 'g'), replaceText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import {
|
|||||||
selectAllInput
|
selectAllInput
|
||||||
} from '../../utils/index'
|
} from '../../utils/index'
|
||||||
|
|
||||||
|
const ASSOCIATIVE_LINE_TEXT_EDIT_WRAP = 'associative-line-text-edit-warp'
|
||||||
|
|
||||||
// 创建文字节点
|
// 创建文字节点
|
||||||
function createText(data) {
|
function createText(data) {
|
||||||
let g = this.associativeLineDraw.group()
|
let g = this.associativeLineDraw.group()
|
||||||
@ -43,7 +45,7 @@ function showEditTextBox(g) {
|
|||||||
// 输入框元素没有创建过,则先创建
|
// 输入框元素没有创建过,则先创建
|
||||||
if (!this.textEditNode) {
|
if (!this.textEditNode) {
|
||||||
this.textEditNode = document.createElement('div')
|
this.textEditNode = document.createElement('div')
|
||||||
this.textEditNode.className = 'associative-line-text-edit-warp'
|
this.textEditNode.className = ASSOCIATIVE_LINE_TEXT_EDIT_WRAP
|
||||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
|
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
|
||||||
this.textEditNode.setAttribute('contenteditable', true)
|
this.textEditNode.setAttribute('contenteditable', true)
|
||||||
this.textEditNode.addEventListener('keyup', e => {
|
this.textEditNode.addEventListener('keyup', e => {
|
||||||
@ -73,7 +75,7 @@ function showEditTextBox(g) {
|
|||||||
this.textEditNode.innerHTML = textLines.join('<br>')
|
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||||
this.textEditNode.style.display = 'block'
|
this.textEditNode.style.display = 'block'
|
||||||
this.updateTextEditBoxPos(g)
|
this.updateTextEditBoxPos(g)
|
||||||
this.showTextEdit = true
|
this.setIsShowTextEdit(true)
|
||||||
// 如果是默认文本要全选输入框
|
// 如果是默认文本要全选输入框
|
||||||
if (text === '' || text === defaultAssociativeLineText) {
|
if (text === '' || text === defaultAssociativeLineText) {
|
||||||
selectAllInput(this.textEditNode)
|
selectAllInput(this.textEditNode)
|
||||||
@ -83,6 +85,16 @@ function showEditTextBox(g) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置文本编辑框是否处于显示状态
|
||||||
|
function setIsShowTextEdit(val) {
|
||||||
|
this.showTextEdit = val
|
||||||
|
if (val) {
|
||||||
|
this.mindMap.keyCommand.stopCheckInSvg()
|
||||||
|
} else {
|
||||||
|
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 删除文本编辑框元素
|
// 删除文本编辑框元素
|
||||||
function removeTextEditEl() {
|
function removeTextEditEl() {
|
||||||
if (!this.textEditNode) return
|
if (!this.textEditNode) return
|
||||||
@ -124,7 +136,7 @@ function hideEditTextBox() {
|
|||||||
})
|
})
|
||||||
this.textEditNode.style.display = 'none'
|
this.textEditNode.style.display = 'none'
|
||||||
this.textEditNode.innerHTML = ''
|
this.textEditNode.innerHTML = ''
|
||||||
this.showTextEdit = false
|
this.setIsShowTextEdit(false)
|
||||||
this.renderText(str, path, text, node, toNode)
|
this.renderText(str, path, text, node, toNode)
|
||||||
this.mindMap.emit('hide_text_edit')
|
this.mindMap.emit('hide_text_edit')
|
||||||
}
|
}
|
||||||
@ -192,6 +204,7 @@ export default {
|
|||||||
styleText,
|
styleText,
|
||||||
onScale,
|
onScale,
|
||||||
showEditTextBox,
|
showEditTextBox,
|
||||||
|
setIsShowTextEdit,
|
||||||
removeTextEditEl,
|
removeTextEditEl,
|
||||||
hideEditTextBox,
|
hideEditTextBox,
|
||||||
updateTextEditBoxPos,
|
updateTextEditBoxPos,
|
||||||
|
|||||||