Compare commits

..

16 Commits

Author SHA1 Message Date
wanglin2
c789a95e4e 支持直接双击文件打开应用进入编辑 2023-05-05 17:37:01 +08:00
wanglin2
b0532072c2 修改打包配置 2023-05-05 16:59:39 +08:00
wanglin2
7d21ae4618 打包 2023-05-05 16:04:15 +08:00
wanglin2
0e608de9da 基本完成 2023-05-05 14:55:01 +08:00
wanglin2
fbff68c635 开发中 2023-05-05 10:51:39 +08:00
wanglin2
ec9517c491 开发中 2023-05-04 22:40:44 +08:00
wanglin2
26e3158bd8 开发中 2023-05-04 17:54:31 +08:00
wanglin2
c2c9de1c03 更新 2023-05-03 22:06:52 +08:00
wanglin2
27d2238d20 更新 2023-05-03 21:26:13 +08:00
wanglin2
d6e625efa6 日常提交 2023-03-14 09:40:09 +08:00
wanglin2
fba0ece8fd 合并主分支 2023-03-08 09:47:35 +08:00
wanglin2
78ed038012 重新设计客户端架构 2023-03-07 16:39:33 +08:00
wanglin2
58f7be77ed Merge branch 'main' into electron 2023-03-06 21:56:30 +08:00
wanglin2
362160c67b 使用Vue CLI Plugin Electron Builder插件 2023-03-06 21:29:59 +08:00
wanglin2
c8a3c569e1 新增electron相关页面 2023-03-06 20:59:00 +08:00
wanglin2
6bc309e3fa 增加图标 2023-03-06 20:57:55 +08:00
650 changed files with 17337 additions and 28405 deletions

21
LICENSE
View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2021-2023 The MindMap Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

129
README.md
View File

