mirror of
https://github.com/wanglin2/mind-map.git
synced 2026-02-17 22:08:25 +08:00
503 lines
13 KiB
JavaScript
503 lines
13 KiB
JavaScript
import View from './src/View'
|
||
import Event from './src/Event'
|
||
import Render from './src/Render'
|
||
import merge from 'deepmerge'
|
||
import theme from './src/themes'
|
||
import Style from './src/Style'
|
||
import KeyCommand from './src/KeyCommand'
|
||
import Command from './src/Command'
|
||
import BatchExecution from './src/BatchExecution'
|
||
import { layoutValueList, CONSTANTS } from './src/utils/constant'
|
||
import { SVG } from '@svgdotjs/svg.js'
|
||
import { simpleDeepClone } from './src/utils'
|
||
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
|
||
|
||
// 默认选项配置
|
||
const defaultOpt = {
|
||
// 是否只读
|
||
readonly: false,
|
||
// 布局
|
||
layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE,
|
||
// 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度
|
||
fishboneDeg: 45,
|
||
// 主题
|
||
theme: 'default', // 内置主题:default(默认主题)
|
||
// 主题配置,会和所选择的主题进行合并
|
||
themeConfig: {},
|
||
// 放大缩小的增量比例
|
||
scaleRatio: 0.1,
|
||
// 最多显示几个标签
|
||
maxTag: 5,
|
||
// 导出图片时的内边距
|
||
exportPadding: 20,
|
||
// 展开收缩按钮尺寸
|
||
expandBtnSize: 20,
|
||
// 节点里图片和文字的间距
|
||
imgTextMargin: 5,
|
||
// 节点里各种文字信息的间距,如图标和文字的间距
|
||
textContentMargin: 2,
|
||
// 多选节点时鼠标移动到边缘时的画布移动偏移量
|
||
selectTranslateStep: 3,
|
||
// 多选节点时鼠标移动距边缘多少距离时开始偏移
|
||
selectTranslateLimit: 20,
|
||
// 自定义节点备注内容显示
|
||
customNoteContentShow: null,
|
||
/*
|
||
{
|
||
show(){},
|
||
hide(){}
|
||
}
|
||
*/
|
||
// 是否开启节点自由拖拽
|
||
enableFreeDrag: false,
|
||
// 水印配置
|
||
watermarkConfig: {
|
||
text: '',
|
||
lineSpacing: 100,
|
||
textSpacing: 100,
|
||
angle: 30,
|
||
textStyle: {
|
||
color: '#999',
|
||
opacity: 0.5,
|
||
fontSize: 14
|
||
}
|
||
},
|
||
// 达到该宽度文本自动换行
|
||
textAutoWrapWidth: 500,
|
||
// 自定义鼠标滚轮事件处理
|
||
// 可以传一个函数,回调参数为事件对象
|
||
customHandleMousewheel: null,
|
||
// 鼠标滚动的行为,如果customHandleMousewheel传了自定义函数,这个属性不生效
|
||
mousewheelAction: CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM,// zoom(放大缩小)、move(上下移动)
|
||
// 当mousewheelAction设为move时,可以通过该属性控制鼠标滚动一下视图移动的步长,单位px
|
||
mousewheelMoveStep: 100,
|
||
// 默认插入的二级节点的文字
|
||
defaultInsertSecondLevelNodeText: '二级节点',
|
||
// 默认插入的二级以下节点的文字
|
||
defaultInsertBelowSecondLevelNodeText: '分支主题',
|
||
// 展开收起按钮的颜色
|
||
expandBtnStyle: {
|
||
color: '#808080',
|
||
fill: '#fff'
|
||
},
|
||
// 自定义展开收起按钮的图标
|
||
expandBtnIcon: {
|
||
open: '',// svg字符串
|
||
close: ''
|
||
},
|
||
// 是否只有当鼠标在画布内才响应快捷键事件
|
||
enableShortcutOnlyWhenMouseInSvg: true,
|
||
// 是否开启节点动画过渡
|
||
enableNodeTransitionMove: true,
|
||
// 如果开启节点动画过渡,可以通过该属性设置过渡的时间,单位ms
|
||
nodeTransitionMoveDuration: 300,
|
||
// 初始根节点的位置
|
||
initRootNodePosition: null,
|
||
// 导出png、svg、pdf时的图形内边距
|
||
exportPaddingX: 10,
|
||
exportPaddingY: 10,
|
||
// 节点文本编辑框的z-index
|
||
nodeTextEditZIndex: 3000,
|
||
// 节点备注浮层的z-index
|
||
nodeNoteTooltipZIndex: 3000,
|
||
// 是否在点击了画布外的区域时结束节点文本的编辑状态
|
||
isEndNodeTextEditOnClickOuter: true,
|
||
// 最大历史记录数
|
||
maxHistoryCount: 1000,
|
||
// 是否一直显示节点的展开收起按钮,默认为鼠标移上去和激活时才显示
|
||
alwaysShowExpandBtn: false,
|
||
// 扩展节点可插入的图标
|
||
iconList: [
|
||
// {
|
||
// name: '',// 分组名称
|
||
// type: '',// 分组的值
|
||
// list: [// 分组下的图标列表
|
||
// {
|
||
// name: '',// 图标名称
|
||
// icon:''// 图标,可以传svg或图片
|
||
// }
|
||
// ]
|
||
// }
|
||
],
|
||
// 节点最大缓存数量
|
||
maxNodeCacheCount: 1000,
|
||
// 关联线默认文字
|
||
defaultAssociativeLineText: '关联'
|
||
}
|
||
|
||
// 思维导图
|
||
class MindMap {
|
||
// 构造函数
|
||
constructor(opt = {}) {
|
||
// 合并选项
|
||
this.opt = this.handleOpt(merge(defaultOpt, opt))
|
||
|
||
// 容器元素
|
||
this.el = this.opt.el
|
||
this.elRect = this.el.getBoundingClientRect()
|
||
|
||
// 画布宽高
|
||
this.width = this.elRect.width
|
||
this.height = this.elRect.height
|
||
|
||
// 画布
|
||
this.svg = SVG().addTo(this.el).size(this.width, this.height)
|
||
this.draw = this.svg.group()
|
||
|
||
// 节点id
|
||
this.uid = 1
|
||
|
||
// 初始化主题
|
||
this.initTheme()
|
||
|
||
// 事件类
|
||
this.event = new Event({
|
||
mindMap: this
|
||
})
|
||
|
||
// 按键类
|
||
this.keyCommand = new KeyCommand({
|
||
mindMap: this
|
||
})
|
||
|
||
// 命令类
|
||
this.command = new Command({
|
||
mindMap: this
|
||
})
|
||
|
||
// 渲染类
|
||
this.renderer = new Render({
|
||
mindMap: this
|
||
})
|
||
|
||
// 视图操作类
|
||
this.view = new View({
|
||
mindMap: this,
|
||
draw: this.draw
|
||
})
|
||
|
||
// 批量执行类
|
||
this.batchExecution = new BatchExecution()
|
||
|
||
// 注册插件
|
||
MindMap.pluginList.forEach((plugin) => {
|
||
this.initPlugin(plugin)
|
||
})
|
||
|
||
// 初始渲染
|
||
this.render()
|
||
setTimeout(() => {
|
||
this.command.addHistory()
|
||
}, 0)
|
||
}
|
||
|
||
// 配置参数处理
|
||
handleOpt(opt) {
|
||
// 检查布局配置
|
||
if (!layoutValueList.includes(opt.layout)) {
|
||
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||
}
|
||
// 检查主题配置
|
||
opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default'
|
||
return opt
|
||
}
|
||
|
||
// 渲染,部分渲染
|
||
render(callback, source = '') {
|
||
this.batchExecution.push('render', () => {
|
||
this.initTheme()
|
||
this.renderer.reRender = false
|
||
this.renderer.render(callback, source)
|
||
})
|
||
}
|
||
|
||
// 重新渲染
|
||
reRender(callback, source = '') {
|
||
this.batchExecution.push('render', () => {
|
||
this.draw.clear()
|
||
this.initTheme()
|
||
this.renderer.reRender = true
|
||
this.renderer.render(callback, source)
|
||
})
|
||
}
|
||
|
||
// 容器尺寸变化,调整尺寸
|
||
resize() {
|
||
this.elRect = this.el.getBoundingClientRect()
|
||
this.width = this.elRect.width
|
||
this.height = this.elRect.height
|
||
this.svg.size(this.width, this.height)
|
||
}
|
||
|
||
// 监听事件
|
||
on(event, fn) {
|
||
this.event.on(event, fn)
|
||
}
|
||
|
||
// 触发事件
|
||
emit(event, ...args) {
|
||
this.event.emit(event, ...args)
|
||
}
|
||
|
||
// 解绑事件
|
||
off(event, fn) {
|
||
this.event.off(event, fn)
|
||
}
|
||
|
||
// 设置主题
|
||
initTheme() {
|
||
// 合并主题配置
|
||
this.themeConfig = merge(theme[this.opt.theme], this.opt.themeConfig)
|
||
// 设置背景样式
|
||
Style.setBackgroundStyle(this.el, this.themeConfig)
|
||
}
|
||
|
||
// 设置主题
|
||
setTheme(theme) {
|
||
this.renderer.clearAllActive()
|
||
this.opt.theme = theme
|
||
this.render(null, CONSTANTS.CHANGE_THEME)
|
||
}
|
||
|
||
// 获取当前主题
|
||
getTheme() {
|
||
return this.opt.theme
|
||
}
|
||
|
||
// 设置主题配置
|
||
setThemeConfig(config) {
|
||
this.opt.themeConfig = config
|
||
// 检查改变的是否是节点大小无关的主题属性
|
||
let res = checkIsNodeSizeIndependenceConfig(config)
|
||
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
|
||
}
|
||
|
||
// 获取自定义主题配置
|
||
getCustomThemeConfig() {
|
||
return this.opt.themeConfig
|
||
}
|
||
|
||
// 获取某个主题配置值
|
||
getThemeConfig(prop) {
|
||
return prop === undefined ? this.themeConfig : this.themeConfig[prop]
|
||
}
|
||
|
||
// 获取配置
|
||
getConfig(prop) {
|
||
return prop === undefined ? this.opt : this.opt[prop]
|
||
}
|
||
|
||
// 更新配置
|
||
updateConfig(opt = {}) {
|
||
this.opt = this.handleOpt(merge.all([defaultOpt, this.opt, opt]))
|
||
}
|
||
|
||
// 获取当前布局结构
|
||
getLayout() {
|
||
return this.opt.layout
|
||
}
|
||
|
||
// 设置布局结构
|
||
setLayout(layout) {
|
||
// 检查布局配置
|
||
if (!layoutValueList.includes(layout)) {
|
||
layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||
}
|
||
this.opt.layout = layout
|
||
this.view.reset()
|
||
this.renderer.setLayout()
|
||
this.render()
|
||
}
|
||
|
||
// 执行命令
|
||
execCommand(...args) {
|
||
this.command.exec(...args)
|
||
}
|
||
|
||
// 动态设置思维导图数据,纯节点数据
|
||
setData(data) {
|
||
this.execCommand('CLEAR_ACTIVE_NODE')
|
||
this.command.clearHistory()
|
||
this.command.addHistory()
|
||
if (this.richText) {
|
||
this.renderer.renderTree = this.richText.handleSetData(data)
|
||
} else {
|
||
this.renderer.renderTree = data
|
||
}
|
||
this.reRender(() => {}, CONSTANTS.SET_DATA)
|
||
}
|
||
|
||
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
|
||
setFullData(data) {
|
||
if (data.root) {
|
||
this.setData(data.root)
|
||
}
|
||
if (data.layout) {
|
||
this.setLayout(data.layout)
|
||
}
|
||
if (data.theme) {
|
||
if (data.theme.template) {
|
||
this.setTheme(data.theme.template)
|
||
}
|
||
if (data.theme.config) {
|
||
this.setThemeConfig(data.theme.config)
|
||
}
|
||
}
|
||
if (data.view) {
|
||
this.view.setTransformData(data.view)
|
||
}
|
||
}
|
||
|
||
// 获取思维导图数据,节点树、主题、布局等
|
||
getData(withConfig) {
|
||
let nodeData = this.command.removeDataUid(this.command.getCopyData())
|
||
let data = {}
|
||
if (withConfig) {
|
||
data = {
|
||
layout: this.getLayout(),
|
||
root: nodeData,
|
||
theme: {
|
||
template: this.getTheme(),
|
||
config: this.getCustomThemeConfig()
|
||
},
|
||
view: this.view.getTransformData()
|
||
}
|
||
} else {
|
||
data = nodeData
|
||
}
|
||
return simpleDeepClone(data)
|
||
}
|
||
|
||
// 导出
|
||
async export(...args) {
|
||
let result = await this.doExport.export(...args)
|
||
return result
|
||
}
|
||
|
||
// 转换位置
|
||
toPos(x, y) {
|
||
return {
|
||
x: x - this.elRect.left,
|
||
y: y - this.elRect.top
|
||
}
|
||
}
|
||
|
||
// 设置只读模式、编辑模式
|
||
setMode(mode) {
|
||
if (![CONSTANTS.MODE.READONLY, CONSTANTS.MODE.EDIT].includes(mode)) {
|
||
return
|
||
}
|
||
this.opt.readonly = mode === CONSTANTS.MODE.READONLY
|
||
if (this.opt.readonly) {
|
||
// 取消当前激活的元素
|
||
this.renderer.clearAllActive()
|
||
}
|
||
this.emit('mode_change', mode)
|
||
}
|
||
|
||
// 获取svg数据
|
||
getSvgData({ paddingX = 0, paddingY = 0 } = {}) {
|
||
const svg = this.svg
|
||
const draw = this.draw
|
||
// 保存原始信息
|
||
const origWidth = svg.width()
|
||
const origHeight = svg.height()
|
||
const origTransform = draw.transform()
|
||
const elRect = this.el.getBoundingClientRect()
|
||
// 去除放大缩小的变换效果
|
||
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
|
||
// 获取变换后的位置尺寸信息,其实是getBoundingClientRect方法的包装方法
|
||
const rect = draw.rbox()
|
||
// 内边距
|
||
rect.width += paddingX
|
||
rect.height += paddingY
|
||
draw.translate(paddingX / 2, paddingY / 2)
|
||
// 将svg设置为实际内容的宽高
|
||
svg.size(rect.width, rect.height)
|
||
// 把实际内容变换
|
||
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
|
||
// 克隆一份数据
|
||
let clone = svg.clone()
|
||
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
|
||
if ((rect.width > origWidth || rect.height > origHeight) && this.watermark && this.watermark.hasWatermark()) {
|
||
this.width = rect.width
|
||
this.height = rect.height
|
||
this.watermark.draw()
|
||
clone = svg.clone()
|
||
this.width = origWidth
|
||
this.height = origHeight
|
||
this.watermark.draw()
|
||
}
|
||
// 恢复原先的大小和变换信息
|
||
svg.size(origWidth, origHeight)
|
||
draw.transform(origTransform)
|
||
|
||
return {
|
||
svg: clone, // 思维导图图形的整体svg元素,包括:svg(画布容器)、g(实际的思维导图组)
|
||
svgHTML: clone.svg(), // svg字符串
|
||
rect: {
|
||
...rect, // 思维导图图形未缩放时的位置尺寸等信息
|
||
ratio: rect.width / rect.height // 思维导图图形的宽高比
|
||
},
|
||
origWidth, // 画布宽度
|
||
origHeight, // 画布高度
|
||
scaleX: origTransform.scaleX, // 思维导图图形的水平缩放值
|
||
scaleY: origTransform.scaleY // 思维导图图形的垂直缩放值
|
||
}
|
||
}
|
||
|
||
// 添加插件
|
||
addPlugin(plugin, opt) {
|
||
let index = MindMap.hasPlugin(plugin)
|
||
if (index === -1) {
|
||
MindMap.usePlugin(plugin, opt)
|
||
this.initPlugin(plugin)
|
||
}
|
||
}
|
||
|
||
// 移除插件
|
||
removePlugin(plugin) {
|
||
let index = MindMap.hasPlugin(plugin)
|
||
if (index !== -1) {
|
||
MindMap.pluginList.splice(index, 1)
|
||
if (this[plugin.instanceName]) {
|
||
if (this[plugin.instanceName].beforePluginRemove) {
|
||
this[plugin.instanceName].beforePluginRemove()
|
||
}
|
||
delete this[plugin.instanceName]
|
||
}
|
||
}
|
||
}
|
||
|
||
// 实例化插件
|
||
initPlugin(plugin) {
|
||
this[plugin.instanceName] = new plugin({
|
||
mindMap: this,
|
||
pluginOpt: plugin.pluginOpt
|
||
})
|
||
}
|
||
}
|
||
|
||
// 插件列表
|
||
MindMap.pluginList = []
|
||
MindMap.usePlugin = (plugin, opt = {}) => {
|
||
plugin.pluginOpt = opt
|
||
MindMap.pluginList.push(plugin)
|
||
return MindMap
|
||
}
|
||
MindMap.hasPlugin = (plugin) => {
|
||
return MindMap.pluginList.findIndex((item) => {
|
||
return item === plugin
|
||
})
|
||
}
|
||
|
||
// 定义新主题
|
||
MindMap.defineTheme = (name, config = {}) => {
|
||
if (theme[name]) {
|
||
return new Error('该主题名称已存在')
|
||
}
|
||
theme[name] = merge(defaultTheme, config)
|
||
}
|
||
|
||
export default MindMap
|