Compare commits
No commits in common. "main" and "0.11.0" have entirely different histories.
3
.gitignore
vendored
@ -2,5 +2,4 @@ node_modules
|
||||
.DS_Store
|
||||
dist_electron
|
||||
simple-mind-map/dist
|
||||
simple-mind-map/types
|
||||
utools/dist
|
||||
simple-mind-map/types
|
||||
2
copy.js
@ -13,4 +13,4 @@ if (fs.existsSync(src)) {
|
||||
fs.unlinkSync(src)
|
||||
}
|
||||
|
||||
// console.warn('请检查付费插件是否启用!!!')
|
||||
console.warn('请检查手绘风格、标记插件、编号插件是否启用!!!')
|
||||
2
dist/css/app.css
vendored
@ -1 +1 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}#app{font-family:Avenir,Helvetica,Arial,sans-serif;color:#2c3e50}.customScrollbar::-webkit-scrollbar{width:7px;height:7px}.customScrollbar::-webkit-scrollbar-thumb{border-radius:7px;background-color:rgba(0,0,0,.3);cursor:pointer}.customScrollbar::-webkit-scrollbar-track{box-shadow:none;background:transparent;display:none}.el-dialog{border-radius:10px}@font-face{font-family:iconfont;src:url(../fonts/iconfont.woff2) format("woff2"),url(../fonts/iconfont.woff) format("woff"),url(../fonts/iconfont.ttf) format("truetype")}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.iconAIshengcheng:before{content:"\e6b5"}.iconprinting:before{content:"\ea28"}.iconwenjianjia:before{content:"\e614"}.iconcontentleft:before{content:"\e8c9"}.iconjuzhongduiqi:before{content:"\ec80"}.iconfile-excel:before{content:"\e7b7"}.iconfreemind:before{content:"\e97d"}.iconwaikuang:before{content:"\e640"}.iconhighlight:before{content:"\e6b8"}.iconyanshibofang:before{content:"\e648"}.iconfujian:before{content:"\e88a"}.icongeshihua:before{content:"\e7a3"}.iconyuanma:before{content:"\e658"}.icongundongtiao:before{content:"\e670"}.iconxietongwendang:before{content:"\e60d"}.iconTXT:before{content:"\e6e1"}.iconwenjian1:before{content:"\e69f"}.icondodeparent:before{content:"\e70f"}.icongongshi:before{content:"\e617"}.icontouming:before{content:"\e60c"}.iconlieri:before{content:"\e60b"}.iconmoon_line:before{content:"\e745"}.iconsousuo:before{content:"\e693"}.iconjiantouyou:before{content:"\e62d"}.iconbianji1:before{content:"\e60a"}.icondaohang1:before{content:"\e632"}.iconyanjing:before{content:"\e8bf"}.iconwangzhan:before{content:"\e628"}.iconcsdn:before{content:"\e608"}.iconshejiaotubiao-10:before{content:"\e644"}.iconstar:before{content:"\e7df"}.iconfork:before{content:"\e641"}.iconxiazai:before{content:"\e613"}.iconteamwork:before{content:"\e870"}.iconshuiyin:before{content:"\e67a"}.iconxmind:before{content:"\ea57"}.iconmouseR:before{content:"\e6bd"}.iconmouseL:before{content:"\e6c0"}.iconwenjian:before{content:"\e607"}.iconpdf:before{content:"\e740"}.iconPNG:before{content:"\ec18"}.iconSVG:before{content:"\e621"}.iconmarkdown:before{content:"\ec04"}.iconjson:before{content:"\ea42"}.iconlianjiexian:before{content:"\e75b"}.iconbangzhu:before{content:"\e620"}.iconshezhi:before{content:"\e8b7"}.iconwushuju:before{content:"\e643"}.iconzuijinliulan:before{content:"\e62f"}.icon3zuidahua-3:before{content:"\e692"}.iconzuixiaohua:before{content:"\e650"}.iconzuidahua:before{content:"\e651"}.iconguanbi:before{content:"\e652"}.icondiannao:before{content:"\eac0"}.iconzhuye:before{content:"\e65c"}.iconbendi1x:before{content:"\e606"}.iconbeijingyanse:before{content:"\e6f8"}.iconqingchu:before{content:"\e605"}.iconcase:before{content:"\e6c6"}.iconxingzhuang-wenzi:before{content:"\eb99"}.iconzitijiacu:before{content:"\ec83"}.iconzitixiahuaxian:before{content:"\ec85"}.iconzitixieti:before{content:"\ec86"}.iconshanchuxian:before{content:"\e612"}.iconzitiyanse:before{content:"\e854"}.icongithub:before{content:"\e64f"}.iconchoose1:before{content:"\e6c5"}.iconzhuti:before{content:"\e7aa"}.icondaochu1:before{content:"\e63e"}.iconlingcunwei:before{content:"\e657"}.iconexport:before{content:"\e642"}.icondakai:before{content:"\ebdf"}.iconxinjian:before{content:"\e64e"}.iconjianqie:before{content:"\e601"}.iconzhengli:before{content:"\e83b"}.iconfuzhi:before{content:"\e604"}.iconniantie:before{content:"\e63f"}.iconshangyi:before{content:"\e6be"}.iconxiayi:before{content:"\e6bf"}.icongaikuozonglan:before{content:"\e609"}.iconquanxuan:before{content:"\f199"}.icondaoru:before{content:"\e6a3"}.iconhoutui-shi:before{content:"\e656"}.iconqianjin1:before{content:"\e654"}.iconwithdraw:before{content:"\e603"}.iconqianjin:before{content:"\e600"}.iconhuifumoren:before{content:"\e60e"}.iconhuanhang:before{content:"\e61e"}.iconsuoxiao:before{content:"\ec13"}.iconbianji:before{content:"\e626"}.iconfangda:before{content:"\e663"}.iconquanping1:before{content:"\e664"}.icondingwei:before{content:"\e616"}.icondaohang:before{content:"\e611"}.iconjianpan:before{content:"\e64d"}.iconquanping:before{content:"\e602"}.icondaochu:before{content:"\e63d"}.iconbiaoqian:before{content:"\e63c"}.iconflow-Mark:before{content:"\e65b"}.iconchaolianjie:before{content:"\e6f4"}.iconjingzi:before{content:"\e610"}.iconxiaolian:before{content:"\e60f"}.iconimage:before{content:"\e629"}.iconjiegou:before{content:"\e61d"}.iconyangshi:before{content:"\e631"}.iconfuhao-dagangshu:before{content:"\e71f"}.icontianjiazijiedian:before{content:"\e622"}.iconjiedian:before{content:"\e655"}.iconshanchu:before{content:"\e696"}.iconzhankai:before{content:"\e64c"}.iconzhankai1:before{content:"\e673"}
|
||||
*{margin:0;padding:0;box-sizing:border-box}#app{font-family:Avenir,Helvetica,Arial,sans-serif;color:#2c3e50}@font-face{font-family:iconfont;src:url(../fonts/iconfont.woff2) format("woff2"),url(../fonts/iconfont.woff) format("woff"),url(../fonts/iconfont.ttf) format("truetype")}#app,.iconfont{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal}.iconwaikuang:before{content:"\e640"}.iconhighlight:before{content:"\e6b8"}.iconyanshibofang:before{content:"\e648"}.iconfujian:before{content:"\e88a"}.icongeshihua:before{content:"\e7a3"}.iconyuanma:before{content:"\e658"}.icongundongtiao:before{content:"\e670"}.iconxietongwendang:before{content:"\e60d"}.iconTXT:before{content:"\e6e1"}.iconwenjian1:before{content:"\e69f"}.icondodeparent:before{content:"\e70f"}.icongongshi:before{content:"\e617"}.icontouming:before{content:"\e60c"}.iconlieri:before{content:"\e60b"}.iconmoon_line:before{content:"\e745"}.iconsousuo:before{content:"\e693"}.iconjiantouyou:before{content:"\e62d"}.iconbianji1:before{content:"\e60a"}.icondaohang1:before{content:"\e632"}.iconyanjing:before{content:"\e8bf"}.iconwangzhan:before{content:"\e628"}.iconcsdn:before{content:"\e608"}.iconshejiaotubiao-10:before{content:"\e644"}.iconstar:before{content:"\e7df"}.iconfork:before{content:"\e641"}.iconxiazai:before{content:"\e613"}.iconteamwork:before{content:"\e870"}.iconshuiyin:before{content:"\e67a"}.iconxmind:before{content:"\ea57"}.iconmouseR:before{content:"\e6bd"}.iconmouseL:before{content:"\e6c0"}.iconwenjian:before{content:"\e607"}.iconpdf:before{content:"\e740"}.iconPNG:before{content:"\ec18"}.iconSVG:before{content:"\e621"}.iconmarkdown:before{content:"\ec04"}.iconjson:before{content:"\ea42"}.iconlianjiexian:before{content:"\e75b"}.iconbangzhu:before{content:"\e620"}.iconshezhi:before{content:"\e8b7"}.iconwushuju:before{content:"\e643"}.iconzuijinliulan:before{content:"\e62f"}.icon3zuidahua-3:before{content:"\e692"}.iconzuixiaohua:before{content:"\e650"}.iconzuidahua:before{content:"\e651"}.iconguanbi:before{content:"\e652"}.icondiannao:before{content:"\eac0"}.iconzhuye:before{content:"\e65c"}.iconbendi1x:before{content:"\e606"}.iconbeijingyanse:before{content:"\e6f8"}.iconqingchu:before{content:"\e605"}.iconcase:before{content:"\e6c6"}.iconxingzhuang-wenzi:before{content:"\eb99"}.iconzitijiacu:before{content:"\ec83"}.iconzitixiahuaxian:before{content:"\ec85"}.iconzitixieti:before{content:"\ec86"}.iconshanchuxian:before{content:"\e612"}.iconzitiyanse:before{content:"\e854"}.icongithub:before{content:"\e64f"}.iconchoose1:before{content:"\e6c5"}.iconzhuti:before{content:"\e7aa"}.icondaochu1:before{content:"\e63e"}.iconlingcunwei:before{content:"\e657"}.iconexport:before{content:"\e642"}.icondakai:before{content:"\ebdf"}.iconxinjian:before{content:"\e64e"}.iconjianqie:before{content:"\e601"}.iconzhengli:before{content:"\e83b"}.iconfuzhi:before{content:"\e604"}.iconniantie:before{content:"\e63f"}.iconshangyi:before{content:"\e6be"}.iconxiayi:before{content:"\e6bf"}.icongaikuozonglan:before{content:"\e609"}.iconquanxuan:before{content:"\f199"}.icondaoru:before{content:"\e6a3"}.iconhoutui-shi:before{content:"\e656"}.iconqianjin1:before{content:"\e654"}.iconwithdraw:before{content:"\e603"}.iconqianjin:before{content:"\e600"}.iconhuifumoren:before{content:"\e60e"}.iconhuanhang:before{content:"\e61e"}.iconsuoxiao:before{content:"\ec13"}.iconbianji:before{content:"\e626"}.iconfangda:before{content:"\e663"}.iconquanping1:before{content:"\e664"}.icondingwei:before{content:"\e616"}.icondaohang:before{content:"\e611"}.iconjianpan:before{content:"\e64d"}.iconquanping:before{content:"\e602"}.icondaochu:before{content:"\e63d"}.iconbiaoqian:before{content:"\e63c"}.iconflow-Mark:before{content:"\e65b"}.iconchaolianjie:before{content:"\e6f4"}.iconjingzi:before{content:"\e610"}.iconxiaolian:before{content:"\e60f"}.iconimage:before{content:"\e629"}.iconjiegou:before{content:"\e61d"}.iconyangshi:before{content:"\e631"}.iconfuhao-dagangshu:before{content:"\e71f"}.icontianjiazijiedian:before{content:"\e622"}.iconjiedian:before{content:"\e655"}.iconshanchu:before{content:"\e696"}.iconzhankai:before{content:"\e64c"}.iconzhankai1:before{content:"\e673"}
|
||||
BIN
dist/fonts/iconfont.ttf
vendored
BIN
dist/fonts/iconfont.woff
vendored
BIN
dist/fonts/iconfont.woff2
vendored
BIN
dist/img/catalogOrganization.jpg
vendored
|
Before Width: | Height: | Size: 6.8 KiB |
BIN
dist/img/catalogOrganization.png
vendored
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
dist/img/classic10.png
vendored
|
Before Width: | Height: | Size: 6.7 KiB |
BIN
dist/img/classic11.png
vendored
|
Before Width: | Height: | Size: 6.7 KiB |
BIN
dist/img/classic12.png
vendored
|
Before Width: | Height: | Size: 6.7 KiB |
BIN
dist/img/classic13.png
vendored
|
Before Width: | Height: | Size: 6.9 KiB |
BIN
dist/img/classic14.png
vendored
|
Before Width: | Height: | Size: 9.2 KiB |
BIN
dist/img/classic15.png
vendored
|
Before Width: | Height: | Size: 6.7 KiB |
BIN
dist/img/classic8.png
vendored
|
Before Width: | Height: | Size: 6.6 KiB |
BIN
dist/img/classic9.png
vendored
|
Before Width: | Height: | Size: 6.7 KiB |
BIN
dist/img/dark5.png
vendored
|
Before Width: | Height: | Size: 6.4 KiB |
BIN
dist/img/dark6.png
vendored
|
Before Width: | Height: | Size: 6.7 KiB |
BIN
dist/img/dark7.png
vendored
|
Before Width: | Height: | Size: 6.4 KiB |
BIN
dist/img/fishbone.jpg
vendored
|
Before Width: | Height: | Size: 6.5 KiB |
BIN
dist/img/fishbone.png
vendored
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
dist/img/fishbone2.jpg
vendored
|
Before Width: | Height: | Size: 9.7 KiB |
BIN
dist/img/logicalStructure.jpg
vendored
|
Before Width: | Height: | Size: 5.5 KiB |
BIN
dist/img/logicalStructure.png
vendored
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
dist/img/logicalStructureLeft.jpg
vendored
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 9.2 KiB |
BIN
dist/img/mindMap.jpg
vendored
|
Before Width: | Height: | Size: 6.2 KiB |
BIN
dist/img/mindMap.png
vendored
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
dist/img/organizationStructure.jpg
vendored
|
Before Width: | Height: | Size: 6.8 KiB |
BIN
dist/img/organizationStructure.png
vendored
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
dist/img/rightFishbone.jpg
vendored
|
Before Width: | Height: | Size: 6.4 KiB |
BIN
dist/img/rightFishbone2.jpg
vendored
|
Before Width: | Height: | Size: 9.5 KiB |
BIN
dist/img/timeline.jpg
vendored
|
Before Width: | Height: | Size: 8.2 KiB |
BIN
dist/img/timeline.png
vendored
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
dist/img/timeline2.jpg
vendored
|
Before Width: | Height: | Size: 7.9 KiB |
BIN
dist/img/timeline2.png
vendored
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
dist/img/verticalTimeline.jpg
vendored
|
Before Width: | Height: | Size: 8.3 KiB |
BIN
dist/img/verticalTimeline.png
vendored
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
dist/img/verticalTimeline2.jpg
vendored
|
Before Width: | Height: | Size: 7.3 KiB |
BIN
dist/img/verticalTimeline3.jpg
vendored
|
Before Width: | Height: | Size: 6.9 KiB |
2
dist/js/app.js
vendored
65
dist/js/chunk-183b683c.js
vendored
69
dist/js/chunk-9e0371c2.js
vendored
Normal file
14
dist/js/chunk-vendors.js
vendored
13
index.html
@ -9,7 +9,7 @@
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}</script><link href="dist/css/chunk-vendors.css?227f61428db154a5d9bc" rel="stylesheet"><link href="dist/css/app.css?227f61428db154a5d9bc" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
|
||||
}</script><link href="dist/css/chunk-vendors.css?1c8f9269e64b9476f0c7" rel="stylesheet"><link href="dist/css/app.css?1c8f9269e64b9476f0c7" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>const getDataFromBackend = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
@ -28,7 +28,6 @@
|
||||
config: {},
|
||||
view: null
|
||||
},
|
||||
mindMapConfig: {},
|
||||
lang: 'zh',
|
||||
localConfig: null
|
||||
})
|
||||
@ -45,14 +44,6 @@
|
||||
window.takeOverAppMethods.saveMindMapData = data => {
|
||||
console.log(data)
|
||||
}
|
||||
// 获取思维导图配置,也就是实例化时会传入的选项
|
||||
window.takeOverAppMethods.getMindMapConfig = () => {
|
||||
return data.mindMapConfig
|
||||
}
|
||||
// 保存思维导图配置
|
||||
window.takeOverAppMethods.saveMindMapConfig = config => {
|
||||
console.log(config)
|
||||
}
|
||||
// 获取语言的函数
|
||||
window.takeOverAppMethods.getLanguage = () => {
|
||||
return data.lang
|
||||
@ -83,4 +74,4 @@
|
||||
// 可以通过window.$bus.$on()来监听应用的一些事件
|
||||
// 实例化页面
|
||||
window.initApp()
|
||||
}</script><script src="dist/js/chunk-vendors.js?227f61428db154a5d9bc"></script><script src="dist/js/app.js?227f61428db154a5d9bc"></script></body></html>
|
||||
}</script><script src="dist/js/chunk-vendors.js?1c8f9269e64b9476f0c7"></script><script src="dist/js/app.js?1c8f9269e64b9476f0c7"></script></body></html>
|
||||
BIN
qrcode.jpg
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
@ -1,21 +0,0 @@
|
||||
const { exec } = require('child_process')
|
||||
const fs = require('fs')
|
||||
|
||||
const base = './src/plugins/'
|
||||
const list = fs.readdirSync(base)
|
||||
const files = []
|
||||
list.forEach(item => {
|
||||
const stat = fs.statSync(base + item)
|
||||
if (stat.isFile()) {
|
||||
files.push(item)
|
||||
}
|
||||
})
|
||||
const str = files
|
||||
.map(item => {
|
||||
return base + item
|
||||
})
|
||||
.join(' ')
|
||||
|
||||
exec(
|
||||
`tsc ${str} --declaration --allowJs --emitDeclarationOnly --outDir types/src/ --target es2017 --skipLibCheck `
|
||||
)
|
||||
@ -17,7 +17,11 @@ const createFullData = () => {
|
||||
};
|
||||
}
|
||||
|
||||
// 节点较多示例数据
|
||||
/**
|
||||
* @Author: 王林
|
||||
* @Date: 2021-04-15 22:23:24
|
||||
* @Desc: 节点较多示例数据
|
||||
*/
|
||||
const data1 = {
|
||||
"root": {
|
||||
"data": {
|
||||
@ -932,5 +936,6 @@ export default {
|
||||
"layout": "logicalStructure",
|
||||
// "layout": "mindMap",
|
||||
// "layout": "catalogOrganization"
|
||||
// "layout": "organizationStructure"
|
||||
// "layout": "organizationStructure",
|
||||
"config": {}
|
||||
}
|
||||
@ -18,20 +18,20 @@ import Formula from './src/plugins/Formula.js'
|
||||
import RainbowLines from './src/plugins/RainbowLines.js'
|
||||
import Demonstrate from './src/plugins/Demonstrate.js'
|
||||
import OuterFrame from './src/plugins/OuterFrame.js'
|
||||
import MindMapLayoutPro from './src/plugins/MindMapLayoutPro.js'
|
||||
import NodeBase64ImageStorage from './src/plugins/NodeBase64ImageStorage.js'
|
||||
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 * as defaultTheme from './src/theme/default.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.version = '0.14.0-fix.1'
|
||||
MindMap.version = '0.11.0'
|
||||
|
||||
MindMap.usePlugin(MiniMap)
|
||||
.usePlugin(Watermark)
|
||||
@ -52,7 +52,5 @@ MindMap.usePlugin(MiniMap)
|
||||
.usePlugin(RainbowLines)
|
||||
.usePlugin(Demonstrate)
|
||||
.usePlugin(OuterFrame)
|
||||
.usePlugin(MindMapLayoutPro)
|
||||
.usePlugin(NodeBase64ImageStorage)
|
||||
|
||||
export default MindMap
|
||||
|
||||
@ -2,7 +2,7 @@ import View from './src/core/view/View'
|
||||
import Event from './src/core/event/Event'
|
||||
import Render from './src/core/render/Render'
|
||||
import merge from 'deepmerge'
|
||||
import theme from './src/theme'
|
||||
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'
|
||||
@ -11,22 +11,19 @@ import {
|
||||
layoutValueList,
|
||||
CONSTANTS,
|
||||
ERROR_TYPES,
|
||||
cssContent,
|
||||
nodeDataNoStylePropList
|
||||
cssContent
|
||||
} from './src/constants/constant'
|
||||
import { SVG, G, Rect } from '@svgdotjs/svg.js'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import {
|
||||
simpleDeepClone,
|
||||
getObjectChangedProps,
|
||||
isUndef,
|
||||
handleGetSvgDataExtraContent,
|
||||
getNodeTreeBoundingRect,
|
||||
mergeTheme,
|
||||
createUidForAppointNodes
|
||||
getNodeTreeBoundingRect
|
||||
} from './src/utils'
|
||||
import defaultTheme, {
|
||||
checkIsNodeSizeIndependenceConfig
|
||||
} from './src/theme/default'
|
||||
} from './src/themes/default'
|
||||
import { defaultOpt } from './src/constants/defaultOptions'
|
||||
|
||||
// 思维导图
|
||||
@ -37,7 +34,6 @@ class MindMap {
|
||||
* @param {defaultOpt} opt
|
||||
*/
|
||||
constructor(opt = {}) {
|
||||
MindMap.instanceCount++
|
||||
// 合并选项
|
||||
this.opt = this.handleOpt(merge(defaultOpt, opt))
|
||||
// 预处理节点数据
|
||||
@ -54,50 +50,9 @@ class MindMap {
|
||||
this.initWidth = this.width
|
||||
this.initHeight = this.height
|
||||
|
||||
// 必要的css样式
|
||||
// 添加css
|
||||
this.cssEl = null
|
||||
this.cssTextMap = {} // 该样式在实例化时会动态添加到页面,同时导出为svg时也会添加到svg源码中
|
||||
|
||||
// 节点前置/后置内容列表
|
||||
/*
|
||||
{
|
||||
name: '',// 一个唯一的类型标识
|
||||
// 创建节点的显示内容:节点元素、宽高
|
||||
createContent: (node) => {
|
||||
return {
|
||||
node: null,
|
||||
width: 0,
|
||||
height: 0
|
||||
}
|
||||
},
|
||||
// 创建保存到节点实例的opt对象中的数据
|
||||
createNodeData: () => {},
|
||||
// 更新节点实例的opt数据,返回数据是否改变了
|
||||
updateNodeData: () => {},
|
||||
}
|
||||
*/
|
||||
this.nodeInnerPrefixList = []
|
||||
this.nodeInnerPostfixList = []
|
||||
|
||||
// 编辑节点的类名列表,快捷键响应会检查事件目标是否是body或该列表中的元素,是的话才会响应
|
||||
// 该检查可以通过customCheckEnableShortcut选项来覆盖
|
||||
this.editNodeClassList = []
|
||||
|
||||
// 扩展的节点形状列表
|
||||
/*
|
||||
{
|
||||
createShape: (node) => {
|
||||
return path
|
||||
},
|
||||
getPadding: ({ node, width, height, paddingX, paddingY }) => {
|
||||
return {
|
||||
paddingX: 0,
|
||||
paddingY: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
this.extendShapeList = []
|
||||
this.addCss()
|
||||
|
||||
// 画布
|
||||
this.initContainer()
|
||||
@ -108,15 +63,6 @@ class MindMap {
|
||||
// 初始化缓存数据
|
||||
this.initCache()
|
||||
|
||||
// 注册插件
|
||||
MindMap.pluginList
|
||||
.filter(plugin => {
|
||||
return plugin.preload
|
||||
})
|
||||
.forEach(plugin => {
|
||||
this.initPlugin(plugin)
|
||||
})
|
||||
|
||||
// 事件类
|
||||
this.event = new Event({
|
||||
mindMap: this
|
||||
@ -146,24 +92,15 @@ class MindMap {
|
||||
this.batchExecution = new BatchExecution()
|
||||
|
||||
// 注册插件
|
||||
MindMap.pluginList
|
||||
.filter(plugin => {
|
||||
return !plugin.preload
|
||||
})
|
||||
.forEach(plugin => {
|
||||
this.initPlugin(plugin)
|
||||
})
|
||||
|
||||
// 添加必要的css样式
|
||||
this.addCss()
|
||||
MindMap.pluginList.forEach(plugin => {
|
||||
this.initPlugin(plugin)
|
||||
})
|
||||
|
||||
// 初始渲染
|
||||
this.render(this.opt.fit ? () => this.view.fit() : () => {})
|
||||
|
||||
// 将初始数据添加到历史记录堆栈中
|
||||
if (this.opt.addHistoryOnInit && this.opt.data) {
|
||||
this.command.addHistory()
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (this.opt.data) this.command.addHistory()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 配置参数处理
|
||||
@ -185,8 +122,6 @@ class MindMap {
|
||||
if (data.data && !data.data.expand) {
|
||||
data.data.expand = true
|
||||
}
|
||||
// 给没有uid的节点添加uid
|
||||
createUidForAppointNodes([data], false, null, true)
|
||||
return data
|
||||
}
|
||||
|
||||
@ -233,75 +168,25 @@ class MindMap {
|
||||
this.otherDraw.clear()
|
||||
}
|
||||
|
||||
// 追加必要的css样式
|
||||
// 该样式在实例化时会动态添加到页面,同时导出为svg时也会添加到svg源码中
|
||||
appendCss(key, str) {
|
||||
this.cssTextMap[key] = str
|
||||
this.removeCss()
|
||||
this.addCss()
|
||||
}
|
||||
|
||||
// 移除追加的css样式
|
||||
removeAppendCss(key) {
|
||||
if (this.cssTextMap[key]) {
|
||||
delete this.cssTextMap[key]
|
||||
this.removeCss()
|
||||
this.addCss()
|
||||
}
|
||||
}
|
||||
|
||||
// 拼接必要的css样式
|
||||
joinCss() {
|
||||
return (
|
||||
cssContent +
|
||||
Object.keys(this.cssTextMap)
|
||||
.map(key => {
|
||||
return this.cssTextMap[key]
|
||||
})
|
||||
.join('\n')
|
||||
)
|
||||
}
|
||||
|
||||
// 添加必要的css样式到页面
|
||||
addCss() {
|
||||
this.cssEl = document.createElement('style')
|
||||
this.cssEl.type = 'text/css'
|
||||
this.cssEl.innerHTML = this.joinCss()
|
||||
this.cssEl.innerHTML = cssContent
|
||||
document.head.appendChild(this.cssEl)
|
||||
}
|
||||
|
||||
// 移除css
|
||||
removeCss() {
|
||||
if (this.cssEl) document.head.removeChild(this.cssEl)
|
||||
}
|
||||
|
||||
// 检查某个编辑节点类名是否存在,返回索引
|
||||
checkEditNodeClassIndex(className) {
|
||||
return this.editNodeClassList.findIndex(item => {
|
||||
return item === className
|
||||
})
|
||||
}
|
||||
|
||||
// 添加一个编辑节点类名
|
||||
addEditNodeClass(className) {
|
||||
const index = this.checkEditNodeClassIndex(className)
|
||||
if (index === -1) {
|
||||
this.editNodeClassList.push(className)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除一个编辑节点类名
|
||||
deleteEditNodeClass(className) {
|
||||
const index = this.checkEditNodeClassIndex(className)
|
||||
if (index !== -1) {
|
||||
this.editNodeClassList.splice(index, 1)
|
||||
}
|
||||
document.head.removeChild(this.cssEl)
|
||||
}
|
||||
|
||||
// 渲染,部分渲染
|
||||
render(callback, source = '') {
|
||||
this.initTheme()
|
||||
this.renderer.render(callback, source)
|
||||
this.batchExecution.push('render', () => {
|
||||
this.initTheme()
|
||||
this.renderer.render(callback, source)
|
||||
})
|
||||
}
|
||||
|
||||
// 重新渲染
|
||||
@ -367,10 +252,7 @@ class MindMap {
|
||||
// 设置主题
|
||||
initTheme() {
|
||||
// 合并主题配置
|
||||
this.themeConfig = mergeTheme(
|
||||
theme[this.opt.theme] || theme.default,
|
||||
this.opt.themeConfig
|
||||
)
|
||||
this.themeConfig = merge(theme[this.opt.theme], this.opt.themeConfig)
|
||||
// 设置背景样式
|
||||
Style.setBackgroundStyle(this.el, this.themeConfig)
|
||||
}
|
||||
@ -397,7 +279,7 @@ class MindMap {
|
||||
this.opt.themeConfig = config
|
||||
if (!notRender) {
|
||||
// 检查改变的是否是节点大小无关的主题属性
|
||||
const res = checkIsNodeSizeIndependenceConfig(changedConfig)
|
||||
let res = checkIsNodeSizeIndependenceConfig(changedConfig)
|
||||
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
|
||||
}
|
||||
}
|
||||
@ -420,11 +302,8 @@ class MindMap {
|
||||
// 更新配置
|
||||
updateConfig(opt = {}) {
|
||||
this.emit('before_update_config', this.opt)
|
||||
const lastOpt = {
|
||||
...this.opt
|
||||
}
|
||||
this.opt = this.handleOpt(merge.all([defaultOpt, this.opt, opt]))
|
||||
this.emit('after_update_config', this.opt, lastOpt)
|
||||
this.emit('after_update_config', this.opt)
|
||||
}
|
||||
|
||||
// 获取当前布局结构
|
||||
@ -454,24 +333,20 @@ class MindMap {
|
||||
|
||||
// 更新画布数据,如果新的数据是在当前画布节点数据基础上增删改查后形成的,那么可以使用该方法来更新画布数据
|
||||
updateData(data) {
|
||||
data = this.handleData(data)
|
||||
this.emit('before_update_data', data)
|
||||
this.renderer.setData(data)
|
||||
this.render()
|
||||
this.command.addHistory()
|
||||
this.emit('update_data', data)
|
||||
}
|
||||
|
||||
// 动态设置思维导图数据,纯节点数据
|
||||
setData(data) {
|
||||
data = this.handleData(data)
|
||||
this.emit('before_set_data', data)
|
||||
this.opt.data = data
|
||||
this.execCommand('CLEAR_ACTIVE_NODE')
|
||||
this.command.clearHistory()
|
||||
this.command.addHistory()
|
||||
this.renderer.setData(data)
|
||||
this.reRender()
|
||||
this.reRender(() => {}, CONSTANTS.SET_DATA)
|
||||
this.emit('set_data', data)
|
||||
}
|
||||
|
||||
@ -544,20 +419,11 @@ class MindMap {
|
||||
}
|
||||
const isReadonly = mode === CONSTANTS.MODE.READONLY
|
||||
if (isReadonly === this.opt.readonly) return
|
||||
if (isReadonly) {
|
||||
// 如果处于编辑态,要隐藏所有的编辑框
|
||||
if (this.renderer.textEdit.isShowTextEdit()) {
|
||||
this.renderer.textEdit.hideEditTextBox()
|
||||
this.command.originAddHistory()
|
||||
}
|
||||
this.opt.readonly = isReadonly
|
||||
if (this.opt.readonly) {
|
||||
// 取消当前激活的元素
|
||||
this.execCommand('CLEAR_ACTIVE_NODE')
|
||||
}
|
||||
this.opt.readonly = isReadonly
|
||||
// 切换为编辑模式时,如果历史记录堆栈是空的,那么进行一次入栈操作
|
||||
if (!isReadonly && this.command.history.length <= 0) {
|
||||
this.command.originAddHistory()
|
||||
}
|
||||
this.emit('mode_change', mode)
|
||||
}
|
||||
|
||||
@ -643,7 +509,7 @@ class MindMap {
|
||||
this.watermark.isInExport = false
|
||||
}
|
||||
// 添加必要的样式
|
||||
[this.joinCss(), ...cssTextList].forEach(s => {
|
||||
;[cssContent, ...cssTextList].forEach(s => {
|
||||
clone.add(SVG(`<style>${s}</style>`))
|
||||
})
|
||||
// 附加内容
|
||||
@ -692,42 +558,13 @@ class MindMap {
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展节点形状
|
||||
addShape(shape) {
|
||||
if (!shape) return
|
||||
const exist = this.extendShapeList.find(item => {
|
||||
return item.name === shape.name
|
||||
})
|
||||
if (exist) return
|
||||
this.extendShapeList.push(shape)
|
||||
}
|
||||
|
||||
// 删除扩展的形状
|
||||
removeShape(name) {
|
||||
const index = this.extendShapeList.findIndex(item => {
|
||||
return item.name === name
|
||||
})
|
||||
if (index !== -1) {
|
||||
this.extendShapeList.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取SVG.js库的一些对象
|
||||
getSvgObjects() {
|
||||
return {
|
||||
SVG,
|
||||
G,
|
||||
Rect
|
||||
}
|
||||
}
|
||||
|
||||
// 添加插件
|
||||
addPlugin(plugin, opt) {
|
||||
let index = MindMap.hasPlugin(plugin)
|
||||
if (index === -1) {
|
||||
MindMap.usePlugin(plugin, opt)
|
||||
this.initPlugin(plugin)
|
||||
}
|
||||
this.initPlugin(plugin)
|
||||
}
|
||||
|
||||
// 移除插件
|
||||
@ -746,7 +583,6 @@ class MindMap {
|
||||
|
||||
// 实例化插件
|
||||
initPlugin(plugin) {
|
||||
if (this[plugin.instanceName]) return
|
||||
this[plugin.instanceName] = new plugin({
|
||||
mindMap: this,
|
||||
pluginOpt: plugin.pluginOpt
|
||||
@ -780,43 +616,9 @@ class MindMap {
|
||||
this.el.innerHTML = ''
|
||||
this.el = null
|
||||
this.removeCss()
|
||||
MindMap.instanceCount--
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展节点数据中非样式的字段列表
|
||||
// 内部会根据这个列表判断,如果不在这个列表里的字段都会认为是样式字段
|
||||
/*
|
||||
比如一个节点的数据为:
|
||||
|
||||
{
|
||||
data: {
|
||||
text: '',
|
||||
note: '',
|
||||
color: ''
|
||||
},
|
||||
children: []
|
||||
}
|
||||
|
||||
color字段不在nodeDataNoStylePropList列表中,所以是样式,内部一些操作的方法会用到,所以如果你新增了自定义的节点数据,并且不是`_`开头的,那么需要通过该方法扩展
|
||||
*/
|
||||
let _extendNodeDataNoStylePropList = []
|
||||
MindMap.extendNodeDataNoStylePropList = (list = []) => {
|
||||
_extendNodeDataNoStylePropList.push(...list)
|
||||
nodeDataNoStylePropList.push(...list)
|
||||
}
|
||||
MindMap.resetNodeDataNoStylePropList = () => {
|
||||
_extendNodeDataNoStylePropList.forEach(item => {
|
||||
const index = nodeDataNoStylePropList.findIndex(item2 => {
|
||||
return item2 === item
|
||||
})
|
||||
if (index !== -1) {
|
||||
nodeDataNoStylePropList.splice(index, 1)
|
||||
}
|
||||
})
|
||||
_extendNodeDataNoStylePropList = []
|
||||
}
|
||||
|
||||
// 插件列表
|
||||
MindMap.pluginList = []
|
||||
MindMap.usePlugin = (plugin, opt = {}) => {
|
||||
@ -830,20 +632,13 @@ MindMap.hasPlugin = plugin => {
|
||||
return item === plugin
|
||||
})
|
||||
}
|
||||
MindMap.instanceCount = 0
|
||||
|
||||
// 定义新主题
|
||||
MindMap.defineTheme = (name, config = {}) => {
|
||||
if (theme[name]) {
|
||||
return new Error('该主题名称已存在')
|
||||
}
|
||||
theme[name] = mergeTheme(defaultTheme, config)
|
||||
}
|
||||
// 移除主题
|
||||
MindMap.removeTheme = name => {
|
||||
if (theme[name]) {
|
||||
theme[name] = null
|
||||
}
|
||||
theme[name] = merge(defaultTheme, config)
|
||||
}
|
||||
|
||||
export default MindMap
|
||||
|
||||
18
simple-mind-map/package-lock.json
generated
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.14.0",
|
||||
"version": "0.10.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.14.0",
|
||||
"version": "0.10.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "3.2.0",
|
||||
@ -15,7 +15,7 @@
|
||||
"katex": "^0.16.8",
|
||||
"mdast-util-from-markdown": "^1.3.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"quill": "^2.0.3",
|
||||
"quill": "^2.0.2",
|
||||
"tern": "^0.24.3",
|
||||
"uuid": "^9.0.0",
|
||||
"ws": "^7.5.9",
|
||||
@ -1822,9 +1822,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/quill": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
|
||||
"integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz",
|
||||
"integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
@ -3553,9 +3553,9 @@
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
|
||||
},
|
||||
"quill": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
|
||||
"integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz",
|
||||
"integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==",
|
||||
"requires": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.14.0-fix.1",
|
||||
"version": "0.11.0",
|
||||
"description": "一个简单的web在线思维导图",
|
||||
"authors": [
|
||||
{
|
||||
@ -22,7 +22,7 @@
|
||||
"scripts": {
|
||||
"lint": "eslint src/",
|
||||
"format": "prettier --write .",
|
||||
"types": "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir types --target es2017 --skipLibCheck & node ./bin/createPluginsTypeFiles.js",
|
||||
"types": "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir types --target es2017 --skipLibCheck",
|
||||
"wsServe": "node ./bin/wsServer.mjs"
|
||||
},
|
||||
"module": "index.js",
|
||||
@ -35,7 +35,7 @@
|
||||
"katex": "^0.16.8",
|
||||
"mdast-util-from-markdown": "^1.3.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"quill": "^2.0.3",
|
||||
"quill": "^2.0.2",
|
||||
"tern": "^0.24.3",
|
||||
"uuid": "^9.0.0",
|
||||
"ws": "^7.5.9",
|
||||
|
||||
@ -1,7 +1,173 @@
|
||||
// 主题列表
|
||||
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: {
|
||||
READONLY: 'readonly',
|
||||
EDIT: 'edit'
|
||||
@ -15,12 +181,7 @@ export const CONSTANTS = {
|
||||
TIMELINE: 'timeline',
|
||||
TIMELINE2: 'timeline2',
|
||||
FISHBONE: 'fishbone',
|
||||
FISHBONE2: 'fishbone2',
|
||||
RIGHT_FISHBONE: 'rightFishbone',
|
||||
RIGHT_FISHBONE2: 'rightFishbone2',
|
||||
VERTICAL_TIMELINE: 'verticalTimeline',
|
||||
VERTICAL_TIMELINE2: 'verticalTimeline2',
|
||||
VERTICAL_TIMELINE3: 'verticalTimeline3'
|
||||
VERTICAL_TIMELINE: 'verticalTimeline'
|
||||
},
|
||||
DIR: {
|
||||
UP: 'up',
|
||||
@ -75,13 +236,7 @@ export const CONSTANTS = {
|
||||
NOT_ACTIVE: 'notActive',
|
||||
ACTIVE_ONLY: 'activeOnly'
|
||||
},
|
||||
TAG_PLACEMENT: {
|
||||
RIGHT: 'right',
|
||||
BOTTOM: 'bottom'
|
||||
},
|
||||
IMG_PLACEMENT: {
|
||||
LEFT: 'left',
|
||||
TOP: 'top',
|
||||
TAG_POSITION: {
|
||||
RIGHT: 'right',
|
||||
BOTTOM: 'bottom'
|
||||
}
|
||||
@ -129,29 +284,9 @@ export const layoutList = [
|
||||
name: '竖向时间轴',
|
||||
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE
|
||||
},
|
||||
{
|
||||
name: '竖向时间轴2',
|
||||
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE2
|
||||
},
|
||||
{
|
||||
name: '竖向时间轴3',
|
||||
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE3
|
||||
},
|
||||
{
|
||||
name: '鱼骨图',
|
||||
value: CONSTANTS.LAYOUT.FISHBONE
|
||||
},
|
||||
{
|
||||
name: '鱼骨图2',
|
||||
value: CONSTANTS.LAYOUT.FISHBONE2
|
||||
},
|
||||
{
|
||||
name: '向右鱼骨图',
|
||||
value: CONSTANTS.LAYOUT.RIGHT_FISHBONE
|
||||
},
|
||||
{
|
||||
name: '向右鱼骨图2',
|
||||
value: CONSTANTS.LAYOUT.RIGHT_FISHBONE2
|
||||
}
|
||||
]
|
||||
export const layoutValueList = [
|
||||
@ -163,12 +298,7 @@ export const layoutValueList = [
|
||||
CONSTANTS.LAYOUT.TIMELINE,
|
||||
CONSTANTS.LAYOUT.TIMELINE2,
|
||||
CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
|
||||
CONSTANTS.LAYOUT.VERTICAL_TIMELINE2,
|
||||
CONSTANTS.LAYOUT.VERTICAL_TIMELINE3,
|
||||
CONSTANTS.LAYOUT.FISHBONE,
|
||||
CONSTANTS.LAYOUT.FISHBONE2,
|
||||
CONSTANTS.LAYOUT.RIGHT_FISHBONE,
|
||||
CONSTANTS.LAYOUT.RIGHT_FISHBONE2
|
||||
CONSTANTS.LAYOUT.FISHBONE
|
||||
]
|
||||
|
||||
// 节点数据中非样式的字段
|
||||
@ -186,7 +316,7 @@ export const nodeDataNoStylePropList = [
|
||||
'isActive',
|
||||
'generalization',
|
||||
'richText',
|
||||
'resetRichText', // 重新创建富文本内容,去掉原有样式
|
||||
'resetRichText',
|
||||
'uid',
|
||||
'activeStyle',
|
||||
'associativeLineTargets',
|
||||
@ -198,15 +328,7 @@ export const nodeDataNoStylePropList = [
|
||||
'notation',
|
||||
'outerFrame',
|
||||
'number',
|
||||
'range',
|
||||
'customLeft',
|
||||
'customTop',
|
||||
'customTextWidth',
|
||||
'checkbox',
|
||||
'dir',
|
||||
'needUpdate', // 重新创建节点内容
|
||||
'imgMap',
|
||||
'nodeLink'
|
||||
'range'
|
||||
]
|
||||
|
||||
// 错误类型
|
||||
@ -239,10 +361,6 @@ export const cssContent = `
|
||||
opacity: 1;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.smm-text-node-wrap, .smm-expand-btn-text {
|
||||
user-select: none;
|
||||
}
|
||||
`
|
||||
|
||||
// html自闭合标签列表
|
||||
@ -255,17 +373,3 @@ export const selfCloseTagList = [
|
||||
'meta',
|
||||
'area'
|
||||
]
|
||||
|
||||
// 非富文本模式下的节点文本行高
|
||||
export const noneRichTextNodeLineHeight = 1.2
|
||||
|
||||
// 富文本支持的样式列表
|
||||
export const richTextSupportStyleList = [
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontWeight',
|
||||
'fontStyle',
|
||||
'textDecoration',
|
||||
'color',
|
||||
'textAlign'
|
||||
]
|
||||
|
||||
@ -7,8 +7,6 @@ export const defaultOpt = {
|
||||
el: null,
|
||||
// 思维导图回显数据
|
||||
data: null,
|
||||
// 要恢复的视图数据,一般通过mindMap.view.getTransformData()方法获取
|
||||
viewData: null,
|
||||
// 是否只读
|
||||
readonly: false,
|
||||
// 布局
|
||||
@ -21,20 +19,12 @@ export const defaultOpt = {
|
||||
themeConfig: {},
|
||||
// 放大缩小的增量比例
|
||||
scaleRatio: 0.2,
|
||||
// 平移的步长比例,只在鼠标滚轮和触控板触发的平移中应用
|
||||
translateRatio: 1,
|
||||
// 最小缩小值,百分数,最小为0,该选项只会影响view.narrow方法(影响的行为为Ctrl+-快捷键、鼠标滚轮及触控板),不会影响其他方法,比如view.setScale,所以需要你自行限制大小
|
||||
minZoomRatio: 20,
|
||||
// 最大放大值,百分数,传-1代表不限制,否则传0以上数字,,该选项只会影响view.enlarge方法
|
||||
maxZoomRatio: 400,
|
||||
// 自定义判断wheel事件是否来自电脑的触控板
|
||||
// 默认是通过判断e.deltaY的值是否小于10,显然这种方法是不准确的,当鼠标滚动的很慢,或者触摸移动的很快时判断就失效了,如果你有更好的方法,欢迎提交issue
|
||||
// 如果你希望自己来判断,那么传递一个函数,接收一个参数e(事件对象),需要返回true或false,代表是否是来自触控板
|
||||
customCheckIsTouchPad: null,
|
||||
// 鼠标缩放是否以鼠标当前位置为中心点,否则以画布中心点
|
||||
mouseScaleCenterUseMousePosition: true,
|
||||
// 最多显示几个标签
|
||||
maxTag: 5,
|
||||
// 标签显示的位置,相对于节点文本,bottom(下方)、right(右侧)
|
||||
tagPosition: CONSTANTS.TAG_POSITION.RIGHT,
|
||||
// 展开收缩按钮尺寸
|
||||
expandBtnSize: 20,
|
||||
// 节点里图片和文字的间距
|
||||
@ -77,14 +67,13 @@ export const defaultOpt = {
|
||||
close: ''
|
||||
},
|
||||
// 处理收起节点数量
|
||||
expandBtnNumHandler: null,
|
||||
expandBtnNumHandler: num => {
|
||||
return num
|
||||
},
|
||||
// 是否显示带数量的收起按钮
|
||||
isShowExpandNum: true,
|
||||
// 是否只有当鼠标在画布内才响应快捷键事件
|
||||
enableShortcutOnlyWhenMouseInSvg: true,
|
||||
// 自定义判断是否响应快捷键事件,优先级比enableShortcutOnlyWhenMouseInSvg选项高
|
||||
// 可以传递一个函数,接收事件对象e为参数,需要返回true或false,返回true代表允许响应快捷键事件,反之不允许,库默认当事件目标为body,或为文本编辑框元素(普通文本编辑框、富文本编辑框、关联线文本编辑框)时响应快捷键,其他不响应
|
||||
customCheckEnableShortcut: null,
|
||||
// 初始根节点的位置
|
||||
initRootNodePosition: null,
|
||||
// 节点文本编辑框的z-index
|
||||
@ -131,8 +120,6 @@ export const defaultOpt = {
|
||||
// 是否在存在一个激活节点时,当按下中文、英文、数字按键时自动进入文本编辑模式
|
||||
// 开启该特性后,需要给你的输入框绑定keydown事件,并禁止冒泡
|
||||
enableAutoEnterTextEditWhenKeydown: false,
|
||||
// 当enableAutoEnterTextEditWhenKeydown选项开启时生效,当通过按键进入文本编辑时是否自动清空原有文本
|
||||
autoEmptyTextWhenKeydownEnterEdit: false,
|
||||
// 自定义对剪贴板文本的处理。当按ctrl+v粘贴时会读取用户剪贴板中的文本和图片,默认只会判断文本是否是普通文本和simple-mind-map格式的节点数据,如果你想处理其他思维导图的数据,比如processon、zhixi等,那么可以传递一个函数,接受当前剪贴板中的文本为参数,返回处理后的数据,可以返回两种类型:
|
||||
/*
|
||||
1.返回一个纯文本,那么会直接以该文本创建一个子节点
|
||||
@ -189,6 +176,11 @@ export const defaultOpt = {
|
||||
addHistoryTime: 100,
|
||||
// 是否禁止拖动画布
|
||||
isDisableDrag: false,
|
||||
// 鼠标移入概要高亮所属节点时的高亮框样式
|
||||
highlightNodeBoxStyle: {
|
||||
stroke: 'rgb(94, 200, 248)',
|
||||
fill: 'transparent'
|
||||
},
|
||||
// 创建新节点时的行为
|
||||
/*
|
||||
DEFAULT :默认会激活新创建的节点,并且进入编辑模式。如果同时创建了多个新节点,那么只会激活而不会进入编辑模式
|
||||
@ -229,7 +221,7 @@ export const defaultOpt = {
|
||||
// 移动节点到画布中心、回到根节点等操作时是否将缩放层级复位为100%
|
||||
// 该选项实际影响的是render.moveNodeToCenter方法,moveNodeToCenter方法本身也存在第二个参数resetScale来设置是否复位,如果resetScale参数没有传递,那么使用resetScaleOnMoveNodeToCenter配置,否则使用resetScale配置
|
||||
resetScaleOnMoveNodeToCenter: false,
|
||||
// 添加附加的节点前置内容,前置内容指和文本同一行的区域中的前置内容,不包括节点图片部分。如果存在编号、任务勾选框内容,这里添加的前置内容会在这两者之后
|
||||
// 添加附加的节点前置内容,前置内容指和文本同一行的区域中的前置内容,不包括节点图片部分
|
||||
createNodePrefixContent: null,
|
||||
// 添加附加的节点后置内容,后置内容指和文本同一行的区域中的后置内容,不包括节点图片部分
|
||||
createNodePostfixContent: null,
|
||||
@ -246,83 +238,6 @@ export const defaultOpt = {
|
||||
padding: 100, // 超出画布四周指定范围内依旧渲染节点
|
||||
removeNodeWhenOutCanvas: true // 节点移除画布可视区域后从画布删除
|
||||
},
|
||||
// 如果节点文本为空,那么为了避免空白节点高度塌陷,会用该字段指定的文本测量一个高度
|
||||
emptyTextMeasureHeightText: 'abc123我和你',
|
||||
// 是否在进行节点文本编辑时实时更新节点大小和节点位置,开启后当节点数量比较多时可能会造成卡顿
|
||||
openRealtimeRenderOnNodeTextEdit: false,
|
||||
// 默认会给容器元素el绑定mousedown事件,可通过该选项设置是否阻止其默认事件
|
||||
// 如果设置为true,会带来一定问题,比如你聚焦在思维导图外的其他输入框,点击画布就不会触发其失焦
|
||||
mousedownEventPreventDefault: false,
|
||||
// 在激活上粘贴用户剪贴板中的数据时,如果同时存在文本和图片,那么只粘贴文本,忽略图片
|
||||
onlyPasteTextWhenHasImgAndText: true,
|
||||
// 是否允许拖拽调整节点的宽度,实际上压缩的是节点里面文本内容的宽度,当节点文本内容宽度压缩到最小时无法继续压缩。如果节点存在图片,那么最小值以图片宽度和文本内容最小宽度的最大值为准(目前该特性仅在两种情况下可用:1.开启了富文本模式,即注册了RichText插件;2.自定义节点内容)
|
||||
enableDragModifyNodeWidth: true,
|
||||
// 当允许拖拽调整节点的宽度时,可以通过该选项设置节点文本内容允许压缩的最小宽度
|
||||
minNodeTextModifyWidth: 20,
|
||||
// 同minNodeTextModifyWidth,最大值,传-1代表不限制
|
||||
maxNodeTextModifyWidth: -1,
|
||||
// 自定义处理节点的连线方法,可以传递一个函数,函数接收三个参数:node(节点实例)、line(节点的某条连线,@svgjs库的path对象), { width, color, dasharray },dasharray(该条连线的虚线样式,为none代表实线),你可以修改line对象来达到修改节点连线样式的效果,比如增加流动效果
|
||||
customHandleLine: null,
|
||||
// 实例化完后是否立刻进行一次历史数据入栈操作
|
||||
// 即调用mindMap.command.addHistory方法
|
||||
addHistoryOnInit: true,
|
||||
// 自定义节点备注图标
|
||||
noteIcon: {
|
||||
icon: '', // svg字符串,如果不是确定要使用svg自带的样式,否则请去除其中的fill等样式属性
|
||||
style: {
|
||||
// size: 20,// 图标大小,不手动设置则会使用主题的iconSize配置
|
||||
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
|
||||
}
|
||||
},
|
||||
// 自定义节点超链接图标
|
||||
hyperlinkIcon: {
|
||||
icon: '', // svg字符串,如果不是确定要使用svg自带的样式,否则请去除其中的fill等样式属性
|
||||
style: {
|
||||
// size: 20,// 图标大小,不手动设置则会使用主题的iconSize配置
|
||||
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
|
||||
}
|
||||
},
|
||||
// 自定义节点附件图标
|
||||
attachmentIcon: {
|
||||
icon: '', // svg字符串,如果不是确定要使用svg自带的样式,否则请去除其中的fill等样式属性
|
||||
style: {
|
||||
// size: 20,// 图标大小,不手动设置则会使用主题的iconSize配置
|
||||
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
|
||||
}
|
||||
},
|
||||
// 是否显示快捷创建子节点按钮
|
||||
isShowCreateChildBtnIcon: true,
|
||||
// 自定义快捷创建子节点按钮图标
|
||||
quickCreateChildBtnIcon: {
|
||||
icon: '', // svg字符串,如果不是确定要使用svg自带的样式,否则请去除其中的fill等样式属性
|
||||
style: {
|
||||
// 图标大小使用的是expandBtnSize选项
|
||||
// color: '',// 图标颜色,不手动设置则会使用expandBtnStyle选项的color字段
|
||||
}
|
||||
},
|
||||
// 自定义快捷创建子节点按钮的点击操作,
|
||||
customQuickCreateChildBtnClick: null,
|
||||
// 添加自定义的节点内容
|
||||
// 可传递一个对象,格式如下:
|
||||
/*
|
||||
{
|
||||
// 返回要添加的DOM元素详细
|
||||
create: (node) => {
|
||||
return {
|
||||
el, // DOM节点
|
||||
width: 20, // 宽高
|
||||
height: 20
|
||||
}
|
||||
},
|
||||
// 处理生成的@svgdotjs/svg.js库的ForeignObject节点实例,可以设置其在节点内的位置
|
||||
handle: ({ content, element, node }) => {
|
||||
|
||||
}
|
||||
}
|
||||
*/
|
||||
addCustomContentToNode: null,
|
||||
// 节点连线样式是否允许继承祖先的连线样式
|
||||
enableInheritAncestorLineStyle: true,
|
||||
|
||||
// 【Select插件】
|
||||
// 多选节点时鼠标移动到边缘时的画布移动偏移量
|
||||
@ -406,8 +321,6 @@ export const defaultOpt = {
|
||||
// 导出png、svg、pdf时会获取画布上的svg数据进行克隆,然后通过该克隆的元素进行导出,如果你想对该克隆元素做一些处理,比如新增、替换、修改其中的一些元素,那么可以通过该参数传递一个处理函数,接收svg元素对象,处理后,需要返回原svg元素对象。
|
||||
// 需要注意的是svg对象指的是@svgdotjs/svg.js库的元素对象,所以你需要阅读该库的文档来操作该对象
|
||||
handleBeingExportSvg: null,
|
||||
// 导出图片或pdf都是通过canvas将svg绘制出来,再导出,所以如果思维导图特别大,宽高可能会超出canvas支持的上限,所以会进行缩放,这个上限可以通过该参数设置,代表canvas宽和高的最大宽度
|
||||
maxCanvasSize: 16384,
|
||||
|
||||
// 【AssociativeLine插件】
|
||||
// 关联线默认文字
|
||||
@ -424,8 +337,6 @@ export const defaultOpt = {
|
||||
},
|
||||
// 是否允许调整关联线两个端点的位置
|
||||
enableAdjustAssociativeLinePoints: true,
|
||||
// 关联线连接即将完成时执行,如果要阻止本次连接可以返回true,函数接收一个参数:node(目标节点实例)
|
||||
beforeAssociativeLineConnection: null,
|
||||
|
||||
// 【TouchEvent插件】
|
||||
// 禁止双指缩放,你仍旧可以使用api进行缩放
|
||||
@ -492,32 +403,11 @@ export const defaultOpt = {
|
||||
transformRichTextOnEnterEdit: null,
|
||||
// 可以传递一个函数,即将结束富文本编辑前会执行该函数,函数接收richText实例,所以你可以在此时机更新quill文档数据
|
||||
beforeHideRichTextEdit: null,
|
||||
// 设置富文本节点编辑框和节点大小一致,形成伪原地编辑的效果
|
||||
// 需要注意的是,只有当节点内只有文本、且形状是矩形才会有比较好的效果
|
||||
richTextEditFakeInPlace: false,
|
||||
|
||||
// 【OuterFrame】插件
|
||||
outerFramePaddingX: 10,
|
||||
outerFramePaddingY: 10,
|
||||
defaultOuterFrameText: '外框',
|
||||
|
||||
// 【Painter】插件
|
||||
// 是否只格式刷节点手动设置的样式,不考虑节点通过主题的应用的样式
|
||||
onlyPainterNodeCustomStyles: false,
|
||||
|
||||
// 【NodeImgAdjust】插件
|
||||
// 拦截节点图片的删除,点击节点图片上的删除按钮删除图片前会调用该函数,如果函数返回true则取消删除
|
||||
beforeDeleteNodeImg: null,
|
||||
// 删除和调整两个按钮的大小
|
||||
imgResizeBtnSize: 25,
|
||||
// 最小允许缩放的尺寸,请传入>=0的数字
|
||||
minImgResizeWidth: 50,
|
||||
minImgResizeHeight: 50,
|
||||
// 最大允许缩放的尺寸依据主题的配置,即主题的imgMaxWidth和imgMaxHeight配置,如果设置为false,那么使用maxImgResizeWidth和maxImgResizeHeight选项
|
||||
maxImgResizeWidthInheritTheme: false,
|
||||
// 最大允许缩放的尺寸,maxImgResizeWidthInheritTheme选项设置为false时生效,不限制最大值可传递Infinity
|
||||
maxImgResizeWidth: Infinity,
|
||||
maxImgResizeHeight: Infinity,
|
||||
// 自定义删除按钮和尺寸调整按钮的内容
|
||||
// 默认为内置图标,你可以传递一个svg字符串,或者其他的html字符串
|
||||
// 整体大小请使用上面的minImgResizeWidth和minImgResizeHeight选项设置
|
||||
customDeleteBtnInnerHTML: '',
|
||||
customResizeBtnInnerHTML: ''
|
||||
outerFramePaddingY: 10
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
transformTreeDataToObject
|
||||
} from '../../utils'
|
||||
import { ERROR_TYPES } from '../../constants/constant'
|
||||
import pkg from '../../../package.json'
|
||||
|
||||
// 命令类
|
||||
class Command {
|
||||
@ -15,11 +14,10 @@ class Command {
|
||||
this.opt = opt
|
||||
this.mindMap = opt.mindMap
|
||||
this.commands = {}
|
||||
this.history = [] // 字符串形式存储
|
||||
this.history = []
|
||||
this.activeHistoryIndex = 0
|
||||
// 注册快捷键
|
||||
this.registerShortcutKeys()
|
||||
this.originAddHistory = this.addHistory.bind(this)
|
||||
this.addHistory = throttle(
|
||||
this.addHistory,
|
||||
this.mindMap.opt.addHistoryTime,
|
||||
@ -62,7 +60,6 @@ class Command {
|
||||
this.commands[name].forEach(fn => {
|
||||
fn(...args)
|
||||
})
|
||||
this.mindMap.emit('afterExecCommand', name, ...args)
|
||||
if (
|
||||
['BACK', 'FORWARD', 'SET_NODE_ACTIVE', 'CLEAR_ACTIVE_NODE'].includes(
|
||||
name
|
||||
@ -106,19 +103,18 @@ class Command {
|
||||
if (this.mindMap.opt.readonly || this.isPause) {
|
||||
return
|
||||
}
|
||||
this.mindMap.emit('beforeAddHistory')
|
||||
const lastDataStr =
|
||||
const lastData =
|
||||
this.history.length > 0 ? this.history[this.activeHistoryIndex] : null
|
||||
const data = this.getCopyData()
|
||||
const dataStr = JSON.stringify(data)
|
||||
// 此次数据和上次一样则不重复添加
|
||||
if (lastDataStr && lastDataStr === dataStr) {
|
||||
if (lastData === data) return
|
||||
if (lastData && JSON.stringify(lastData) === JSON.stringify(data)) {
|
||||
return
|
||||
}
|
||||
this.emitDataUpdatesEvent(lastDataStr, dataStr)
|
||||
this.emitDataUpdatesEvent(lastData, data)
|
||||
// 删除当前历史指针后面的数据
|
||||
this.history = this.history.slice(0, this.activeHistoryIndex + 1)
|
||||
this.history.push(dataStr)
|
||||
this.history.push(simpleDeepClone(data))
|
||||
// 历史记录数超过最大数量
|
||||
if (this.history.length > this.mindMap.opt.maxHistoryCount) {
|
||||
this.history.shift()
|
||||
@ -138,16 +134,15 @@ class Command {
|
||||
return
|
||||
}
|
||||
if (this.activeHistoryIndex - step >= 0) {
|
||||
const lastDataStr = this.history[this.activeHistoryIndex]
|
||||
const lastData = this.history[this.activeHistoryIndex]
|
||||
this.activeHistoryIndex -= step
|
||||
this.mindMap.emit(
|
||||
'back_forward',
|
||||
this.activeHistoryIndex,
|
||||
this.history.length
|
||||
)
|
||||
const dataStr = this.history[this.activeHistoryIndex]
|
||||
const data = JSON.parse(dataStr)
|
||||
this.emitDataUpdatesEvent(lastDataStr, dataStr)
|
||||
const data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.emitDataUpdatesEvent(lastData, data)
|
||||
return data
|
||||
}
|
||||
}
|
||||
@ -159,16 +154,15 @@ class Command {
|
||||
}
|
||||
let len = this.history.length
|
||||
if (this.activeHistoryIndex + step <= len - 1) {
|
||||
const lastDataStr = this.history[this.activeHistoryIndex]
|
||||
const lastData = this.history[this.activeHistoryIndex]
|
||||
this.activeHistoryIndex += step
|
||||
this.mindMap.emit(
|
||||
'back_forward',
|
||||
this.activeHistoryIndex,
|
||||
this.history.length
|
||||
)
|
||||
const dataStr = this.history[this.activeHistoryIndex]
|
||||
const data = JSON.parse(dataStr)
|
||||
this.emitDataUpdatesEvent(lastDataStr, dataStr)
|
||||
const data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.emitDataUpdatesEvent(lastData, data)
|
||||
return data
|
||||
}
|
||||
}
|
||||
@ -176,9 +170,7 @@ class Command {
|
||||
// 获取渲染树数据副本
|
||||
getCopyData() {
|
||||
if (!this.mindMap.renderer.renderTree) return null
|
||||
const res = copyRenderTree({}, this.mindMap.renderer.renderTree, true)
|
||||
res.smmVersion = pkg.version
|
||||
return res
|
||||
return copyRenderTree({}, this.mindMap.renderer.renderTree, true)
|
||||
}
|
||||
|
||||
// 移除节点数据中的uid
|
||||
@ -197,14 +189,12 @@ class Command {
|
||||
}
|
||||
|
||||
// 派发思维导图更新明细事件
|
||||
emitDataUpdatesEvent(lastDataStr, dataStr) {
|
||||
emitDataUpdatesEvent(lastData, data) {
|
||||
try {
|
||||
// 如果data_change_detail没有监听者,那么不进行计算,节省性能
|
||||
const eventName = 'data_change_detail'
|
||||
const count = this.mindMap.event.listenerCount(eventName)
|
||||
if (count > 0 && lastDataStr && dataStr) {
|
||||
const lastData = JSON.parse(lastDataStr)
|
||||
const data = JSON.parse(dataStr)
|
||||
if (count > 0 && lastData && data) {
|
||||
const lastDataObj = simpleDeepClone(transformTreeDataToObject(lastData))
|
||||
const dataObj = simpleDeepClone(transformTreeDataToObject(data))
|
||||
const res = []
|
||||
|
||||
@ -12,23 +12,9 @@ export default class KeyCommand {
|
||||
this.shortcutMapCache = {}
|
||||
this.isPause = false
|
||||
this.isInSvg = false
|
||||
this.isStopCheckInSvg = false
|
||||
this.defaultEnableCheck = this.defaultEnableCheck.bind(this)
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
// 扩展按键映射
|
||||
extendKeyMap(key, code) {
|
||||
keyMap[key] = code
|
||||
}
|
||||
|
||||
// 从按键映射中删除某个键
|
||||
removeKeyMap(key) {
|
||||
if (typeof keyMap[key] !== 'undefined') {
|
||||
delete keyMap[key]
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停快捷键响应
|
||||
pause() {
|
||||
this.isPause = true
|
||||
@ -59,22 +45,6 @@ export default class KeyCommand {
|
||||
this.shortcutMapCache = {}
|
||||
}
|
||||
|
||||
// 停止对鼠标是否在画布内的检查,前提是开启了enableShortcutOnlyWhenMouseInSvg选项
|
||||
// 库内部节点文本编辑、关联线文本编辑、外框文本编辑前都会暂停检查,否则无法响应回车快捷键用于结束编辑
|
||||
// 如果你新增了额外的文本编辑,也可以在编辑前调用此方法
|
||||
stopCheckInSvg() {
|
||||
const { enableShortcutOnlyWhenMouseInSvg } = this.mindMap.opt
|
||||
if (!enableShortcutOnlyWhenMouseInSvg) return
|
||||
this.isStopCheckInSvg = true
|
||||
}
|
||||
|
||||
// 恢复对鼠标是否在画布内的检查
|
||||
recoveryCheckInSvg() {
|
||||
const { enableShortcutOnlyWhenMouseInSvg } = this.mindMap.opt
|
||||
if (!enableShortcutOnlyWhenMouseInSvg) return
|
||||
this.isStopCheckInSvg = true
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
this.onKeydown = this.onKeydown.bind(this)
|
||||
@ -83,6 +53,13 @@ export default class KeyCommand {
|
||||
this.isInSvg = true
|
||||
})
|
||||
this.mindMap.on('svg_mouseleave', () => {
|
||||
if (this.mindMap.renderer.textEdit.isShowTextEdit()) return
|
||||
if (
|
||||
this.mindMap.associativeLine &&
|
||||
this.mindMap.associativeLine.showTextEdit
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.isInSvg = false
|
||||
})
|
||||
window.addEventListener('keydown', this.onKeydown)
|
||||
@ -96,37 +73,11 @@ export default class KeyCommand {
|
||||
window.removeEventListener('keydown', this.onKeydown)
|
||||
}
|
||||
|
||||
// 根据事件目标判断是否响应快捷键事件
|
||||
defaultEnableCheck(e) {
|
||||
const target = e.target
|
||||
if (target === document.body) return true
|
||||
for (let i = 0; i < this.mindMap.editNodeClassList.length; i++) {
|
||||
const cur = this.mindMap.editNodeClassList[i]
|
||||
if (target.classList.contains(cur)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 按键事件
|
||||
onKeydown(e) {
|
||||
const {
|
||||
enableShortcutOnlyWhenMouseInSvg,
|
||||
beforeShortcutRun,
|
||||
customCheckEnableShortcut
|
||||
} = this.mindMap.opt
|
||||
const checkFn =
|
||||
typeof customCheckEnableShortcut === 'function'
|
||||
? customCheckEnableShortcut
|
||||
: this.defaultEnableCheck
|
||||
if (!checkFn(e)) return
|
||||
if (
|
||||
this.isPause ||
|
||||
(enableShortcutOnlyWhenMouseInSvg &&
|
||||
!this.isStopCheckInSvg &&
|
||||
!this.isInSvg)
|
||||
) {
|
||||
const { enableShortcutOnlyWhenMouseInSvg, beforeShortcutRun } =
|
||||
this.mindMap.opt
|
||||
if (this.isPause || (enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) {
|
||||
return
|
||||
}
|
||||
Object.keys(this.shortcutMap).forEach(key => {
|
||||
|
||||
@ -156,14 +156,8 @@ class Event extends EventEmitter {
|
||||
// 判断是否是触控板
|
||||
let isTouchPad = false
|
||||
// mac、windows
|
||||
// if (e.wheelDeltaY === e.deltaY * -3 || Math.abs(e.wheelDeltaY) <= 10) {
|
||||
// isTouchPad = true
|
||||
// }
|
||||
const { customCheckIsTouchPad } = this.mindMap.opt
|
||||
if (typeof customCheckIsTouchPad === 'function') {
|
||||
isTouchPad = customCheckIsTouchPad(e)
|
||||
} else {
|
||||
isTouchPad = Math.abs(e.deltaY) <= 10
|
||||
if (e.wheelDeltaY === e.deltaY * -3 || Math.abs(e.wheelDeltaY) <= 10) {
|
||||
isTouchPad = true
|
||||
}
|
||||
this.emit('mousewheel', e, dirs, this, isTouchPad)
|
||||
}
|
||||
|
||||
@ -6,17 +6,9 @@ import {
|
||||
htmlEscape,
|
||||
handleInputPasteText,
|
||||
checkSmmFormatData,
|
||||
getTextFromHtml,
|
||||
isWhite,
|
||||
getVisibleColorFromTheme
|
||||
getTextFromHtml
|
||||
} from '../../utils'
|
||||
import {
|
||||
ERROR_TYPES,
|
||||
CONSTANTS,
|
||||
noneRichTextNodeLineHeight
|
||||
} from '../../constants/constant'
|
||||
|
||||
const SMM_NODE_EDIT_WRAP = 'smm-node-edit-wrap'
|
||||
import { ERROR_TYPES, CONSTANTS } from '../../constants/constant'
|
||||
|
||||
// 节点文字编辑类
|
||||
export default class TextEdit {
|
||||
@ -33,10 +25,6 @@ export default class TextEdit {
|
||||
// 如果编辑过程中缩放画布了,那么缓存当前编辑的内容
|
||||
this.cacheEditingText = ''
|
||||
this.hasBodyMousedown = false
|
||||
this.textNodePaddingX = 5
|
||||
this.textNodePaddingY = 3
|
||||
this.isNeedUpdateTextEditNode = false
|
||||
this.mindMap.addEditNodeClass(SMM_NODE_EDIT_WRAP)
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@ -95,51 +83,13 @@ export default class TextEdit {
|
||||
})
|
||||
})
|
||||
this.mindMap.on('scale', this.onScale)
|
||||
// 监听按键事件,判断是否自动进入文本编辑模式
|
||||
// // 监听按键事件,判断是否自动进入文本编辑模式
|
||||
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
|
||||
window.addEventListener('keydown', this.onKeydown)
|
||||
}
|
||||
this.mindMap.on('beforeDestroy', () => {
|
||||
this.unBindEvent()
|
||||
})
|
||||
this.mindMap.on('after_update_config', (opt, lastOpt) => {
|
||||
if (
|
||||
opt.openRealtimeRenderOnNodeTextEdit !==
|
||||
lastOpt.openRealtimeRenderOnNodeTextEdit
|
||||
) {
|
||||
if (this.mindMap.richText) {
|
||||
this.mindMap.richText.onOpenRealtimeRenderOnNodeTextEditConfigUpdate(
|
||||
opt.openRealtimeRenderOnNodeTextEdit
|
||||
)
|
||||
} else {
|
||||
this.onOpenRealtimeRenderOnNodeTextEditConfigUpdate(
|
||||
opt.openRealtimeRenderOnNodeTextEdit
|
||||
)
|
||||
}
|
||||
}
|
||||
if (
|
||||
opt.enableAutoEnterTextEditWhenKeydown !==
|
||||
lastOpt.enableAutoEnterTextEditWhenKeydown
|
||||
) {
|
||||
window[
|
||||
opt.enableAutoEnterTextEditWhenKeydown
|
||||
? 'addEventListener'
|
||||
: 'removeEventListener'
|
||||
]('keydown', this.onKeydown)
|
||||
}
|
||||
})
|
||||
// 正在编辑文本时,给节点添加了图标等其他内容时需要更新编辑框的位置
|
||||
this.mindMap.on('afterExecCommand', () => {
|
||||
if (!this.isShowTextEdit()) return
|
||||
this.isNeedUpdateTextEditNode = true
|
||||
})
|
||||
this.mindMap.on('node_tree_render_end', () => {
|
||||
if (!this.isShowTextEdit()) return
|
||||
if (this.isNeedUpdateTextEditNode) {
|
||||
this.isNeedUpdateTextEditNode = false
|
||||
this.updateTextEditNode()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
@ -149,15 +99,11 @@ export default class TextEdit {
|
||||
|
||||
// 按键事件
|
||||
onKeydown(e) {
|
||||
if (e.target !== document.body) return
|
||||
const activeNodeList = this.mindMap.renderer.activeNodeList
|
||||
if (activeNodeList.length <= 0 || activeNodeList.length > 1) return
|
||||
const node = activeNodeList[0]
|
||||
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
|
||||
if (node && this.checkIsAutoEnterTextEditKey(e)) {
|
||||
// 忽略第一个键值,避免中文输入法时进入编辑会导致第一个键值变成字母的问题
|
||||
// 带来的问题是按的第一下纯粹是进入文本编辑,但没有变成输入
|
||||
e.preventDefault()
|
||||
this.show({
|
||||
node,
|
||||
e,
|
||||
@ -180,6 +126,7 @@ export default class TextEdit {
|
||||
|
||||
// 注册临时快捷键
|
||||
registerTmpShortcut() {
|
||||
// 注册回车快捷键
|
||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
@ -196,17 +143,7 @@ export default class TextEdit {
|
||||
return this.showTextEdit
|
||||
}
|
||||
|
||||
// 设置文本编辑框是否处于显示状态
|
||||
setIsShowTextEdit(val) {
|
||||
this.showTextEdit = val
|
||||
if (val) {
|
||||
this.mindMap.keyCommand.stopCheckInSvg()
|
||||
} else {
|
||||
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||
}
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
// 显示文本编辑框
|
||||
// isInserting:是否是刚创建的节点
|
||||
// isFromKeyDown:是否是在按键事件进入的编辑
|
||||
async show({
|
||||
@ -219,13 +156,7 @@ export default class TextEdit {
|
||||
if (node.isUseCustomNodeContent()) {
|
||||
return
|
||||
}
|
||||
// 如果有正在编辑中的节点,那么先结束它
|
||||
const currentEditNode = this.getCurrentEditNode()
|
||||
if (currentEditNode) {
|
||||
this.hideEditTextBox()
|
||||
}
|
||||
const { beforeTextEdit, openRealtimeRenderOnNodeTextEdit } =
|
||||
this.mindMap.opt
|
||||
const { beforeTextEdit } = this.mindMap.opt
|
||||
if (typeof beforeTextEdit === 'function') {
|
||||
let isShow = false
|
||||
try {
|
||||
@ -236,18 +167,10 @@ export default class TextEdit {
|
||||
}
|
||||
if (!isShow) return
|
||||
}
|
||||
this.currentNode = node
|
||||
const { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
|
||||
this.mindMap.view.translateXY(offsetLeft, offsetTop)
|
||||
const g = node._textData.node
|
||||
// 需要先显示,不然宽高获取到的可能是0
|
||||
if (openRealtimeRenderOnNodeTextEdit) {
|
||||
g.show()
|
||||
}
|
||||
const rect = g.node.getBoundingClientRect()
|
||||
// 如果开启了大小实时更新,那么直接隐藏节点原文本
|
||||
if (openRealtimeRenderOnNodeTextEdit) {
|
||||
g.hide()
|
||||
}
|
||||
const rect = node._textData.node.node.getBoundingClientRect()
|
||||
const params = {
|
||||
node,
|
||||
rect,
|
||||
@ -259,25 +182,9 @@ export default class TextEdit {
|
||||
this.mindMap.richText.showEditText(params)
|
||||
return
|
||||
}
|
||||
this.currentNode = node
|
||||
this.showEditTextBox(params)
|
||||
}
|
||||
|
||||
// 当openRealtimeRenderOnNodeTextEdit配置更新后需要更新编辑框样式
|
||||
onOpenRealtimeRenderOnNodeTextEditConfigUpdate(
|
||||
openRealtimeRenderOnNodeTextEdit
|
||||
) {
|
||||
if (!this.textEditNode) return
|
||||
this.textEditNode.style.background = openRealtimeRenderOnNodeTextEdit
|
||||
? 'transparent'
|
||||
: this.currentNode
|
||||
? this.getBackground(this.currentNode)
|
||||
: ''
|
||||
this.textEditNode.style.boxShadow = openRealtimeRenderOnNodeTextEdit
|
||||
? 'none'
|
||||
: '0 0 20px rgba(0,0,0,.5)'
|
||||
}
|
||||
|
||||
// 处理画布缩放
|
||||
onScale() {
|
||||
const node = this.getCurrentEditNode()
|
||||
@ -288,7 +195,7 @@ export default class TextEdit {
|
||||
this.mindMap.richText.showTextEdit = false
|
||||
} else {
|
||||
this.cacheEditingText = this.getEditText()
|
||||
this.setIsShowTextEdit(false)
|
||||
this.showTextEdit = false
|
||||
}
|
||||
this.show({
|
||||
node,
|
||||
@ -299,35 +206,15 @@ export default class TextEdit {
|
||||
// 显示文本编辑框
|
||||
showEditTextBox({ node, rect, isInserting, isFromKeyDown, isFromScale }) {
|
||||
if (this.showTextEdit) return
|
||||
const {
|
||||
nodeTextEditZIndex,
|
||||
textAutoWrapWidth,
|
||||
selectTextOnEnterEditText,
|
||||
openRealtimeRenderOnNodeTextEdit,
|
||||
autoEmptyTextWhenKeydownEnterEdit
|
||||
} = this.mindMap.opt
|
||||
const { nodeTextEditZIndex, textAutoWrapWidth, selectTextOnEnterEditText } =
|
||||
this.mindMap.opt
|
||||
if (!isFromScale) {
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
}
|
||||
this.registerTmpShortcut()
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.classList.add(SMM_NODE_EDIT_WRAP)
|
||||
this.textEditNode.style.cssText = `
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
${
|
||||
openRealtimeRenderOnNodeTextEdit
|
||||
? ''
|
||||
: `box-shadow: 0 0 20px rgba(0,0,0,.5);`
|
||||
}
|
||||
padding: ${this.textNodePaddingY}px ${this.textNodePaddingX}px;
|
||||
margin-left: -${this.textNodePaddingX}px;
|
||||
margin-top: -${this.textNodePaddingY}px;
|
||||
outline: none;
|
||||
word-break: break-all;
|
||||
line-break: anywhere;
|
||||
`
|
||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
|
||||
this.textEditNode.setAttribute('contenteditable', true)
|
||||
this.textEditNode.addEventListener('keyup', e => {
|
||||
e.stopPropagation()
|
||||
@ -352,49 +239,35 @@ export default class TextEdit {
|
||||
} else {
|
||||
handleInputPasteText(e)
|
||||
}
|
||||
this.emitTextChangeEvent()
|
||||
})
|
||||
this.textEditNode.addEventListener('input', () => {
|
||||
this.emitTextChangeEvent()
|
||||
})
|
||||
const targetNode =
|
||||
this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
const scale = this.mindMap.view.scale
|
||||
const fontSize = node.style.merge('fontSize')
|
||||
const textLines = (this.cacheEditingText || node.getData('text'))
|
||||
let scale = this.mindMap.view.scale
|
||||
let lineHeight = node.style.merge('lineHeight')
|
||||
let fontSize = node.style.merge('fontSize')
|
||||
let textLines = (this.cacheEditingText || node.getData('text'))
|
||||
.split(/\n/gim)
|
||||
.map(item => {
|
||||
return htmlEscape(item)
|
||||
})
|
||||
const isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
|
||||
node.style.domText(this.textEditNode, scale)
|
||||
if (!openRealtimeRenderOnNodeTextEdit) {
|
||||
this.textEditNode.style.background = this.getBackground(node)
|
||||
}
|
||||
let isMultiLine = node._textData.node.attr('data-ismultiLine') === 'true'
|
||||
node.style.domText(this.textEditNode, scale, isMultiLine)
|
||||
this.textEditNode.style.zIndex = nodeTextEditZIndex
|
||||
if (isFromKeyDown && autoEmptyTextWhenKeydownEnterEdit) {
|
||||
this.textEditNode.innerHTML = ''
|
||||
} else {
|
||||
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||
}
|
||||
this.textEditNode.style.minWidth =
|
||||
rect.width + this.textNodePaddingX * 2 + 'px'
|
||||
this.textEditNode.style.minHeight = rect.height + 'px'
|
||||
this.textEditNode.style.left = Math.floor(rect.left) + 'px'
|
||||
this.textEditNode.style.top = Math.floor(rect.top) + 'px'
|
||||
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'
|
||||
if (isMultiLine) {
|
||||
this.textEditNode.style.lineHeight = noneRichTextNodeLineHeight
|
||||
if (isMultiLine && lineHeight !== 1) {
|
||||
this.textEditNode.style.transform = `translateY(${
|
||||
(((noneRichTextNodeLineHeight - 1) * fontSize) / 2) * scale
|
||||
-((lineHeight * fontSize - fontSize) / 2) * scale
|
||||
}px)`
|
||||
} else {
|
||||
this.textEditNode.style.lineHeight = 'normal'
|
||||
}
|
||||
this.setIsShowTextEdit(true)
|
||||
this.showTextEdit = true
|
||||
// 选中文本
|
||||
// if (!this.cacheEditingText) {
|
||||
// selectAllInput(this.textEditNode)
|
||||
@ -407,54 +280,6 @@ export default class TextEdit {
|
||||
this.cacheEditingText = ''
|
||||
}
|
||||
|
||||
// 派发节点文本编辑事件
|
||||
emitTextChangeEvent() {
|
||||
this.mindMap.emit('node_text_edit_change', {
|
||||
node: this.currentNode,
|
||||
text: this.getEditText(),
|
||||
richText: false
|
||||
})
|
||||
}
|
||||
|
||||
// 更新文本编辑框的大小和位置
|
||||
updateTextEditNode() {
|
||||
if (this.mindMap.richText) {
|
||||
this.mindMap.richText.updateTextEditNode()
|
||||
return
|
||||
}
|
||||
if (!this.showTextEdit || !this.currentNode) {
|
||||
return
|
||||
}
|
||||
const rect = this.currentNode._textData.node.node.getBoundingClientRect()
|
||||
this.textEditNode.style.minWidth =
|
||||
rect.width + this.textNodePaddingX * 2 + 'px'
|
||||
this.textEditNode.style.minHeight =
|
||||
rect.height + this.textNodePaddingY * 2 + 'px'
|
||||
this.textEditNode.style.left = Math.floor(rect.left) + 'px'
|
||||
this.textEditNode.style.top = Math.floor(rect.top) + 'px'
|
||||
}
|
||||
|
||||
// 获取编辑区域的背景填充
|
||||
getBackground(node) {
|
||||
const gradientStyle = node.style.merge('gradientStyle')
|
||||
// 当前使用的是渐变色背景
|
||||
if (gradientStyle) {
|
||||
const startColor = node.style.merge('startColor')
|
||||
const endColor = node.style.merge('endColor')
|
||||
return `linear-gradient(to right, ${startColor}, ${endColor})`
|
||||
} else {
|
||||
// 单色背景
|
||||
const bgColor = node.style.merge('fillColor')
|
||||
const color = node.style.merge('color')
|
||||
// 默认使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
|
||||
return bgColor === 'transparent'
|
||||
? isWhite(color)
|
||||
? getVisibleColorFromTheme(this.mindMap.themeConfig)
|
||||
: '#fff'
|
||||
: bgColor
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文本编辑元素
|
||||
removeTextEditEl() {
|
||||
if (this.mindMap.richText) {
|
||||
@ -479,8 +304,21 @@ export default class TextEdit {
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
const currentNode = this.currentNode
|
||||
const text = this.getEditText()
|
||||
this.renderer.activeNodeList.forEach(node => {
|
||||
let str = this.getEditText()
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', node, str)
|
||||
if (node.isGeneralization) {
|
||||
// 概要节点
|
||||
node.generalizationBelongNode.updateGeneralization()
|
||||
}
|
||||
this.mindMap.render()
|
||||
})
|
||||
this.mindMap.emit(
|
||||
'hide_text_edit',
|
||||
this.textEditNode,
|
||||
this.renderer.activeNodeList,
|
||||
this.currentNode
|
||||
)
|
||||
this.currentNode = null
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.textEditNode.innerHTML = ''
|
||||
@ -488,19 +326,7 @@ export default class TextEdit {
|
||||
this.textEditNode.style.fontSize = 'inherit'
|
||||
this.textEditNode.style.fontWeight = 'normal'
|
||||
this.textEditNode.style.transform = 'translateY(0)'
|
||||
this.setIsShowTextEdit(false)
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', currentNode, text)
|
||||
// if (currentNode.isGeneralization) {
|
||||
// // 概要节点
|
||||
// currentNode.generalizationBelongNode.updateGeneralization()
|
||||
// }
|
||||
this.mindMap.render()
|
||||
this.mindMap.emit(
|
||||
'hide_text_edit',
|
||||
this.textEditNode,
|
||||
this.renderer.activeNodeList,
|
||||
currentNode
|
||||
)
|
||||
this.showTextEdit = false
|
||||
}
|
||||
|
||||
// 获取当前正在编辑中的节点实例
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import Style from './Style'
|
||||
import Shape from './Shape'
|
||||
import { G, Rect, Text, SVG } from '@svgdotjs/svg.js'
|
||||
import { G, Rect, Text } 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 nodeModifyWidthMethods from './nodeModifyWidth'
|
||||
import nodeCooperateMethods from './nodeCooperate'
|
||||
import quickCreateChildBtnMethods from './quickCreateChildBtn'
|
||||
import nodeLayoutMethods from './nodeLayout'
|
||||
import { CONSTANTS } from '../../../constants/constant'
|
||||
import { copyNodeTree, createUid, addXmlns } from '../../../utils/index'
|
||||
import {
|
||||
copyNodeTree,
|
||||
createForeignObjectNode,
|
||||
createUid,
|
||||
addXmlns
|
||||
} from '../../../utils/index'
|
||||
|
||||
// 节点类
|
||||
class MindMapNode {
|
||||
@ -20,8 +22,6 @@ class MindMapNode {
|
||||
this.opt = opt
|
||||
// 节点数据
|
||||
this.nodeData = this.handleData(opt.data || {})
|
||||
// 保存本次更新时的节点数据快照
|
||||
this.nodeDataSnapshot = ''
|
||||
// uid
|
||||
this.uid = opt.uid
|
||||
// 控制实例
|
||||
@ -34,8 +34,6 @@ class MindMapNode {
|
||||
this.lineDraw = this.mindMap.lineDraw
|
||||
// 样式实例
|
||||
this.style = new Style(this)
|
||||
// 节点当前生效的全部样式
|
||||
this.effectiveStyles = {}
|
||||
// 形状实例
|
||||
this.shapeInstance = new Shape(this)
|
||||
this.shapePadding = {
|
||||
@ -54,8 +52,6 @@ class MindMapNode {
|
||||
this.width = opt.width || 0
|
||||
// 节点高
|
||||
this.height = opt.height || 0
|
||||
// 自定义文本的宽度
|
||||
this.customTextWidth = opt.data.data.customTextWidth || undefined
|
||||
// left
|
||||
this._left = opt.left || 0
|
||||
// top
|
||||
@ -86,6 +82,7 @@ class MindMapNode {
|
||||
this.noteEl = null
|
||||
this.noteContentIsShow = false
|
||||
this._attachmentData = null
|
||||
this._numberData = null
|
||||
this._prefixData = null
|
||||
this._postfixData = null
|
||||
this._expandBtn = null
|
||||
@ -99,16 +96,22 @@ class MindMapNode {
|
||||
this._generalizationList = []
|
||||
this._unVisibleRectRegionNode = null
|
||||
this._isMouseenter = false
|
||||
this._customContentAddToNodeAdd = null
|
||||
// 尺寸信息
|
||||
this._rectInfo = {
|
||||
imgContentWidth: 0,
|
||||
imgContentHeight: 0,
|
||||
textContentWidth: 0,
|
||||
textContentHeight: 0,
|
||||
textContentWidthWithoutTag: 0
|
||||
textContentHeight: 0
|
||||
}
|
||||
// 概要节点的宽高
|
||||
this._generalizationNodeWidth = 0
|
||||
this._generalizationNodeHeight = 0
|
||||
// 编号字符
|
||||
this.number = opt.number || ''
|
||||
// 各种文字信息的间距
|
||||
this.textContentItemMargin = this.mindMap.opt.textContentMargin
|
||||
// 图片和文字节点的间距
|
||||
this.blockContentMargin = this.mindMap.opt.imgTextMargin
|
||||
// 展开收缩按钮尺寸
|
||||
this.expandBtnSize = this.mindMap.opt.expandBtnSize
|
||||
// 是否是多选节点
|
||||
@ -119,10 +122,6 @@ class MindMapNode {
|
||||
this.isHide = false
|
||||
const proto = Object.getPrototypeOf(this)
|
||||
if (!proto.bindEvent) {
|
||||
// 节点尺寸计算和布局相关方法
|
||||
Object.keys(nodeLayoutMethods).forEach(item => {
|
||||
proto[item] = nodeLayoutMethods[item]
|
||||
})
|
||||
// 概要相关方法
|
||||
Object.keys(nodeGeneralizationMethods).forEach(item => {
|
||||
proto[item] = nodeGeneralizationMethods[item]
|
||||
@ -149,24 +148,10 @@ class MindMapNode {
|
||||
proto[item] = nodeCooperateMethods[item]
|
||||
})
|
||||
}
|
||||
// 拖拽调整节点宽度
|
||||
Object.keys(nodeModifyWidthMethods).forEach(item => {
|
||||
proto[item] = nodeModifyWidthMethods[item]
|
||||
})
|
||||
// 快捷创建子节点按钮
|
||||
if (this.mindMap.opt.isShowCreateChildBtnIcon) {
|
||||
Object.keys(quickCreateChildBtnMethods).forEach(item => {
|
||||
proto[item] = quickCreateChildBtnMethods[item]
|
||||
})
|
||||
this.initQuickCreateChildBtn()
|
||||
}
|
||||
proto.bindEvent = true
|
||||
}
|
||||
// 初始化
|
||||
this.getSize()
|
||||
// 初始需要计算一下概要节点的大小,否则计算布局时获取不到概要的大小
|
||||
this.updateGeneralization()
|
||||
this.initDragHandle()
|
||||
}
|
||||
|
||||
// 支持自定义位置
|
||||
@ -210,54 +195,15 @@ class MindMapNode {
|
||||
}
|
||||
|
||||
// 创建节点的各个内容对象数据
|
||||
// recreateTypes:[] custom、image、icon、text、hyperlink、tag、note、attachment、numbers、prefix、postfix、checkbox
|
||||
createNodeData(recreateTypes) {
|
||||
createNodeData() {
|
||||
// 自定义节点内容
|
||||
const {
|
||||
let {
|
||||
isUseCustomNodeContent,
|
||||
customCreateNodeContent,
|
||||
createNodePrefixContent,
|
||||
createNodePostfixContent,
|
||||
addCustomContentToNode
|
||||
createNodePostfixContent
|
||||
} = this.mindMap.opt
|
||||
// 需要创建的内容类型
|
||||
const typeList = [
|
||||
'custom',
|
||||
'image',
|
||||
'icon',
|
||||
'text',
|
||||
'hyperlink',
|
||||
'tag',
|
||||
'note',
|
||||
'attachment',
|
||||
'prefix',
|
||||
'postfix',
|
||||
...this.mindMap.nodeInnerPrefixList.map(item => {
|
||||
return item.name
|
||||
}),
|
||||
...this.mindMap.nodeInnerPostfixList.map(item => {
|
||||
return item.name
|
||||
})
|
||||
]
|
||||
const createTypes = {}
|
||||
if (Array.isArray(recreateTypes)) {
|
||||
// 重新创建指定的内容类型
|
||||
typeList.forEach(item => {
|
||||
if (recreateTypes.includes(item)) {
|
||||
createTypes[item] = true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 创建所有类型
|
||||
typeList.forEach(item => {
|
||||
createTypes[item] = true
|
||||
})
|
||||
}
|
||||
if (
|
||||
isUseCustomNodeContent &&
|
||||
customCreateNodeContent &&
|
||||
createTypes.custom
|
||||
) {
|
||||
if (isUseCustomNodeContent && customCreateNodeContent) {
|
||||
this._customNodeContent = customCreateNodeContent(this)
|
||||
}
|
||||
// 如果没有返回内容,那么还是使用内置的节点内容
|
||||
@ -265,73 +211,356 @@ class MindMapNode {
|
||||
addXmlns(this._customNodeContent)
|
||||
return
|
||||
}
|
||||
if (createTypes.image) this._imgData = this.createImgNode()
|
||||
if (createTypes.icon) this._iconData = this.createIconNode()
|
||||
if (createTypes.text) this._textData = this.createTextNode()
|
||||
if (createTypes.hyperlink) this._hyperlinkData = this.createHyperlinkNode()
|
||||
if (createTypes.tag) this._tagData = this.createTagNode()
|
||||
if (createTypes.note) this._noteData = this.createNoteNode()
|
||||
if (createTypes.attachment)
|
||||
this._attachmentData = this.createAttachmentNode()
|
||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||||
if (createTypes[item.name]) {
|
||||
this[`_${item.name}Data`] = item.createContent(this)
|
||||
}
|
||||
})
|
||||
if (createTypes.prefix) {
|
||||
this._prefixData = createNodePrefixContent
|
||||
? createNodePrefixContent(this)
|
||||
: null
|
||||
if (this._prefixData && this._prefixData.el) {
|
||||
addXmlns(this._prefixData.el)
|
||||
}
|
||||
this._imgData = this.createImgNode()
|
||||
this._iconData = this.createIconNode()
|
||||
this._textData = this.createTextNode()
|
||||
this._hyperlinkData = this.createHyperlinkNode()
|
||||
this._tagData = this.createTagNode()
|
||||
this._noteData = this.createNoteNode()
|
||||
this._attachmentData = this.createAttachmentNode()
|
||||
if (this.mindMap.numbers) {
|
||||
this._numberData = this.mindMap.numbers.createNumberContent(this)
|
||||
}
|
||||
if (createTypes.postfix) {
|
||||
this._postfixData = createNodePostfixContent
|
||||
? createNodePostfixContent(this)
|
||||
: null
|
||||
if (this._postfixData && this._postfixData.el) {
|
||||
addXmlns(this._postfixData.el)
|
||||
}
|
||||
this._prefixData = createNodePrefixContent
|
||||
? createNodePrefixContent(this)
|
||||
: null
|
||||
if (this._prefixData && this._prefixData.el) {
|
||||
addXmlns(this._prefixData.el)
|
||||
}
|
||||
this.mindMap.nodeInnerPostfixList.forEach(item => {
|
||||
if (createTypes[item.name]) {
|
||||
this[`_${item.name}Data`] = item.createContent(this)
|
||||
}
|
||||
})
|
||||
if (
|
||||
addCustomContentToNode &&
|
||||
typeof addCustomContentToNode.create === 'function'
|
||||
) {
|
||||
this._customContentAddToNodeAdd = addCustomContentToNode.create(this)
|
||||
if (
|
||||
this._customContentAddToNodeAdd &&
|
||||
this._customContentAddToNodeAdd.el
|
||||
) {
|
||||
addXmlns(this._customContentAddToNodeAdd.el)
|
||||
}
|
||||
this._postfixData = createNodePostfixContent
|
||||
? createNodePostfixContent(this)
|
||||
: null
|
||||
if (this._postfixData && this._postfixData.el) {
|
||||
addXmlns(this._postfixData.el)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算节点的宽高
|
||||
getSize(recreateTypes, opt = {}) {
|
||||
const ignoreUpdateCustomTextWidth = opt.ignoreUpdateCustomTextWidth || false
|
||||
if (!ignoreUpdateCustomTextWidth) {
|
||||
this.customTextWidth = this.getData('customTextWidth') || undefined
|
||||
}
|
||||
getSize() {
|
||||
this.customLeft = this.getData('customLeft') || undefined
|
||||
this.customTop = this.getData('customTop') || undefined
|
||||
// 这里不要更新概要,不然即使概要没修改,每次也会重新渲染
|
||||
// this.updateGeneralization()
|
||||
this.createNodeData(recreateTypes)
|
||||
const { width, height } = this.getNodeRect()
|
||||
this.createNodeData()
|
||||
let { width, height } = this.getNodeRect()
|
||||
// 判断节点尺寸是否有变化
|
||||
const changed = this.width !== width || this.height !== height
|
||||
let changed = this.width !== width || this.height !== height
|
||||
this.width = width
|
||||
this.height = height
|
||||
return changed
|
||||
}
|
||||
|
||||
// 计算节点尺寸信息
|
||||
getNodeRect() {
|
||||
// 自定义节点内容
|
||||
if (this.isUseCustomNodeContent()) {
|
||||
let rect = this.measureCustomNodeContentSize(this._customNodeContent)
|
||||
return {
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
}
|
||||
const { tagPosition } = this.mindMap.opt
|
||||
const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM
|
||||
// 宽高
|
||||
let imgContentWidth = 0
|
||||
let imgContentHeight = 0
|
||||
let textContentWidth = 0
|
||||
let textContentHeight = 0
|
||||
let tagContentWidth = 0
|
||||
let tagContentHeight = 0
|
||||
// 存在图片
|
||||
if (this._imgData) {
|
||||
this._rectInfo.imgContentWidth = imgContentWidth = this._imgData.width
|
||||
this._rectInfo.imgContentHeight = imgContentHeight = this._imgData.height
|
||||
}
|
||||
// 编号内容
|
||||
if (this._numberData) {
|
||||
textContentWidth += this._numberData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._numberData.height)
|
||||
}
|
||||
// 自定义前置内容
|
||||
if (this._prefixData) {
|
||||
textContentWidth += this._prefixData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._prefixData.height)
|
||||
}
|
||||
// 图标
|
||||
if (this._iconData.length > 0) {
|
||||
textContentWidth += this._iconData.reduce((sum, cur) => {
|
||||
textContentHeight = Math.max(textContentHeight, cur.height)
|
||||
return (sum += cur.width + this.textContentItemMargin)
|
||||
}, 0)
|
||||
}
|
||||
// 文字
|
||||
if (this._textData) {
|
||||
textContentWidth += this._textData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._textData.height)
|
||||
}
|
||||
// 超链接
|
||||
if (this._hyperlinkData) {
|
||||
textContentWidth += this._hyperlinkData.width
|
||||
textContentHeight = Math.max(
|
||||
textContentHeight,
|
||||
this._hyperlinkData.height
|
||||
)
|
||||
}
|
||||
// 标签
|
||||
if (this._tagData.length > 0) {
|
||||
let maxTagHeight = 0
|
||||
const totalTagWidth = this._tagData.reduce((sum, cur) => {
|
||||
maxTagHeight = Math.max(maxTagHeight, cur.height)
|
||||
return (sum += cur.width + this.textContentItemMargin)
|
||||
}, 0)
|
||||
if (tagIsBottom) {
|
||||
// 文字下方
|
||||
tagContentWidth = totalTagWidth
|
||||
tagContentHeight = maxTagHeight
|
||||
} else {
|
||||
// 否则在右侧
|
||||
textContentWidth += totalTagWidth
|
||||
textContentHeight = Math.max(textContentHeight, maxTagHeight)
|
||||
}
|
||||
}
|
||||
// 备注
|
||||
if (this._noteData) {
|
||||
textContentWidth += this._noteData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._noteData.height)
|
||||
}
|
||||
// 附件
|
||||
if (this._attachmentData) {
|
||||
textContentWidth += this._attachmentData.width
|
||||
textContentHeight = Math.max(
|
||||
textContentHeight,
|
||||
this._attachmentData.height
|
||||
)
|
||||
}
|
||||
// 自定义后置内容
|
||||
if (this._postfixData) {
|
||||
textContentWidth += this._postfixData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._postfixData.height)
|
||||
}
|
||||
// 文字内容部分的尺寸
|
||||
this._rectInfo.textContentWidth = textContentWidth
|
||||
this._rectInfo.textContentHeight = textContentHeight
|
||||
// 间距
|
||||
let margin =
|
||||
imgContentHeight > 0 && textContentHeight > 0
|
||||
? this.blockContentMargin
|
||||
: 0
|
||||
let { paddingX, paddingY } = this.getPaddingVale()
|
||||
// 纯内容宽高
|
||||
let _width = Math.max(imgContentWidth, textContentWidth)
|
||||
let _height = imgContentHeight + textContentHeight
|
||||
// 如果标签在文字下方
|
||||
if (tagIsBottom && tagContentHeight > 0 && textContentHeight > 0) {
|
||||
// 那么文字和标签之间也需要间距
|
||||
margin += this.blockContentMargin
|
||||
// 整体高度要考虑标签宽度
|
||||
_width = Math.max(_width, tagContentWidth)
|
||||
// 整体高度要加上标签的高度
|
||||
_height += tagContentHeight
|
||||
}
|
||||
// 计算节点形状需要的附加内边距
|
||||
let { paddingX: shapePaddingX, paddingY: shapePaddingY } =
|
||||
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
|
||||
this.shapePadding.paddingX = shapePaddingX
|
||||
this.shapePadding.paddingY = shapePaddingY
|
||||
// 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点
|
||||
const borderWidth = this.getBorderWidth()
|
||||
return {
|
||||
width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth,
|
||||
height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth
|
||||
}
|
||||
}
|
||||
|
||||
// 定位节点内容
|
||||
layout() {
|
||||
if (!this.group) return
|
||||
// 清除之前的内容
|
||||
this.group.clear()
|
||||
const { hoverRectPadding, tagPosition } = this.mindMap.opt
|
||||
let { width, height, textContentItemMargin } = this
|
||||
let { paddingY } = this.getPaddingVale()
|
||||
const halfBorderWidth = this.getBorderWidth() / 2
|
||||
paddingY += this.shapePadding.paddingY + halfBorderWidth
|
||||
// 节点形状
|
||||
this.shapeNode = this.shapeInstance.createShape()
|
||||
this.shapeNode.addClass('smm-node-shape')
|
||||
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
|
||||
this.style.shape(this.shapeNode)
|
||||
this.group.add(this.shapeNode)
|
||||
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
// 创建协同头像节点
|
||||
if (this.createUserListNode) this.createUserListNode()
|
||||
// 概要节点添加一个带所属节点id的类名
|
||||
if (this.isGeneralization && this.generalizationBelongNode) {
|
||||
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
|
||||
}
|
||||
// 激活hover和激活边框
|
||||
const addHoverNode = () => {
|
||||
this.hoverNode = new Rect()
|
||||
.size(width + hoverRectPadding * 2, height + hoverRectPadding * 2)
|
||||
.x(-hoverRectPadding)
|
||||
.y(-hoverRectPadding)
|
||||
this.hoverNode.addClass('smm-hover-node')
|
||||
this.style.hoverNode(this.hoverNode, width, height)
|
||||
this.group.add(this.hoverNode)
|
||||
}
|
||||
// 如果存在自定义节点内容,那么使用自定义节点内容
|
||||
if (this.isUseCustomNodeContent()) {
|
||||
const foreignObject = createForeignObjectNode({
|
||||
el: this._customNodeContent,
|
||||
width,
|
||||
height
|
||||
})
|
||||
this.group.add(foreignObject)
|
||||
addHoverNode()
|
||||
return
|
||||
}
|
||||
const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM
|
||||
const { textContentHeight } = this._rectInfo
|
||||
// 图片节点
|
||||
let imgHeight = 0
|
||||
if (this._imgData) {
|
||||
imgHeight = this._imgData.height
|
||||
this.group.add(this._imgData.node)
|
||||
this._imgData.node.cx(width / 2).y(paddingY)
|
||||
}
|
||||
// 内容节点
|
||||
let textContentNested = new G()
|
||||
let textContentOffsetX = 0
|
||||
// 编号内容
|
||||
if (this._numberData) {
|
||||
this._numberData.node
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._numberData.height) / 2)
|
||||
textContentNested.add(this._numberData.node)
|
||||
textContentOffsetX += this._numberData.width + textContentItemMargin
|
||||
}
|
||||
// 自定义前置内容
|
||||
if (this._prefixData) {
|
||||
const foreignObject = createForeignObjectNode({
|
||||
el: this._prefixData.el,
|
||||
width: this._prefixData.width,
|
||||
height: this._prefixData.height
|
||||
})
|
||||
foreignObject
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._prefixData.height) / 2)
|
||||
textContentNested.add(foreignObject)
|
||||
textContentOffsetX += this._prefixData.width + textContentItemMargin
|
||||
}
|
||||
// icon
|
||||
let iconNested = new G()
|
||||
if (this._iconData && this._iconData.length > 0) {
|
||||
let iconLeft = 0
|
||||
this._iconData.forEach(item => {
|
||||
item.node
|
||||
.x(textContentOffsetX + iconLeft)
|
||||
.y((textContentHeight - item.height) / 2)
|
||||
iconNested.add(item.node)
|
||||
iconLeft += item.width + textContentItemMargin
|
||||
})
|
||||
textContentNested.add(iconNested)
|
||||
textContentOffsetX += iconLeft
|
||||
}
|
||||
// 文字
|
||||
if (this._textData) {
|
||||
const oldX = this._textData.node.attr('data-offsetx') || 0
|
||||
this._textData.node.attr('data-offsetx', textContentOffsetX)
|
||||
// 修复safari浏览器节点存在图标时文字位置不正确的问题
|
||||
;(this._textData.nodeContent || this._textData.node)
|
||||
.x(-oldX) // 修复非富文本模式下同时存在图标和换行的文本时,被收起和展开时图标与文字距离会逐渐拉大的问题
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._textData.height) / 2)
|
||||
textContentNested.add(this._textData.node)
|
||||
textContentOffsetX += this._textData.width + textContentItemMargin
|
||||
}
|
||||
// 超链接
|
||||
if (this._hyperlinkData) {
|
||||
this._hyperlinkData.node
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._hyperlinkData.height) / 2)
|
||||
textContentNested.add(this._hyperlinkData.node)
|
||||
textContentOffsetX += this._hyperlinkData.width + textContentItemMargin
|
||||
}
|
||||
// 标签
|
||||
let tagNested = new G()
|
||||
if (this._tagData && this._tagData.length > 0) {
|
||||
if (tagIsBottom) {
|
||||
// 标签显示在文字下方
|
||||
let tagLeft = 0
|
||||
this._tagData.forEach(item => {
|
||||
item.node.x(tagLeft).y(0)
|
||||
tagNested.add(item.node)
|
||||
tagLeft += item.width + textContentItemMargin
|
||||
})
|
||||
tagNested.cx(width / 2).y(
|
||||
paddingY + // 内边距
|
||||
imgHeight + // 图片高度
|
||||
textContentHeight + // 文本区域高度
|
||||
(imgHeight > 0 && textContentHeight > 0
|
||||
? this.blockContentMargin
|
||||
: 0) + // 图片和文本之间的间距
|
||||
this.blockContentMargin // 标签和文本之间的间距
|
||||
)
|
||||
this.group.add(tagNested)
|
||||
} else {
|
||||
// 标签显示在文字右侧
|
||||
let tagLeft = 0
|
||||
this._tagData.forEach(item => {
|
||||
item.node
|
||||
.x(textContentOffsetX + tagLeft)
|
||||
.y((textContentHeight - item.height) / 2)
|
||||
tagNested.add(item.node)
|
||||
tagLeft += item.width + textContentItemMargin
|
||||
})
|
||||
textContentNested.add(tagNested)
|
||||
textContentOffsetX += tagLeft
|
||||
}
|
||||
}
|
||||
// 备注
|
||||
if (this._noteData) {
|
||||
this._noteData.node
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._noteData.height) / 2)
|
||||
textContentNested.add(this._noteData.node)
|
||||
textContentOffsetX += this._noteData.width
|
||||
}
|
||||
// 附件
|
||||
if (this._attachmentData) {
|
||||
this._attachmentData.node
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._attachmentData.height) / 2)
|
||||
textContentNested.add(this._attachmentData.node)
|
||||
textContentOffsetX += this._attachmentData.width
|
||||
}
|
||||
// 自定义后置内容
|
||||
if (this._postfixData) {
|
||||
const foreignObject = createForeignObjectNode({
|
||||
el: this._postfixData.el,
|
||||
width: this._postfixData.width,
|
||||
height: this._postfixData.height
|
||||
})
|
||||
foreignObject
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._postfixData.height) / 2)
|
||||
textContentNested.add(foreignObject)
|
||||
textContentOffsetX += this._postfixData.width
|
||||
}
|
||||
this.group.add(textContentNested)
|
||||
// 文字内容整体
|
||||
textContentNested.translate(
|
||||
width / 2 - textContentNested.bbox().width / 2,
|
||||
paddingY + // 内边距
|
||||
imgHeight + // 图片高度
|
||||
(imgHeight > 0 && textContentHeight > 0 ? this.blockContentMargin : 0) // 和图片的间距
|
||||
)
|
||||
addHoverNode()
|
||||
this.mindMap.emit('node_layout_end', this)
|
||||
}
|
||||
|
||||
// 给节点绑定事件
|
||||
bindGroupEvent() {
|
||||
// 单击事件,选中节点
|
||||
@ -351,15 +580,12 @@ class MindMapNode {
|
||||
this.active(e)
|
||||
})
|
||||
this.group.on('mousedown', e => {
|
||||
e.preventDefault()
|
||||
const {
|
||||
readonly,
|
||||
enableCtrlKeyNodeSelection,
|
||||
useLeftKeySelectionRightKeyDrag,
|
||||
mousedownEventPreventDefault
|
||||
useLeftKeySelectionRightKeyDrag
|
||||
} = this.mindMap.opt
|
||||
if (mousedownEventPreventDefault) {
|
||||
e.preventDefault()
|
||||
}
|
||||
// 只读模式不需要阻止冒泡
|
||||
if (!readonly) {
|
||||
if (this.isRoot) {
|
||||
@ -377,7 +603,7 @@ class MindMapNode {
|
||||
// 多选和取消多选
|
||||
if (!readonly && (e.ctrlKey || e.metaKey) && enableCtrlKeyNodeSelection) {
|
||||
this.isMultipleChoice = true
|
||||
const isActive = this.getData('isActive')
|
||||
let isActive = this.getData('isActive')
|
||||
if (!isActive)
|
||||
this.mindMap.emit(
|
||||
'before_node_active',
|
||||
@ -483,15 +709,10 @@ class MindMapNode {
|
||||
return
|
||||
}
|
||||
this.updateNodeActiveClass()
|
||||
const {
|
||||
alwaysShowExpandBtn,
|
||||
notShowExpandBtn,
|
||||
isShowCreateChildBtnIcon,
|
||||
readonly
|
||||
} = this.mindMap.opt
|
||||
const childrenLength = this.getChildrenLength()
|
||||
const { alwaysShowExpandBtn, notShowExpandBtn } = this.mindMap.opt
|
||||
// 不显示展开收起按钮则不需要处理
|
||||
if (!notShowExpandBtn) {
|
||||
const childrenLength = this.nodeData.children.length
|
||||
if (alwaysShowExpandBtn) {
|
||||
// 需要移除展开收缩按钮
|
||||
if (this._expandBtn && childrenLength <= 0) {
|
||||
@ -501,7 +722,7 @@ class MindMapNode {
|
||||
this.renderExpandBtn()
|
||||
}
|
||||
} else {
|
||||
const { isActive, expand } = this.getData()
|
||||
let { isActive, expand } = this.getData()
|
||||
// 展开状态且非激活状态,且当前鼠标不在它上面,才隐藏
|
||||
if (childrenLength <= 0) {
|
||||
this.removeExpandBtn()
|
||||
@ -512,41 +733,23 @@ class MindMapNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 更新快速创建子节点按钮
|
||||
if (isShowCreateChildBtnIcon) {
|
||||
if (childrenLength > 0) {
|
||||
this.removeQuickCreateChildBtn()
|
||||
} else {
|
||||
const { isActive } = this.getData()
|
||||
if (isActive) {
|
||||
this.showQuickCreateChildBtn()
|
||||
} else {
|
||||
this.hideQuickCreateChildBtn()
|
||||
}
|
||||
}
|
||||
}
|
||||
// 更新拖拽手柄的显示与否
|
||||
this.updateDragHandle()
|
||||
// 更新概要
|
||||
this.renderGeneralization(forceRender)
|
||||
// 更新协同头像
|
||||
if (this.updateUserListNode) this.updateUserListNode()
|
||||
// 更新节点位置
|
||||
const t = this.group.transform()
|
||||
// 保存一份当前节点数据快照
|
||||
this.nodeDataSnapshot = readonly ? '' : JSON.stringify(this.getData())
|
||||
// 节点位置变化才更新,因为即使值没有变化属性设置操作也是耗时的
|
||||
if (this.left !== t.translateX || this.top !== t.translateY) {
|
||||
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
}
|
||||
let t = this.group.transform()
|
||||
// 如果节点位置没有变化,则返回
|
||||
if (this.left === t.translateX && this.top === t.translateY) return
|
||||
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
}
|
||||
|
||||
// 获取节点相当于画布的位置
|
||||
getNodePosInClient(_left, _top) {
|
||||
const drawTransform = this.mindMap.draw.transform()
|
||||
const { scaleX, scaleY, translateX, translateY } = drawTransform
|
||||
const left = _left * scaleX + translateX
|
||||
const top = _top * scaleY + translateY
|
||||
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
|
||||
@ -565,8 +768,8 @@ class MindMapNode {
|
||||
}
|
||||
|
||||
// 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置
|
||||
reRender(recreateTypes, opt) {
|
||||
const sizeChange = this.getSize(recreateTypes, opt)
|
||||
reRender() {
|
||||
let sizeChange = this.getSize()
|
||||
this.layout()
|
||||
this.update()
|
||||
return sizeChange
|
||||
@ -582,21 +785,13 @@ class MindMapNode {
|
||||
// 根据是否激活更新节点
|
||||
updateNodeByActive(active) {
|
||||
if (this.group) {
|
||||
const { isShowCreateChildBtnIcon } = this.mindMap.opt
|
||||
// 切换激活状态,需要切换展开收起按钮的显隐
|
||||
if (active) {
|
||||
this.showExpandBtn()
|
||||
if (isShowCreateChildBtnIcon) {
|
||||
this.showQuickCreateChildBtn()
|
||||
}
|
||||
} else {
|
||||
this.hideExpandBtn()
|
||||
if (isShowCreateChildBtnIcon) {
|
||||
this.hideQuickCreateChildBtn()
|
||||
}
|
||||
}
|
||||
this.updateNodeActiveClass()
|
||||
this.updateDragHandle()
|
||||
}
|
||||
}
|
||||
|
||||
@ -720,10 +915,10 @@ class MindMapNode {
|
||||
|
||||
// 隐藏节点
|
||||
hide() {
|
||||
if (this.group) this.group.hide()
|
||||
this.group.hide()
|
||||
this.hideGeneralization()
|
||||
if (this.parent) {
|
||||
const index = this.parent.children.indexOf(this)
|
||||
let index = this.parent.children.indexOf(this)
|
||||
this.parent._lines[index] && this.parent._lines[index].hide()
|
||||
this._lines.forEach(item => {
|
||||
item.hide()
|
||||
@ -745,7 +940,7 @@ class MindMapNode {
|
||||
this.group.show()
|
||||
this.showGeneralization()
|
||||
if (this.parent) {
|
||||
const index = this.parent.children.indexOf(this)
|
||||
let index = this.parent.children.indexOf(this)
|
||||
this.parent._lines[index] && this.parent._lines[index].show()
|
||||
this._lines.forEach(item => {
|
||||
item.show()
|
||||
@ -763,7 +958,7 @@ class MindMapNode {
|
||||
// 包括连接线和下级节点
|
||||
setOpacity(val) {
|
||||
// 自身及连线
|
||||
if (this.group) this.group.opacity(val)
|
||||
this.group.opacity(val)
|
||||
this._lines.forEach(line => {
|
||||
line.opacity(val)
|
||||
})
|
||||
@ -802,13 +997,13 @@ class MindMapNode {
|
||||
// 被拖拽中
|
||||
startDrag() {
|
||||
this.isDrag = true
|
||||
if (this.group) this.group.addClass('smm-node-dragging')
|
||||
this.group.addClass('smm-node-dragging')
|
||||
}
|
||||
|
||||
// 拖拽结束
|
||||
endDrag() {
|
||||
this.isDrag = false
|
||||
if (this.group) this.group.removeClass('smm-node-dragging')
|
||||
this.group.removeClass('smm-node-dragging')
|
||||
}
|
||||
|
||||
// 连线
|
||||
@ -816,12 +1011,13 @@ class MindMapNode {
|
||||
if (this.getData('expand') === false) {
|
||||
return
|
||||
}
|
||||
let childrenLen = this.getChildrenLength()
|
||||
let childrenLen = this.nodeData.children.length
|
||||
// 切换为鱼骨结构时,清空根节点和二级节点的连线
|
||||
if (this.mindMap.renderer.layout.nodeIsRemoveAllLines) {
|
||||
if (this.mindMap.renderer.layout.nodeIsRemoveAllLines(this)) {
|
||||
childrenLen = 0
|
||||
}
|
||||
if (
|
||||
this.mindMap.opt.layout === CONSTANTS.LAYOUT.FISHBONE &&
|
||||
(this.isRoot || this.layerIndex === 1)
|
||||
) {
|
||||
childrenLen = 0
|
||||
}
|
||||
if (childrenLen > this._lines.length) {
|
||||
// 创建缺少的线
|
||||
@ -897,18 +1093,15 @@ class MindMapNode {
|
||||
|
||||
// 设置连线样式
|
||||
styleLine(line, childNode, enableMarker) {
|
||||
const { enableInheritAncestorLineStyle } = this.mindMap.opt
|
||||
const getName = enableInheritAncestorLineStyle
|
||||
? 'getSelfInhertStyle'
|
||||
: 'getSelfStyle'
|
||||
const width =
|
||||
childNode[getName]('lineWidth') || childNode.getStyle('lineWidth', true)
|
||||
childNode.getSelfInhertStyle('lineWidth') ||
|
||||
childNode.getStyle('lineWidth', true)
|
||||
const color =
|
||||
childNode[getName]('lineColor') ||
|
||||
childNode.getSelfInhertStyle('lineColor') ||
|
||||
this.getRainbowLineColor(childNode) ||
|
||||
childNode.getStyle('lineColor', true)
|
||||
const dasharray =
|
||||
childNode[getName]('lineDasharray') ||
|
||||
childNode.getSelfInhertStyle('lineDasharray') ||
|
||||
childNode.getStyle('lineDasharray', true)
|
||||
this.style.line(
|
||||
line,
|
||||
@ -985,15 +1178,16 @@ class MindMapNode {
|
||||
|
||||
// 获取padding值
|
||||
getPaddingVale() {
|
||||
let { isActive } = this.getData()
|
||||
return {
|
||||
paddingX: this.getStyle('paddingX'),
|
||||
paddingY: this.getStyle('paddingY')
|
||||
paddingX: this.getStyle('paddingX', true, isActive),
|
||||
paddingY: this.getStyle('paddingY', true, isActive)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取某个样式
|
||||
getStyle(prop, root) {
|
||||
const v = this.style.merge(prop, root)
|
||||
let v = this.style.merge(prop, root)
|
||||
return v === undefined ? '' : v
|
||||
}
|
||||
|
||||
@ -1053,16 +1247,16 @@ class MindMapNode {
|
||||
|
||||
// 获取节点的尺寸和位置信息,宽高是应用了缩放效果后的实际宽高,位置是相对于浏览器窗口左上角的位置
|
||||
getRect() {
|
||||
return this.group ? this.group.rbox() : null
|
||||
return this.group.rbox()
|
||||
}
|
||||
|
||||
// 获取节点的尺寸和位置信息,宽高是应用了缩放效果后的实际宽高,位置信息相对于画布
|
||||
getRectInSvg() {
|
||||
const { scaleX, scaleY, translateX, translateY } =
|
||||
let { scaleX, scaleY, translateX, translateY } =
|
||||
this.mindMap.draw.transform()
|
||||
let { left, top, width, height } = this
|
||||
const right = (left + width) * scaleX + translateX
|
||||
const bottom = (top + height) * scaleY + translateY
|
||||
let right = (left + width) * scaleX + translateX
|
||||
let bottom = (top + height) * scaleY + translateY
|
||||
left = left * scaleX + translateX
|
||||
top = top * scaleY + translateY
|
||||
return {
|
||||
@ -1102,44 +1296,6 @@ class MindMapNode {
|
||||
createSvgTextNode(text = '') {
|
||||
return new Text().text(text)
|
||||
}
|
||||
|
||||
// 获取SVG.js库的一些对象
|
||||
getSvgObjects() {
|
||||
return {
|
||||
SVG,
|
||||
G,
|
||||
Rect
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否支持拖拽调整宽度
|
||||
// 1.富文本模式
|
||||
// 2.自定义节点内容
|
||||
checkEnableDragModifyNodeWidth() {
|
||||
const {
|
||||
enableDragModifyNodeWidth,
|
||||
isUseCustomNodeContent,
|
||||
customCreateNodeContent
|
||||
} = this.mindMap.opt
|
||||
return (
|
||||
enableDragModifyNodeWidth &&
|
||||
(this.mindMap.richText ||
|
||||
(isUseCustomNodeContent && customCreateNodeContent))
|
||||
)
|
||||
}
|
||||
|
||||
// 是否存在自定义宽度
|
||||
hasCustomWidth() {
|
||||
return (
|
||||
this.checkEnableDragModifyNodeWidth() &&
|
||||
this.customTextWidth !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
// 获取子节点的数量
|
||||
getChildrenLength() {
|
||||
return this.nodeData.children ? this.nodeData.children.length : 0
|
||||
}
|
||||
}
|
||||
|
||||
export default MindMapNode
|
||||
|
||||
@ -52,36 +52,14 @@ export default class Shape {
|
||||
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
|
||||
paddingY: actHeight < actWidth ? actOffset / 2 : 0
|
||||
}
|
||||
}
|
||||
const extendShape = this.getShapeFromExtendList(shape)
|
||||
if (extendShape) {
|
||||
return (
|
||||
extendShape.getPadding({
|
||||
node: this.node,
|
||||
width,
|
||||
height,
|
||||
paddingX,
|
||||
paddingY
|
||||
}) || {
|
||||
default:
|
||||
return {
|
||||
paddingX: 0,
|
||||
paddingY: 0
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return {
|
||||
paddingX: 0,
|
||||
paddingY: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从形状扩展列表里获取指定名称的形状
|
||||
getShapeFromExtendList(shape) {
|
||||
return this.mindMap.extendShapeList.find(item => {
|
||||
return item.name === shape
|
||||
})
|
||||
}
|
||||
|
||||
// 创建形状节点
|
||||
createShape() {
|
||||
const shape = this.node.getShape()
|
||||
@ -114,13 +92,7 @@ export default class Shape {
|
||||
// 圆
|
||||
node = this.createCircle()
|
||||
}
|
||||
if (!node) {
|
||||
const extendShape = this.getShapeFromExtendList(shape)
|
||||
if (extendShape) {
|
||||
node = extendShape.createShape(this.node)
|
||||
}
|
||||
}
|
||||
return node || this.createRect()
|
||||
return node
|
||||
}
|
||||
|
||||
// 获取节点减去节点边框宽度、hover节点边框宽度后的尺寸
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { checkIsNodeStyleDataKey } from '../../../utils/index'
|
||||
|
||||
const rootProp = ['paddingX', 'paddingY']
|
||||
const backgroundStyleProps = [
|
||||
'backgroundColor',
|
||||
'backgroundImage',
|
||||
@ -8,23 +9,10 @@ const backgroundStyleProps = [
|
||||
'backgroundSize'
|
||||
]
|
||||
|
||||
export const shapeStyleProps = [
|
||||
'gradientStyle',
|
||||
'startColor',
|
||||
'endColor',
|
||||
'startDir',
|
||||
'endDir',
|
||||
'fillColor',
|
||||
'borderColor',
|
||||
'borderWidth',
|
||||
'borderDasharray'
|
||||
]
|
||||
|
||||
// 样式类
|
||||
class Style {
|
||||
// 设置背景样式
|
||||
static setBackgroundStyle(el, themeConfig) {
|
||||
if (!el) return
|
||||
// 缓存容器元素原本的样式
|
||||
if (!Style.cacheStyle) {
|
||||
Style.cacheStyle = {}
|
||||
@ -74,11 +62,10 @@ class Style {
|
||||
// 合并样式
|
||||
merge(prop, root) {
|
||||
let themeConfig = this.ctx.mindMap.themeConfig
|
||||
let defaultConfig = null
|
||||
let useRoot = false
|
||||
if (root) {
|
||||
// 使用最外层样式
|
||||
useRoot = true
|
||||
// 三级及以下节点
|
||||
let defaultConfig = themeConfig.node
|
||||
if (root || rootProp.includes(prop)) {
|
||||
// 直接使用最外层样式
|
||||
defaultConfig = themeConfig
|
||||
} else if (this.ctx.isGeneralization) {
|
||||
// 概要节点
|
||||
@ -89,27 +76,11 @@ class Style {
|
||||
} else if (this.ctx.layerIndex === 1) {
|
||||
// 二级节点
|
||||
defaultConfig = themeConfig.second
|
||||
} else {
|
||||
// 三级及以下节点
|
||||
defaultConfig = themeConfig.node
|
||||
}
|
||||
let value = ''
|
||||
// 优先使用节点本身的样式
|
||||
if (this.getSelfStyle(prop) !== undefined) {
|
||||
value = this.getSelfStyle(prop)
|
||||
} else if (defaultConfig[prop] !== undefined) {
|
||||
// 否则使用对应层级的样式
|
||||
value = defaultConfig[prop]
|
||||
} else {
|
||||
// 否则使用最外层样式
|
||||
value = themeConfig[prop]
|
||||
}
|
||||
if (!useRoot) {
|
||||
this.addToEffectiveStyles({
|
||||
[prop]: value
|
||||
})
|
||||
}
|
||||
return value
|
||||
return this.getSelfStyle(prop) !== undefined
|
||||
? this.getSelfStyle(prop)
|
||||
: defaultConfig[prop]
|
||||
}
|
||||
|
||||
// 获取某个样式值
|
||||
@ -122,16 +93,6 @@ class Style {
|
||||
return this.ctx.getData(prop)
|
||||
}
|
||||
|
||||
// 更新当前节点生效的样式数据
|
||||
addToEffectiveStyles(styles) {
|
||||
// effectiveStyles目前只提供给格式刷插件使用,所以如果没有注册该插件,那么不需要保存该数据
|
||||
if (!this.ctx.mindMap.painter) return
|
||||
this.ctx.effectiveStyles = {
|
||||
...this.ctx.effectiveStyles,
|
||||
...styles
|
||||
}
|
||||
}
|
||||
|
||||
// 矩形
|
||||
rect(node) {
|
||||
this.shape(node)
|
||||
@ -140,23 +101,18 @@ class Style {
|
||||
|
||||
// 形状
|
||||
shape(node) {
|
||||
const styles = {}
|
||||
shapeStyleProps.forEach(key => {
|
||||
styles[key] = this.merge(key)
|
||||
})
|
||||
if (styles.gradientStyle) {
|
||||
if (this.merge('gradientStyle')) {
|
||||
if (!this._gradient) {
|
||||
this._gradient = this.ctx.nodeDraw.gradient('linear')
|
||||
}
|
||||
this._gradient.update(add => {
|
||||
add.stop(0, styles.startColor)
|
||||
add.stop(1, styles.endColor)
|
||||
add.stop(0, this.merge('startColor'))
|
||||
add.stop(1, this.merge('endColor'))
|
||||
})
|
||||
this._gradient.from(...styles.startDir).to(...styles.endDir)
|
||||
node.fill(this._gradient)
|
||||
} else {
|
||||
node.fill({
|
||||
color: styles.fillColor
|
||||
color: this.merge('fillColor')
|
||||
})
|
||||
}
|
||||
// 节点使用横线样式,不需要渲染非激活状态的边框样式
|
||||
@ -169,53 +125,56 @@ class Style {
|
||||
// return
|
||||
// }
|
||||
node.stroke({
|
||||
color: styles.borderColor,
|
||||
width: styles.borderWidth,
|
||||
dasharray: styles.borderDasharray
|
||||
color: this.merge('borderColor'),
|
||||
width: this.merge('borderWidth'),
|
||||
dasharray: this.merge('borderDasharray')
|
||||
})
|
||||
}
|
||||
|
||||
// 文字
|
||||
text(node) {
|
||||
const styles = {
|
||||
color: this.merge('color'),
|
||||
fontFamily: this.merge('fontFamily'),
|
||||
fontSize: this.merge('fontSize'),
|
||||
fontWeight: this.merge('fontWeight'),
|
||||
fontStyle: this.merge('fontStyle'),
|
||||
textDecoration: this.merge('textDecoration')
|
||||
}
|
||||
node
|
||||
.fill({
|
||||
color: styles.color
|
||||
color: this.merge('color')
|
||||
})
|
||||
.css({
|
||||
'font-family': styles.fontFamily,
|
||||
'font-size': styles.fontSize + 'px',
|
||||
'font-weight': styles.fontWeight,
|
||||
'font-style': styles.fontStyle,
|
||||
'text-decoration': styles.textDecoration
|
||||
'font-family': this.merge('fontFamily'),
|
||||
'font-size': this.merge('fontSize'),
|
||||
'font-weight': this.merge('fontWeight'),
|
||||
'font-style': this.merge('fontStyle'),
|
||||
'text-decoration': this.merge('textDecoration')
|
||||
})
|
||||
}
|
||||
|
||||
// html文字节点
|
||||
domText(node, fontSizeScale = 1) {
|
||||
const styles = {
|
||||
color: this.merge('color'),
|
||||
fontFamily: this.merge('fontFamily'),
|
||||
// 生成内联样式
|
||||
createStyleText() {
|
||||
return `
|
||||
color: ${this.merge('color')};
|
||||
font-family: ${this.merge('fontFamily')};
|
||||
font-size: ${this.merge('fontSize') + 'px'};
|
||||
font-weight: ${this.merge('fontWeight')};
|
||||
font-style: ${this.merge('fontStyle')};
|
||||
text-decoration: ${this.merge('textDecoration')}
|
||||
`
|
||||
}
|
||||
|
||||
// 获取文本样式
|
||||
getTextFontStyle() {
|
||||
return {
|
||||
italic: this.merge('fontStyle') === 'italic',
|
||||
bold: this.merge('fontWeight'),
|
||||
fontSize: this.merge('fontSize'),
|
||||
fontWeight: this.merge('fontWeight'),
|
||||
fontStyle: this.merge('fontStyle'),
|
||||
textDecoration: this.merge('textDecoration'),
|
||||
textAlign: this.merge('textAlign')
|
||||
fontFamily: this.merge('fontFamily')
|
||||
}
|
||||
node.style.color = styles.color
|
||||
node.style.textDecoration = styles.textDecoration
|
||||
node.style.fontFamily = styles.fontFamily
|
||||
node.style.fontSize = styles.fontSize * fontSizeScale + 'px'
|
||||
node.style.fontWeight = styles.fontWeight || 'normal'
|
||||
node.style.fontStyle = styles.fontStyle
|
||||
node.style.textAlign = styles.textAlign
|
||||
}
|
||||
|
||||
// html文字节点
|
||||
domText(node, fontSizeScale = 1, isMultiLine) {
|
||||
node.style.fontFamily = this.merge('fontFamily')
|
||||
node.style.fontSize = this.merge('fontSize') * fontSizeScale + 'px'
|
||||
node.style.fontWeight = this.merge('fontWeight') || 'normal'
|
||||
node.style.lineHeight = !isMultiLine ? 'normal' : this.merge('lineHeight')
|
||||
node.style.fontStyle = this.merge('fontStyle')
|
||||
}
|
||||
|
||||
// 标签文字
|
||||
@ -240,18 +199,14 @@ class Style {
|
||||
}
|
||||
|
||||
// 内置图标
|
||||
iconNode(node, color) {
|
||||
iconNode(node) {
|
||||
node.attr({
|
||||
fill: color || this.merge('color')
|
||||
fill: this.merge('color')
|
||||
})
|
||||
}
|
||||
|
||||
// 连线
|
||||
line(line, { width, color, dasharray } = {}, enableMarker, childNode) {
|
||||
const { customHandleLine } = this.ctx.mindMap.opt
|
||||
if (typeof customHandleLine === 'function') {
|
||||
customHandleLine(this.ctx, line, { width, color, dasharray })
|
||||
}
|
||||
line.stroke({ color, dasharray, width }).fill({ color: 'none' })
|
||||
// 可以显示箭头
|
||||
if (enableMarker) {
|
||||
@ -314,7 +269,7 @@ class Style {
|
||||
node2.fill({ color: color })
|
||||
fillNode.fill({ color: fill })
|
||||
if (this.ctx.mindMap.opt.isShowExpandNum) {
|
||||
node.attr({ 'font-size': fontSize + 'px', 'font-color': fontColor })
|
||||
node.attr({ 'font-size': fontSize, 'font-color': fontColor })
|
||||
}
|
||||
}
|
||||
|
||||
@ -329,23 +284,10 @@ class Style {
|
||||
return res
|
||||
}
|
||||
|
||||
// 获取自定义的样式
|
||||
getCustomStyle() {
|
||||
const customStyle = {}
|
||||
Object.keys(this.ctx.getData()).forEach(item => {
|
||||
if (checkIsNodeStyleDataKey(item)) {
|
||||
customStyle[item] = this.ctx.getData(item)
|
||||
}
|
||||
})
|
||||
return customStyle
|
||||
}
|
||||
|
||||
// hover和激活节点
|
||||
hoverNode(node) {
|
||||
const hoverRectColor =
|
||||
this.merge('hoverRectColor') || this.ctx.mindMap.opt.hoverRectColor
|
||||
const hoverRectRadius = this.merge('hoverRectRadius')
|
||||
node.radius(hoverRectRadius).fill('none').stroke({
|
||||
const { hoverRectColor } = this.ctx.mindMap.opt
|
||||
node.radius(5).fill('none').stroke({
|
||||
color: hoverRectColor
|
||||
})
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ function createTextAvatar(item) {
|
||||
color: '#fff'
|
||||
})
|
||||
.css({
|
||||
'font-size': fontSize + 'px'
|
||||
'font-size': fontSize
|
||||
})
|
||||
.dx(-fontSize / 2)
|
||||
.dy((avatarSize - fontSize) / 2)
|
||||
|
||||
@ -1,26 +1,17 @@
|
||||
import {
|
||||
measureText,
|
||||
resizeImgSize,
|
||||
removeRichTextStyes,
|
||||
removeHtmlStyle,
|
||||
addHtmlStyle,
|
||||
checkIsRichText,
|
||||
isUndef,
|
||||
createForeignObjectNode,
|
||||
addXmlns,
|
||||
generateColorByContent,
|
||||
camelCaseToHyphen,
|
||||
getNodeRichTextStyles
|
||||
generateColorByContent
|
||||
} from '../../../utils'
|
||||
import { Image as SVGImage, SVG, A, G, Rect, Text } from '@svgdotjs/svg.js'
|
||||
import iconsSvg from '../../../svg/icons'
|
||||
import { noneRichTextNodeLineHeight } from '../../../constants/constant'
|
||||
|
||||
// 测量svg文本宽高
|
||||
const measureText = (text, style) => {
|
||||
const g = new G()
|
||||
const node = new Text().text(text)
|
||||
style.text(node)
|
||||
g.add(node)
|
||||
return g.bbox()
|
||||
}
|
||||
import { CONSTANTS } from '../../../constants/constant'
|
||||
|
||||
// 标签默认的样式
|
||||
const defaultTagStyle = {
|
||||
@ -32,20 +23,12 @@ const defaultTagStyle = {
|
||||
//width: 30 // 标签矩形的宽度,如果不设置,默认以文字的宽度+paddingX*2为宽度
|
||||
}
|
||||
|
||||
// 获取图片的真实url
|
||||
// 因为如果注册了NodeBase64ImageStorage插件,那么节点图片字段保存的实际是一个id,所以如果要获取图片真实的url可以通过该方法
|
||||
function getImageUrl() {
|
||||
const img = this.getData('image')
|
||||
return (this.mindMap.renderer.renderTree.data.imgMap || {})[img] || img
|
||||
}
|
||||
|
||||
// 创建图片节点
|
||||
function createImgNode() {
|
||||
const img = this.getImageUrl()
|
||||
const img = this.getData('image')
|
||||
if (!img) {
|
||||
return
|
||||
}
|
||||
img = (this.mindMap.renderer.renderTree.data.imgMap || {})[img] || img
|
||||
const imgSize = this.getImgShowSize()
|
||||
const node = new SVGImage().load(img).size(...imgSize)
|
||||
// 如果指定了加载失败显示的图片,那么加载一下图片检测是否失败
|
||||
@ -60,11 +43,8 @@ function createImgNode() {
|
||||
if (this.getData('imageTitle')) {
|
||||
node.attr('title', this.getData('imageTitle'))
|
||||
}
|
||||
node.on('click', e => {
|
||||
this.mindMap.emit('node_img_click', this, node, e)
|
||||
})
|
||||
node.on('dblclick', e => {
|
||||
this.mindMap.emit('node_img_dblclick', this, e, node)
|
||||
this.mindMap.emit('node_img_dblclick', this, e)
|
||||
})
|
||||
node.on('mouseenter', e => {
|
||||
this.mindMap.emit('node_img_mouseenter', this, node, e)
|
||||
@ -134,38 +114,46 @@ function createIconNode() {
|
||||
}
|
||||
|
||||
// 创建富文本节点
|
||||
function createRichTextNode(specifyText) {
|
||||
const hasCustomWidth = this.hasCustomWidth()
|
||||
let text =
|
||||
typeof specifyText === 'string' ? specifyText : this.getData('text')
|
||||
let { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt
|
||||
textAutoWrapWidth = hasCustomWidth ? this.customTextWidth : textAutoWrapWidth
|
||||
const g = new G()
|
||||
// 创建富文本结构,或复位富文本样式
|
||||
function createRichTextNode() {
|
||||
const { textAutoWrapWidth } = this.mindMap.opt
|
||||
let g = new G()
|
||||
// 重新设置富文本节点内容
|
||||
let recoverText = false
|
||||
if (this.getData('resetRichText')) {
|
||||
delete this.nodeData.data.resetRichText
|
||||
recoverText = true
|
||||
}
|
||||
if ([CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
|
||||
// 如果自定义过样式则不允许覆盖
|
||||
if (!this.hasCustomStyle()) {
|
||||
recoverText = true
|
||||
}
|
||||
}
|
||||
let text = this.getData('text')
|
||||
if (recoverText && !isUndef(text)) {
|
||||
if (checkIsRichText(text)) {
|
||||
// 如果是富文本那么移除内联样式
|
||||
text = removeRichTextStyes(text)
|
||||
// 判断节点内容是否是富文本
|
||||
let isRichText = checkIsRichText(text)
|
||||
// 样式字符串
|
||||
let style = this.style.createStyleText()
|
||||
if (isRichText) {
|
||||
// 如果是富文本那么线移除内联样式
|
||||
text = removeHtmlStyle(text)
|
||||
// 再添加新的内联样式
|
||||
let _text = text
|
||||
text = addHtmlStyle(text, 'span', style)
|
||||
// 给span添加样式没有成功,则尝试给strong标签添加样式
|
||||
if (text === _text) {
|
||||
text = addHtmlStyle(text, 'strong', style)
|
||||
}
|
||||
} else {
|
||||
// 非富文本则改为富文本结构
|
||||
text = `<p>${text}</p>`
|
||||
// 非富文本
|
||||
text = `<p><span style="${style}">${text}</span></p>`
|
||||
}
|
||||
this.setData({
|
||||
text
|
||||
text: text
|
||||
})
|
||||
}
|
||||
// 节点的富文本样式数据
|
||||
const nodeTextStyleList = []
|
||||
const nodeRichTextStyles = getNodeRichTextStyles(this)
|
||||
Object.keys(nodeRichTextStyles).forEach(prop => {
|
||||
nodeTextStyleList.push([prop, nodeRichTextStyles[prop]])
|
||||
})
|
||||
// 测量文本大小
|
||||
let html = `<div>${this.getData('text')}</div>`
|
||||
if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) {
|
||||
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl =
|
||||
document.createElement('div')
|
||||
@ -177,27 +165,16 @@ function createRichTextNode(specifyText) {
|
||||
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
|
||||
)
|
||||
}
|
||||
const div = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
|
||||
// 应用节点的文本样式
|
||||
nodeTextStyleList.forEach(([prop, value]) => {
|
||||
div.style[prop] = value
|
||||
})
|
||||
div.style.lineHeight = 1.2
|
||||
const html = `<div>${text}</div>`
|
||||
let div = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
|
||||
div.innerHTML = html
|
||||
const el = div.children[0]
|
||||
let el = div.children[0]
|
||||
el.classList.add('smm-richtext-node-wrap')
|
||||
addXmlns(el)
|
||||
el.style.maxWidth = textAutoWrapWidth + 'px'
|
||||
if (hasCustomWidth) {
|
||||
el.style.width = this.customTextWidth + 'px'
|
||||
} else {
|
||||
el.style.width = ''
|
||||
}
|
||||
let { width, height } = el.getBoundingClientRect()
|
||||
// 如果文本为空,那么需要计算一个默认高度
|
||||
if (height <= 0) {
|
||||
div.innerHTML = `<p>${emptyTextMeasureHeightText}</p>`
|
||||
div.innerHTML = '<p>abc123我和你</p>'
|
||||
let elTmp = div.children[0]
|
||||
elTmp.classList.add('smm-richtext-node-wrap')
|
||||
height = elTmp.getBoundingClientRect().height
|
||||
@ -212,15 +189,6 @@ function createRichTextNode(specifyText) {
|
||||
width,
|
||||
height
|
||||
})
|
||||
// 应用节点文本样式
|
||||
// 进入文本编辑时,这个样式也会同样添加到文本编辑框的元素上
|
||||
const foreignObjectStyle = {
|
||||
'line-height': 1.2
|
||||
}
|
||||
nodeTextStyleList.forEach(([prop, value]) => {
|
||||
foreignObjectStyle[camelCaseToHyphen(prop)] = value
|
||||
})
|
||||
foreignObject.css(foreignObjectStyle)
|
||||
g.add(foreignObject)
|
||||
return {
|
||||
node: g,
|
||||
@ -231,30 +199,24 @@ function createRichTextNode(specifyText) {
|
||||
}
|
||||
|
||||
// 创建文本节点
|
||||
function createTextNode(specifyText) {
|
||||
if (this.getData('needUpdate')) {
|
||||
delete this.nodeData.data.needUpdate
|
||||
}
|
||||
// 如果是富文本内容,那么转给富文本函数
|
||||
function createTextNode() {
|
||||
if (this.getData('richText')) {
|
||||
return this.createRichTextNode(specifyText)
|
||||
return this.createRichTextNode()
|
||||
}
|
||||
const text =
|
||||
typeof specifyText === 'string' ? specifyText : this.getData('text')
|
||||
if (this.getData('resetRichText')) {
|
||||
delete this.nodeData.data.resetRichText
|
||||
}
|
||||
const g = new G()
|
||||
const fontSize = this.getStyle('fontSize', false)
|
||||
const textAlign = this.getStyle('textAlign', false)
|
||||
let g = new G()
|
||||
let fontSize = this.getStyle('fontSize', false)
|
||||
let lineHeight = this.getStyle('lineHeight', false)
|
||||
// 文本超长自动换行
|
||||
let textStyle = this.style.getTextFontStyle()
|
||||
let textArr = []
|
||||
if (!isUndef(text)) {
|
||||
textArr = String(text).split(/\n/gim)
|
||||
if (!isUndef(this.getData('text'))) {
|
||||
textArr = String(this.getData('text')).split(/\n/gim)
|
||||
}
|
||||
const { textAutoWrapWidth: maxWidth, emptyTextMeasureHeightText } =
|
||||
this.mindMap.opt
|
||||
let isMultiLine = textArr.length > 1
|
||||
let maxWidth = this.mindMap.opt.textAutoWrapWidth
|
||||
let isMultiLine = false
|
||||
textArr.forEach((item, index) => {
|
||||
let arr = item.split('')
|
||||
let lines = []
|
||||
@ -262,7 +224,7 @@ function createTextNode(specifyText) {
|
||||
while (arr.length) {
|
||||
let str = arr.shift()
|
||||
let text = [...line, str].join('')
|
||||
if (measureText(text, this.style).width <= maxWidth) {
|
||||
if (measureText(text, textStyle).width <= maxWidth) {
|
||||
line.push(str)
|
||||
} else {
|
||||
lines.push(line.join(''))
|
||||
@ -277,38 +239,14 @@ function createTextNode(specifyText) {
|
||||
}
|
||||
textArr[index] = lines.join('\n')
|
||||
})
|
||||
textArr = textArr.join('\n').replace(/\n$/g, '').split(/\n/gim)
|
||||
textArr = textArr.join('\n').split(/\n/gim)
|
||||
textArr.forEach((item, index) => {
|
||||
// 避免尾部的空行不占宽度
|
||||
// 同时解决该问题:https://github.com/wanglin2/mind-map/issues/1037
|
||||
if (item === '') {
|
||||
item = ''
|
||||
}
|
||||
const node = new Text().text(item)
|
||||
node.addClass('smm-text-node-wrap')
|
||||
node.attr(
|
||||
'text-anchor',
|
||||
{
|
||||
left: 'start',
|
||||
center: 'middle',
|
||||
right: 'end'
|
||||
}[textAlign] || 'start'
|
||||
)
|
||||
let node = new Text().text(item)
|
||||
this.style.text(node)
|
||||
node.y(
|
||||
fontSize * noneRichTextNodeLineHeight * index +
|
||||
((noneRichTextNodeLineHeight - 1) * fontSize) / 2
|
||||
)
|
||||
node.y(fontSize * lineHeight * index)
|
||||
g.add(node)
|
||||
})
|
||||
let { width, height } = g.bbox()
|
||||
// 如果文本为空,那么需要计算一个默认高度
|
||||
if (height <= 0) {
|
||||
const tmpNode = new Text().text(emptyTextMeasureHeightText)
|
||||
this.style.text(tmpNode)
|
||||
const tmpBbox = tmpNode.bbox()
|
||||
height = tmpBbox.height
|
||||
}
|
||||
width = Math.min(Math.ceil(width), maxWidth)
|
||||
height = Math.ceil(height)
|
||||
g.attr('data-width', width)
|
||||
@ -323,16 +261,15 @@ function createTextNode(specifyText) {
|
||||
|
||||
// 创建超链接节点
|
||||
function createHyperlinkNode() {
|
||||
const { hyperlink, hyperlinkTitle } = this.getData()
|
||||
let { hyperlink, hyperlinkTitle } = this.getData()
|
||||
if (!hyperlink) {
|
||||
return
|
||||
}
|
||||
const { customHyperlinkJump, hyperlinkIcon } = this.mindMap.opt
|
||||
const { icon, style } = hyperlinkIcon
|
||||
const iconSize = this.getNodeIconSize('hyperlinkIcon')
|
||||
const node = new SVG().size(iconSize, iconSize)
|
||||
const { customHyperlinkJump } = this.mindMap.opt
|
||||
let iconSize = this.mindMap.themeConfig.iconSize
|
||||
let node = new SVG().size(iconSize, iconSize)
|
||||
// 超链接节点
|
||||
const a = new A().to(hyperlink).target('_blank')
|
||||
let a = new A().to(hyperlink).target('_blank')
|
||||
a.node.addEventListener('click', e => {
|
||||
if (typeof customHyperlinkJump === 'function') {
|
||||
e.preventDefault()
|
||||
@ -345,8 +282,8 @@ function createHyperlinkNode() {
|
||||
// 添加一个透明的层,作为鼠标区域
|
||||
a.rect(iconSize, iconSize).fill({ color: 'transparent' })
|
||||
// 超链接图标
|
||||
const iconNode = SVG(icon || iconsSvg.hyperlink).size(iconSize, iconSize)
|
||||
this.style.iconNode(iconNode, style.color)
|
||||
let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize)
|
||||
this.style.iconNode(iconNode)
|
||||
a.add(iconNode)
|
||||
node.add(a)
|
||||
return {
|
||||
@ -431,17 +368,16 @@ function createNoteNode() {
|
||||
if (!this.getData('note')) {
|
||||
return null
|
||||
}
|
||||
const { icon, style } = this.mindMap.opt.noteIcon
|
||||
const iconSize = this.getNodeIconSize('noteIcon')
|
||||
const node = new SVG()
|
||||
let iconSize = this.mindMap.themeConfig.iconSize
|
||||
let node = new SVG()
|
||||
.attr('cursor', 'pointer')
|
||||
.addClass('smm-node-note')
|
||||
.size(iconSize, iconSize)
|
||||
// 透明的层,用来作为鼠标区域
|
||||
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
|
||||
// 备注图标
|
||||
const iconNode = SVG(icon || iconsSvg.note).size(iconSize, iconSize)
|
||||
this.style.iconNode(iconNode, style.color)
|
||||
let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize)
|
||||
this.style.iconNode(iconNode)
|
||||
node.add(iconNode)
|
||||
// 备注tooltip
|
||||
if (!this.mindMap.opt.customNoteContentShow) {
|
||||
@ -487,9 +423,6 @@ function createNoteNode() {
|
||||
node.on('click', e => {
|
||||
this.mindMap.emit('node_note_click', this, e, node)
|
||||
})
|
||||
node.on('dblclick', e => {
|
||||
this.mindMap.emit('node_note_dblclick', this, e, node)
|
||||
})
|
||||
return {
|
||||
node,
|
||||
width: iconSize,
|
||||
@ -503,8 +436,7 @@ function createAttachmentNode() {
|
||||
if (!attachmentUrl) {
|
||||
return
|
||||
}
|
||||
const iconSize = this.getNodeIconSize('attachmentIcon')
|
||||
const { icon, style } = this.mindMap.opt.attachmentIcon
|
||||
const iconSize = this.mindMap.themeConfig.iconSize
|
||||
const node = new SVG().attr('cursor', 'pointer').size(iconSize, iconSize)
|
||||
if (attachmentName) {
|
||||
node.add(SVG(`<title>${attachmentName}</title>`))
|
||||
@ -512,8 +444,8 @@ function createAttachmentNode() {
|
||||
// 透明的层,用来作为鼠标区域
|
||||
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
|
||||
// 备注图标
|
||||
const iconNode = SVG(icon || iconsSvg.attachment).size(iconSize, iconSize)
|
||||
this.style.iconNode(iconNode, style.color)
|
||||
const iconNode = SVG(iconsSvg.attachment).size(iconSize, iconSize)
|
||||
this.style.iconNode(iconNode)
|
||||
node.add(iconNode)
|
||||
node.on('click', e => {
|
||||
this.mindMap.emit('node_attachmentClick', this, e, node)
|
||||
@ -528,15 +460,9 @@ function createAttachmentNode() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点图标大小
|
||||
function getNodeIconSize(prop) {
|
||||
const { style } = this.mindMap.opt[prop]
|
||||
return isUndef(style.size) ? this.mindMap.themeConfig.iconSize : style.size
|
||||
}
|
||||
|
||||
// 获取节点备注显示位置
|
||||
function getNoteContentPosition() {
|
||||
const iconSize = this.getNodeIconSize('noteIcon')
|
||||
const iconSize = this.mindMap.themeConfig.iconSize
|
||||
const { scaleY } = this.mindMap.view.getTransformData().transform
|
||||
const iconSizeAddScale = iconSize * scaleY
|
||||
let { left, top } = this._noteData.node.node.getBoundingClientRect()
|
||||
@ -577,7 +503,6 @@ function isUseCustomNodeContent() {
|
||||
}
|
||||
|
||||
export default {
|
||||
getImageUrl,
|
||||
createImgNode,
|
||||
getImgShowSize,
|
||||
createIconNode,
|
||||
@ -588,7 +513,6 @@ export default {
|
||||
createNoteNode,
|
||||
createAttachmentNode,
|
||||
getNoteContentPosition,
|
||||
getNodeIconSize,
|
||||
measureCustomNodeContentSize,
|
||||
isUseCustomNodeContent
|
||||
}
|
||||
|
||||
@ -1,42 +1,39 @@
|
||||
import btnsSvg from '../../../svg/btns'
|
||||
import { SVG, Circle, G, Text } from '@svgdotjs/svg.js'
|
||||
import { isUndef } from '../../../utils'
|
||||
|
||||
// 创建展开收起按钮的内容节点
|
||||
function createExpandNodeContent() {
|
||||
if (this._openExpandNode) {
|
||||
return
|
||||
}
|
||||
const { expandBtnSize, expandBtnIcon, isShowExpandNum } = this.mindMap.opt
|
||||
let { close, open } = expandBtnIcon || {}
|
||||
let { close, open } = this.mindMap.opt.expandBtnIcon || {}
|
||||
// 根据配置判断是否显示数量按钮
|
||||
if (isShowExpandNum) {
|
||||
if (this.mindMap.opt.isShowExpandNum) {
|
||||
// 展开的节点
|
||||
this._openExpandNode = new Text()
|
||||
this._openExpandNode.addClass('smm-expand-btn-text')
|
||||
// 文本垂直居中
|
||||
this._openExpandNode.attr({
|
||||
'text-anchor': 'middle',
|
||||
'dominant-baseline': 'middle',
|
||||
x: expandBtnSize / 2,
|
||||
x: this.expandBtnSize / 2,
|
||||
y: 2
|
||||
})
|
||||
} else {
|
||||
this._openExpandNode = SVG(open || btnsSvg.open).size(
|
||||
expandBtnSize,
|
||||
expandBtnSize
|
||||
this.expandBtnSize,
|
||||
this.expandBtnSize
|
||||
)
|
||||
this._openExpandNode.x(0).y(-expandBtnSize / 2)
|
||||
this._openExpandNode.x(0).y(-this.expandBtnSize / 2)
|
||||
}
|
||||
// 收起的节点
|
||||
this._closeExpandNode = SVG(close || btnsSvg.close).size(
|
||||
expandBtnSize,
|
||||
expandBtnSize
|
||||
this.expandBtnSize,
|
||||
this.expandBtnSize
|
||||
)
|
||||
this._closeExpandNode.x(0).y(-expandBtnSize / 2)
|
||||
this._closeExpandNode.x(0).y(-this.expandBtnSize / 2)
|
||||
// 填充节点
|
||||
this._fillExpandNode = new Circle().size(expandBtnSize)
|
||||
this._fillExpandNode.x(0).y(-expandBtnSize / 2)
|
||||
this._fillExpandNode = new Circle().size(this.expandBtnSize)
|
||||
this._fillExpandNode.x(0).y(-this.expandBtnSize / 2)
|
||||
|
||||
// 设置样式
|
||||
this.style.iconBtn(
|
||||
@ -80,13 +77,8 @@ function updateExpandBtnNode() {
|
||||
color: expandBtnStyle.strokeColor
|
||||
})
|
||||
// 计算子节点数量
|
||||
let count = this.sumNode(this.nodeData.children || [])
|
||||
if (typeof expandBtnNumHandler === 'function') {
|
||||
const res = expandBtnNumHandler(count, this)
|
||||
if (!isUndef(res)) {
|
||||
count = res
|
||||
}
|
||||
}
|
||||
let count = this.sumNode(this.nodeData.children)
|
||||
count = expandBtnNumHandler(count)
|
||||
node.text(String(count))
|
||||
} else {
|
||||
this._fillExpandNode.stroke('none')
|
||||
@ -106,7 +98,11 @@ function updateExpandBtnPos() {
|
||||
|
||||
// 创建展开收缩按钮
|
||||
function renderExpandBtn() {
|
||||
if (this.getChildrenLength() <= 0 || this.isRoot) {
|
||||
if (
|
||||
!this.nodeData.children ||
|
||||
this.nodeData.children.length <= 0 ||
|
||||
this.isRoot
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (this._expandBtn) {
|
||||
|
||||
@ -3,12 +3,15 @@ import { Rect } from '@svgdotjs/svg.js'
|
||||
// 渲染展开收起按钮的隐藏占位元素
|
||||
function renderExpandBtnPlaceholderRect() {
|
||||
// 根节点或没有子节点不需要渲染
|
||||
if (this.getChildrenLength() <= 0 || this.isRoot) {
|
||||
if (
|
||||
!this.nodeData.children ||
|
||||
this.nodeData.children.length <= 0 ||
|
||||
this.isRoot
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 默认显示展开按钮的情况下或不显示展开收起按钮的情况下不需要渲染
|
||||
const { alwaysShowExpandBtn, notShowExpandBtn, expandBtnSize } =
|
||||
this.mindMap.opt
|
||||
const { alwaysShowExpandBtn, notShowExpandBtn } = this.mindMap.opt
|
||||
if (!alwaysShowExpandBtn && !notShowExpandBtn) {
|
||||
let { width, height } = this
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
@ -20,7 +23,7 @@ function renderExpandBtnPlaceholderRect() {
|
||||
this.group.add(this._unVisibleRectRegionNode)
|
||||
this.renderer.layout.renderExpandBtnRect(
|
||||
this._unVisibleRectRegionNode,
|
||||
expandBtnSize,
|
||||
this.expandBtnSize,
|
||||
width,
|
||||
height,
|
||||
this
|
||||
@ -45,7 +48,7 @@ function updateExpandBtnPlaceholderRect() {
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
}
|
||||
// 没有子节点到有子节点需要渲染
|
||||
if (this.getChildrenLength() > 0) {
|
||||
if (this.nodeData.children && this.nodeData.children.length > 0) {
|
||||
if (!this._unVisibleRectRegionNode) {
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ function renderGeneralization(forceRender) {
|
||||
|
||||
// 更新节点概要数据
|
||||
function updateGeneralizationData() {
|
||||
const childrenLength = this.getChildrenLength()
|
||||
const childrenLength = this.nodeData.children.length
|
||||
const list = this.formatGetGeneralization()
|
||||
const newList = []
|
||||
list.forEach(item => {
|
||||
@ -186,29 +186,15 @@ function handleGeneralizationMouseenter() {
|
||||
const list = belongNode.formatGetGeneralization()
|
||||
const index = belongNode.getGeneralizationNodeIndex(this)
|
||||
const generalizationData = list[index]
|
||||
// 如果主题中设置了hoverRectColor颜色,那么使用该颜色
|
||||
// 否则使用hoverRectColor实例化选项的颜色
|
||||
// 兜底使用highlightNode方法的默认颜色
|
||||
const hoverRectColor = this.getStyle('hoverRectColor')
|
||||
const color = hoverRectColor || this.mindMap.opt.hoverRectColor
|
||||
const style = color
|
||||
? {
|
||||
stroke: color
|
||||
}
|
||||
: null
|
||||
// 区间概要,框子节点
|
||||
if (
|
||||
Array.isArray(generalizationData.range) &&
|
||||
generalizationData.range.length > 0
|
||||
) {
|
||||
this.mindMap.renderer.highlightNode(
|
||||
belongNode,
|
||||
generalizationData.range,
|
||||
style
|
||||
)
|
||||
this.mindMap.renderer.highlightNode(belongNode, generalizationData.range)
|
||||
} else {
|
||||
// 否则框自己
|
||||
this.mindMap.renderer.highlightNode(belongNode, null, style)
|
||||
this.mindMap.renderer.highlightNode(belongNode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,516 +0,0 @@
|
||||
import { CONSTANTS } from '../../../constants/constant'
|
||||
import { G, Rect } from '@svgdotjs/svg.js'
|
||||
import { createForeignObjectNode } from '../../../utils/index'
|
||||
|
||||
// 根据图片放置位置返回图片和文本的间距值
|
||||
function getImgTextMarin(dir, imgWidth, textWidth, imgHeight, textHeight) {
|
||||
// 图片和文字节点的间距
|
||||
const { imgTextMargin } = this.mindMap.opt
|
||||
if (dir === 'v') {
|
||||
// 垂直
|
||||
return imgHeight > 0 && textHeight > 0 ? imgTextMargin : 0
|
||||
} else {
|
||||
// 水平
|
||||
return imgWidth > 0 && textWidth > 0 ? imgTextMargin : 0
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签内容的大小
|
||||
function getTagContentSize(space) {
|
||||
let maxTagHeight = 0
|
||||
let width = this._tagData.reduce((sum, cur) => {
|
||||
maxTagHeight = Math.max(maxTagHeight, cur.height)
|
||||
return (sum += cur.width)
|
||||
}, 0)
|
||||
width += (this._tagData.length - 1) * space
|
||||
return {
|
||||
width,
|
||||
height: maxTagHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 计算节点尺寸信息
|
||||
function getNodeRect() {
|
||||
// 自定义节点内容
|
||||
if (this.isUseCustomNodeContent()) {
|
||||
const rect = this.measureCustomNodeContentSize(
|
||||
this._customNodeContent.cloneNode(true)
|
||||
)
|
||||
return {
|
||||
width: this.hasCustomWidth() ? this.customTextWidth : rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
}
|
||||
const { TAG_PLACEMENT, IMG_PLACEMENT } = CONSTANTS
|
||||
const { textContentMargin } = this.mindMap.opt
|
||||
const tagPlacement = this.getStyle('tagPlacement') || TAG_PLACEMENT.RIGHT
|
||||
const tagIsBottom = tagPlacement === TAG_PLACEMENT.BOTTOM
|
||||
const imgPlacement = this.getStyle('imgPlacement') || IMG_PLACEMENT.TOP
|
||||
// 宽高
|
||||
let imgContentWidth = 0
|
||||
let imgContentHeight = 0
|
||||
let textContentWidth = 0
|
||||
let textContentHeight = 0
|
||||
let tagContentWidth = 0
|
||||
let tagContentHeight = 0
|
||||
let spaceCount = 0
|
||||
// 存在图片
|
||||
if (this._imgData) {
|
||||
imgContentWidth = this._imgData.width
|
||||
imgContentHeight = this._imgData.height
|
||||
}
|
||||
// 库前置内容
|
||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||||
const itemData = this[`_${item.name}Data`]
|
||||
if (itemData) {
|
||||
textContentWidth += itemData.width
|
||||
textContentHeight = Math.max(textContentHeight, itemData.height)
|
||||
spaceCount++
|
||||
}
|
||||
})
|
||||
// 自定义前置内容
|
||||
if (this._prefixData) {
|
||||
textContentWidth += this._prefixData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._prefixData.height)
|
||||
spaceCount++
|
||||
}
|
||||
// 图标
|
||||
if (this._iconData.length > 0) {
|
||||
textContentWidth +=
|
||||
this._iconData.reduce((sum, cur) => {
|
||||
textContentHeight = Math.max(textContentHeight, cur.height)
|
||||
return (sum += cur.width)
|
||||
}, 0) +
|
||||
(this._iconData.length - 1) * textContentMargin
|
||||
spaceCount++
|
||||
}
|
||||
// 文字
|
||||
if (this._textData) {
|
||||
textContentWidth += this._textData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._textData.height)
|
||||
spaceCount++
|
||||
}
|
||||
// 超链接
|
||||
if (this._hyperlinkData) {
|
||||
textContentWidth += this._hyperlinkData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._hyperlinkData.height)
|
||||
spaceCount++
|
||||
}
|
||||
// 标签
|
||||
if (this._tagData.length > 0) {
|
||||
const { width: totalTagWidth, height: maxTagHeight } =
|
||||
this.getTagContentSize(textContentMargin)
|
||||
if (tagIsBottom) {
|
||||
// 文字下方
|
||||
tagContentWidth = totalTagWidth
|
||||
tagContentHeight = maxTagHeight
|
||||
} else {
|
||||
// 否则在右侧
|
||||
textContentWidth += totalTagWidth
|
||||
textContentHeight = Math.max(textContentHeight, maxTagHeight)
|
||||
spaceCount++
|
||||
}
|
||||
}
|
||||
// 备注
|
||||
if (this._noteData) {
|
||||
textContentWidth += this._noteData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._noteData.height)
|
||||
spaceCount++
|
||||
}
|
||||
// 附件
|
||||
if (this._attachmentData) {
|
||||
textContentWidth += this._attachmentData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._attachmentData.height)
|
||||
spaceCount++
|
||||
}
|
||||
// 自定义后置内容
|
||||
if (this._postfixData) {
|
||||
textContentWidth += this._postfixData.width
|
||||
textContentHeight = Math.max(textContentHeight, this._postfixData.height)
|
||||
spaceCount++
|
||||
}
|
||||
// 库后置内容
|
||||
this.mindMap.nodeInnerPostfixList.forEach(item => {
|
||||
const itemData = this[`_${item.name}Data`]
|
||||
if (itemData) {
|
||||
textContentWidth += itemData.width
|
||||
textContentHeight = Math.max(textContentHeight, itemData.height)
|
||||
spaceCount++
|
||||
}
|
||||
})
|
||||
textContentWidth += (spaceCount - 1) * textContentMargin
|
||||
// 文字内容部分的尺寸
|
||||
if (tagIsBottom && textContentWidth > 0 && tagContentHeight > 0) {
|
||||
this._rectInfo.textContentWidthWithoutTag = textContentWidth
|
||||
textContentWidth = Math.max(textContentWidth, tagContentWidth)
|
||||
textContentHeight = textContentHeight + textContentMargin + tagContentHeight
|
||||
}
|
||||
this._rectInfo.textContentWidth = textContentWidth
|
||||
this._rectInfo.textContentHeight = textContentHeight
|
||||
|
||||
// 纯内容宽高
|
||||
let _width = 0
|
||||
let _height = 0
|
||||
if ([IMG_PLACEMENT.TOP, IMG_PLACEMENT.BOTTOM].includes(imgPlacement)) {
|
||||
// 图片在上下
|
||||
_width = Math.max(imgContentWidth, textContentWidth)
|
||||
_height =
|
||||
imgContentHeight +
|
||||
textContentHeight +
|
||||
this.getImgTextMarin('v', 0, 0, imgContentHeight, textContentHeight)
|
||||
} else {
|
||||
// 图片在左右
|
||||
_width =
|
||||
imgContentWidth +
|
||||
textContentWidth +
|
||||
this.getImgTextMarin('h', imgContentWidth, textContentWidth)
|
||||
_height = Math.max(imgContentHeight, textContentHeight)
|
||||
}
|
||||
const { paddingX, paddingY } = this.getPaddingVale()
|
||||
// 计算节点形状需要的附加内边距
|
||||
const { paddingX: shapePaddingX, paddingY: shapePaddingY } =
|
||||
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
|
||||
this.shapePadding.paddingX = shapePaddingX
|
||||
this.shapePadding.paddingY = shapePaddingY
|
||||
// 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点
|
||||
const borderWidth = this.getBorderWidth()
|
||||
return {
|
||||
width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth,
|
||||
height: _height + paddingY * 2 + shapePaddingY * 2 + borderWidth
|
||||
}
|
||||
}
|
||||
|
||||
// 激活hover和激活边框
|
||||
function addHoverNode(width, height) {
|
||||
const { hoverRectPadding } = this.mindMap.opt
|
||||
this.hoverNode = new Rect()
|
||||
.size(width + hoverRectPadding * 2, height + hoverRectPadding * 2)
|
||||
.x(-hoverRectPadding)
|
||||
.y(-hoverRectPadding)
|
||||
this.hoverNode.addClass('smm-hover-node')
|
||||
this.style.hoverNode(this.hoverNode, width, height)
|
||||
this.group.add(this.hoverNode)
|
||||
}
|
||||
|
||||
// 当使用了完全自定义节点内容后,可以通过该方法实时更新节点大小
|
||||
function customNodeContentRealtimeLayout() {
|
||||
if (!this.group) return
|
||||
if (!this.isUseCustomNodeContent()) return
|
||||
// 删除除foreignObject外的其他元素
|
||||
if (this.shapeNode) this.shapeNode.remove()
|
||||
if (this._unVisibleRectRegionNode) this._unVisibleRectRegionNode.remove()
|
||||
if (this.hoverNode) this.hoverNode.remove()
|
||||
const { width, height } = this
|
||||
const halfBorderWidth = this.getBorderWidth() / 2
|
||||
// 节点形状
|
||||
this.shapeNode = this.shapeInstance.createShape()
|
||||
this.shapeNode.addClass('smm-node-shape')
|
||||
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
|
||||
this.style.shape(this.shapeNode)
|
||||
this.group.add(this.shapeNode)
|
||||
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
// 概要节点添加一个带所属节点id的类名
|
||||
if (this.isGeneralization && this.generalizationBelongNode) {
|
||||
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
|
||||
}
|
||||
// 激活hover和激活边框
|
||||
this.addHoverNode(width, height)
|
||||
// 将形状元素移至底层,避免遮挡foreignObject
|
||||
this.shapeNode.back()
|
||||
// 更新foreignObject元素大小
|
||||
this.group.findOne('foreignObject').size(width, height)
|
||||
}
|
||||
|
||||
// 定位节点内容
|
||||
function layout() {
|
||||
if (!this.group) return
|
||||
// 清除之前的内容
|
||||
this.group.clear()
|
||||
const {
|
||||
openRealtimeRenderOnNodeTextEdit,
|
||||
textContentMargin,
|
||||
addCustomContentToNode
|
||||
} = this.mindMap.opt
|
||||
// 避免编辑过程中展开收起按钮闪烁的问题
|
||||
// 暂时去掉,带来的问题太多
|
||||
// if (
|
||||
// openRealtimeRenderOnNodeTextEdit &&
|
||||
// this._expandBtn &&
|
||||
// this.getChildrenLength() > 0
|
||||
// ) {
|
||||
// this.group.add(this._expandBtn)
|
||||
// }
|
||||
const { width, height } = this
|
||||
let { paddingX, paddingY } = this.getPaddingVale()
|
||||
const halfBorderWidth = this.getBorderWidth() / 2
|
||||
paddingX += this.shapePadding.paddingX + halfBorderWidth
|
||||
paddingY += this.shapePadding.paddingY + halfBorderWidth
|
||||
// 节点形状
|
||||
this.shapeNode = this.shapeInstance.createShape()
|
||||
this.shapeNode.addClass('smm-node-shape')
|
||||
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
|
||||
this.style.shape(this.shapeNode)
|
||||
this.group.add(this.shapeNode)
|
||||
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
|
||||
this.renderExpandBtnPlaceholderRect()
|
||||
// 创建协同头像节点
|
||||
if (this.createUserListNode) this.createUserListNode()
|
||||
// 概要节点添加一个带所属节点id的类名
|
||||
if (this.isGeneralization && this.generalizationBelongNode) {
|
||||
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
|
||||
}
|
||||
// 如果存在自定义节点内容,那么使用自定义节点内容
|
||||
if (this.isUseCustomNodeContent()) {
|
||||
const foreignObject = createForeignObjectNode({
|
||||
el: this._customNodeContent,
|
||||
width,
|
||||
height
|
||||
})
|
||||
this.group.add(foreignObject)
|
||||
this.addHoverNode(width, height)
|
||||
return
|
||||
}
|
||||
const { IMG_PLACEMENT, TAG_PLACEMENT } = CONSTANTS
|
||||
const imgPlacement = this.getStyle('imgPlacement') || IMG_PLACEMENT.TOP
|
||||
const tagPlacement = this.getStyle('tagPlacement') || TAG_PLACEMENT.RIGHT
|
||||
const tagIsBottom = tagPlacement === TAG_PLACEMENT.BOTTOM
|
||||
let { textContentWidth, textContentHeight, textContentWidthWithoutTag } =
|
||||
this._rectInfo
|
||||
const textContentHeightWithTag = textContentHeight
|
||||
// 如果存在显示在文本下方的标签,那么非标签内容的整体高度需要减去标签高度
|
||||
let totalTagWidth = 0
|
||||
let maxTagHeight = 0
|
||||
const hasTagContent = this._tagData && this._tagData.length > 0
|
||||
if (hasTagContent) {
|
||||
const res = this.getTagContentSize(textContentMargin)
|
||||
totalTagWidth = res.width
|
||||
maxTagHeight = res.height
|
||||
if (tagIsBottom) {
|
||||
textContentHeight -= maxTagHeight + textContentMargin
|
||||
}
|
||||
}
|
||||
// 图片节点
|
||||
let imgWidth = 0
|
||||
let imgHeight = 0
|
||||
if (this._imgData) {
|
||||
imgWidth = this._imgData.width
|
||||
imgHeight = this._imgData.height
|
||||
this.group.add(this._imgData.node)
|
||||
switch (imgPlacement) {
|
||||
case IMG_PLACEMENT.TOP:
|
||||
this._imgData.node.cx(width / 2).y(paddingY)
|
||||
break
|
||||
case IMG_PLACEMENT.BOTTOM:
|
||||
this._imgData.node.cx(width / 2).y(height - paddingY - imgHeight)
|
||||
break
|
||||
case IMG_PLACEMENT.LEFT:
|
||||
this._imgData.node.x(paddingX).cy(height / 2)
|
||||
break
|
||||
case IMG_PLACEMENT.RIGHT:
|
||||
this._imgData.node.x(width - paddingX - imgWidth).cy(height / 2)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
// 内容节点
|
||||
let textContentNested = new G()
|
||||
let textContentOffsetX = 0
|
||||
if (hasTagContent && tagIsBottom) {
|
||||
textContentOffsetX =
|
||||
textContentWidthWithoutTag < textContentWidth
|
||||
? (textContentWidth - textContentWidthWithoutTag) / 2
|
||||
: 0
|
||||
}
|
||||
// 库前置内容
|
||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||||
const itemData = this[`_${item.name}Data`]
|
||||
if (itemData) {
|
||||
itemData.node
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - itemData.height) / 2)
|
||||
textContentNested.add(itemData.node)
|
||||
textContentOffsetX += itemData.width + textContentMargin
|
||||
}
|
||||
})
|
||||
// 自定义前置内容
|
||||
if (this._prefixData) {
|
||||
const foreignObject = createForeignObjectNode({
|
||||
el: this._prefixData.el,
|
||||
width: this._prefixData.width,
|
||||
height: this._prefixData.height
|
||||
})
|
||||
foreignObject
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._prefixData.height) / 2)
|
||||
textContentNested.add(foreignObject)
|
||||
textContentOffsetX += this._prefixData.width + textContentMargin
|
||||
}
|
||||
// icon
|
||||
let iconNested = new G()
|
||||
if (this._iconData && this._iconData.length > 0) {
|
||||
let iconLeft = 0
|
||||
this._iconData.forEach(item => {
|
||||
item.node
|
||||
.x(textContentOffsetX + iconLeft)
|
||||
.y((textContentHeight - item.height) / 2)
|
||||
iconNested.add(item.node)
|
||||
iconLeft += item.width + textContentMargin
|
||||
})
|
||||
textContentNested.add(iconNested)
|
||||
textContentOffsetX += iconLeft
|
||||
}
|
||||
// 文字
|
||||
if (this._textData) {
|
||||
const oldX = this._textData.node.attr('data-offsetx') || 0
|
||||
this._textData.node.attr('data-offsetx', textContentOffsetX)
|
||||
// 修复safari浏览器节点存在图标时文字位置不正确的问题
|
||||
;(this._textData.nodeContent || this._textData.node)
|
||||
.x(-oldX) // 修复非富文本模式下同时存在图标和换行的文本时,被收起和展开时图标与文字距离会逐渐拉大的问题
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._textData.height) / 2)
|
||||
// 如果开启了文本编辑实时渲染,需要判断当前渲染的节点是否是正在编辑的节点,是的话将透明度设置为0不显示
|
||||
if (openRealtimeRenderOnNodeTextEdit) {
|
||||
this._textData.node.opacity(
|
||||
this.mindMap.renderer.textEdit.getCurrentEditNode() === this ? 0 : 1
|
||||
)
|
||||
}
|
||||
textContentNested.add(this._textData.node)
|
||||
textContentOffsetX += this._textData.width + textContentMargin
|
||||
}
|
||||
// 超链接
|
||||
if (this._hyperlinkData) {
|
||||
this._hyperlinkData.node
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._hyperlinkData.height) / 2)
|
||||
textContentNested.add(this._hyperlinkData.node)
|
||||
textContentOffsetX += this._hyperlinkData.width + textContentMargin
|
||||
}
|
||||
// 标签
|
||||
let tagNested = new G()
|
||||
if (hasTagContent) {
|
||||
if (tagIsBottom) {
|
||||
// 标签显示在文字下方
|
||||
let tagLeft = 0
|
||||
this._tagData.forEach(item => {
|
||||
item.node.x(tagLeft).y((maxTagHeight - item.height) / 2)
|
||||
tagNested.add(item.node)
|
||||
tagLeft += item.width + textContentMargin
|
||||
})
|
||||
tagNested
|
||||
.x((textContentWidth - totalTagWidth) / 2)
|
||||
.y(textContentHeightWithTag - maxTagHeight)
|
||||
textContentNested.add(tagNested)
|
||||
} else {
|
||||
// 标签显示在文字右侧
|
||||
let tagLeft = 0
|
||||
this._tagData.forEach(item => {
|
||||
item.node
|
||||
.x(textContentOffsetX + tagLeft)
|
||||
.y((textContentHeight - item.height) / 2)
|
||||
tagNested.add(item.node)
|
||||
tagLeft += item.width + textContentMargin
|
||||
})
|
||||
textContentNested.add(tagNested)
|
||||
textContentOffsetX += tagLeft
|
||||
}
|
||||
}
|
||||
// 备注
|
||||
if (this._noteData) {
|
||||
this._noteData.node
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._noteData.height) / 2)
|
||||
textContentNested.add(this._noteData.node)
|
||||
textContentOffsetX += this._noteData.width + textContentMargin
|
||||
}
|
||||
// 附件
|
||||
if (this._attachmentData) {
|
||||
this._attachmentData.node
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._attachmentData.height) / 2)
|
||||
textContentNested.add(this._attachmentData.node)
|
||||
textContentOffsetX += this._attachmentData.width + textContentMargin
|
||||
}
|
||||
// 自定义后置内容
|
||||
if (this._postfixData) {
|
||||
const foreignObject = createForeignObjectNode({
|
||||
el: this._postfixData.el,
|
||||
width: this._postfixData.width,
|
||||
height: this._postfixData.height
|
||||
})
|
||||
foreignObject
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._postfixData.height) / 2)
|
||||
textContentNested.add(foreignObject)
|
||||
textContentOffsetX += this._postfixData.width + textContentMargin
|
||||
}
|
||||
// 库后置内容
|
||||
this.mindMap.nodeInnerPostfixList.forEach(item => {
|
||||
const itemData = this[`_${item.name}Data`]
|
||||
if (itemData) {
|
||||
itemData.node
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - itemData.height) / 2)
|
||||
textContentNested.add(itemData.node)
|
||||
textContentOffsetX += itemData.width + textContentMargin
|
||||
}
|
||||
})
|
||||
this.group.add(textContentNested)
|
||||
// 文字内容整体
|
||||
const { width: bboxWidth, height: bboxHeight } = textContentNested.bbox()
|
||||
let translateX = 0
|
||||
let translateY = 0
|
||||
switch (imgPlacement) {
|
||||
case IMG_PLACEMENT.TOP:
|
||||
translateX = width / 2 - bboxWidth / 2
|
||||
translateY =
|
||||
paddingY + // 内边距
|
||||
imgHeight + // 图片高度
|
||||
this.getImgTextMarin('v', 0, 0, imgHeight, textContentHeightWithTag) // 和图片的间距
|
||||
break
|
||||
case IMG_PLACEMENT.BOTTOM:
|
||||
translateX = width / 2 - bboxWidth / 2
|
||||
translateY = paddingY
|
||||
break
|
||||
case IMG_PLACEMENT.LEFT:
|
||||
translateX =
|
||||
imgWidth +
|
||||
paddingX +
|
||||
this.getImgTextMarin('h', imgWidth, textContentWidth)
|
||||
translateY = height / 2 - bboxHeight / 2
|
||||
break
|
||||
case IMG_PLACEMENT.RIGHT:
|
||||
translateX = paddingX
|
||||
translateY = height / 2 - bboxHeight / 2
|
||||
break
|
||||
}
|
||||
textContentNested.translate(translateX, translateY)
|
||||
this.addHoverNode(width, height)
|
||||
if (this._customContentAddToNodeAdd && this._customContentAddToNodeAdd.el) {
|
||||
const foreignObject = createForeignObjectNode(
|
||||
this._customContentAddToNodeAdd
|
||||
)
|
||||
this.group.add(foreignObject)
|
||||
if (
|
||||
addCustomContentToNode &&
|
||||
typeof addCustomContentToNode.handle === 'function'
|
||||
) {
|
||||
addCustomContentToNode.handle({
|
||||
content: this._customContentAddToNodeAdd,
|
||||
element: foreignObject,
|
||||
node: this
|
||||
})
|
||||
}
|
||||
}
|
||||
this.mindMap.emit('node_layout_end', this)
|
||||
}
|
||||
|
||||
export default {
|
||||
getImgTextMarin,
|
||||
getTagContentSize,
|
||||
getNodeRect,
|
||||
addHoverNode,
|
||||
layout,
|
||||
customNodeContentRealtimeLayout
|
||||
}
|
||||
@ -1,153 +0,0 @@
|
||||
import { Rect } from '@svgdotjs/svg.js'
|
||||
|
||||
// 初始化拖拽
|
||||
function initDragHandle() {
|
||||
if (!this.checkEnableDragModifyNodeWidth()) {
|
||||
return
|
||||
}
|
||||
// 拖拽手柄元素
|
||||
this._dragHandleNodes = null
|
||||
// 手柄元素的宽度
|
||||
this.dragHandleWidth = 4
|
||||
// 鼠标按下时的x坐标
|
||||
this.dragHandleMousedownX = 0
|
||||
// 鼠标是否处于按下状态
|
||||
this.isDragHandleMousedown = false
|
||||
// 当前拖拽的手柄序号
|
||||
this.dragHandleIndex = 0
|
||||
// 鼠标按下时记录当前的customTextWidth值
|
||||
this.dragHandleMousedownCustomTextWidth = 0
|
||||
// 鼠标按下时记录当前的手型样式
|
||||
this.dragHandleMousedownBodyCursor = ''
|
||||
// 鼠标按下时记录当前节点的left值
|
||||
this.dragHandleMousedownLeft = 0
|
||||
|
||||
this.onDragMousemoveHandle = this.onDragMousemoveHandle.bind(this)
|
||||
window.addEventListener('mousemove', this.onDragMousemoveHandle)
|
||||
this.onDragMouseupHandle = this.onDragMouseupHandle.bind(this)
|
||||
window.addEventListener('mouseup', this.onDragMouseupHandle)
|
||||
this.mindMap.on('node_mouseup', this.onDragMouseupHandle)
|
||||
}
|
||||
|
||||
// 鼠标移动事件
|
||||
function onDragMousemoveHandle(e) {
|
||||
if (!this.isDragHandleMousedown) return
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
let {
|
||||
minNodeTextModifyWidth,
|
||||
maxNodeTextModifyWidth,
|
||||
isUseCustomNodeContent,
|
||||
customCreateNodeContent
|
||||
} = this.mindMap.opt
|
||||
const useCustomContent =
|
||||
isUseCustomNodeContent && customCreateNodeContent && this._customNodeContent
|
||||
document.body.style.cursor = 'ew-resize'
|
||||
this.group.css({
|
||||
cursor: 'ew-resize'
|
||||
})
|
||||
const { scaleX } = this.mindMap.draw.transform()
|
||||
const ox = e.clientX - this.dragHandleMousedownX
|
||||
let newWidth =
|
||||
this.dragHandleMousedownCustomTextWidth +
|
||||
(this.dragHandleIndex === 0 ? -ox : ox) / scaleX
|
||||
newWidth = Math.max(newWidth, minNodeTextModifyWidth)
|
||||
if (maxNodeTextModifyWidth !== -1) {
|
||||
newWidth = Math.min(newWidth, maxNodeTextModifyWidth)
|
||||
}
|
||||
// 如果存在图片,那么最小值需要考虑图片宽度
|
||||
if (!useCustomContent && this.getData('image')) {
|
||||
const imgSize = this.getImgShowSize()
|
||||
if (
|
||||
this._rectInfo.textContentWidth - this.customTextWidth + newWidth <=
|
||||
imgSize[0]
|
||||
) {
|
||||
newWidth =
|
||||
imgSize[0] + this.customTextWidth - this._rectInfo.textContentWidth
|
||||
}
|
||||
}
|
||||
this.customTextWidth = newWidth
|
||||
if (this.dragHandleIndex === 0) {
|
||||
this.left = this.dragHandleMousedownLeft + ox / scaleX
|
||||
}
|
||||
// 自定义内容不重新渲染,交给开发者
|
||||
this.reRender(useCustomContent ? [] : ['text'], {
|
||||
ignoreUpdateCustomTextWidth: true
|
||||
})
|
||||
}
|
||||
|
||||
// 鼠标松开事件
|
||||
function onDragMouseupHandle() {
|
||||
if (!this.isDragHandleMousedown) return
|
||||
document.body.style.cursor = this.dragHandleMousedownBodyCursor
|
||||
this.group.css({
|
||||
cursor: 'default'
|
||||
})
|
||||
this.isDragHandleMousedown = false
|
||||
this.dragHandleMousedownX = 0
|
||||
this.dragHandleIndex = 0
|
||||
this.dragHandleMousedownCustomTextWidth = 0
|
||||
this.setData({
|
||||
customTextWidth: this.customTextWidth
|
||||
})
|
||||
this.mindMap.render()
|
||||
this.mindMap.emit('dragModifyNodeWidthEnd', this)
|
||||
}
|
||||
|
||||
// 插件拖拽手柄元素
|
||||
function createDragHandleNode() {
|
||||
const list = [new Rect(), new Rect()]
|
||||
list.forEach((node, index) => {
|
||||
node
|
||||
.size(this.dragHandleWidth, this.height)
|
||||
.fill({
|
||||
color: 'transparent'
|
||||
})
|
||||
.css({
|
||||
cursor: 'ew-resize'
|
||||
})
|
||||
node.on('mousedown', e => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
this.dragHandleMousedownX = e.clientX
|
||||
this.dragHandleIndex = index
|
||||
this.dragHandleMousedownCustomTextWidth =
|
||||
this.customTextWidth === undefined
|
||||
? this._textData
|
||||
? this._textData.width
|
||||
: this.width
|
||||
: this.customTextWidth
|
||||
this.dragHandleMousedownBodyCursor = document.body.style.cursor
|
||||
this.dragHandleMousedownLeft = this.left
|
||||
this.isDragHandleMousedown = true
|
||||
})
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
// 更新拖拽按钮的显隐和位置尺寸
|
||||
function updateDragHandle() {
|
||||
if (!this.checkEnableDragModifyNodeWidth()) return
|
||||
if (!this._dragHandleNodes) {
|
||||
this._dragHandleNodes = this.createDragHandleNode()
|
||||
}
|
||||
if (this.getData('isActive')) {
|
||||
this._dragHandleNodes.forEach(node => {
|
||||
node.height(this.height)
|
||||
this.group.add(node)
|
||||
})
|
||||
this._dragHandleNodes[1].x(this.width - this.dragHandleWidth)
|
||||
} else {
|
||||
this._dragHandleNodes.forEach(node => {
|
||||
node.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
initDragHandle,
|
||||
onDragMousemoveHandle,
|
||||
onDragMouseupHandle,
|
||||
createDragHandleNode,
|
||||
updateDragHandle
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
import btnsSvg from '../../../svg/btns'
|
||||
import { SVG, Circle, G } from '@svgdotjs/svg.js'
|
||||
|
||||
function initQuickCreateChildBtn() {
|
||||
if (this.isGeneralization) return
|
||||
this._quickCreateChildBtn = null
|
||||
this._showQuickCreateChildBtn = false
|
||||
}
|
||||
|
||||
// 显示按钮
|
||||
function showQuickCreateChildBtn() {
|
||||
if (this.isGeneralization || this.getChildrenLength() > 0) return
|
||||
// 创建按钮
|
||||
if (this._quickCreateChildBtn) {
|
||||
this.group.add(this._quickCreateChildBtn)
|
||||
} else {
|
||||
const { quickCreateChildBtnIcon, expandBtnStyle, expandBtnSize } =
|
||||
this.mindMap.opt
|
||||
const { icon, style } = quickCreateChildBtnIcon
|
||||
let { color, fill } = expandBtnStyle || {
|
||||
color: '#808080',
|
||||
fill: '#fff'
|
||||
}
|
||||
color = style.color || color
|
||||
// 图标节点
|
||||
const iconNode = SVG(icon || btnsSvg.quickCreateChild).size(
|
||||
expandBtnSize,
|
||||
expandBtnSize
|
||||
)
|
||||
iconNode.css({
|
||||
cursor: 'pointer'
|
||||
})
|
||||
iconNode.x(0).y(-expandBtnSize / 2)
|
||||
this.style.iconNode(iconNode, color)
|
||||
// 填充节点
|
||||
const fillNode = new Circle().size(expandBtnSize)
|
||||
fillNode.x(0).y(-expandBtnSize / 2)
|
||||
fillNode.fill({ color: fill }).css({
|
||||
cursor: 'pointer'
|
||||
})
|
||||
// 容器节点
|
||||
this._quickCreateChildBtn = new G()
|
||||
this._quickCreateChildBtn.add(fillNode).add(iconNode)
|
||||
this._quickCreateChildBtn.on('click', e => {
|
||||
e.stopPropagation()
|
||||
this.mindMap.emit('quick_create_btn_click', this)
|
||||
const { customQuickCreateChildBtnClick } = this.mindMap.opt
|
||||
if (typeof customQuickCreateChildBtnClick === 'function') {
|
||||
customQuickCreateChildBtnClick(this)
|
||||
return
|
||||
}
|
||||
this.mindMap.execCommand('INSERT_CHILD_NODE', true, [this])
|
||||
})
|
||||
this._quickCreateChildBtn.on('dblclick', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this._quickCreateChildBtn.addClass('smm-quick-create-child-btn')
|
||||
this.group.add(this._quickCreateChildBtn)
|
||||
}
|
||||
this._showQuickCreateChildBtn = true
|
||||
// 更新按钮
|
||||
this.renderer.layout.renderExpandBtn(this, this._quickCreateChildBtn)
|
||||
}
|
||||
|
||||
// 移除按钮
|
||||
function removeQuickCreateChildBtn() {
|
||||
if (this.isGeneralization) return
|
||||
if (this._quickCreateChildBtn && this._showQuickCreateChildBtn) {
|
||||
this._quickCreateChildBtn.remove()
|
||||
this._showQuickCreateChildBtn = false
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏按钮
|
||||
function hideQuickCreateChildBtn() {
|
||||
if (this.isGeneralization) return
|
||||
const { isActive } = this.getData()
|
||||
if (!isActive) {
|
||||
this.removeQuickCreateChildBtn()
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
initQuickCreateChildBtn,
|
||||
showQuickCreateChildBtn,
|
||||
removeQuickCreateChildBtn,
|
||||
hideQuickCreateChildBtn
|
||||
}
|
||||
@ -30,11 +30,8 @@ class View {
|
||||
})
|
||||
// 拖动视图
|
||||
this.mindMap.event.on('mousedown', e => {
|
||||
const { isDisableDrag, mousedownEventPreventDefault } = this.mindMap.opt
|
||||
if (isDisableDrag) return
|
||||
if (mousedownEventPreventDefault) {
|
||||
e.preventDefault()
|
||||
}
|
||||
if (this.mindMap.opt.isDisableDrag) return
|
||||
e.preventDefault()
|
||||
this.sx = this.x
|
||||
this.sy = this.y
|
||||
})
|
||||
@ -66,8 +63,7 @@ class View {
|
||||
mouseScaleCenterUseMousePosition,
|
||||
mousewheelMoveStep,
|
||||
mousewheelZoomActionReverse,
|
||||
disableMouseWheelZoom,
|
||||
translateRatio
|
||||
disableMouseWheelZoom
|
||||
} = this.mindMap.opt
|
||||
// 是否自定义鼠标滚轮事件
|
||||
if (
|
||||
@ -115,34 +111,26 @@ class View {
|
||||
}
|
||||
} else {
|
||||
// 2.鼠标滚轮事件控制画布移动
|
||||
let stepX = 0
|
||||
let stepY = 0
|
||||
if (isTouchPad) {
|
||||
// 如果是触控板,那么直接使用触控板滑动距离
|
||||
stepX = Math.abs(e.wheelDeltaX)
|
||||
stepY = Math.abs(e.wheelDeltaY)
|
||||
} else {
|
||||
stepX = stepY = mousewheelMoveStep
|
||||
}
|
||||
const step = isTouchPad ? 10 : mousewheelMoveStep
|
||||
let mx = 0
|
||||
let my = 0
|
||||
// 上移
|
||||
if (dirs.includes(CONSTANTS.DIR.DOWN)) {
|
||||
my = -stepY
|
||||
my = -step
|
||||
}
|
||||
// 下移
|
||||
if (dirs.includes(CONSTANTS.DIR.UP)) {
|
||||
my = stepY
|
||||
my = step
|
||||
}
|
||||
// 右移
|
||||
if (dirs.includes(CONSTANTS.DIR.LEFT)) {
|
||||
mx = stepX
|
||||
mx = step
|
||||
}
|
||||
// 左移
|
||||
if (dirs.includes(CONSTANTS.DIR.RIGHT)) {
|
||||
mx = -stepX
|
||||
mx = -step
|
||||
}
|
||||
this.translateXY(mx * translateRatio, my * translateRatio)
|
||||
this.translateXY(mx, my)
|
||||
}
|
||||
})
|
||||
this.mindMap.on('resize', () => {
|
||||
@ -250,9 +238,8 @@ class View {
|
||||
|
||||
// 缩小
|
||||
narrow(cx, cy, isTouchPad) {
|
||||
let { scaleRatio, minZoomRatio } = this.mindMap.opt
|
||||
scaleRatio = scaleRatio / (isTouchPad ? 5 : 1)
|
||||
const scale = Math.max(this.scale - scaleRatio, minZoomRatio / 100)
|
||||
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.emitEvent('scale')
|
||||
@ -260,14 +247,8 @@ class View {
|
||||
|
||||
// 放大
|
||||
enlarge(cx, cy, isTouchPad) {
|
||||
let { scaleRatio, maxZoomRatio } = this.mindMap.opt
|
||||
scaleRatio = scaleRatio / (isTouchPad ? 5 : 1)
|
||||
let scale = 0
|
||||
if (maxZoomRatio === -1) {
|
||||
scale = this.scale + scaleRatio
|
||||
} else {
|
||||
scale = Math.min(this.scale + scaleRatio, maxZoomRatio / 100)
|
||||
}
|
||||
const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1)
|
||||
const scale = this.scale + scaleRatio
|
||||
this.scaleInCenter(scale, cx, cy)
|
||||
this.transform()
|
||||
this.emitEvent('scale')
|
||||
@ -354,10 +335,6 @@ class View {
|
||||
|
||||
// 判断是否需要将思维导图限制在画布内
|
||||
checkNeedMindMapInCanvas() {
|
||||
// 如果当前在演示模式,那么不需要限制
|
||||
if (this.mindMap.demonstrate && this.mindMap.demonstrate.isInDemonstrate) {
|
||||
return false
|
||||
}
|
||||
const { isLimitMindMapInCanvasWhenHasScrollbar, isLimitMindMapInCanvas } =
|
||||
this.mindMap.opt
|
||||
// 如果注册了滚动条插件,那么使用isLimitMindMapInCanvasWhenHasScrollbar配置
|
||||
|
||||
@ -49,7 +49,10 @@ class Base {
|
||||
|
||||
// 检查当前来源是否需要重新计算节点大小
|
||||
checkIsNeedResizeSources() {
|
||||
return this.renderer.checkHasRenderSource(CONSTANTS.CHANGE_THEME)
|
||||
return [
|
||||
CONSTANTS.CHANGE_THEME,
|
||||
CONSTANTS.TRANSFORM_TO_NORMAL_NODE
|
||||
].includes(this.renderer.renderSource)
|
||||
}
|
||||
|
||||
// 层级类型改变
|
||||
@ -61,88 +64,48 @@ class Base {
|
||||
|
||||
// 检查是否是结构布局改变重新渲染展开收起按钮占位元素
|
||||
checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) {
|
||||
if (this.renderer.checkHasRenderSource(CONSTANTS.CHANGE_LAYOUT)) {
|
||||
if (this.renderer.renderSource === CONSTANTS.CHANGE_LAYOUT) {
|
||||
node.needRerenderExpandBtnPlaceholderRect = true
|
||||
}
|
||||
}
|
||||
|
||||
// 节点节点数据是否发生了改变
|
||||
checkIsNodeDataChange(lastData, curData) {
|
||||
if (lastData) {
|
||||
// 对比忽略激活状态和展开收起状态
|
||||
lastData = typeof lastData === 'string' ? JSON.parse(lastData) : lastData
|
||||
lastData.isActive = curData.isActive
|
||||
lastData.expand = curData.expand
|
||||
lastData = JSON.stringify(lastData)
|
||||
} else {
|
||||
// 只在都有数据时才进行对比
|
||||
return false
|
||||
// 获取节点编号信息
|
||||
getNumberInfo({ parent, ancestors, layerIndex, index }) {
|
||||
// 编号
|
||||
const hasNumberPlugin = !!this.mindMap.numbers
|
||||
const parentNumberStr =
|
||||
hasNumberPlugin && parent && parent._node.number
|
||||
? parent._node.number
|
||||
: ''
|
||||
const newNumberStr = hasNumberPlugin
|
||||
? this.mindMap.numbers.getNodeNumberStr({
|
||||
ancestors,
|
||||
layerIndex,
|
||||
num: index + 1,
|
||||
parentNumberStr
|
||||
})
|
||||
: ''
|
||||
return {
|
||||
hasNumberPlugin,
|
||||
newNumberStr
|
||||
}
|
||||
return lastData !== JSON.stringify(curData)
|
||||
}
|
||||
|
||||
// 检查库前置或后置内容是否改变了
|
||||
checkNodeFixChange(newNode, nodeInnerPrefixData, nodeInnerPostfixData) {
|
||||
// 库前置内容是否改变了
|
||||
let isNodeInnerPrefixChange = false
|
||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||||
if (item.updateNodeData) {
|
||||
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData)
|
||||
if (isChange) {
|
||||
isNodeInnerPrefixChange = isChange
|
||||
}
|
||||
}
|
||||
})
|
||||
// 库后置内容是否改变了
|
||||
let isNodeInnerPostfixChange = false
|
||||
this.mindMap.nodeInnerPostfixList.forEach(item => {
|
||||
if (item.updateNodeData) {
|
||||
const isChange = item.updateNodeData(newNode, nodeInnerPostfixData)
|
||||
if (isChange) {
|
||||
isNodeInnerPostfixChange = isChange
|
||||
}
|
||||
}
|
||||
})
|
||||
return isNodeInnerPrefixChange || isNodeInnerPostfixChange
|
||||
}
|
||||
|
||||
// 创建节点实例
|
||||
createNode(data, parent, isRoot, layerIndex, index, ancestors) {
|
||||
// 编号
|
||||
const { hasNumberPlugin, newNumberStr } = this.getNumberInfo({
|
||||
parent,
|
||||
ancestors,
|
||||
layerIndex,
|
||||
index
|
||||
})
|
||||
// 创建节点
|
||||
// 库前置内容数据
|
||||
const nodeInnerPrefixData = {}
|
||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||||
if (item.createNodeData) {
|
||||
const [key, value] = item.createNodeData({
|
||||
data,
|
||||
parent,
|
||||
ancestors,
|
||||
layerIndex,
|
||||
index
|
||||
})
|
||||
nodeInnerPrefixData[key] = value
|
||||
}
|
||||
})
|
||||
// 库后置内容数据
|
||||
const nodeInnerPostfixData = {}
|
||||
this.mindMap.nodeInnerPostfixList.forEach(item => {
|
||||
if (item.createNodeData) {
|
||||
const [key, value] = item.createNodeData({
|
||||
data,
|
||||
parent,
|
||||
ancestors,
|
||||
layerIndex,
|
||||
index
|
||||
})
|
||||
nodeInnerPostfixData[key] = value
|
||||
}
|
||||
})
|
||||
const uid = data.data.uid
|
||||
let newNode = null
|
||||
// 数据上保存了节点引用,那么直接复用节点
|
||||
if (data && data._node && !this.renderer.reRender) {
|
||||
newNode = data._node
|
||||
// 节点层级改变了
|
||||
const isLayerTypeChange = this.checkIsLayerTypeChange(
|
||||
newNode.layerIndex,
|
||||
layerIndex
|
||||
@ -156,33 +119,25 @@ class Base {
|
||||
}
|
||||
this.cacheNode(data._node.uid, newNode)
|
||||
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
|
||||
// 库前置或后置内容是否改变了
|
||||
const isNodeInnerFixChange = this.checkNodeFixChange(
|
||||
newNode,
|
||||
nodeInnerPrefixData,
|
||||
nodeInnerPostfixData
|
||||
)
|
||||
// 主题或主题配置改变了
|
||||
const isResizeSource = this.checkIsNeedResizeSources()
|
||||
// 节点数据改变了
|
||||
const isNodeDataChange = this.checkIsNodeDataChange(
|
||||
data._node.nodeDataSnapshot,
|
||||
data.data
|
||||
)
|
||||
// 重新计算节点大小和布局
|
||||
// 判断编号是否改变
|
||||
let isNumberChange = false
|
||||
if (hasNumberPlugin) {
|
||||
isNumberChange = this.mindMap.numbers.updateNumber(
|
||||
newNode,
|
||||
newNumberStr
|
||||
)
|
||||
}
|
||||
// 主题或主题配置改变了、节点层级改变了,需要重新渲染节点文本等情况需要重新计算节点大小和布局
|
||||
if (
|
||||
isResizeSource ||
|
||||
isNodeDataChange ||
|
||||
this.checkIsNeedResizeSources() ||
|
||||
isLayerTypeChange ||
|
||||
(newNode.getData('resetRichText') && // 自定义节点内容可以直接忽略resetRichText
|
||||
!newNode.isUseCustomNodeContent()) ||
|
||||
newNode.getData('needUpdate') ||
|
||||
isNodeInnerFixChange
|
||||
newNode.getData('resetRichText') ||
|
||||
isNumberChange
|
||||
) {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
}
|
||||
this.checkGetGeneralizationChange(newNode, isResizeSource)
|
||||
this.checkGetGeneralizationChange(newNode)
|
||||
} else if (
|
||||
(this.lru.has(uid) || this.renderer.lastNodeCache[uid]) &&
|
||||
!this.renderer.reRender
|
||||
@ -194,7 +149,6 @@ class Base {
|
||||
newNode = this.lru.get(uid) || this.renderer.lastNodeCache[uid]
|
||||
// 保存该节点上一次的数据
|
||||
const lastData = JSON.stringify(newNode.getData())
|
||||
// 节点层级改变了
|
||||
const isLayerTypeChange = this.checkIsLayerTypeChange(
|
||||
newNode.layerIndex,
|
||||
layerIndex
|
||||
@ -212,28 +166,27 @@ class Base {
|
||||
data._node = newNode
|
||||
// 主题或主题配置改变了需要重新计算节点大小和布局
|
||||
const isResizeSource = this.checkIsNeedResizeSources()
|
||||
// 点数据改变了
|
||||
const isNodeDataChange = this.checkIsNodeDataChange(lastData, data.data)
|
||||
// 库前置或后置内容是否改变了
|
||||
const isNodeInnerFixChange = this.checkNodeFixChange(
|
||||
newNode,
|
||||
nodeInnerPrefixData,
|
||||
nodeInnerPostfixData
|
||||
)
|
||||
// 重新计算节点大小和布局
|
||||
// 主题或主题配置改变了、节点层级改变了,需要重新渲染节点文本,节点数据改变了等情况需要重新计算节点大小和布局
|
||||
const isNodeDataChange = lastData !== JSON.stringify(data.data)
|
||||
// 判断编号是否改变
|
||||
let isNumberChange = false
|
||||
if (hasNumberPlugin) {
|
||||
isNumberChange = this.mindMap.numbers.updateNumber(
|
||||
newNode,
|
||||
newNumberStr
|
||||
)
|
||||
}
|
||||
if (
|
||||
isResizeSource ||
|
||||
isNodeDataChange ||
|
||||
isLayerTypeChange ||
|
||||
(newNode.getData('resetRichText') &&
|
||||
!newNode.isUseCustomNodeContent()) ||
|
||||
newNode.getData('needUpdate') ||
|
||||
isNodeInnerFixChange
|
||||
newNode.getData('resetRichText') ||
|
||||
isNumberChange
|
||||
) {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
}
|
||||
this.checkGetGeneralizationChange(newNode, isResizeSource)
|
||||
this.checkGetGeneralizationChange(newNode)
|
||||
} else {
|
||||
// 创建新节点
|
||||
const newUid = uid || createUid()
|
||||
@ -246,7 +199,7 @@ class Base {
|
||||
layerIndex,
|
||||
isRoot,
|
||||
parent: !isRoot ? parent._node : null,
|
||||
...nodeInnerPrefixData
|
||||
number: newNumberStr
|
||||
})
|
||||
// uid保存到数据上,为了节点复用
|
||||
data.data.uid = newUid
|
||||
@ -275,7 +228,7 @@ class Base {
|
||||
}
|
||||
|
||||
// 检查概要节点是否需要更新
|
||||
checkGetGeneralizationChange(node, isResizeSource) {
|
||||
checkGetGeneralizationChange(node) {
|
||||
const generalizationList = node.getData('generalization')
|
||||
if (
|
||||
generalizationList &&
|
||||
@ -286,13 +239,8 @@ class Base {
|
||||
const gNode = item.generalizationNode
|
||||
const oldData = gNode.getData()
|
||||
const newData = generalizationList[index]
|
||||
if (
|
||||
isResizeSource ||
|
||||
(newData && JSON.stringify(oldData) !== JSON.stringify(newData))
|
||||
) {
|
||||
if (newData) {
|
||||
gNode.nodeData.data = newData
|
||||
}
|
||||
if (newData && JSON.stringify(oldData) !== JSON.stringify(newData)) {
|
||||
gNode.nodeData.data = newData
|
||||
gNode.getSize()
|
||||
gNode.needLayout = true
|
||||
}
|
||||
@ -423,32 +371,18 @@ class Base {
|
||||
}
|
||||
|
||||
// 二次贝塞尔曲线
|
||||
quadraticCurvePath(x1, y1, x2, y2, v = false) {
|
||||
let cx, cy
|
||||
if (v) {
|
||||
cx = x1 + (x2 - x1) * 0.8
|
||||
cy = y1 + (y2 - y1) * 0.2
|
||||
} else {
|
||||
cx = x1 + (x2 - x1) * 0.2
|
||||
cy = y1 + (y2 - y1) * 0.8
|
||||
}
|
||||
quadraticCurvePath(x1, y1, x2, y2) {
|
||||
let cx = x1 + (x2 - x1) * 0.2
|
||||
let cy = y1 + (y2 - y1) * 0.8
|
||||
return `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
}
|
||||
|
||||
// 三次贝塞尔曲线
|
||||
cubicBezierPath(x1, y1, x2, y2, v = false) {
|
||||
let cx1, cy1, cx2, cy2
|
||||
if (v) {
|
||||
cx1 = x1
|
||||
cy1 = y1 + (y2 - y1) / 2
|
||||
cx2 = x2
|
||||
cy2 = cy1
|
||||
} else {
|
||||
cx1 = x1 + (x2 - x1) / 2
|
||||
cy1 = y1
|
||||
cx2 = cx1
|
||||
cy2 = y2
|
||||
}
|
||||
cubicBezierPath(x1, y1, x2, y2) {
|
||||
let cx1 = x1 + (x2 - x1) / 2
|
||||
let cy1 = y1
|
||||
let cx2 = cx1
|
||||
let cy2 = y2
|
||||
return `M ${x1},${y1} C ${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}`
|
||||
}
|
||||
|
||||
@ -487,10 +421,8 @@ class Base {
|
||||
const end = list[len - 1]
|
||||
// 如果三点在一条直线,那么不用处理
|
||||
const isOneLine =
|
||||
(start[0].toFixed(0) === center[0].toFixed(0) &&
|
||||
center[0].toFixed(0) === end[0].toFixed(0)) ||
|
||||
(start[1].toFixed(0) === center[1].toFixed(0) &&
|
||||
center[1].toFixed(0) === end[1].toFixed(0))
|
||||
(start[0] === center[0] && center[0] === end[0]) ||
|
||||
(start[1] === center[1] && center[1] === end[1])
|
||||
if (!isOneLine) {
|
||||
const cStart = this.computeNewPoint(start, center, lineRadius)
|
||||
const cEnd = this.computeNewPoint(end, center, lineRadius)
|
||||
|
||||
@ -2,78 +2,14 @@ import Base from './Base'
|
||||
import { walk, asyncRun, degToRad, getNodeIndexInNodeList } from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
import utils from './fishboneUtils'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import { shapeStyleProps } from '../core/render/node/Style'
|
||||
|
||||
// 鱼骨图
|
||||
class Fishbone extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}, layout) {
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
this.layout = layout
|
||||
this.indent = 0.3
|
||||
this.childIndent = 0.5
|
||||
this.fishTail = null
|
||||
this.maxx = 0
|
||||
this.headRatio = 1
|
||||
this.tailRatio = 0.6
|
||||
this.paddingXRatio = 0.3
|
||||
this.fishHeadPathStr =
|
||||
'M4,181 C4,181, 0,177, 4,173 Q 96.09523809523809,0, 288.2857142857143,0 L 288.2857142857143,354 Q 48.047619047619044,354, 8,218.18367346938777 C8,218.18367346938777, 6,214.18367346938777, 8,214.18367346938777 L 41.183673469387756,214.18367346938777 Z'
|
||||
this.fishTailPathStr =
|
||||
'M 606.9342905223708 0 Q 713.1342905223709 -177 819.3342905223708 -177 L 766.2342905223709 0 L 819.3342905223708 177 Q 713.1342905223709 177 606.9342905223708 0 z'
|
||||
this.bindEvent()
|
||||
this.extendShape()
|
||||
this.beforeChange = this.beforeChange.bind(this)
|
||||
}
|
||||
|
||||
// 重新渲染时,节点连线是否全部删除
|
||||
// 鱼尾鱼骨图会多渲染一些连线,按需删除无法删除掉,只能全部删除重新创建
|
||||
nodeIsRemoveAllLines(node) {
|
||||
return node.isRoot || node.layerIndex === 1
|
||||
}
|
||||
|
||||
// 是否是带鱼头鱼尾的鱼骨图
|
||||
isFishbone2() {
|
||||
return this.layout === CONSTANTS.LAYOUT.FISHBONE2
|
||||
}
|
||||
|
||||
bindEvent() {
|
||||
if (!this.isFishbone2()) return
|
||||
this.onCheckUpdateFishTail = this.onCheckUpdateFishTail.bind(this)
|
||||
this.mindMap.on('afterExecCommand', this.onCheckUpdateFishTail)
|
||||
}
|
||||
|
||||
unBindEvent() {
|
||||
this.mindMap.off('afterExecCommand', this.onCheckUpdateFishTail)
|
||||
}
|
||||
|
||||
// 扩展节点形状
|
||||
extendShape() {
|
||||
if (!this.isFishbone2()) return
|
||||
// 扩展鱼头形状
|
||||
this.mindMap.addShape({
|
||||
name: 'fishHead',
|
||||
createShape: node => {
|
||||
const rect = SVG(`<path d="${this.fishHeadPathStr}"></path>`)
|
||||
const { width, height } = node.shapeInstance.getNodeSize()
|
||||
rect.size(width, height)
|
||||
return rect
|
||||
},
|
||||
getPadding: ({ width, height, paddingX, paddingY }) => {
|
||||
width += paddingX * 2
|
||||
height += paddingY * 2
|
||||
let shapePaddingX = this.paddingXRatio * width
|
||||
let shapePaddingY = 0
|
||||
width += shapePaddingX * 2
|
||||
const newHeight = width / this.headRatio
|
||||
shapePaddingY = (newHeight - height) / 2
|
||||
return {
|
||||
paddingX: shapePaddingX,
|
||||
paddingY: shapePaddingY
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 布局
|
||||
@ -81,14 +17,12 @@ class Fishbone extends Base {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
this.addFishTail()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
this.updateFishTailPosition()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
@ -97,75 +31,14 @@ class Fishbone extends Base {
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 创建鱼尾
|
||||
addFishTail() {
|
||||
if (!this.isFishbone2()) return
|
||||
const exist = this.mindMap.lineDraw.findOne('.smm-layout-fishbone-tail')
|
||||
if (!exist) {
|
||||
this.fishTail = SVG(`<path d="${this.fishTailPathStr}"></path>`)
|
||||
this.fishTail.addClass('smm-layout-fishbone-tail')
|
||||
} else {
|
||||
this.fishTail = exist
|
||||
}
|
||||
const tailHeight = this.root.height
|
||||
const tailWidth = tailHeight * this.tailRatio
|
||||
this.fishTail.size(tailWidth, tailHeight)
|
||||
this.styleFishTail()
|
||||
this.mindMap.lineDraw.add(this.fishTail)
|
||||
}
|
||||
|
||||
// 如果根节点更新了形状样式,那么鱼尾也要更新
|
||||
onCheckUpdateFishTail(name, node, data) {
|
||||
if (name === 'SET_NODE_DATA') {
|
||||
let hasShapeProp = false
|
||||
Object.keys(data).forEach(key => {
|
||||
if (shapeStyleProps.includes(key)) {
|
||||
hasShapeProp = true
|
||||
}
|
||||
})
|
||||
if (hasShapeProp) {
|
||||
this.styleFishTail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
styleFishTail() {
|
||||
this.root.style.shape(this.fishTail)
|
||||
}
|
||||
|
||||
// 删除鱼尾
|
||||
removeFishTail() {
|
||||
const exist = this.mindMap.lineDraw.findOne('.smm-layout-fishbone-tail')
|
||||
if (exist) {
|
||||
exist.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新鱼尾形状位置
|
||||
updateFishTailPosition() {
|
||||
if (!this.isFishbone2()) return
|
||||
this.fishTail.x(this.maxx).cy(this.root.top + this.root.height / 2)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index, ancestors) => {
|
||||
if (isRoot && this.isFishbone2()) {
|
||||
// 将根节点形状强制修改为鱼头
|
||||
node.data.shape = 'fishHead'
|
||||
}
|
||||
// 创建节点
|
||||
let newNode = this.createNode(
|
||||
node,
|
||||
parent,
|
||||
isRoot,
|
||||
layerIndex,
|
||||
index,
|
||||
ancestors
|
||||
)
|
||||
let newNode = this.createNode(node, parent, isRoot, layerIndex, index, ancestors)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
@ -184,14 +57,10 @@ class Fishbone extends Base {
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
let marginY = this.getMarginY(layerIndex)
|
||||
// 带鱼头鱼尾的鱼骨图因为根节点高度比较大,所以二级节点需要向中间靠一点
|
||||
const topOffset = this.isFishbone2() ? parent._node.height / 4 : 0
|
||||
if (this.checkIsTop(newNode)) {
|
||||
newNode.top =
|
||||
parent._node.top - newNode.height - marginY + topOffset
|
||||
newNode.top = parent._node.top - newNode.height - marginY
|
||||
} else {
|
||||
newNode.top =
|
||||
parent._node.top + parent._node.height + marginY - topOffset
|
||||
newNode.top = parent._node.top + parent._node.height + marginY
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,11 +82,8 @@ class Fishbone extends Base {
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (node.isRoot) {
|
||||
let marginX = this.getMarginX(layerIndex + 1)
|
||||
const heightOffsetRatio = this.isFishbone2() ? 2 : 1
|
||||
let topTotalLeft =
|
||||
node.left + node.width + node.height / heightOffsetRatio + marginX
|
||||
let bottomTotalLeft =
|
||||
node.left + node.width + node.height / heightOffsetRatio + marginX
|
||||
let topTotalLeft = node.left + node.width + node.height + marginX
|
||||
let bottomTotalLeft = node.left + node.width + node.height + marginX
|
||||
node.children.forEach(item => {
|
||||
if (this.checkIsTop(item)) {
|
||||
item.left = topTotalLeft
|
||||
@ -267,27 +133,19 @@ class Fishbone extends Base {
|
||||
if (node.isRoot) {
|
||||
let topTotalLeft = 0
|
||||
let bottomTotalLeft = 0
|
||||
let maxx = -Infinity
|
||||
node.children.forEach(item => {
|
||||
if (this.checkIsTop(item)) {
|
||||
item.left += topTotalLeft
|
||||
this.updateChildren(item.children, 'left', topTotalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
if (right > maxx) {
|
||||
maxx = right
|
||||
}
|
||||
topTotalLeft += right - left
|
||||
} else {
|
||||
item.left += bottomTotalLeft
|
||||
this.updateChildren(item.children, 'left', bottomTotalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
if (right > maxx) {
|
||||
maxx = right
|
||||
}
|
||||
bottomTotalLeft += right - left
|
||||
}
|
||||
})
|
||||
this.maxx = maxx
|
||||
}
|
||||
},
|
||||
true
|
||||
@ -391,8 +249,7 @@ class Fishbone extends Base {
|
||||
// 水平线段到二级节点的连线
|
||||
let marginY = this.getMarginY(item.layerIndex)
|
||||
let nodeLineX = item.left
|
||||
let offset =
|
||||
node.height / 2 + marginY - (this.isFishbone2() ? node.height / 4 : 0)
|
||||
let offset = node.height / 2 + marginY
|
||||
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
let line = this.lineDraw.path()
|
||||
if (this.checkIsTop(item)) {
|
||||
@ -420,14 +277,11 @@ class Fishbone extends Base {
|
||||
let nodeHalfTop = node.top + node.height / 2
|
||||
let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1)
|
||||
let line = this.lineDraw.path()
|
||||
const lineEndX = this.isFishbone2()
|
||||
? this.maxx
|
||||
: maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
line.plot(
|
||||
this.transformPath(
|
||||
`M ${
|
||||
node.left + node.width
|
||||
},${nodeHalfTop} L ${lineEndX},${nodeHalfTop}`
|
||||
`M ${node.left + node.width},${nodeHalfTop} L ${
|
||||
maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
},${nodeHalfTop}`
|
||||
)
|
||||
)
|
||||
node.style.line(line)
|
||||
@ -552,16 +406,6 @@ class Fishbone extends Base {
|
||||
rect.size(width, expandBtnSize).x(0).y(height)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换切换为其他结构时的处理
|
||||
beforeChange() {
|
||||
// 删除鱼尾
|
||||
if (!this.isFishbone2()) return
|
||||
this.root.nodeData.data.shape = CONSTANTS.SHAPE.RECTANGLE
|
||||
this.removeFishTail()
|
||||
this.unBindEvent()
|
||||
this.mindMap.removeShape('fishHead')
|
||||
}
|
||||
}
|
||||
|
||||
export default Fishbone
|
||||
|
||||
370
simple-mind-map/src/layouts/FishboneBottom.js
Normal file
@ -0,0 +1,370 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
|
||||
const degToRad = deg => {
|
||||
return (Math.PI / 180) * deg
|
||||
}
|
||||
|
||||
// 下方鱼骨图
|
||||
class Fishbone extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
// 创建节点
|
||||
let newNode = this.createNode(node, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 三级及以下节点以上级方向为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
newNode.top = parent._node.top + parent._node.height
|
||||
}
|
||||
}
|
||||
if (!node.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
if (node.isRoot) {
|
||||
let totalLeft = node.left + node.width
|
||||
node.children.forEach(item => {
|
||||
item.left = totalLeft
|
||||
totalLeft += item.width
|
||||
})
|
||||
}
|
||||
if (layerIndex === 1 && node.children) {
|
||||
// 遍历二级节点的子节点
|
||||
let startLeft = node.left + node.width * 0.5
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top =
|
||||
totalTop +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
totalTop +=
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
if (layerIndex > 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let startLeft = node.left + node.width * 0.5
|
||||
let totalTop =
|
||||
node.top -
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top = totalTop - item.height
|
||||
totalTop -=
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
// 调整三级节点的top
|
||||
// if (layerIndex === 2 && len > 0) {
|
||||
// let totalHeight = node.children.reduce((h, item) => {
|
||||
// return h + item.height
|
||||
// }, 0)
|
||||
// this.updateBrothersTop(node, totalHeight)
|
||||
// }
|
||||
if (layerIndex > 2 && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
)
|
||||
}, 0)
|
||||
this.updateBrothersTop(node, -totalHeight)
|
||||
}
|
||||
},
|
||||
(node, parent) => {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = 0
|
||||
let totalHeight2 = 0
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let hasChildren = this.getNodeActChildrenLength(item) > 0
|
||||
let nodeTotalHeight = this.getNodeAreaHeight(item)
|
||||
let offset =
|
||||
hasChildren > 0
|
||||
? nodeTotalHeight -
|
||||
item.height -
|
||||
(hasChildren ? item.expandBtnSize : 0)
|
||||
: 0
|
||||
let _top = totalHeight + offset
|
||||
item.top += _top
|
||||
// 调整left
|
||||
let offsetLeft =
|
||||
(totalHeight2 + nodeTotalHeight) /
|
||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
item.left += offsetLeft
|
||||
totalHeight += offset
|
||||
totalHeight2 += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
this.updateChildrenPro(item.children, {
|
||||
top: _top,
|
||||
left: offsetLeft
|
||||
})
|
||||
})
|
||||
}
|
||||
// 调整二级节点的子节点的left值
|
||||
if (node.isRoot) {
|
||||
let totalLeft = 0
|
||||
node.children.forEach(item => {
|
||||
item.left += totalLeft
|
||||
this.updateChildren(item.children, 'left', totalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
totalLeft += right - left
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
loop(node)
|
||||
return totalHeight
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node) {
|
||||
let childrenList = node.children
|
||||
let totalAddWidth = 0
|
||||
childrenList.forEach(item => {
|
||||
item.left += totalAddWidth
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
||||
}
|
||||
// let areaWidth = this.getNodeAreaWidth(item)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
let areaWidth = right - left
|
||||
let difference = areaWidth - item.width
|
||||
if (difference > 0) {
|
||||
totalAddWidth += difference
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, node.layerIndex === 3 ? 0 : addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
let len = node.children.length
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 = prevBother.left + prevBother.width
|
||||
let x2 = item.left
|
||||
let y = node.top + node.height / 2
|
||||
let path = `M ${x1},${y} L ${x2},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
let maxy = -Infinity
|
||||
let miny = Infinity
|
||||
let maxx = -Infinity
|
||||
let x = node.left + node.width * 0.3
|
||||
node.children.forEach((item, index) => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
let y = item.top + item.height / 2
|
||||
if (y > maxy) {
|
||||
maxy = y
|
||||
}
|
||||
if (y < miny) {
|
||||
miny = y
|
||||
}
|
||||
// 水平线
|
||||
if (node.layerIndex > 1) {
|
||||
let path = `M ${x},${y} L ${item.left},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.lineDraw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * 0.3
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top + height} L ${x + lineLength},${
|
||||
top +
|
||||
height +
|
||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(`M ${x},${top} L ${x},${miny}`)
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default Fishbone
|
||||
351
simple-mind-map/src/layouts/FishboneTop.js
Normal file
@ -0,0 +1,351 @@
|
||||
import Base from './Base'
|
||||
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
|
||||
import { CONSTANTS } from '../utils/constant'
|
||||
|
||||
const degToRad = deg => {
|
||||
return (Math.PI / 180) * deg
|
||||
}
|
||||
|
||||
// 上方鱼骨图
|
||||
class Fishbone extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
super(opt)
|
||||
}
|
||||
|
||||
// 布局
|
||||
doLayout(callback) {
|
||||
let task = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
}
|
||||
]
|
||||
asyncRun(task)
|
||||
}
|
||||
|
||||
// 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
// 创建节点
|
||||
let newNode = this.createNode(node, parent, isRoot, layerIndex)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
} else {
|
||||
// 非根节点
|
||||
// 三级及以下节点以上级方向为准
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
|
||||
}
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
newNode.top = parent._node.top - newNode.height
|
||||
}
|
||||
}
|
||||
if (!node.data.expand) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
// 遍历节点树计算节点的left、top
|
||||
computedLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex, index) => {
|
||||
if (node.isRoot) {
|
||||
let totalLeft = node.left + node.width
|
||||
node.children.forEach(item => {
|
||||
item.left = totalLeft
|
||||
totalLeft += item.width
|
||||
})
|
||||
}
|
||||
if (layerIndex >= 1 && node.children) {
|
||||
// 遍历三级及以下节点的子节点
|
||||
let startLeft = node.left + node.width * 0.5
|
||||
let totalTop =
|
||||
node.top +
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
node.children.forEach(item => {
|
||||
item.left = startLeft
|
||||
item.top += totalTop
|
||||
totalTop +=
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 调整节点left、top
|
||||
adjustLeftTopValue() {
|
||||
walk(
|
||||
this.root,
|
||||
null,
|
||||
(node, parent, isRoot, layerIndex) => {
|
||||
if (!node.getData('expand')) {
|
||||
return
|
||||
}
|
||||
// 调整top
|
||||
let len = node.children.length
|
||||
// 调整三级及以下节点的top
|
||||
if (parent && !parent.isRoot && len > 0) {
|
||||
let totalHeight = node.children.reduce((h, item) => {
|
||||
return (
|
||||
h +
|
||||
item.height +
|
||||
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
|
||||
)
|
||||
}, 0)
|
||||
this.updateBrothersTop(node, totalHeight)
|
||||
}
|
||||
},
|
||||
(node, parent) => {
|
||||
// 将二级节点的子节点移到上方
|
||||
if (parent && parent.isRoot) {
|
||||
// 遍历二级节点的子节点
|
||||
let totalHeight = 0
|
||||
node.children.forEach(item => {
|
||||
// 调整top
|
||||
let nodeTotalHeight = this.getNodeAreaHeight(item)
|
||||
let _top = item.top
|
||||
item.top =
|
||||
node.top - (item.top - node.top) - nodeTotalHeight + node.height
|
||||
// 调整left
|
||||
let offsetLeft =
|
||||
(nodeTotalHeight + totalHeight) /
|
||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
item.left += offsetLeft
|
||||
totalHeight += nodeTotalHeight
|
||||
// 同步更新后代节点
|
||||
this.updateChildrenPro(item.children, {
|
||||
top: item.top - _top,
|
||||
left: offsetLeft
|
||||
})
|
||||
})
|
||||
}
|
||||
// 调整二级节点的子节点的left值
|
||||
if (node.isRoot) {
|
||||
let totalLeft = 0
|
||||
node.children.forEach(item => {
|
||||
item.left += totalLeft
|
||||
this.updateChildren(item.children, 'left', totalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
totalLeft += right - left
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 递归计算节点的宽度
|
||||
getNodeAreaHeight(node) {
|
||||
let totalHeight = 0
|
||||
let loop = node => {
|
||||
totalHeight +=
|
||||
node.height +
|
||||
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
|
||||
if (node.children.length) {
|
||||
node.children.forEach(item => {
|
||||
loop(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
loop(node)
|
||||
return totalHeight
|
||||
}
|
||||
|
||||
// 调整兄弟节点的left
|
||||
updateBrothersLeft(node) {
|
||||
let childrenList = node.children
|
||||
let totalAddWidth = 0
|
||||
childrenList.forEach(item => {
|
||||
item.left += totalAddWidth
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'left', totalAddWidth)
|
||||
}
|
||||
// let areaWidth = this.getNodeAreaWidth(item)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
let areaWidth = right - left
|
||||
let difference = areaWidth - item.width
|
||||
if (difference > 0) {
|
||||
totalAddWidth += difference
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 调整兄弟节点的top
|
||||
updateBrothersTop(node, addHeight) {
|
||||
if (node.parent && !node.parent.isRoot) {
|
||||
let childrenList = node.parent.children
|
||||
let index = getNodeIndexInNodeList(node, childrenList)
|
||||
childrenList.forEach((item, _index) => {
|
||||
if (item.hasCustomPosition()) {
|
||||
// 适配自定义位置
|
||||
return
|
||||
}
|
||||
let _offset = 0
|
||||
// 下面的节点往下移
|
||||
if (_index > index) {
|
||||
_offset = addHeight
|
||||
}
|
||||
item.top += _offset
|
||||
// 同步更新子节点的位置
|
||||
if (item.children && item.children.length) {
|
||||
this.updateChildren(item.children, 'top', _offset)
|
||||
}
|
||||
})
|
||||
// 更新父节点的位置
|
||||
this.updateBrothersTop(node.parent, addHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
let len = node.children.length
|
||||
if (node.isRoot) {
|
||||
// 当前节点是根节点
|
||||
let prevBother = node
|
||||
// 根节点的子节点是和根节点同一水平线排列
|
||||
node.children.forEach((item, index) => {
|
||||
let x1 = prevBother.left + prevBother.width
|
||||
let x2 = item.left
|
||||
let y = node.top + node.height / 2
|
||||
let path = `M ${x1},${y} L ${x2},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
prevBother = item
|
||||
})
|
||||
} else {
|
||||
// 当前节点为非根节点
|
||||
let maxy = -Infinity
|
||||
let miny = Infinity
|
||||
let maxx = -Infinity
|
||||
let x = node.left + node.width * 0.3
|
||||
node.children.forEach((item, index) => {
|
||||
if (item.left > maxx) {
|
||||
maxx = item.left
|
||||
}
|
||||
let y = item.top + item.height / 2
|
||||
if (y > maxy) {
|
||||
maxy = y
|
||||
}
|
||||
if (y < miny) {
|
||||
miny = y
|
||||
}
|
||||
// 水平线
|
||||
if (node.layerIndex > 1) {
|
||||
let path = `M ${x},${y} L ${item.left},${y}`
|
||||
lines[index].plot(path)
|
||||
style && style(lines[index], item)
|
||||
}
|
||||
})
|
||||
// 竖线
|
||||
if (len > 0) {
|
||||
let line = this.lineDraw.path()
|
||||
expandBtnSize = len > 0 ? expandBtnSize : 0
|
||||
let lineLength = maxx - node.left - node.width * 0.3
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.isRoot &&
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
|
||||
) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
top -
|
||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
line.plot(
|
||||
`M ${x},${top} L ${x + lineLength},${
|
||||
top -
|
||||
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
|
||||
}`
|
||||
)
|
||||
} else {
|
||||
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
|
||||
}
|
||||
}
|
||||
node.style.line(line)
|
||||
node._lines.push(line)
|
||||
style && style(line, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染按钮
|
||||
renderExpandBtn(node, btn) {
|
||||
let { width, height, expandBtnSize, isRoot } = node
|
||||
if (!isRoot) {
|
||||
let { translateX, translateY } = btn.transform()
|
||||
if (node.parent && node.parent.isRoot) {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
-expandBtnSize / 2 - translateY
|
||||
)
|
||||
} else {
|
||||
btn.translate(
|
||||
width * 0.3 - expandBtnSize / 2 - translateX,
|
||||
height + expandBtnSize / 2 - translateY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建概要节点
|
||||
renderGeneralization(node, gLine, gNode) {
|
||||
let {
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
generalizationLineMargin,
|
||||
generalizationNodeMargin
|
||||
} = this.getNodeBoundaries(node, 'h')
|
||||
let x1 = right + generalizationLineMargin
|
||||
let y1 = top
|
||||
let x2 = right + generalizationLineMargin
|
||||
let y2 = bottom
|
||||
let cx = x1 + 20
|
||||
let cy = y1 + (y2 - y1) / 2
|
||||
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
|
||||
gLine.plot(path)
|
||||
gNode.left = right + generalizationNodeMargin
|
||||
gNode.top = top + (bottom - top - gNode.height) / 2
|
||||
}
|
||||
}
|
||||
|
||||
export default Fishbone
|
||||
@ -35,14 +35,7 @@ class MindMap extends Base {
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(cur, parent, isRoot, layerIndex, index, ancestors) => {
|
||||
let newNode = this.createNode(
|
||||
cur,
|
||||
parent,
|
||||
isRoot,
|
||||
layerIndex,
|
||||
index,
|
||||
ancestors
|
||||
)
|
||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex, index, ancestors)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
@ -54,10 +47,9 @@ class MindMap extends Base {
|
||||
} else {
|
||||
// 节点生长方向
|
||||
newNode.dir =
|
||||
newNode.getData('dir') ||
|
||||
(index % 2 === 0
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT)
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
}
|
||||
// 根据生长方向定位到父节点的左侧或右侧
|
||||
newNode.left =
|
||||
|
||||
@ -34,14 +34,7 @@ class OrganizationStructure extends Base {
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(cur, parent, isRoot, layerIndex, index, ancestors) => {
|
||||
let newNode = this.createNode(
|
||||
cur,
|
||||
parent,
|
||||
isRoot,
|
||||
layerIndex,
|
||||
index,
|
||||
ancestors
|
||||
)
|
||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex, index, ancestors)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
@ -155,56 +148,13 @@ class OrganizationStructure extends Base {
|
||||
|
||||
// 绘制连线,连接该节点到其子节点
|
||||
renderLine(node, lines, style, lineStyle) {
|
||||
if (lineStyle === 'curve') {
|
||||
this.renderLineCurve(node, lines, style)
|
||||
} else if (lineStyle === 'direct') {
|
||||
if (lineStyle === 'direct') {
|
||||
this.renderLineDirect(node, lines, style)
|
||||
} else {
|
||||
this.renderLineStraight(node, lines, style)
|
||||
}
|
||||
}
|
||||
|
||||
// 曲线风格连线
|
||||
renderLineCurve(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
return []
|
||||
}
|
||||
let { left, top, width, height, expandBtnSize } = node
|
||||
const { alwaysShowExpandBtn, notShowExpandBtn } = this.mindMap.opt
|
||||
if (!alwaysShowExpandBtn || notShowExpandBtn) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
const {
|
||||
nodeUseLineStyle,
|
||||
rootLineStartPositionKeepSameInCurve,
|
||||
rootLineKeepSameInCurve
|
||||
} = this.mindMap.themeConfig
|
||||
node.children.forEach((item, index) => {
|
||||
if (node.layerIndex === 0) {
|
||||
expandBtnSize = 0
|
||||
}
|
||||
let x1 = left + width / 2
|
||||
let y1 =
|
||||
node.layerIndex === 0 && !rootLineStartPositionKeepSameInCurve
|
||||
? top + height / 2
|
||||
: top + height + expandBtnSize
|
||||
let x2 = item.left + item.width / 2
|
||||
let y2 = item.top
|
||||
let path = ''
|
||||
// 节点使用横线风格,需要额外渲染横线
|
||||
let nodeUseLineStylePath = nodeUseLineStyle
|
||||
? ` L ${item.left},${y2} L ${item.left + item.width},${y2}`
|
||||
: ''
|
||||
if (node.isRoot && !rootLineKeepSameInCurve) {
|
||||
path =
|
||||
this.quadraticCurvePath(x1, y1, x2, y2, true) + nodeUseLineStylePath
|
||||
} else {
|
||||
path = this.cubicBezierPath(x1, y1, x2, y2, true) + nodeUseLineStylePath
|
||||
}
|
||||
this.setLineStyle(style, lines[index], path, item)
|
||||
})
|
||||
}
|
||||
|
||||
// 直连风格
|
||||
renderLineDirect(node, lines, style) {
|
||||
if (node.children.length <= 0) {
|
||||
|
||||
@ -35,14 +35,7 @@ class VerticalTimeline extends Base {
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(cur, parent, isRoot, layerIndex, index, ancestors) => {
|
||||
let newNode = this.createNode(
|
||||
cur,
|
||||
parent,
|
||||
isRoot,
|
||||
layerIndex,
|
||||
index,
|
||||
ancestors
|
||||
)
|
||||
let newNode = this.createNode(cur, parent, isRoot, layerIndex, index, ancestors)
|
||||
// 根节点定位在画布中心位置
|
||||
if (isRoot) {
|
||||
this.setNodeCenter(newNode)
|
||||
@ -53,16 +46,10 @@ class VerticalTimeline extends Base {
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
if (this.layout === CONSTANTS.LAYOUT.VERTICAL_TIMELINE2) {
|
||||
newNode.dir = CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
} else if (this.layout === CONSTANTS.LAYOUT.VERTICAL_TIMELINE3) {
|
||||
newNode.dir = CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
} else {
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
}
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
}
|
||||
// 定位二级节点的left
|
||||
if (parent._node.isRoot) {
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
|
||||
const getNodeText = node => {
|
||||
if (node.type === 'list') return ''
|
||||
let textStr = ''
|
||||
|
||||
;(node.children || []).forEach(item => {
|
||||
if (['inlineCode', 'text'].includes(item.type)) {
|
||||
textStr += item.value || ''
|
||||
} else {
|
||||
textStr += getNodeText(item)
|
||||
}
|
||||
// 优先找出其中的text类型的子节点
|
||||
let textChild = (node.children || []).find(item => {
|
||||
return item.type === 'text'
|
||||
})
|
||||
|
||||
return textStr
|
||||
// 没有找到,那么直接使用第一个子节点
|
||||
textChild = textChild || node.children[0]
|
||||
if (textChild) {
|
||||
if (textChild.value !== undefined) {
|
||||
return textChild.value
|
||||
}
|
||||
return getNodeText(textChild)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 处理list的情况
|
||||
|
||||
@ -253,9 +253,7 @@ const transformToXmind = async (data, name) => {
|
||||
}
|
||||
// 标签
|
||||
if (node.data.tag !== undefined) {
|
||||
newData.labels = (node.data.tag || []).map(item => {
|
||||
return typeof item === 'object' && item !== null ? item.text : item
|
||||
})
|
||||
newData.labels = node.data.tag || []
|
||||
}
|
||||
// 图片
|
||||
handleNodeImageToXmind(node, newNode, waitLoadImageList, imageList)
|
||||
|
||||
@ -11,27 +11,11 @@ import {
|
||||
import associativeLineControlsMethods from './associativeLine/associativeLineControls'
|
||||
import associativeLineTextMethods from './associativeLine/associativeLineText'
|
||||
|
||||
const styleProps = [
|
||||
'associativeLineWidth',
|
||||
'associativeLineColor',
|
||||
'associativeLineActiveWidth',
|
||||
'associativeLineActiveColor',
|
||||
'associativeLineDasharray',
|
||||
'associativeLineTextColor',
|
||||
'associativeLineTextFontSize',
|
||||
'associativeLineTextLineHeight',
|
||||
'associativeLineTextFontFamily'
|
||||
]
|
||||
|
||||
const ASSOCIATIVE_LINE_TEXT_EDIT_WRAP = 'associative-line-text-edit-warp'
|
||||
|
||||
// 关联线插件
|
||||
class AssociativeLine {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
this.associativeLineDraw = this.mindMap.associativeLineDraw
|
||||
// 本次不要重新渲染连线
|
||||
this.isNotRenderAllLines = false
|
||||
// 当前所有连接线
|
||||
this.lineList = []
|
||||
// 当前激活的连接线
|
||||
@ -43,6 +27,9 @@ class AssociativeLine {
|
||||
this.overlapNode = null // 创建过程中的目标节点
|
||||
// 是否有节点正在被拖拽
|
||||
this.isNodeDragging = false
|
||||
// 箭头图标
|
||||
this.markerPath = null
|
||||
this.marker = this.createMarker()
|
||||
// 控制点
|
||||
this.controlLine1 = null
|
||||
this.controlLine2 = null
|
||||
@ -64,11 +51,9 @@ class AssociativeLine {
|
||||
this[item] = associativeLineControlsMethods[item].bind(this)
|
||||
})
|
||||
// 关联线文字相关方法
|
||||
this.showTextEdit = false
|
||||
Object.keys(associativeLineTextMethods).forEach(item => {
|
||||
this[item] = associativeLineTextMethods[item].bind(this)
|
||||
})
|
||||
this.mindMap.addEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@ -127,25 +112,6 @@ class AssociativeLine {
|
||||
this.mindMap.off('beforeDestroy', this.onBeforeDestroy)
|
||||
}
|
||||
|
||||
// 获取关联线的样式配置
|
||||
// 优先级:关联线自定义样式、节点自定义样式、主题的节点层级样式、主题的最外层样式
|
||||
getStyleConfig(node, toNode) {
|
||||
let lineStyle = {}
|
||||
if (toNode) {
|
||||
const associativeLineStyle = node.getData('associativeLineStyle') || {}
|
||||
lineStyle = associativeLineStyle[toNode.getData('uid')] || {}
|
||||
}
|
||||
const res = {}
|
||||
styleProps.forEach(prop => {
|
||||
if (typeof lineStyle[prop] !== 'undefined') {
|
||||
res[prop] = lineStyle[prop]
|
||||
} else {
|
||||
res[prop] = node.getStyle(prop)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 实例销毁时清除关联线文字编辑框
|
||||
onBeforeDestroy() {
|
||||
this.hideEditTextBox()
|
||||
@ -161,7 +127,6 @@ class AssociativeLine {
|
||||
// 取消激活关联线
|
||||
if (!this.isControlPointMousedown) {
|
||||
this.clearActiveLine()
|
||||
this.renderAllLines()
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,17 +136,16 @@ class AssociativeLine {
|
||||
this.completeCreateLine(node)
|
||||
} else {
|
||||
this.clearActiveLine()
|
||||
this.renderAllLines()
|
||||
}
|
||||
}
|
||||
|
||||
// 创建箭头
|
||||
createMarker(callback = () => {}) {
|
||||
createMarker() {
|
||||
return this.associativeLineDraw.marker(20, 20, add => {
|
||||
add.ref(12, 5)
|
||||
add.size(10, 10)
|
||||
add.attr('orient', 'auto-start-reverse')
|
||||
callback(add.path('M0,0 L2,5 L0,10 L10,5 Z'))
|
||||
this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
|
||||
})
|
||||
}
|
||||
|
||||
@ -208,10 +172,6 @@ class AssociativeLine {
|
||||
|
||||
// 渲染所有连线
|
||||
renderAllLines() {
|
||||
if (this.isNotRenderAllLines) {
|
||||
this.isNotRenderAllLines = false
|
||||
return
|
||||
}
|
||||
// 先移除
|
||||
this.removeAllLines()
|
||||
this.removeControls()
|
||||
@ -263,14 +223,11 @@ class AssociativeLine {
|
||||
associativeLineWidth,
|
||||
associativeLineColor,
|
||||
associativeLineActiveWidth,
|
||||
associativeLineActiveColor,
|
||||
associativeLineDasharray
|
||||
} = this.getStyleConfig(node, toNode)
|
||||
} = this.mindMap.themeConfig
|
||||
// 箭头
|
||||
let markerPath = null
|
||||
const marker = this.createMarker(p => {
|
||||
markerPath = p
|
||||
})
|
||||
markerPath
|
||||
this.markerPath
|
||||
.stroke({ color: associativeLineColor })
|
||||
.fill({ color: associativeLineColor })
|
||||
// 路径
|
||||
@ -286,11 +243,11 @@ class AssociativeLine {
|
||||
.stroke({
|
||||
width: associativeLineWidth,
|
||||
color: associativeLineColor,
|
||||
dasharray: associativeLineDasharray || '6,4'
|
||||
dasharray: associativeLineDasharray || [6, 4]
|
||||
})
|
||||
.fill({ color: 'none' })
|
||||
path.plot(pathStr)
|
||||
path.marker('end', marker)
|
||||
path.marker('end', this.marker)
|
||||
// 不可见的点击线
|
||||
let clickPath = this.associativeLineDraw.path()
|
||||
clickPath
|
||||
@ -301,7 +258,6 @@ class AssociativeLine {
|
||||
let text = this.createText({
|
||||
path,
|
||||
clickPath,
|
||||
markerPath,
|
||||
node,
|
||||
toNode,
|
||||
startPoint,
|
||||
@ -314,7 +270,6 @@ class AssociativeLine {
|
||||
this.setActiveLine({
|
||||
path,
|
||||
clickPath,
|
||||
markerPath,
|
||||
text,
|
||||
node,
|
||||
toNode,
|
||||
@ -329,73 +284,14 @@ class AssociativeLine {
|
||||
this.showEditTextBox(text)
|
||||
})
|
||||
// 渲染关联线文字
|
||||
this.renderText(this.getText(node, toNode), path, text, node, toNode)
|
||||
this.renderText(this.getText(node, toNode), path, text)
|
||||
this.lineList.push([path, clickPath, text, node, toNode])
|
||||
}
|
||||
|
||||
// 更新当前激活连线的样式,一般在自定义了节点关联线的样式后调用
|
||||
// 直接调用node.setStyle方法更新样式会直接触发关联线更新,但是关联线的激活状态会丢失
|
||||
// 所以可以调用node.setData方法更新数据,然后再调用该方法更新样式,这样关联线激活状态不会丢失
|
||||
updateActiveLineStyle() {
|
||||
if (!this.activeLine) return
|
||||
this.isNotRenderAllLines = true
|
||||
const [path, clickPath, text, node, toNode, markerPath] = this.activeLine
|
||||
const {
|
||||
associativeLineWidth,
|
||||
associativeLineColor,
|
||||
associativeLineDasharray,
|
||||
associativeLineActiveWidth,
|
||||
associativeLineActiveColor,
|
||||
associativeLineTextColor,
|
||||
associativeLineTextFontFamily,
|
||||
associativeLineTextFontSize
|
||||
} = this.getStyleConfig(node, toNode)
|
||||
path
|
||||
.stroke({
|
||||
width: associativeLineWidth,
|
||||
color: associativeLineColor,
|
||||
dasharray: associativeLineDasharray || '6,4'
|
||||
})
|
||||
.fill({ color: 'none' })
|
||||
clickPath
|
||||
.stroke({
|
||||
width: associativeLineActiveWidth,
|
||||
color: associativeLineActiveColor
|
||||
})
|
||||
.fill({ color: 'none' })
|
||||
markerPath
|
||||
.stroke({ color: associativeLineColor })
|
||||
.fill({ color: associativeLineColor })
|
||||
text.find('text').forEach(textNode => {
|
||||
textNode
|
||||
.fill({
|
||||
color: associativeLineTextColor
|
||||
})
|
||||
.css({
|
||||
'font-family': associativeLineTextFontFamily,
|
||||
'font-size': associativeLineTextFontSize + 'px'
|
||||
})
|
||||
})
|
||||
if (this.controlLine1) {
|
||||
this.controlLine1.stroke({ color: associativeLineActiveColor })
|
||||
}
|
||||
if (this.controlLine2) {
|
||||
this.controlLine2.stroke({ color: associativeLineActiveColor })
|
||||
}
|
||||
if (this.controlPoint1) {
|
||||
this.controlPoint1.stroke({ color: associativeLineActiveColor })
|
||||
}
|
||||
if (this.controlPoint2) {
|
||||
this.controlPoint2.stroke({ color: associativeLineActiveColor })
|
||||
}
|
||||
this.updateTextPos(path, text)
|
||||
}
|
||||
|
||||
// 激活某根关联线
|
||||
setActiveLine({
|
||||
path,
|
||||
clickPath,
|
||||
markerPath,
|
||||
text,
|
||||
node,
|
||||
toNode,
|
||||
@ -403,33 +299,25 @@ class AssociativeLine {
|
||||
endPoint,
|
||||
controlPoints
|
||||
}) {
|
||||
let { associativeLineActiveColor } = this.getStyleConfig(node, toNode)
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
// 如果当前存在激活节点,那么取消激活节点
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
// 否则清除当前的关联线的激活状态,如果有的话
|
||||
this.clearActiveLine()
|
||||
// 保存当前激活的关联线信息
|
||||
this.activeLine = [path, clickPath, text, node, toNode, markerPath]
|
||||
this.activeLine = [path, clickPath, text, node, toNode]
|
||||
// 让不可见的点击线显示
|
||||
clickPath.stroke({ color: associativeLineActiveColor })
|
||||
// 如果没有输入过关联线文字,那么显示默认文字
|
||||
if (!this.getText(node, toNode)) {
|
||||
this.renderText(
|
||||
this.mindMap.opt.defaultAssociativeLineText,
|
||||
path,
|
||||
text,
|
||||
node,
|
||||
toNode
|
||||
)
|
||||
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text)
|
||||
}
|
||||
// 渲染控制点和连线
|
||||
this.renderControls(
|
||||
startPoint,
|
||||
endPoint,
|
||||
controlPoints[0],
|
||||
controlPoints[1],
|
||||
node,
|
||||
toNode
|
||||
controlPoints[1]
|
||||
)
|
||||
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
|
||||
this.front()
|
||||
@ -458,7 +346,7 @@ class AssociativeLine {
|
||||
associativeLineWidth,
|
||||
associativeLineColor,
|
||||
associativeLineDasharray
|
||||
} = this.getStyleConfig(fromNode)
|
||||
} = this.mindMap.themeConfig
|
||||
if (this.isCreatingLine || !fromNode) return
|
||||
this.front()
|
||||
this.isCreatingLine = true
|
||||
@ -468,18 +356,14 @@ class AssociativeLine {
|
||||
.stroke({
|
||||
width: associativeLineWidth,
|
||||
color: associativeLineColor,
|
||||
dasharray: associativeLineDasharray || '6,4'
|
||||
dasharray: associativeLineDasharray || [6, 4]
|
||||
})
|
||||
.fill({ color: 'none' })
|
||||
// 箭头
|
||||
let markerPath = null
|
||||
const marker = this.createMarker(p => {
|
||||
markerPath = p
|
||||
})
|
||||
markerPath
|
||||
this.markerPath
|
||||
.stroke({ color: associativeLineColor })
|
||||
.fill({ color: associativeLineColor })
|
||||
this.creatingLine.marker('end', marker)
|
||||
this.creatingLine.marker('end', this.marker)
|
||||
}
|
||||
|
||||
// 取消创建关联线
|
||||
@ -562,12 +446,6 @@ class AssociativeLine {
|
||||
// 完成创建连接线
|
||||
completeCreateLine(node) {
|
||||
if (this.creatingStartNode.uid === node.uid) return
|
||||
const { beforeAssociativeLineConnection } = this.mindMap.opt
|
||||
let stop = false
|
||||
if (typeof beforeAssociativeLineConnection === 'function') {
|
||||
stop = beforeAssociativeLineConnection(node)
|
||||
}
|
||||
if (stop) return
|
||||
this.addLine(this.creatingStartNode, node)
|
||||
if (this.overlapNode && this.overlapNode.getData('isActive')) {
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, false)
|
||||
@ -645,8 +523,7 @@ class AssociativeLine {
|
||||
associativeLineTargets,
|
||||
associativeLinePoint,
|
||||
associativeLineTargetControlOffsets,
|
||||
associativeLineText,
|
||||
associativeLineStyle
|
||||
associativeLineText
|
||||
} = node.getData()
|
||||
associativeLinePoint = associativeLinePoint || []
|
||||
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
|
||||
@ -659,15 +536,6 @@ class AssociativeLine {
|
||||
}
|
||||
})
|
||||
}
|
||||
// 更新关联线样式数据
|
||||
let newAssociativeLineStyle = {}
|
||||
if (associativeLineStyle) {
|
||||
Object.keys(associativeLineStyle).forEach(item => {
|
||||
if (item !== toNode.getData('uid')) {
|
||||
newAssociativeLineStyle[item] = associativeLineStyle[item]
|
||||
}
|
||||
})
|
||||
}
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
// 目标
|
||||
associativeLineTargets: associativeLineTargets.filter((_, index) => {
|
||||
@ -684,9 +552,7 @@ class AssociativeLine {
|
||||
})
|
||||
: [],
|
||||
// 文本
|
||||
associativeLineText: newAssociativeLineText,
|
||||
// 样式
|
||||
associativeLineStyle: newAssociativeLineStyle
|
||||
associativeLineText: newAssociativeLineText
|
||||
})
|
||||
}
|
||||
|
||||
@ -706,7 +572,6 @@ class AssociativeLine {
|
||||
this.activeLine = null
|
||||
this.removeControls()
|
||||
this.back()
|
||||
this.mindMap.emit('associative_line_deactivate')
|
||||
}
|
||||
}
|
||||
|
||||
@ -749,13 +614,11 @@ class AssociativeLine {
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.mindMap.deleteEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
||||
this.unBindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.mindMap.deleteEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,18 +43,6 @@ class Demonstrate {
|
||||
this.mindMap.opt.demonstrateConfig || {}
|
||||
)
|
||||
this.needRestorePerformanceMode = false
|
||||
this.onConfigUpdate = this.onConfigUpdate.bind(this)
|
||||
this.mindMap.on('after_update_config', this.onConfigUpdate)
|
||||
}
|
||||
|
||||
// 监听配置更新
|
||||
onConfigUpdate(opt) {
|
||||
if (typeof opt.demonstrateConfig !== 'undefined') {
|
||||
this.config = {
|
||||
...this.config,
|
||||
...opt.demonstrateConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 进入演示模式
|
||||
@ -429,13 +417,11 @@ class Demonstrate {
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.unBindEvent()
|
||||
this.mindMap.off('after_update_config', this.onConfigUpdate)
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.unBindEvent()
|
||||
this.mindMap.off('after_update_config', this.onConfigUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -93,6 +93,7 @@ class Drag extends Base {
|
||||
) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
this.isMousedown = true
|
||||
// 记录鼠标按下时的节点
|
||||
this.mousedownNode = node
|
||||
@ -407,12 +408,7 @@ class Drag extends Base {
|
||||
TIMELINE,
|
||||
TIMELINE2,
|
||||
VERTICAL_TIMELINE,
|
||||
VERTICAL_TIMELINE2,
|
||||
VERTICAL_TIMELINE3,
|
||||
FISHBONE,
|
||||
FISHBONE2,
|
||||
RIGHT_FISHBONE,
|
||||
RIGHT_FISHBONE2
|
||||
FISHBONE
|
||||
} = CONSTANTS.LAYOUT
|
||||
this.overlapNode = null
|
||||
this.prevNode = null
|
||||
@ -448,14 +444,9 @@ class Drag extends Base {
|
||||
this.handleTimeLine2(node)
|
||||
break
|
||||
case VERTICAL_TIMELINE:
|
||||
case VERTICAL_TIMELINE2:
|
||||
case VERTICAL_TIMELINE3:
|
||||
this.handleLogicalStructure(node)
|
||||
break
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
this.handleFishbone(node)
|
||||
break
|
||||
default:
|
||||
@ -479,12 +470,7 @@ class Drag extends Base {
|
||||
TIMELINE,
|
||||
TIMELINE2,
|
||||
VERTICAL_TIMELINE,
|
||||
VERTICAL_TIMELINE2,
|
||||
VERTICAL_TIMELINE3,
|
||||
FISHBONE,
|
||||
FISHBONE2,
|
||||
RIGHT_FISHBONE,
|
||||
RIGHT_FISHBONE2
|
||||
FISHBONE
|
||||
} = CONSTANTS.LAYOUT
|
||||
const { LEFT, TOP, RIGHT, BOTTOM } = CONSTANTS.LAYOUT_GROW_DIR
|
||||
const layerIndex = this.overlapNode.layerIndex
|
||||
@ -578,8 +564,6 @@ class Drag extends Base {
|
||||
}
|
||||
break
|
||||
case VERTICAL_TIMELINE:
|
||||
case VERTICAL_TIMELINE2:
|
||||
case VERTICAL_TIMELINE3:
|
||||
if (layerIndex === 0) {
|
||||
x =
|
||||
lastNodeRect.originLeft +
|
||||
@ -597,9 +581,6 @@ class Drag extends Base {
|
||||
}
|
||||
break
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
if (layerIndex <= 1) {
|
||||
notRenderPlaceholder = true
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
|
||||
@ -675,8 +656,6 @@ class Drag extends Base {
|
||||
}
|
||||
break
|
||||
case VERTICAL_TIMELINE:
|
||||
case VERTICAL_TIMELINE2:
|
||||
case VERTICAL_TIMELINE3:
|
||||
if (layerIndex === 0) {
|
||||
rotate = true
|
||||
}
|
||||
@ -690,9 +669,6 @@ class Drag extends Base {
|
||||
halfPlaceholderHeight
|
||||
break
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
if (layerIndex <= 1) {
|
||||
notRenderPlaceholder = true
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
|
||||
@ -728,12 +704,7 @@ class Drag extends Base {
|
||||
MIND_MAP,
|
||||
TIMELINE2,
|
||||
VERTICAL_TIMELINE,
|
||||
VERTICAL_TIMELINE2,
|
||||
VERTICAL_TIMELINE3,
|
||||
FISHBONE,
|
||||
FISHBONE2,
|
||||
RIGHT_FISHBONE,
|
||||
RIGHT_FISHBONE2
|
||||
FISHBONE
|
||||
} = CONSTANTS.LAYOUT
|
||||
switch (this.mindMap.opt.layout) {
|
||||
case LOGICAL_STRUCTURE:
|
||||
@ -743,12 +714,7 @@ class Drag extends Base {
|
||||
case MIND_MAP:
|
||||
case TIMELINE2:
|
||||
case VERTICAL_TIMELINE:
|
||||
case VERTICAL_TIMELINE2:
|
||||
case VERTICAL_TIMELINE3:
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
return node.dir
|
||||
default:
|
||||
return ''
|
||||
@ -760,22 +726,17 @@ class Drag extends Base {
|
||||
handleVerticalCheck(node, checkList, isReverse = false) {
|
||||
const { layout } = this.mindMap.opt
|
||||
const { LAYOUT, LAYOUT_GROW_DIR } = CONSTANTS
|
||||
const {
|
||||
VERTICAL_TIMELINE,
|
||||
VERTICAL_TIMELINE2,
|
||||
VERTICAL_TIMELINE3,
|
||||
FISHBONE,
|
||||
FISHBONE2,
|
||||
RIGHT_FISHBONE,
|
||||
RIGHT_FISHBONE2
|
||||
} = LAYOUT
|
||||
const { LEFT } = LAYOUT_GROW_DIR
|
||||
const { VERTICAL_TIMELINE, FISHBONE } = LAYOUT
|
||||
const { BOTTOM, LEFT } = LAYOUT_GROW_DIR
|
||||
const mouseMoveX = this.mouseMoveX
|
||||
const mouseMoveY = this.mouseMoveY
|
||||
const nodeRect = this.getNodeRect(node)
|
||||
const dir = this.getNewChildNodeDir(node)
|
||||
const layerIndex = node.layerIndex
|
||||
if (isReverse) {
|
||||
if (
|
||||
isReverse ||
|
||||
(layout === FISHBONE && dir === BOTTOM && layerIndex >= 3)
|
||||
) {
|
||||
checkList = checkList.reverse()
|
||||
}
|
||||
let oneFourthHeight = nodeRect.originHeight / 4
|
||||
@ -810,8 +771,6 @@ class Drag extends Base {
|
||||
let notRenderLine = false
|
||||
switch (layout) {
|
||||
case VERTICAL_TIMELINE:
|
||||
case VERTICAL_TIMELINE2:
|
||||
case VERTICAL_TIMELINE3:
|
||||
if (layerIndex === 1) {
|
||||
x =
|
||||
nodeRect.originLeft +
|
||||
@ -819,11 +778,6 @@ class Drag extends Base {
|
||||
this.placeholderWidth / 2
|
||||
}
|
||||
break
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
x =
|
||||
nodeRect.originLeft + nodeRect.originWidth - this.placeholderWidth
|
||||
break
|
||||
default:
|
||||
}
|
||||
if (checkIsPrevNode) {
|
||||
@ -838,9 +792,6 @@ class Drag extends Base {
|
||||
this.placeholderHeight / 2
|
||||
switch (layout) {
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
if (layerIndex === 2) {
|
||||
notRenderLine = true
|
||||
y =
|
||||
@ -870,9 +821,6 @@ class Drag extends Base {
|
||||
this.placeholderHeight / 2
|
||||
switch (layout) {
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
if (layerIndex === 2) {
|
||||
notRenderLine = true
|
||||
y =
|
||||
@ -909,14 +857,7 @@ class Drag extends Base {
|
||||
handleHorizontalCheck(node, checkList) {
|
||||
const { layout } = this.mindMap.opt
|
||||
const { LAYOUT } = CONSTANTS
|
||||
const {
|
||||
FISHBONE,
|
||||
FISHBONE2,
|
||||
RIGHT_FISHBONE,
|
||||
RIGHT_FISHBONE2,
|
||||
TIMELINE,
|
||||
TIMELINE2
|
||||
} = LAYOUT
|
||||
const { FISHBONE, TIMELINE, TIMELINE2 } = LAYOUT
|
||||
let mouseMoveX = this.mouseMoveX
|
||||
let mouseMoveY = this.mouseMoveY
|
||||
let nodeRect = this.getNodeRect(node)
|
||||
@ -956,9 +897,6 @@ class Drag extends Base {
|
||||
this.placeholderWidth / 2
|
||||
break
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
if (layerIndex === 1) {
|
||||
notRenderLine = true
|
||||
y =
|
||||
@ -970,11 +908,7 @@ class Drag extends Base {
|
||||
default:
|
||||
}
|
||||
if (checkIsPrevNode) {
|
||||
if ([RIGHT_FISHBONE, RIGHT_FISHBONE2].includes(layout)) {
|
||||
this.nextNode = node
|
||||
} else {
|
||||
this.prevNode = node
|
||||
}
|
||||
this.prevNode = node
|
||||
this.setPlaceholderRect({
|
||||
x:
|
||||
nodeRect.originRight +
|
||||
@ -985,11 +919,7 @@ class Drag extends Base {
|
||||
notRenderLine
|
||||
})
|
||||
} else if (checkIsNextNode) {
|
||||
if ([RIGHT_FISHBONE, RIGHT_FISHBONE2].includes(layout)) {
|
||||
this.prevNode = node
|
||||
} else {
|
||||
this.nextNode = node
|
||||
}
|
||||
this.nextNode = node
|
||||
this.setPlaceholderRect({
|
||||
x:
|
||||
nodeRect.originLeft -
|
||||
@ -1213,11 +1143,7 @@ class Drag extends Base {
|
||||
this.handleHorizontalCheck(node, checkList)
|
||||
} else {
|
||||
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
|
||||
const is2LayerTop =
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP && node.layerIndex === 2
|
||||
const is2MoreLayerBottom =
|
||||
node.dir === CONSTANTS.LAYOUT_GROW_DIR.BOTTOM && node.layerIndex >= 3
|
||||
if (is2LayerTop || is2MoreLayerBottom) {
|
||||
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP && node.layerIndex === 2) {
|
||||
this.handleVerticalCheck(node, checkList, true)
|
||||
} else {
|
||||
this.handleVerticalCheck(node, checkList)
|
||||
|
||||
@ -128,14 +128,7 @@ class Export {
|
||||
}
|
||||
|
||||
// svg转png
|
||||
svgToPng(
|
||||
svgSrc,
|
||||
transparent,
|
||||
clipData = null,
|
||||
fitBg = false,
|
||||
format = 'image/png'
|
||||
) {
|
||||
const { maxCanvasSize, minExportImgCanvasScale } = this.mindMap.opt
|
||||
svgToPng(svgSrc, transparent, clipData = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
|
||||
@ -143,8 +136,10 @@ class Export {
|
||||
img.onload = async () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
const dpr = Math.max(window.devicePixelRatio, minExportImgCanvasScale)
|
||||
// 图片原始大小
|
||||
const dpr = Math.max(
|
||||
window.devicePixelRatio,
|
||||
this.mindMap.opt.minExportImgCanvasScale
|
||||
)
|
||||
let imgWidth = img.width
|
||||
let imgHeight = img.height
|
||||
// 如果是裁减操作的话,那么需要手动添加内边距,及调整图片大小为实际的裁减区域的大小,不要忘了内边距哦
|
||||
@ -156,80 +151,33 @@ class Export {
|
||||
imgWidth = clipData.width + paddingX * 2
|
||||
imgHeight = clipData.height + paddingY * 2
|
||||
}
|
||||
// 适配背景图片的大小
|
||||
let fitBgImgWidth = 0
|
||||
let fitBgImgHeight = 0
|
||||
const { backgroundImage } = this.mindMap.themeConfig
|
||||
if (fitBg && backgroundImage && !transparent) {
|
||||
const bgImgSize = await new Promise(resolve => {
|
||||
const bgImg = new Image()
|
||||
bgImg.onload = () => {
|
||||
resolve([bgImg.width, bgImg.height])
|
||||
}
|
||||
bgImg.onerror = () => {
|
||||
resolve(null)
|
||||
}
|
||||
bgImg.src = backgroundImage
|
||||
})
|
||||
if (bgImgSize) {
|
||||
const imgRatio = imgWidth / imgHeight
|
||||
const bgRatio = bgImgSize[0] / bgImgSize[1]
|
||||
if (imgRatio > bgRatio) {
|
||||
fitBgImgWidth = imgWidth
|
||||
fitBgImgHeight = imgWidth / bgRatio
|
||||
} else {
|
||||
fitBgImgHeight = imgHeight
|
||||
fitBgImgWidth = imgHeight * bgRatio
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查是否超出canvas支持的像素上限
|
||||
// canvas大小需要乘以dpr
|
||||
let scaleX = 1
|
||||
let scaleY = 1
|
||||
let canvasWidth = (fitBgImgWidth || imgWidth) * dpr
|
||||
let canvasHeight = (fitBgImgHeight || imgHeight) * dpr
|
||||
if (canvasWidth > maxCanvasSize || canvasHeight > maxCanvasSize) {
|
||||
const maxSize = 16384 / dpr
|
||||
const maxArea = maxSize * maxSize
|
||||
if (imgWidth * imgHeight > maxArea) {
|
||||
let newWidth = null
|
||||
let newHeight = null
|
||||
if (canvasWidth > maxCanvasSize) {
|
||||
// 如果宽度超出限制,那么调整为上限值
|
||||
newWidth = maxCanvasSize
|
||||
} else if (canvasHeight > maxCanvasSize) {
|
||||
// 高度同理
|
||||
newHeight = maxCanvasSize
|
||||
if (imgWidth > maxSize) {
|
||||
newWidth = maxArea / imgHeight
|
||||
} else if (imgHeight > maxSize) {
|
||||
newHeight = maxArea / imgWidth
|
||||
}
|
||||
// 计算缩放后的宽高
|
||||
const res = resizeImgSize(
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
newWidth,
|
||||
newHeight
|
||||
)
|
||||
scaleX = res[0] / canvasWidth
|
||||
scaleY = res[1] / canvasHeight
|
||||
canvasWidth = res[0]
|
||||
canvasHeight = res[1]
|
||||
const res = resizeImgSize(imgWidth, imgHeight, newWidth, newHeight)
|
||||
imgWidth = res[0]
|
||||
imgHeight = res[1]
|
||||
}
|
||||
canvas.width = canvasWidth
|
||||
canvas.height = canvasHeight
|
||||
const styleWidth = canvasWidth / dpr
|
||||
const styleHeight = canvasHeight / dpr
|
||||
// canvas元素实际上的大小
|
||||
canvas.style.width = styleWidth + 'px'
|
||||
canvas.style.height = styleHeight + 'px'
|
||||
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 (!transparent) {
|
||||
await this.drawBackgroundToCanvas(ctx, styleWidth, styleHeight)
|
||||
await this.drawBackgroundToCanvas(ctx, imgWidth, imgHeight)
|
||||
}
|
||||
// 图片绘制到canvas里
|
||||
// 如果有裁减数据,那么需要进行裁减
|
||||
const fitBgLeft =
|
||||
(fitBgImgWidth > 0 ? (fitBgImgWidth - imgWidth) / 2 : 0) * scaleX
|
||||
const fitBgTop =
|
||||
(fitBgImgHeight > 0 ? (fitBgImgHeight - imgHeight) / 2 : 0) * scaleY
|
||||
if (clipData) {
|
||||
ctx.drawImage(
|
||||
img,
|
||||
@ -237,21 +185,15 @@ class Export {
|
||||
clipData.top,
|
||||
clipData.width,
|
||||
clipData.height,
|
||||
paddingX * scaleX + fitBgLeft,
|
||||
paddingY * scaleY + fitBgTop,
|
||||
clipData.width * scaleX,
|
||||
clipData.height * scaleY
|
||||
paddingX,
|
||||
paddingY,
|
||||
clipData.width,
|
||||
clipData.height
|
||||
)
|
||||
} else {
|
||||
ctx.drawImage(
|
||||
img,
|
||||
fitBgLeft,
|
||||
fitBgTop,
|
||||
imgWidth * scaleX,
|
||||
imgHeight * scaleY
|
||||
)
|
||||
ctx.drawImage(img, 0, 0, imgWidth, imgHeight)
|
||||
}
|
||||
resolve(canvas.toDataURL(format))
|
||||
resolve(canvas.toDataURL())
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
@ -329,35 +271,16 @@ class Export {
|
||||
})
|
||||
}
|
||||
|
||||
// 导出为指定格式的图片
|
||||
async _image(format, name, transparent = false, node = null, fitBg = false) {
|
||||
this.mindMap.renderer.textEdit.hideEditTextBox()
|
||||
this.handleNodeExport(node)
|
||||
const { str, clipData } = await this.getSvgData(node)
|
||||
const svgUrl = await this.fixSvgStrAndToBlob(str)
|
||||
const res = await this.svgToPng(
|
||||
svgUrl,
|
||||
transparent,
|
||||
clipData,
|
||||
fitBg,
|
||||
format
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
// 导出为png
|
||||
/**
|
||||
* 方法1.把svg的图片都转化成data:url格式,再转换
|
||||
* 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换
|
||||
*/
|
||||
async png(...args) {
|
||||
const res = await this._image('image/png', ...args)
|
||||
return res
|
||||
}
|
||||
|
||||
// 导出为jpg
|
||||
async jpg(...args) {
|
||||
const res = await this._image('image/jpg', ...args)
|
||||
async png(name, transparent = false, node = null) {
|
||||
this.handleNodeExport(node)
|
||||
const { str, clipData } = await this.getSvgData(node)
|
||||
const svgUrl = await this.fixSvgStrAndToBlob(str)
|
||||
const res = await this.svgToPng(svgUrl, transparent, clipData)
|
||||
return res
|
||||
}
|
||||
|
||||
@ -373,11 +296,11 @@ class Export {
|
||||
}
|
||||
|
||||
// 导出为pdf
|
||||
async pdf(name, transparent = false, fitBg = false) {
|
||||
async pdf(name, transparent = false) {
|
||||
if (!this.mindMap.doExportPDF) {
|
||||
throw new Error('请注册ExportPDF插件')
|
||||
}
|
||||
const img = await this.png(name, transparent, null, fitBg)
|
||||
const img = await this.png(name, transparent)
|
||||
// 使用jspdf库
|
||||
// await this.mindMap.doExportPDF.pdf(name, img)
|
||||
// 使用pdf-lib库
|
||||
@ -398,7 +321,6 @@ class Export {
|
||||
|
||||
// 导出为svg
|
||||
async svg(name) {
|
||||
this.mindMap.renderer.textEdit.hideEditTextBox()
|
||||
const { node } = await this.getSvgData()
|
||||
node.first().before(SVG(`<title>${name}</title>`))
|
||||
await this.drawBackgroundToSvg(node)
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import katex from 'katex'
|
||||
import Quill from 'quill'
|
||||
import { getChromeVersion, htmlEscape } from '../utils/index'
|
||||
import { getChromeVersion } from '../utils/index'
|
||||
import { getBaseStyleText, getFontStyleText } from './FormulaStyle'
|
||||
|
||||
let extended = false
|
||||
const QuillFormula = Quill.import('formats/formula')
|
||||
|
||||
// 数学公式支持插件
|
||||
// 该插件在富文本模式下可用
|
||||
class Formula {
|
||||
@ -19,18 +16,6 @@ class Formula {
|
||||
this.cssEl = null
|
||||
this.addStyle()
|
||||
this.extendQuill()
|
||||
this.onDestroy = this.onDestroy.bind(this)
|
||||
this.mindMap.on('beforeDestroy', this.onDestroy)
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
const instanceCount = Object.getPrototypeOf(this.mindMap).constructor
|
||||
.instanceCount
|
||||
// 如果思维导图实例数量变成0了,那么就恢复成默认的
|
||||
if (instanceCount <= 1) {
|
||||
extended = false
|
||||
Quill.register('formats/formula', QuillFormula, true)
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
@ -65,9 +50,7 @@ class Formula {
|
||||
|
||||
// 修改formula格式工具
|
||||
extendQuill() {
|
||||
if (extended) return
|
||||
extended = true
|
||||
|
||||
const QuillFormula = Quill.import('formats/formula')
|
||||
const self = this
|
||||
|
||||
class CustomFormulaBlot extends QuillFormula {
|
||||
@ -75,7 +58,7 @@ class Formula {
|
||||
let node = super.create(value)
|
||||
if (typeof value === 'string') {
|
||||
katex.render(value, node, self.config)
|
||||
node.setAttribute('data-value', htmlEscape(value))
|
||||
node.setAttribute('data-value', value)
|
||||
}
|
||||
return node
|
||||
}
|
||||
@ -107,13 +90,14 @@ class Formula {
|
||||
|
||||
// 给指定的节点插入指定公式
|
||||
insertFormulaToNode(node, formula) {
|
||||
const richTextPlugin = this.mindMap.richText
|
||||
let richTextPlugin = this.mindMap.richText
|
||||
richTextPlugin.showEditText({ node })
|
||||
richTextPlugin.quill.insertEmbed(
|
||||
richTextPlugin.quill.getLength() - 1,
|
||||
'formula',
|
||||
formula
|
||||
)
|
||||
richTextPlugin.setTextStyleIfNotRichText(richTextPlugin.node)
|
||||
richTextPlugin.hideEditText([node])
|
||||
}
|
||||
|
||||
@ -126,18 +110,12 @@ class Formula {
|
||||
for (const el of els)
|
||||
nodeText = nodeText.replace(
|
||||
el.outerHTML,
|
||||
`$${el.getAttribute('data-value')}$`
|
||||
`\$${el
|
||||
.getAttribute('data-value')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')}\$`
|
||||
)
|
||||
// 如果开启了实时渲染,那么意味公式转换为源码时会影响节点尺寸,需要派发事件触发渲染
|
||||
if (this.mindMap.opt.openRealtimeRenderOnNodeTextEdit) {
|
||||
setTimeout(() => {
|
||||
this.mindMap.emit('node_text_edit_change', {
|
||||
node: this.mindMap.richText.node,
|
||||
text: this.mindMap.richText.getEditText(),
|
||||
richText: true
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
return nodeText
|
||||
}
|
||||
@ -194,13 +172,11 @@ class Formula {
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.removeStyle()
|
||||
this.mindMap.off('beforeDestroy', this.onDestroy)
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.removeStyle()
|
||||
this.mindMap.off('beforeDestroy', this.onDestroy)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
|
||||
// 该插件会向节点数据的data中添加dir字段
|
||||
/*
|
||||
需要更新数据的情况:
|
||||
|
||||
1.实例化时的数据
|
||||
2.调用setData和updateData方法
|
||||
3.执行完命令
|
||||
4.切换结构
|
||||
*/
|
||||
|
||||
class MindMapLayoutPro {
|
||||
constructor(opt) {
|
||||
this.opt = opt
|
||||
this.mindMap = opt.mindMap
|
||||
this.init()
|
||||
}
|
||||
|
||||
init() {
|
||||
this.updateNodeTree = this.updateNodeTree.bind(this)
|
||||
this.afterExecCommand = this.afterExecCommand.bind(this)
|
||||
this.layoutChange = this.layoutChange.bind(this)
|
||||
|
||||
// 处理实例化时传入的数据
|
||||
if (this.mindMap.opt.data && this.isMindMapLayout()) {
|
||||
this.updateNodeTree(this.mindMap.opt.data)
|
||||
}
|
||||
|
||||
this.mindMap.on('layout_change', this.layoutChange)
|
||||
this.mindMap.on('afterExecCommand', this.afterExecCommand)
|
||||
this.mindMap.on('before_update_data', this.updateNodeTree)
|
||||
this.mindMap.on('before_set_data', this.updateNodeTree)
|
||||
}
|
||||
|
||||
restore() {
|
||||
this.mindMap.off('layout_change', this.layoutChange)
|
||||
this.mindMap.off('afterExecCommand', this.afterExecCommand)
|
||||
this.mindMap.off('before_update_data', this.updateNodeTree)
|
||||
this.mindMap.off('before_set_data', this.updateNodeTree)
|
||||
}
|
||||
|
||||
// 监听命令执行后的事件
|
||||
afterExecCommand(name) {
|
||||
if (!this.isMindMapLayout()) return
|
||||
if (
|
||||
![
|
||||
'BACK',
|
||||
'FORWARD',
|
||||
'INSERT_NODE',
|
||||
'INSERT_MULTI_NODE',
|
||||
'INSERT_CHILD_NODE',
|
||||
'INSERT_MULTI_CHILD_NODE',
|
||||
'INSERT_PARENT_NODE',
|
||||
'UP_NODE',
|
||||
'DOWN_NODE',
|
||||
'MOVE_UP_ONE_LEVEL',
|
||||
'INSERT_AFTER',
|
||||
'INSERT_BEFORE',
|
||||
'MOVE_NODE_TO',
|
||||
'REMOVE_NODE',
|
||||
'REMOVE_CURRENT_NODE',
|
||||
'PASTE_NODE',
|
||||
'CUT_NODE'
|
||||
].includes(name)
|
||||
)
|
||||
return
|
||||
this.updateRenderTree()
|
||||
}
|
||||
|
||||
// 更新布局结构
|
||||
layoutChange(layout) {
|
||||
if (layout === CONSTANTS.LAYOUT.MIND_MAP) {
|
||||
this.updateRenderTree()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前的渲染树
|
||||
updateRenderTree() {
|
||||
this.updateNodeTree(this.mindMap.renderer.renderTree)
|
||||
}
|
||||
|
||||
// 更新节点树,修改二级节点的排列位置
|
||||
updateNodeTree(tree) {
|
||||
if (!this.isMindMapLayout()) return
|
||||
const root = tree
|
||||
const childrenLength = root.children.length
|
||||
if (childrenLength <= 0) return
|
||||
const center = Math.ceil(childrenLength / 2)
|
||||
root.children.forEach((item, index) => {
|
||||
if (index + 1 <= center) {
|
||||
item.data.dir = CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
} else {
|
||||
item.data.dir = CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 判断当前是否是思维导图布局结构
|
||||
isMindMapLayout() {
|
||||
return this.mindMap.opt.layout === CONSTANTS.LAYOUT.MIND_MAP
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.restore()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.restore()
|
||||
}
|
||||
}
|
||||
|
||||
MindMapLayoutPro.instanceName = 'mindMapLayoutPro'
|
||||
|
||||
export default MindMapLayoutPro
|
||||
@ -1,7 +1,8 @@
|
||||
import {
|
||||
isWhite,
|
||||
isTransparent,
|
||||
getVisibleColorFromTheme
|
||||
getVisibleColorFromTheme,
|
||||
readBlob
|
||||
} from '../utils/index'
|
||||
|
||||
// 小地图插件
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
import { walk, createUid } from '../utils/index'
|
||||
|
||||
// 修改base64格式的节点图片在数据中的存储方式
|
||||
// 将base64格式的图片以key-map的形式存储在根节点的imgMap字段里,其他节点只保存key,避免不同的节点引用相同的图片重复存储的问题,普通url格式的图片不处理
|
||||
class NodeBase64ImageStorage {
|
||||
constructor(opt) {
|
||||
this.opt = opt
|
||||
this.mindMap = opt.mindMap
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
bindEvent() {
|
||||
this.onBeforeAddHistory = this.onBeforeAddHistory.bind(this)
|
||||
this.mindMap.on('beforeAddHistory', this.onBeforeAddHistory)
|
||||
}
|
||||
|
||||
unBindEvent() {
|
||||
this.mindMap.off('beforeAddHistory', this.onBeforeAddHistory)
|
||||
}
|
||||
|
||||
isBase64ImgUrl(url) {
|
||||
return /^data:/.test(url)
|
||||
}
|
||||
|
||||
isImageKey(url) {
|
||||
return /^smm_img_key_/.test(url)
|
||||
}
|
||||
|
||||
createImageKey() {
|
||||
return 'smm_img_key_' + createUid()
|
||||
}
|
||||
|
||||
onBeforeAddHistory() {
|
||||
const renderTree = this.mindMap.renderer.renderTree
|
||||
if (!renderTree) return
|
||||
let imgMap = renderTree.data.imgMap
|
||||
if (!imgMap) {
|
||||
imgMap = renderTree.data.imgMap = {}
|
||||
}
|
||||
const useIds = []
|
||||
|
||||
const getImgIds = () => {
|
||||
return Object.keys(imgMap)
|
||||
}
|
||||
|
||||
const getImgId = image => {
|
||||
return getImgIds().find(id => {
|
||||
return imgMap[id] === image
|
||||
})
|
||||
}
|
||||
|
||||
walk(renderTree, null, node => {
|
||||
const image = node.data.image
|
||||
if (image) {
|
||||
// 如果是base64图片url
|
||||
if (this.isBase64ImgUrl(image)) {
|
||||
// 检查该图片是否已存在
|
||||
const hasId = getImgId(image)
|
||||
if (hasId) {
|
||||
// 已存在则直接使用现有的key
|
||||
useIds.push(hasId)
|
||||
node.data.image = hasId
|
||||
} else {
|
||||
// 不存在则生成key,并存储
|
||||
const newId = this.createImageKey()
|
||||
node.data.image = newId
|
||||
imgMap[newId] = image
|
||||
useIds.push(newId)
|
||||
}
|
||||
} else if (this.isImageKey(image)) {
|
||||
// 如果是key,那么收集一下
|
||||
if (getImgIds().includes(image)) {
|
||||
useIds.push(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 删除已无节点引用的图片
|
||||
getImgIds().forEach(id => {
|
||||
if (!useIds.includes(id)) {
|
||||
delete imgMap[id]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
NodeBase64ImageStorage.instanceName = 'nodeBase64ImageStorage'
|
||||
|
||||
export default NodeBase64ImageStorage
|
||||
@ -6,18 +6,13 @@ 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.mousedownDrawTransform = null //鼠标按下时对当前画布的变换
|
||||
this.mousedownOffset = {
|
||||
// 鼠标按下时位置和图片右下角相差的距离
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
this.currentImgWidth = 0 // 当前拖拽实时图片的大小
|
||||
this.currentImgHeight = 0
|
||||
this.isAdjusted = false // 是否是拖拽结束后的渲染期间
|
||||
@ -31,14 +26,12 @@ class NodeImgAdjust {
|
||||
this.onMousemove = this.onMousemove.bind(this)
|
||||
this.onMouseup = this.onMouseup.bind(this)
|
||||
this.onRenderEnd = this.onRenderEnd.bind(this)
|
||||
this.onScale = this.onScale.bind(this)
|
||||
this.mindMap.on('node_img_mouseleave', this.onNodeImgMouseleave)
|
||||
this.mindMap.on('node_img_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)
|
||||
this.mindMap.on('scale', this.onScale)
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
@ -49,15 +42,6 @@ class NodeImgAdjust {
|
||||
this.mindMap.off('mouseup', this.onMouseup)
|
||||
this.mindMap.off('node_mouseup', this.onMouseup)
|
||||
this.mindMap.off('node_tree_render_end', this.onRenderEnd)
|
||||
this.mindMap.off('scale', this.onScale)
|
||||
}
|
||||
|
||||
// 如果当前操作按钮正在显示时缩放了画布,那么需要更新位置
|
||||
onScale() {
|
||||
if (this.node && this.img && this.isShowHandleEl) {
|
||||
this.rect = this.img.rbox()
|
||||
this.setHandleElRect()
|
||||
}
|
||||
}
|
||||
|
||||
// 节点图片鼠标移动事件
|
||||
@ -94,7 +78,6 @@ class NodeImgAdjust {
|
||||
|
||||
// 显示自定义元素
|
||||
showHandleEl() {
|
||||
if (this.isShowHandleEl) return
|
||||
if (!this.handleEl) {
|
||||
this.createResizeBtnEl()
|
||||
}
|
||||
@ -133,11 +116,6 @@ class NodeImgAdjust {
|
||||
|
||||
// 创建调整按钮元素
|
||||
createResizeBtnEl() {
|
||||
const {
|
||||
imgResizeBtnSize,
|
||||
customResizeBtnInnerHTML,
|
||||
customDeleteBtnInnerHTML
|
||||
} = this.mindMap.opt
|
||||
// 容器元素
|
||||
this.handleEl = document.createElement('div')
|
||||
this.handleEl.style.cssText = `
|
||||
@ -149,15 +127,15 @@ class NodeImgAdjust {
|
||||
this.handleEl.className = 'node-img-handle'
|
||||
// 调整按钮元素
|
||||
const btnEl = document.createElement('div')
|
||||
btnEl.innerHTML = customResizeBtnInnerHTML || btnsSvg.imgAdjust
|
||||
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: ${imgResizeBtnSize}px;
|
||||
height: ${imgResizeBtnSize}px;
|
||||
width: ${this.resizeBtnSize}px;
|
||||
height: ${this.resizeBtnSize}px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -194,14 +172,14 @@ class NodeImgAdjust {
|
||||
const btnRemove = document.createElement('div')
|
||||
this.handleEl.prepend(btnRemove)
|
||||
btnRemove.className = 'node-image-remove'
|
||||
btnRemove.innerHTML = customDeleteBtnInnerHTML || btnsSvg.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: ${imgResizeBtnSize}px;
|
||||
height: ${imgResizeBtnSize}px;
|
||||
width: ${this.resizeBtnSize}px;
|
||||
height: ${this.resizeBtnSize}px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -214,15 +192,8 @@ class NodeImgAdjust {
|
||||
if (this.isMousedown) return
|
||||
this.hideHandleEl()
|
||||
})
|
||||
btnRemove.addEventListener('click', async e => {
|
||||
let stop = false
|
||||
if (typeof this.mindMap.opt.beforeDeleteNodeImg === 'function') {
|
||||
stop = await this.mindMap.opt.beforeDeleteNodeImg(this.node)
|
||||
}
|
||||
if (!stop) {
|
||||
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null })
|
||||
this.mindMap.emit('delete_node_img_from_delete_btn', this.node)
|
||||
}
|
||||
btnRemove.addEventListener('click', e => {
|
||||
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null })
|
||||
})
|
||||
// 添加元素到页面
|
||||
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
@ -230,14 +201,10 @@ class NodeImgAdjust {
|
||||
}
|
||||
|
||||
// 鼠标按钮按下事件
|
||||
onMousedown(e) {
|
||||
this.mindMap.emit('node_img_adjust_btn_mousedown', this.node)
|
||||
onMousedown() {
|
||||
this.isMousedown = true
|
||||
this.mousedownDrawTransform = this.mindMap.draw.transform()
|
||||
// 隐藏节点实际图片
|
||||
this.hideNodeImage()
|
||||
this.mousedownOffset.x = e.clientX - this.rect.x2
|
||||
this.mousedownOffset.y = e.clientY - this.rect.y2
|
||||
// 将节点图片渲染到自定义元素上
|
||||
this.handleEl.style.backgroundImage = `url(${this.node.getData('image')})`
|
||||
}
|
||||
@ -246,48 +213,13 @@ class NodeImgAdjust {
|
||||
onMousemove(e) {
|
||||
if (!this.isMousedown) return
|
||||
e.preventDefault()
|
||||
const { scaleX, scaleY } = this.mousedownDrawTransform
|
||||
// 图片原始大小
|
||||
const { width: imageOriginWidth, height: imageOriginHeight } =
|
||||
this.node.getData('imageSize')
|
||||
let {
|
||||
minImgResizeWidth,
|
||||
minImgResizeHeight,
|
||||
maxImgResizeWidthInheritTheme,
|
||||
maxImgResizeWidth,
|
||||
maxImgResizeHeight
|
||||
} = this.mindMap.opt
|
||||
// 主题设置的最小图片宽高
|
||||
const minRatio = minImgResizeWidth / minImgResizeHeight
|
||||
const oRatio = imageOriginWidth / imageOriginHeight
|
||||
if (minRatio > oRatio) {
|
||||
// 如果最小值比例大于图片原始比例,那么要调整高度最小值
|
||||
minImgResizeHeight = minImgResizeWidth / oRatio
|
||||
} else {
|
||||
// 否则调整宽度最小值
|
||||
minImgResizeWidth = minImgResizeHeight * oRatio
|
||||
}
|
||||
// 主题设置的最大图片宽高
|
||||
let imgMaxWidth, imgMaxHeight
|
||||
if (maxImgResizeWidthInheritTheme) {
|
||||
imgMaxWidth = this.mindMap.getThemeConfig('imgMaxWidth')
|
||||
imgMaxHeight = this.mindMap.getThemeConfig('imgMaxHeight')
|
||||
} else {
|
||||
imgMaxWidth = maxImgResizeWidth
|
||||
imgMaxHeight = maxImgResizeHeight
|
||||
}
|
||||
imgMaxWidth = imgMaxWidth * scaleX
|
||||
imgMaxHeight = imgMaxHeight * scaleY
|
||||
// 计算当前拖拽位置对应的图片的实时大小
|
||||
let newWidth = Math.abs(e.clientX - this.rect.x - this.mousedownOffset.x)
|
||||
let newHeight = Math.abs(e.clientY - this.rect.y - this.mousedownOffset.y)
|
||||
// 限制最小值
|
||||
if (newWidth < minImgResizeWidth) newWidth = minImgResizeWidth
|
||||
if (newHeight < minImgResizeHeight) newHeight = minImgResizeHeight
|
||||
// 限制最大值
|
||||
if (newWidth > imgMaxWidth) newWidth = imgMaxWidth
|
||||
if (newHeight > imgMaxHeight) newHeight = imgMaxHeight
|
||||
const [actWidth, actHeight] = resizeImgSizeByOriginRatio(
|
||||
let { width: imageOriginWidth, height: imageOriginHeight } =
|
||||
this.node.getData('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,
|
||||
@ -306,27 +238,17 @@ class NodeImgAdjust {
|
||||
// 隐藏自定义元素
|
||||
this.hideHandleEl()
|
||||
// 更新节点图片为新的大小
|
||||
const { image, imageTitle } = this.node.getData()
|
||||
const { scaleX, scaleY } = this.mousedownDrawTransform
|
||||
const newWidth = this.currentImgWidth / scaleX
|
||||
const newHeight = this.currentImgHeight / scaleY
|
||||
if (
|
||||
Math.abs(newWidth - this.rect.width) > 1 ||
|
||||
Math.abs(newHeight - this.rect.height) > 1
|
||||
) {
|
||||
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, {
|
||||
url: image,
|
||||
title: imageTitle,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
custom: true // 代表自定义了图片大小
|
||||
})
|
||||
this.isAdjusted = true
|
||||
}
|
||||
let { image, imageTitle } = this.node.getData()
|
||||
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
|
||||
this.mousedownDrawTransform = null
|
||||
this.mousedownOffset.x = 0
|
||||
this.mousedownOffset.y = 0
|
||||
}
|
||||
|
||||
// 渲染完成事件
|
||||
|
||||
@ -1,68 +1,149 @@
|
||||
import {
|
||||
formatDataToArray,
|
||||
walk,
|
||||
getTopAncestorsFomNodeList,
|
||||
getNodeListBoundingRect,
|
||||
createUid
|
||||
} from '../utils'
|
||||
import {
|
||||
parseAddNodeList,
|
||||
getNodeOuterFrameList
|
||||
} from './outerFrame/outerFrameUtils'
|
||||
import outerFrameTextMethods from './outerFrame/outerFrameText'
|
||||
|
||||
// 解析要添加外框的节点实例列表
|
||||
const parseAddNodeList = list => {
|
||||
// 找出顶层节点
|
||||
list = getTopAncestorsFomNodeList(list)
|
||||
const cache = {}
|
||||
const uidToParent = {}
|
||||
// 找出列表中节点在兄弟节点中的索引,并和父节点关联起来
|
||||
list.forEach(node => {
|
||||
const parent = node.parent
|
||||
if (parent) {
|
||||
const pUid = parent.uid
|
||||
uidToParent[pUid] = parent
|
||||
const index = node.getIndexInBrothers()
|
||||
const data = {
|
||||
node,
|
||||
index
|
||||
}
|
||||
if (cache[pUid]) {
|
||||
if (
|
||||
!cache[pUid].find(item => {
|
||||
return item.index === data.index
|
||||
})
|
||||
) {
|
||||
cache[pUid].push(data)
|
||||
}
|
||||
} else {
|
||||
cache[pUid] = [data]
|
||||
}
|
||||
}
|
||||
})
|
||||
const res = []
|
||||
Object.keys(cache).forEach(uid => {
|
||||
const indexList = cache[uid]
|
||||
const parentNode = uidToParent[uid]
|
||||
if (indexList.length > 1) {
|
||||
// 多个节点
|
||||
const rangeList = indexList
|
||||
.map(item => {
|
||||
return item.index
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a - b
|
||||
})
|
||||
const minIndex = rangeList[0]
|
||||
const maxIndex = rangeList[rangeList.length - 1]
|
||||
let curStart = -1
|
||||
let curEnd = -1
|
||||
for (let i = minIndex; i <= maxIndex; i++) {
|
||||
// 连续索引
|
||||
if (rangeList.includes(i)) {
|
||||
if (curStart === -1) {
|
||||
curStart = i
|
||||
}
|
||||
curEnd = i
|
||||
} else {
|
||||
// 连续断开
|
||||
if (curStart !== -1 && curEnd !== -1) {
|
||||
res.push({
|
||||
node: parentNode,
|
||||
range: [curStart, curEnd]
|
||||
})
|
||||
}
|
||||
curStart = -1
|
||||
curEnd = -1
|
||||
}
|
||||
}
|
||||
// 不要忘了最后一段索引
|
||||
if (curStart !== -1 && curEnd !== -1) {
|
||||
res.push({
|
||||
node: parentNode,
|
||||
range: [curStart, curEnd]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 单个节点
|
||||
res.push({
|
||||
node: parentNode,
|
||||
range: [indexList[0].index, indexList[0].index]
|
||||
})
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 解析获取节点的子节点生成的外框列表
|
||||
const getNodeOuterFrameList = node => {
|
||||
const children = node.children
|
||||
if (!children || children.length <= 0) return
|
||||
const res = []
|
||||
const map = {}
|
||||
children.forEach((item, index) => {
|
||||
const outerFrameData = item.getData('outerFrame')
|
||||
if (!outerFrameData) return
|
||||
const groupId = outerFrameData.groupId
|
||||
if (groupId) {
|
||||
if (!map[groupId]) {
|
||||
map[groupId] = []
|
||||
}
|
||||
map[groupId].push({
|
||||
node: item,
|
||||
index
|
||||
})
|
||||
} else {
|
||||
res.push({
|
||||
nodeList: [item],
|
||||
range: [index, index]
|
||||
})
|
||||
}
|
||||
})
|
||||
Object.keys(map).forEach(id => {
|
||||
const list = map[id]
|
||||
res.push({
|
||||
nodeList: list.map(item => {
|
||||
return item.node
|
||||
}),
|
||||
range: [list[0].index, list[list.length - 1].index]
|
||||
})
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 默认外框样式
|
||||
const defaultStyle = {
|
||||
// 外框圆角大小
|
||||
radius: 5,
|
||||
// 外框边框宽度
|
||||
strokeWidth: 2,
|
||||
// 外框边框颜色
|
||||
strokeColor: '#0984e3',
|
||||
// 外框边框虚线样式
|
||||
strokeDasharray: '5,5',
|
||||
// 外框填充颜色
|
||||
fill: 'rgba(9,132,227,0.05)',
|
||||
// 外框文字字号
|
||||
fontSize: 14,
|
||||
// 外框文字字体
|
||||
fontFamily: '微软雅黑, Microsoft YaHei',
|
||||
// 加粗
|
||||
fontWeight: 'normal', // bold
|
||||
// 斜体
|
||||
fontStyle: 'normal', // italic
|
||||
// 外框文字颜色
|
||||
color: '#fff',
|
||||
// 外框文字行高
|
||||
lineHeight: 1.2,
|
||||
// 外框文字背景
|
||||
textFill: '#0984e3',
|
||||
// 外框文字圆角
|
||||
textFillRadius: 5,
|
||||
// 外框文字矩内边距,左上右下
|
||||
textFillPadding: [5, 5, 5, 5],
|
||||
// 外框文字水平显示位置,相对于外框
|
||||
textAlign: 'left' // left、center、right
|
||||
fill: 'rgba(9,132,227,0.05)'
|
||||
}
|
||||
|
||||
const OUTER_FRAME_TEXT_EDIT_WRAP = 'outer-frame-text-edit-warp'
|
||||
|
||||
// 外框插件
|
||||
class OuterFrame {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
this.draw = null
|
||||
this.createDrawContainer()
|
||||
this.isNotRenderOuterFrames = false
|
||||
this.textNodeList = []
|
||||
this.outerFrameElList = []
|
||||
this.activeOuterFrame = null
|
||||
// 文字相关方法
|
||||
this.textEditNode = null
|
||||
this.showTextEdit = false
|
||||
Object.keys(outerFrameTextMethods).forEach(item => {
|
||||
this[item] = outerFrameTextMethods[item].bind(this)
|
||||
})
|
||||
this.mindMap.addEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@ -83,11 +164,6 @@ class OuterFrame {
|
||||
this.clearActiveOuterFrame = this.clearActiveOuterFrame.bind(this)
|
||||
this.mindMap.on('draw_click', this.clearActiveOuterFrame)
|
||||
this.mindMap.on('node_click', this.clearActiveOuterFrame)
|
||||
// 缩放事件
|
||||
this.mindMap.on('scale', this.onScale)
|
||||
// 实例销毁事件
|
||||
this.onBeforeDestroy = this.onBeforeDestroy.bind(this)
|
||||
this.mindMap.on('beforeDestroy', this.onBeforeDestroy)
|
||||
|
||||
this.addOuterFrame = this.addOuterFrame.bind(this)
|
||||
this.mindMap.command.add('ADD_OUTER_FRAME', this.addOuterFrame)
|
||||
@ -105,8 +181,6 @@ class OuterFrame {
|
||||
this.mindMap.off('data_change', this.renderOuterFrames)
|
||||
this.mindMap.off('draw_click', this.clearActiveOuterFrame)
|
||||
this.mindMap.off('node_click', this.clearActiveOuterFrame)
|
||||
this.mindMap.off('scale', this.onScale)
|
||||
this.mindMap.off('beforeDestroy', this.onBeforeDestroy)
|
||||
this.mindMap.command.remove('ADD_OUTER_FRAME', this.addOuterFrame)
|
||||
this.mindMap.keyCommand.removeShortcut(
|
||||
'Del|Backspace',
|
||||
@ -114,12 +188,6 @@ class OuterFrame {
|
||||
)
|
||||
}
|
||||
|
||||
// 实例销毁时清除关联线文字编辑框
|
||||
onBeforeDestroy() {
|
||||
this.hideEditTextBox()
|
||||
this.removeTextEditEl()
|
||||
}
|
||||
|
||||
// 给节点添加外框数据
|
||||
/*
|
||||
config: {
|
||||
@ -188,47 +256,20 @@ class OuterFrame {
|
||||
this.mindMap.emit('outer_frame_delete')
|
||||
}
|
||||
|
||||
// 删除当前激活外框的文字
|
||||
removeActiveOuterFrameText() {
|
||||
this.updateActiveOuterFrame({
|
||||
text: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 更新当前激活的外框
|
||||
// 执行了该方法后请立即隐藏你的样式面板,因为会清除当前激活的外框
|
||||
updateActiveOuterFrame(config = {}) {
|
||||
if (!this.activeOuterFrame) return
|
||||
this.isNotRenderOuterFrames = true
|
||||
const { el, node, range } = this.activeOuterFrame
|
||||
let newStrokeDasharray = ''
|
||||
const { node, range } = this.activeOuterFrame
|
||||
this.getRangeNodeList(node, range).forEach(node => {
|
||||
const outerFrame = node.getData('outerFrame')
|
||||
const newData = {
|
||||
...outerFrame,
|
||||
...config
|
||||
}
|
||||
newStrokeDasharray = newData.strokeDasharray
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
outerFrame: newData
|
||||
outerFrame: {
|
||||
...outerFrame,
|
||||
...config
|
||||
}
|
||||
})
|
||||
})
|
||||
el.cacheStyle = {
|
||||
dasharray: newStrokeDasharray
|
||||
}
|
||||
this.updateOuterFrameStyle()
|
||||
}
|
||||
|
||||
// 更新当前激活外框的样式
|
||||
updateOuterFrameStyle() {
|
||||
const { el, node, range, textNode } = this.activeOuterFrame
|
||||
const firstNode = this.getNodeRangeFirstNode(node, range)
|
||||
const styleConfig = this.getStyle(firstNode)
|
||||
this.styleOuterFrame(el, {
|
||||
...styleConfig,
|
||||
strokeDasharray: 'none'
|
||||
})
|
||||
const text = this.getText(firstNode)
|
||||
this.renderText(text, el, textNode, node, range)
|
||||
}
|
||||
|
||||
// 获取某个节点指定范围的带外框的子节点列表
|
||||
@ -238,19 +279,8 @@ class OuterFrame {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取某个节点指定范围的带外框的第一个子节点
|
||||
getNodeRangeFirstNode(node, range) {
|
||||
return node.children[range[0]]
|
||||
}
|
||||
|
||||
// 渲染外框
|
||||
renderOuterFrames() {
|
||||
if (this.isNotRenderOuterFrames) {
|
||||
this.isNotRenderOuterFrames = false
|
||||
return
|
||||
}
|
||||
this.clearActiveOuterFrame()
|
||||
this.clearTextNodes()
|
||||
this.clearOuterFrameElList()
|
||||
let tree = this.mindMap.renderer.root
|
||||
if (!tree) return
|
||||
@ -287,15 +317,11 @@ class OuterFrame {
|
||||
t.scaleY,
|
||||
(width + outerFramePaddingX * 2) / t.scaleX,
|
||||
(height + outerFramePaddingY * 2) / t.scaleY,
|
||||
this.getStyle(nodeList[0]) // 使用第一个节点的外框样式
|
||||
nodeList[0].getData('outerFrame') // 使用第一个节点的外框样式
|
||||
)
|
||||
// 渲染文字,如果有的话
|
||||
const textNode = this.createText(el, cur, range)
|
||||
this.textNodeList.push(textNode)
|
||||
this.renderText(this.getText(nodeList[0]), el, textNode, cur, range)
|
||||
el.on('click', e => {
|
||||
e.stopPropagation()
|
||||
this.setActiveOuterFrame(el, cur, range, textNode)
|
||||
this.setActiveOuterFrame(el, cur, range)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -307,67 +333,37 @@ class OuterFrame {
|
||||
}
|
||||
|
||||
// 激活外框
|
||||
setActiveOuterFrame(el, node, range, textNode) {
|
||||
setActiveOuterFrame(el, node, range) {
|
||||
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
|
||||
this.clearActiveOuterFrame()
|
||||
this.activeOuterFrame = {
|
||||
el,
|
||||
node,
|
||||
range,
|
||||
textNode
|
||||
range
|
||||
}
|
||||
el.stroke({
|
||||
dasharray: 'none'
|
||||
})
|
||||
// 如果没有输入过文字,那么显示默认文字
|
||||
if (!this.getText(this.getNodeRangeFirstNode(node, range))) {
|
||||
this.renderText(
|
||||
this.mindMap.opt.defaultOuterFrameText,
|
||||
el,
|
||||
textNode,
|
||||
node,
|
||||
range
|
||||
)
|
||||
}
|
||||
this.mindMap.emit('outer_frame_active', el, node, range)
|
||||
}
|
||||
|
||||
// 清除当前激活的外框
|
||||
clearActiveOuterFrame() {
|
||||
if (!this.activeOuterFrame) return
|
||||
const { el, textNode, node, range } = this.activeOuterFrame
|
||||
const { el } = this.activeOuterFrame
|
||||
el.stroke({
|
||||
dasharray: el.cacheStyle.dasharray || defaultStyle.strokeDasharray
|
||||
})
|
||||
// 隐藏文本编辑框
|
||||
this.hideEditTextBox()
|
||||
// 如果没有输入过文字,那么隐藏
|
||||
if (!this.getText(this.getNodeRangeFirstNode(node, range))) {
|
||||
textNode.clear()
|
||||
}
|
||||
this.activeOuterFrame = null
|
||||
this.mindMap.emit('outer_frame_deactivate')
|
||||
}
|
||||
|
||||
// 获取指定外框的样式
|
||||
getStyle(node) {
|
||||
return { ...defaultStyle, ...(node.getData('outerFrame') || {}) }
|
||||
}
|
||||
|
||||
// 创建外框元素
|
||||
createOuterFrameEl(x, y, width, height, styleConfig = {}) {
|
||||
const el = this.draw.rect().size(width, height).x(x).y(y)
|
||||
this.styleOuterFrame(el, styleConfig)
|
||||
el.cacheStyle = {
|
||||
dasharray: styleConfig.strokeDasharray
|
||||
}
|
||||
this.outerFrameElList.push(el)
|
||||
return el
|
||||
}
|
||||
|
||||
// 设置外框样式
|
||||
styleOuterFrame(el, styleConfig) {
|
||||
el.radius(styleConfig.radius)
|
||||
styleConfig = { ...defaultStyle, ...styleConfig }
|
||||
const el = this.draw
|
||||
.rect()
|
||||
.size(width, height)
|
||||
.radius(styleConfig.radius)
|
||||
.stroke({
|
||||
width: styleConfig.strokeWidth,
|
||||
color: styleConfig.strokeColor,
|
||||
@ -376,13 +372,13 @@ class OuterFrame {
|
||||
.fill({
|
||||
color: styleConfig.fill
|
||||
})
|
||||
}
|
||||
|
||||
// 清除文本元素
|
||||
clearTextNodes() {
|
||||
this.textNodeList.forEach(item => {
|
||||
item.remove()
|
||||
})
|
||||
.x(x)
|
||||
.y(y)
|
||||
el.cacheStyle = {
|
||||
dasharray: styleConfig.strokeDasharray
|
||||
}
|
||||
this.outerFrameElList.push(el)
|
||||
return el
|
||||
}
|
||||
|
||||
// 清除外框元素
|
||||
@ -396,18 +392,15 @@ class OuterFrame {
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||
this.unBindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
OuterFrame.instanceName = 'outerFrame'
|
||||
OuterFrame.defaultStyle = defaultStyle
|
||||
|
||||
export default OuterFrame
|
||||
|
||||
@ -53,22 +53,17 @@ class Painter {
|
||||
node.uid === this.painterNode.uid
|
||||
)
|
||||
return
|
||||
let style = {}
|
||||
// 格式刷节点所有生效的样式
|
||||
if (!this.mindMap.opt.onlyPainterNodeCustomStyles) {
|
||||
style = {
|
||||
...this.painterNode.effectiveStyles
|
||||
}
|
||||
}
|
||||
const style = {}
|
||||
const painterNodeData = this.painterNode.getData()
|
||||
Object.keys(painterNodeData).forEach(key => {
|
||||
if (checkIsNodeStyleDataKey(key)) {
|
||||
style[key] = painterNodeData[key]
|
||||
}
|
||||
})
|
||||
// 先去除目标节点的样式
|
||||
this.mindMap.renderer._handleRemoveCustomStyles(node.getData())
|
||||
node.setStyles(style)
|
||||
if (painterNodeData.activeStyle) {
|
||||
node.setStyles(painterNodeData.activeStyle, true)
|
||||
}
|
||||
}
|
||||
|
||||
// 插件被移除前做的事情
|
||||
|
||||
@ -4,15 +4,15 @@ import 'quill/dist/quill.snow.css'
|
||||
import {
|
||||
walk,
|
||||
getTextFromHtml,
|
||||
isWhite,
|
||||
getVisibleColorFromTheme,
|
||||
isUndef,
|
||||
checkSmmFormatData,
|
||||
removeHtmlNodeByClass,
|
||||
formatGetNodeGeneralization,
|
||||
nodeRichTextToTextWithWrap,
|
||||
getNodeRichTextStyles,
|
||||
htmlEscape,
|
||||
compareVersion
|
||||
nodeRichTextToTextWithWrap
|
||||
} from '../utils'
|
||||
import { richTextSupportStyleList } from '../constants/constant'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
import MindMapNode from '../core/render/node/MindMapNode'
|
||||
import { Scope } from 'parchment'
|
||||
|
||||
@ -40,8 +40,6 @@ let fontSizeList = new Array(100).fill(0).map((_, index) => {
|
||||
return index + 'px'
|
||||
})
|
||||
|
||||
const RICH_TEXT_EDIT_WRAP = 'ql-editor'
|
||||
|
||||
// 富文本编辑插件
|
||||
class RichText {
|
||||
constructor({ mindMap, pluginOpt }) {
|
||||
@ -57,72 +55,42 @@ class RichText {
|
||||
this.isInserting = false
|
||||
this.styleEl = null
|
||||
this.cacheEditingText = ''
|
||||
this.lostStyle = false
|
||||
this.isCompositing = false
|
||||
this.textNodePaddingX = 6
|
||||
this.textNodePaddingY = 4
|
||||
this.mindMap.addEditNodeClass(RICH_TEXT_EDIT_WRAP)
|
||||
this.initOpt()
|
||||
this.extendQuill()
|
||||
this.appendCss()
|
||||
this.bindEvent()
|
||||
|
||||
this.handleDataToRichTextOnInit()
|
||||
// 处理数据,转成富文本格式
|
||||
if (this.mindMap.opt.data) {
|
||||
this.mindMap.opt.data = this.handleSetData(this.mindMap.opt.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
this.onCompositionStart = this.onCompositionStart.bind(this)
|
||||
this.onCompositionUpdate = this.onCompositionUpdate.bind(this)
|
||||
this.onCompositionEnd = this.onCompositionEnd.bind(this)
|
||||
this.handleSetData = this.handleSetData.bind(this)
|
||||
window.addEventListener('compositionstart', this.onCompositionStart)
|
||||
window.addEventListener('compositionupdate', this.onCompositionUpdate)
|
||||
window.addEventListener('compositionend', this.onCompositionEnd)
|
||||
this.mindMap.on('before_update_data', this.handleSetData)
|
||||
this.mindMap.on('before_set_data', this.handleSetData)
|
||||
}
|
||||
|
||||
// 解绑事件
|
||||
unbindEvent() {
|
||||
window.removeEventListener('compositionstart', this.onCompositionStart)
|
||||
window.removeEventListener('compositionupdate', this.onCompositionUpdate)
|
||||
window.removeEventListener('compositionend', this.onCompositionEnd)
|
||||
this.mindMap.off('before_update_data', this.handleSetData)
|
||||
this.mindMap.off('before_set_data', this.handleSetData)
|
||||
}
|
||||
|
||||
// 插入样式
|
||||
appendCss() {
|
||||
this.mindMap.appendCss(
|
||||
'richText',
|
||||
`
|
||||
.smm-richtext-node-wrap {
|
||||
word-break: break-all;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ql-editor .ql-align-left,
|
||||
.smm-richtext-node-wrap .ql-align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.smm-richtext-node-wrap .ql-align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.smm-richtext-node-wrap .ql-align-center {
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
)
|
||||
let cssText = `
|
||||
.${RICH_TEXT_EDIT_WRAP} {
|
||||
.ql-editor {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
line-height: 1.2;
|
||||
line-height: normal;
|
||||
-webkit-user-select: text;
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
.ql-container {
|
||||
@ -133,6 +101,19 @@ class RichText {
|
||||
.ql-container.ql-snow {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.smm-richtext-node-wrap {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.smm-richtext-node-wrap p {
|
||||
font-family: auto;
|
||||
|
||||
}
|
||||
|
||||
.smm-richtext-node-edit-wrap p {
|
||||
font-family: auto;
|
||||
}
|
||||
`
|
||||
this.styleEl = document.createElement('style')
|
||||
this.styleEl.type = 'text/css'
|
||||
@ -165,8 +146,6 @@ class RichText {
|
||||
|
||||
this.extendFont([])
|
||||
|
||||
this.extendAlign()
|
||||
|
||||
// 扩展quill的字号列表
|
||||
const SizeAttributor = Quill.import('attributors/class/size')
|
||||
SizeAttributor.whitelist = fontSizeList
|
||||
@ -191,30 +170,19 @@ class RichText {
|
||||
Quill.register(FontStyle, true)
|
||||
}
|
||||
|
||||
// 扩展文本对齐方式
|
||||
extendAlign() {
|
||||
const AlignFormat = Quill.import('formats/align')
|
||||
AlignFormat.whitelist = ['right', 'center', 'justify', 'left']
|
||||
Quill.register(AlignFormat, true)
|
||||
}
|
||||
|
||||
// 显示文本编辑控件
|
||||
showEditText({ node, rect, isInserting, isFromKeyDown, isFromScale }) {
|
||||
if (this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
let {
|
||||
const {
|
||||
richTextEditFakeInPlace,
|
||||
customInnerElsAppendTo,
|
||||
nodeTextEditZIndex,
|
||||
textAutoWrapWidth,
|
||||
selectTextOnEnterEditText,
|
||||
transformRichTextOnEnterEdit,
|
||||
openRealtimeRenderOnNodeTextEdit,
|
||||
autoEmptyTextWhenKeydownEnterEdit
|
||||
transformRichTextOnEnterEdit
|
||||
} = this.mindMap.opt
|
||||
textAutoWrapWidth = node.hasCustomWidth()
|
||||
? node.customTextWidth
|
||||
: textAutoWrapWidth
|
||||
this.node = node
|
||||
this.isInserting = isInserting
|
||||
if (!rect) rect = node._textData.node.node.getBoundingClientRect()
|
||||
@ -227,26 +195,27 @@ class RichText {
|
||||
let originWidth = g.attr('data-width')
|
||||
let originHeight = g.attr('data-height')
|
||||
// 缩放值
|
||||
const scaleX = Math.ceil(rect.width) / originWidth
|
||||
const scaleY = Math.ceil(rect.height) / originHeight
|
||||
let scaleX = rect.width / originWidth
|
||||
let scaleY = rect.height / originHeight
|
||||
// 内边距
|
||||
let paddingX = this.textNodePaddingX
|
||||
let paddingY = this.textNodePaddingY
|
||||
let paddingX = 6
|
||||
let paddingY = 4
|
||||
if (richTextEditFakeInPlace) {
|
||||
let paddingValue = node.getPaddingVale()
|
||||
paddingX = paddingValue.paddingX
|
||||
paddingY = paddingValue.paddingY
|
||||
}
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.classList.add('smm-richtext-node-edit-wrap')
|
||||
this.textEditNode.style.cssText = `
|
||||
position:fixed;
|
||||
box-sizing: border-box;
|
||||
${
|
||||
openRealtimeRenderOnNodeTextEdit
|
||||
? ''
|
||||
: 'box-shadow: 0 0 20px rgba(0,0,0,.5);'
|
||||
}
|
||||
outline: none;
|
||||
word-break: break-all;
|
||||
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;
|
||||
line-height: 1.2;
|
||||
`
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
@ -262,14 +231,10 @@ class RichText {
|
||||
const targetNode = customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
this.addNodeTextStyleToTextEditNode(node)
|
||||
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
|
||||
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
|
||||
this.textEditNode.style.zIndex = nodeTextEditZIndex
|
||||
if (!openRealtimeRenderOnNodeTextEdit) {
|
||||
this.textEditNode.style.background =
|
||||
this.mindMap.renderer.textEdit.getBackground(node)
|
||||
}
|
||||
this.textEditNode.style.background = this.getBackground(node)
|
||||
this.textEditNode.style.minWidth = originWidth + paddingX * 2 + 'px'
|
||||
this.textEditNode.style.minHeight = originHeight + 'px'
|
||||
this.textEditNode.style.left = rect.left + 'px'
|
||||
@ -278,6 +243,13 @@ class RichText {
|
||||
this.textEditNode.style.maxWidth = textAutoWrapWidth + paddingX * 2 + 'px'
|
||||
this.textEditNode.style.transform = `scale(${scaleX}, ${scaleY})`
|
||||
this.textEditNode.style.transformOrigin = 'left top'
|
||||
if (richTextEditFakeInPlace) {
|
||||
this.textEditNode.style.borderRadius =
|
||||
(node.style.merge('borderRadius') || 5) + 'px'
|
||||
if (node.style.merge('shape') == 'roundedRectangle') {
|
||||
this.textEditNode.style.borderRadius = (node.height || 50) + 'px'
|
||||
}
|
||||
}
|
||||
// 节点文本内容
|
||||
let nodeText = node.getData('text')
|
||||
if (typeof transformRichTextOnEnterEdit === 'function') {
|
||||
@ -287,9 +259,11 @@ class RichText {
|
||||
const isEmptyText = isUndef(nodeText)
|
||||
// 是否是非空的非富文本
|
||||
const noneEmptyNoneRichText = !node.getData('richText') && !isEmptyText
|
||||
if (isFromKeyDown && autoEmptyTextWhenKeydownEnterEdit) {
|
||||
this.textEditNode.innerHTML = ''
|
||||
} else if (noneEmptyNoneRichText) {
|
||||
// 如果是空文本,那么设置为丢失样式状态,否则输入不会带上样式
|
||||
if (isEmptyText) {
|
||||
this.lostStyle = true
|
||||
}
|
||||
if (noneEmptyNoneRichText) {
|
||||
// 还不是富文本
|
||||
let text = String(nodeText).split(/\n/gim).join('<br>')
|
||||
let html = `<p>${text}</p>`
|
||||
@ -299,60 +273,20 @@ class RichText {
|
||||
this.textEditNode.innerHTML = this.cacheEditingText || nodeText
|
||||
}
|
||||
this.initQuillEditor()
|
||||
this.setQuillContainerMinHeight(originHeight)
|
||||
this.setIsShowTextEdit(true)
|
||||
document.querySelector('.ql-editor').style.minHeight = originHeight + 'px'
|
||||
this.showTextEdit = true
|
||||
// 如果是刚创建的节点,那么默认全选,否则普通激活不全选,除非selectTextOnEnterEditText配置为true
|
||||
// 在selectTextOnEnterEditText时,如果是在keydown事件进入的节点编辑,也不需要全选
|
||||
this.focus(
|
||||
isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null
|
||||
)
|
||||
if (noneEmptyNoneRichText) {
|
||||
// 如果是非富文本的情况,需要手动应用文本样式
|
||||
this.setTextStyleIfNotRichText(node)
|
||||
}
|
||||
this.cacheEditingText = ''
|
||||
}
|
||||
|
||||
// 当openRealtimeRenderOnNodeTextEdit配置更新后需要更新编辑框样式
|
||||
onOpenRealtimeRenderOnNodeTextEditConfigUpdate(
|
||||
openRealtimeRenderOnNodeTextEdit
|
||||
) {
|
||||
if (!this.textEditNode) return
|
||||
this.textEditNode.style.background = openRealtimeRenderOnNodeTextEdit
|
||||
? 'transparent'
|
||||
: this.node
|
||||
? this.mindMap.renderer.textEdit.getBackground(this.node)
|
||||
: ''
|
||||
this.textEditNode.style.boxShadow = openRealtimeRenderOnNodeTextEdit
|
||||
? 'none'
|
||||
: '0 0 20px rgba(0,0,0,.5)'
|
||||
}
|
||||
|
||||
// 将指定节点的文本样式添加到编辑框元素上
|
||||
addNodeTextStyleToTextEditNode(node) {
|
||||
const style = getNodeRichTextStyles(node)
|
||||
Object.keys(style).forEach(prop => {
|
||||
this.textEditNode.style[prop] = style[prop]
|
||||
})
|
||||
}
|
||||
|
||||
// 设置quill编辑器容器的最小高度
|
||||
setQuillContainerMinHeight(minHeight) {
|
||||
document.querySelector('.' + RICH_TEXT_EDIT_WRAP).style.minHeight =
|
||||
minHeight + 'px'
|
||||
}
|
||||
|
||||
// 更新文本编辑框的大小和位置
|
||||
updateTextEditNode() {
|
||||
if (!this.node) return
|
||||
const g = this.node._textData.node
|
||||
const rect = g.node.getBoundingClientRect()
|
||||
const originWidth = g.attr('data-width')
|
||||
const originHeight = g.attr('data-height')
|
||||
this.textEditNode.style.minWidth =
|
||||
originWidth + this.textNodePaddingX * 2 + 'px'
|
||||
this.textEditNode.style.minHeight = originHeight + 'px'
|
||||
this.textEditNode.style.left = rect.left + 'px'
|
||||
this.textEditNode.style.top = rect.top + 'px'
|
||||
this.setQuillContainerMinHeight(originHeight)
|
||||
}
|
||||
|
||||
// 删除文本编辑框元素
|
||||
removeTextEditEl() {
|
||||
if (!this.textEditNode) return
|
||||
@ -360,18 +294,50 @@ class RichText {
|
||||
targetNode.removeChild(this.textEditNode)
|
||||
}
|
||||
|
||||
// 获取编辑区域的背景填充
|
||||
getBackground(node) {
|
||||
const gradientStyle = node.style.merge('gradientStyle')
|
||||
// 当前使用的是渐变色背景
|
||||
if (gradientStyle) {
|
||||
const startColor = node.style.merge('startColor')
|
||||
const endColor = node.style.merge('endColor')
|
||||
return `linear-gradient(to right, ${startColor}, ${endColor})`
|
||||
} else {
|
||||
// 单色背景
|
||||
const bgColor = node.style.merge('fillColor')
|
||||
const color = node.style.merge('color')
|
||||
// 默认使用节点的填充色,否则如果节点颜色是白色的话编辑时看不见
|
||||
return bgColor === 'transparent'
|
||||
? isWhite(color)
|
||||
? getVisibleColorFromTheme(this.mindMap.themeConfig)
|
||||
: '#fff'
|
||||
: bgColor
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是非富文本的情况,需要手动应用文本样式
|
||||
setTextStyleIfNotRichText(node) {
|
||||
let style = {
|
||||
font: node.style.merge('fontFamily'),
|
||||
color: node.style.merge('color'),
|
||||
italic: node.style.merge('fontStyle') === 'italic',
|
||||
bold: node.style.merge('fontWeight') === 'bold',
|
||||
size: node.style.merge('fontSize') + 'px',
|
||||
underline: node.style.merge('textDecoration') === 'underline',
|
||||
strike: node.style.merge('textDecoration') === 'line-through'
|
||||
}
|
||||
this.pureFormatAllText(style)
|
||||
}
|
||||
|
||||
// 获取当前正在编辑的内容
|
||||
getEditText() {
|
||||
// https://github.com/slab/quill/issues/4509
|
||||
return this.quill.container.firstChild.innerHTML.replace(/ +/g, match =>
|
||||
' '.repeat(match.length)
|
||||
)
|
||||
let html = this.quill.container.firstChild.innerHTML
|
||||
// 去除ql-cursor节点
|
||||
// https://github.com/wanglin2/mind-map/commit/138cc4b3e824671143f0bf70e5c46796f48520d0
|
||||
// https://github.com/wanglin2/mind-map/commit/0760500cebe8ec4e8ad84ab63f877b8b2a193aa1
|
||||
// html = removeHtmlNodeByClass(html, '.ql-cursor')
|
||||
// 去除最后的空行
|
||||
// return html.replace(/<p><br><\/p>$/, '')
|
||||
return html.replace(/<p><br><\/p>$/, '')
|
||||
}
|
||||
|
||||
// 隐藏文本编辑控件,即完成编辑
|
||||
@ -383,14 +349,9 @@ class RichText {
|
||||
if (typeof beforeHideRichTextEdit === 'function') {
|
||||
beforeHideRichTextEdit(this)
|
||||
}
|
||||
const html = this.getEditText()
|
||||
const list = nodes && nodes.length > 0 ? nodes : [this.node]
|
||||
const node = this.node
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.setIsShowTextEdit(false)
|
||||
this.mindMap.emit('rich_text_selection_change', false)
|
||||
this.node = null
|
||||
this.isInserting = false
|
||||
let html = this.getEditText()
|
||||
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) {
|
||||
@ -399,7 +360,12 @@ class RichText {
|
||||
// }
|
||||
this.mindMap.render()
|
||||
})
|
||||
this.mindMap.emit('hide_text_edit', this.textEditNode, list, node)
|
||||
this.mindMap.emit('hide_text_edit', this.textEditNode, list, this.node)
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.showTextEdit = false
|
||||
this.mindMap.emit('rich_text_selection_change', false)
|
||||
this.node = null
|
||||
this.isInserting = false
|
||||
}
|
||||
|
||||
// 初始化Quill富文本编辑器
|
||||
@ -460,18 +426,6 @@ class RichText {
|
||||
}
|
||||
}
|
||||
},
|
||||
formats: [
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'strike',
|
||||
'color',
|
||||
'background',
|
||||
'font',
|
||||
'size',
|
||||
'formula',
|
||||
'align'
|
||||
], // 明确指定允许的格式,不包含有序列表,无序列表等
|
||||
theme: 'snow'
|
||||
})
|
||||
// 拦截复制事件,即Ctrl + c,去除多余的空行
|
||||
@ -491,10 +445,7 @@ class RichText {
|
||||
})
|
||||
this.quill.on('selection-change', range => {
|
||||
// 刚创建的节点全选不需要显示操作条
|
||||
if (this.isInserting) {
|
||||
this.isInserting = false
|
||||
return
|
||||
}
|
||||
if (this.isInserting) return
|
||||
this.lastRange = this.range
|
||||
this.range = null
|
||||
if (range) {
|
||||
@ -527,18 +478,24 @@ class RichText {
|
||||
}
|
||||
})
|
||||
this.quill.on('text-change', () => {
|
||||
this.mindMap.emit('node_text_edit_change', {
|
||||
node: this.node,
|
||||
text: this.getEditText(),
|
||||
richText: true
|
||||
})
|
||||
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(this.formatPasteText(node.data), style)
|
||||
// })
|
||||
// 剪贴板里只要存在文本就会走这里,所以当剪贴板里是纯文本,或文本+图片都可以监听到和拦截,但是只有纯图片时不会走这里,所以无法拦截
|
||||
this.quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
|
||||
let ops = []
|
||||
let style = this.getPasteTextStyle()
|
||||
@ -554,20 +511,6 @@ class RichText {
|
||||
delta.ops = ops
|
||||
return delta
|
||||
})
|
||||
// 拦截图片的粘贴,当剪贴板里是纯图片,或文本+图片都可以拦截到,但是带来的问题是文本+图片时里面的文本也无法粘贴
|
||||
this.quill.root.addEventListener(
|
||||
'paste',
|
||||
e => {
|
||||
if (
|
||||
e.clipboardData &&
|
||||
e.clipboardData.files &&
|
||||
e.clipboardData.files.length
|
||||
) {
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
// 获取粘贴的文本的样式
|
||||
@ -601,32 +544,16 @@ class RichText {
|
||||
this.isCompositing = true
|
||||
}
|
||||
|
||||
// 中文输入中
|
||||
onCompositionUpdate() {
|
||||
if (!this.showTextEdit || !this.node) return
|
||||
this.mindMap.emit('node_text_edit_change', {
|
||||
node: this.node,
|
||||
text: this.getEditText(),
|
||||
richText: true
|
||||
})
|
||||
}
|
||||
|
||||
// 中文输入结束
|
||||
onCompositionEnd() {
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
this.isCompositing = false
|
||||
}
|
||||
|
||||
// 设置文本编辑框是否处于显示状态
|
||||
setIsShowTextEdit(val) {
|
||||
this.showTextEdit = val
|
||||
if (val) {
|
||||
this.mindMap.keyCommand.stopCheckInSvg()
|
||||
} else {
|
||||
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||
if (!this.lostStyle) {
|
||||
return
|
||||
}
|
||||
this.setTextStyleIfNotRichText(this.node)
|
||||
}
|
||||
|
||||
// 选中全部
|
||||
@ -636,28 +563,19 @@ class RichText {
|
||||
|
||||
// 聚焦
|
||||
focus(start) {
|
||||
const len = this.quill.getLength()
|
||||
let len = this.quill.getLength()
|
||||
this.quill.setSelection(typeof start === 'number' ? start : len, len)
|
||||
}
|
||||
|
||||
// 格式化当前选中的文本
|
||||
formatText(config = {}, clear = false) {
|
||||
formatText(config = {}, clear = false, pure = false) {
|
||||
if (!this.range && !this.lastRange) return
|
||||
const rangeLost = !this.range
|
||||
const range = rangeLost ? this.lastRange : this.range
|
||||
if (clear) {
|
||||
this.quill.removeFormat(range.index, range.length)
|
||||
} else {
|
||||
const { align, ...rest } = config
|
||||
// 文本对齐需要对行进行格式化
|
||||
if (align) {
|
||||
this.quill.formatLine(range.index, range.length, 'align', align)
|
||||
}
|
||||
// 其他内容对文本
|
||||
if (Object.keys(rest).length > 0) {
|
||||
this.quill.formatText(range.index, range.length, rest)
|
||||
}
|
||||
}
|
||||
if (!pure) 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)
|
||||
if (rangeLost) {
|
||||
this.quill.setSelection(this.lastRange.index, this.lastRange.length)
|
||||
}
|
||||
@ -665,25 +583,70 @@ class RichText {
|
||||
|
||||
// 清除当前选中文本的样式
|
||||
removeFormat() {
|
||||
// 先移除全部样式
|
||||
this.formatText({}, true)
|
||||
// 再将样式恢复为当前主题改节点的默认样式
|
||||
const style = {}
|
||||
if (this.node) {
|
||||
;[
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontWeight',
|
||||
'fontStyle',
|
||||
'textDecoration',
|
||||
'color'
|
||||
].forEach(key => {
|
||||
style[key] = this.node.style.merge(key)
|
||||
})
|
||||
}
|
||||
const config = this.normalStyleToRichTextStyle(style)
|
||||
this.formatText(config, false, true)
|
||||
}
|
||||
|
||||
// 格式化指定范围的文本
|
||||
formatRangeText(range, config = {}) {
|
||||
if (!range) return
|
||||
this.syncFormatToNodeConfig(config)
|
||||
this.quill.formatText(range.index, range.length, config)
|
||||
}
|
||||
|
||||
// 格式化所有文本
|
||||
formatAllText(config = {}) {
|
||||
this.syncFormatToNodeConfig(config)
|
||||
this.pureFormatAllText(config)
|
||||
}
|
||||
|
||||
// 纯粹的格式化所有文本
|
||||
pureFormatAllText(config = {}) {
|
||||
this.quill.formatText(0, this.quill.getLength(), config)
|
||||
}
|
||||
|
||||
// 同步格式化到节点样式配置
|
||||
syncFormatToNodeConfig(config, clear) {
|
||||
if (!this.node) return
|
||||
if (clear) {
|
||||
// 清除文本样式
|
||||
;[
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontWeight',
|
||||
'fontStyle',
|
||||
'textDecoration',
|
||||
'color'
|
||||
].forEach(prop => {
|
||||
delete this.node.nodeData.data[prop]
|
||||
})
|
||||
} else {
|
||||
let data = this.richTextStyleToNormalStyle(config)
|
||||
this.mindMap.execCommand('SET_NODE_DATA', this.node, data)
|
||||
}
|
||||
}
|
||||
|
||||
// 将普通节点样式对象转换成富文本样式对象
|
||||
normalStyleToRichTextStyle(style) {
|
||||
const config = {}
|
||||
let config = {}
|
||||
Object.keys(style).forEach(prop => {
|
||||
const value = style[prop]
|
||||
let value = style[prop]
|
||||
switch (prop) {
|
||||
case 'fontFamily':
|
||||
config.font = value
|
||||
@ -704,9 +667,6 @@ class RichText {
|
||||
case 'color':
|
||||
config.color = value
|
||||
break
|
||||
case 'textAlign':
|
||||
config.align = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -716,9 +676,9 @@ class RichText {
|
||||
|
||||
// 将富文本样式对象转换成普通节点样式对象
|
||||
richTextStyleToNormalStyle(config) {
|
||||
const data = {}
|
||||
let data = {}
|
||||
Object.keys(config).forEach(prop => {
|
||||
const value = config[prop]
|
||||
let value = config[prop]
|
||||
switch (prop) {
|
||||
case 'font':
|
||||
data.fontFamily = value
|
||||
@ -741,9 +701,6 @@ class RichText {
|
||||
case 'color':
|
||||
data.color = value
|
||||
break
|
||||
case 'align':
|
||||
data.textAlign = value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -751,55 +708,40 @@ class RichText {
|
||||
return data
|
||||
}
|
||||
|
||||
// 判断一个对象是否包含了富文本支持的样式字段
|
||||
isHasRichTextStyle(obj) {
|
||||
const keys = Object.keys(obj)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
if (richTextSupportStyleList.includes(key)) {
|
||||
return true
|
||||
}
|
||||
// 给未激活的节点设置富文本样式
|
||||
setNotActiveNodeStyle(node, style) {
|
||||
const config = this.normalStyleToRichTextStyle(style)
|
||||
if (Object.keys(config).length > 0) {
|
||||
this.showEditText({ node })
|
||||
this.formatAllText(config)
|
||||
this.hideEditText([node])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查指定节点是否存在自定义的富文本样式
|
||||
checkNodeHasCustomRichTextStyle(node) {
|
||||
const list = [
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontWeight',
|
||||
'fontStyle',
|
||||
'textDecoration',
|
||||
'color'
|
||||
]
|
||||
const nodeData = node instanceof MindMapNode ? node.getData() : node
|
||||
for (let i = 0; i < richTextSupportStyleList.length; i++) {
|
||||
if (nodeData[richTextSupportStyleList[i]] !== undefined) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (nodeData[list[i]] !== undefined) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 转换数据后的渲染操作
|
||||
afterHandleData() {
|
||||
// 清空历史数据,并且触发数据变化
|
||||
this.mindMap.command.clearHistory()
|
||||
this.mindMap.command.addHistory()
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 插件实例化时处理思维导图数据,转换为富文本数据
|
||||
handleDataToRichTextOnInit() {
|
||||
// 处理数据,转成富文本格式
|
||||
if (this.mindMap.renderer.renderTree) {
|
||||
// 如果已经存在渲染树了,那么直接更新渲染树,并且触发重新渲染
|
||||
this.handleSetData(this.mindMap.renderer.renderTree)
|
||||
this.afterHandleData()
|
||||
} else if (this.mindMap.opt.data) {
|
||||
this.handleSetData(this.mindMap.opt.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 将所有节点转换成非富文本节点
|
||||
transformAllNodesToNormalNode() {
|
||||
const renderTree = this.mindMap.renderer.renderTree
|
||||
if (!renderTree) return
|
||||
if (!this.mindMap.renderer.renderTree) return
|
||||
walk(
|
||||
renderTree,
|
||||
this.mindMap.renderer.renderTree,
|
||||
null,
|
||||
node => {
|
||||
if (node.data.richText) {
|
||||
@ -820,36 +762,25 @@ class RichText {
|
||||
0,
|
||||
0
|
||||
)
|
||||
this.afterHandleData()
|
||||
}
|
||||
|
||||
handleDataToRichText(data) {
|
||||
const oldIsRichText = data.richText
|
||||
data.richText = true
|
||||
data.resetRichText = true
|
||||
// 如果原本就是富文本,那么不能转换
|
||||
if (!oldIsRichText) {
|
||||
data.text = htmlEscape(data.text)
|
||||
}
|
||||
// 清空历史数据,并且触发数据变化
|
||||
this.mindMap.command.clearHistory()
|
||||
this.mindMap.command.addHistory()
|
||||
this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE)
|
||||
}
|
||||
|
||||
// 处理导入数据
|
||||
handleSetData(data) {
|
||||
if (!data) return
|
||||
// 短期处理,为了兼容老数据,长期会去除
|
||||
const isOldRichTextVersion =
|
||||
!data.smmVersion || compareVersion(data.smmVersion, '0.13.0') === '<'
|
||||
const walk = root => {
|
||||
if (root.data && (!root.data.richText || isOldRichTextVersion)) {
|
||||
this.handleDataToRichText(root.data)
|
||||
let walk = root => {
|
||||
if (root.data && !root.data.richText) {
|
||||
root.data.richText = true
|
||||
root.data.resetRichText = true
|
||||
}
|
||||
// 概要
|
||||
if (root.data) {
|
||||
const generalizationList = formatGetNodeGeneralization(root.data)
|
||||
generalizationList.forEach(item => {
|
||||
if (!item.richText || isOldRichTextVersion) {
|
||||
this.handleDataToRichText(item)
|
||||
}
|
||||
item.richText = true
|
||||
item.resetRichText = true
|
||||
})
|
||||
}
|
||||
if (root.children && root.children.length > 0) {
|
||||
@ -867,15 +798,12 @@ class RichText {
|
||||
this.transformAllNodesToNormalNode()
|
||||
document.head.removeChild(this.styleEl)
|
||||
this.unbindEvent()
|
||||
this.mindMap.removeAppendCss('richText')
|
||||
this.mindMap.deleteEditNodeClass(RICH_TEXT_EDIT_WRAP)
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
document.head.removeChild(this.styleEl)
|
||||
this.unbindEvent()
|
||||
this.mindMap.deleteEditNodeClass(RICH_TEXT_EDIT_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -107,7 +107,6 @@ class Search {
|
||||
|
||||
// 搜索匹配的节点
|
||||
doSearch() {
|
||||
this.clearHighlightOnReadonly()
|
||||
this.updateMatchNodeList([])
|
||||
this.currentIndex = -1
|
||||
const { isOnlySearchCurrentRenderNodes } = this.mindMap.opt
|
||||
@ -175,17 +174,19 @@ class Search {
|
||||
}
|
||||
}
|
||||
const { readonly } = this.mindMap.opt
|
||||
// 只读模式下需要清除之前节点的高亮
|
||||
this.clearHighlightOnReadonly()
|
||||
// 只读模式下需要激活之前节点的高亮
|
||||
if (readonly) {
|
||||
this.matchNodeList.forEach(node => {
|
||||
if (this.isNodeInstance(node)) {
|
||||
node.closeHighlight()
|
||||
}
|
||||
})
|
||||
}
|
||||
const currentNode = this.matchNodeList[this.currentIndex]
|
||||
this.notResetSearchText = true
|
||||
const uid = this.isNodeInstance(currentNode)
|
||||
? currentNode.getData('uid')
|
||||
: currentNode.data.uid
|
||||
if (!uid) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const targetNode = this.mindMap.renderer.findNodeByUid(uid)
|
||||
this.mindMap.execCommand('GO_TARGET_NODE', uid, node => {
|
||||
if (!this.isNodeInstance(currentNode)) {
|
||||
@ -204,18 +205,6 @@ class Search {
|
||||
})
|
||||
}
|
||||
|
||||
// 只读模式下清除现有匹配节点的高亮
|
||||
clearHighlightOnReadonly() {
|
||||
const { readonly } = this.mindMap.opt
|
||||
if (readonly) {
|
||||
this.matchNodeList.forEach(node => {
|
||||
if (this.isNodeInstance(node)) {
|
||||
node.closeHighlight()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 定位到指定搜索结果索引的节点
|
||||
jump(index, callback = () => {}) {
|
||||
this.searchNext(callback, index)
|
||||
@ -235,15 +224,9 @@ class Search {
|
||||
replaceText = String(replaceText)
|
||||
let currentNode = this.matchNodeList[this.currentIndex]
|
||||
if (!currentNode) return
|
||||
// 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的
|
||||
const keep = replaceText.includes(this.searchText)
|
||||
const text = this.getReplacedText(currentNode, this.searchText, replaceText)
|
||||
let text = this.getReplacedText(currentNode, this.searchText, replaceText)
|
||||
this.notResetSearchText = true
|
||||
currentNode.setText(text, currentNode.getData('richText'))
|
||||
if (keep) {
|
||||
this.updateMatchNodeList(this.matchNodeList)
|
||||
return
|
||||
}
|
||||
currentNode.setText(text, currentNode.getData('richText'), true)
|
||||
const newList = this.matchNodeList.filter(node => {
|
||||
return currentNode !== node
|
||||
})
|
||||
@ -266,27 +249,25 @@ class Search {
|
||||
)
|
||||
return
|
||||
replaceText = String(replaceText)
|
||||
// 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的
|
||||
const keep = replaceText.includes(this.searchText)
|
||||
this.notResetSearchText = true
|
||||
this.matchNodeList.forEach(node => {
|
||||
const text = this.getReplacedText(node, this.searchText, replaceText)
|
||||
if (this.isNodeInstance(node)) {
|
||||
const data = {
|
||||
text
|
||||
}
|
||||
this.mindMap.renderer.setNodeDataRender(node, data, true)
|
||||
this.mindMap.renderer.setNodeDataRender(
|
||||
node,
|
||||
{
|
||||
text,
|
||||
resetRichText: !!node.getData('richText')
|
||||
},
|
||||
true
|
||||
)
|
||||
} else {
|
||||
node.data.text = text
|
||||
node.data.resetRichText = !!node.data.richText
|
||||
}
|
||||
})
|
||||
this.mindMap.render()
|
||||
this.mindMap.command.addHistory()
|
||||
if (keep) {
|
||||
this.updateMatchNodeList(this.matchNodeList)
|
||||
} else {
|
||||
this.endSearch()
|
||||
}
|
||||
this.endSearch()
|
||||
}
|
||||
|
||||
// 获取某个节点替换后的文本
|
||||
@ -297,7 +278,7 @@ class Search {
|
||||
if (richText) {
|
||||
return replaceHtmlText(text, searchText, replaceText)
|
||||
} else {
|
||||
return text.replace(new RegExp(searchText, 'g'), replaceText)
|
||||
return text.replaceAll(searchText, replaceText)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -41,8 +41,7 @@ class Select {
|
||||
|
||||
// 鼠标按下
|
||||
onMousedown(e) {
|
||||
const { readonly, mousedownEventPreventDefault } = this.mindMap.opt
|
||||
if (readonly) {
|
||||
if (this.mindMap.opt.readonly) {
|
||||
return
|
||||
}
|
||||
let { useLeftKeySelectionRightKeyDrag } = this.mindMap.opt
|
||||
@ -52,9 +51,7 @@ class Select {
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (mousedownEventPreventDefault) {
|
||||
e.preventDefault()
|
||||
}
|
||||
e.preventDefault()
|
||||
this.isMousedown = true
|
||||
this.cacheActiveList = [...this.mindMap.renderer.activeNodeList]
|
||||
let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
|
||||
|
||||
@ -6,8 +6,8 @@ import {
|
||||
} from './associativeLineUtils'
|
||||
|
||||
// 创建控制点、连线节点
|
||||
function createControlNodes(node, toNode) {
|
||||
let { associativeLineActiveColor } = this.getStyleConfig(node, toNode)
|
||||
function createControlNodes() {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
// 连线
|
||||
this.controlLine1 = this.associativeLineDraw
|
||||
.line()
|
||||
@ -16,13 +16,13 @@ function createControlNodes(node, toNode) {
|
||||
.line()
|
||||
.stroke({ color: associativeLineActiveColor, width: 2 })
|
||||
// 控制点
|
||||
this.controlPoint1 = this.createOneControlNode('controlPoint1', node, toNode)
|
||||
this.controlPoint2 = this.createOneControlNode('controlPoint2', node, toNode)
|
||||
this.controlPoint1 = this.createOneControlNode('controlPoint1')
|
||||
this.controlPoint2 = this.createOneControlNode('controlPoint2')
|
||||
}
|
||||
|
||||
// 创建控制点
|
||||
function createOneControlNode(pointKey, node, toNode) {
|
||||
let { associativeLineActiveColor } = this.getStyleConfig(node, toNode)
|
||||
function createOneControlNode(pointKey) {
|
||||
let { associativeLineActiveColor } = this.mindMap.themeConfig
|
||||
return this.associativeLineDraw
|
||||
.circle(this.controlPointDiameter)
|
||||
.stroke({ color: associativeLineActiveColor })
|
||||
@ -202,7 +202,6 @@ function onControlPointMouseup(e) {
|
||||
associativeLineTargetControlOffsets: offsetList,
|
||||
associativeLinePoint
|
||||
})
|
||||
this.isNotRenderAllLines = true
|
||||
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚,所以重置isControlPointMousedown需要等draw_click事件触发完以后
|
||||
setTimeout(() => {
|
||||
this.resetControlPoint()
|
||||
@ -222,10 +221,10 @@ function resetControlPoint() {
|
||||
}
|
||||
|
||||
// 渲染控制点
|
||||
function renderControls(startPoint, endPoint, point1, point2, node, toNode) {
|
||||
function renderControls(startPoint, endPoint, point1, point2) {
|
||||
if (!this.mindMap.opt.enableAdjustAssociativeLinePoints) return
|
||||
if (!this.controlLine1) {
|
||||
this.createControlNodes(node, toNode)
|
||||
this.createControlNodes()
|
||||
}
|
||||
let radius = this.controlPointDiameter / 2
|
||||
// 控制点和起终点的连线
|
||||
|
||||
@ -5,8 +5,6 @@ import {
|
||||
selectAllInput
|
||||
} from '../../utils/index'
|
||||
|
||||
const ASSOCIATIVE_LINE_TEXT_EDIT_WRAP = 'associative-line-text-edit-warp'
|
||||
|
||||
// 创建文字节点
|
||||
function createText(data) {
|
||||
let g = this.associativeLineDraw.group()
|
||||
@ -45,7 +43,6 @@ function showEditTextBox(g) {
|
||||
// 输入框元素没有创建过,则先创建
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.className = ASSOCIATIVE_LINE_TEXT_EDIT_WRAP
|
||||
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
|
||||
this.textEditNode.setAttribute('contenteditable', true)
|
||||
this.textEditNode.addEventListener('keyup', e => {
|
||||
@ -57,14 +54,14 @@ function showEditTextBox(g) {
|
||||
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
let [, , , node, toNode] = this.activeLine
|
||||
let {
|
||||
associativeLineTextFontSize,
|
||||
associativeLineTextFontFamily,
|
||||
associativeLineTextLineHeight
|
||||
} = this.getStyleConfig(node, toNode)
|
||||
} = this.mindMap.themeConfig
|
||||
let { defaultAssociativeLineText, nodeTextEditZIndex } = this.mindMap.opt
|
||||
let scale = this.mindMap.view.scale
|
||||
let [, , , node, toNode] = this.activeLine
|
||||
let text = this.getText(node, toNode)
|
||||
let textLines = (text || defaultAssociativeLineText).split(/\n/gim)
|
||||
this.textEditNode.style.fontFamily = associativeLineTextFontFamily
|
||||
@ -75,7 +72,7 @@ function showEditTextBox(g) {
|
||||
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.updateTextEditBoxPos(g)
|
||||
this.setIsShowTextEdit(true)
|
||||
this.showTextEdit = true
|
||||
// 如果是默认文本要全选输入框
|
||||
if (text === '' || text === defaultAssociativeLineText) {
|
||||
selectAllInput(this.textEditNode)
|
||||
@ -85,16 +82,6 @@ function showEditTextBox(g) {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置文本编辑框是否处于显示状态
|
||||
function setIsShowTextEdit(val) {
|
||||
this.showTextEdit = val
|
||||
if (val) {
|
||||
this.mindMap.keyCommand.stopCheckInSvg()
|
||||
} else {
|
||||
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文本编辑框元素
|
||||
function removeTextEditEl() {
|
||||
if (!this.textEditNode) return
|
||||
@ -136,8 +123,8 @@ function hideEditTextBox() {
|
||||
})
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.textEditNode.innerHTML = ''
|
||||
this.setIsShowTextEdit(false)
|
||||
this.renderText(str, path, text, node, toNode)
|
||||
this.showTextEdit = false
|
||||
this.renderText(str, path, text)
|
||||
this.mindMap.emit('hide_text_edit')
|
||||
}
|
||||
|
||||
@ -151,41 +138,35 @@ function getText(node, toNode) {
|
||||
}
|
||||
|
||||
// 渲染关联线文字
|
||||
function renderText(str, path, text, node, toNode) {
|
||||
function renderText(str, path, text) {
|
||||
if (!str) return
|
||||
let { associativeLineTextFontSize, associativeLineTextLineHeight } =
|
||||
this.getStyleConfig(node, toNode)
|
||||
this.mindMap.themeConfig
|
||||
text.clear()
|
||||
let textArr = str.replace(/\n$/g, '').split(/\n/gim)
|
||||
let textArr = str.split(/\n/gim)
|
||||
textArr.forEach((item, index) => {
|
||||
// 避免尾部的空行不占宽度,导致文本编辑框定位异常的问题
|
||||
if (item === '') {
|
||||
item = ''
|
||||
}
|
||||
let textNode = new Text().text(item)
|
||||
textNode.y(
|
||||
associativeLineTextFontSize * associativeLineTextLineHeight * index
|
||||
)
|
||||
this.styleText(textNode, node, toNode)
|
||||
text.add(textNode)
|
||||
let node = new Text().text(item)
|
||||
node.y(associativeLineTextFontSize * associativeLineTextLineHeight * index)
|
||||
this.styleText(node)
|
||||
text.add(node)
|
||||
})
|
||||
updateTextPos(path, text)
|
||||
}
|
||||
|
||||
// 给文本设置样式
|
||||
function styleText(textNode, node, toNode) {
|
||||
function styleText(node) {
|
||||
let {
|
||||
associativeLineTextColor,
|
||||
associativeLineTextFontSize,
|
||||
associativeLineTextFontFamily
|
||||
} = this.getStyleConfig(node, toNode)
|
||||
textNode
|
||||
} = this.mindMap.themeConfig
|
||||
node
|
||||
.fill({
|
||||
color: associativeLineTextColor
|
||||
})
|
||||
.css({
|
||||
'font-family': associativeLineTextFontFamily,
|
||||
'font-size': associativeLineTextFontSize + 'px'
|
||||
'font-size': associativeLineTextFontSize
|
||||
})
|
||||
}
|
||||
|
||||
@ -204,7 +185,6 @@ export default {
|
||||
styleText,
|
||||
onScale,
|
||||
showEditTextBox,
|
||||
setIsShowTextEdit,
|
||||
removeTextEditEl,
|
||||
hideEditTextBox,
|
||||
updateTextEditBoxPos,
|
||||
|
||||
@ -1,234 +0,0 @@
|
||||
import { Text, Rect, G } from '@svgdotjs/svg.js'
|
||||
import {
|
||||
getStrWithBrFromHtml,
|
||||
focusInput,
|
||||
selectAllInput
|
||||
} from '../../utils/index'
|
||||
|
||||
const OUTER_FRAME_TEXT_EDIT_WRAP = 'outer-frame-text-edit-warp'
|
||||
|
||||
// 创建文字节点
|
||||
function createText(el, cur, range) {
|
||||
const g = this.draw.group()
|
||||
const setActive = () => {
|
||||
if (!this.activeOuterFrame || this.activeOuterFrame.el !== el) {
|
||||
this.setActiveOuterFrame(el, cur, range, g)
|
||||
}
|
||||
}
|
||||
g.click(e => {
|
||||
e.stopPropagation()
|
||||
setActive()
|
||||
})
|
||||
g.on('dblclick', e => {
|
||||
e.stopPropagation()
|
||||
setActive()
|
||||
this.showEditTextBox(g)
|
||||
})
|
||||
return g
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
function showEditTextBox(g) {
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
// 注册回车快捷键
|
||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
// 输入框元素没有创建过,则先创建
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.className = OUTER_FRAME_TEXT_EDIT_WRAP
|
||||
this.textEditNode.style.cssText = `
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,.5);
|
||||
outline: none;
|
||||
word-break: break-all;
|
||||
`
|
||||
this.textEditNode.setAttribute('contenteditable', true)
|
||||
this.textEditNode.addEventListener('keyup', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
const { node, range } = this.activeOuterFrame
|
||||
const style = this.getStyle(this.getNodeRangeFirstNode(node, range))
|
||||
const [pl, pt, pr, pb] = style.textFillPadding
|
||||
let { defaultOuterFrameText, nodeTextEditZIndex } = this.mindMap.opt
|
||||
let scale = this.mindMap.view.scale
|
||||
let text = this.getText(this.getNodeRangeFirstNode(node, range))
|
||||
let textLines = (text || defaultOuterFrameText).split(/\n/gim)
|
||||
this.textEditNode.style.padding = `${pl}px ${pt}px ${pr}px ${pb}px`
|
||||
this.textEditNode.style.fontFamily = style.fontFamily
|
||||
this.textEditNode.style.fontSize = style.fontSize * scale + 'px'
|
||||
this.textEditNode.style.fontWeight = style.fontWeight
|
||||
this.textEditNode.style.fontStyle = style.fontStyle
|
||||
this.textEditNode.style.lineHeight =
|
||||
textLines.length > 1 ? style.lineHeight : 'normal'
|
||||
this.textEditNode.style.zIndex = nodeTextEditZIndex
|
||||
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.updateTextEditBoxPos(g)
|
||||
this.setIsShowTextEdit(true)
|
||||
// 如果是默认文本要全选输入框
|
||||
if (text === '' || text === defaultOuterFrameText) {
|
||||
selectAllInput(this.textEditNode)
|
||||
} else {
|
||||
// 否则聚焦即可
|
||||
focusInput(this.textEditNode)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置文本编辑框是否处于显示状态
|
||||
function setIsShowTextEdit(val) {
|
||||
this.showTextEdit = val
|
||||
if (val) {
|
||||
this.mindMap.keyCommand.stopCheckInSvg()
|
||||
} else {
|
||||
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文本编辑框元素
|
||||
function removeTextEditEl() {
|
||||
if (!this.textEditNode) return
|
||||
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.removeChild(this.textEditNode)
|
||||
}
|
||||
|
||||
// 处理画布缩放
|
||||
function onScale() {
|
||||
this.hideEditTextBox()
|
||||
}
|
||||
|
||||
// 更新文本编辑框位置
|
||||
function updateTextEditBoxPos(g) {
|
||||
let rect = g.node.getBoundingClientRect()
|
||||
if (this.textEditNode) {
|
||||
this.textEditNode.style.minWidth = `${rect.width}px`
|
||||
this.textEditNode.style.minHeight = `${rect.height}px`
|
||||
this.textEditNode.style.left = `${rect.left}px`
|
||||
this.textEditNode.style.top = `${rect.top}px`
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏文本编辑框
|
||||
function hideEditTextBox() {
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
let { el, textNode, node, range } = this.activeOuterFrame
|
||||
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||
// 如果是默认文本,那么不保存
|
||||
let isDefaultText = str === this.mindMap.opt.defaultOuterFrameText
|
||||
str = isDefaultText ? '' : str
|
||||
this.updateActiveOuterFrame({
|
||||
text: str
|
||||
})
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.textEditNode.innerHTML = ''
|
||||
this.setIsShowTextEdit(false)
|
||||
this.renderText(str, el, textNode, node, range)
|
||||
this.mindMap.emit('hide_text_edit')
|
||||
}
|
||||
|
||||
// 渲染文字
|
||||
function renderText(str, rect, textNode, node, range) {
|
||||
if (!str) return
|
||||
// 先清空文字节点原内容
|
||||
textNode.clear()
|
||||
// 创建背景矩形
|
||||
const shape = new Rect()
|
||||
textNode.add(shape)
|
||||
// 获取样式配置
|
||||
const style = this.getStyle(this.getNodeRangeFirstNode(node, range))
|
||||
const [pl, pt, pr, pb] = style.textFillPadding
|
||||
// 创建文本节点
|
||||
let textArr = str.replace(/\n$/g, '').split(/\n/gim)
|
||||
const g = new G()
|
||||
textArr.forEach((item, index) => {
|
||||
// 避免尾部的空行不占宽度,导致文本编辑框定位异常的问题
|
||||
if (item === '') {
|
||||
item = ''
|
||||
}
|
||||
let text = new Text().text(item)
|
||||
text.y(style.fontSize * style.lineHeight * index)
|
||||
this.styleText(text, style)
|
||||
g.add(text)
|
||||
})
|
||||
textNode.add(g)
|
||||
// 计算高度
|
||||
const { width: textWidth, height: textHeight } = textNode.bbox()
|
||||
const totalWidth = textWidth + pl + pr
|
||||
const totalHeight = textHeight + pt + pb
|
||||
shape.size(totalWidth, totalHeight).x(0).dy(0)
|
||||
this.styleTextShape(shape, style)
|
||||
// 设置节点位置
|
||||
let tx = 0
|
||||
switch (style.textAlign) {
|
||||
case 'left':
|
||||
tx = rect.x()
|
||||
break
|
||||
case 'center':
|
||||
tx = rect.x() + rect.width() / 2 - totalWidth / 2
|
||||
break
|
||||
case 'right':
|
||||
tx = rect.x() + rect.width() - totalWidth
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
const ty = rect.y() - totalHeight
|
||||
shape.x(tx)
|
||||
shape.y(ty)
|
||||
g.x(tx + pl)
|
||||
g.y(ty + pt)
|
||||
}
|
||||
|
||||
// 给文本背景设置样式
|
||||
function styleTextShape(shape, style) {
|
||||
shape
|
||||
.fill({
|
||||
color: style.textFill
|
||||
})
|
||||
.radius(style.textFillRadius)
|
||||
}
|
||||
|
||||
// 给文本设置样式
|
||||
function styleText(textNode, style) {
|
||||
textNode
|
||||
.fill({
|
||||
color: style.color
|
||||
})
|
||||
.css({
|
||||
'font-family': style.fontFamily,
|
||||
'font-size': style.fontSize + 'px',
|
||||
'font-weight': style.fontWeight,
|
||||
'font-style': style.fontStyle
|
||||
})
|
||||
}
|
||||
|
||||
// 获取外框文字
|
||||
function getText(node) {
|
||||
const data = node.getData('outerFrame')
|
||||
return data && data.text ? data.text : ''
|
||||
}
|
||||
|
||||
export default {
|
||||
getText,
|
||||
createText,
|
||||
styleTextShape,
|
||||
styleText,
|
||||
onScale,
|
||||
showEditTextBox,
|
||||
setIsShowTextEdit,
|
||||
removeTextEditEl,
|
||||
hideEditTextBox,
|
||||
updateTextEditBoxPos,
|
||||
renderText
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
import { getTopAncestorsFomNodeList } from '../../utils'
|
||||
|
||||
// 解析要添加外框的节点实例列表
|
||||
export const parseAddNodeList = list => {
|
||||
// 找出顶层节点
|
||||
list = getTopAncestorsFomNodeList(list)
|
||||
const cache = {}
|
||||
const uidToParent = {}
|
||||
// 找出列表中节点在兄弟节点中的索引,并和父节点关联起来
|
||||
list.forEach(node => {
|
||||
const parent = node.parent
|
||||
if (parent) {
|
||||
const pUid = parent.uid
|
||||
uidToParent[pUid] = parent
|
||||
const index = node.getIndexInBrothers()
|
||||
const data = {
|
||||
node,
|
||||
index
|
||||
}
|
||||
if (cache[pUid]) {
|
||||
if (
|
||||
!cache[pUid].find(item => {
|
||||
return item.index === data.index
|
||||
})
|
||||
) {
|
||||
cache[pUid].push(data)
|
||||
}
|
||||
} else {
|
||||
cache[pUid] = [data]
|
||||
}
|
||||
}
|
||||
})
|
||||
const res = []
|
||||
Object.keys(cache).forEach(uid => {
|
||||
const indexList = cache[uid]
|
||||
const parentNode = uidToParent[uid]
|
||||
if (indexList.length > 1) {
|
||||
// 多个节点
|
||||
const rangeList = indexList
|
||||
.map(item => {
|
||||
return item.index
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a - b
|
||||
})
|
||||
const minIndex = rangeList[0]
|
||||
const maxIndex = rangeList[rangeList.length - 1]
|
||||
let curStart = -1
|
||||
let curEnd = -1
|
||||
for (let i = minIndex; i <= maxIndex; i++) {
|
||||
// 连续索引
|
||||
if (rangeList.includes(i)) {
|
||||
if (curStart === -1) {
|
||||
curStart = i
|
||||
}
|
||||
curEnd = i
|
||||
} else {
|
||||
// 连续断开
|
||||
if (curStart !== -1 && curEnd !== -1) {
|
||||
res.push({
|
||||
node: parentNode,
|
||||
range: [curStart, curEnd]
|
||||
})
|
||||
}
|
||||
curStart = -1
|
||||
curEnd = -1
|
||||
}
|
||||
}
|
||||
// 不要忘了最后一段索引
|
||||
if (curStart !== -1 && curEnd !== -1) {
|
||||
res.push({
|
||||
node: parentNode,
|
||||
range: [curStart, curEnd]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 单个节点
|
||||
res.push({
|
||||
node: parentNode,
|
||||
range: [indexList[0].index, indexList[0].index]
|
||||
})
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// 解析获取节点的子节点生成的外框列表
|
||||
export const getNodeOuterFrameList = node => {
|
||||
const children = node.children
|
||||
if (!children || children.length <= 0) return
|
||||
const res = []
|
||||
const map = {}
|
||||
children.forEach((item, index) => {
|
||||
const outerFrameData = item.getData('outerFrame')
|
||||
if (!outerFrameData) return
|
||||
const groupId = outerFrameData.groupId
|
||||
if (groupId) {
|
||||
if (!map[groupId]) {
|
||||
map[groupId] = []
|
||||
}
|
||||
map[groupId].push({
|
||||
node: item,
|
||||
index
|
||||
})
|
||||
} else {
|
||||
res.push({
|
||||
nodeList: [item],
|
||||
range: [index, index]
|
||||
})
|
||||
}
|
||||
})
|
||||
Object.keys(map).forEach(id => {
|
||||
const list = map[id]
|
||||
res.push({
|
||||
nodeList: list.map(item => {
|
||||
return item.node
|
||||
}),
|
||||
range: [list[0].index, list[list.length - 1].index]
|
||||
})
|
||||
})
|
||||
return res
|
||||
}
|
||||
@ -1,22 +1,18 @@
|
||||
// 展开按钮
|
||||
const open = `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M475.136 327.168v147.968h-147.968v74.24h147.968v147.968h74.24v-147.968h147.968v-74.24h-147.968v-147.968h-74.24z m36.864-222.208c225.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"></path></svg>`
|
||||
const open = `<svg t="1618141562310" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13476" width="200" height="200"><path d="M475.136 327.168v147.968h-147.968v74.24h147.968v147.968h74.24v-147.968h147.968v-74.24h-147.968v-147.968h-74.24z m36.864-222.208c225.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="13477"></path></svg>`
|
||||
|
||||
// 收缩按钮
|
||||
const close = `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" 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"></path><path d="M252.928 474.624h518.144v74.24h-518.144z"></path></svg>`
|
||||
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" 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"></path><path fill="#ffffff" d="M252.928 474.624h518.144v74.24h-518.144z"></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>`
|
||||
|
||||
// 快捷创建子节点按钮
|
||||
const quickCreateChild = `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path d="M514.048 62.464q93.184 0 175.616 35.328t143.872 96.768 96.768 143.872 35.328 175.616q0 94.208-35.328 176.128t-96.768 143.36-143.872 96.768-175.616 35.328q-94.208 0-176.64-35.328t-143.872-96.768-96.768-143.36-35.328-176.128q0-93.184 35.328-175.616t96.768-143.872 143.872-96.768 176.64-35.328zM772.096 576.512q26.624 0 45.056-18.944t18.432-45.568-18.432-45.056-45.056-18.432l-192.512 0 0-192.512q0-26.624-18.944-45.568t-45.568-18.944-45.056 18.944-18.432 45.568l0 192.512-192.512 0q-26.624 0-45.056 18.432t-18.432 45.056 18.432 45.568 45.056 18.944l192.512 0 0 191.488q0 26.624 18.432 45.568t45.056 18.944 45.568-18.944 18.944-45.568l0-191.488 192.512 0z"></path></svg>`
|
||||
|
||||
export default {
|
||||
open,
|
||||
close,
|
||||
remove,
|
||||
imgAdjust,
|
||||
quickCreateChild
|
||||
imgAdjust
|
||||
}
|
||||
|
||||