@@ -11,32 +11,29 @@
本项目包含两部分:
1.一个 js 思维导图库,不依赖任何框架,你可以使用它来快速完成 Web 思维导图产品的开发。
1.一个js思维导图库不依赖任何框架你可以使用它来快速完成Web思维导图产品的开发。
开发文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)
开发文档:[https://wanglin2.github.io/mind-map/#/doc/zh/](https://wanglin2.github.io/mind-map/#/doc/zh/)
2.一个 Web 思维导图基于思维导图库、Vue2.x、ElementUI 开发,可以操作电脑本地文件,所以你可以直接把它当做一个在线版思维导图应用使用,如果觉得 github 的响应速度慢,你也可以部署到你的服务器上。
2.一个Web思维导图基于思维导图库、Vue2.x、ElementUI开发可以操作电脑本地文件所以你可以直接把它当做一个在线版思维导图应用使用如果觉得github的响应速度慢你也可以部署到你的服务器上。
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
另外也提供了客户端可供下载使用,支持`Windows``Mac``Linux`,下载地址:
Github[releases](https://github.com/wanglin2/mind-map/releases)。
百度云盘:[地址](https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3)。
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
# 特性
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴(横向、竖向)、鱼骨图结构
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴、鱼骨图六种结构
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
- [x] 节点内容支持文本(普通文本、富文本)、图片、图标、超链接、备注、标签、概要
- [x] 节点支持拖拽(拖拽移动、自由调整)、多种节点形状,支持使用 DDM 完全自定义节点内容
- [x] 支持画布拖动、缩放
- [x] 支持鼠标按键拖动选择和Ctrl+左键两种多选节点方式
- [x] 支持导出为`json``png``svg``pdf``markdown``xmind`,支持从`json``xmind``markdown`导入
- [x] 支持快捷键、前进后退、关联线、搜索替换、小地图、水印
- [x] 提供丰富的配置,满足各种场景各种使用习惯
- [x] 支持快捷键
- [x] 节点内容支持图片、图标、超链接、备注、标签、概要
- [x] 支持前进后退
- [x] 支持拖动、缩放
- [x] 支持右键和Ctrl+左键两种多选方式
- [x] 支持节点自由拖拽、拖拽调整
- [x] 支持多种节点形状
- [x] 支持导出为`json``png``svg``pdf``markdown`,支持从`json``xmind``markdown`导入
- [x] 支持小地图、支持水印
- [x] 支持关联线
# 安装
@@ -83,100 +80,8 @@ const mindMap = new MindMap({
# License
[MIT](./LICENSE)
MIT
# 微信交流群
群聊人数较多,无法通过二维码入群,可以微信添加`wanglinguanfang`拉你入群。
# 请作者喝杯咖啡
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡哟~
> 厚椰乳一盒 + 纯牛奶半盒 + 冰块 + 咖啡液 = 生椰拿铁 yyds
> 推荐使用支付宝,微信获取不到头像。转账请备注【思维导图】。你的头像和名字将会出现在下面和[文档页面](https://wanglin2.github.io/mind-map/#/doc/zh/introduction/%E8%AF%B7%E4%BD%9C%E8%80%85%E5%96%9D%E6%9D%AF%E5%92%96%E5%95%A1)
<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/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/小土渣的宇宙.jpeg" style="width: 50px;height: 50px;" />
<span>小土渣的宇宙</span>
</span>
<span>
<img src="./web/src/assets/avatar/qp.jpg" style="width: 50px;height: 50px;" />
<span>qp</span>
</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/Chris.jpg" style="width: 50px;height: 50px;" />
<span>Chris</span>
</span>
<span>
<img src="./web/src/assets/avatar/水车.jpg" style="width: 50px;height: 50px;" />
<span>水车</span>
</span>
<span>
<img src="./web/src/assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;" />
<span>仓鼠</span>
</span>
<span>
<img src="./web/src/assets/avatar/千帆.jpg" style="width: 50px;height: 50px;" />
<span>千帆</span>
</span>
<span>
<img src="./web/src/assets/avatar/才镇.jpg" style="width: 50px;height: 50px;" />
<span>才镇</span>
</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>
</p>
![](./qrcode.jpg)

File diff suppressed because one or more lines are too long

BIN
qrcode.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,48 +1,28 @@
import MindMap from './index'
import MiniMap from './src/plugins/MiniMap.js'
import Watermark from './src/plugins/Watermark.js'
import KeyboardNavigation from './src/plugins/KeyboardNavigation.js'
import ExportXMind from './src/plugins/ExportXMind.js'
import ExportPDF from './src/plugins/ExportPDF.js'
import Export from './src/plugins/Export.js'
import Drag from './src/plugins/Drag.js'
import Select from './src/plugins/Select.js'
import AssociativeLine from './src/plugins/AssociativeLine'
import RichText from './src/plugins/RichText'
import NodeImgAdjust from './src/plugins/NodeImgAdjust.js'
import TouchEvent from './src/plugins/TouchEvent.js'
import Search from './src/plugins/Search.js'
import Painter from './src/plugins/Painter.js'
import Scrollbar from './src/plugins/Scrollbar.js'
import MiniMap from './src/MiniMap.js'
import Watermark from './src/Watermark.js'
import KeyboardNavigation from './src/KeyboardNavigation.js'
import Export from './src/Export.js'
import Drag from './src/Drag.js'
import Select from './src/Select.js'
import AssociativeLine from './src/AssociativeLine'
import RichText from './src/RichText'
import xmind from './src/parse/xmind.js'
import markdown from './src/parse/markdown.js'
import icons from './src/svg/icons.js'
import * as constants from './src/constants/constant.js'
import themes from './src/themes/index.js'
import * as defaultTheme from './src/themes/default.js'
MindMap.xmind = xmind
MindMap.markdown = markdown
MindMap.iconList = icons.nodeIconList
MindMap.constants = constants
MindMap.themes = themes
MindMap.defaultTheme = defaultTheme
MindMap
.usePlugin(MiniMap)
.usePlugin(Watermark)
.usePlugin(Drag)
.usePlugin(KeyboardNavigation)
.usePlugin(ExportXMind)
.usePlugin(ExportPDF)
.usePlugin(Export)
.usePlugin(Select)
.usePlugin(AssociativeLine)
.usePlugin(RichText)
.usePlugin(TouchEvent)
.usePlugin(NodeImgAdjust)
.usePlugin(Search)
.usePlugin(Painter)
.usePlugin(Scrollbar)
export default MindMap

View File

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

View File

@@ -1,25 +1,129 @@
import View from './src/core/view/View'
import Event from './src/core/event/Event'
import Render from './src/core/render/Render'
import View from './src/View'
import Event from './src/Event'
import Render from './src/Render'
import merge from 'deepmerge'
import theme from './src/themes'
import Style from './src/core/render/node/Style'
import KeyCommand from './src/core/command/KeyCommand'
import Command from './src/core/command/Command'
import BatchExecution from './src/utils/BatchExecution'
import {
layoutValueList,
CONSTANTS,
commonCaches,
ERROR_TYPES,
cssContent
} from './src/constants/constant'
import Style from './src/Style'
import KeyCommand from './src/KeyCommand'
import Command from './src/Command'
import BatchExecution from './src/BatchExecution'
import { layoutValueList, CONSTANTS } from './src/utils/constant'
import { SVG } from '@svgdotjs/svg.js'
import { simpleDeepClone, getType, getObjectChangedProps } from './src/utils'
import defaultTheme, {
checkIsNodeSizeIndependenceConfig
} from './src/themes/default'
import { defaultOpt } from './src/constants/defaultOptions'
import { simpleDeepClone } from './src/utils'
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
// 默认选项配置
const defaultOpt = {
// 是否只读
readonly: false,
// 布局
layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
fishboneDeg: 45,
// 主题
theme: 'default', // 内置主题default默认主题
// 主题配置,会和所选择的主题进行合并
themeConfig: {},
// 放大缩小的增量比例
scaleRatio: 0.1,
// 最多显示几个标签
maxTag: 5,
// 导出图片时的内边距
exportPadding: 20,
// 展开收缩按钮尺寸
expandBtnSize: 20,
// 节点里图片和文字的间距
imgTextMargin: 5,
// 节点里各种文字信息的间距,如图标和文字的间距
textContentMargin: 2,
// 多选节点时鼠标移动到边缘时的画布移动偏移量
selectTranslateStep: 3,
// 多选节点时鼠标移动距边缘多少距离时开始偏移
selectTranslateLimit: 20,
// 自定义节点备注内容显示
customNoteContentShow: null,
/*
{
show(){},
hide(){}
}
*/
// 是否开启节点自由拖拽
enableFreeDrag: false,
// 水印配置
watermarkConfig: {
text: '',
lineSpacing: 100,
textSpacing: 100,
angle: 30,
textStyle: {
color: '#999',
opacity: 0.5,
fontSize: 14
}
},
// 达到该宽度文本自动换行
textAutoWrapWidth: 500,
// 自定义鼠标滚轮事件处理
// 可以传一个函数,回调参数为事件对象
customHandleMousewheel: null,
// 鼠标滚动的行为如果customHandleMousewheel传了自定义函数这个属性不生效
mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM,// zoom放大缩小、move上下移动
// 当mousewheelAction设为move时可以通过该属性控制鼠标滚动一下视图移动的步长单位px
mousewheelMoveStep: 100,
// 默认插入的二级节点的文字
defaultInsertSecondLevelNodeText: '二级节点',
// 默认插入的二级以下节点的文字
defaultInsertBelowSecondLevelNodeText: '分支主题',
// 展开收起按钮的颜色
expandBtnStyle: {
color: '#808080',
fill: '#fff'
},
// 自定义展开收起按钮的图标
expandBtnIcon: {
open: '',// svg字符串
close: ''
},
// 是否只有当鼠标在画布内才响应快捷键事件
enableShortcutOnlyWhenMouseInSvg: true,
// 是否开启节点动画过渡
enableNodeTransitionMove: true,
// 如果开启节点动画过渡可以通过该属性设置过渡的时间单位ms
nodeTransitionMoveDuration: 300,
// 初始根节点的位置
initRootNodePosition: null,
// 导出png、svg、pdf时的图形内边距
exportPaddingX: 10,
exportPaddingY: 10,
// 节点文本编辑框的z-index
nodeTextEditZIndex: 3000,
// 节点备注浮层的z-index
nodeNoteTooltipZIndex: 3000,
// 是否在点击了画布外的区域时结束节点文本的编辑状态
isEndNodeTextEditOnClickOuter: true,
// 最大历史记录数
maxHistoryCount: 1000,
// 是否一直显示节点的展开收起按钮,默认为鼠标移上去和激活时才显示
alwaysShowExpandBtn: false,
// 扩展节点可插入的图标
iconList: [
// {
// name: '',// 分组名称
// type: '',// 分组的值
// list: [// 分组下的图标列表
// {
// name: '',// 图标名称
// icon:''// 图标可以传svg或图片
// }
// ]
// }
],
// 节点最大缓存数量
maxNodeCacheCount: 1000,
// 关联线默认文字
defaultAssociativeLineText: '关联'
}
// 思维导图
class MindMap {
@@ -30,28 +134,22 @@ class MindMap {
// 容器元素
this.el = this.opt.el
if (!this.el) throw new Error('缺少容器元素el')
this.elRect = this.el.getBoundingClientRect()
// 画布宽高
this.width = this.elRect.width
this.height = this.elRect.height
if (this.width <= 0 || this.height <= 0) throw new Error('容器元素el的宽高不能为0')
// 添加css
this.cssEl = null
this.addCss()
// 画布
this.svg = SVG().addTo(this.el).size(this.width, this.height)
this.draw = this.svg.group()
// 节点id
this.uid = 1
// 初始化主题
this.initTheme()
// 初始化缓存数据
this.initCache()
// 事件类
this.event = new Event({
mindMap: this
@@ -82,7 +180,7 @@ class MindMap {
this.batchExecution = new BatchExecution()
// 注册插件
MindMap.pluginList.forEach(plugin => {
MindMap.pluginList.forEach((plugin) => {
this.initPlugin(plugin)
})
@@ -95,8 +193,6 @@ class MindMap {
// 配置参数处理
handleOpt(opt) {
// 深拷贝一份节点数据
opt.data = simpleDeepClone(opt.data || {})
// 检查布局配置
if (!layoutValueList.includes(opt.layout)) {
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
@@ -106,19 +202,6 @@ class MindMap {
return opt
}
// 添加必要的css样式到页面
addCss() {
this.cssEl = document.createElement('style')
this.cssEl.type = 'text/css'
this.cssEl.innerHTML = cssContent
document.head.appendChild(this.cssEl)
}
// 移除css
removeCss() {
document.head.removeChild(this.cssEl)
}
// 渲染,部分渲染
render(callback, source = '') {
this.batchExecution.push('render', () => {
@@ -161,23 +244,6 @@ class MindMap {
this.event.off(event, fn)
}
// 初始化缓存数据
initCache() {
Object.keys(commonCaches).forEach(key => {
let type = getType(commonCaches[key])
let value = ''
switch (type) {
case 'Boolean':
value = false
break
default:
value = null
break
}
commonCaches[key] = value
})
}
// 设置主题
initTheme() {
// 合并主题配置
@@ -191,7 +257,6 @@ class MindMap {
this.renderer.clearAllActive()
this.opt.theme = theme
this.render(null, CONSTANTS.CHANGE_THEME)
this.emit('view_theme_change', theme)
}
// 获取当前主题
@@ -201,11 +266,9 @@ class MindMap {
// 设置主题配置
setThemeConfig(config) {
// 计算改变了的配置
const changedConfig = getObjectChangedProps(this.themeConfig, config)
this.opt.themeConfig = config
// 检查改变的是否是节点大小无关的主题属性
let res = checkIsNodeSizeIndependenceConfig(changedConfig)
let res = checkIsNodeSizeIndependenceConfig(config)
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
}
@@ -243,7 +306,7 @@ class MindMap {
this.opt.layout = layout
this.view.reset()
this.renderer.setLayout()
this.render(null, CONSTANTS.CHANGE_LAYOUT)
this.render()
}
// 执行命令
@@ -287,7 +350,7 @@ class MindMap {
// 获取思维导图数据,节点树、主题、布局等
getData(withConfig) {
let nodeData = this.command.getCopyData()
let nodeData = this.command.removeDataUid(this.command.getCopyData())
let data = {}
if (withConfig) {
data = {
@@ -307,12 +370,8 @@ class MindMap {
// 导出
async export(...args) {
try {
let result = await this.doExport.export(...args)
return result
} catch (error) {
this.opt.errorHandler(ERROR_TYPES.EXPORT_ERROR, error)
}
let result = await this.doExport.export(...args)
return result
}
// 转换位置
@@ -350,23 +409,17 @@ class MindMap {
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 内边距
rect.width += paddingX * 2
rect.height += paddingY * 2
draw.translate(paddingX, paddingY)
rect.width += paddingX
rect.height += paddingY
draw.translate(paddingX / 2, paddingY / 2)
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
// 克隆一份数据
let clone = svg.clone()
// 添加必要的样式
clone.add(SVG(`<style>${ cssContent }</style>`))
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
if (
(rect.width > origWidth || rect.height > origHeight) &&
this.watermark &&
this.watermark.hasWatermark()
) {
if ((rect.width > origWidth || rect.height > origHeight) && this.watermark && this.watermark.hasWatermark()) {
this.width = rect.width
this.height = rect.height
this.watermark.draw()
@@ -423,38 +476,17 @@ class MindMap {
pluginOpt: plugin.pluginOpt
})
}
// 销毁
destroy() {
// 移除插件
;[...MindMap.pluginList].forEach(plugin => {
if (this[plugin.instanceName].beforePluginDestroy) {
this[plugin.instanceName].beforePluginDestroy()
}
this[plugin.instanceName] = null
})
// 解绑事件
this.event.unbind()
// 移除画布节点
this.svg.remove()
// 去除给容器元素设置的背景样式
Style.removeBackgroundStyle(this.el)
this.el.innerHTML = ''
this.el = null
this.removeCss()
}
}
// 插件列表
MindMap.pluginList = []
MindMap.usePlugin = (plugin, opt = {}) => {
if (MindMap.hasPlugin(plugin) !== -1) return
plugin.pluginOpt = opt
MindMap.pluginList.push(plugin)
return MindMap
}
MindMap.hasPlugin = plugin => {
return MindMap.pluginList.findIndex(item => {
MindMap.hasPlugin = (plugin) => {
return MindMap.pluginList.findIndex((item) => {
return item === plugin
})
}

View File

@@ -1,16 +1,18 @@
{
"name": "simple-mind-map",
"version": "0.6.15-fix.2",
"version": "0.4.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "0.6.15-fix.2",
"version": "0.4.7",
"license": "MIT",
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",
"canvg": "^3.0.7",
"deepmerge": "^1.5.2",
"eventemitter3": "^4.0.7",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0",
@@ -158,8 +160,7 @@
"node_modules/@types/raf": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz",
"integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==",
"optional": true
"integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw=="
},
"node_modules/@types/unist": {
"version": "2.0.6",
@@ -254,7 +255,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
@@ -305,7 +305,6 @@
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
@@ -382,7 +381,6 @@
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz",
"integrity": "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==",
"hasInstallScript": true,
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
@@ -411,7 +409,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
@@ -938,7 +935,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
@@ -1817,8 +1813,7 @@
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"optional": true
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
},
"node_modules/prelude-ls": {
"version": "1.2.1",
@@ -1913,7 +1908,6 @@
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
@@ -1988,7 +1982,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
@@ -2082,7 +2075,6 @@
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz",
"integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
@@ -2135,7 +2127,6 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
@@ -2144,7 +2135,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
@@ -2209,7 +2199,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
@@ -2397,8 +2386,7 @@
"@types/raf": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz",
"integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==",
"optional": true
"integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw=="
},
"@types/unist": {
"version": "2.0.6",
@@ -2465,8 +2453,7 @@
"base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="
},
"brace-expansion": {
"version": "1.1.11",
@@ -2502,7 +2489,6 @@
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"optional": true,
"requires": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
@@ -2558,8 +2544,7 @@
"core-js": {
"version": "3.27.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz",
"integrity": "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==",
"optional": true
"integrity": "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww=="
},
"core-util-is": {
"version": "1.0.3",
@@ -2581,7 +2566,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"requires": {
"utrie": "^1.0.2"
}
@@ -2972,7 +2956,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"requires": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
@@ -3528,8 +3511,7 @@
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"optional": true
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
},
"prelude-ls": {
"version": "1.2.1",
@@ -3594,7 +3576,6 @@
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"optional": true,
"requires": {
"performance-now": "^2.1.0"
}
@@ -3649,8 +3630,7 @@
"rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"optional": true
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="
},
"rimraf": {
"version": "3.0.2",
@@ -3711,8 +3691,7 @@
"stackblur-canvas": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz",
"integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==",
"optional": true
"integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ=="
},
"string_decoder": {
"version": "1.1.1",
@@ -3749,14 +3728,12 @@
"svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"optional": true
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="
},
"text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"requires": {
"utrie": "^1.0.2"
}
@@ -3808,7 +3785,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"requires": {
"base64-arraybuffer": "^1.0.2"
}

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.7.1",
"version": "0.5.11",
"description": "一个简单的web在线思维导图",
"authors": [
{
@@ -25,8 +25,10 @@
"__main": "./dist/simpleMindMap.umd.min.js",
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",
"canvg": "^3.0.7",
"deepmerge": "^1.5.2",
"eventemitter3": "^4.0.7",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"mdast-util-from-markdown": "^1.3.0",

View File

@@ -1,4 +1,4 @@
import { walk, bfsWalk, throttle } from '../utils'
import { walk, bfsWalk, throttle } from './utils/'
import { v4 as uuid } from 'uuid'
import {
getAssociativeLineTargetIndex,
@@ -7,11 +7,11 @@ import {
getNodePoint,
computeNodePoints,
getNodeLinePath
} from './associativeLine/associativeLineUtils'
import associativeLineControlsMethods from './associativeLine/associativeLineControls'
import associativeLineTextMethods from './associativeLine/associativeLineText'
} from './utils/associativeLineUtils'
import associativeLineControlsMethods from './utils/associativeLineControls'
import associativeLineTextMethods from './utils/associativeLineText'
// 关联线插件
// 关联线
class AssociativeLine {
constructor(opt = {}) {
this.mindMap = opt.mindMap
@@ -91,7 +91,12 @@ class AssociativeLine {
this.mindMap.on('node_dragging', this.onNodeDragging.bind(this))
this.mindMap.on('node_dragend', this.onNodeDragend.bind(this))
// 拖拽控制点
this.mindMap.on('mouseup', this.onControlPointMouseup.bind(this))
window.addEventListener('mousemove', e => {
this.onControlPointMousemove(e)
})
window.addEventListener('mouseup', e => {
this.onControlPointMouseup(e)
})
// 缩放事件
this.mindMap.on('scale', this.onScale)
}
@@ -99,33 +104,12 @@ class AssociativeLine {
// 创建箭头
createMarker() {
return this.draw.marker(20, 20, add => {
add.ref(12, 5)
add.ref(2, 5)
add.size(10, 10)
add.attr('orient', 'auto-start-reverse')
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
})
}
// 判断关联线坐标是否变更,有变更则使用变化后的坐标,无则默认坐标
updateAllLinesPos(node, toNode, associativeLinePoint) {
associativeLinePoint = associativeLinePoint || {}
let [startPoint, endPoint] = computeNodePoints(node, toNode)
let nodeRange = 0
let nodeDir = ''
let toNodeRange = 0
let toNodeDir = ''
if (associativeLinePoint.startPoint) {
nodeRange = associativeLinePoint.startPoint.range || 0
nodeDir = associativeLinePoint.startPoint.dir || 'right'
startPoint = getNodePoint(node, nodeDir, nodeRange)
}
if (associativeLinePoint.endPoint) {
toNodeRange = associativeLinePoint.endPoint.range || 0
toNodeDir = associativeLinePoint.endPoint.dir || 'right'
endPoint = getNodePoint(toNode, toNodeDir, toNodeRange)
}
return [startPoint, endPoint]
}
// 渲染所有连线
renderAllLines() {
@@ -158,17 +142,10 @@ class AssociativeLine {
0
)
nodeToIds.forEach((ids, node) => {
ids.forEach((id, index) => {
ids.forEach(id => {
let toNode = idToNode.get(id)
if (!node || !toNode) return
const associativeLinePoint = (node.nodeData.data.associativeLinePoint ||
[])[index]
// 切换结构和布局,都会更新坐标
const [startPoint, endPoint] = this.updateAllLinesPos(
node,
toNode,
associativeLinePoint
)
let [startPoint, endPoint] = computeNodePoints(node, toNode)
this.drawLine(startPoint, endPoint, node, toNode)
})
})
@@ -211,28 +188,11 @@ class AssociativeLine {
.fill({ color: 'none' })
clickPath.plot(pathStr)
// 文字
let text = this.createText({
path,
clickPath,
node,
toNode,
startPoint,
endPoint,
controlPoints
})
let text = this.createText({ path, clickPath, node, toNode, startPoint, endPoint, controlPoints })
// 点击事件
clickPath.click(e => {
e.stopPropagation()
this.setActiveLine({
path,
clickPath,
text,
node,
toNode,
startPoint,
endPoint,
controlPoints
})
this.setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints })
})
// 渲染关联线文字
this.renderText(this.getText(node, toNode), path, text)
@@ -240,17 +200,10 @@ class AssociativeLine {
}
// 激活某根关联线
setActiveLine({
path,
clickPath,
text,
node,
toNode,
startPoint,
endPoint,
controlPoints
}) {
let { associativeLineActiveColor } = this.mindMap.themeConfig
setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints }) {
let {
associativeLineActiveColor
} = this.mindMap.themeConfig
// 如果当前存在激活节点,那么取消激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.clearActiveNodes()
@@ -313,13 +266,12 @@ class AssociativeLine {
// 鼠标移动事件
onMousemove(e) {
this.onControlPointMousemove(e)
if (!this.isCreatingLine) return
this.updateCreatingLine(e)
}
// 更新创建过程中的连接线
updateCreatingLine(e) {
if (!this.isCreatingLine) return
let { x, y } = this.getTransformedEventPos(e)
let startPoint = getNodePoint(this.creatingStartNode)
let offsetX = x > startPoint.x ? -10 : 10
@@ -339,22 +291,6 @@ class AssociativeLine {
}
}
// 计算节点偏移位置
getNodePos(node) {
const { scaleX, scaleY, translateX, translateY } =
this.mindMap.draw.transform()
const { left, top, width, height } = node
let translateLeft = left * scaleX + translateX
let translateTop = top * scaleY + translateY
return {
left,
top,
translateLeft,
translateTop,
width,
height
}
}
// 检测当前移动到的目标节点
checkOverlapNode(x, y) {
this.overlapNode = null
@@ -362,7 +298,7 @@ class AssociativeLine {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
}
if (node.uid === this.creatingStartNode.uid || this.overlapNode) {
if (node === this.creatingStartNode || this.overlapNode) {
return
}
let { left, top, width, height } = node
@@ -379,7 +315,7 @@ class AssociativeLine {
// 完成创建连接线
completeCreateLine(node) {
if (this.creatingStartNode.uid === node.uid) return
if (this.creatingStartNode === node) return
this.addLine(this.creatingStartNode, node)
if (this.overlapNode && this.overlapNode.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
@@ -404,11 +340,6 @@ class AssociativeLine {
}
// 将目标节点id保存起来
let list = fromNode.nodeData.data.associativeLineTargets || []
// 连线节点是否存在相同的id,存在则阻止添加关联线
const sameLine = list.some(item => item === id)
if (sameLine) {
return
}
list.push(id)
// 保存控制点
let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
@@ -431,13 +362,9 @@ class AssociativeLine {
y: controlPoints[1].y - endPoint.y
}
]
let associativeLinePoint = fromNode.nodeData.data.associativeLinePoint || []
// 记录关联的起始|结束坐标
associativeLinePoint[list.length - 1] = { startPoint, endPoint }
this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
associativeLineTargets: list,
associativeLineTargetControlOffsets: offsetList,
associativeLinePoint
associativeLineTargetControlOffsets: offsetList
})
}
@@ -446,18 +373,13 @@ class AssociativeLine {
if (!this.activeLine) return
let [, , , node, toNode] = this.activeLine
this.removeControls()
let {
associativeLineTargets,
associativeLinePoint,
associativeLineTargetControlOffsets,
associativeLineText
} = node.nodeData.data
associativeLinePoint = associativeLinePoint || []
let { associativeLineTargets, associativeLineTargetControlOffsets, associativeLineText } =
node.nodeData.data
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
// 更新关联线文本数据
let newAssociativeLineText = {}
if (associativeLineText) {
Object.keys(associativeLineText).forEach(item => {
Object.keys(associativeLineText).forEach((item) => {
if (item !== toNode.nodeData.data.id) {
newAssociativeLineText[item] = associativeLineText[item]
}
@@ -468,10 +390,6 @@ class AssociativeLine {
associativeLineTargets: associativeLineTargets.filter((_, index) => {
return index !== targetIndex
}),
// 连接线坐标
associativeLinePoint: associativeLinePoint.filter((_, index) => {
return index !== targetIndex
}),
// 偏移量
associativeLineTargetControlOffsets: associativeLineTargetControlOffsets
? associativeLineTargetControlOffsets.filter((_, index) => {

View File

@@ -1,4 +1,4 @@
import { nextTick } from '.'
import { nextTick } from './utils'
// 批量执行
class BatchExecution {

View File

@@ -1,4 +1,4 @@
import { copyRenderTree, simpleDeepClone, nextTick } from '../../utils'
import { copyRenderTree, simpleDeepClone, nextTick } from './utils'
// 命令类
class Command {
@@ -89,7 +89,7 @@ class Command {
this.history.shift()
}
this.activeHistoryIndex = this.history.length - 1
this.mindMap.emit('data_change', data)
this.mindMap.emit('data_change', this.removeDataUid(data))
this.mindMap.emit(
'back_forward',
this.activeHistoryIndex,
@@ -110,7 +110,7 @@ class Command {
this.history.length
)
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
this.mindMap.emit('data_change', data)
this.mindMap.emit('data_change', this.removeDataUid(data))
return data
}
}
@@ -125,7 +125,7 @@ class Command {
this.activeHistoryIndex += step
this.mindMap.emit('back_forward', this.activeHistoryIndex, this.history.length)
let data = simpleDeepClone(this.history[this.activeHistoryIndex])
this.mindMap.emit('data_change', data)
this.mindMap.emit('data_change', this.removeDataUid(data))
return data
}
}

320
simple-mind-map/src/Drag.js Normal file
View File

@@ -0,0 +1,320 @@
import { bfsWalk, throttle } from './utils'
import Base from './layouts/Base'
// 节点拖动类
class Drag extends Base {
// 构造函数
constructor({ mindMap }) {
super(mindMap.renderer)
this.mindMap = mindMap
this.reset()
this.bindEvent()
}
// 复位
reset() {
// 当前拖拽节点
this.node = null
// 当前重叠节点
this.overlapNode = null
// 当前上一个同级节点
this.prevNode = null
// 当前下一个同级节点
this.nextNode = null
// 画布的变换数据
this.drawTransform = null
// 克隆节点
this.clone = null
// 连接线
this.line = null
// 同级位置占位符
this.placeholder = null
// 鼠标按下位置和节点左上角的偏移量
this.offsetX = 0
this.offsetY = 0
// 克隆节点左上角的坐标
this.cloneNodeLeft = 0
this.cloneNodeTop = 0
// 当前鼠标是否按下
this.isMousedown = false
// 拖拽的鼠标位置变量
this.mouseDownX = 0
this.mouseDownY = 0
this.mouseMoveX = 0
this.mouseMoveY = 0
// 鼠标移动的距离距鼠标按下的位置距离多少以上才认为是拖动事件
this.checkDragOffset = 10
}
// 绑定事件
bindEvent() {
this.checkOverlapNode = throttle(this.checkOverlapNode, 300, this)
this.mindMap.on('node_mousedown', (node, e) => {
if (this.mindMap.opt.readonly || node.isGeneralization) {
return
}
if (e.which !== 1 || node.isRoot) {
return
}
e.preventDefault()
// 计算鼠标按下的位置距离节点左上角的距离
this.drawTransform = this.mindMap.draw.transform()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.offsetX = x - (node.left * scaleX + translateX)
this.offsetY = y - (node.top * scaleY + translateY)
this.node = node
this.isMousedown = true
this.mouseDownX = x
this.mouseDownY = y
})
this.mindMap.on('mousemove', e => {
if (this.mindMap.opt.readonly) {
return
}
if (!this.isMousedown) {
return
}
this.mindMap.emit('node_dragging', this.node)
e.preventDefault()
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseMoveX = x
this.mouseMoveY = y
if (
Math.abs(x - this.mouseDownX) <= this.checkDragOffset &&
Math.abs(y - this.mouseDownY) <= this.checkDragOffset &&
!this.node.isDrag
) {
return
}
this.mindMap.renderer.clearAllActive()
this.onMove(x, y)
})
this.onMouseup = this.onMouseup.bind(this)
this.mindMap.on('node_mouseup', this.onMouseup)
this.mindMap.on('mouseup', this.onMouseup)
}
// 鼠标松开事件
onMouseup(e) {
if (!this.isMousedown) {
return
}
this.isMousedown = false
let _nodeIsDrag = this.node.isDrag
this.node.isDrag = false
this.node.show()
this.removeCloneNode()
// 存在重叠子节点,则移动作为其子节点
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
this.mindMap.execCommand('MOVE_NODE_TO', this.node, this.overlapNode)
} else if (this.prevNode) {
// 存在前一个相邻节点,作为其下一个兄弟节点
this.mindMap.renderer.setNodeActive(this.prevNode, false)
this.mindMap.execCommand('INSERT_AFTER', this.node, this.prevNode)
} else if (this.nextNode) {
// 存在下一个相邻节点,作为其前一个兄弟节点
this.mindMap.renderer.setNodeActive(this.nextNode, false)
this.mindMap.execCommand('INSERT_BEFORE', this.node, this.nextNode)
} else if (_nodeIsDrag && this.mindMap.opt.enableFreeDrag) {
// 自定义位置
let { x, y } = this.mindMap.toPos(
e.clientX - this.offsetX,
e.clientY - this.offsetY
)
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
x = (x - translateX) / scaleX
y = (y - translateY) / scaleY
this.node.left = x
this.node.top = y
this.node.customLeft = x
this.node.customTop = y
this.mindMap.execCommand('SET_NODE_CUSTOM_POSITION', this.node, x, y)
this.mindMap.render()
}
this.reset()
this.mindMap.emit('node_dragend')
}
// 创建克隆节点
createCloneNode() {
if (!this.clone) {
// 节点
this.clone = this.node.group.clone()
this.clone.opacity(0.5)
this.clone.css('z-index', 99999)
this.node.isDrag = true
this.node.hide()
// 连接线
this.line = this.draw.path()
this.line.opacity(0.5)
this.node.styleLine(this.line, this.node)
// 同级位置占位符
this.placeholder = this.draw.rect().fill({
color: this.node.style.merge('lineColor', true)
})
this.mindMap.draw.add(this.clone)
}
}
// 移除克隆节点
removeCloneNode() {
if (!this.clone) {
return
}
this.clone.remove()
this.line.remove()
this.placeholder.remove()
}
// 拖动中
onMove(x, y) {
if (!this.isMousedown) {
return
}
this.createCloneNode()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
this.cloneNodeLeft = x - this.offsetX
this.cloneNodeTop = y - this.offsetY
x = (this.cloneNodeLeft - translateX) / scaleX
y = (this.cloneNodeTop - translateY) / scaleY
let t = this.clone.transform()
this.clone.translate(x - t.translateX, y - t.translateY)
// 连接线
let parent = this.node.parent
this.line.plot(
this.quadraticCurvePath(
parent.left + parent.width / 2,
parent.top + parent.height / 2,
x + this.node.width / 2,
y + this.node.height / 2
)
)
this.checkOverlapNode()
}
// 检测重叠节点
checkOverlapNode() {
if (!this.drawTransform) {
return
}
let x = this.mouseMoveX
let y = this.mouseMoveY
this.overlapNode = null
this.prevNode = null
this.nextNode = null
this.placeholder.size(0, 0)
bfsWalk(this.mindMap.renderer.root, node => {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
}
if (node === this.node || this.node.isParent(node)) {
return
}
if (this.overlapNode || (this.prevNode && this.nextNode)) {
return
}
let nodeRect = this.getNodeRect(node)
let oneFourthHeight = nodeRect.height / 4
// 前一个和后一个节点
let checkList = node.parent ? node.parent.children.filter((item) => {
return item !== this.node
}) : []
let index = checkList.findIndex((item) => {
return item === node
})
let prevBrother = null
let nextBrother = null
if (index !== -1) {
if (index - 1 >= 0) {
prevBrother = checkList[index - 1]
}
if (index + 1 <= checkList.length - 1) {
nextBrother = checkList[index + 1]
}
}
// 和前一个兄弟节点的距离
let prevBrotherOffset = 0
if (prevBrother) {
let prevNodeRect = this.getNodeRect(prevBrother)
prevBrotherOffset = nodeRect.top - prevNodeRect.bottom
// 间距小于10就当它不存在
prevBrotherOffset = prevBrotherOffset >= 10 ? prevBrotherOffset / 2 : 0
} else {
// 没有前一个兄弟节点那么假设和前一个节点的距离为20
prevBrotherOffset = 10
}
// 和后一个兄弟节点的距离
let nextBrotherOffset = 0
if (nextBrother) {
let nextNodeRect = this.getNodeRect(nextBrother)
nextBrotherOffset = nextNodeRect.top - nodeRect.bottom
nextBrotherOffset = nextBrotherOffset >= 10 ? nextBrotherOffset / 2 : 0
} else {
nextBrotherOffset = 10
}
if (nodeRect.left <= x && nodeRect.right >= x) {
// 检测兄弟节点位置
if (!this.overlapNode && !this.prevNode && !this.nextNode && !node.isRoot) {
let checkIsPrevNode = nextBrotherOffset > 0 ? // 距离下一个兄弟节点的距离大于0
y > nodeRect.bottom && y <= (nodeRect.bottom + nextBrotherOffset) : // 那么在当前节点外底部判断
y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断
let checkIsNextNode = prevBrotherOffset > 0 ? // 距离上一个兄弟节点的距离大于0
y < nodeRect.top && y >= (nodeRect.top - prevBrotherOffset) : // 那么在当前节点外底部判断
y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
if (checkIsPrevNode) {
this.prevNode = node
let size = nextBrotherOffset > 0 ? nextBrotherOffset : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originBottom)
} else if (checkIsNextNode) {
this.nextNode = node
let size = prevBrotherOffset > 0 ? prevBrotherOffset : 5
this.placeholder.size(node.width, size).move(nodeRect.originLeft, nodeRect.originTop - size)
}
}
// 检测是否重叠
if (!this.overlapNode && !this.prevNode && !this.nextNode) {
if (
nodeRect.top + (prevBrotherOffset > 0 ? 0 : oneFourthHeight) <= y &&
nodeRect.bottom - (nextBrotherOffset > 0 ? 0 : oneFourthHeight) >= y
) {
this.overlapNode = node
}
}
}
})
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
}
}
// 计算节点的位置尺寸信息
getNodeRect(node) {
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let { left, top, width, height } = node
let originLeft = left
let originTop = top
let originBottom = top + height
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
return {
width,
height,
left,
top,
right,
bottom,
originLeft,
originTop,
originBottom
}
}
}
Drag.instanceName = 'drag'
export default Drag

View File

@@ -1,5 +1,5 @@
import EventEmitter from 'eventemitter3'
import { CONSTANTS } from '../../constants/constant'
import { CONSTANTS } from './utils/constant'
// 事件类
class Event extends EventEmitter {
@@ -9,8 +9,6 @@ class Event extends EventEmitter {
this.opt = opt
this.mindMap = opt.mindMap
this.isLeftMousedown = false
this.isRightMousedown = false
this.isMiddleMousedown = false
this.mousedownPos = {
x: 0,
y: 0
@@ -91,10 +89,6 @@ class Event extends EventEmitter {
// 鼠标左键
if (e.which === 1) {
this.isLeftMousedown = true
} else if (e.which === 3) {
this.isRightMousedown = true
} else if (e.which === 2) {
this.isMiddleMousedown = true
}
this.mousedownPos.x = e.clientX
this.mousedownPos.y = e.clientY
@@ -103,18 +97,12 @@ class Event extends EventEmitter {
// 鼠标移动事件
onMousemove(e) {
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
this.mousemovePos.x = e.clientX
this.mousemovePos.y = e.clientY
this.mousemoveOffset.x = e.clientX - this.mousedownPos.x
this.mousemoveOffset.y = e.clientY - this.mousedownPos.y
this.emit('mousemove', e, this)
if (
this.isMiddleMousedown ||
(useLeftKeySelectionRightKeyDrag
? this.isRightMousedown
: this.isLeftMousedown)
) {
if (this.isLeftMousedown) {
e.preventDefault()
this.emit('drag', e, this)
}
@@ -123,8 +111,6 @@ class Event extends EventEmitter {
// 鼠标松开事件
onMouseup(e) {
this.isLeftMousedown = false
this.isRightMousedown = false
this.isMiddleMousedown = false
this.emit('mouseup', e, this)
}
@@ -145,13 +131,7 @@ class Event extends EventEmitter {
if ((e.wheelDeltaX || e.detail) > 0) dir = CONSTANTS.DIR.LEFT
if ((e.wheelDeltaX || e.detail) < 0) dir = CONSTANTS.DIR.RIGHT
}
// 判断是否是触控板
let isTouchPad = false
// mac、windows
if (e.wheelDeltaY === e.deltaY * -3 || Math.abs(e.wheelDeltaY) <= 10) {
isTouchPad = true
}
this.emit('mousewheel', e, dir, this, isTouchPad)
this.emit('mousewheel', e, dir, this)
}
// 鼠标右键菜单事件

View File

@@ -1,19 +1,16 @@
import {
imgToDataUrl,
downloadFile,
readBlob,
removeHTMLEntities
} from '../utils'
import { imgToDataUrl, downloadFile, readBlob } from './utils'
import JsPDF from 'jspdf'
import { SVG } from '@svgdotjs/svg.js'
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
import { transformToMarkdown } from '../parse/toMarkdown'
import { a4Size } from '../constants/constant'
import drawBackgroundImageToCanvas from './utils/simulateCSSBackgroundInCanvas'
import { transformToMarkdown } from './parse/toMarkdown'
const URL = window.URL || window.webkitURL || window
// 导出插件
// 导出
class Export {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
this.exportPadding = this.mindMap.opt.exportPadding
}
// 导出
@@ -40,10 +37,6 @@ class Export {
let imageList = svg.find('image')
let task = imageList.map(async item => {
let imgUlr = item.attr('href') || item.attr('xlink:href')
// 已经是data:URL形式不用转换
if (/^data:/.test(imgUlr)) {
return
}
let imgData = await imgToDataUrl(imgUlr)
item.attr('href', imgData)
})
@@ -58,42 +51,33 @@ class Export {
}
// svg转png
svgToPng(svgSrc, transparent, checkRotate = () => { return false }) {
svgToPng(svgSrc, transparent) {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
img.setAttribute('crossOrigin', 'anonymous')
img.onload = async () => {
try {
const canvas = document.createElement('canvas')
const dpr = Math.max(window.devicePixelRatio, this.mindMap.opt.minExportImgCanvasScale)
const imgWidth = img.width
const imgHeight = img.height
// 如果宽比高长那么旋转90度
const needRotate = checkRotate(imgWidth, imgHeight)
if (needRotate) {
canvas.width = imgHeight * dpr
canvas.height = imgWidth * dpr
canvas.style.width = imgHeight + 'px'
canvas.style.height = imgWidth + 'px'
} else {
canvas.width = imgWidth * dpr
canvas.height = imgHeight * dpr
canvas.style.width = imgWidth + 'px'
canvas.style.height = imgHeight + 'px'
}
const ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr)
if (needRotate) {
ctx.rotate(0.5 * Math.PI)
ctx.translate(0, -imgHeight)
}
let canvas = document.createElement('canvas')
canvas.width = img.width + this.exportPadding * 2
canvas.height = img.height + this.exportPadding * 2
let ctx = canvas.getContext('2d')
// 绘制背景
if (!transparent) {
await this.drawBackgroundToCanvas(ctx, imgWidth, imgHeight)
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
}
// 图片绘制到canvas里
ctx.drawImage(img, 0, 0, imgWidth, imgHeight)
ctx.drawImage(
img,
0,
0,
img.width,
img.height,
this.exportPadding,
this.exportPadding,
img.width,
img.height
)
resolve(canvas.toDataURL())
} catch (error) {
reject(error)
@@ -114,7 +98,7 @@ class Export {
backgroundImage,
backgroundRepeat = 'no-repeat',
backgroundPosition = 'center center',
backgroundSize = 'cover'
backgroundSize = 'cover',
} = this.mindMap.themeConfig
// 背景颜色
ctx.save()
@@ -125,25 +109,18 @@ class Export {
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
ctx.save()
drawBackgroundImageToCanvas(
ctx,
width,
height,
backgroundImage,
{
backgroundRepeat,
backgroundPosition,
backgroundSize
},
err => {
if (err) {
reject(err)
} else {
resolve()
}
ctx.restore()
drawBackgroundImageToCanvas(ctx, width, height, backgroundImage, {
backgroundRepeat,
backgroundPosition,
backgroundSize
}, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
)
ctx.restore()
})
} else {
resolve()
}
@@ -177,25 +154,13 @@ class Export {
* 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png(name, transparent = false, checkRotate) {
async png(name, transparent = false) {
let { node, str } = await this.getSvgData()
str = removeHTMLEntities(str)
// 如果开启了富文本则使用htmltocanvas转换为图片
if (this.mindMap.richText) {
// 覆盖html默认的样式
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>${ this.mindMap.opt.resetCss }</style>`))
}
str = node.svg()
// 使用其他库html2canvas、dom-to-image-more等来完成导出
// let res = await this.mindMap.richText.handleExportPng(node.node)
// let imgDataUrl = await this.svgToPng(
// res,
// transparent,
// checkRotate
// )
// return imgDataUrl
let res = await this.mindMap.richText.handleExportPng(node.node)
let imgDataUrl = await this.svgToPng(res, transparent)
return imgDataUrl
}
// 转换成blob数据
let blob = new Blob([str], {
@@ -204,52 +169,58 @@ class Export {
// 转换成data:url数据
let svgUrl = await readBlob(blob)
// 绘制到canvas上
let res = await this.svgToPng(
svgUrl,
transparent,
checkRotate
)
let res = await this.svgToPng(svgUrl, transparent)
return res
}
// 导出为pdf
async pdf(name, useMultiPageExport) {
if (!this.mindMap.doExportPDF) {
throw new Error('请注册ExportPDF插件')
async pdf(name) {
let img = await this.png()
let pdf = new JsPDF('', 'pt', 'a4')
let a4Width = 595
let a4Height = 841
let a4Ratio = a4Width / a4Height
let image = new Image()
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
let imageRatio = imageWidth / imageHeight
let w, h
if (imageWidth <= a4Width && imageHeight <= a4Height) {
// 使用图片原始宽高
w = imageWidth
h = imageHeight
} else if (a4Ratio > imageRatio) {
// 以a4Height为高度缩放图片宽度
w = imageRatio * a4Height
h = a4Height
} else {
// 以a4Width为宽度缩放图片高度
w = a4Width
h = a4Width / imageRatio
}
pdf.addImage(img, 'PNG', (a4Width - w) / 2, (a4Height - h) / 2, w, h)
pdf.save(name)
}
let img = await this.png('', false, (width, height) => {
if (width <= a4Size.width && height && a4Size.height) return false
return (width / height) > 1
})
this.mindMap.doExportPDF.pdf(name, img, useMultiPageExport)
}
// 导出为xmind
async xmind(name) {
if (!this.mindMap.doExportXMind) {
throw new Error('请注册ExportXMind插件')
}
const data = this.mindMap.getData()
const blob = await this.mindMap.doExportXMind.xmind(data, name)
const res = await readBlob(blob)
return res
image.src = img
}
// 导出为svg
// plusCssText附加的css样式如果svg中存在dom节点想要设置一些针对节点的样式可以通过这个参数传入
async svg(name) {
async svg(name, plusCssText) {
let { node } = await this.getSvgData()
// 开启了节点富文本编辑
if (this.mindMap.richText) {
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>${ this.mindMap.opt.resetCss }</style>`))
if (plusCssText) {
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>${plusCssText}</style>`))
}
}
}
node.first().before(SVG(`<title>${name}</title>`))
await this.drawBackgroundToSvg(node)
let str = node.svg()
str = removeHTMLEntities(str)
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'

View File

@@ -1,4 +1,4 @@
import { keyMap } from './keyMap'
import { keyMap } from './utils/keyMap'
// 快捷按键、命令处理类
export default class KeyCommand {
// 构造函数
@@ -57,11 +57,8 @@ export default class KeyCommand {
}
Object.keys(this.shortcutMap).forEach(key => {
if (this.checkKey(e, key)) {
// 粘贴事件不组织因为要监听paste事件
if (!this.checkKey(e, 'Control+v')) {
e.stopPropagation()
e.preventDefault()
}
e.stopPropagation()
e.preventDefault()
this.shortcutMap[key].forEach(fn => {
fn()
})
@@ -108,11 +105,6 @@ export default class KeyCommand {
return arr
}
// 判断是否按下了组合键
hasCombinationKey(e) {
return e.ctrlKey || e.metaKey || e.altKey || e.shiftKey
}
// 获取快捷键对应的键值数组
getKeyCodeArr(key) {
let keyArr = key.split(/\s*\+\s*/)

View File

@@ -1,7 +1,7 @@
import { bfsWalk } from '../utils'
import { CONSTANTS } from '../constants/constant'
import { bfsWalk } from './utils'
import { CONSTANTS } from './utils/constant'
// 键盘导航插件
// 键盘导航
class KeyboardNavigation {
// 构造函数
constructor(opt) {
@@ -28,7 +28,8 @@ class KeyboardNavigation {
this.focus(dir)
} else {
let root = this.mindMap.renderer.root
this.mindMap.execCommand('GO_TARGET_NODE', root)
this.mindMap.renderer.moveNodeToCenter(root)
root.active()
}
}
@@ -80,7 +81,8 @@ class KeyboardNavigation {
// 找到了则让目标节点聚焦
if (targetNode) {
this.mindMap.execCommand('GO_TARGET_NODE', targetNode)
this.mindMap.renderer.moveNodeToCenter(targetNode)
targetNode.active()
}
}
@@ -94,7 +96,7 @@ class KeyboardNavigation {
// 遍历节点树
bfsWalk(this.mindMap.renderer.root, node => {
// 跳过当前聚焦的节点
if (node.uid === currentActiveNode.uid) return
if (node === currentActiveNode) return
// 当前遍历到的节点的位置信息
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
@@ -131,7 +133,7 @@ class KeyboardNavigation {
checkNodeDis
}) {
bfsWalk(this.mindMap.renderer.root, node => {
if (node.uid === currentActiveNode.uid) return
if (node === currentActiveNode) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
@@ -173,7 +175,7 @@ class KeyboardNavigation {
let cX = (currentActiveNodeRect.right + currentActiveNodeRect.left) / 2
let cY = (currentActiveNodeRect.bottom + currentActiveNodeRect.top) / 2
bfsWalk(this.mindMap.renderer.root, node => {
if (node.uid === currentActiveNode.uid) return
if (node === currentActiveNode) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
// 遍历到的节点的中心点

View File

@@ -1,6 +1,4 @@
import { isWhite, isTransparent, getVisibleColorFromTheme } from '../utils/index'
// 小地图插件
// 小地图类
class MiniMap {
// 构造函数
constructor(opt) {
@@ -22,14 +20,9 @@ class MiniMap {
* boxHeight小地图容器的高度
*/
calculationMiniMap(boxWidth, boxHeight) {
let { svg, rect, origWidth, origHeight, scaleX, scaleY } =
let { svgHTML, rect, origWidth, origHeight, scaleX, scaleY } =
this.mindMap.getSvgData()
// 计算数据
const elRect = this.mindMap.elRect
rect.x -= elRect.left
rect.x2 -= elRect.left
rect.y -= elRect.top
rect.y2 -= elRect.top
let boxRatio = boxWidth / boxHeight
let actWidth = 0
let actHeight = 0
@@ -60,31 +53,20 @@ class MiniMap {
bottom: 0
}
viewBoxStyle.left =
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft + 'px'
viewBoxStyle.right =
Math.max(0, ((_rectX2 - origWidth) / _rectWidth) * actWidth) +
miniMapBoxLeft
miniMapBoxLeft +
'px'
viewBoxStyle.top =
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop + 'px'
viewBoxStyle.bottom =
Math.max(0, ((_rectY2 - origHeight) / _rectHeight) * actHeight) +
miniMapBoxTop
if (viewBoxStyle.top > miniMapBoxTop + actHeight) {
viewBoxStyle.top = miniMapBoxTop + actHeight
}
if (viewBoxStyle.left > miniMapBoxLeft + actWidth) {
viewBoxStyle.left = miniMapBoxLeft + actWidth
}
Object.keys(viewBoxStyle).forEach((key) => {
viewBoxStyle[key] = viewBoxStyle[key] + 'px'
})
this.removeNodeContent(svg)
miniMapBoxTop +
'px'
return {
svgHTML: svg.svg(), // 小地图html
svgHTML, // 小地图html
viewBoxStyle, // 视图框的位置信息
miniMapBoxScale, // 视图框的缩放值
miniMapBoxLeft, // 视图框的left值
@@ -92,26 +74,6 @@ class MiniMap {
}
}
// 移除节点的内容
removeNodeContent(svg) {
if (svg.hasClass('smm-node')) {
let shape = svg.findOne('.smm-node-shape')
let fill = shape.attr('fill')
if (isWhite(fill) || isTransparent(fill)) {
shape.attr('fill', getVisibleColorFromTheme(this.mindMap.themeConfig))
}
svg.clear()
svg.add(shape)
return
}
let children = svg.children()
if (children && children.length > 0) {
children.forEach((node) => {
this.removeNodeContent(node)
})
}
}
// 小地图鼠标按下事件
onMousedown(e) {
this.isMousedown = true

View File

@@ -1,12 +1,12 @@
import Style from './Style'
import Shape from './Shape'
import { G, ForeignObject, SVG, Rect } from '@svgdotjs/svg.js'
import nodeGeneralizationMethods from './nodeGeneralization'
import nodeExpandBtnMethods from './nodeExpandBtn'
import nodeCommandWrapsMethods from './nodeCommandWraps'
import nodeCreateContentsMethods from './nodeCreateContents'
import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect'
import { CONSTANTS } from '../../../constants/constant'
import { asyncRun } from './utils'
import { G, Rect } from '@svgdotjs/svg.js'
import nodeGeneralizationMethods from './utils/nodeGeneralization'
import nodeExpandBtnMethods from './utils/nodeExpandBtn'
import nodeCommandWrapsMethods from './utils/nodeCommandWraps'
import nodeCreateContentsMethods from './utils/nodeCreateContents'
import { CONSTANTS } from './utils/constant'
// 节点类
class Node {
@@ -58,9 +58,7 @@ class Node {
// 节点内容的容器
this.group = null
this.shapeNode = null // 节点形状节点
this.hoverNode = null // 节点hover和激活的节点
// 节点内容对象
this._customNodeContent = null
this._imgData = null
this._iconData = null
this._textData = null
@@ -99,8 +97,6 @@ class Node {
this.isMultipleChoice = false
// 是否需要重新layout
this.needLayout = false
// 当前是否是隐藏状态
this.isHide = false
// 概要相关方法
Object.keys(nodeGeneralizationMethods).forEach(item => {
this[item] = nodeGeneralizationMethods[item].bind(this)
@@ -109,10 +105,6 @@ class Node {
Object.keys(nodeExpandBtnMethods).forEach(item => {
this[item] = nodeExpandBtnMethods[item].bind(this)
})
// 展开收起按钮占位元素相关方法
Object.keys(nodeExpandBtnPlaceholderRectMethods).forEach(item => {
this[item] = nodeExpandBtnPlaceholderRectMethods[item].bind(this)
})
// 命令的相关方法
Object.keys(nodeCommandWrapsMethods).forEach(item => {
this[item] = nodeCommandWrapsMethods[item].bind(this)
@@ -162,13 +154,6 @@ class Node {
// 创建节点的各个内容对象数据
createNodeData() {
// 自定义节点内容
let { isUseCustomNodeContent, customCreateNodeContent } = this.mindMap.opt
if (isUseCustomNodeContent && customCreateNodeContent) {
this._customNodeContent = customCreateNodeContent(this)
}
// 如果没有返回内容,那么还是使用内置的节点内容
if (this._customNodeContent) return
this._imgData = this.createImgNode()
this._iconData = this.createIconNode()
this._textData = this.createTextNode()
@@ -191,14 +176,6 @@ class Node {
// 计算节点尺寸信息
getNodeRect() {
// 自定义节点内容
if (this.isUseCustomNodeContent()) {
let rect = this.measureCustomNodeContentSize(this._customNodeContent)
return {
width: rect.width,
height: rect.height
}
}
// 宽高
let imgContentWidth = 0
let imgContentHeight = 0
@@ -258,11 +235,9 @@ class Node {
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
this.shapePadding.paddingX = shapePaddingX
this.shapePadding.paddingY = shapePaddingY
// 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点
const borderWidth = this.getBorderWidth()
return {
width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth,
height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth
width: _width + paddingX * 2 + shapePaddingX * 2,
height: _height + paddingY * 2 + margin + shapePaddingY * 2
}
}
@@ -270,43 +245,27 @@ class Node {
layout() {
// 清除之前的内容
this.group.clear()
const { hoverRectPadding } = this.mindMap.opt
let { width, height, textContentItemMargin } = this
let { paddingY } = this.getPaddingVale()
const halfBorderWidth = this.getBorderWidth() / 2
paddingY += this.shapePadding.paddingY + halfBorderWidth
paddingY += this.shapePadding.paddingY
// 节点形状
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.updateNodeShape()
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
this.renderExpandBtnPlaceholderRect()
if (!this.mindMap.opt.alwaysShowExpandBtn) {
if (!this._unVisibleRectRegionNode) {
this._unVisibleRectRegionNode = new Rect()
}
this._unVisibleRectRegionNode.fill({
color: 'transparent'
}).size(this.expandBtnSize, height).x(width).y(0)
this.group.add(this._unVisibleRectRegionNode)
}
// 概要节点添加一个带所属节点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()) {
let foreignObject = new ForeignObject()
foreignObject.width(width)
foreignObject.height(height)
foreignObject.add(this._customNodeContent)
this.group.add(foreignObject)
addHoverNode()
return
}
// 图片节点
let imgHeight = 0
if (this._imgData) {
@@ -378,39 +337,29 @@ class Node {
: 0)
)
this.group.add(textContentNested)
addHoverNode()
}
// 给节点绑定事件
bindGroupEvent() {
// 单击事件,选中节点
this.group.on('click', e => {
this.mindMap.emit('node_click', this, e)
if (this.isMultipleChoice) {
e.stopPropagation()
this.isMultipleChoice = false
return
}
this.mindMap.emit('node_click', this, e)
this.active(e)
})
this.group.on('mousedown', e => {
const { readonly, enableCtrlKeyNodeSelection, useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
// 只读模式不需要阻止冒泡
if (!readonly) {
if (this.isRoot) {
// 根节点,右键拖拽画布模式下不需要阻止冒泡
if (e.which === 3 && !useLeftKeySelectionRightKeyDrag) {
e.stopPropagation()
}
} else {
// 非根节点,且按下的是非鼠标中键,需要阻止事件冒泡
if (e.which !== 2) {
e.stopPropagation()
}
}
if (this.isRoot && e.which === 3) {
e.stopPropagation()
}
if (!this.isRoot) {
e.stopPropagation()
}
// 多选和取消多选
if (e.ctrlKey && enableCtrlKeyNodeSelection) {
if (e.ctrlKey) {
this.isMultipleChoice = true
let isActive = this.nodeData.data.isActive
if (!isActive)
@@ -426,13 +375,13 @@ class Node {
this.mindMap.emit(
'node_active',
isActive ? null : this,
[...this.mindMap.renderer.activeNodeList]
this.mindMap.renderer.activeNodeList
)
}
this.mindMap.emit('node_mousedown', this, e)
})
this.group.on('mouseup', e => {
if (!this.isRoot && e.which !== 2 && !this.mindMap.opt.readonly) {
if (!this.isRoot) {
e.stopPropagation()
}
this.mindMap.emit('node_mouseup', this, e)
@@ -458,17 +407,12 @@ class Node {
})
// 右键菜单事件
this.group.on('contextmenu', e => {
const { readonly, useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
// 按住ctrl键点击鼠标左键不知为何触发的是contextmenu事件
if (readonly || e.ctrlKey) {
if (this.mindMap.opt.readonly || e.ctrlKey) {// || this.isGeneralization
return
}
e.stopPropagation()
e.preventDefault()
// 如果是多选节点结束,那么不要触发右键菜单事件
if(this.mindMap.select && !useLeftKeySelectionRightKeyDrag && this.mindMap.select.hasSelectRange()) {
return
}
if (this.nodeData.data.isActive) {
this.renderer.clearActive()
}
@@ -490,16 +434,16 @@ class Node {
this.renderer.clearActive()
this.mindMap.execCommand('SET_NODE_ACTIVE', this, true)
this.renderer.addActiveNode(this)
this.mindMap.emit('node_active', this, [...this.renderer.activeNodeList])
this.mindMap.emit('node_active', this, this.renderer.activeNodeList)
}
// 更新节点
update() {
update(isLayout = false) {
if (!this.group) {
return
}
this.updateNodeActive()
let { alwaysShowExpandBtn } = this.mindMap.opt
let { enableNodeTransitionMove, nodeTransitionMoveDuration, alwaysShowExpandBtn } =
this.mindMap.opt
if (alwaysShowExpandBtn) {
// 需要移除展开收缩按钮
if (this._expandBtn && this.nodeData.children.length <= 0) {
@@ -521,45 +465,14 @@ class Node {
this.renderGeneralization()
// 更新节点位置
let t = this.group.transform()
// // 如果上次不在可视区内,且本次也不在,那么直接返回
// let { left: ox, top: oy } = this.getNodePosInClient(
// t.translateX,
// t.translateY
// )
// let oldIsInClient =
// ox > 0 && oy > 0 && ox < this.mindMap.width && oy < this.mindMap.height
// let { left: nx, top: ny } = this.getNodePosInClient(this.left, this.top)
// let newIsNotInClient =
// nx + this.width < 0 ||
// ny + this.height < 0 ||
// nx > this.mindMap.width ||
// ny > this.mindMap.height
// if (!oldIsInClient && newIsNotInClient) {
// if (!this.isHide) {
// this.isHide = true
// this.group.hide()
// }
// return
// }
// // 如果当前是隐藏状态,那么先显示
// if (this.isHide) {
// this.isHide = false
// this.group.show()
// }
// 如果节点位置没有变化,则返回
if (this.left === t.translateX && this.top === t.translateY) return
this.group.translate(this.left - t.translateX, this.top - t.translateY)
}
// 获取节点相当于画布的位置
getNodePosInClient(_left, _top) {
let drawTransform = this.mindMap.draw.transform()
let { scaleX, scaleY, translateX, translateY } = drawTransform
let left = _left * scaleX + translateX
let top = _top * scaleY + translateY
return {
left,
top
if (!isLayout && enableNodeTransitionMove) {
this.group
.animate(nodeTransitionMoveDuration)
.translate(this.left - t.translateX, this.top - t.translateY)
} else {
this.group.translate(this.left - t.translateX, this.top - t.translateY)
}
}
@@ -571,36 +484,40 @@ class Node {
return sizeChange
}
// 更新节点激活状态
updateNodeActive() {
if (!this.group) return
const isActive = this.nodeData.data.isActive
this.group[isActive ? 'addClass' : 'removeClass']('active')
// 更新节点形状样式
updateNodeShape() {
if (!this.shapeNode) return
const shape = this.getShape()
this.style[shape === CONSTANTS.SHAPE.RECTANGLE ? 'rect' : 'shape'](
this.shapeNode
)
}
// 递归渲染
render(callback = () => {}) {
let { enableNodeTransitionMove, nodeTransitionMoveDuration } =
this.mindMap.opt
// 节点
// 重新渲染连线
this.renderLine()
let isLayout = false
if (!this.group) {
isLayout = true
// 创建组
this.group = new G()
this.group.addClass('smm-node')
this.group.css({
cursor: 'default'
})
this.bindGroupEvent()
this.draw.add(this.group)
this.layout()
this.update()
this.update(isLayout)
} else {
this.draw.add(this.group)
if (this.needLayout) {
this.needLayout = false
this.layout()
}
this.updateExpandBtnPlaceholderRect()
this.update()
}
// 子节点
@@ -610,23 +527,33 @@ class Node {
this.nodeData.data.expand !== false
) {
let index = 0
this.children.forEach(item => {
item.render(() => {
index++
if (index >= this.children.length) {
callback()
asyncRun(
this.children.map(item => {
return () => {
item.render(() => {
index++
if (index >= this.children.length) {
callback()
}
})
}
})
})
)
} else {
callback()
if (enableNodeTransitionMove && !isLayout) {
setTimeout(() => {
callback()
}, nodeTransitionMoveDuration)
} else {
callback()
}
}
// 手动插入的节点立即获得焦点并且开启编辑模式
if (this.nodeData.inserting) {
delete this.nodeData.inserting
this.active()
setTimeout(() => {
this.mindMap.emit('node_dblclick', this, null, true)
this.mindMap.emit('node_dblclick', this)
}, 0)
}
}
@@ -639,9 +566,13 @@ class Node {
this.removeLine()
// 子节点
if (this.children && this.children.length) {
this.children.forEach(item => {
item.remove()
})
asyncRun(
this.children.map(item => {
return () => {
item.remove()
}
})
)
}
}
@@ -667,9 +598,13 @@ class Node {
}
// 子节点
if (this.children && this.children.length) {
this.children.forEach(item => {
item.hide()
})
asyncRun(
this.children.map(item => {
return () => {
item.hide()
}
})
)
}
}
@@ -689,9 +624,13 @@ class Node {
}
// 子节点
if (this.children && this.children.length) {
this.children.forEach(item => {
item.show()
})
asyncRun(
this.children.map(item => {
return () => {
item.show()
}
})
)
}
}
@@ -794,12 +733,12 @@ class Node {
// 检测当前节点是否是某个节点的祖先节点
isParent(node) {
if (this.uid === node.uid) {
if (this === node) {
return false
}
let parent = node.parent
while (parent) {
if (this.uid === parent.uid) {
if (this === parent) {
return true
}
parent = parent.parent
@@ -809,17 +748,17 @@ class Node {
// 检测当前节点是否是某个节点的兄弟节点
isBrother(node) {
if (!this.parent || this.uid === node.uid) {
if (!this.parent || this === node) {
return false
}
return this.parent.children.find(item => {
return item.uid === node.uid
return item === node
})
}
// 获取padding值
getPaddingVale() {
let { isActive } = this.nodeData.data
let { isActive }= this.nodeData.data
return {
paddingX: this.getStyle('paddingX', true, isActive),
paddingY: this.getStyle('paddingY', true, isActive)
@@ -827,8 +766,8 @@ class Node {
}
// 获取某个样式
getStyle(prop, root) {
let v = this.style.merge(prop, root)
getStyle(prop, root, isActive) {
let v = this.style.merge(prop, root, isActive)
return v === undefined ? '' : v
}
@@ -855,20 +794,10 @@ class Node {
) // 父级
}
// 获取节点非节点状态的边框大小
getBorderWidth() {
return this.style.merge('borderWidth', false) || 0
}
// 获取数据
getData(key) {
return key ? this.nodeData.data[key] || '' : this.nodeData.data
}
// 是否存在自定义样式
hasCustomStyle() {
return this.style.hasCustomStyle()
}
}
export default Node

View File

@@ -1,23 +1,15 @@
import merge from 'deepmerge'
import LogicalStructure from '../../layouts/LogicalStructure'
import MindMap from '../../layouts/MindMap'
import CatalogOrganization from '../../layouts/CatalogOrganization'
import OrganizationStructure from '../../layouts/OrganizationStructure'
import Timeline from '../../layouts/Timeline'
import VerticalTimeline from '../../layouts/VerticalTimeline'
import Fishbone from '../../layouts/Fishbone'
import LogicalStructure from './layouts/LogicalStructure'
import MindMap from './layouts/MindMap'
import CatalogOrganization from './layouts/CatalogOrganization'
import OrganizationStructure from './layouts/OrganizationStructure'
import Timeline from './layouts/Timeline'
import Fishbone from './layouts/Fishbone'
import TextEdit from './TextEdit'
import {
copyNodeTree,
simpleDeepClone,
walk,
bfsWalk,
loadImage,
isUndef
} from '../../utils'
import { shapeList } from './node/Shape'
import { lineStyleProps } from '../../themes/default'
import { CONSTANTS, ERROR_TYPES } from '../../constants/constant'
import { copyNodeTree, simpleDeepClone, walk } from './utils'
import { shapeList } from './Shape'
import { lineStyleProps } from './themes/default'
import { CONSTANTS } from './utils/constant'
// 布局列表
const layouts = {
@@ -33,10 +25,8 @@ const layouts = {
[CONSTANTS.LAYOUT.TIMELINE]: Timeline,
// 时间轴2
[CONSTANTS.LAYOUT.TIMELINE2]: Timeline,
// 竖向时间轴
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline,
// 鱼骨图
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone,
}
// 渲染
@@ -67,12 +57,6 @@ class Render {
this.root = null
// 文本编辑框需要再bindEvent之前实例化否则单击事件只能触发隐藏文本编辑框而无法保存文本修改
this.textEdit = new TextEdit(this)
// 当前复制的数据
this.lastBeingCopyData = null
this.beingCopyData = null
this.beingPasteText = ''
this.beingPasteImgSize = 0
this.currentBeingPasteType = ''
// 布局
this.setLayout()
// 绑定事件
@@ -95,27 +79,12 @@ class Render {
// 绑定事件
bindEvent() {
// 点击事件
this.mindMap.on('draw_click', e => {
this.mindMap.on('draw_click', () => {
// 清除激活状态
let isTrueClick = true
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
if (useLeftKeySelectionRightKeyDrag) {
let mousedownPos = this.mindMap.event.mousedownPos
isTrueClick =
Math.abs(e.clientX - mousedownPos.x) <= 5 &&
Math.abs(e.clientY - mousedownPos.y) <= 5
}
if (isTrueClick && this.activeNodeList.length > 0) {
if (this.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
})
// let timer = null
// this.mindMap.on('view_data_change', () => {
// clearTimeout(timer)
// timer = setTimeout(() => {
// this.render()
// }, 300)
// })
}
// 注册命令
@@ -157,12 +126,9 @@ class Render {
// 剪切节点
this.cutNode = this.cutNode.bind(this)
this.mindMap.command.add('CUT_NODE', this.cutNode)
// 修改节点单个样式
// 修改节点样式
this.setNodeStyle = this.setNodeStyle.bind(this)
this.mindMap.command.add('SET_NODE_STYLE', this.setNodeStyle)
// 修改节点多个样式
this.setNodeStyles = this.setNodeStyles.bind(this)
this.mindMap.command.add('SET_NODE_STYLES', this.setNodeStyles)
// 切换节点是否激活
this.setNodeActive = this.setNodeActive.bind(this)
this.mindMap.command.add('SET_NODE_ACTIVE', this.setNodeActive)
@@ -220,9 +186,6 @@ class Render {
// 设置节点形状
this.setNodeShape = this.setNodeShape.bind(this)
this.mindMap.command.add('SET_NODE_SHAPE', this.setNodeShape)
// 定位节点
this.goTargetNode = this.goTargetNode.bind(this)
this.mindMap.command.add('GO_TARGET_NODE', this.goTargetNode)
}
// 注册快捷键
@@ -240,7 +203,7 @@ class Render {
}
this.mindMap.keyCommand.addShortcut('Enter', this.insertNodeWrap)
// 插入概要
this.mindMap.keyCommand.addShortcut('Control+g', this.addGeneralization)
this.mindMap.keyCommand.addShortcut('Control+s', this.addGeneralization)
// 展开/收起节点
this.toggleActiveExpand = this.toggleActiveExpand.bind(this)
this.mindMap.keyCommand.addShortcut('/', this.toggleActiveExpand)
@@ -267,13 +230,6 @@ class Render {
// 下移节点
this.mindMap.keyCommand.addShortcut('Control+Down', this.downNode)
// 复制节点、剪切节点、粘贴节点的快捷键需开发者自行注册实现可参考demo
this.copy = this.copy.bind(this)
this.mindMap.keyCommand.addShortcut('Control+c', this.copy)
this.mindMap.keyCommand.addShortcut('Control+v', () => {
this.onPaste()
})
this.cut = this.cut.bind(this)
this.mindMap.keyCommand.addShortcut('Control+x', this.cut)
}
// 开启文字编辑,会禁用回车键和删除键相关快捷键防止冲突
@@ -294,6 +250,7 @@ class Render {
// 渲染
render(callback = () => {}, source) {
let t = Date.now()
// 如果当前还没有渲染完毕,不再触发渲染
if (this.isRendering) {
// 等待当前渲染完毕后再进行一次渲染
@@ -313,7 +270,7 @@ class Render {
// 计算布局
this.layout.doLayout(root => {
// 删除本次渲染时不再需要的节点
Object.keys(this.lastNodeCache).forEach(uid => {
Object.keys(this.lastNodeCache).forEach((uid) => {
if (!this.nodeCache[uid]) {
this.lastNodeCache[uid].destroy()
if (this.lastNodeCache[uid].parent) {
@@ -324,7 +281,7 @@ class Render {
// 更新根节点
this.root = root
// 渲染节点
this.root.render(() => {
const onEnd = () => {
this.isRendering = false
this.mindMap.emit('node_tree_render_end')
callback && callback()
@@ -333,16 +290,25 @@ class Render {
this.render(callback, source)
} else {
// 触发一次保存,因为修改了渲染树的数据
if (
this.mindMap.richText &&
[CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)
) {
if (this.mindMap.richText && [CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)) {
this.mindMap.command.addHistory()
}
}
}
let { enableNodeTransitionMove, nodeTransitionMoveDuration } =
this.mindMap.opt
this.root.render(() => {
let dur = Date.now() - t
if (enableNodeTransitionMove && dur <= nodeTransitionMoveDuration) {
setTimeout(() => {
onEnd()
}, nodeTransitionMoveDuration - dur);
} else {
onEnd()
}
})
})
this.mindMap.emit('node_active', null, [...this.activeNodeList])
this.mindMap.emit('node_active', null, this.activeNodeList)
}
// 清除当前激活的节点
@@ -382,7 +348,7 @@ class Render {
// 检索某个节点在激活列表里的索引
findActiveNodeIndex(node) {
return this.activeNodeList.findIndex(item => {
return item.uid === node.uid
return item === node
})
}
@@ -390,7 +356,7 @@ class Render {
getNodeIndex(node) {
return node.parent
? node.parent.children.findIndex(item => {
return item.uid === node.uid
return item === node
})
: 0
}
@@ -407,7 +373,7 @@ class Render {
// 激活节点需要显示展开收起按钮
node.showExpandBtn()
setTimeout(() => {
node.updateNodeActive()
node.updateNodeShape()
}, 0)
}
},
@@ -416,7 +382,6 @@ class Render {
0,
0
)
this.mindMap.emit('node_active', null, [...this.activeNodeList])
}
// 回退
@@ -442,95 +407,60 @@ class Render {
// 规范指定节点数据
formatAppointNodes(appointNodes) {
if (!appointNodes) return []
return Array.isArray(appointNodes) ? appointNodes : [appointNodes]
return Array.isArray(appointNodes) ? appointNodes: [appointNodes]
}
// 插入同级节点,多个节点只会操作第一个节点
insertNode(
openEdit = true,
appointNodes = [],
appointData = null,
appointChildren = []
) {
insertNode(openEdit = true, appointNodes = [], appointData = null) {
appointNodes = this.formatAppointNodes(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.textEdit.hideEditTextBox()
let {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt
let { defaultInsertSecondLevelNodeText, defaultInsertBelowSecondLevelNodeText } = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
let first = list[0]
if (first.isGeneralization) {
return
}
if (first.isRoot) {
this.insertChildNode(openEdit, appointNodes, appointData)
} else {
let text =
first.layerIndex === 1
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
let text = first.layerIndex === 1 ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
if (first.layerIndex === 1) {
first.parent.destroy()
}
let index = this.getNodeIndex(first)
let isRichText = !!this.mindMap.richText
first.parent.nodeData.children.splice(index + 1, 0, {
inserting: openEdit,
data: {
text: text,
expand: true,
richText: isRichText,
resetRichText: isRichText,
...(appointData || {})
},
children: [...appointChildren]
children: []
})
this.mindMap.render()
}
}
// 插入子节点
insertChildNode(
openEdit = true,
appointNodes = [],
appointData = null,
appointChildren = []
) {
insertChildNode(openEdit = true, appointNodes = [], appointData = null) {
appointNodes = this.formatAppointNodes(appointNodes)
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.textEdit.hideEditTextBox()
let {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
} = this.mindMap.opt
let { defaultInsertSecondLevelNodeText, defaultInsertBelowSecondLevelNodeText } = this.mindMap.opt
let list = appointNodes.length > 0 ? appointNodes : this.activeNodeList
list.forEach(node => {
if (node.isGeneralization) {
return
}
if (!node.nodeData.children) {
node.nodeData.children = []
}
let text = node.isRoot
? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText
let isRichText = !!this.mindMap.richText
let text = node.isRoot ? defaultInsertSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
node.nodeData.children.push({
inserting: openEdit,
data: {
text: text,
expand: true,
richText: isRichText,
resetRichText: isRichText,
...(appointData || {})
},
children: [...appointChildren]
children: []
})
// 插入子节点时自动展开子节点
node.nodeData.data.expand = true
@@ -553,7 +483,7 @@ class Render {
let parent = node.parent
let childList = parent.children
let index = childList.findIndex(item => {
return item.uid === node.uid
return item === node
})
if (index === -1 || index === 0) {
return
@@ -580,7 +510,7 @@ class Render {
let parent = node.parent
let childList = parent.children
let index = childList.findIndex(item => {
return item.uid === node.uid
return item === node
})
if (index === -1 || index === childList.length - 1) {
return
@@ -595,164 +525,18 @@ class Render {
this.mindMap.render()
}
// 复制节点
copy() {
this.beingCopyData = this.copyNode()
this.setCoptyDataToClipboard(this.beingCopyData)
}
// 剪切节点
cut() {
this.mindMap.execCommand('CUT_NODE', copyData => {
this.beingCopyData = copyData
this.setCoptyDataToClipboard(copyData)
})
}
// 将粘贴或剪切的数据设置到用户剪切板中
setCoptyDataToClipboard(data) {
if (navigator.clipboard) {
navigator.clipboard.writeText(
JSON.stringify({
simpleMindMap: true,
data
})
)
}
}
// 粘贴节点
paste() {
if (this.beingCopyData) {
this.mindMap.execCommand('PASTE_NODE', this.beingCopyData)
}
}
// 粘贴事件
async onPaste() {
const { errorHandler } = this.mindMap.opt
// 读取剪贴板的文字和图片
let text = null
let img = null
if (navigator.clipboard) {
try {
text = await navigator.clipboard.readText()
const items = await navigator.clipboard.read()
if (items && items.length > 0) {
for (const clipboardItem of items) {
for (const type of clipboardItem.types) {
if (/^image\//.test(type)) {
img = await clipboardItem.getType(type)
break
}
}
}
}
} catch (error) {
errorHandler(ERROR_TYPES.READ_CLIPBOARD_ERROR, error)
}
}
// 检查剪切板数据是否有变化
// 通过图片大小来判断图片是否发生变化,可能是不准确的,但是目前没有其他好方法
const imgSize = img ? img.size : 0
if (this.beingPasteText !== text || this.beingPasteImgSize !== imgSize) {
this.currentBeingPasteType = CONSTANTS.PASTE_TYPE.CLIP_BOARD
this.beingPasteText = text
this.beingPasteImgSize = imgSize
}
// 检查要粘贴的节点数据是否有变化,节点优先级高于剪切板
if (this.lastBeingCopyData !== this.beingCopyData) {
this.lastBeingCopyData = this.beingCopyData
this.currentBeingPasteType = CONSTANTS.PASTE_TYPE.CANVAS
}
// 粘贴剪切板的数据
if (this.currentBeingPasteType === CONSTANTS.PASTE_TYPE.CLIP_BOARD) {
// 存在文本,则创建子节点
if (text) {
// 判断粘贴的是否是simple-mind-map的数据
let smmData = null
let useDefault = true
// 用户自定义处理
if (this.mindMap.opt.customHandleClipboardText) {
try {
const res = await this.mindMap.opt.customHandleClipboardText(text)
if (!isUndef(res)) {
useDefault = false
if (typeof res === 'object' && res.simpleMindMap) {
smmData = res.data
} else {
text = String(res)
}
}
} catch (error) {
errorHandler(ERROR_TYPES.CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR, error)
}
}
// 默认处理
if (useDefault) {
try {
const parsedData = JSON.parse(text)
if (parsedData && parsedData.simpleMindMap) {
smmData = parsedData.data
}
} catch (error) {
errorHandler(ERROR_TYPES.PARSE_PASTE_DATA_ERROR, error)
}
}
if (smmData) {
this.mindMap.execCommand(
'INSERT_CHILD_NODE',
false,
[],
{
...smmData.data
},
[...smmData.children]
)
} else {
this.mindMap.execCommand('INSERT_CHILD_NODE', false, [], {
text
})
}
}
// 存在图片,则添加到当前激活节点
if (img) {
try {
let imgData = await loadImage(img)
if (this.activeNodeList.length > 0) {
this.activeNodeList.forEach(node => {
this.mindMap.execCommand('SET_NODE_IMAGE', node, {
url: imgData.url,
title: '',
width: imgData.size.width,
height: imgData.size.height
})
})
}
} catch (error) {
errorHandler(ERROR_TYPES.LOAD_CLIPBOARD_IMAGE_ERROR, error)
}
}
} else {
// 粘贴节点数据
this.paste()
}
}
// 将节点移动到另一个节点的前面
insertBefore(node, exist) {
if (node.isRoot) {
return
}
// 如果是二级节点变成了下级节点,或是下级节点变成了二级节点,节点样式需要更新
let nodeLayerChanged =
(node.layerIndex === 1 && exist.layerIndex !== 1) ||
(node.layerIndex !== 1 && exist.layerIndex === 1)
let nodeLayerChanged = (node.layerIndex === 1 && exist.layerIndex !== 1) || (node.layerIndex !== 1 && exist.layerIndex === 1)
// 移动节点
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
let nodeIndex = nodeBorthers.findIndex(item => {
return item.uid === node.uid
return item === node
})
if (nodeIndex === -1) {
return
@@ -764,7 +548,7 @@ class Render {
let existParent = exist.parent
let existBorthers = existParent.children
let existIndex = existBorthers.findIndex(item => {
return item.uid === exist.uid
return item === exist
})
if (existIndex === -1) {
return
@@ -784,14 +568,12 @@ class Render {
return
}
// 如果是二级节点变成了下级节点,或是下级节点变成了二级节点,节点样式需要更新
let nodeLayerChanged =
(node.layerIndex === 1 && exist.layerIndex !== 1) ||
(node.layerIndex !== 1 && exist.layerIndex === 1)
let nodeLayerChanged = (node.layerIndex === 1 && exist.layerIndex !== 1) || (node.layerIndex !== 1 && exist.layerIndex === 1)
// 移动节点
let nodeParent = node.parent
let nodeBorthers = nodeParent.children
let nodeIndex = nodeBorthers.findIndex(item => {
return item.uid === node.uid
return item === node
})
if (nodeIndex === -1) {
return
@@ -803,7 +585,7 @@ class Render {
let existParent = exist.parent
let existBorthers = existParent.children
let existIndex = existBorthers.findIndex(item => {
return item.uid === exist.uid
return item === exist
})
if (existIndex === -1) {
return
@@ -824,11 +606,9 @@ class Render {
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
// 删除节点后需要激活的节点
let needActiveNode = null
let isAppointNodes = appointNodes.length > 0
let list = isAppointNodes ? appointNodes : this.activeNodeList
let root = list.find(node => {
let root = list.find((node) => {
return node.isRoot
})
if (root) {
@@ -839,28 +619,6 @@ class Render {
root.children = []
root.nodeData.children = []
} else {
// 如果只选中了一个节点,删除后激活其兄弟节点或者父节点
if (
this.activeNodeList.length === 1 &&
!this.activeNodeList[0].isGeneralization &&
this.mindMap.opt.deleteNodeActive
) {
const node = this.activeNodeList[0]
const broList = node.parent.children
const nodeIndex = broList.findIndex(item => item.uid === node.uid)
// 如果后面有兄弟节点
if (nodeIndex < broList.length - 1) {
needActiveNode = broList[nodeIndex + 1]
} else {
// 如果前面有兄弟节点
if (nodeIndex > 0) {
needActiveNode = broList[nodeIndex - 1]
} else {
// 没有兄弟节点
needActiveNode = node.parent
}
}
}
for (let i = 0; i < list.length; i++) {
let node = list[i]
if (isAppointNodes) list.splice(i, 1)
@@ -879,14 +637,7 @@ class Render {
}
}
}
this.activeNodeList = []
// 激活被删除节点的兄弟节点或父节点
if (needActiveNode) {
this.activeNodeList.push(needActiveNode)
this.setNodeActive(needActiveNode, true)
needActiveNode = null
}
this.mindMap.emit('node_active', null, [...this.activeNodeList])
this.mindMap.emit('node_active', null, this.activeNodeList)
this.mindMap.render()
}
@@ -918,7 +669,7 @@ class Render {
let copyData = copyNodeTree({}, node, true)
this.removeActiveNode(node)
this.removeOneNode(node)
this.mindMap.emit('node_active', null, [...this.activeNodeList])
this.mindMap.emit('node_active', null, this.activeNodeList)
this.mindMap.render()
if (callback && typeof callback === 'function') {
callback(copyData)
@@ -930,11 +681,11 @@ class Render {
if (node.isRoot) {
return
}
// let copyData = copyNodeTree({}, node, false, true)
let copyData = copyNodeTree({}, node, false, true)
this.removeActiveNode(node)
this.removeOneNode(node)
this.mindMap.emit('node_active', null, [...this.activeNodeList])
toNode.nodeData.children.push(node.nodeData)
this.mindMap.emit('node_active', null, this.activeNodeList)
toNode.nodeData.children.push(copyData)
this.mindMap.render()
if (toNode.isRoot) {
toNode.destroy()
@@ -943,7 +694,7 @@ class Render {
// 粘贴节点到节点
pasteNode(data) {
if (this.activeNodeList.length <= 0 || !data) {
if (this.activeNodeList.length <= 0) {
return
}
this.activeNodeList.forEach(item => {
@@ -953,9 +704,19 @@ class Render {
}
// 设置节点样式
setNodeStyle(node, prop, value) {
let data = {
[prop]: value
setNodeStyle(node, prop, value, isActive) {
let data = {}
if (isActive) {
data = {
activeStyle: {
...(node.nodeData.data.activeStyle || {}),
[prop]: value
}
}
} else {
data = {
[prop]: value
}
}
// 如果开启了富文本,则需要应用到富文本上
if (this.mindMap.richText) {
@@ -975,32 +736,6 @@ class Render {
}
}
// 设置节点多个样式
setNodeStyles(node, style) {
let data = { ...style }
// 如果开启了富文本,则需要应用到富文本上
if (this.mindMap.richText) {
let config = this.mindMap.richText.normalStyleToRichTextStyle(style)
if (Object.keys(config).length > 0) {
this.mindMap.richText.showEditText(node)
this.mindMap.richText.formatAllText(config)
this.mindMap.richText.hideEditText([node])
}
}
this.setNodeDataRender(node, data)
// 更新了连线的样式
let props = Object.keys(style)
let hasLineStyleProps = false
props.forEach(key => {
if (lineStyleProps.includes(key)) {
hasLineStyleProps = true
}
})
if (hasLineStyleProps) {
;(node.parent || node).renderLine(true)
}
}
// 设置节点是否激活
setNodeActive(node, active) {
this.setNodeData(node, {
@@ -1012,7 +747,7 @@ class Render {
} else {
node.hideExpandBtn()
}
node.updateNodeActive()
node.updateNodeShape()
}
// 设置节点是否展开
@@ -1114,30 +849,21 @@ class Render {
}
// 设置节点文本
setNodeText(node, text, richText, resetRichText) {
setNodeText(node, text, richText) {
this.setNodeDataRender(node, {
text,
richText,
resetRichText
richText
})
}
// 设置节点图片
setNodeImage(node, data) {
const {
url,
title,
width,
height,
custom = false
} = data || { url: '', title: '', width: 0, height: 0, custom: false }
setNodeImage(node, { url, title, width, height }) {
this.setNodeDataRender(node, {
image: url,
imageTitle: title || '',
imageSize: {
width,
height,
custom
height
}
})
}
@@ -1250,20 +976,6 @@ class Render {
})
}
// 定位到指定节点
goTargetNode(node, callback = () => {}) {
let uid = typeof node === 'string' ? node : node.nodeData.data.uid
if (!uid) return
this.expandToNodeUid(uid, () => {
let targetNode = this.findNodeByUid(uid)
if (targetNode) {
targetNode.active()
this.moveNodeToCenter(targetNode)
callback()
}
})
}
// 更新节点数据
setNodeData(node, data) {
Object.keys(data).forEach(key => {
@@ -1272,7 +984,7 @@ class Render {
}
// 设置节点数据,并判断是否渲染
setNodeDataRender(node, data, notRender = false) {
setNodeDataRender(node, data) {
this.setNodeData(node, data)
let changed = node.reRender()
if (changed) {
@@ -1280,9 +992,7 @@ class Render {
// 概要节点
node.generalizationBelongNode.updateGeneralization()
}
if (!notRender) this.mindMap.render()
} else {
this.mindMap.emit('node_tree_render_end')
this.mindMap.render()
}
}
@@ -1302,44 +1012,6 @@ class Render {
this.mindMap.view.translateY(offsetY)
this.mindMap.view.setScale(1)
}
// 展开到指定uid的节点
expandToNodeUid(uid, callback = () => {}) {
let parentsList = []
const cache = {}
bfsWalk(this.renderTree, (node, parent) => {
if (node.data.uid === uid) {
parentsList = parent ? [...cache[parent.data.uid], parent] : []
return 'stop'
} else {
cache[node.data.uid] = parent ? [...cache[parent.data.uid], parent] : []
}
})
let needRender = false
parentsList.forEach(node => {
if (!node.data.expand) {
needRender = true
node.data.expand = true
}
})
if (needRender) {
this.mindMap.render(callback)
} else {
callback()
}
}
// 根据uid找到对应的节点实例
findNodeByUid(uid) {
let res = null
walk(this.root, null, node => {
if (node.nodeData.data.uid === uid) {
res = node
return true
}
})
return res
}
}
export default Render

View File

@@ -1,13 +1,8 @@
import Quill from 'quill'
import Delta from 'quill-delta'
import 'quill/dist/quill.snow.css'
import {
walk,
getTextFromHtml,
isWhite,
getVisibleColorFromTheme
} from '../utils'
import { CONSTANTS } from '../constants/constant'
import html2canvas from 'html2canvas'
import { walk, getTextFromHtml } from './utils'
import { CONSTANTS } from './utils/constant'
let extended = false
@@ -33,7 +28,7 @@ let fontSizeList = new Array(100).fill(0).map((_, index) => {
return index + 'px'
})
// 富文本编辑插件
// 节点支持富文本编辑功能
class RichText {
constructor({ mindMap, pluginOpt }) {
this.mindMap = mindMap
@@ -43,17 +38,12 @@ class RichText {
this.quill = null
this.range = null
this.lastRange = null
this.pasteUseRange = null
this.node = null
this.isInserting = false
this.styleEl = null
this.cacheEditingText = ''
this.lostStyle = false
this.isCompositing = false
this.initOpt()
this.extendQuill()
this.appendCss()
this.bindEvent()
// 处理数据,转成富文本格式
if (this.mindMap.opt.data) {
@@ -61,20 +51,6 @@ class RichText {
}
}
// 绑定事件
bindEvent() {
this.onCompositionStart = this.onCompositionStart.bind(this)
this.onCompositionEnd = this.onCompositionEnd.bind(this)
window.addEventListener('compositionstart', this.onCompositionStart)
window.addEventListener('compositionend', this.onCompositionEnd)
}
// 解绑事件
unbindEvent() {
window.removeEventListener('compositionstart', this.onCompositionStart)
window.removeEventListener('compositionend', this.onCompositionEnd)
}
// 插入样式
appendCss() {
let cssText = `
@@ -83,7 +59,6 @@ class RichText {
padding: 0;
height: auto;
line-height: normal;
-webkit-user-select: text;
}
.ql-container {
@@ -152,19 +127,11 @@ class RichText {
}
// 显示文本编辑控件
showEditText(node, rect, isInserting, isFromKeyDown) {
showEditText(node, rect) {
if (this.showTextEdit) {
return
}
const {
richTextEditFakeInPlace,
customInnerElsAppendTo,
nodeTextEditZIndex,
textAutoWrapWidth,
selectTextOnEnterEditText
} = this.mindMap.opt
this.node = node
this.isInserting = isInserting
if (!rect) rect = node._textData.node.node.getBoundingClientRect()
this.mindMap.emit('before_show_text_edit')
this.mindMap.renderer.textEdit.registerTmpShortcut()
@@ -176,80 +143,43 @@ class RichText {
let scaleX = rect.width / originWidth
let scaleY = rect.height / originHeight
// 内边距
let paddingX = 6
let paddingY = 4
if (richTextEditFakeInPlace) {
let paddingValue = node.getPaddingVale()
paddingX = paddingValue.paddingX
paddingY = paddingValue.paddingY
}
const paddingX = 6
const paddingY = 4
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.classList.add('smm-richtext-node-edit-wrap')
this.textEditNode.style.cssText = `
position:fixed;
box-sizing: border-box;
box-shadow: 0 0 20px rgba(0,0,0,.5);
outline: none;
word-break:
break-all;padding: ${paddingY}px ${paddingX}px;
`
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;box-shadow: 0 0 20px rgba(0,0,0,.5);outline: none; word-break: break-all;padding: ${paddingY}px ${paddingX}px;`
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('keydown', e => {
if (this.mindMap.renderer.textEdit.checkIsAutoEnterTextEditKey(e)) {
e.stopPropagation()
}
})
const targetNode = customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
document.body.appendChild(this.textEditNode)
}
// 使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
let bgColor = node.style.merge('fillColor')
let color = node.style.merge('color')
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
this.textEditNode.style.zIndex = nodeTextEditZIndex
this.textEditNode.style.backgroundColor =
bgColor === 'transparent'
? isWhite(color)
? getVisibleColorFromTheme(this.mindMap.themeConfig)
: '#fff'
: bgColor
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.style.backgroundColor = bgColor === 'transparent' ? '#fff' : bgColor
this.textEditNode.style.minWidth = originWidth + paddingX * 2 + 'px'
this.textEditNode.style.minHeight = originHeight + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
this.textEditNode.style.display = 'block'
this.textEditNode.style.maxWidth = textAutoWrapWidth + paddingX * 2 + 'px'
this.textEditNode.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + paddingX * 2 + 'px'
this.textEditNode.style.transform = `scale(${scaleX}, ${scaleY})`
this.textEditNode.style.transformOrigin = 'left top'
if (richTextEditFakeInPlace) {
this.textEditNode.style.borderRadius =
(node.style.merge('borderRadius') || 5) + 'px'
if (node.style.merge('shape') == 'roundedRectangle') {
this.textEditNode.style.borderRadius = (node.height || 50) + 'px'
}
}
if (!node.nodeData.data.richText) {
// 还不是富文本的情况
let text = node.nodeData.data.text.split(/\n/gim).join('<br>')
let html = `<p>${text}</p>`
this.textEditNode.innerHTML = this.cacheEditingText || html
} else {
this.textEditNode.innerHTML =
this.cacheEditingText || node.nodeData.data.text
this.textEditNode.innerHTML = this.cacheEditingText || node.nodeData.data.text
}
this.initQuillEditor()
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
this.showTextEdit = true
// 如果是刚创建的节点那么默认全选否则普通激活不全选除非selectTextOnEnterEditText配置为true
// 在selectTextOnEnterEditText时如果是在keydown事件进入的节点编辑也不需要全选
this.focus(isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null)
this.focus()
if (!node.nodeData.data.richText) {
// 如果是非富文本的情况,需要手动应用文本样式
this.setTextStyleIfNotRichText(node)
@@ -268,7 +198,7 @@ class RichText {
underline: node.style.merge('textDecoration') === 'underline',
strike: node.style.merge('textDecoration') === 'line-through'
}
this.pureFormatAllText(style)
this.formatAllText(style)
}
// 获取当前正在编辑的内容
@@ -284,8 +214,7 @@ class RichText {
return
}
let html = this.getEditText()
let list =
nodes && nodes.length > 0 ? nodes : this.mindMap.renderer.activeNodeList
let list = nodes && nodes.length > 0 ? nodes : this.mindMap.renderer.activeNodeList
list.forEach(node => {
this.mindMap.execCommand('SET_NODE_TEXT', node, html, true)
if (node.isGeneralization) {
@@ -294,12 +223,15 @@ class RichText {
}
this.mindMap.render()
})
this.mindMap.emit('hide_text_edit', this.textEditNode, list)
this.mindMap.emit(
'hide_text_edit',
this.textEditNode,
list
)
this.textEditNode.style.display = 'none'
this.showTextEdit = false
this.mindMap.emit('rich_text_selection_change', false)
this.node = null
this.isInserting = false
}
// 初始化Quill富文本编辑器
@@ -314,12 +246,6 @@ class RichText {
handler: function () {
// 覆盖默认的回车键换行
}
},
tab: {
key: 9,
handler: function () {
// 覆盖默认的tab键
}
}
}
}
@@ -327,12 +253,9 @@ class RichText {
theme: 'snow'
})
this.quill.on('selection-change', range => {
// 刚创建的节点全选不需要显示操作条
if (this.isInserting) return
this.lastRange = this.range
this.range = null
if (range) {
this.pasteUseRange = range
let bounds = this.quill.getBounds(range.index, range.length)
let rect = this.textEditNode.getBoundingClientRect()
let rectInfo = {
@@ -358,69 +281,6 @@ class RichText {
)
}
})
this.quill.on('text-change', () => {
let contents = this.quill.getContents()
let len = contents.ops.length
// 如果编辑过程中删除所有字符,那么会丢失主题的样式
if (len <= 0 || (len === 1 && contents.ops[0].insert === '\n')) {
this.lostStyle = true
// 需要删除节点的样式数据
this.syncFormatToNodeConfig(null, true)
} else if (this.lostStyle && !this.isCompositing) {
// 如果处于样式丢失状态,那么需要进行格式化加回样式
this.setTextStyleIfNotRichText(this.node)
this.lostStyle = false
}
})
// 拦截粘贴,只允许粘贴纯文本
this.quill.clipboard.addMatcher(Node.TEXT_NODE, (node) => {
let style = this.getPasteTextStyle()
return new Delta().insert(node.data, style)
})
this.quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
let ops = []
let style = this.getPasteTextStyle()
delta.ops.forEach(op => {
// 过滤出文本内容,过滤掉换行
if (op.insert && typeof op.insert === 'string' && op.insert !== '\n') {
ops.push({
attributes: { ...style },
insert: op.insert
})
}
})
delta.ops = ops
return delta
})
}
// 获取粘贴的文本的样式
getPasteTextStyle() {
// 粘贴的数据使用当前光标位置处的文本样式
if (this.pasteUseRange) {
return this.quill.getFormat(
this.pasteUseRange.index,
this.pasteUseRange.length
)
}
return {}
}
// 正则输入中文
onCompositionStart() {
if (!this.showTextEdit) {
return
}
this.isCompositing = true
}
// 中文输入结束
onCompositionEnd() {
if (!this.showTextEdit || !this.lostStyle) {
return
}
this.isCompositing = false
this.setTextStyleIfNotRichText(this.node)
}
// 选中全部
@@ -429,9 +289,9 @@ class RichText {
}
// 聚焦
focus(start) {
focus() {
let len = this.quill.getLength()
this.quill.setSelection(typeof start === 'number' ? start : len, len)
this.quill.setSelection(len, len)
}
// 格式化当前选中的文本
@@ -440,9 +300,7 @@ class RichText {
this.syncFormatToNodeConfig(config, clear)
let rangeLost = !this.range
let range = rangeLost ? this.lastRange : this.range
clear
? this.quill.removeFormat(range.index, range.length)
: this.quill.formatText(range.index, range.length, config)
clear ? this.quill.removeFormat(range.index, range.length) : this.quill.formatText(range.index, range.length, config)
if (rangeLost) {
this.quill.setSelection(this.lastRange.index, this.lastRange.length)
}
@@ -463,11 +321,6 @@ class RichText {
// 格式化所有文本
formatAllText(config = {}) {
this.syncFormatToNodeConfig(config)
this.pureFormatAllText(config)
}
// 纯粹的格式化所有文本
pureFormatAllText(config = {}) {
this.quill.formatText(0, this.quill.getLength(), config)
}
@@ -476,14 +329,7 @@ class RichText {
if (!this.node) return
if (clear) {
// 清除文本样式
;[
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
].forEach(prop => {
['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'textDecoration', 'color'].forEach((prop) => {
delete this.node.nodeData.data[prop]
})
} else {
@@ -566,26 +412,21 @@ class RichText {
el.appendChild(node)
this.mindMap.el.appendChild(el)
// 遍历所有节点将它们的margin和padding设为0
let walk = root => {
let walk = (root) => {
root.style.margin = 0
root.style.padding = 0
if (root.hasChildNodes()) {
Array.from(root.children).forEach(item => {
Array.from(root.children).forEach((item) => {
walk(item)
})
}
}
walk(node)
// 如果使用html2canvas
// let canvas = await html2canvas(el, {
// backgroundColor: null
// })
// return canvas.toDataURL()
const res = await domtoimage.toPng(el)
let canvas = await html2canvas(el, {
backgroundColor: null
})
this.mindMap.el.removeChild(el)
return res
return canvas.toDataURL()
}
// 将所有节点转换成非富文本节点
@@ -613,13 +454,13 @@ class RichText {
// 处理导入数据
handleSetData(data) {
let walk = root => {
let walk = (root) => {
if (!root.data.richText) {
root.data.richText = true
root.data.resetRichText = true
}
if (root.children && root.children.length > 0) {
Array.from(root.children).forEach(item => {
Array.from(root.children).forEach((item) => {
walk(item)
})
}
@@ -633,11 +474,6 @@ class RichText {
this.transformAllNodesToNormalNode()
document.head.removeChild(this.styleEl)
}
// 插件被卸载前做的事情
beforePluginDestroy() {
document.head.removeChild(this.styleEl)
}
}
RichText.instanceName = 'richText'

View File

@@ -1,6 +1,7 @@
import { bfsWalk, throttle } from '../utils'
import { bfsWalk, throttle } from './utils'
// 选择节点类
// 节点选择插件
class Select {
// 构造函数
constructor({ mindMap }) {
@@ -11,8 +12,6 @@ class Select {
this.mouseDownY = 0
this.mouseMoveX = 0
this.mouseMoveY = 0
this.isSelecting = false
this.cacheActiveList = []
this.bindEvent()
}
@@ -23,16 +22,10 @@ class Select {
if (this.mindMap.opt.readonly) {
return
}
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
if (
!e.ctrlKey &&
(useLeftKeySelectionRightKeyDrag ? e.which !== 1 : e.which !== 3)
) {
if (!e.ctrlKey && e.which !== 3) {
return
}
e.preventDefault()
this.isMousedown = true
this.cacheActiveList = [...this.mindMap.renderer.activeNodeList]
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseDownX = x
this.mouseDownY = y
@@ -54,40 +47,8 @@ class Select {
) {
return
}
this.clearAutoMoveTimer()
this.onMove(
e.clientX,
e.clientY,
() => {
this.isSelecting = true
// 绘制矩形
this.rect.plot([
[this.mouseDownX, this.mouseDownY],
[this.mouseMoveX, this.mouseDownY],
[this.mouseMoveX, this.mouseMoveY],
[this.mouseDownX, this.mouseMoveY]
])
this.checkInNodes()
},
(dir, step) => {
switch (dir) {
case 'left':
this.mouseDownX += step
break
case 'top':
this.mouseDownY += step
break
case 'right':
this.mouseDownX -= step
break
case 'bottom':
this.mouseDownY -= step
break
default:
break
}
}
)
clearTimeout(this.autoMoveTimer)
this.onMove(x, y)
})
this.mindMap.on('mouseup', () => {
if (this.mindMap.opt.readonly) {
@@ -96,92 +57,68 @@ class Select {
if (!this.isMousedown) {
return
}
this.checkTriggerNodeActiveEvent()
this.mindMap.emit(
'node_active',
null,
this.mindMap.renderer.activeNodeList
)
clearTimeout(this.autoMoveTimer)
this.isMousedown = false
this.cacheActiveList = []
if (this.rect) this.rect.remove()
this.rect = null
setTimeout(() => {
this.isSelecting = false
}, 0)
})
}
// 如果激活节点改变了,那么触发事件
checkTriggerNodeActiveEvent() {
let isNumChange =
this.cacheActiveList.length !==
this.mindMap.renderer.activeNodeList.length
let isNodeChange = false
if (!isNumChange) {
for (let i = 0; i < this.cacheActiveList.length; i++) {
let cur = this.cacheActiveList[i]
if (
!this.mindMap.renderer.activeNodeList.find(item => {
return item.nodeData.data.uid === cur.nodeData.data.uid
})
) {
isNodeChange = true
break
}
}
}
if (isNumChange || isNodeChange) {
this.mindMap.emit('node_active', null, [
...this.mindMap.renderer.activeNodeList
])
}
}
// 鼠标移动事件
onMove(x, y, callback = () => {}, handle = () => {}) {
callback()
onMove(x, y) {
// 绘制矩形
this.rect.plot([
[this.mouseDownX, this.mouseDownY],
[this.mouseMoveX, this.mouseDownY],
[this.mouseMoveX, this.mouseMoveY],
[this.mouseDownX, this.mouseMoveY]
])
this.checkInNodes()
// 检测边缘移动
let step = this.mindMap.opt.selectTranslateStep
let limit = this.mindMap.opt.selectTranslateLimit
let count = 0
// 左边缘
if (x <= this.mindMap.elRect.left + limit) {
handle('left', step)
this.mouseDownX += step
this.mindMap.view.translateX(step)
count++
}
// 右边缘
if (x >= this.mindMap.elRect.right - limit) {
handle('right', step)
this.mouseDownX -= step
this.mindMap.view.translateX(-step)
count++
}
// 上边缘
if (y <= this.mindMap.elRect.top + limit) {
handle('top', step)
this.mouseDownY += step
this.mindMap.view.translateY(step)
count++
}
// 下边缘
if (y >= this.mindMap.elRect.bottom - limit) {
handle('bottom', step)
this.mouseDownY -= step
this.mindMap.view.translateY(-step)
count++
}
if (count > 0) {
this.startAutoMove(x, y, callback, handle)
this.startAutoMove(x, y)
}
}
// 开启自动移动
startAutoMove(x, y, callback, handle) {
startAutoMove(x, y) {
this.autoMoveTimer = setTimeout(() => {
this.onMove(x, y, callback, handle)
this.onMove(x, y)
}, 20)
}
// 清除自动移动定时器
clearAutoMoveTimer() {
clearTimeout(this.autoMoveTimer)
}
// 创建矩形
createRect(x, y) {
this.rect = this.mindMap.svg
@@ -209,33 +146,29 @@ class Select {
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
if (
((left >= minx && left <= maxx) || (right >= minx && right <= maxx)) &&
((top >= miny && top <= maxy) || (bottom >= miny && bottom <= maxy))
) {
if ((left >= minx && left <= maxx ||
right >= minx && right <= maxx) &&
(top >= miny && top <= maxy ||
bottom >= miny && bottom <= maxy)
) {
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, true)
this.mindMap.renderer.addActiveNode(node)
if (node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, true)
this.mindMap.renderer.addActiveNode(node)
// })
} else if (node.nodeData.data.isActive) {
// this.mindMap.batchExecution.push('activeNode' + node.uid, () => {
if (!node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, false)
this.mindMap.renderer.removeActiveNode(node)
if (!node.nodeData.data.isActive) {
return
}
this.mindMap.renderer.setNodeActive(node, false)
this.mindMap.renderer.removeActiveNode(node)
// })
}
})
}
// 是否存在选区
hasSelectRange() {
return this.isSelecting
}
}
Select.instanceName = 'select'

View File

@@ -1,5 +1,5 @@
import { Rect, Polygon, Path } from '@svgdotjs/svg.js'
import { CONSTANTS } from '../../../constants/constant'
import { CONSTANTS } from './utils/constant'
// 节点形状类
export default class Shape {
@@ -62,10 +62,11 @@ export default class Shape {
// 创建形状节点
createShape() {
const shape = this.node.getShape()
let { width, height } = this.node
let node = null
// 矩形
if (shape === CONSTANTS.SHAPE.RECTANGLE) {
node = this.createRect()
node = new Rect().size(width, height)
} else if (shape === CONSTANTS.SHAPE.DIAMOND) {
// 菱形
node = this.createDiamond()
@@ -94,41 +95,9 @@ export default class Shape {
return node
}
// 获取节点减去节点边框宽度、hover节点边框宽度后的尺寸
getNodeSize() {
const borderWidth = this.node.getBorderWidth()
let { width, height } = this.node
width -= borderWidth
height -= borderWidth
return {
width,
height
}
}
// 创建矩形
createRect() {
let { width, height } = this.getNodeSize()
let borderRadius = this.node.style.merge('borderRadius')
return new Path().plot(`
M${borderRadius},0
L${width - borderRadius},0
C${width - borderRadius},0 ${width},${0} ${width},${borderRadius}
L${width},${height - borderRadius}
C${width},${height - borderRadius} ${width},${height} ${
width - borderRadius
},${height}
L${borderRadius},${height}
C${borderRadius},${height} ${0},${height} ${0},${height - borderRadius}
L${0},${borderRadius}
C${0},${borderRadius} ${0},${0} ${borderRadius},${0}
Z
`)
}
// 创建菱形
createDiamond() {
let { width, height } = this.getNodeSize()
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
let topX = halfWidth
@@ -151,7 +120,7 @@ export default class Shape {
createParallelogram() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.getNodeSize()
let { width, height } = this.node
return new Polygon().plot([
[paddingX, 0],
[width, 0],
@@ -162,7 +131,7 @@ export default class Shape {
// 创建圆角矩形
createRoundedRectangle() {
let { width, height } = this.getNodeSize()
let { width, height } = this.node
let halfHeight = height / 2
return new Path().plot(`
M${halfHeight},0
@@ -176,7 +145,7 @@ export default class Shape {
// 创建八角矩形
createOctagonalRectangle() {
let w = 5
let { width, height } = this.getNodeSize()
let { width, height } = this.node
return new Polygon().plot([
[0, w],
[w, 0],
@@ -193,7 +162,7 @@ export default class Shape {
createOuterTriangularRectangle() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.getNodeSize()
let { width, height } = this.node
return new Polygon().plot([
[paddingX, 0],
[width - paddingX, 0],
@@ -208,7 +177,7 @@ export default class Shape {
createInnerTriangularRectangle() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.getNodeSize()
let { width, height } = this.node
return new Polygon().plot([
[0, 0],
[width, 0],
@@ -221,7 +190,7 @@ export default class Shape {
// 创建椭圆
createEllipse() {
let { width, height } = this.getNodeSize()
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return new Path().plot(`
@@ -234,7 +203,7 @@ export default class Shape {
// 创建圆
createCircle() {
let { width, height } = this.getNodeSize()
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return new Path().plot(`

View File

@@ -1,37 +1,13 @@
import { tagColorList } from '../../../constants/constant'
import { checkIsNodeStyleDataKey } from '../../../utils/index'
import { tagColorList } from './utils/constant'
const rootProp = ['paddingX', 'paddingY']
const backgroundStyleProps = [
'backgroundColor',
'backgroundImage',
'backgroundRepeat',
'backgroundPosition',
'backgroundSize'
]
// 样式类
class Style {
// 设置背景样式
static setBackgroundStyle(el, themeConfig) {
// 缓存容器元素原本的样式
if (!Style.cacheStyle) {
Style.cacheStyle = {}
let style = window.getComputedStyle(el)
backgroundStyleProps.forEach(prop => {
Style.cacheStyle[prop] = style[prop]
})
}
// 设置新样式
let {
backgroundColor,
backgroundImage,
backgroundRepeat,
backgroundPosition,
backgroundSize
} = themeConfig
let { backgroundColor, backgroundImage, backgroundRepeat, backgroundPosition, backgroundSize } = themeConfig
el.style.backgroundColor = backgroundColor
if (backgroundImage && backgroundImage !== 'none') {
if (backgroundImage) {
el.style.backgroundImage = `url(${backgroundImage})`
el.style.backgroundRepeat = backgroundRepeat
el.style.backgroundPosition = backgroundPosition
@@ -41,22 +17,13 @@ class Style {
}
}
// 移除背景样式
static removeBackgroundStyle(el) {
if (!Style.cacheStyle) return
backgroundStyleProps.forEach(prop => {
el.style[prop] = Style.cacheStyle[prop]
})
Style.cacheStyle = null
}
// 构造函数
constructor(ctx) {
this.ctx = ctx
}
// 合并样式
merge(prop, root) {
merge(prop, root, isActive) {
let themeConfig = this.ctx.mindMap.themeConfig
// 三级及以下节点
let defaultConfig = themeConfig.node
@@ -73,6 +40,17 @@ class Style {
// 二级节点
defaultConfig = themeConfig.second
}
// 激活状态
if (isActive !== undefined ? isActive : this.ctx.nodeData.data.isActive) {
if (
this.ctx.nodeData.data.activeStyle &&
this.ctx.nodeData.data.activeStyle[prop] !== undefined
) {
return this.ctx.nodeData.data.activeStyle[prop]
} else if (defaultConfig.active && defaultConfig.active[prop]) {
return defaultConfig.active[prop]
}
}
// 优先使用节点本身的样式
return this.getSelfStyle(prop) !== undefined
? this.getSelfStyle(prop)
@@ -80,8 +58,8 @@ class Style {
}
// 获取某个样式值
getStyle(prop, root) {
return this.merge(prop, root)
getStyle(prop, root, isActive) {
return this.merge(prop, root, isActive)
}
// 获取自身自定义样式
@@ -145,10 +123,10 @@ class Style {
// 获取文本样式
getTextFontStyle() {
return {
italic: this.merge('fontStyle') === 'italic',
bold: this.merge('fontWeight'),
fontSize: this.merge('fontSize'),
return {
italic: this.merge('fontStyle') === 'italic',
bold: this.merge('fontWeight'),
fontSize: this.merge('fontSize'),
fontFamily: this.merge('fontFamily')
}
}
@@ -204,45 +182,14 @@ class Style {
// 展开收起按钮
iconBtn(node, node2, fillNode) {
let { color, fill, fontSize, fontColor } = this.ctx.mindMap.opt
.expandBtnStyle || {
let { color, fill } = this.ctx.mindMap.opt.expandBtnStyle || {
color: '#808080',
fill: '#fff',
fontSize: 12,
strokeColor: '#333333',
fontColor: '#333333'
fill: '#fff'
}
node.fill({ color: color })
node2.fill({ color: color })
fillNode.fill({ color: fill })
if (this.ctx.mindMap.opt.isShowExpandNum) {
node.attr({ 'font-size': fontSize, 'font-color': fontColor })
}
}
// 是否设置了自定义的样式
hasCustomStyle() {
let res = false
Object.keys(this.ctx.nodeData.data).forEach(item => {
if (checkIsNodeStyleDataKey(item)) {
res = true
}
})
return res
}
// hover和激活节点
hoverNode(node) {
const { hoverRectColor } = this.ctx.mindMap.opt
node
.radius(5)
.fill('none')
.stroke({
color: hoverRectColor
})
}
}
Style.cacheStyle = null
export default Style

View File

@@ -1,5 +1,4 @@
import { getStrWithBrFromHtml, checkNodeOuter } from '../../utils'
import { ERROR_TYPES } from '../../constants/constant'
import { getStrWithBrFromHtml, checkNodeOuter } from './utils'
// 节点文字编辑类
export default class TextEdit {
@@ -55,29 +54,6 @@ export default class TextEdit {
this.show(this.renderer.activeNodeList[0])
})
this.mindMap.on('scale', this.onScale)
// // 监听按键事件,判断是否自动进入文本编辑模式
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
window.addEventListener('keydown', e => {
const activeNodeList = this.mindMap.renderer.activeNodeList
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
const node = activeNodeList[0]
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
if (node && this.checkIsAutoEnterTextEditKey(e)) {
this.show(node, e, false, true)
}
})
}
}
// 判断是否是自动进入文本编模式的按钮
checkIsAutoEnterTextEditKey(e) {
const keyCode = e.keyCode
return (
(keyCode === 229 ||
(keyCode >= 65 && keyCode <= 90) ||
(keyCode >= 48 && keyCode <= 57)) &&
!this.mindMap.keyCommand.hasCombinationKey(e)
)
}
// 注册临时快捷键
@@ -86,47 +62,26 @@ export default class TextEdit {
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
this.mindMap.keyCommand.addShortcut('Tab', () => {
this.hideEditTextBox()
})
}
// 显示文本编辑框
// isInserting是否是刚创建的节点
// isFromKeyDown是否是在按键事件进入的编辑
async show(node, e, isInserting = false, isFromKeyDown = false) {
// 使用了自定义节点内容那么不响应编辑事件
if (node.isUseCustomNodeContent()) {
return
}
let { beforeTextEdit } = this.mindMap.opt
if (typeof beforeTextEdit === 'function') {
let isShow = false
try {
isShow = await beforeTextEdit(node, isInserting)
} catch (error) {
isShow = false
this.mindMap.opt.errorHandler(ERROR_TYPES.BEFORE_TEXT_EDIT_ERROR, error)
}
if (!isShow) return
}
show(node) {
this.currentNode = node
let { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
this.mindMap.view.translateXY(offsetLeft, offsetTop)
let rect = node._textData.node.node.getBoundingClientRect()
if (this.mindMap.richText) {
this.mindMap.richText.showEditText(node, rect, isInserting, isFromKeyDown)
this.mindMap.richText.showEditText(node, rect)
return
}
this.showEditTextBox(node, rect, isInserting, isFromKeyDown)
this.showEditTextBox(node, rect)
}
// 处理画布缩放
onScale() {
if (!this.currentNode) return
if (this.mindMap.richText) {
this.mindMap.richText.cacheEditingText =
this.mindMap.richText.getEditText()
this.mindMap.richText.cacheEditingText = this.mindMap.richText.getEditText()
this.mindMap.richText.showTextEdit = false
} else {
this.cacheEditingText = this.getEditText()
@@ -136,10 +91,7 @@ export default class TextEdit {
}
// 显示文本编辑框
showEditTextBox(node, rect, isInserting, isFromKeyDown) {
if (this.showTextEdit) return
const { nodeTextEditZIndex, textAutoWrapWidth, selectTextOnEnterEditText } =
this.mindMap.opt
showEditTextBox(node, rect) {
this.mindMap.emit('before_show_text_edit')
this.registerTmpShortcut()
if (!this.textEditNode) {
@@ -152,62 +104,33 @@ export default class TextEdit {
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('keydown', e => {
if (this.checkIsAutoEnterTextEditKey(e)) {
e.stopPropagation()
}
})
const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
document.body.appendChild(this.textEditNode)
}
let scale = this.mindMap.view.scale
let lineHeight = node.style.merge('lineHeight')
let fontSize = node.style.merge('fontSize')
let textLines = (this.cacheEditingText || node.nodeData.data.text).split(
/\n/gim
)
let textLines = (this.cacheEditingText || node.nodeData.data.text).split(/\n/gim)
let isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
node.style.domText(this.textEditNode, scale, isMultiLine)
this.textEditNode.style.zIndex = nodeTextEditZIndex
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.innerHTML = textLines.join('<br>')
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
this.textEditNode.style.display = 'block'
this.textEditNode.style.maxWidth = textAutoWrapWidth * scale + 'px'
this.textEditNode.style.maxWidth = this.mindMap.opt.textAutoWrapWidth * scale + 'px'
if (isMultiLine && lineHeight !== 1) {
this.textEditNode.style.transform = `translateY(${
-((lineHeight * fontSize - fontSize) / 2) * scale
}px)`
this.textEditNode.style.transform = `translateY(${-((lineHeight * fontSize - fontSize) / 2) * scale}px)`
}
this.showTextEdit = true
// 选中文本
// if (!this.cacheEditingText) {
// this.selectNodeText()
// }
if (isInserting || (selectTextOnEnterEditText && !isFromKeyDown)) {
if (!this.cacheEditingText) {
this.selectNodeText()
} else {
this.focus()
}
this.cacheEditingText = ''
}
// 聚焦
focus() {
let selection = window.getSelection()
let range = document.createRange()
range.selectNodeContents(this.textEditNode)
range.collapse()
selection.removeAllRanges()
selection.addRange(range)
}
// 选中文本
selectNodeText() {
let selection = window.getSelection()

206
simple-mind-map/src/View.js Normal file
View File

@@ -0,0 +1,206 @@
import { CONSTANTS } from './utils/constant'
// 视图操作类
class View {
// 构造函数
constructor(opt = {}) {
this.opt = opt
this.mindMap = this.opt.mindMap
this.scale = 1
this.sx = 0
this.sy = 0
this.x = 0
this.y = 0
this.firstDrag = true
this.setTransformData(this.mindMap.opt.viewData)
this.bind()
}
// 绑定
bind() {
// 快捷键
this.mindMap.keyCommand.addShortcut('Control+=', () => {
this.enlarge()
})
this.mindMap.keyCommand.addShortcut('Control+-', () => {
this.narrow()
})
this.mindMap.keyCommand.addShortcut('Control+Enter', () => {
this.reset()
})
this.mindMap.svg.on('dblclick', () => {
this.reset()
})
// 拖动视图
this.mindMap.event.on('mousedown', () => {
this.sx = this.x
this.sy = this.y
})
this.mindMap.event.on('drag', (e, event) => {
if (e.ctrlKey) {
// 按住ctrl键拖动为多选
return
}
if (this.firstDrag) {
this.firstDrag = false
// 清除激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
}
this.x = this.sx + event.mousemoveOffset.x
this.y = this.sy + event.mousemoveOffset.y
this.transform()
})
this.mindMap.event.on('mouseup', () => {
this.firstDrag = true
})
// 放大缩小视图
this.mindMap.event.on('mousewheel', (e, dir) => {
if (this.mindMap.opt.customHandleMousewheel && typeof this.mindMap.opt.customHandleMousewheel === 'function') {
return this.mindMap.opt.customHandleMousewheel(e)
}
if (this.mindMap.opt.mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
switch (dir) {
// 鼠标滚轮,向上和向左,都是缩小
case CONSTANTS.DIR.UP:
case CONSTANTS.DIR.LEFT:
this.narrow()
break
// 鼠标滚轮,向下和向右,都是放大
case CONSTANTS.DIR.DOWN:
case CONSTANTS.DIR.RIGHT:
this.enlarge()
break
}
} else {
switch (dir){
// 上移
case CONSTANTS.DIR.DOWN:
this.translateY(-this.mindMap.opt.mousewheelMoveStep)
break
// 下移
case CONSTANTS.DIR.UP:
this.translateY(this.mindMap.opt.mousewheelMoveStep)
break
// 右移
case CONSTANTS.DIR.LEFT:
this.translateX(-this.mindMap.opt.mousewheelMoveStep)
break
// 左移
case CONSTANTS.DIR.RIGHT:
this.translateX(this.mindMap.opt.mousewheelMoveStep)
break
}
}
})
}
// 获取当前变换状态数据
getTransformData() {
return {
transform: this.mindMap.draw.transform(),
state: {
scale: this.scale,
x: this.x,
y: this.y,
sx: this.sx,
sy: this.sy
}
}
}
// 动态设置变换状态数据
setTransformData(viewData) {
if (viewData) {
Object.keys(viewData.state).forEach(prop => {
this[prop] = viewData.state[prop]
})
this.mindMap.draw.transform({
...viewData.transform
})
this.mindMap.emit('view_data_change', this.getTransformData())
this.mindMap.emit('scale', this.scale)
}
}
// 平移x,y方向
translateXY(x, y) {
this.x += x
this.y += y
this.transform()
}
// 平移x方向
translateX(step) {
this.x += step
this.transform()
}
// 平移x方式到
translateXTo(x) {
this.x = x
this.transform()
}
// 平移y方向
translateY(step) {
this.y += step
this.transform()
}
// 平移y方向到
translateYTo(y) {
this.y = y
this.transform()
}
// 应用变换
transform() {
this.mindMap.draw.transform({
scale: this.scale,
// origin: 'center center',
translate: [this.x, this.y]
})
this.mindMap.emit('view_data_change', this.getTransformData())
}
// 恢复
reset() {
let scaleChange = this.scale !== 1
this.scale = 1
this.x = 0
this.y = 0
this.transform()
if (scaleChange) {
this.mindMap.emit('scale', this.scale)
}
}
// 缩小
narrow() {
if (this.scale - this.mindMap.opt.scaleRatio > 0.1) {
this.scale -= this.mindMap.opt.scaleRatio
} else {
this.scale = 0.1
}
this.transform()
this.mindMap.emit('scale', this.scale)
}
// 放大
enlarge() {
this.scale += this.mindMap.opt.scaleRatio
this.transform()
this.mindMap.emit('scale', this.scale)
}
// 设置缩放
setScale(scale) {
this.scale = scale
this.transform()
this.mindMap.emit('scale', this.scale)
}
}
export default View

View File

@@ -1,8 +1,8 @@
import { Text, G } from '@svgdotjs/svg.js'
import { degToRad, camelCaseToHyphen } from '../utils'
import { degToRad, camelCaseToHyphen } from './utils'
import merge from 'deepmerge'
// 水印插件
// 水印
class Watermark {
constructor(opt = {}) {
this.mindMap = opt.mindMap

View File

@@ -1,190 +0,0 @@
import { CONSTANTS } from './constant'
// 默认选项配置
export const defaultOpt = {
// 是否只读
readonly: false,
// 布局
layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
fishboneDeg: 45,
// 主题
theme: 'default', // 内置主题default默认主题
// 主题配置,会和所选择的主题进行合并
themeConfig: {},
// 放大缩小的增量比例
scaleRatio: 0.2,
// 鼠标缩放是否以鼠标当前位置为中心点,否则以画布中心点
mouseScaleCenterUseMousePosition: true,
// 最多显示几个标签
maxTag: 5,
// 展开收缩按钮尺寸
expandBtnSize: 20,
// 节点里图片和文字的间距
imgTextMargin: 5,
// 节点里各种文字信息的间距,如图标和文字的间距
textContentMargin: 2,
// 多选节点时鼠标移动到边缘时的画布移动偏移量
selectTranslateStep: 3,
// 多选节点时鼠标移动距边缘多少距离时开始偏移
selectTranslateLimit: 20,
// 自定义节点备注内容显示
customNoteContentShow: null,
/*
{
show(){},
hide(){}
}
*/
// 是否开启节点自由拖拽
enableFreeDrag: false,
// 水印配置
watermarkConfig: {
text: '',
lineSpacing: 100,
textSpacing: 100,
angle: 30,
textStyle: {
color: '#999',
opacity: 0.5,
fontSize: 14
}
},
// 达到该宽度文本自动换行
textAutoWrapWidth: 500,
// 自定义鼠标滚轮事件处理
// 可以传一个函数,回调参数为事件对象
customHandleMousewheel: null,
// 鼠标滚动的行为如果customHandleMousewheel传了自定义函数这个属性不生效
mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM, // zoom放大缩小、move上下移动
// 当mousewheelAction设为move时可以通过该属性控制鼠标滚动一下视图移动的步长单位px
mousewheelMoveStep: 100,
// 当mousewheelAction设为zoom时默认向前滚动是缩小向后滚动是放大如果该属性设为true那么会反过来
mousewheelZoomActionReverse: false,
// 默认插入的二级节点的文字
defaultInsertSecondLevelNodeText: '二级节点',
// 默认插入的二级以下节点的文字
defaultInsertBelowSecondLevelNodeText: '分支主题',
// 展开收起按钮的颜色
expandBtnStyle: {
color: '#808080',
fill: '#fff',
fontSize: 13,
strokeColor: '#333333'
},
// 自定义展开收起按钮的图标
expandBtnIcon: {
open: '', // svg字符串
close: ''
},
// 处理收起节点数量
expandBtnNumHandler: num => {
return num
},
// 是否显示带数量的收起按钮
isShowExpandNum: true,
// 是否只有当鼠标在画布内才响应快捷键事件
enableShortcutOnlyWhenMouseInSvg: true,
// 初始根节点的位置
initRootNodePosition: null,
// 导出png、svg、pdf时的图形内边距注意是单侧内边距
exportPaddingX: 10,
exportPaddingY: 10,
// 节点文本编辑框的z-index
nodeTextEditZIndex: 3000,
// 节点备注浮层的z-index
nodeNoteTooltipZIndex: 3000,
// 是否在点击了画布外的区域时结束节点文本的编辑状态
isEndNodeTextEditOnClickOuter: true,
// 最大历史记录数
maxHistoryCount: 1000,
// 是否一直显示节点的展开收起按钮,默认为鼠标移上去和激活时才显示
alwaysShowExpandBtn: false,
// 扩展节点可插入的图标
iconList: [
// {
// name: '',// 分组名称
// type: '',// 分组的值
// list: [// 分组下的图标列表
// {
// name: '',// 图标名称
// icon:''// 图标可以传svg或图片
// }
// ]
// }
],
// 节点最大缓存数量
maxNodeCacheCount: 1000,
// 关联线默认文字
defaultAssociativeLineText: '关联',
// 思维导图适应画布大小时的内边距
fitPadding: 50,
// 是否开启按住ctrl键多选节点功能
enableCtrlKeyNodeSelection: true,
// 设置为左键多选节点,右键拖动画布
useLeftKeySelectionRightKeyDrag: false,
// 节点即将进入编辑前的回调方法如果该方法返回true以外的值那么将取消编辑函数可以返回一个值或一个Promise回调参数为节点实例
beforeTextEdit: null,
// 是否开启自定义节点内容
isUseCustomNodeContent: false,
// 自定义返回节点内容的方法
customCreateNodeContent: null,
// 指定内部一些元素节点文本编辑元素、节点备注显示元素、关联线文本编辑元素、节点图片调整按钮元素添加到的位置默认添加到document.body下
customInnerElsAppendTo: null,
// 拖拽元素时,指示元素新位置的块的最大高度
nodeDragPlaceholderMaxSize: 20,
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
// 开启该特性后需要给你的输入框绑定keydown事件并禁止冒泡
enableAutoEnterTextEditWhenKeydown: false,
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
richTextEditFakeInPlace: false,
// 自定义对剪贴板文本的处理。当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据如果你想处理其他思维导图的数据比如processon、zhixi等那么可以传递一个函数接受当前剪贴板中的文本为参数返回处理后的数据可以返回两种类型
/*
1.返回一个纯文本,那么会直接以该文本创建一个子节点
2.返回一个节点对象,格式如下:
{
// 代表是simple-mind-map格式的数据
simpleMindMap: true,
// 节点数据同simple-mind-map节点数据格式
data: {
data: {
text: ''
},
children: []
}
}
*/
// 如果你的处理逻辑存在异步逻辑也可以返回一个promise
customHandleClipboardText: null,
// 禁止鼠标滚轮缩放你仍旧可以使用api进行缩放
disableMouseWheelZoom: false,
// 错误处理函数
errorHandler: (code, error) => {
console.error(code, error)
},
// 设置导出图片和svg时针对富文本节点内容也就是嵌入到svg中的html节点的默认样式覆盖
// 如果不覆盖,会发生偏移问题
resetCss: `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
`,
// 开启鼠标双击复位思维导图位置及缩放
enableDblclickReset: false,
// 导出图片时canvas的缩放倍数该配置会和window.devicePixelRatio值取最大值
minExportImgCanvasScale: 2,
// 节点鼠标hover和激活时显示的矩形边框的颜色
hoverRectColor: 'rgb(94, 200, 248)',
// 节点鼠标hover和激活时显示的矩形边框距节点内容的距离
hoverRectPadding: 2,
// 双击节点进入节点文本编辑时是否默认选中文本,默认只在创建新节点时会选中
selectTextOnEnterEditText: false,
// 删除节点后激活相邻节点
deleteNodeActive: true,
// 拖拽节点时鼠标移动到画布边缘是否开启画布自动移动
autoMoveWhenMouseInEdgeOnDrag: true
}

View File

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

View File

@@ -1,308 +0,0 @@
import { CONSTANTS } from '../../constants/constant'
// 视图操作类
class View {
// 构造函数
constructor(opt = {}) {
this.opt = opt
this.mindMap = this.opt.mindMap
this.scale = 1
this.sx = 0
this.sy = 0
this.x = 0
this.y = 0
this.firstDrag = true
this.setTransformData(this.mindMap.opt.viewData)
this.bind()
}
// 绑定
bind() {
// 快捷键
this.mindMap.keyCommand.addShortcut('Control+=', () => {
this.enlarge()
})
this.mindMap.keyCommand.addShortcut('Control+-', () => {
this.narrow()
})
this.mindMap.keyCommand.addShortcut('Control+Enter', () => {
this.reset()
})
this.mindMap.keyCommand.addShortcut('Control+i', () => {
this.fit()
})
this.mindMap.svg.on('dblclick', () => {
if (!this.mindMap.opt.enableDblclickReset) return
this.reset()
})
// 拖动视图
this.mindMap.event.on('mousedown', () => {
this.sx = this.x
this.sy = this.y
})
this.mindMap.event.on('drag', (e, event) => {
if (e.ctrlKey) {
// 按住ctrl键拖动为多选
return
}
if (this.firstDrag) {
this.firstDrag = false
// 清除激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
}
this.x = this.sx + event.mousemoveOffset.x
this.y = this.sy + event.mousemoveOffset.y
this.transform()
})
this.mindMap.event.on('mouseup', () => {
this.firstDrag = true
})
// 放大缩小视图
this.mindMap.event.on('mousewheel', (e, dir, event, isTouchPad) => {
let {
customHandleMousewheel,
mousewheelAction,
mouseScaleCenterUseMousePosition,
mousewheelMoveStep,
mousewheelZoomActionReverse,
disableMouseWheelZoom
} = this.mindMap.opt
// 是否自定义鼠标滚轮事件
if (
customHandleMousewheel &&
typeof customHandleMousewheel === 'function'
) {
return customHandleMousewheel(e)
}
// 鼠标滚轮事件控制缩放
if (mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM) {
if (disableMouseWheelZoom) return
const { x: clientX, y: clientY } = this.mindMap.toPos(e.clientX, e.clientY)
let cx = mouseScaleCenterUseMousePosition ? clientX : undefined
let cy = mouseScaleCenterUseMousePosition ? clientY : undefined
switch (dir) {
// 鼠标滚轮,向上和向左,都是缩小
case CONSTANTS.DIR.UP:
case CONSTANTS.DIR.LEFT:
mousewheelZoomActionReverse
? this.enlarge(cx, cy, isTouchPad)
: this.narrow(cx, cy, isTouchPad)
break
// 鼠标滚轮,向下和向右,都是放大
case CONSTANTS.DIR.DOWN:
case CONSTANTS.DIR.RIGHT:
mousewheelZoomActionReverse
? this.narrow(cx, cy, isTouchPad)
: this.enlarge(cx, cy, isTouchPad)
break
}
} else {
// 鼠标滚轮事件控制画布移动
let step = mousewheelMoveStep
if (isTouchPad) {
step = 5
}
switch (dir) {
// 上移
case CONSTANTS.DIR.DOWN:
this.translateY(-step)
break
// 下移
case CONSTANTS.DIR.UP:
this.translateY(step)
break
// 右移
case CONSTANTS.DIR.LEFT:
this.translateX(-step)
break
// 左移
case CONSTANTS.DIR.RIGHT:
this.translateX(step)
break
}
}
})
}
// 获取当前变换状态数据
getTransformData() {
return {
transform: this.mindMap.draw.transform(),
state: {
scale: this.scale,
x: this.x,
y: this.y,
sx: this.sx,
sy: this.sy
}
}
}
// 动态设置变换状态数据
setTransformData(viewData) {
if (viewData) {
Object.keys(viewData.state).forEach(prop => {
this[prop] = viewData.state[prop]
})
this.mindMap.draw.transform({
...viewData.transform
})
this.mindMap.emit('view_data_change', this.getTransformData())
this.mindMap.emit('scale', this.scale)
}
}
// 平移x,y方向
translateXY(x, y) {
if (x === 0 && y === 0) return
this.x += x
this.y += y
this.transform()
}
// 平移x方向
translateX(step) {
if (step === 0) return
this.x += step
this.transform()
}
// 平移x方式到
translateXTo(x) {
this.x = x
this.transform()
}
// 平移y方向
translateY(step) {
if (step === 0) return
this.y += step
this.transform()
}
// 平移y方向到
translateYTo(y) {
this.y = y
this.transform()
}
// 应用变换
transform() {
this.mindMap.draw.transform({
origin: [0, 0],
scale: this.scale,
translate: [this.x, this.y]
})
this.mindMap.emit('view_data_change', this.getTransformData())
}
// 恢复
reset() {
let scaleChange = this.scale !== 1
this.scale = 1
this.x = 0
this.y = 0
this.transform()
if (scaleChange) {
this.mindMap.emit('scale', this.scale)
}
}
// 缩小
narrow(cx, cy, isTouchPad) {
const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1)
const scale = Math.max(this.scale - scaleRatio, 0.1)
this.scaleInCenter(scale, cx, cy)
this.transform()
this.mindMap.emit('scale', this.scale)
}
// 放大
enlarge(cx, cy, isTouchPad) {
const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1)
const scale = this.scale + scaleRatio
this.scaleInCenter(scale, cx, cy)
this.transform()
this.mindMap.emit('scale', this.scale)
}
// 基于指定中心进行缩放cxcy 可不指定,此时会使用画布中心点
scaleInCenter(scale, cx, cy) {
if (cx === undefined || cy === undefined) {
cx = this.mindMap.width / 2
cy = this.mindMap.height / 2
}
const prevScale = this.scale
const ratio = 1 - scale / prevScale
const dx = (cx - this.x) * ratio
const dy = (cy - this.y) * ratio
this.x += dx
this.y += dy
this.scale = scale
}
// 设置缩放
setScale(scale, cx, cy) {
if (cx !== undefined && cy !== undefined) {
this.scaleInCenter(scale, cx, cy)
} else {
this.scale = scale
}
this.transform()
this.mindMap.emit('scale', this.scale)
}
// 适应画布大小
fit() {
let { fitPadding } = this.mindMap.opt
let draw = this.mindMap.draw
let origTransform = draw.transform()
let rect = draw.rbox()
let drawWidth = rect.width / origTransform.scaleX
let drawHeight = rect.height / origTransform.scaleY
let drawRatio = drawWidth / drawHeight
let { width: elWidth, height: elHeight } =
this.mindMap.el.getBoundingClientRect()
elWidth = elWidth - fitPadding * 2
elHeight = elHeight - fitPadding * 2
let elRatio = elWidth / elHeight
let newScale = 0
let flag = ''
if (drawWidth <= elWidth && drawHeight <= elHeight) {
newScale = 1
flag = 1
} else {
let newWidth = 0
let newHeight = 0
if (drawRatio > elRatio) {
newWidth = elWidth
newHeight = elWidth / drawRatio
flag = 2
} else {
newHeight = elHeight
newWidth = elHeight * drawRatio
flag = 3
}
newScale = newWidth / drawWidth
}
this.setScale(newScale)
let newRect = draw.rbox()
let newX = 0
let newY = 0
if (flag === 1) {
newX = -newRect.x + fitPadding + (elWidth - newRect.width) / 2
newY = -newRect.y + fitPadding + (elHeight - newRect.height) / 2
} else if (flag === 2) {
newX = -newRect.x + fitPadding
newY = -newRect.y + fitPadding + (elHeight - newRect.height) / 2
} else if (flag === 3) {
newX = -newRect.x + fitPadding + (elWidth - newRect.width) / 2
newY = -newRect.y + fitPadding
}
this.translateXY(newX, newY)
}
}
export default View

View File

@@ -1,7 +1,6 @@
import Node from '../core/render/node/Node'
import { CONSTANTS, initRootNodePositionMap } from '../constants/constant'
import Node from '../Node'
import { CONSTANTS, initRootNodePositionMap } from '../utils/constant'
import Lru from '../utils/Lru'
import { createUid } from '../utils/index'
// 布局基类
class Base {
@@ -46,24 +45,7 @@ class Base {
// 检查当前来源是否需要重新计算节点大小
checkIsNeedResizeSources() {
return [
CONSTANTS.CHANGE_THEME,
CONSTANTS.TRANSFORM_TO_NORMAL_NODE
].includes(this.renderer.renderSource)
}
// 层级类型改变
checkIsLayerTypeChange(oldIndex, newIndex) {
if (oldIndex >= 2 && newIndex >= 2) return false
if (oldIndex >= 2 && newIndex < 2) return true
if (oldIndex < 2 && newIndex >= 2) return true
}
// 检查是否是结构布局改变重新渲染展开收起按钮占位元素
checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) {
if (this.renderer.renderSource === CONSTANTS.CHANGE_LAYOUT) {
node.needRerenderExpandBtnPlaceholderRect = true
}
return [CONSTANTS.CHANGE_THEME, CONSTANTS.TRANSFORM_TO_NORMAL_NODE].includes(this.renderer.renderSource)
}
// 创建节点实例
@@ -73,16 +55,11 @@ class Base {
// 数据上保存了节点引用,那么直接复用节点
if (data && data._node && !this.renderer.reRender) {
newNode = data._node
let isLayerTypeChange = this.checkIsLayerTypeChange(
newNode.layerIndex,
layerIndex
)
newNode.reset()
newNode.layerIndex = layerIndex
this.cacheNode(data._node.uid, newNode)
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
// 主题或主题配置改变了需要重新计算节点大小和布局
if (this.checkIsNeedResizeSources() || isLayerTypeChange) {
if (this.checkIsNeedResizeSources()) {
newNode.getSize()
newNode.needLayout = true
}
@@ -91,27 +68,22 @@ class Base {
newNode = this.lru.get(data.data.uid)
// 保存该节点上一次的数据
let lastData = JSON.stringify(newNode.nodeData.data)
let isLayerTypeChange = this.checkIsLayerTypeChange(
newNode.layerIndex,
layerIndex
)
newNode.reset()
newNode.nodeData = newNode.handleData(data || {})
newNode.layerIndex = layerIndex
this.cacheNode(data.data.uid, newNode)
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
data._node = newNode
// 主题或主题配置改变了需要重新计算节点大小和布局
let isResizeSource = this.checkIsNeedResizeSources()
// 节点数据改变了需要重新计算节点大小和布局
let isNodeDataChange = lastData !== JSON.stringify(data.data)
if (isResizeSource || isNodeDataChange || isLayerTypeChange) {
if (isResizeSource || isNodeDataChange) {
newNode.getSize()
newNode.needLayout = true
}
} else {
// 创建新节点
let uid = data.data.uid || createUid()
let uid = this.mindMap.uid++
newNode = new Node({
data,
uid,
@@ -148,7 +120,7 @@ class Base {
} else if (initRootNodePositionMap[value] !== undefined) {
return size * initRootNodePositionMap[value]
} else if (/^\d\d*%$/.test(value)) {
return (Number.parseFloat(value) / 100) * size
return Number.parseFloat(value) / 100 * size
} else {
return (size - nodeSize) / 2
}
@@ -157,24 +129,12 @@ class Base {
// 定位节点到画布中间
setNodeCenter(node) {
let { initRootNodePosition } = this.mindMap.opt
let { CENTER } = CONSTANTS.INIT_ROOT_NODE_POSITION
if (
!initRootNodePosition ||
!Array.isArray(initRootNodePosition) ||
initRootNodePosition.length < 2
) {
let { CENTER }= CONSTANTS.INIT_ROOT_NODE_POSITION
if (!initRootNodePosition || !Array.isArray(initRootNodePosition) || initRootNodePosition.length < 2) {
initRootNodePosition = [CENTER, CENTER]
}
node.left = this.formatPosition(
initRootNodePosition[0],
this.mindMap.width,
node.width
)
node.top = this.formatPosition(
initRootNodePosition[1],
this.mindMap.height,
node.height
)
node.left = this.formatPosition(initRootNodePosition[0], this.mindMap.width, node.width)
node.top = this.formatPosition(initRootNodePosition[1], this.mindMap.height, node.height)
}
// 更新子节点属性
@@ -191,7 +151,7 @@ class Base {
// 更新子节点多个属性
updateChildrenPro(children, props) {
children.forEach(item => {
Object.keys(props).forEach(prop => {
Object.keys(props).forEach((prop) => {
item[prop] += props[prop]
})
if (item.children && item.children.length && !item.hasCustomPosition()) {
@@ -202,13 +162,9 @@ class Base {
}
// 递归计算节点的宽度
getNodeAreaWidth(node, withGeneralization = false) {
getNodeAreaWidth(node) {
let widthArr = []
let totalGeneralizationNodeWidth = 0
let loop = (node, width) => {
if (withGeneralization && node.checkHasGeneralization()) {
totalGeneralizationNodeWidth += node._generalizationNodeWidth
}
if (node.children.length) {
width += node.width / 2
node.children.forEach(item => {
@@ -220,7 +176,7 @@ class Base {
}
}
loop(node, 0)
return Math.max(...widthArr) + totalGeneralizationNodeWidth
return Math.max(...widthArr)
}
// 二次贝塞尔曲线
@@ -241,22 +197,16 @@ class Base {
// 获取节点的marginX
getMarginX(layerIndex) {
const { themeConfig, opt } = this.mindMap
const { second, node } = themeConfig
const hoverRectPadding = opt.hoverRectPadding * 2
return layerIndex === 1
? second.marginX + hoverRectPadding
: node.marginX + hoverRectPadding
? this.mindMap.themeConfig.second.marginX
: this.mindMap.themeConfig.node.marginX
}
// 获取节点的marginY
getMarginY(layerIndex) {
const { themeConfig, opt } = this.mindMap
const { second, node } = themeConfig
const hoverRectPadding = opt.hoverRectPadding * 2
return layerIndex === 1
? second.marginY + hoverRectPadding
: node.marginY + hoverRectPadding
? this.mindMap.themeConfig.second.marginY
: this.mindMap.themeConfig.node.marginY
}
// 获取节点包括概要在内的宽度

View File

@@ -87,11 +87,11 @@ class CatalogOrganization extends Base {
totalLeft += cur.width + marginX
})
} else {
let totalTop = node.top + this.getNodeHeightWithGeneralization(node) + marginY + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
let totalTop = node.top + node.height + marginY + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(cur => {
cur.left = node.left + node.width * 0.5
cur.top = totalTop
totalTop += this.getNodeHeightWithGeneralization(cur) + marginY + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
totalTop += cur.height + marginY + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0)
})
}
}
@@ -112,7 +112,7 @@ class CatalogOrganization extends Base {
}
// 调整left
if (parent && parent.isRoot) {
let areaWidth = this.getNodeAreaWidth(node, true)
let areaWidth = this.getNodeAreaWidth(node)
let difference = areaWidth - node.width
if (difference > 0) {
this.updateBrothersLeft(node, difference)
@@ -124,7 +124,7 @@ class CatalogOrganization extends Base {
let marginY = this.getMarginY(layerIndex + 1)
let totalHeight =
node.children.reduce((h, item) => {
return h + this.getNodeHeightWithGeneralization(item) + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
return h + item.height + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
}, 0) +
len * marginY
this.updateBrothersTop(node, totalHeight)
@@ -349,11 +349,6 @@ class CatalogOrganization extends Base {
gNode.left = right + generalizationNodeMargin
gNode.top = top + (bottom - top - gNode.height) / 2
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
rect.size(width, expandBtnSize).x(0).y(height)
}
}
export default CatalogOrganization

View File

@@ -1,6 +1,6 @@
import Base from './Base'
import { walk, asyncRun, degToRad } from '../utils'
import { CONSTANTS } from '../constants/constant'
import { CONSTANTS } from '../utils/constant'
import utils from './fishboneUtils'
// 鱼骨图
@@ -51,16 +51,15 @@ class Fishbone extends Base {
// 节点生长方向
newNode.dir =
index % 2 === 0
? CONSTANTS.LAYOUT_GROW_DIR.TOP
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
? CONSTANTS.TIMELINE_DIR.TOP
: CONSTANTS.TIMELINE_DIR.BOTTOM
}
// 计算二级节点的top值
if (parent._node.isRoot) {
let marginY = this.getMarginY(layerIndex)
if (this.checkIsTop(newNode)) {
newNode.top = parent._node.top - newNode.height - marginY
newNode.top = parent._node.top - newNode.height
} else {
newNode.top = parent._node.top + parent._node.height + marginY
newNode.top = parent._node.top + parent._node.height
}
}
}
@@ -81,16 +80,15 @@ class Fishbone extends Base {
null,
(node, parent, isRoot, layerIndex) => {
if (node.isRoot) {
let marginX = this.getMarginX(layerIndex + 1)
let topTotalLeft = node.left + node.width + node.height + marginX
let bottomTotalLeft = node.left + node.width + node.height + marginX
let topTotalLeft = node.left + node.width + node.height
let bottomTotalLeft = node.left + node.width + node.height
node.children.forEach(item => {
if (this.checkIsTop(item)) {
item.left = topTotalLeft
topTotalLeft += item.width + marginX
topTotalLeft += item.width
} else {
item.left = bottomTotalLeft + 20
bottomTotalLeft += item.width + marginX
bottomTotalLeft += item.width
}
})
}
@@ -156,10 +154,9 @@ class Fishbone extends Base {
getNodeAreaHeight(node) {
let totalHeight = 0
let loop = node => {
let marginY = this.getMarginY(node.layerIndex)
totalHeight +=
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + marginY
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
if (node.children.length) {
node.children.forEach(item => {
loop(item)
@@ -225,7 +222,7 @@ class Fishbone extends Base {
// 检查节点是否是上方节点
checkIsTop(node) {
return node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
return node.dir === CONSTANTS.TIMELINE_DIR.TOP
}
// 绘制连线,连接该节点到其子节点
@@ -242,27 +239,26 @@ class Fishbone extends Base {
// 当前节点是根节点
// 根节点的子节点是和根节点同一水平线排列
let maxx = -Infinity
node.children.forEach(item => {
node.children.forEach((item) => {
if (item.left > maxx) {
maxx = item.left
}
// 水平线段到二级节点的连线
let marginY = this.getMarginY(item.layerIndex)
let nodeLineX = item.left
let offset = node.height / 2 + marginY
let offset = node.height / 2
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
let line = this.draw.path()
if (this.checkIsTop(item)) {
line.plot(
`M ${nodeLineX - offsetX},${item.top + item.height + offset} L ${
item.left
},${item.top + item.height}`
`M ${nodeLineX - offsetX},${item.top + item.height + offset} L ${item.left},${
item.top + item.height
}`
)
} else {
line.plot(
`M ${nodeLineX - offsetX},${item.top - offset} L ${nodeLineX},${
item.top
}`
`M ${nodeLineX - offsetX},${
item.top - offset
} L ${nodeLineX},${item.top}`
)
}
node.style.line(line)
@@ -271,7 +267,7 @@ class Fishbone extends Base {
})
// 从根节点出发的水平线
let nodeHalfTop = node.top + node.height / 2
let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1)
let offset = node.height / 2
let line = this.draw.path()
line.plot(
`M ${node.left + node.width},${nodeHalfTop} L ${
@@ -377,27 +373,6 @@ class Fishbone extends Base {
gNode.left = right + generalizationNodeMargin
gNode.top = top + (bottom - top - gNode.height) / 2
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
let dir = ''
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
dir =
node.layerIndex === 1
? CONSTANTS.LAYOUT_GROW_DIR.TOP
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
} else {
dir =
node.layerIndex === 1
? CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
: CONSTANTS.LAYOUT_GROW_DIR.TOP
}
if (dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
rect.size(width, expandBtnSize).x(0).y(-expandBtnSize)
} else {
rect.size(width, expandBtnSize).x(0).y(height)
}
}
}
export default Fishbone

View File

@@ -52,8 +52,8 @@ class Fishbone extends Base {
// 节点生长方向
newNode.dir =
index % 2 === 0
? CONSTANTS.LAYOUT_GROW_DIR.TOP
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
? CONSTANTS.TIMELINE_DIR.TOP
: CONSTANTS.TIMELINE_DIR.BOTTOM
}
// 计算二级节点的top值
if (parent._node.isRoot) {

View File

@@ -52,8 +52,8 @@ class Fishbone extends Base {
// 节点生长方向
newNode.dir =
index % 2 === 0
? CONSTANTS.LAYOUT_GROW_DIR.TOP
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
? CONSTANTS.TIMELINE_DIR.TOP
: CONSTANTS.TIMELINE_DIR.BOTTOM
}
// 计算二级节点的top值
if (parent._node.isRoot) {
@@ -281,7 +281,7 @@ class Fishbone extends Base {
if (
node.parent &&
node.parent.isRoot &&
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
node.dir === CONSTANTS.TIMELINE_DIR.TOP
) {
line.plot(
`M ${x},${top} L ${x + lineLength},${

View File

@@ -56,9 +56,6 @@ class LogicalStructure extends Base {
}, 0) +
(len + 1) * this.getMarginY(layerIndex + 1)
: 0
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeHeight = cur._node.checkHasGeneralization() ? cur._node._generalizationNodeHeight + this.getMarginY(layerIndex + 1) : 0
cur._node.childrenAreaHeight2 = Math.max(cur._node.childrenAreaHeight, generalizationNodeHeight)
},
true,
0
@@ -102,8 +99,9 @@ class LogicalStructure extends Base {
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaHeight2 -
this.getMarginY(layerIndex + 1) * 2 - node.height
node.childrenAreaHeight -
this.getMarginY(layerIndex + 1) * 2 -
node.height
if (difference > 0) {
this.updateBrothers(node, difference / 2)
}
@@ -174,7 +172,9 @@ class LogicalStructure extends Base {
let x2 = item.left
let y2 = item.top + item.height / 2
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStyleOffset = nodeUseLineStyle ? item.width : 0
let nodeUseLineStyleOffset = nodeUseLineStyle
? item.width
: 0
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
let path = `M ${x1},${y1} L ${x1 + s1},${y1} L ${x1 + s1},${y2} L ${
@@ -236,7 +236,7 @@ class LogicalStructure extends Base {
let nodeUseLineStylePath = nodeUseLineStyle
? ` L ${item.left + item.width},${y2}`
: ''
if (node.isRoot && !this.mindMap.themeConfig.rootLineKeepSameInCurve) {
if (node.isRoot) {
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
} else {
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath
@@ -260,7 +260,10 @@ class LogicalStructure extends Base {
if (_x === translateX && _y === translateY) {
return
}
btn.translate(_x - translateX, _y - translateY)
btn.translate(
_x - translateX,
_y - translateY
)
}
// 创建概要节点
@@ -283,11 +286,6 @@ class LogicalStructure extends Base {
gNode.left = right + generalizationNodeMargin
gNode.top = top + (bottom - top - gNode.height) / 2
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
rect.size(expandBtnSize, height).x(width).y(0)
}
}
export default LogicalStructure

View File

@@ -1,6 +1,5 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { CONSTANTS } from '../constants/constant'
// 思维导图
class MindMap extends Base {
@@ -46,14 +45,11 @@ class MindMap extends Base {
newNode.dir = parent._node.dir
} else {
// 节点生长方向
newNode.dir =
index % 2 === 0
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
newNode.dir = index % 2 === 0 ? 'right' : 'left'
}
// 根据生长方向定位到父节点的左侧或右侧
newNode.left =
newNode.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT
newNode.dir === 'right'
? parent._node.left +
parent._node.width +
this.getMarginX(layerIndex)
@@ -76,7 +72,7 @@ class MindMap extends Base {
let leftChildrenAreaHeight = 0
let rightChildrenAreaHeight = 0
cur._node.children.forEach(item => {
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
if (item.dir === 'left') {
leftLen++
leftChildrenAreaHeight += item.height
} else {
@@ -90,11 +86,6 @@ class MindMap extends Base {
cur._node.rightChildrenAreaHeight =
rightChildrenAreaHeight +
(rightLen + 1) * this.getMarginY(layerIndex + 1)
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeHeight = cur._node.checkHasGeneralization() ? cur._node._generalizationNodeHeight + this.getMarginY(layerIndex + 1) : 0
cur._node.leftChildrenAreaHeight2 = Math.max(cur._node.leftChildrenAreaHeight, generalizationNodeHeight)
cur._node.rightChildrenAreaHeight2 = Math.max(cur._node.rightChildrenAreaHeight, generalizationNodeHeight)
},
true,
0
@@ -118,7 +109,7 @@ class MindMap extends Base {
let leftTotalTop = baseTop - node.leftChildrenAreaHeight / 2
let rightTotalTop = baseTop - node.rightChildrenAreaHeight / 2
node.children.forEach(cur => {
if (cur.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
if (cur.dir === 'left') {
cur.top = leftTotalTop
leftTotalTop += cur.height + marginY
} else {
@@ -144,8 +135,8 @@ class MindMap extends Base {
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let base = this.getMarginY(layerIndex + 1) * 2 + node.height
let leftDifference = node.leftChildrenAreaHeight2 - base
let rightDifference = node.rightChildrenAreaHeight2 - base
let leftDifference = node.leftChildrenAreaHeight - base
let rightDifference = node.rightChildrenAreaHeight - base
if (leftDifference > 0 || rightDifference > 0) {
this.updateBrothers(node, leftDifference / 2, rightDifference / 2)
}
@@ -171,10 +162,7 @@ class MindMap extends Base {
return
}
let _offset = 0
let addHeight =
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
? leftAddHeight
: rightAddHeight
let addHeight = item.dir === 'left' ? leftAddHeight : rightAddHeight
// 上面的节点往上移
if (_index < index) {
_offset = -addHeight
@@ -220,8 +208,10 @@ class MindMap extends Base {
let x1 = 0
let _s = 0
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStyleOffset = nodeUseLineStyle ? item.width : 0
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
let nodeUseLineStyleOffset = nodeUseLineStyle
? item.width
: 0
if (item.dir === 'left') {
_s = -s1
x1 = node.layerIndex === 0 ? left : left - expandBtnSize
nodeUseLineStyleOffset = -nodeUseLineStyleOffset
@@ -230,10 +220,7 @@ class MindMap extends Base {
x1 = node.layerIndex === 0 ? left + width : left + width + expandBtnSize
}
let y1 = top + height / 2
let x2 =
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
? item.left + item.width
: item.left
let x2 = item.dir === 'left' ? item.left + item.width : item.left
let y2 = item.top + item.height / 2
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
@@ -259,21 +246,18 @@ class MindMap extends Base {
let x1 =
node.layerIndex === 0
? left + width / 2
: item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
: item.dir === 'left'
? left - expandBtnSize
: left + width + expandBtnSize
let y1 = top + height / 2
let x2 =
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
? item.left + item.width
: item.left
let x2 = item.dir === 'left' ? item.left + item.width : item.left
let y2 = item.top + item.height / 2
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
y2 = nodeUseLineStyle ? y2 + item.height / 2 : y2
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = ''
if (nodeUseLineStyle) {
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
if (item.dir === 'left') {
nodeUseLineStylePath = ` L ${item.left},${y2}`
} else {
nodeUseLineStylePath = ` L ${item.left + item.width},${y2}`
@@ -299,14 +283,11 @@ class MindMap extends Base {
let x1 =
node.layerIndex === 0
? left + width / 2
: item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
: item.dir === 'left'
? left - expandBtnSize
: left + width + expandBtnSize
let y1 = top + height / 2
let x2 =
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
? item.left + item.width
: item.left
let x2 = item.dir === 'left' ? item.left + item.width : item.left
let y2 = item.top + item.height / 2
let path = ''
y1 = nodeUseLineStyle && !node.isRoot ? y1 + height / 2 : y1
@@ -314,13 +295,13 @@ class MindMap extends Base {
// 节点使用横线风格,需要额外渲染横线
let nodeUseLineStylePath = ''
if (this.mindMap.themeConfig.nodeUseLineStyle) {
if (item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
if (item.dir === 'left') {
nodeUseLineStylePath = ` L ${item.left},${y2}`
} else {
nodeUseLineStylePath = ` L ${item.left + item.width},${y2}`
}
}
if (node.isRoot && !this.mindMap.themeConfig.rootLineKeepSameInCurve) {
if (node.isRoot) {
path = this.quadraticCurvePath(x1, y1, x2, y2) + nodeUseLineStylePath
} else {
path = this.cubicBezierPath(x1, y1, x2, y2) + nodeUseLineStylePath
@@ -339,8 +320,7 @@ class MindMap extends Base {
? height / 2
: 0
// 位置没有变化则返回
let _x =
node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT ? 0 - expandBtnSize : width
let _x = (node.dir === 'left' ? 0 - expandBtnSize : width)
let _y = height / 2 + nodeUseLineStyleOffset
if (_x === translateX && _y === translateY) {
return
@@ -352,7 +332,7 @@ class MindMap extends Base {
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let isLeft = node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
let isLeft = node.dir === 'left'
let {
top,
bottom,
@@ -378,15 +358,6 @@ class MindMap extends Base {
(isLeft ? gNode.width : 0)
gNode.top = top + (bottom - top - gNode.height) / 2
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
rect.size(expandBtnSize, height).x(-expandBtnSize).y(0)
} else {
rect.size(expandBtnSize, height).x(width).y(0)
}
}
}
export default MindMap

View File

@@ -57,10 +57,6 @@ class OrganizationStructure extends Base {
}, 0) +
(len + 1) * this.getMarginY(layerIndex + 1)
: 0
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeWidth = cur._node.checkHasGeneralization() ? cur._node._generalizationNodeWidth + this.getMarginY(layerIndex + 1) : 0
cur._node.childrenAreaWidth2 = Math.max(cur._node.childrenAreaWidth, generalizationNodeWidth)
},
true,
0
@@ -104,7 +100,7 @@ class OrganizationStructure extends Base {
}
// 判断子节点所占的宽度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaWidth2 -
node.childrenAreaWidth -
this.getMarginY(layerIndex + 1) * 2 -
node.width
if (difference > 0) {
@@ -259,11 +255,6 @@ class OrganizationStructure extends Base {
gNode.top = bottom + generalizationNodeMargin
gNode.left = left + (right - left - gNode.width) / 2
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
rect.size(width, expandBtnSize).x(0).y(height)
}
}
export default OrganizationStructure

View File

@@ -1,6 +1,6 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { CONSTANTS } from '../constants/constant'
import { CONSTANTS } from '../utils/constant'
// 时间轴
class Timeline extends Base {
@@ -50,8 +50,8 @@ class Timeline extends Base {
// 节点生长方向
newNode.dir =
index % 2 === 0
? CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
: CONSTANTS.LAYOUT_GROW_DIR.TOP
? CONSTANTS.TIMELINE_DIR.BOTTOM
: CONSTANTS.TIMELINE_DIR.TOP
}
} else {
newNode.dir = ''
@@ -151,7 +151,7 @@ class Timeline extends Base {
if (
parent &&
parent.isRoot &&
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
node.dir === CONSTANTS.TIMELINE_DIR.TOP
) {
// 遍历二级节点的子节点
node.children.forEach(item => {
@@ -280,7 +280,7 @@ class Timeline extends Base {
if (
node.parent &&
node.parent.isRoot &&
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
node.dir === CONSTANTS.TIMELINE_DIR.TOP
) {
line.plot(`M ${x},${top} L ${x},${miny}`)
} else {
@@ -301,7 +301,7 @@ class Timeline extends Base {
if (
node.parent &&
node.parent.isRoot &&
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
node.dir === CONSTANTS.TIMELINE_DIR.TOP
) {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
@@ -336,28 +336,6 @@ class Timeline extends Base {
gNode.left = right + generalizationNodeMargin
gNode.top = top + (bottom - top - gNode.height) / 2
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
if (this.layout === CONSTANTS.LAYOUT.TIMELINE) {
rect.size(width, expandBtnSize).x(0).y(height)
} else {
let dir = ''
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
dir =
node.layerIndex === 1
? CONSTANTS.LAYOUT_GROW_DIR.TOP
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
} else {
dir = CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
}
if (dir === CONSTANTS.LAYOUT_GROW_DIR.TOP) {
rect.size(width, expandBtnSize).x(0).y(-expandBtnSize)
} else {
rect.size(width, expandBtnSize).x(0).y(height)
}
}
}
}
export default Timeline

View File

@@ -1,431 +0,0 @@
import Base from './Base'
import { walk, asyncRun } from '../utils'
import { CONSTANTS } from '../constants/constant'
// 竖向时间轴
class VerticalTimeline extends Base {
// 构造函数
constructor(opt = {}, layout) {
super(opt)
this.layout = layout
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedTopValue()
},
() => {
this.adjustLeftTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据创建节点、计算根节点的位置计算根节点的子节点的top值
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(cur, parent, isRoot, layerIndex, index) => {
let newNode = this.createNode(cur, 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.RIGHT
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
}
// 定位二级节点的left
if (parent._node.isRoot) {
newNode.left =
parent._node.left +
(cur._node.width > parent._node.width
? -(cur._node.width - parent._node.width) / 2
: (parent._node.width - cur._node.width) / 2)
} else {
newNode.left =
newNode.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT
? parent._node.left +
parent._node.width +
this.getMarginX(layerIndex)
: parent._node.left -
this.getMarginX(layerIndex) -
newNode.width
}
}
if (!cur.data.expand) {
return true
}
},
(cur, parent, isRoot, layerIndex) => {
// 返回时计算节点的areaHeight也就是子节点所占的高度之和包括外边距
if (isRoot) {
return
}
let len = cur.data.expand === false ? 0 : cur._node.children.length
cur._node.childrenAreaHeight = len
? cur._node.children.reduce((h, item) => {
return h + item.height
}, 0) +
(len + 1) * this.getMarginY(layerIndex + 1)
: 0
},
true,
0
)
}
// 遍历节点树计算节点的top
computedTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex, index) => {
if (
node.nodeData.data.expand &&
node.children &&
node.children.length
) {
let marginY = this.getMarginY(layerIndex + 1)
// 定位二级节点的top
if (isRoot) {
let top = node.top + node.height
let totalTop = top + marginY
node.children.forEach(cur => {
cur.top = totalTop
totalTop += cur.height + marginY
})
} else {
// 定位三级及以下节点的top
let marginY = this.getMarginY(layerIndex + 1)
let baseTop = node.top + node.height / 2 + marginY
// 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半
let totalTop = baseTop - node.childrenAreaHeight / 2
node.children.forEach(cur => {
cur.top = totalTop
totalTop += cur.height + marginY
})
}
}
},
null,
true
)
}
// 调整节点left、top
adjustLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.nodeData.data.expand) {
return
}
if (isRoot) return
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let base = this.getMarginY(layerIndex + 1) * 2 + node.height
let difference = node.childrenAreaHeight - base
if (difference > 0) {
this.updateBrothers(node, difference / 2)
}
},
null,
true
)
}
// 更新兄弟节点的top
updateBrothers(node, addHeight) {
if (node.parent) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
childrenList.forEach((item, _index) => {
// 自定义节点位置
if (item.hasCustomPosition()) return
// 三级或三级以下节点自身位置不需要动
if (!node.parent.isRoot && item === node) return
let _offset = 0
// 二级节点上面的兄弟节点不需要移动,自身需要往下移动
if (node.parent.isRoot) {
// 上面的节点不用移
if (_index < index) {
_offset = 0
} else if (_index > index) {
// 下面的节点往下移
_offset = addHeight * 2
} else {
// 自身也要移动
_offset = addHeight
}
} else {
// 三级或三级以下节点两侧的兄弟节点向两侧移动
// 上面的节点往上移
if (_index < index) {
_offset = -addHeight
} else if (_index > index) {
// 下面的节点往下移
_offset = addHeight
}
}
item.top += _offset
// 同步更新子节点的位置
if (item.children && item.children.length) {
this.updateChildren(item.children, 'top', _offset)
}
})
// 更新父节点的位置
this.updateBrothers(node.parent, addHeight)
}
}
// 调整兄弟节点的top
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = childrenList.findIndex(item => {
return item === node
})
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, lineStyle) {
if (lineStyle === 'curve') {
this.renderLineCurve(node, lines, style)
} else if (lineStyle === 'direct') {
this.renderLineDirect(node, lines, style)
} else {
this.renderLineStraight(node, lines, style)
}
}
// 直线连接
renderLineStraight(node, lines, style) {
if (node.children.length <= 0) {
return []
}
let { expandBtnSize } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
if (node.isRoot) {
// 当前节点是根节点
let prevBother = node
// 根节点的子节点是和根节点同一水平线排列
node.children.forEach((item, index) => {
let y1 = prevBother.top + prevBother.height
let y2 = item.top
let x = node.left + node.width / 2
let path = `M ${x},${y1} L ${x},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
prevBother = item
})
} else {
// 当前节点为非根节点
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT) {
let nodeRight = node.left + node.width
let nodeYCenter = node.top + node.height / 2
let marginX = this.getMarginX(node.layerIndex + 1)
let offset = (marginX - expandBtnSize) * 0.6
node.children.forEach((item, index) => {
let itemLeft = item.left
let itemYCenter = item.top + item.height / 2
let path = `
M ${nodeRight},${nodeYCenter}
L ${nodeRight + offset},${nodeYCenter}
L ${nodeRight + offset},${itemYCenter}
L ${itemLeft},${itemYCenter}`
lines[index].plot(path)
style && style(lines[index], item)
})
} else {
let nodeLeft = node.left
let nodeYCenter = node.top + node.height / 2
let marginX = this.getMarginX(node.layerIndex + 1)
let offset = (marginX - expandBtnSize) * 0.6
node.children.forEach((item, index) => {
let itemRight = item.left + item.width
let itemYCenter = item.top + item.height / 2
let path = `
M ${nodeLeft},${nodeYCenter}
L ${nodeLeft - offset},${nodeYCenter}
L ${nodeLeft - offset},${itemYCenter}
L ${itemRight},${itemYCenter}`
lines[index].plot(path)
style && style(lines[index], item)
})
}
}
}
// 直连
renderLineDirect(node, lines, style) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
node.children.forEach((item, index) => {
if (node.isRoot) {
let prevBother = node
// 根节点的子节点是和根节点同一水平线排列
node.children.forEach((item, index) => {
let y1 = prevBother.top + prevBother.height
let y2 = item.top
let x = node.left + node.width / 2
let path = `M ${x},${y1} L ${x},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
prevBother = item
})
} else {
let x1 =
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
? left - expandBtnSize
: left + width + expandBtnSize
let y1 = top + height / 2
let x2 =
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
? item.left + item.width
: item.left
let y2 = item.top + item.height / 2
let path = `M ${x1},${y1} L ${x2},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
}
})
}
// 曲线风格连线
renderLineCurve(node, lines, style) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
if (!this.mindMap.opt.alwaysShowExpandBtn) {
expandBtnSize = 0
}
node.children.forEach((item, index) => {
if (node.isRoot) {
let prevBother = node
// 根节点的子节点是和根节点同一水平线排列
node.children.forEach((item, index) => {
let y1 = prevBother.top + prevBother.height
let y2 = item.top
let x = node.left + node.width / 2
let path = `M ${x},${y1} L ${x},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
prevBother = item
})
} else {
let x1 =
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
? left - expandBtnSize
: left + width + expandBtnSize
let y1 = top + height / 2
let x2 =
item.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
? item.left + item.width
: item.left
let y2 = item.top + item.height / 2
let path = this.cubicBezierPath(x1, y1, x2, y2)
lines[index].plot(path)
style && style(lines[index], item)
}
})
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.RIGHT) {
btn.translate(width - translateX, height / 2 - translateY)
} else {
btn.translate(-expandBtnSize - translateX, height / 2 - translateY)
}
}
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let isLeft = node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT
let {
top,
bottom,
left,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeBoundaries(node, 'h', isLeft)
let x = isLeft
? left - generalizationLineMargin
: right + generalizationLineMargin
let x1 = x
let y1 = top
let x2 = x
let y2 = bottom
let cx = x1 + (isLeft ? -20 : 20)
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
gLine.plot(path)
gNode.left =
x +
(isLeft ? -generalizationNodeMargin : generalizationNodeMargin) -
(isLeft ? gNode.width : 0)
gNode.top = top + (bottom - top - gNode.height) / 2
}
// 渲染展开收起按钮的隐藏占位元素
renderExpandBtnRect(rect, expandBtnSize, width, height, node) {
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.LEFT) {
rect.size(expandBtnSize, height).x(-expandBtnSize).y(0)
} else {
rect.size(expandBtnSize, height).x(width).y(0)
}
}
}
export default VerticalTimeline

View File

@@ -47,32 +47,30 @@ export default {
computedLeftTopValue({ layerIndex, node, ctx }) {
if (layerIndex >= 1 && node.children) {
// 遍历三级及以下节点的子节点
let marginY = ctx.getMarginY(layerIndex + 1)
let startLeft = node.left + node.width * ctx.childIndent
let totalTop =
node.top +
node.height +
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(item => {
item.left = startLeft
item.top += totalTop
totalTop +=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
},
adjustLeftTopValueBefore({ node, parent, ctx, layerIndex }) {
adjustLeftTopValueBefore({ node, parent, ctx }) {
// 调整top
let len = node.children.length
let marginY = ctx.getMarginY(layerIndex + 1)
// 调整三级及以下节点的top
if (parent && !parent.isRoot && len > 0) {
let totalHeight = node.children.reduce((h, item) => {
return (
h +
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
)
}, 0)
ctx.updateBrothersTop(node, totalHeight)
@@ -82,8 +80,7 @@ export default {
// 将二级节点的子节点移到上方
if (parent && parent.isRoot) {
// 遍历二级节点的子节点
let marginY = ctx.getMarginY(node.layerIndex + 1)
let totalHeight = node.expandBtnSize + marginY
let totalHeight = node.expandBtnSize
node.children.forEach(item => {
// 调整top
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
@@ -137,14 +134,13 @@ export default {
}
},
computedLeftTopValue({ layerIndex, node, ctx }) {
let marginY = ctx.getMarginY(layerIndex + 1)
if (layerIndex === 1 && node.children) {
// 遍历二级节点的子节点
let startLeft = node.left + node.width * ctx.childIndent
let totalTop =
node.top +
node.height +
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(item => {
item.left = startLeft
@@ -153,7 +149,7 @@ export default {
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
totalTop +=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
if (layerIndex > 1 && node.children) {
@@ -161,26 +157,25 @@ export default {
let startLeft = node.left + node.width * ctx.childIndent
let totalTop =
node.top -
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) - marginY
(ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(item => {
item.left = startLeft
item.top = totalTop - item.height
totalTop -=
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
},
adjustLeftTopValueBefore({ node, ctx, layerIndex }) {
// 调整top
let marginY = ctx.getMarginY(layerIndex + 1)
let len = node.children.length
if (layerIndex > 2 && len > 0) {
let totalHeight = node.children.reduce((h, item) => {
return (
h +
item.height +
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + marginY
(ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
)
}, 0)
ctx.updateBrothersTop(node, -totalHeight)
@@ -190,7 +185,6 @@ export default {
// 将二级节点的子节点移到上方
if (parent && parent.isRoot) {
// 遍历二级节点的子节点
let marginY = ctx.getMarginY(node.layerIndex + 1)
let totalHeight = 0
let totalHeight2 = node.expandBtnSize
node.children.forEach(item => {
@@ -198,12 +192,11 @@ export default {
let hasChildren = ctx.getNodeActChildrenLength(item) > 0
let nodeTotalHeight = ctx.getNodeAreaHeight(item)
let offset =
hasChildren
hasChildren > 0
? nodeTotalHeight -
item.height -
(hasChildren ? item.expandBtnSize : 0)
: 0
offset -= (hasChildren ? marginY : 0)
let _top = totalHeight + offset
let _left = item.left
item.top += _top

View File

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

View File

@@ -1,12 +1,5 @@
import JSZip from 'jszip'
import xmlConvert from 'xml-js'
import {
getTextFromHtml,
imgToDataUrl,
parseDataUrl,
getImageSize,
isUndef
} from '../utils/index'
// 解析.xmind文件
const parseXmindFile = file => {
@@ -15,13 +8,11 @@ const parseXmindFile = file => {
async zip => {
try {
let content = ''
let jsonFile = zip.files['content.json']
let xmlFile = zip.files['content.xml'] || zip.files['/content.xml']
if (jsonFile) {
let json = await jsonFile.async('string')
content = await transformXmind(json, zip.files)
} else if (xmlFile) {
let xml = await xmlFile.async('string')
if (zip.files['content.json']) {
let json = await zip.files['content.json'].async('string')
content = transformXmind(json)
} else if (zip.files['content.xml']) {
let xml = await zip.files['content.xml'].async('string')
let json = xmlConvert.xml2json(xml)
content = transformOldXmind(json)
}
@@ -42,20 +33,18 @@ const parseXmindFile = file => {
}
// 转换xmind数据
const transformXmind = async (content, files) => {
const transformXmind = content => {
let data = JSON.parse(content)[0]
let nodeTree = data.rootTopic
let newTree = {}
let waitLoadImageList = []
let walk = async (node, newNode) => {
let walk = (node, newNode) => {
newNode.data = {
// 节点内容
text: isUndef(node.title) ? '' : node.title
text: node.title
}
// 节点备注
if (node.notes) {
let notesData = node.notes.realHTML || node.notes.plain
newNode.data.note = notesData ? notesData.content || '' : ''
newNode.data.note = (node.notes.realHTML || node.notes.plain).content
}
// 超链接
if (node.href && /^https?:\/\//.test(node.href)) {
@@ -65,42 +54,6 @@ const transformXmind = async (content, files) => {
if (node.labels && node.labels.length > 0) {
newNode.data.tag = node.labels
}
// 图片
if (node.image && /\.(jpg|jpeg|png|gif|webp)$/.test(node.image.src)) {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
try {
// 读取图片
let imageType = /\.([^.]+)$/.exec(node.image.src)[1]
let imageBase64 =
`data:image/${imageType};base64,` +
(await files['resources/' + node.image.src.split('/')[1]].async(
'base64'
))
newNode.data.image = imageBase64
// 如果图片尺寸不存在
if (!node.image.width && !node.image.height) {
let imageSize = await getImageSize(imageBase64)
newNode.data.imageSize = {
width: imageSize.width,
height: imageSize.height
}
} else {
newNode.data.imageSize = {
width: node.image.width,
height: node.image.height
}
}
resolve()
} catch (error) {
console.log(error)
resolve()
}
}
// 子节点
newNode.children = []
if (
@@ -116,7 +69,6 @@ const transformXmind = async (content, files) => {
}
}
walk(nodeTree, newTree)
await Promise.all(waitLoadImageList)
return newTree
}
@@ -126,7 +78,6 @@ const transformOldXmind = content => {
let elements = data.elements
let root = null
let getRoot = arr => {
if (!arr) return
for (let i = 0; i < arr.length; i++) {
if (!root && arr[i].name === 'topic') {
root = arr[i]
@@ -146,11 +97,9 @@ const transformOldXmind = content => {
}
let walk = (node, newNode) => {
let nodeElements = node.elements
let nodeTitle = getItemByName(nodeElements, 'title')
nodeTitle = nodeTitle && nodeTitle.elements && nodeTitle.elements[0].text
newNode.data = {
// 节点内容
text: isUndef(nodeTitle) ? '' : nodeTitle
text: getItemByName(nodeElements, 'title').elements[0].text
}
try {
// 节点备注
@@ -191,7 +140,7 @@ const transformOldXmind = content => {
if (_children && _children.elements && _children.elements.length > 0) {
_children.elements.forEach(item => {
if (item.name === 'topics') {
(item.elements || []).forEach(item2 => {
item.elements.forEach(item2 => {
let newChild = {}
newNode.children.push(newChild)
walk(item2, newChild)
@@ -208,128 +157,8 @@ const transformOldXmind = content => {
return newTree
}
// 数据转换为xmind文件
const transformToXmind = async (data, name) => {
const id = 'simpleMindMap_' + Date.now()
const imageList = []
// 转换核心数据
let newTree = {}
let waitLoadImageList = []
let walk = async (node, newNode, isRoot) => {
let newData = {
id: node.data.uid,
structureClass: 'org.xmind.ui.logic.right',
title: getTextFromHtml(node.data.text), // 节点文本
children: {
attached: []
}
}
// 备注
if (node.data.note !== undefined) {
newData.notes = {
realHTML: {
content: node.data.note
},
plain: {
content: node.data.note
}
}
}
// 超链接
if (node.data.hyperlink !== undefined) {
newData.href = node.data.hyperlink
}
// 标签
if (node.data.tag !== undefined) {
newData.labels = node.data.tag || []
}
// 图片
if (node.data.image) {
// 处理异步逻辑
let resolve = null
let promise = new Promise(_resolve => {
resolve = _resolve
})
waitLoadImageList.push(promise)
try {
let imgName = ''
let imgData = node.data.image
// base64之外的其他图片要先转换成data:url
if (!/^data:/.test(node.data.image)) {
imgData = await imgToDataUrl(node.data.image)
}
// 从data:url中解析出图片类型和ase64
let dataUrlRes = parseDataUrl(imgData)
imgName = 'image_' + imageList.length + '.' + dataUrlRes.type
imageList.push({
name: imgName,
data: dataUrlRes.base64
})
newData.image = {
src: 'xap:resources/' + imgName,
width: node.data.imageSize.width,
height: node.data.imageSize.height
}
resolve()
} catch (error) {
console.log(error)
resolve()
}
}
// 样式
// 暂时不考虑样式
if (isRoot) {
newData.class = 'topic'
newNode.id = id
newNode.class = 'sheet'
newNode.title = name
newNode.extensions = []
newNode.topicPositioning = 'fixed'
newNode.topicOverlapping = 'overlap'
newNode.coreVersion = '2.100.0'
newNode.rootTopic = newData
} else {
Object.keys(newData).forEach(key => {
newNode[key] = newData[key]
})
}
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
let newChild = {}
walk(child, newChild)
newData.children.attached.push(newChild)
})
}
}
walk(data, newTree, true)
await Promise.all(waitLoadImageList)
const contentData = [newTree]
// 创建压缩包
const zip = new JSZip()
zip.file('content.json', JSON.stringify(contentData))
zip.file(
'metadata.json',
`{"modifier":"","dataStructureVersion":"1","layoutEngineVersion":"2","activeSheetId":"${id}"}`
)
const manifestData = {
'file-entries': { 'content.json': {}, 'metadata.json': {} }
}
// 图片
if (imageList.length > 0) {
imageList.forEach(item => {
manifestData['file-entries']['resources/' + item.name] = {}
const img = zip.folder('resources')
img.file(item.name, item.data, { base64: true })
})
}
zip.file('manifest.json', JSON.stringify(manifestData))
const zipData = await zip.generateAsync({ type: 'blob' })
return zipData
}
export default {
parseXmindFile,
transformXmind,
transformOldXmind,
transformToXmind
transformOldXmind
}

View File

@@ -1,605 +0,0 @@
import { bfsWalk, throttle } from '../utils'
import Base from '../layouts/Base'
// 节点拖动插件
class Drag extends Base {
// 构造函数
constructor({ mindMap }) {
super(mindMap.renderer)
this.mindMap = mindMap
this.reset()
this.bindEvent()
}
// 复位
reset() {
// 当前画布节点列表
this.nodeList = []
// 当前拖拽节点
this.node = null
// 当前重叠节点
this.overlapNode = null
// 当前上一个同级节点
this.prevNode = null
// 当前下一个同级节点
this.nextNode = null
// 画布的变换数据
this.drawTransform = null
// 克隆节点
this.clone = null
// 连接线
this.line = null
// 同级位置占位符
this.placeholder = null
// 鼠标按下位置和节点左上角的偏移量
this.offsetX = 0
this.offsetY = 0
// 克隆节点左上角的坐标
this.cloneNodeLeft = 0
this.cloneNodeTop = 0
// 当前鼠标是否按下
this.isMousedown = false
// 拖拽的鼠标位置变量
this.mouseDownX = 0
this.mouseDownY = 0
this.mouseMoveX = 0
this.mouseMoveY = 0
// 鼠标移动的距离距鼠标按下的位置距离多少以上才认为是拖动事件
this.checkDragOffset = 10
this.minOffset = 10
}
// 绑定事件
bindEvent() {
this.checkOverlapNode = throttle(this.checkOverlapNode, 300, this)
this.mindMap.on('node_mousedown', (node, e) => {
if (this.mindMap.opt.readonly || node.isGeneralization) {
return
}
if (e.which !== 1 || node.isRoot) {
return
}
e.preventDefault()
// 计算鼠标按下的位置距离节点左上角的距离
this.drawTransform = this.mindMap.draw.transform()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.offsetX = x - (node.left * scaleX + translateX)
this.offsetY = y - (node.top * scaleY + translateY)
this.node = node
this.isMousedown = true
this.mouseDownX = x
this.mouseDownY = y
this.nodeTreeToList()
})
this.mindMap.on('mousemove', e => {
if (this.mindMap.opt.readonly) {
return
}
if (!this.isMousedown) {
return
}
this.mindMap.emit('node_dragging', this.node)
e.preventDefault()
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
this.mouseMoveX = x
this.mouseMoveY = y
if (
Math.abs(x - this.mouseDownX) <= this.checkDragOffset &&
Math.abs(y - this.mouseDownY) <= this.checkDragOffset &&
!this.node.isDrag
) {
return
}
this.mindMap.renderer.clearAllActive()
this.onMove(x, y, e)
})
this.onMouseup = this.onMouseup.bind(this)
this.mindMap.on('node_mouseup', this.onMouseup)
this.mindMap.on('mouseup', this.onMouseup)
}
// 鼠标松开事件
onMouseup(e) {
if (!this.isMousedown) {
return
}
this.isMousedown = false
let _nodeIsDrag = this.node.isDrag
this.node.isDrag = false
this.node.show()
this.removeCloneNode()
let overlapNodeUid = this.overlapNode
? this.overlapNode.nodeData.data.uid
: ''
let prevNodeUid = this.prevNode ? this.prevNode.nodeData.data.uid : ''
let nextNodeUid = this.nextNode ? this.nextNode.nodeData.data.uid : ''
// 存在重叠子节点,则移动作为其子节点
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, false)
this.mindMap.execCommand('MOVE_NODE_TO', this.node, this.overlapNode)
} else if (this.prevNode) {
// 存在前一个相邻节点,作为其下一个兄弟节点
this.mindMap.renderer.setNodeActive(this.prevNode, false)
this.mindMap.execCommand('INSERT_AFTER', this.node, this.prevNode)
} else if (this.nextNode) {
// 存在下一个相邻节点,作为其前一个兄弟节点
this.mindMap.renderer.setNodeActive(this.nextNode, false)
this.mindMap.execCommand('INSERT_BEFORE', this.node, this.nextNode)
} else if (_nodeIsDrag && this.mindMap.opt.enableFreeDrag) {
// 自定义位置
let { x, y } = this.mindMap.toPos(
e.clientX - this.offsetX,
e.clientY - this.offsetY
)
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
x = (x - translateX) / scaleX
y = (y - translateY) / scaleY
this.node.left = x
this.node.top = y
this.node.customLeft = x
this.node.customTop = y
this.mindMap.execCommand('SET_NODE_CUSTOM_POSITION', this.node, x, y)
this.mindMap.render()
}
this.reset()
this.mindMap.emit('node_dragend', {
overlapNodeUid,
prevNodeUid,
nextNodeUid
})
}
// 创建克隆节点
createCloneNode() {
if (!this.clone) {
// 节点
this.clone = this.node.group.clone()
this.clone.opacity(0.5)
this.clone.css('z-index', 99999)
this.node.isDrag = true
this.node.hide()
// 连接线
this.line = this.draw.path()
this.line.opacity(0.5)
this.node.styleLine(this.line, this.node)
// 同级位置占位符
this.placeholder = this.draw.rect().fill({
color: this.node.style.merge('lineColor', true)
})
this.mindMap.draw.add(this.clone)
}
}
// 移除克隆节点
removeCloneNode() {
if (!this.clone) {
return
}
this.clone.remove()
this.line.remove()
this.placeholder.remove()
}
// 拖动中
onMove(x, y, e) {
if (!this.isMousedown) {
return
}
this.createCloneNode()
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
this.cloneNodeLeft = x - this.offsetX
this.cloneNodeTop = y - this.offsetY
x = (this.cloneNodeLeft - translateX) / scaleX
y = (this.cloneNodeTop - translateY) / scaleY
let t = this.clone.transform()
this.clone.translate(x - t.translateX, y - t.translateY)
// 连接线
let parent = this.node.parent
this.line.plot(
this.quadraticCurvePath(
parent.left + parent.width / 2,
parent.top + parent.height / 2,
x + this.node.width / 2,
y + this.node.height / 2
)
)
this.checkOverlapNode()
// 如果注册了多选节点插件,那么复用它的边缘自动移动画布功能
if (this.mindMap.opt.autoMoveWhenMouseInEdgeOnDrag && this.mindMap.select) {
this.drawTransform = this.mindMap.draw.transform()
this.mindMap.select.clearAutoMoveTimer()
this.mindMap.select.onMove(e.clientX, e.clientY)
}
}
// 检测重叠节点
checkOverlapNode() {
if (!this.drawTransform || !this.placeholder) {
return
}
this.overlapNode = null
this.prevNode = null
this.nextNode = null
this.placeholder.size(0, 0)
this.nodeList.forEach(node => {
if (node.nodeData.data.isActive) {
this.mindMap.renderer.setNodeActive(node, false)
}
if (node.uid === this.node.uid) {
return
}
if (this.overlapNode || (this.prevNode && this.nextNode)) {
return
}
switch (this.mindMap.opt.layout) {
case 'logicalStructure':
this.handleLogicalStructure(node)
break
case 'mindMap':
this.handleMindMap(node)
break
case 'organizationStructure':
this.handleOrganizationStructure(node)
break
case 'catalogOrganization':
this.handleCatalogOrganization(node)
break
case 'timeline':
this.handleTimeLine(node)
break
case 'timeline2':
this.handleTimeLine2(node)
break
case 'verticalTimeline':
this.handleLogicalStructure(node)
break
case 'fishbone':
this.handleFishbone(node)
break
default:
this.handleLogicalStructure(node)
}
})
if (this.overlapNode) {
this.mindMap.renderer.setNodeActive(this.overlapNode, true)
}
}
// 垂直方向比较
// isReverse是否反向
handleVerticalCheck(node, checkList, isReverse = false) {
let x = this.mouseMoveX
let y = this.mouseMoveY
let nodeRect = this.getNodeRect(node)
if (isReverse) {
checkList = checkList.reverse()
}
let oneFourthHeight = nodeRect.height / 4
let { prevBrotherOffset, nextBrotherOffset } =
this.getNodeDistanceToSiblingNode(checkList, node, nodeRect, 'v')
if (nodeRect.left <= x && nodeRect.right >= x) {
// 检测兄弟节点位置
if (
!this.overlapNode &&
!this.prevNode &&
!this.nextNode &&
!node.isRoot
) {
let checkIsPrevNode =
nextBrotherOffset > 0 // 距离下一个兄弟节点的距离大于0
? y > nodeRect.bottom && y <= nodeRect.bottom + nextBrotherOffset // 那么在当前节点外底部判断
: y >= nodeRect.bottom - oneFourthHeight && y <= nodeRect.bottom // 否则在当前节点内底部1/4区间判断
let checkIsNextNode =
prevBrotherOffset > 0 // 距离上一个兄弟节点的距离大于0
? y < nodeRect.top && y >= nodeRect.top - prevBrotherOffset // 那么在当前节点外底部判断
: y >= nodeRect.top && y <= nodeRect.top + oneFourthHeight
if (checkIsPrevNode) {
if (isReverse) {
this.nextNode = node
} else {
this.prevNode = node
}
let size = this.formatPlaceholderSize(nextBrotherOffset)
this.setPlaceholderRect(
node.width,
size,
nodeRect.originLeft,
nodeRect.originBottom
)
} else if (checkIsNextNode) {
if (isReverse) {
this.prevNode = node
} else {
this.nextNode = node
}
let size = this.formatPlaceholderSize(prevBrotherOffset)
this.setPlaceholderRect(
node.width,
size,
nodeRect.originLeft,
nodeRect.originTop - size
)
}
}
// 检测是否重叠
this.checkIsOverlap({
node,
dir: 'v',
prevBrotherOffset,
nextBrotherOffset,
size: oneFourthHeight,
pos: y,
nodeRect
})
}
}
// 水平方向比较
handleHorizontalCheck(node, checkList) {
let x = this.mouseMoveX
let y = this.mouseMoveY
let nodeRect = this.getNodeRect(node)
let oneFourthWidth = nodeRect.width / 4
let { prevBrotherOffset, nextBrotherOffset } =
this.getNodeDistanceToSiblingNode(checkList, node, nodeRect, 'h')
if (nodeRect.top <= y && nodeRect.bottom >= y) {
// 检测兄弟节点位置
if (
!this.overlapNode &&
!this.prevNode &&
!this.nextNode &&
!node.isRoot
) {
let checkIsPrevNode =
nextBrotherOffset > 0 // 距离下一个兄弟节点的距离大于0
? x < nodeRect.right + nextBrotherOffset && x >= nodeRect.right // 那么在当前节点外底部判断
: x <= nodeRect.right && x >= nodeRect.right - oneFourthWidth // 否则在当前节点内底部1/4区间判断
let checkIsNextNode =
prevBrotherOffset > 0 // 距离上一个兄弟节点的距离大于0
? x > nodeRect.left - prevBrotherOffset && x <= nodeRect.left // 那么在当前节点外底部判断
: x <= nodeRect.left + oneFourthWidth && x >= nodeRect.left
if (checkIsPrevNode) {
this.prevNode = node
let size = this.formatPlaceholderSize(nextBrotherOffset)
this.setPlaceholderRect(
size,
node.height,
nodeRect.originRight,
nodeRect.originTop
)
} else if (checkIsNextNode) {
this.nextNode = node
let size = this.formatPlaceholderSize(prevBrotherOffset)
this.setPlaceholderRect(
size,
node.height,
nodeRect.originLeft - size,
nodeRect.originTop
)
}
}
// 检测是否重叠
this.checkIsOverlap({
node,
dir: 'h',
prevBrotherOffset,
nextBrotherOffset,
size: oneFourthWidth,
pos: x,
nodeRect
})
}
}
// 获取节点距前一个和后一个节点的距离
getNodeDistanceToSiblingNode(checkList, node, nodeRect, dir) {
let dir1 = dir === 'v' ? 'top' : 'left'
let dir2 = dir === 'v' ? 'bottom' : 'right'
let index = checkList.findIndex(item => {
return item.uid === node.uid
})
let prevBrother = null
let nextBrother = null
if (index !== -1) {
if (index - 1 >= 0) {
prevBrother = checkList[index - 1]
}
if (index + 1 <= checkList.length - 1) {
nextBrother = checkList[index + 1]
}
}
// 和前一个兄弟节点的距离
let prevBrotherOffset = 0
if (prevBrother) {
let prevNodeRect = this.getNodeRect(prevBrother)
prevBrotherOffset = nodeRect[dir1] - prevNodeRect[dir2]
// 间距小于10就当它不存在
prevBrotherOffset =
prevBrotherOffset >= this.minOffset ? prevBrotherOffset / 2 : 0
} else {
// 没有前一个兄弟节点那么假设和前一个节点的距离为20
prevBrotherOffset = this.minOffset
}
// 和后一个兄弟节点的距离
let nextBrotherOffset = 0
if (nextBrother) {
let nextNodeRect = this.getNodeRect(nextBrother)
nextBrotherOffset = nextNodeRect[dir1] - nodeRect[dir2]
nextBrotherOffset =
nextBrotherOffset >= this.minOffset ? nextBrotherOffset / 2 : 0
} else {
nextBrotherOffset = this.minOffset
}
return {
prevBrotherOffset,
nextBrotherOffset
}
}
// 处理提示元素的大小
formatPlaceholderSize(size) {
const { nodeDragPlaceholderMaxSize } = this.mindMap.opt
return size > 0 ? Math.min(size, nodeDragPlaceholderMaxSize) : 5
}
// 设置提示元素的大小和位置
setPlaceholderRect(w, h, x, y) {
this.placeholder.size(w, h).move(x, y)
}
// 检测是否重叠
checkIsOverlap({
node,
dir,
prevBrotherOffset,
nextBrotherOffset,
size,
pos,
nodeRect
}) {
let dir1 = dir === 'v' ? 'top' : 'left'
let dir2 = dir === 'v' ? 'bottom' : 'right'
if (!this.overlapNode && !this.prevNode && !this.nextNode) {
if (
nodeRect[dir1] + (prevBrotherOffset > 0 ? 0 : size) <= pos &&
nodeRect[dir2] - (nextBrotherOffset > 0 ? 0 : size) >= pos
) {
this.overlapNode = node
}
}
}
// 处理逻辑结构图
handleLogicalStructure(node) {
const checkList = this.commonGetNodeCheckList(node)
this.handleVerticalCheck(node, checkList)
}
// 处理思维导图
handleMindMap(node) {
const checkList = node.parent
? node.parent.children.filter(item => {
let sameDir = true
if (node.layerIndex === 1) {
sameDir = item.dir === node.dir
}
return item !== this.node && sameDir
})
: []
this.handleVerticalCheck(node, checkList)
}
// 处理组织结构图
handleOrganizationStructure(node) {
const checkList = this.commonGetNodeCheckList(node)
this.handleHorizontalCheck(node, checkList)
}
// 处理目录组织图
handleCatalogOrganization(node) {
const checkList = this.commonGetNodeCheckList(node)
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
this.handleVerticalCheck(node, checkList)
}
}
// 处理时间轴
handleTimeLine(node) {
let checkList = this.commonGetNodeCheckList(node)
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
this.handleVerticalCheck(node, checkList)
}
}
// 处理时间轴2
handleTimeLine2(node) {
let checkList = this.commonGetNodeCheckList(node)
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
if (node.dir === 'top' && node.layerIndex === 2) {
this.handleVerticalCheck(node, checkList, true)
} else {
this.handleVerticalCheck(node, checkList)
}
}
}
// 处理鱼骨图
handleFishbone(node) {
let checkList = node.parent
? node.parent.children.filter(item => {
return item !== this.node && item.layerIndex > 1
})
: []
if (node.layerIndex === 1) {
this.handleHorizontalCheck(node, checkList)
} else {
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
if (node.dir === 'top' && node.layerIndex === 2) {
this.handleVerticalCheck(node, checkList, true)
} else {
this.handleVerticalCheck(node, checkList)
}
}
}
// 获取节点的兄弟节点列表通用方法
commonGetNodeCheckList(node) {
return node.parent
? node.parent.children.filter(item => {
return item !== this.node
})
: []
}
// 计算节点的位置尺寸信息
getNodeRect(node) {
let { scaleX, scaleY, translateX, translateY } = this.drawTransform
let { left, top, width, height } = node
let originLeft = left
let originTop = top
let originBottom = top + height
let originRight = left + width
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
return {
width,
height,
left,
top,
right,
bottom,
originLeft,
originTop,
originBottom,
originRight
}
}
// 节点由树转换成数组,从子节点到根节点
nodeTreeToList() {
const list = []
bfsWalk(this.mindMap.renderer.root, node => {
if (!list[node.layerIndex]) {
list[node.layerIndex] = []
}
list[node.layerIndex].push(node)
})
this.nodeList = list.reduceRight((res, cur) => {
return [...res, ...cur]
}, [])
}
}
Drag.instanceName = 'drag'
export default Drag

View File

@@ -1,96 +0,0 @@
import JsPDF from 'jspdf'
import { a4Size } from '../constants/constant'
// 导出PDF插件需要通过Export插件使用
class ExportPDF {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
}
// 导出为pdf
pdf(name, img, useMultiPageExport = false) {
if (useMultiPageExport) {
this.multiPageExport(name, img)
} else {
this.onePageExport(name, img)
}
}
// 单页导出
onePageExport(name, img) {
let pdf = new JsPDF('', 'pt', 'a4')
let a4Ratio = a4Size.width / a4Size.height
let image = new Image()
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
let imageRatio = imageWidth / imageHeight
let w, h
if (imageWidth <= a4Size.width && imageHeight <= a4Size.height) {
// 使用图片原始宽高
w = imageWidth
h = imageHeight
} else if (a4Ratio > imageRatio) {
// 以a4Height为高度缩放图片宽度
w = imageRatio * a4Size.height
h = a4Size.height
} else {
// 以a4Width为宽度缩放图片高度
w = a4Size.width
h = a4Size.width / imageRatio
}
pdf.addImage(img, 'PNG', (a4Size.width - w) / 2, (a4Size.height - h) / 2, w, h)
pdf.save(name)
}
image.src = img
}
// 多页导出
multiPageExport(name, img) {
let image = new Image()
image.onload = () => {
let imageWidth = image.width
let imageHeight = image.height
// 一页pdf显示高度
let pageHeight = (imageWidth / a4Size.width) * a4Size.height
// 未生成pdf的高度
let leftHeight = imageHeight
// 偏移
let position = 0
// a4纸的尺寸[595.28,841.89]图片在pdf中图片的宽高
let imgWidth = a4Size.width
let imgHeight = (a4Size.width / imageWidth) * imageHeight
let pdf = new JsPDF('', 'pt', 'a4')
// 有两个高度需要区分一个是图片的实际高度和生成pdf的页面高度(841.89)
// 当内容未超过pdf一页显示的范围无需分页
if (leftHeight < pageHeight) {
pdf.addImage(
img,
'PNG',
(a4Size.width - imgWidth) / 2,
(a4Size.height - imgHeight) / 2,
imgWidth,
imgHeight
)
} else {
// 分页
while (leftHeight > 0) {
pdf.addImage(img, 'PNG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight
position -= a4Size.height
// 避免添加空白页
if (leftHeight > 0) {
pdf.addPage()
}
}
}
pdf.save(name)
}
image.src = img
}
}
ExportPDF.instanceName = 'doExportPDF'
export default ExportPDF

View File

@@ -1,24 +0,0 @@
import xmind from '../parse/xmind'
// 导出XMind插件需要通过Export插件使用
class ExportXMind {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
}
// 导出xmind
async xmind(data, name) {
const zipData = await xmind.transformToXmind(data, name)
return zipData
}
// 获取解析器
getXmind() {
return xmind
}
}
ExportXMind.instanceName = 'doExportXMind'
export default ExportXMind

View File

@@ -1,275 +0,0 @@
// 节点图片大小调整插件
import { resizeImgSizeByOriginRatio } from '../utils/index'
import btnsSvg from '../svg/btns'
class NodeImgAdjust {
// 构造函数
constructor({ mindMap }) {
this.mindMap = mindMap
this.resizeBtnSize = 26 // 调整按钮的大小
this.handleEl = null // 自定义元素,用来渲染临时图片、调整按钮
this.isShowHandleEl = false // 自定义元素是否在显示中
this.node = null // 当前节点实例
this.img = null // 当前节点的图片节点
this.rect = null // 当前图片节点的尺寸信息
this.isMousedown = false // 当前是否是按住调整按钮状态
this.currentImgWidth = 0 // 当前拖拽实时图片的大小
this.currentImgHeight = 0
this.isAdjusted = false // 是否是拖拽结束后的渲染期间
this.bindEvent()
}
// 监听事件
bindEvent() {
this.onNodeImgMouseleave = this.onNodeImgMouseleave.bind(this)
this.onNodeImgMousemove = this.onNodeImgMousemove.bind(this)
this.onMousemove = this.onMousemove.bind(this)
this.onMouseup = this.onMouseup.bind(this)
this.onRenderEnd = this.onRenderEnd.bind(this)
this.mindMap.on('node_img_mouseleave', this.onNodeImgMouseleave)
this.mindMap.on('node_img_mousemove', this.onNodeImgMousemove)
this.mindMap.on('mousemove', this.onMousemove)
this.mindMap.on('mouseup', this.onMouseup)
this.mindMap.on('node_mouseup', this.onMouseup)
this.mindMap.on('node_tree_render_end', this.onRenderEnd)
}
// 解绑事件
unBindEvent() {
this.mindMap.off('node_img_mouseleave', this.onNodeImgMouseleave)
this.mindMap.off('node_img_mousemove', this.onNodeImgMousemove)
this.mindMap.off('mousemove', this.onMousemove)
this.mindMap.off('mouseup', this.onMouseup)
this.mindMap.off('node_mouseup', this.onMouseup)
this.mindMap.off('node_tree_render_end', this.onRenderEnd)
}
// 节点图片鼠标移动事件
onNodeImgMousemove(node, img) {
// 如果当前正在拖动调整中那么直接返回
if (this.isMousedown || this.isAdjusted || this.mindMap.opt.readonly) return
// 如果在当前节点内移动,以及自定义元素已经是显示状态,那么直接返回
if ((this.node && this.node.uid === node.uid) && this.isShowHandleEl) return
// 更新当前节点信息
this.node = node
this.img = img
this.rect = this.img.rbox()
// 显示自定义元素
this.showHandleEl()
}
// 节点图片鼠标移出事件
onNodeImgMouseleave() {
if (this.isMousedown) return
this.hideHandleEl()
}
// 隐藏节点实际的图片
hideNodeImage() {
if (!this.img) return
this.img.hide()
}
// 显示节点实际的图片
showNodeImage() {
if (!this.img) return
this.img.show()
}
// 显示自定义元素
showHandleEl() {
if (!this.handleEl) {
this.createResizeBtnEl()
}
this.setHandleElRect()
this.handleEl.style.display = 'block'
this.isShowHandleEl = true
}
// 隐藏自定义元素
hideHandleEl() {
if (!this.isShowHandleEl) return
this.isShowHandleEl = false
this.handleEl.style.display = 'none'
this.handleEl.style.backgroundImage = ``
this.handleEl.style.width = 0
this.handleEl.style.height = 0
this.handleEl.style.left = 0
this.handleEl.style.top = 0
}
// 设置自定义元素尺寸位置信息
setHandleElRect() {
let { width, height, x, y } = this.rect
this.handleEl.style.left = `${x}px`
this.handleEl.style.top = `${y}px`
this.currentImgWidth = width
this.currentImgHeight = height
this.updateHandleElSize()
}
// 更新自定义元素宽高
updateHandleElSize() {
this.handleEl.style.width = `${this.currentImgWidth}px`
this.handleEl.style.height = `${this.currentImgHeight}px`
}
// 创建调整按钮元素
createResizeBtnEl() {
// 容器元素
this.handleEl = document.createElement('div')
this.handleEl.style.cssText = `
pointer-events: none;
position: fixed;
display:none;
background-size: cover;
`
this.handleEl.className = 'node-img-handle'
// 调整按钮元素
const btnEl = document.createElement('div')
btnEl.innerHTML = btnsSvg.imgAdjust
btnEl.style.cssText = `
position: absolute;
right: 0;
bottom: 0;
pointer-events: auto;
background-color: rgba(0, 0, 0, 0.3);
width: ${this.resizeBtnSize}px;
height: ${this.resizeBtnSize}px;
display: flex;
justify-content: center;
align-items: center;
cursor: nwse-resize;
`
btnEl.className = 'node-image-resize'
// 给按钮元素绑定事件
btnEl.addEventListener('mouseenter', () => {
// 移入按钮,会触发节点图片的移出事件,所以需要再次显示按钮
this.showHandleEl()
})
btnEl.addEventListener('mouseleave', () => {
// 移除按钮,需要隐藏按钮
if (this.isMousedown) return
this.hideHandleEl()
})
btnEl.addEventListener('mousedown', e => {
e.stopPropagation()
this.onMousedown(e)
})
btnEl.addEventListener('mouseup', e => {
setTimeout(() => {
//点击后直接松开异常处理; 其他事件响应之后处理
this.hideHandleEl()
this.isAdjusted = false
}, 0)
})
btnEl.addEventListener('click', e => {
e.stopPropagation()
})
this.handleEl.appendChild(btnEl)
// 删除按钮
const btnRemove = document.createElement('div')
this.handleEl.prepend(btnRemove)
btnRemove.className = 'node-image-remove'
btnRemove.innerHTML = btnsSvg.remove
btnRemove.style.cssText = `
position: absolute;
right: 0;top:0;color:#fff;
pointer-events: auto;
background-color: rgba(0, 0, 0, 0.3);
width: ${this.resizeBtnSize}px;
height: ${this.resizeBtnSize}px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
`
btnRemove.addEventListener('mouseenter', e => {
this.showHandleEl()
})
btnRemove.addEventListener('mouseleave', e => {
if (this.isMousedown) return
this.hideHandleEl()
})
btnRemove.addEventListener('click', e => {
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null })
})
// 添加元素到页面
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.handleEl)
}
// 鼠标按钮按下事件
onMousedown() {
this.isMousedown = true
// 隐藏节点实际图片
this.hideNodeImage()
// 将节点图片渲染到自定义元素上
this.handleEl.style.backgroundImage = `url(${this.node.nodeData.data.image})`
}
// 鼠标移动
onMousemove(e) {
if (!this.isMousedown) return
e.preventDefault()
// 计算当前拖拽位置对应的图片的实时大小
let { width: imageOriginWidth, height: imageOriginHeight } =
this.node.nodeData.data.imageSize
let newWidth = e.clientX - this.rect.x
let newHeight = e.clientY - this.rect.y
if (newWidth <= 0 || newHeight <= 0) return
let [actWidth, actHeight] = resizeImgSizeByOriginRatio(
imageOriginWidth,
imageOriginHeight,
newWidth,
newHeight
)
this.currentImgWidth = actWidth
this.currentImgHeight = actHeight
this.updateHandleElSize()
}
// 鼠标松开
onMouseup() {
if (!this.isMousedown) return
// 显示节点实际图片
this.showNodeImage()
// 隐藏自定义元素
this.hideHandleEl()
// 更新节点图片为新的大小
let { image, imageTitle } = this.node.nodeData.data
let { scaleX, scaleY } = this.mindMap.draw.transform()
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, {
url: image,
title: imageTitle,
width: this.currentImgWidth / scaleX,
height: this.currentImgHeight / scaleY,
custom: true // 代表自定义了图片大小
})
this.isAdjusted = true
this.isMousedown = false
}
// 渲染完成事件
onRenderEnd() {
if (!this.isAdjusted) {
this.hideHandleEl()
return
}
this.isAdjusted = false
}
// 插件被移除前做的事情
beforePluginRemove() {
this.unBindEvent()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
NodeImgAdjust.instanceName = 'nodeImgAdjust'
export default NodeImgAdjust

View File

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

View File

@@ -1,258 +0,0 @@
import { throttle } from '../utils/index'
import { CONSTANTS } from '../constants/constant'
// 滚动条插件
class Scrollbar {
// 构造函数
constructor(opt) {
this.mindMap = opt.mindMap
this.scrollbarWrapSize = {
width: 0, // 水平滚动条的容器宽度
height: 0 // 垂直滚动条的容器高度
}
// 思维导图实际高度
this.chartHeight = 0
this.chartWidth = 0
this.reset()
this.bindEvent()
}
// 复位数据
reset() {
// 当前拖拽的滚动条类型
this.currentScrollType = ''
this.isMousedown = false
this.mousedownPos = {
x: 0,
y: 0
}
// 鼠标按下时,滚动条位置
this.mousedownScrollbarPos = 0
}
// 绑定事件
bindEvent() {
this.onMousemove = this.onMousemove.bind(this)
this.onMouseup = this.onMouseup.bind(this)
this.updateScrollbar = this.updateScrollbar.bind(this)
this.updateScrollbar = throttle(this.updateScrollbar, 16, this) // 加个节流
this.mindMap.on('mousemove', this.onMousemove)
this.mindMap.on('mouseup', this.onMouseup)
this.mindMap.on('node_tree_render_end', this.updateScrollbar)
this.mindMap.on('view_data_change', this.updateScrollbar)
}
// 解绑事件
unBindEvent() {
this.mindMap.off('mousemove', this.onMousemove)
this.mindMap.off('mouseup', this.onMouseup)
this.mindMap.off('node_tree_render_end', this.updateScrollbar)
this.mindMap.off('view_data_change', this.updateScrollbar)
}
// 渲染后、数据改变需要更新滚动条
updateScrollbar() {
// 当前正在拖拽滚动条时不需要更新
if (this.isMousedown) return
const res = this.calculationScrollbar()
this.emitEvent(res)
}
// 发送滚动条改变事件
emitEvent(data) {
this.mindMap.emit('scrollbar_change', data)
}
// 设置滚动条容器的大小,指滚动条容器的大小,对于水平滚动条,即宽度,对于垂直滚动条,即高度
setScrollBarWrapSize(width, height) {
this.scrollbarWrapSize.width = width
this.scrollbarWrapSize.height = height
}
// 计算滚动条大小和位置
calculationScrollbar() {
const rect = this.mindMap.draw.rbox()
// 减去画布距离浏览器窗口左上角的距离
const elRect = this.mindMap.elRect
rect.x -= elRect.left
rect.y -= elRect.top
// 垂直滚动条
const canvasHeight = this.mindMap.height // 画布高度
const paddingY = canvasHeight / 2 // 首尾允许超出的距离,默认为高度的一半
const chartHeight = rect.height + paddingY * 2 // 思维导图高度
this.chartHeight = chartHeight
const chartTop = rect.y - paddingY // 思维导图顶部距画布顶部的距离
const height = Math.min((canvasHeight / chartHeight) * 100, 100) // 滚动条高度 = 画布高度 / 思维导图高度
let top = (-chartTop / chartHeight) * 100 // 滚动条距离 = 思维导图顶部距画布顶部的距离 / 思维导图高度
// 判断是否到达边界
if (top < 0) {
top = 0
}
if (top > 100 - height) {
top = 100 - height
}
// 水平滚动条
const canvasWidth = this.mindMap.width
const paddingX = canvasWidth / 2
const chartWidth = rect.width + paddingX * 2
this.chartWidth = chartWidth
const chartLeft = rect.x - paddingX
const width = Math.min((canvasWidth / chartWidth) * 100, 100)
let left = (-chartLeft / chartWidth) * 100
if (left < 0) {
left = 0
}
if (left > 100 - width) {
left = 100 - width
}
const res = {
// 垂直滚动条
vertical: {
top,
height
},
// 水平滚动条
horizontal: {
left,
width
}
}
return res
}
// 滚动条鼠标按下事件处理函数
onMousedown(e, type) {
e.preventDefault()
e.stopPropagation()
this.currentScrollType = type
this.isMousedown = true
this.mousedownPos = {
x: e.clientX,
y: e.clientY
}
// 保存滚动条当前的位置
const styles = window.getComputedStyle(e.target)
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
this.mousedownScrollbarPos = Number.parseFloat(styles.top)
} else {
this.mousedownScrollbarPos = Number.parseFloat(styles.left)
}
}
// 鼠标移动事件处理函数
onMousemove(e) {
if (!this.isMousedown) {
return
}
e.preventDefault()
e.stopPropagation()
if (this.currentScrollType === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
const oy = e.clientY - this.mousedownPos.y + this.mousedownScrollbarPos
this.updateMindMapView(CONSTANTS.SCROLL_BAR_DIR.VERTICAL, oy)
} else {
const ox = e.clientX - this.mousedownPos.x + this.mousedownScrollbarPos
this.updateMindMapView(CONSTANTS.SCROLL_BAR_DIR.HORIZONTAL, ox)
}
}
// 鼠标松开事件处理函数
onMouseup() {
this.isMousedown = false
this.reset()
}
// 更新视图
updateMindMapView(type, offset) {
const scrollbarData = this.calculationScrollbar()
const t = this.mindMap.draw.transform()
const drawRect = this.mindMap.draw.rbox()
const rootRect = this.mindMap.renderer.root.group.rbox()
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
// 滚动条新位置
let oy = offset
// 判断是否达到首尾
if (oy <= 0) {
oy = 0
}
let max =
((100 - scrollbarData.vertical.height) / 100) *
this.scrollbarWrapSize.height
if (oy >= max) {
oy = max
}
// 转换成百分比
const oyPercentage = (oy / this.scrollbarWrapSize.height) * 100
// 转换成相对于图形高度的距离
const oyPx = (-oyPercentage / 100) * this.chartHeight
// 节点中心点到图形最上方的距离
const yOffset = rootRect.y - drawRect.y
// 内边距
const paddingY = this.mindMap.height / 2
// 图形新位置
let chartTop = oyPx + yOffset - paddingY * t.scaleY + paddingY
this.mindMap.view.translateYTo(chartTop)
this.emitEvent({
horizontal: scrollbarData.horizontal,
vertical: {
top: oyPercentage,
height: scrollbarData.vertical.height
}
})
} else {
// 滚动条新位置
let ox = offset
// 判断是否达到首尾
if (ox <= 0) {
ox = 0
}
let max =
((100 - scrollbarData.horizontal.width) / 100) *
this.scrollbarWrapSize.width
if (ox >= max) {
ox = max
}
// 转换成百分比
const oxPercentage = (ox / this.scrollbarWrapSize.width) * 100
// 转换成相对于图形高度的距离
const oxPx = (-oxPercentage / 100) * this.chartWidth
// 节点中心点到图形最左边的距离
const xOffset = rootRect.x - drawRect.x
// 内边距
const paddingX = this.mindMap.width / 2
// 图形新位置
let chartLeft = oxPx + xOffset - paddingX * t.scaleX + paddingX
this.mindMap.view.translateXTo(chartLeft)
this.emitEvent({
vertical: scrollbarData.vertical,
horizontal: {
left: oxPercentage,
width: scrollbarData.horizontal.width
}
})
}
}
// 滚动条的点击事件
onClick(e, type) {
let offset = 0
if (type === CONSTANTS.SCROLL_BAR_DIR.VERTICAL) {
offset = e.clientY - e.currentTarget.getBoundingClientRect().top
} else {
offset = e.clientX - e.currentTarget.getBoundingClientRect().left
}
this.updateMindMapView(type, offset)
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
Scrollbar.instanceName = 'scrollbar'
export default Scrollbar

View File

@@ -1,172 +0,0 @@
import { bfsWalk, getTextFromHtml, isUndef, replaceHtmlText } from '../utils/index'
// 搜索插件
class Search {
// 构造函数
constructor({ mindMap }) {
this.mindMap = mindMap
// 是否正在搜索
this.isSearching = false
// 搜索文本
this.searchText = ''
// 匹配的节点列表
this.matchNodeList = []
// 当前所在的节点列表索引
this.currentIndex = -1
// 不要复位搜索文本
this.notResetSearchText = false
// 是否自动跳转下一个匹配节点
this.isJumpNext = false
this.onDataChange = this.onDataChange.bind(this)
this.mindMap.on('data_change', this.onDataChange)
}
// 节点数据改变了,需要重新搜索
onDataChange() {
if (this.isJumpNext) {
this.isJumpNext = false
this.search(this.searchText)
return
}
if (this.notResetSearchText) {
this.notResetSearchText = false
return
}
this.searchText = ''
}
// 搜索
search(text, callback = () => {}) {
if (isUndef(text)) return this.endSearch()
text = String(text)
this.isSearching = true
if (this.searchText === text) {
// 和上一次搜索文本一样,那么搜索下一个
this.searchNext(callback)
} else {
// 和上次搜索文本不一样,那么重新开始
this.searchText = text
this.doSearch()
this.searchNext(callback)
}
this.emitEvent()
}
// 结束搜索
endSearch() {
if (!this.isSearching) return
this.searchText = ''
this.matchNodeList = []
this.currentIndex = -1
this.notResetSearchText = false
this.isSearching = false
this.emitEvent()
}
// 搜索匹配的节点
doSearch() {
this.matchNodeList = []
this.currentIndex = -1
bfsWalk(this.mindMap.renderer.root, node => {
let { richText, text } = node.nodeData.data
if (richText) {
text = getTextFromHtml(text)
}
if (text.includes(this.searchText)) {
this.matchNodeList.push(node)
}
})
}
// 搜索下一个,定位到下一个匹配节点
searchNext(callback) {
if (!this.isSearching || this.matchNodeList.length <= 0) return
if (this.currentIndex < this.matchNodeList.length - 1) {
this.currentIndex++
} else {
this.currentIndex = 0
}
let currentNode = this.matchNodeList[this.currentIndex]
this.notResetSearchText = true
this.mindMap.execCommand('GO_TARGET_NODE', currentNode, () => {
this.notResetSearchText = false
callback()
})
}
// 替换当前节点
replace(replaceText, jumpNext = false) {
if (
replaceText === null ||
replaceText === undefined ||
!this.isSearching ||
this.matchNodeList.length <= 0
)
return
// 自动跳转下一个匹配节点
this.isJumpNext = jumpNext
replaceText = String(replaceText)
let currentNode = this.matchNodeList[this.currentIndex]
if (!currentNode) return
let text = this.getReplacedText(currentNode, this.searchText, replaceText)
this.notResetSearchText = true
currentNode.setText(text, currentNode.nodeData.data.richText, true)
this.matchNodeList = this.matchNodeList.filter(node => {
return currentNode !== node
})
if (this.currentIndex > this.matchNodeList.length - 1) {
this.currentIndex = -1
} else {
this.currentIndex--
}
this.emitEvent()
}
// 替换所有
replaceAll(replaceText) {
if (
replaceText === null ||
replaceText === undefined ||
!this.isSearching ||
this.matchNodeList.length <= 0
)
return
replaceText = String(replaceText)
this.matchNodeList.forEach(node => {
let text = this.getReplacedText(node, this.searchText, replaceText)
this.mindMap.renderer.setNodeDataRender(
node,
{
text,
resetRichText: !!node.nodeData.data.richText
},
true
)
})
this.mindMap.render()
this.mindMap.command.addHistory()
this.endSearch()
}
// 获取某个节点替换后的文本
getReplacedText(node, searchText, replaceText) {
let { richText, text } = node.nodeData.data
if (richText) {
return replaceHtmlText(text, searchText, replaceText)
} else {
return text.replaceAll(searchText, replaceText)
}
}
// 发送事件
emitEvent() {
this.mindMap.emit('search_info_change', {
currentIndex: this.currentIndex,
total: this.matchNodeList.length
})
}
}
Search.instanceName = 'search'
export default Search

View File

@@ -1,160 +0,0 @@
// 手势事件支持插件
class TouchEvent {
// 构造函数
constructor({ mindMap }) {
this.mindMap = mindMap
this.touchesNum = 0
this.singleTouchstartEvent = null
this.clickNum = 0
this.touchStartScaleView = null
this.bindEvent()
}
// 绑定事件
bindEvent() {
this.onTouchstart = this.onTouchstart.bind(this)
this.onTouchmove = this.onTouchmove.bind(this)
this.onTouchcancel = this.onTouchcancel.bind(this)
this.onTouchend = this.onTouchend.bind(this)
window.addEventListener('touchstart', this.onTouchstart)
window.addEventListener('touchmove', this.onTouchmove)
window.addEventListener('touchcancel', this.onTouchcancel)
window.addEventListener('touchend', this.onTouchend)
}
// 解绑事件
unBindEvent() {
window.removeEventListener('touchstart', this.onTouchstart)
window.removeEventListener('touchmove', this.onTouchmove)
window.removeEventListener('touchcancel', this.onTouchcancel)
window.removeEventListener('touchend', this.onTouchend)
}
// 手指按下事件
onTouchstart(e) {
this.touchesNum = e.touches.length
this.touchStartScaleView = null
if (this.touchesNum === 1) {
let touch = e.touches[0]
this.singleTouchstartEvent = touch
this.dispatchMouseEvent('mousedown', touch.target, touch)
}
}
// 手指移动事件
onTouchmove(e) {
let len = e.touches.length
if (len === 1) {
let touch = e.touches[0]
this.dispatchMouseEvent('mousemove', touch.target, touch)
} else if (len === 2) {
let touch1 = e.touches[0]
let touch2 = e.touches[1]
let ox = touch1.clientX - touch2.clientX
let oy = touch1.clientY - touch2.clientY
let distance = Math.sqrt(Math.pow(ox, 2) + Math.pow(oy, 2))
// 以两指中心点进行缩放
let { x: touch1ClientX, y: touch1ClientY } = this.mindMap.toPos(
touch1.clientX,
touch1.clientY
)
let { x: touch2ClientX, y: touch2ClientY } = this.mindMap.toPos(
touch2.clientX,
touch2.clientY
)
let cx = (touch1ClientX + touch2ClientX) / 2
let cy = (touch1ClientY + touch2ClientY) / 2
// 手势缩放,基于最开始的位置进行缩放(基于前一个位置缩放不是线性关系); 缩放同时支持位置拖动
const view = this.mindMap.view
if (!this.touchStartScaleView) {
this.touchStartScaleView = {
distance: distance,
scale: view.scale,
x: view.x,
y: view.y,
cx: cx,
cy: cy
}
return
}
const viewBefore = this.touchStartScaleView
let scale = viewBefore.scale * (distance / viewBefore.distance)
if (Math.abs(distance - viewBefore.distance) <= 10) {
scale = viewBefore.scale
}
const ratio = 1 - scale / viewBefore.scale
view.scale = scale < 0.1 ? 0.1 : scale
view.x =
viewBefore.x +
(cx - viewBefore.x) * ratio +
(cx - viewBefore.cx) * scale
view.y =
viewBefore.y +
(cy - viewBefore.y) * ratio +
(cy - viewBefore.cy) * scale
view.transform()
this.mindMap.emit('scale', scale)
}
}
// 手指取消事件
onTouchcancel(e) {}
// 手指松开事件
onTouchend(e) {
this.dispatchMouseEvent('mouseup', e.target)
if (this.touchesNum === 1) {
// 模拟双击事件
this.clickNum++
setTimeout(() => {
this.clickNum = 0
}, 300)
let ev = this.singleTouchstartEvent
if (this.clickNum > 1) {
this.clickNum = 0
this.dispatchMouseEvent('dblclick', ev.target, ev)
} else {
// 点击事件应该不用模拟
// this.dispatchMouseEvent('click', ev.target, ev)
}
}
this.touchesNum = 0
this.singleTouchstartEvent = null
this.touchStartScaleView = null
}
// 发送鼠标事件
dispatchMouseEvent(eventName, target, e) {
let opt = {}
if (e) {
opt = {
screenX: e.screenX,
screenY: e.screenY,
clientX: e.clientX,
clientY: e.clientY,
which: 1
}
}
let event = new MouseEvent(eventName, {
view: window,
bubbles: true,
cancelable: true,
...opt
})
target.dispatchEvent(event)
}
// 插件被移除前做的事情
beforePluginRemove() {
this.unBindEvent()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
TouchEvent.instanceName = 'touchEvent'
export default TouchEvent

View File

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

View File

@@ -18,7 +18,11 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '#e68112',
borderWidth: 0,
fontSize: 24
fontSize: 24,
active: {
borderColor: '#b0bc47',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -26,12 +30,18 @@ export default merge(defaultTheme, {
color: '#8c5416',
borderColor: '#b0bc47',
borderWidth: 2,
fontSize: 18
fontSize: 18,
active: {
borderColor: '#e68112'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#8c5416'
color: '#8c5416',
active: {
borderColor: '#b0bc47'
}
},
// 概要节点样式
generalization: {
@@ -39,6 +49,9 @@ export default merge(defaultTheme, {
fillColor: '#ffd683',
borderColor: '#b0bc47',
borderWidth: 2,
color: '#8c5416'
color: '#8c5416',
active: {
borderColor: '#e68112'
}
}
})

View File

@@ -18,7 +18,11 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '#94c143',
borderWidth: 0,
fontSize: 24
fontSize: 24,
active: {
borderColor: '#749336',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -26,12 +30,18 @@ export default merge(defaultTheme, {
color: '#749336',
borderColor: '#aec668',
borderWidth: 2,
fontSize: 18
fontSize: 18,
active: {
borderColor: '#749336'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#749336'
color: '#749336',
active: {
borderColor: '#749336'
}
},
// 概要节点样式
generalization: {
@@ -39,6 +49,9 @@ export default merge(defaultTheme, {
fillColor: '#cee498',
borderColor: '#aec668',
borderWidth: 2,
color: '#749336'
color: '#749336',
active: {
borderColor: '#749336'
}
}
})

View File

@@ -18,7 +18,11 @@ export default merge(defaultTheme, {
color: 'rgb(111, 61, 6)',
borderColor: '',
borderWidth: 0,
fontSize: 24
fontSize: 24,
active: {
borderColor: '#fff',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -26,12 +30,18 @@ export default merge(defaultTheme, {
color: 'rgb(225, 201, 158)',
borderColor: 'rgb(245, 224, 191)',
borderWidth: 2,
fontSize: 18
fontSize: 18,
active: {
borderColor: 'rgb(255, 208, 124)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(231, 203, 155)'
color: 'rgb(231, 203, 155)',
active: {
borderColor: 'rgb(255, 208, 124)'
}
},
// 概要节点样式
generalization: {
@@ -39,6 +49,9 @@ export default merge(defaultTheme, {
fillColor: 'rgb(56, 45, 34)',
borderColor: 'rgb(104, 84, 61)',
borderWidth: 2,
color: 'rgb(242, 216, 176)'
color: 'rgb(242, 216, 176)',
active: {
borderColor: 'rgb(255, 208, 124)'
}
}
})

View File

@@ -18,7 +18,11 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24
fontSize: 24,
active: {
borderColor: 'rgb(254, 199, 13)',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -26,12 +30,19 @@ export default merge(defaultTheme, {
color: 'rgb(0, 0, 0)',
borderColor: '',
borderWidth: 0,
fontSize: 18
fontSize: 18,
active: {
borderColor: 'rgb(36, 179, 96)',
borderWidth: 3
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(204, 204, 204)'
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(254, 199, 13)'
}
},
// 概要节点样式
generalization: {
@@ -39,6 +50,9 @@ export default merge(defaultTheme, {
fillColor: 'rgb(27, 31, 34)',
borderColor: 'rgb(255, 119, 34)',
borderWidth: 2,
color: 'rgb(204, 204, 204)'
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(36, 179, 96)'
}
}
})

View File

@@ -13,7 +13,10 @@ export default merge(defaultTheme, {
generalizationLineColor: '#333',
// 根节点样式
root: {
fillColor: 'rgb(115, 161, 191)'
fillColor: 'rgb(115, 161, 191)',
active: {
borderColor: 'rgb(57, 80, 96)'
}
},
// 二级节点样式
second: {
@@ -21,17 +24,26 @@ export default merge(defaultTheme, {
color: '#333',
borderColor: 'rgb(115, 161, 191)',
borderWidth: 1,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(57, 80, 96)'
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(57, 80, 96)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(57, 80, 96)'
}
}
})

View File

@@ -13,7 +13,10 @@ export default merge(defaultTheme, {
generalizationLineColor: '#333',
// 根节点样式
root: {
fillColor: 'rgb(191, 115, 148)'
fillColor: 'rgb(191, 115, 148)',
active: {
borderColor: 'rgb(96, 57, 74)'
}
},
// 二级节点样式
second: {
@@ -21,17 +24,26 @@ export default merge(defaultTheme, {
color: '#333',
borderColor: 'rgb(191, 115, 148)',
borderWidth: 1,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(96, 57, 74)'
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(96, 57, 74)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(96, 57, 74)'
}
}
})

View File

@@ -18,13 +18,16 @@ export default merge(defaultTheme, {
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=',
// 背景重复
backgroundRepeat: 'repeat',
backgroundSize: 'auto',
// 根节点样式
root: {
fillColor: 'rgb(233, 223, 152)',
color: '#333',
fontSize: 24,
borderRadius: 21
borderRadius: 21,
active: {
fillColor: 'rgb(254, 219, 0)',
borderColor: 'transparent'
}
},
// 二级节点样式
second: {
@@ -32,18 +35,30 @@ export default merge(defaultTheme, {
borderColor: 'transparent',
color: '#333',
fontSize: 16,
borderRadius: 10
borderRadius: 10,
active: {
fillColor: 'rgb(254, 219, 0)',
borderColor: 'transparent'
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#fff',
fontWeight: 'bold'
fontWeight: 'bold',
active: {
fillColor: 'rgb(254, 219, 0)',
borderColor: 'transparent'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'transparent',
color: '#333'
color: '#333',
active: {
fillColor: 'rgb(254, 219, 0)',
borderColor: 'transparent'
}
}
})

View File

@@ -18,7 +18,10 @@ export default merge(defaultTheme, {
fillColor: 'rgb(18, 187, 55)',
color: '#fff',
fontSize: 24,
borderRadius: 10
borderRadius: 10,
active: {
borderColor: 'rgb(51, 51, 51)'
}
},
// 二级节点样式
second: {
@@ -26,18 +29,27 @@ export default merge(defaultTheme, {
borderColor: 'transparent',
color: '#1a1a1a',
fontSize: 18,
borderRadius: 10
borderRadius: 10,
active: {
borderColor: 'rgb(51, 51, 51)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#1a1a1a'
color: '#1a1a1a',
active: {
borderColor: 'rgb(51, 51, 51)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'rgb(51, 51, 51)',
borderWidth: 2,
color: '#1a1a1a'
color: '#1a1a1a',
active: {
borderColor: 'rgb(18, 187, 55)'
}
}
})

View File

@@ -20,7 +20,10 @@ export default merge(defaultTheme, {
fontSize: 24,
borderRadius: 10,
borderColor: 'rgb(249, 199, 84)',
borderWidth: 1
borderWidth: 1,
active: {
borderColor: 'rgb(94, 202, 110)'
}
},
// 二级节点样式
second: {
@@ -29,18 +32,27 @@ export default merge(defaultTheme, {
borderWidth: 1,
color: '#1a1a1a',
fontSize: 18,
borderRadius: 10
borderRadius: 10,
active: {
borderColor: 'rgb(94, 202, 110)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#1a1a1a'
color: '#1a1a1a',
active: {
borderColor: 'rgb(94, 202, 110)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#1a1a1a',
color: '#1a1a1a',
borderWidth: 2
borderWidth: 2,
active: {
borderColor: 'rgb(94, 202, 110)'
}
}
})

View File

@@ -20,7 +20,10 @@ export default merge(defaultTheme, {
fontSize: 24,
borderRadius: 10,
borderColor: 'rgb(189, 197, 201)',
borderWidth: 2
borderWidth: 2,
active: {
borderColor: 'rgb(169, 218, 218)'
}
},
// 二级节点样式
second: {
@@ -29,7 +32,10 @@ export default merge(defaultTheme, {
borderWidth: 2,
color: '#fff',
fontSize: 18,
borderRadius: 10
borderRadius: 10,
active: {
borderColor: 'rgb(56, 123, 233)'
}
},
// 三级及以下节点样式
node: {
@@ -37,13 +43,19 @@ export default merge(defaultTheme, {
color: 'rgb(30, 53, 86)',
borderColor: 'rgb(30, 53, 86)',
borderWidth: 1,
marginY: 20
marginY: 20,
active: {
borderColor: 'rgb(169, 218, 218)'
}
},
// 概要节点样式
generalization: {
fillColor: 'rgb(56, 123, 233)',
borderColor: 'rgb(56, 123, 233)',
color: '#fff',
borderWidth: 0
borderWidth: 0,
active: {
borderColor: 'rgb(169, 218, 218)'
}
}
})

View File

@@ -16,7 +16,10 @@ export default merge(defaultTheme, {
// 根节点样式
root: {
fillColor: 'rgb(255, 255, 255)',
color: '#222'
color: '#222',
active: {
borderColor: 'rgb(94, 199, 248)'
}
},
// 二级节点样式
second: {
@@ -24,17 +27,26 @@ export default merge(defaultTheme, {
color: '#222',
borderColor: 'rgb(255, 255, 255)',
borderWidth: 1,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(94, 199, 248)'
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(94, 199, 248)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'rgb(51, 51, 51)',
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(94, 199, 248)'
}
}
})

View File

@@ -14,7 +14,10 @@ export default merge(defaultTheme, {
// 根节点样式
root: {
fillColor: 'rgb(253, 244, 217)',
color: '#222'
color: '#222',
active: {
borderColor: 'rgb(94, 199, 248)'
}
},
// 二级节点样式
second: {
@@ -22,18 +25,27 @@ export default merge(defaultTheme, {
color: '#222',
borderColor: 'rgb(242, 200, 104)',
borderWidth: 1,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(94, 199, 248)'
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(94, 199, 248)'
}
},
// 概要节点样式
generalization: {
fillColor: 'rgb(123, 199, 120)',
borderColor: 'transparent',
borderWidth: 2,
color: '#fff'
color: '#fff',
active: {
borderColor: 'rgb(94, 199, 248)'
}
}
})

View File

@@ -16,7 +16,11 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24
fontSize: 24,
active: {
borderColor: 'rgb(173, 123, 91)',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -24,12 +28,18 @@ export default merge(defaultTheme, {
color: 'rgb(125, 86, 42)',
borderColor: '',
borderWidth: 0,
fontSize: 18
fontSize: 18,
active: {
borderColor: 'rgb(173, 123, 91)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(96, 71, 47)'
color: 'rgb(96, 71, 47)',
active: {
borderColor: 'rgb(173, 123, 91)'
}
},
// 概要节点样式
generalization: {
@@ -37,6 +47,9 @@ export default merge(defaultTheme, {
fillColor: 'rgb(255, 249, 239)',
borderColor: 'rgb(173, 123, 91)',
borderWidth: 2,
color: 'rgb(122, 83, 44)'
color: 'rgb(122, 83, 44)',
active: {
borderColor: 'rgb(202, 117, 79)'
}
}
})

View File

@@ -16,7 +16,11 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 24
fontSize: 24,
active: {
borderColor: 'rgb(173, 91, 12)',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -24,12 +28,18 @@ export default merge(defaultTheme, {
color: 'rgb(50, 113, 96)',
borderColor: 'rgb(113, 195, 169)',
borderWidth: 2,
fontSize: 18
fontSize: 18,
active: {
borderColor: 'rgb(173, 91, 12)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(10, 59, 43)'
color: 'rgb(10, 59, 43)',
active: {
borderColor: 'rgb(173, 91, 12)'
}
},
// 概要节点样式
generalization: {
@@ -37,6 +47,9 @@ export default merge(defaultTheme, {
fillColor: 'rgb(246, 238, 211)',
borderColor: '',
borderWidth: 0,
color: 'rgb(173, 91, 12)'
color: 'rgb(173, 91, 12)',
active: {
borderColor: 'rgb(113, 195, 169)'
}
}
})

View File

@@ -18,7 +18,10 @@ export default merge(defaultTheme, {
fillColor: 'rgb(28, 178, 43)',
color: '#fff',
fontSize: 24,
borderRadius: 10
borderRadius: 10,
active: {
borderColor: 'rgb(17, 68, 23)'
}
},
// 二级节点样式
second: {
@@ -26,17 +29,26 @@ export default merge(defaultTheme, {
color: 'rgb(147,148,149)',
fontSize: 18,
borderRadius: 10,
borderWidth: 0
borderWidth: 0,
active: {
borderColor: 'rgb(17, 68, 23)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(147, 148, 149)'
color: 'rgb(147, 148, 149)',
active: {
borderColor: 'rgb(17, 68, 23)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'transparent',
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(17, 68, 23)'
}
}
})

View File

@@ -17,7 +17,11 @@ export default merge(defaultTheme, {
fillColor: 'rgb(36, 179, 96)',
color: '#fff',
borderColor: '',
borderWidth: 0
borderWidth: 0,
active: {
borderColor: 'rgb(254, 199, 13)',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -25,18 +29,28 @@ export default merge(defaultTheme, {
color: 'rgb(0, 0, 0)',
borderColor: '',
borderWidth: 0,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(36, 179, 96)',
borderWidth: 2
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: 'rgb(204, 204, 204)'
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(254, 199, 13)'
}
},
// 概要节点样式
generalization: {
fillColor: 'transparent',
borderColor: 'rgb(255, 119, 34)',
borderWidth: 2,
color: 'rgb(204, 204, 204)'
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(254, 199, 13)'
}
}
})

View File

@@ -18,8 +18,6 @@ export default {
lineDasharray: 'none',
// 连线风格
lineStyle: 'straight', // 针对logicalStructure、mindMap两种结构。曲线curve、直线straight、直连direct
// 曲线连接时,根节点和其他节点的连接线样式保持统一,默认根节点为 ( 型,其他节点为 { 型设为true后都为 { 型
rootLineKeepSameInCurve: true,
// 概要连线的粗细
generalizationLineWidth: 1,
// 概要连线的颜色
@@ -70,7 +68,12 @@ export default {
borderWidth: 0,
borderDasharray: 'none',
borderRadius: 5,
textDecoration: 'none'
textDecoration: 'none',
active: {
borderColor: 'rgb(57, 80, 96)',
borderWidth: 3,
borderDasharray: 'none'
}
},
// 二级节点样式
second: {
@@ -88,7 +91,12 @@ export default {
borderWidth: 1,
borderDasharray: 'none',
borderRadius: 5,
textDecoration: 'none'
textDecoration: 'none',
active: {
borderColor: 'rgb(57, 80, 96)',
borderWidth: 3,
borderDasharray: 'none'
}
},
// 三级及以下节点样式
node: {
@@ -106,7 +114,12 @@ export default {
borderWidth: 0,
borderRadius: 5,
borderDasharray: 'none',
textDecoration: 'none'
textDecoration: 'none',
active: {
borderColor: 'rgb(57, 80, 96)',
borderWidth: 3,
borderDasharray: 'none'
}
},
// 概要节点样式
generalization: {
@@ -124,7 +137,12 @@ export default {
borderWidth: 1,
borderDasharray: 'none',
borderRadius: 5,
textDecoration: 'none'
textDecoration: 'none',
active: {
borderColor: 'rgb(57, 80, 96)',
borderWidth: 3,
borderDasharray: 'none'
}
}
}
@@ -158,8 +176,7 @@ const nodeSizeIndependenceList = [
'backgroundImage',
'backgroundRepeat',
'backgroundPosition',
'backgroundSize',
'rootLineKeepSameInCurve'
'backgroundSize'
]
export const checkIsNodeSizeIndependenceConfig = (config) => {
let keys = Object.keys(config)

View File

@@ -13,7 +13,10 @@ export default merge(defaultTheme, {
generalizationLineColor: '#333',
// 根节点样式
root: {
fillColor: 'rgb(191, 147, 115)'
fillColor: 'rgb(191, 147, 115)',
active: {
borderColor: 'rgb(96, 73, 57)'
}
},
// 二级节点样式
second: {
@@ -21,17 +24,26 @@ export default merge(defaultTheme, {
color: '#333',
borderColor: 'rgb(191, 147, 115)',
borderWidth: 1,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(96, 73, 57)'
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(96, 73, 57)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(96, 73, 57)'
}
}
})

View File

@@ -26,6 +26,11 @@ export default merge(defaultTheme, {
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(57, 80, 96)',
borderWidth: 3,
borderDasharray: 'none'
}
}
})

View File

@@ -13,7 +13,10 @@ export default merge(defaultTheme, {
generalizationLineColor: '#333',
// 根节点样式
root: {
fillColor: 'rgb(191, 115, 115)'
fillColor: 'rgb(191, 115, 115)',
active: {
borderColor: 'rgb(96, 57, 57)'
}
},
// 二级节点样式
second: {
@@ -21,17 +24,26 @@ export default merge(defaultTheme, {
color: '#333',
borderColor: 'rgb(191, 115, 115)',
borderWidth: 1,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(96, 57, 57)'
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(96, 57, 57)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(96, 57, 57)'
}
}
})

View File

@@ -17,7 +17,11 @@ export default merge(defaultTheme, {
fillColor: 'rgb(51, 56, 62)',
color: 'rgb(247, 208, 160)',
borderColor: '',
borderWidth: 0
borderWidth: 0,
active: {
borderColor: 'rgb(247, 208, 160)',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -25,17 +29,27 @@ export default merge(defaultTheme, {
color: 'rgb(81, 58, 42)',
borderColor: '',
borderWidth: 0,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(51, 56, 62)',
borderWidth: 2
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222'
color: '#222',
active: {
borderColor: 'rgb(0, 192, 184)'
}
},
// 概要节点样式
generalization: {
fillColor: 'rgb(127, 93, 64)',
borderColor: 'transparent',
color: 'rgb(255, 214, 175)'
color: 'rgb(255, 214, 175)',
active: {
borderColor: 'rgb(51, 56, 62)'
}
}
})

View File

@@ -17,7 +17,11 @@ export default merge(defaultTheme, {
fillColor: 'rgb(25, 193, 73)',
color: '#fff',
borderColor: '',
borderWidth: 0
borderWidth: 0,
active: {
borderColor: '#222',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -25,18 +29,28 @@ export default merge(defaultTheme, {
color: 'rgb(69, 149, 96)',
borderColor: '',
borderWidth: 0,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(25, 193, 73)',
borderWidth: 2
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222'
color: '#222',
active: {
borderColor: 'rgb(25, 193, 73)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'rgb(251, 158, 0)',
borderWidth: 2,
color: 'rgb(51, 51, 51)'
color: 'rgb(51, 51, 51)',
active: {
borderColor: 'rgb(25, 193, 73)'
}
}
})

View File

@@ -18,7 +18,11 @@ export default merge(defaultTheme, {
color: 'rgb(255, 255, 255)',
borderColor: '',
borderWidth: 0,
fontSize: 24
fontSize: 24,
active: {
borderColor: 'rgb(255, 119, 34)',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -26,12 +30,19 @@ export default merge(defaultTheme, {
color: 'rgb(209, 210, 210)',
borderColor: '',
borderWidth: 0,
fontSize: 18
fontSize: 18,
active: {
borderColor: 'rgb(255, 119, 34)',
borderWidth: 3
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(204, 204, 204)'
color: 'rgb(204, 204, 204)',
active: {
borderColor: 'rgb(255, 119, 34)'
}
},
// 概要节点样式
generalization: {
@@ -39,6 +50,9 @@ export default merge(defaultTheme, {
fillColor: 'rgb(255, 119, 34)',
borderColor: '',
borderWidth: 2,
color: '#fff'
color: '#fff',
active: {
borderColor: 'rgb(23, 153, 243)'
}
}
})

View File

@@ -16,7 +16,10 @@ export default merge(defaultTheme, {
root: {
fillColor: 'rgb(55, 165, 255)',
borderColor: 'rgb(51, 51, 51)',
borderWidth: 3
borderWidth: 3,
active: {
borderColor: 'rgb(255, 160, 36)'
}
},
// 二级节点样式
second: {
@@ -24,17 +27,26 @@ export default merge(defaultTheme, {
color: '#222',
borderColor: 'rgb(51, 51, 51)',
borderWidth: 3,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(55, 165, 255)'
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222'
color: '#222',
active: {
borderColor: 'rgb(55, 165, 255)'
}
},
// 概要节点样式
generalization: {
borderColor: '#222',
borderWidth: 3,
color: '#222'
color: '#222',
active: {
borderColor: 'rgb(55, 165, 255)'
}
}
})

View File

@@ -16,7 +16,11 @@ export default merge(defaultTheme, {
root: {
fillColor: 'rgb(0, 192, 184)',
borderColor: '',
borderWidth: 0
borderWidth: 0,
active: {
borderColor: 'rgb(255, 160, 36)',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -24,17 +28,26 @@ export default merge(defaultTheme, {
color: '#222',
borderColor: 'rgb(184, 235, 233)',
borderWidth: 2,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(0, 192, 184)'
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222'
color: '#222',
active: {
borderColor: 'rgb(0, 192, 184)'
}
},
// 概要节点样式
generalization: {
fillColor: 'rgb(90, 206, 241)',
borderColor: 'transparent',
color: '#fff'
color: '#fff',
active: {
borderColor: 'rgb(0, 192, 184)'
}
}
})

View File

@@ -18,7 +18,11 @@ export default merge(defaultTheme, {
color: '#110501',
borderColor: '#ff6811',
borderWidth: 0,
fontSize: 24
fontSize: 24,
active: {
borderColor: '#a9a4a9',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -26,12 +30,18 @@ export default merge(defaultTheme, {
color: '#a9a4a9',
borderColor: '#ff6811',
borderWidth: 2,
fontSize: 18
fontSize: 18,
active: {
borderColor: '#110501'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#a9a4a9'
color: '#a9a4a9',
active: {
borderColor: '#ff6811'
}
},
// 概要节点样式
generalization: {
@@ -39,6 +49,9 @@ export default merge(defaultTheme, {
fillColor: '',
borderColor: '#ff6811',
borderWidth: 2,
color: '#a9a4a9'
color: '#a9a4a9',
active: {
borderColor: '#110501'
}
}
})

View File

@@ -16,7 +16,11 @@ export default merge(defaultTheme, {
root: {
fillColor: 'rgb(139, 109, 225)',
borderColor: '',
borderWidth: 0
borderWidth: 0,
active: {
borderColor: 'rgb(243, 104, 138)',
borderWidth: 2
}
},
// 二级节点样式
second: {
@@ -24,17 +28,28 @@ export default merge(defaultTheme, {
color: '#fff',
borderColor: '',
borderWidth: 0,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(139, 109, 225)',
borderWidth: 2
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222'
color: '#222',
active: {
borderColor: 'rgb(139, 109, 225)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'transparent',
color: '#222'
color: '#222',
active: {
borderColor: 'rgb(139, 109, 225)',
borderWidth: 2
}
}
})

View File

@@ -18,7 +18,11 @@ export default merge(defaultTheme, {
color: 'rgb(255, 233, 157)',
borderColor: '',
borderWidth: 0,
fontSize: 24
fontSize: 24,
active: {
borderColor: 'rgb(255, 233, 157)',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -26,12 +30,18 @@ export default merge(defaultTheme, {
color: 'rgb(211, 58, 21)',
borderColor: 'rgb(222, 101, 85)',
borderWidth: 2,
fontSize: 18
fontSize: 18,
active: {
borderColor: 'rgb(255, 233, 157)'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(144, 71, 43)'
color: 'rgb(144, 71, 43)',
active: {
borderColor: 'rgb(255, 233, 157)'
}
},
// 概要节点样式
generalization: {
@@ -39,6 +49,9 @@ export default merge(defaultTheme, {
fillColor: 'rgb(255, 247, 211)',
borderColor: 'rgb(255, 202, 162)',
borderWidth: 2,
color: 'rgb(187, 101, 69)'
color: 'rgb(187, 101, 69)',
active: {
borderColor: 'rgb(222, 101, 85)'
}
}
})

View File

@@ -13,7 +13,10 @@ export default merge(defaultTheme, {
generalizationLineColor: '#333',
// 根节点样式
root: {
fillColor: 'rgb(123, 115, 191)'
fillColor: 'rgb(123, 115, 191)',
active: {
borderColor: 'rgb(61, 57, 96)'
}
},
// 二级节点样式
second: {
@@ -21,17 +24,26 @@ export default merge(defaultTheme, {
color: '#333',
borderColor: 'rgb(123, 115, 191)',
borderWidth: 1,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(61, 57, 96)'
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(61, 57, 96)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: '#333',
color: '#333'
color: '#333',
active: {
borderColor: 'rgb(61, 57, 96)'
}
}
})

View File

@@ -16,7 +16,10 @@ export default merge(defaultTheme, {
color: 'rgb(34, 34, 34)',
borderColor: 'rgb(34, 34, 34)',
borderWidth: 3,
fontSize: 24
fontSize: 24,
active: {
borderColor: '#a13600'
}
},
// 二级节点样式
second: {
@@ -24,12 +27,18 @@ export default merge(defaultTheme, {
color: 'rgb(34, 34, 34)',
borderColor: 'rgb(34, 34, 34)',
borderWidth: 3,
fontSize: 18
fontSize: 18,
active: {
borderColor: '#a13600'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: 'rgb(34, 34, 34)'
color: 'rgb(34, 34, 34)',
active: {
borderColor: '#a13600'
}
},
// 概要节点样式
generalization: {
@@ -37,6 +46,9 @@ export default merge(defaultTheme, {
fillColor: 'transparent',
borderColor: 'rgb(34, 34, 34)',
borderWidth: 2,
color: 'rgb(34, 34, 34)'
color: 'rgb(34, 34, 34)',
active: {
borderColor: '#a13600'
}
}
})

View File

@@ -17,7 +17,11 @@ export default merge(defaultTheme, {
fillColor: '#fff',
borderColor: '',
borderWidth: 0,
color: 'rgb(65, 89, 158)'
color: 'rgb(65, 89, 158)',
active: {
borderColor: 'rgb(251, 227, 188)',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -25,17 +29,27 @@ export default merge(defaultTheme, {
color: 'rgb(65, 89, 158)',
borderColor: '',
borderWidth: 0,
fontSize: 14
fontSize: 14,
active: {
borderColor: '#fff',
borderWidth: 2
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: 'rgb(65, 89, 158)'
color: 'rgb(65, 89, 158)',
active: {
borderColor: 'rgb(251, 227, 188)'
}
},
// 概要节点样式
generalization: {
fillColor: '#fff',
borderColor: 'transparent',
color: 'rgb(65, 89, 158)'
color: 'rgb(65, 89, 158)',
active: {
borderColor: 'rgb(251, 227, 188)'
}
}
})

View File

@@ -17,7 +17,11 @@ export default merge(defaultTheme, {
fillColor: 'rgb(255, 112, 52)',
color: '#fff',
borderColor: '',
borderWidth: 0
borderWidth: 0,
active: {
borderColor: 'rgb(51, 51, 51)',
borderWidth: 3
}
},
// 二级节点样式
second: {
@@ -25,17 +29,27 @@ export default merge(defaultTheme, {
color: 'rgb(51, 51, 51)',
borderColor: '',
borderWidth: 0,
fontSize: 14
fontSize: 14,
active: {
borderColor: 'rgb(255, 112, 52)',
borderWidth: 2
}
},
// 三级及以下节点样式
node: {
fontSize: 12,
color: '#222'
color: '#222',
active: {
borderColor: 'rgb(255, 112, 52)'
}
},
// 概要节点样式
generalization: {
fillColor: 'rgb(255, 222, 69)',
borderColor: 'transparent',
color: 'rgb(51, 51, 51)'
color: 'rgb(51, 51, 51)',
active: {
borderColor: 'rgb(255, 112, 52)'
}
}
})

View File

@@ -1,7 +1,7 @@
import {
getAssociativeLineTargetIndex,
joinCubicBezierPath,
getNodePoint,
computeNodePoints,
getDefaultControlPointOffsets
} from './associativeLineUtils'
@@ -61,22 +61,15 @@ function onControlPointMousemove(e) {
}
// 更新当前拖拽的控制点的位置
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
let [, , , node, toNode] = this.activeLine
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
let { associativeLinePoint, associativeLineTargetControlOffsets } =
node.nodeData.data
associativeLinePoint = associativeLinePoint || []
const nodePos = this.getNodePos(node)
const toNodePos = this.getNodePos(toNode)
let [startPoint, endPoint] = this.updateAllLinesPos(
node,
toNode,
associativeLinePoint[targetIndex]
)
let [path, clickPath, text, node, toNode] = this.activeLine
let [startPoint, endPoint] = computeNodePoints(node, toNode)
this.controlPointMousemoveState.startPoint = startPoint
this.controlPointMousemoveState.endPoint = endPoint
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
this.controlPointMousemoveState.targetIndex = targetIndex
let offsets = []
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
if (!associativeLineTargetControlOffsets) {
// 兼容0.4.5版本没有associativeLineTargetControlOffsets的情况
offsets = getDefaultControlPointOffsets(startPoint, endPoint)
@@ -85,14 +78,8 @@ function onControlPointMousemove(e) {
}
let point1 = null
let point2 = null
const { x: clientX, y: clientY } = this.mindMap.toPos(e.clientX, e.clientY)
const _e = {
clientX,
clientY
}
// 拖拽的是控制点1
if (this.mousedownControlPointKey === 'controlPoint1') {
startPoint = getNodePoint(nodePos, '', 0, _e)
point1 = {
x,
y
@@ -101,15 +88,10 @@ function onControlPointMousemove(e) {
x: endPoint.x + offsets[1].x,
y: endPoint.y + offsets[1].y
}
if (startPoint) {
// 保存更新后的坐标
this.controlPointMousemoveState.startPoint = startPoint
// 更新控制点1的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
}
// 更新控制点1的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
} else {
// 拖拽的是控制点2
endPoint = getNodePoint(toNodePos, '', 0, _e)
point1 = {
x: startPoint.x + offsets[0].x,
y: startPoint.y + offsets[0].y
@@ -118,39 +100,18 @@ function onControlPointMousemove(e) {
x,
y
}
if (endPoint) {
// 保存更新后结束节点的坐标
this.controlPointMousemoveState.endPoint = endPoint
// 更新控制点2的连线
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
}
// 更新控制点2的连线
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
}
this.updataAassociativeLine(
startPoint,
endPoint,
point1,
point2,
this.activeLine
)
}
function updataAassociativeLine(
startPoint,
endPoint,
point1,
point2,
activeLine
) {
const [path, clickPath, text] = activeLine
// 更新关联线
const pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
path.plot(pathStr)
clickPath.plot(pathStr)
this.updateTextPos(path, text)
this.updateTextEditBoxPos(text)
}
// 控制点的鼠标松开事件
// 控制点的鼠标移动事件
function onControlPointMouseup(e) {
if (!this.isControlPointMousedown) return
e.stopPropagation()
@@ -159,15 +120,8 @@ function onControlPointMouseup(e) {
this.controlPointMousemoveState
let [, , , node] = this.activeLine
let offsetList = []
let { associativeLinePoint, associativeLineTargetControlOffsets } =
node.nodeData.data
if (!associativeLinePoint) {
associativeLinePoint = []
}
associativeLinePoint[targetIndex] = associativeLinePoint[targetIndex] || {
startPoint,
endPoint
}
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
if (!associativeLineTargetControlOffsets) {
// 兼容0.4.5版本没有associativeLineTargetControlOffsets的情况
offsetList[targetIndex] = getDefaultControlPointOffsets(
@@ -186,7 +140,6 @@ function onControlPointMouseup(e) {
y: pos.y - startPoint.y
}
offset2 = offsetList[targetIndex][1]
associativeLinePoint[targetIndex].startPoint = startPoint
} else {
// 更新控制点2数据
offset1 = offsetList[targetIndex][0]
@@ -194,12 +147,10 @@ function onControlPointMouseup(e) {
x: pos.x - endPoint.x,
y: pos.y - endPoint.y
}
associativeLinePoint[targetIndex].endPoint = endPoint
}
offsetList[targetIndex] = [offset1, offset2]
this.mindMap.execCommand('SET_NODE_DATA', node, {
associativeLineTargetControlOffsets: offsetList,
associativeLinePoint
associativeLineTargetControlOffsets: offsetList
})
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚所以重置isControlPointMousedown需要等draw_click事件触发完以后
setTimeout(() => {
@@ -286,6 +237,5 @@ export default {
renderControls,
removeControls,
hideControls,
showControls,
updataAassociativeLine
showControls
}

View File

@@ -1,5 +1,5 @@
import { Text } from '@svgdotjs/svg.js'
import { getStrWithBrFromHtml } from '../../utils/index'
import { getStrWithBrFromHtml } from './index'
// 创建文字节点
function createText(data) {
@@ -36,7 +36,7 @@ function showEditTextBox(g) {
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
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;`
@@ -47,8 +47,7 @@ function showEditTextBox(g) {
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode)
document.body.appendChild(this.textEditNode)
}
let {
associativeLineTextFontSize,
@@ -62,8 +61,7 @@ function showEditTextBox(g) {
).split(/\n/gim)
this.textEditNode.style.fontFamily = associativeLineTextFontFamily
this.textEditNode.style.fontSize = associativeLineTextFontSize * scale + 'px'
this.textEditNode.style.lineHeight =
textLines.length > 1 ? associativeLineTextLineHeight : 'normal'
this.textEditNode.style.lineHeight = textLines.length > 1 ? associativeLineTextLineHeight : 'normal'
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.innerHTML = textLines.join('<br>')
this.textEditNode.style.display = 'block'
@@ -79,12 +77,10 @@ function onScale() {
// 更新文本编辑框位置
function updateTextEditBoxPos(g) {
let rect = g.node.getBoundingClientRect()
if (this.textEditNode) {
this.textEditNode.style.minWidth = `${rect.width + 10}px`
this.textEditNode.style.minHeight = `${rect.height + 6}px`
this.textEditNode.style.left = `${rect.left}px`
this.textEditNode.style.top = `${rect.top}px`
}
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
}
// 隐藏文本编辑框
@@ -119,10 +115,8 @@ function getText(node, toNode) {
// 渲染关联线文字
function renderText(str, path, text) {
if (!str) return
let {
associativeLineTextFontSize,
associativeLineTextLineHeight
} = this.mindMap.themeConfig
let { associativeLineTextFontSize, associativeLineTextLineHeight } =
this.mindMap.themeConfig
text.clear()
let textArr = str.split(/\n/gim)
textArr.forEach((item, index) => {

View File

@@ -54,136 +54,28 @@ export const cubicBezierPath = (x1, y1, x2, y2) => {
)
}
export const calcPoint = (node, e) => {
const { left, top, translateLeft, translateTop, width, height } = node
const clientX = e.clientX
const clientY = e.clientY
// 中心点的坐标
const centerX = translateLeft + width / 2
const centerY = translateTop + height / 2
const translateCenterX = left + width / 2
const translateCenterY = top + height / 2
const theta = Math.atan(height / width)
// 矩形左上角坐标
const deltaX = clientX - centerX
const deltaY = centerY - clientY
// 方向值
const direction = Math.atan2(deltaY, deltaX)
// 默认坐标
let x = left + width
let y = top + height
if (direction < theta && direction >= -theta) {
// 右边
// 正切值 = 对边/邻边,对边 = 正切值*邻边
const range = direction * (width / 2)
if (direction < theta && direction >= 0) {
// 中心点上边
y = translateCenterY - range
} else if (direction >= -theta && direction < 0) {
// 中心点下方
y = translateCenterY - range
}
return {
x,
y,
dir: 'right',
range
}
} else if (direction >= theta && direction < Math.PI - theta) {
// 上边
y = top
let range = 0
if (direction < Math.PI / 2 - theta && direction >= theta) {
// 正切值 = 对边/邻边,邻边 = 对边/正切值
const side = height / 2 / direction
range = -side
// 中心点右侧
x = translateCenterX + side
} else if (
direction >= Math.PI / 2 - theta &&
direction < Math.PI - theta
) {
// 中心点左侧
const tanValue = (centerX - clientX) / (centerY - clientY)
const side = (height / 2) * tanValue
range = side
x = translateCenterX - side
}
return {
x,
y,
dir: 'top',
range
}
} else if (direction < -theta && direction >= theta - Math.PI) {
// 下边
let range = 0
if (direction >= theta - Math.PI / 2 && direction < -theta) {
// 中心点右侧
// 正切值 = 对边/邻边,邻边 = 对边/正切值
const side = height / 2 / direction
range = side
x = translateCenterX - side
} else if (
direction < theta - Math.PI / 2 &&
direction >= theta - Math.PI
) {
// 中心点左侧
const tanValue = (centerX - clientX) / (centerY - clientY)
const side = (height / 2) * tanValue
range = -side
x = translateCenterX + side
}
return {
x,
y,
dir: 'bottom',
range
}
}
// 左边
x = left
const tanValue = (centerY - clientY) / (centerX - clientX)
const range = tanValue * (width / 2)
if (direction >= -Math.PI && direction < theta - Math.PI) {
// 中心点右侧
y = translateCenterY - range
} else if (direction < Math.PI && direction >= Math.PI - theta) {
// 中心点左侧
y = translateCenterY - range
}
return {
x,
y,
dir: 'left',
range
}
}
// 获取节点的连接点
export const getNodePoint = (node, dir = 'right', range = 0, e = null) => {
export const getNodePoint = (node, dir = 'right') => {
let { left, top, width, height } = node
if (e) {
return calcPoint(node, e)
}
switch (dir) {
case 'left':
return {
x: left,
y: top + height / 2 - range
y: top + height / 2
}
case 'right':
return {
x: left + width,
y: top + height / 2 - range
y: top + height / 2
}
case 'top':
return {
x: left + width / 2 - range,
x: left + width / 2,
y: top
}
case 'bottom':
return {
x: left + width / 2 - range,
x: left + width / 2,
y: top + height
}
default:
@@ -219,8 +111,8 @@ export const computeNodePoints = (fromNode, toNode) => {
toDir = 'bottom'
} else if (offsetY > 0 && -offsetY < offsetX && offsetY > offsetX) {
// down
fromDir = 'right'
toDir = 'right'
fromDir = 'bottom'
toDir = 'top'
}
return [getNodePoint(fromNode, fromDir), getNodePoint(toNode, toDir)]
}
@@ -287,4 +179,4 @@ export const getDefaultControlPointOffsets = (startPoint, endPoint) => {
y: controlPoints[1].y - endPoint.y
}
]
}
}

View File

@@ -27,169 +27,136 @@ export const themeList = [
{
name: '默认',
value: 'default',
dark: false
},
{
name: '暗色2',
value: 'dark2',
dark: true
},
{
name: '天清绿',
value: 'skyGreen',
dark: false
},
{
name: '脑图经典2',
value: 'classic2',
dark: false
},
{
name: '脑图经典3',
value: 'classic3',
dark: false
},
{
name: '经典绿',
value: 'classicGreen',
dark: false
},
{
name: '经典蓝',
value: 'classicBlue',
dark: false
},
{
name: '天空蓝',
value: 'blueSky',
dark: false
},
{
name: '脑残粉',
value: 'brainImpairedPink',
dark: false
},
{
name: '暗色',
value: 'dark',
dark: true
},
{
name: '泥土黄',
value: 'earthYellow',
dark: false
},
{
name: '清新绿',
value: 'freshGreen',
dark: false
},
{
name: '清新红',
value: 'freshRed',
dark: false
},
{
name: '浪漫紫',
value: 'romanticPurple',
dark: false
},
{
name: '粉红葡萄',
value: 'pinkGrape',
dark: false
},
{
name: '薄荷',
value: 'mint',
dark: false
},
{
name: '金色vip',
value: 'gold',
dark: false
},
{
name: '活力橙',
value: 'vitalityOrange',
dark: false
},
{
name: '绿叶',
value: 'greenLeaf',
dark: false
},
{
name: '脑图经典',
value: 'classic',
dark: true
},
{
name: '脑图经典4',
value: 'classic4',
dark: false
},
{
name: '小黄人',
value: 'minions',
dark: false
},
{
name: '简约黑',
value: 'simpleBlack',
dark: false
},
{
name: '课程绿',
value: 'courseGreen',
dark: false
},
{
name: '咖啡',
value: 'coffee',
dark: false
},
{
name: '红色精神',
value: 'redSpirit',
dark: false
},
{
name: '黑色幽默',
value: 'blackHumour',
dark: true
},
{
name: '深夜办公室',
value: 'lateNightOffice',
dark: true
},
{
name: '黑金',
value: 'blackGold',
dark: true
},
{
name: '牛油果',
value: 'avocado',
dark: false
},
{
name: '秋天',
value: 'autumn',
dark: false
},
{
name: '橙汁',
value: 'orangeJuice',
dark: true
}
]
// 常量
export const CONSTANTS = {
CHANGE_THEME: 'changeTheme',
CHANGE_LAYOUT: 'changeLayout',
SET_DATA: 'setData',
TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode',
MODE: {
@@ -203,8 +170,7 @@ export const CONSTANTS = {
CATALOG_ORGANIZATION: 'catalogOrganization',
TIMELINE: 'timeline',
TIMELINE2: 'timeline2',
FISHBONE: 'fishbone',
VERTICAL_TIMELINE: 'verticalTimeline'
FISHBONE: 'fishbone'
},
DIR: {
UP: 'up',
@@ -240,19 +206,9 @@ export const CONSTANTS = {
BOTTOM: 'bottom',
CENTER: 'center'
},
LAYOUT_GROW_DIR: {
LEFT: 'left',
TIMELINE_DIR: {
TOP: 'top',
RIGHT: 'right',
BOTTOM: 'bottom'
},
PASTE_TYPE: {
CLIP_BOARD: 'clipBoard',
CANVAS: 'canvas'
},
SCROLL_BAR_DIR: {
VERTICAL: 'vertical',
HORIZONTAL: 'horizontal'
}
}
@@ -290,10 +246,6 @@ export const layoutList = [
name: '时间轴2',
value: CONSTANTS.LAYOUT.TIMELINE2,
},
{
name: '竖向时间轴',
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
},
{
name: '鱼骨图',
value: CONSTANTS.LAYOUT.FISHBONE,
@@ -306,72 +258,5 @@ export const layoutValueList = [
CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE,
CONSTANTS.LAYOUT.TIMELINE,
CONSTANTS.LAYOUT.TIMELINE2,
CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
CONSTANTS.LAYOUT.FISHBONE
]
// 节点数据中非样式的字段
export const nodeDataNoStylePropList = [
'text',
'image',
'imageTitle',
'imageSize',
'icon',
'tag',
'hyperlink',
'hyperlinkTitle',
'note',
'expand',
'isActive',
'generalization',
'richText',
'resetRichText',
'uid',
'activeStyle',
'associativeLineTargets',
'associativeLineTargetControlOffsets',
'associativeLinePoint',
'associativeLineText'
]
// 数据缓存
export const commonCaches = {
measureCustomNodeContentSizeEl: null,
measureRichtextNodeTextSizeEl: null
}
// 错误类型
export const ERROR_TYPES = {
READ_CLIPBOARD_ERROR: 'read_clipboard_error',
PARSE_PASTE_DATA_ERROR: 'parse_paste_data_error',
CUSTOM_HANDLE_CLIPBOARD_TEXT_ERROR: 'custom_handle_clipboard_text_error',
LOAD_CLIPBOARD_IMAGE_ERROR: 'load_clipboard_image_error',
BEFORE_TEXT_EDIT_ERROR: 'before_text_edit_error',
EXPORT_ERROR: 'export_error'
}
// a4纸的宽高
export const a4Size = {
width: 592.28,
height: 841.89
}
// css
export const cssContent = `
/* 鼠标hover和激活时渲染的矩形 */
.smm-hover-node{
display: none;
opacity: 0.6;
stroke-width: 1;
}
.smm-node:hover .smm-hover-node{
display: block;
}
.smm-node.active .smm-hover-node{
display: block;
opacity: 1;
stroke-width: 2;
}
`
]

View File

@@ -1,6 +1,3 @@
import { v4 as uuidv4 } from 'uuid'
import { nodeDataNoStylePropList } from '../constants/constant'
// 深度优先遍历树
export const walk = (
root,
@@ -34,11 +31,9 @@ export const walk = (
// 广度优先遍历树
export const bfsWalk = (root, callback) => {
callback(root)
let stack = [root]
let isStop = false
if (callback(root, null) === 'stop') {
isStop = true
}
while (stack.length) {
if (isStop) {
break
@@ -46,9 +41,8 @@ export const bfsWalk = (root, callback) => {
let cur = stack.shift()
if (cur.children && cur.children.length) {
cur.children.forEach(item => {
if (isStop) return
stack.push(item)
if (callback(item, cur) === 'stop') {
if (callback(item) === 'stop') {
isStop = true
}
})
@@ -56,26 +50,6 @@ export const bfsWalk = (root, callback) => {
}
}
// 按原比例缩放图片
export const resizeImgSizeByOriginRatio = (
width,
height,
newWidth,
newHeight
) => {
let arr = []
let nRatio = width / height
let mRatio = newWidth / newHeight
if (nRatio > mRatio) {
// 固定高度
arr = [nRatio * newHeight, newHeight]
} else {
// 固定宽度
arr = [newWidth, newWidth / nRatio]
}
return arr
}
// 缩放图片尺寸
export const resizeImgSize = (width, height, maxWidth, maxHeight) => {
let nRatio = width / height
@@ -163,12 +137,7 @@ export const copyRenderTree = (tree, root, removeActiveState = false) => {
}
// 复制节点树数据
export const copyNodeTree = (
tree,
root,
removeActiveState = false,
keepId = false
) => {
export const copyNodeTree = (tree, root, removeActiveState = false, keepId = false) => {
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data)
// 去除节点id因为节点id不能重复
if (tree.data.id && !keepId) delete tree.data.id
@@ -219,18 +188,6 @@ export const imgToDataUrl = src => {
})
}
// 解析dataUrl
export const parseDataUrl = data => {
if (!/^data:/.test(data)) return data
let [typeStr, base64] = data.split(',')
let res = /^data:[^/]+\/([^;]+);/.exec(typeStr)
let type = res[1]
return {
type,
base64
}
}
// 下载文件
export const downloadFile = (file, fileName) => {
let a = document.createElement('a')
@@ -279,8 +236,8 @@ export const degToRad = deg => {
return deg * (Math.PI / 180)
}
// 驼峰转连字符
export const camelCaseToHyphen = str => {
// 驼峰转连字符
export const camelCaseToHyphen = (str) => {
return str.replace(/([a-z])([A-Z])/g, (...args) => {
return args[1] + '-' + args[2].toLowerCase()
})
@@ -301,8 +258,11 @@ export const measureText = (text, { italic, bold, fontSize, fontFamily }) => {
}
measureTextContext.save()
measureTextContext.font = font
const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } =
measureTextContext.measureText(text)
const {
width,
actualBoundingBoxAscent,
actualBoundingBoxDescent
} = measureTextContext.measureText(text)
measureTextContext.restore()
const height = actualBoundingBoxAscent + actualBoundingBoxDescent
return { width, height }
@@ -310,9 +270,7 @@ export const measureText = (text, { italic, bold, fontSize, fontFamily }) => {
// 拼接font字符串
export const joinFontStr = ({ italic, bold, fontSize, fontFamily }) => {
return `${italic ? 'italic ' : ''} ${
bold ? 'bold ' : ''
} ${fontSize}px ${fontFamily} `
return `${italic ? 'italic ' : ''} ${bold ? 'bold ' : ''} ${fontSize}px ${fontFamily} `
}
// 在下一个事件循环里执行任务
@@ -378,7 +336,7 @@ export const checkNodeOuter = (mindMap, node) => {
// 提取html字符串里的纯文本
let getTextFromHtmlEl = null
export const getTextFromHtml = html => {
export const getTextFromHtml = (html) => {
if (!getTextFromHtmlEl) {
getTextFromHtmlEl = document.createElement('div')
}
@@ -387,285 +345,15 @@ export const getTextFromHtml = html => {
}
// 将blob转成data:url
export const readBlob = blob => {
export const readBlob = (blob) => {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onload = evt => {
reader.onload = (evt) => {
resolve(evt.target.result)
}
reader.onerror = err => {
reader.onerror = (err) => {
reject(err)
}
reader.readAsDataURL(blob)
})
}
// 将dom节点转换成html字符串
let nodeToHTMLWrapEl = null
export const nodeToHTML = node => {
if (!nodeToHTMLWrapEl) {
nodeToHTMLWrapEl = document.createElement('div')
}
nodeToHTMLWrapEl.innerHTML = ''
nodeToHTMLWrapEl.appendChild(node)
return nodeToHTMLWrapEl.innerHTML
}
// 获取图片大小
export const getImageSize = src => {
return new Promise(resolve => {
let img = new Image()
img.src = src
img.onload = () => {
resolve({
width: img.width,
height: img.height
})
}
img.onerror = () => {
resolve({
width: 0,
height: 0
})
}
})
}
// 创建节点唯一的id
export const createUid = () => {
return uuidv4()
}
// 加载图片文件
export const loadImage = imgFile => {
return new Promise((resolve, reject) => {
let fr = new FileReader()
fr.readAsDataURL(imgFile)
fr.onload = async e => {
let url = e.target.result
let size = await getImageSize(url)
resolve({
url,
size
})
}
fr.onerror = error => {
reject(error)
}
})
}
// 移除字符串中的html实体
export const removeHTMLEntities = str => {
;[['&nbsp;', '&#160;']].forEach(item => {
str = str.replaceAll(item[0], item[1])
})
return str
}
// 获取一个数据的类型
export const getType = data => {
return Object.prototype.toString.call(data).slice(7, -1)
}
// 判断一个数据是否是null和undefined和空字符串
export const isUndef = data => {
return data === null || data === undefined || data === ''
}
// 移除html字符串中节点的内联样式
export const removeHtmlStyle = html => {
return html.replaceAll(/(<[^\s]+)\s+style=["'][^'"]+["']\s*(>)/g, '$1$2')
}
// 给html标签中指定的标签添加内联样式
export const addHtmlStyle = (html, tag, style) => {
const reg = new RegExp(`(<${tag}[^>]*)(>[^<>]*</${tag}>)`, 'g')
return html.replaceAll(reg, `$1 style="${style}"$2`)
}
// 检查一个字符串是否是富文本字符
let checkIsRichTextEl = null
export const checkIsRichText = str => {
if (!checkIsRichTextEl) {
checkIsRichTextEl = document.createElement('div')
}
checkIsRichTextEl.innerHTML = str
for (let c = checkIsRichTextEl.childNodes, i = c.length; i--; ) {
if (c[i].nodeType == 1) return true
}
return false
}
// 搜索和替换html字符串中指定的文本
let replaceHtmlTextEl = null
export const replaceHtmlText = (html, searchText, replaceText) => {
if (!replaceHtmlTextEl) {
replaceHtmlTextEl = document.createElement('div')
}
replaceHtmlTextEl.innerHTML = html
let walk = root => {
let childNodes = root.childNodes
childNodes.forEach(node => {
if (node.nodeType === 1) {
// 元素节点
walk(node)
} else if (node.nodeType === 3) {
// 文本节点
root.replaceChild(
document.createTextNode(
node.nodeValue.replaceAll(searchText, replaceText)
),
node
)
}
})
}
walk(replaceHtmlTextEl)
return replaceHtmlTextEl.innerHTML
}
// 判断一个颜色是否是白色
export const isWhite = color => {
color = String(color).replaceAll(/\s+/g, '')
return (
['#fff', '#ffffff', '#FFF', '#FFFFFF', 'rgb(255,255,255)'].includes(
color
) || /rgba\(255,255,255,[^)]+\)/.test(color)
)
}
// 判断一个颜色是否是透明
export const isTransparent = color => {
color = String(color).replaceAll(/\s+/g, '')
return (
['', 'transparent'].includes(color) || /rgba\(\d+,\d+,\d+,0\)/.test(color)
)
}
// 从当前主题里获取一个非透明非白色的颜色
export const getVisibleColorFromTheme = themeConfig => {
let { lineColor, root, second, node } = themeConfig
let list = [
lineColor,
root.fillColor,
root.color,
second.fillColor,
second.color,
node.fillColor,
node.color,
root.borderColor,
second.borderColor,
node.borderColor
]
for (let i = 0; i < list.length; i++) {
let color = list[i]
if (!isTransparent(color) && !isWhite(color)) {
return color
}
}
}
// 将<p><span></span><p>形式的节点富文本内容转换成\n换行的文本
let nodeRichTextToTextWithWrapEl = null
export const nodeRichTextToTextWithWrap = html => {
if (!nodeRichTextToTextWithWrapEl) {
nodeRichTextToTextWithWrapEl = document.createElement('div')
}
nodeRichTextToTextWithWrapEl.innerHTML = html
const childNodes = nodeRichTextToTextWithWrapEl.childNodes
let res = ''
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i]
if (node.nodeType === 1) {
// 元素节点
if (node.tagName.toLowerCase() === 'p') {
res += node.textContent + '\n'
} else {
res += node.textContent
}
} else if (node.nodeType === 3) {
// 文本节点
res += node.nodeValue
}
}
return res.replace(/\n$/, '')
}
// 将<br>换行的文本转换成<p><span></span><p>形式的节点富文本内容
let textToNodeRichTextWithWrapEl = null
export const textToNodeRichTextWithWrap = html => {
if (!textToNodeRichTextWithWrapEl) {
textToNodeRichTextWithWrapEl = document.createElement('div')
}
textToNodeRichTextWithWrapEl.innerHTML = html
const childNodes = textToNodeRichTextWithWrapEl.childNodes
let list = []
let str = ''
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i]
if (node.nodeType === 1) {
// 元素节点
if (node.tagName.toLowerCase() === 'br') {
list.push(str)
str = ''
} else {
str += node.textContent
}
} else if (node.nodeType === 3) {
// 文本节点
str += node.nodeValue
}
}
if (str) {
list.push(str)
}
return list
.map(item => {
return `<p><span>${item}</span></p>`
})
.join('')
}
// 判断是否是移动端环境
export const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
}
// 获取对象改变了的的属性
export const getObjectChangedProps = (oldObject, newObject) => {
const res = {}
Object.keys(newObject).forEach((prop) => {
const oldVal = oldObject[prop]
const newVal = newObject[prop]
if (getType(oldVal) !== getType(newVal)) {
res[prop] = newVal
return
}
if (getType(oldVal) === 'Object') {
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
res[prop] = newVal
return
}
} else {
if (oldVal !== newVal) {
res[prop] = newVal
return
}
}
})
return res
}
// 判断一个字段是否是节点数据中的样式字段
export const checkIsNodeStyleDataKey = (key) => {
// 用户自定义字段
if (/^_/.test(key)) return false
// 不在节点非样式字段列表里,那么就是样式字段
if (!nodeDataNoStylePropList.includes(key)) {
return true
}
return false
}

View File

@@ -4,8 +4,8 @@ function setData(data = {}) {
}
// 设置文本
function setText(text, richText, resetRichText) {
this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText, resetRichText)
function setText(text, richText) {
this.mindMap.execCommand('SET_NODE_TEXT', this, text, richText)
}
// 设置图片
@@ -39,13 +39,8 @@ function setShape(shape) {
}
// 修改某个样式
function setStyle(prop, value) {
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value)
}
// 修改多个样式
function setStyles(style) {
this.mindMap.execCommand('SET_NODE_STYLES', this, style)
function setStyle(prop, value, isActive) {
this.mindMap.execCommand('SET_NODE_STYLE', this, prop, value, isActive)
}
export default {
@@ -57,6 +52,5 @@ export default {
setNote,
setTag,
setShape,
setStyle,
setStyles
setStyle
}

View File

@@ -1,13 +1,7 @@
import {
measureText,
resizeImgSize,
removeHtmlStyle,
addHtmlStyle,
checkIsRichText
} from '../../../utils'
import { measureText, resizeImgSize, getTextFromHtml } from '../utils'
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
import iconsSvg from '../../../svg/icons'
import { CONSTANTS, commonCaches } from '../../../constants/constant'
import iconsSvg from '../svg/icons'
import { CONSTANTS } from './constant'
// 创建图片节点
function createImgNode() {
@@ -23,15 +17,6 @@ function createImgNode() {
node.on('dblclick', e => {
this.mindMap.emit('node_img_dblclick', this, e)
})
node.on('mouseenter', e => {
this.mindMap.emit('node_img_mouseenter', this, node, e)
})
node.on('mouseleave', e => {
this.mindMap.emit('node_img_mouseleave', this, node, e)
})
node.on('mousemove', e => {
this.mindMap.emit('node_img_mousemove', this, node, e)
})
return {
node,
width: imgSize[0],
@@ -41,12 +26,9 @@ function createImgNode() {
// 获取图片显示宽高
function getImgShowSize() {
const { custom, width, height } = this.nodeData.data.imageSize
// 如果是自定义了图片的宽高,那么不受最大宽高限制
if (custom) return [width, height]
return resizeImgSize(
width,
height,
this.nodeData.data.imageSize.width,
this.nodeData.data.imageSize.height,
this.mindMap.themeConfig.imgMaxWidth,
this.mindMap.themeConfig.imgMaxHeight
)
@@ -60,10 +42,7 @@ function createIconNode() {
}
let iconSize = this.mindMap.themeConfig.iconSize
return _data.icon.map(item => {
let src = iconsSvg.getNodeIconListIcon(
item,
this.mindMap.opt.iconList || []
)
let src = iconsSvg.getNodeIconListIcon(item, this.mindMap.opt.iconList || [])
let node = null
// svg图标
if (/^<svg/.test(src)) {
@@ -73,9 +52,6 @@ function createIconNode() {
node = new Image().load(src)
}
node.size(iconSize, iconSize)
node.on('click', e => {
this.mindMap.emit('node_icon_click', this, item, e)
})
return {
node,
width: iconSize,
@@ -86,66 +62,33 @@ function createIconNode() {
// 创建富文本节点
function createRichTextNode() {
const { textAutoWrapWidth } = this.mindMap.opt
let g = new G()
// 重新设置富文本节点内容
let recoverText = false
if (this.nodeData.data.resetRichText) {
if (this.nodeData.data.resetRichText || [CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
delete this.nodeData.data.resetRichText
recoverText = true
}
if ([CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
// 如果自定义过样式则不允许覆盖
if (!this.hasCustomStyle()) {
recoverText = true
}
}
if (recoverText) {
let text = this.nodeData.data.text
// 判断节点内容是否是富文本
let isRichText = checkIsRichText(text)
// 样式字符串
let style = this.style.createStyleText()
if (isRichText) {
// 如果是富文本那么线移除内联样式
text = removeHtmlStyle(text)
// 再添加新的内联样式
text = addHtmlStyle(text, 'span', style)
} else {
// 非富文本
text = `<p><span style="${style}">${text}</span></p>`
}
this.nodeData.data.text = text
let text = getTextFromHtml(this.nodeData.data.text)
this.nodeData.data.text = `<p><span style="${this.style.createStyleText()}">${text}</span></p>`
}
let html = `<div>${this.nodeData.data.text}</div>`
if (!commonCaches.measureRichtextNodeTextSizeEl) {
commonCaches.measureRichtextNodeTextSizeEl = document.createElement('div')
commonCaches.measureRichtextNodeTextSizeEl.style.position = 'fixed'
commonCaches.measureRichtextNodeTextSizeEl.style.left = '-999999px'
this.mindMap.el.appendChild(commonCaches.measureRichtextNodeTextSizeEl)
}
let div = commonCaches.measureRichtextNodeTextSizeEl
let div = document.createElement('div')
div.innerHTML = html
div.style.cssText = `position: fixed; left: -999999px;`
let el = div.children[0]
el.classList.add('smm-richtext-node-wrap')
el.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
el.style.maxWidth = textAutoWrapWidth + 'px'
el.style.maxWidth = this.mindMap.opt.textAutoWrapWidth + 'px'
this.mindMap.el.appendChild(div)
let { width, height } = el.getBoundingClientRect()
// 如果文本为空,那么需要计算一个默认高度
if (height <= 0) {
div.innerHTML = '<p>abc123我和你</p>'
let elTmp = div.children[0]
elTmp.classList.add('smm-richtext-node-wrap')
height = elTmp.getBoundingClientRect().height
}
width = Math.ceil(width) + 1 // 修复getBoundingClientRect方法对实际宽度是小数的元素获取到的值是整数导致宽度不够文本发生换行的问题
width = Math.ceil(width)
height = Math.ceil(height)
g.attr('data-width', width)
g.attr('data-height', height)
html = div.innerHTML
this.mindMap.el.removeChild(div)
let foreignObject = new ForeignObject()
foreignObject.width(width)
foreignObject.height(height)
foreignObject.add(div.children[0])
foreignObject.add(SVG(html))
g.add(foreignObject)
return {
node: g,
@@ -160,8 +103,12 @@ function createTextNode() {
return this.createRichTextNode()
}
let g = new G()
let fontSize = this.getStyle('fontSize', false)
let lineHeight = this.getStyle('lineHeight', false)
let fontSize = this.getStyle('fontSize', false, this.nodeData.data.isActive)
let lineHeight = this.getStyle(
'lineHeight',
false,
this.nodeData.data.isActive
)
// 文本超长自动换行
let textStyle = this.style.getTextFontStyle()
let textArr = this.nodeData.data.text.split(/\n/gim)
@@ -283,17 +230,15 @@ function createNoteNode() {
if (!this.noteEl) {
this.noteEl = document.createElement('div')
this.noteEl.style.cssText = `
position: fixed;
position: absolute;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
display: none;
background-color: #fff;
z-index: ${this.mindMap.opt.nodeNoteTooltipZIndex}
z-index: ${ this.mindMap.opt.nodeNoteTooltipZIndex }
`
const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body
targetNode.appendChild(this.noteEl)
document.body.appendChild(this.noteEl)
}
this.noteEl.innerText = this.nodeData.data.note
}
@@ -325,40 +270,13 @@ function createNoteNode() {
}
}
// 测量自定义节点内容元素的宽高
function measureCustomNodeContentSize(content) {
if (!commonCaches.measureCustomNodeContentSizeEl) {
commonCaches.measureCustomNodeContentSizeEl = document.createElement('div')
commonCaches.measureCustomNodeContentSizeEl.style.cssText = `
position: fixed;
left: -99999px;
top: -99999px;
`
this.mindMap.el.appendChild(commonCaches.measureCustomNodeContentSizeEl)
}
commonCaches.measureCustomNodeContentSizeEl.innerHTML = ''
commonCaches.measureCustomNodeContentSizeEl.appendChild(content)
let rect = commonCaches.measureCustomNodeContentSizeEl.getBoundingClientRect()
return {
width: rect.width,
height: rect.height
}
}
// 是否使用的是自定义节点内容
function isUseCustomNodeContent() {
return !!this._customNodeContent
}
export default {
createImgNode,
getImgShowSize,
createIconNode,
createRichTextNode,
createTextNode,
createHyperlinkNode,
createTagNode,
createNoteNode,
measureCustomNodeContentSize,
isUseCustomNodeContent
}
createImgNode,
getImgShowSize,
createIconNode,
createRichTextNode,
createTextNode,
createHyperlinkNode,
createTagNode,
createNoteNode
}

View File

@@ -1,4 +1,4 @@
import btnsSvg from '../../../svg/btns'
import btnsSvg from '../svg/btns'
import { SVG, Circle, G } from '@svgdotjs/svg.js'
// 创建展开收起按钮的内容节点
@@ -6,27 +6,13 @@ function createExpandNodeContent() {
if (this._openExpandNode) {
return
}
let { close, open } = this.mindMap.opt.expandBtnIcon || {}
// 根据配置判断是否显示数量按钮
if (this.mindMap.opt.isShowExpandNum) {
// 展开的节点
this._openExpandNode = SVG()
.text()
.size(this.expandBtnSize, this.expandBtnSize)
// 文本垂直居中
this._openExpandNode.attr({
'text-anchor': 'middle',
'dominant-baseline': 'middle',
x: this.expandBtnSize / 2,
y: 2
})
} else {
this._openExpandNode = SVG(open || btnsSvg.open).size(
this.expandBtnSize,
this.expandBtnSize
)
this._openExpandNode.x(0).y(-this.expandBtnSize / 2)
}
let { open, close } = this.mindMap.opt.expandBtnIcon || {}
// 展开的节点
this._openExpandNode = SVG(open || btnsSvg.open).size(
this.expandBtnSize,
this.expandBtnSize
)
this._openExpandNode.x(0).y(-this.expandBtnSize / 2)
// 收起的节点
this._closeExpandNode = SVG(close || btnsSvg.close).size(
this.expandBtnSize,
@@ -36,7 +22,6 @@ function createExpandNodeContent() {
// 填充节点
this._fillExpandNode = new Circle().size(this.expandBtnSize)
this._fillExpandNode.x(0).y(-this.expandBtnSize / 2)
// 设置样式
this.style.iconBtn(
this._openExpandNode,
@@ -44,12 +29,7 @@ function createExpandNodeContent() {
this._fillExpandNode
)
}
function sumNode(data = []) {
return data.reduce(
(total, cur) => total + this.sumNode(cur.children || []),
data.length
)
}
// 创建或更新展开收缩按钮内容
function updateExpandBtnNode() {
let { expand } = this.nodeData.data
@@ -67,26 +47,7 @@ function updateExpandBtnNode() {
node = this._closeExpandNode
this._lastExpandBtnType = true
}
if (this._expandBtn) {
// 如果是收起按钮加上边框
let { isShowExpandNum, expandBtnStyle, expandBtnNumHandler } = this.mindMap.opt
if (isShowExpandNum) {
if (!expand) {
// 数字按钮添加边框
this._fillExpandNode.stroke({
color: expandBtnStyle.strokeColor
})
// 计算子节点数量
let count = this.sumNode(this.nodeData.children)
count = expandBtnNumHandler(count)
node.text(count)
} else {
this._fillExpandNode.stroke('none')
}
}
this._expandBtn.add(this._fillExpandNode).add(node)
}
if (this._expandBtn) this._expandBtn.add(this._fillExpandNode).add(node)
}
// 更新展开收缩按钮位置
@@ -177,6 +138,5 @@ export default {
renderExpandBtn,
removeExpandBtn,
showExpandBtn,
hideExpandBtn,
sumNode
hideExpandBtn
}

View File

@@ -1,5 +1,4 @@
import Node from './Node'
import { createUid } from '../../../utils/index'
import Node from '../Node'
// 检查是否存在概要
function checkHasGeneralization () {
@@ -19,7 +18,7 @@ function createGeneralizationNode () {
data: {
data: this.nodeData.data.generalization
},
uid: createUid(),
uid: this.mindMap.uid++,
renderer: this.renderer,
mindMap: this.mindMap,
draw: this.draw,
@@ -36,14 +35,15 @@ function createGeneralizationNode () {
// 更新概要节点
function updateGeneralization () {
if (this.isGeneralization) return
this.removeGeneralization()
this.createGeneralizationNode()
}
// 渲染概要节点
function renderGeneralization () {
if (this.isGeneralization) return
if (this.isGeneralization) {
return
}
if (!this.checkHasGeneralization()) {
this.removeGeneralization()
this._generalizationNodeWidth = 0
@@ -66,7 +66,6 @@ function renderGeneralization () {
// 删除概要节点
function removeGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.remove()
this._generalizationLine = null
@@ -87,7 +86,6 @@ function removeGeneralization () {
// 隐藏概要节点
function hideGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.hide()
}
@@ -98,7 +96,6 @@ function hideGeneralization () {
// 显示概要节点
function showGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.show()
}

View File

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

BIN
web/build/icons/icon.icns Normal file

Binary file not shown.

BIN
web/build/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
web/build/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

2023
web/dist_electron/index.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,84 @@
{
"name": "thoughts",
"version": "0.1.0",
"private": true,
"description": "一个简洁的思维导图",
"author": "街角小林<1013335014@qq.com>",
"repository": {
"type": "git",
"url": "https://github.com/wanglin2/mind-map"
},
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build && node ../copy.js",
"lint": "vue-cli-service lint",
"autoBuildDoc": "node ./scripts/autoBuildDoc.js",
"buildDoc": "node ./scripts/buildDoc.js",
"electron:build": "vue-cli-service electron:build",
"electron:build-all": "vue-cli-service electron:build -p never -mwl",
"electron:build-mac": "vue-cli-service electron:build -p never -m",
"electron:build-win": "vue-cli-service electron:build -p never -w",
"electron:build-linux": "vue-cli-service electron:build -p never -l",
"electron:serve": "vue-cli-service electron:serve",
"buildLibrary": "vue-cli-service build --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js",
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
"@toast-ui/editor": "^3.1.5",
"core-js": "^3.6.5",
"electron-json-storage": "^4.6.0",
"element-ui": "^2.15.1",
"fs-extra": "^7.0.1",
"highlight.js": "^10.7.3",
"open": "^6.4.0",
"uuid": "^3.4.0",
"v-viewer": "^1.6.4",
"vue": "^2.6.11",
"vue-i18n": "^8.27.2",
"vue-router": "^3.5.1",
"vuex": "^3.6.2",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.0",
"@vue/cli-plugin-eslint": "^4.5.0",
"@vue/cli-service": "^4.5.0",
"babel-eslint": "^10.1.0",
"chokidar": "^3.5.3",
"electron": "^23.1.1",
"electron-devtools-installer": "^3.1.0",
"esbuild": "^0.17.15",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"less": "^3.12.2",
"less-loader": "^7.1.0",
"markdown-it": "^13.0.1",
"markdown-it-checkbox": "^1.1.0",
"prettier": "^1.19.1",
"vue-cli-plugin-electron-builder": "~2.1.1",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.44.2"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

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