mind_map/simple-mind-map/index.js
2024-08-23 15:48:04 +08:00

638 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/themes'
import Style from './src/core/render/node/Style'
import KeyCommand from './src/core/command/KeyCommand'
import Command from './src/core/command/Command'
import BatchExecution from './src/utils/BatchExecution'
import {
layoutValueList,
CONSTANTS,
ERROR_TYPES,
cssContent
} from './src/constants/constant'
import { SVG } from '@svgdotjs/svg.js'
import {
simpleDeepClone,
getObjectChangedProps,
isUndef,
handleGetSvgDataExtraContent,
getNodeTreeBoundingRect
} from './src/utils'
import defaultTheme, {
checkIsNodeSizeIndependenceConfig
} from './src/themes/default'
import { defaultOpt } from './src/constants/defaultOptions'
// 思维导图
class MindMap {
// 构造函数
/**
*
* @param {defaultOpt} opt
*/
constructor(opt = {}) {
// 合并选项
this.opt = this.handleOpt(merge(defaultOpt, opt))
// 预处理节点数据
this.opt.data = this.handleData(this.opt.data)
// 容器元素
this.el = this.opt.el
if (!this.el) throw new Error('缺少容器元素el')
// 获取容器尺寸位置信息
this.getElRectInfo()
// 画布初始大小
this.initWidth = this.width
this.initHeight = this.height
// 添加css
this.cssEl = null
this.addCss()
// 画布
this.initContainer()
// 初始化主题
this.initTheme()
// 初始化缓存数据
this.initCache()
// 事件类
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
})
// 批量执行类
this.batchExecution = new BatchExecution()
// 注册插件
MindMap.pluginList.forEach(plugin => {
this.initPlugin(plugin)
})
// 初始渲染
this.render(this.opt.fit ? () => this.view.fit() : () => {})
setTimeout(() => {
if (this.opt.data) 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
}
// 预处理节点数据
handleData(data) {
if (isUndef(data) || Object.keys(data).length <= 0) return null
data = simpleDeepClone(data || {})
// 根节点不能收起
if (data.data && !data.data.expand) {
data.data.expand = true
}
return data
}
// 创建容器元素
initContainer() {
const { associativeLineIsAlwaysAboveNode } = this.opt
// 给容器元素添加一个类名
this.el.classList.add('smm-mind-map-container')
// 节点关联线容器
const createAssociativeLineDraw = () => {
this.associativeLineDraw = this.draw.group()
this.associativeLineDraw.addClass('smm-associative-line-container')
}
// 画布
this.svg = SVG().addTo(this.el).size(this.width, this.height)
// 容器
this.draw = this.svg.group()
this.draw.addClass('smm-container')
// 节点连线容器
this.lineDraw = this.draw.group()
this.lineDraw.addClass('smm-line-container')
// 默认处于节点下方
if (!associativeLineIsAlwaysAboveNode) {
createAssociativeLineDraw()
}
// 节点容器
this.nodeDraw = this.draw.group()
this.nodeDraw.addClass('smm-node-container')
// 关联线始终处于节点上方
if (associativeLineIsAlwaysAboveNode) {
createAssociativeLineDraw()
}
// 其他内容的容器
this.otherDraw = this.draw.group()
this.otherDraw.addClass('smm-other-container')
}
// 清空各容器
clearDraw() {
this.lineDraw.clear()
this.associativeLineDraw.clear()
this.nodeDraw.clear()
this.otherDraw.clear()
}
// 添加必要的css样式到页面
addCss() {
this.cssEl = document.createElement('style')
this.cssEl.type = 'text/css'
this.cssEl.innerHTML = cssContent
document.head.appendChild(this.cssEl)
}
// 移除css
removeCss() {
document.head.removeChild(this.cssEl)
}
// 渲染,部分渲染
render(callback, source = '') {
this.batchExecution.push('render', () => {
this.initTheme()
this.renderer.render(callback, source)
})
}
// 重新渲染
reRender(callback, source = '') {
this.renderer.reRender = true // 标记为重新渲染
this.renderer.clearCache() // 清空节点缓存池
this.clearDraw() // 清空画布
this.render(callback, source)
}
// 获取或更新容器尺寸位置信息
getElRectInfo() {
this.elRect = this.el.getBoundingClientRect()
this.width = this.elRect.width
this.height = this.elRect.height
if (this.width <= 0 || this.height <= 0)
throw new Error('容器元素el的宽高不能为0')
}
// 容器尺寸变化,调整尺寸
resize() {
const oldWidth = this.width
const oldHeight = this.height
this.getElRectInfo()
this.svg.size(this.width, this.height)
if (oldWidth !== this.width || oldHeight !== this.height) {
// 如果画布宽高改变了需要触发一次渲染
this.render()
}
this.emit('resize')
}
// 监听事件
on(event, fn) {
this.event.on(event, fn)
}
// 触发事件
emit(event, ...args) {
this.event.emit(event, ...args)
}
// 解绑事件
off(event, fn) {
this.event.off(event, fn)
}
// 初始化缓存数据
initCache() {
this.commonCaches = {
measureCustomNodeContentSizeEl: null,
measureRichtextNodeTextSizeEl: null
}
}
// 设置主题
initTheme() {
// 合并主题配置
this.themeConfig = merge(theme[this.opt.theme], this.opt.themeConfig)
// 设置背景样式
Style.setBackgroundStyle(this.el, this.themeConfig)
}
// 设置主题
setTheme(theme, notRender = false) {
this.execCommand('CLEAR_ACTIVE_NODE')
this.opt.theme = theme
if (!notRender) {
this.render(null, CONSTANTS.CHANGE_THEME)
}
this.emit('view_theme_change', theme)
}
// 获取当前主题
getTheme() {
return this.opt.theme
}
// 设置主题配置
setThemeConfig(config, notRender = false) {
// 计算改变了的配置
const changedConfig = getObjectChangedProps(this.themeConfig, config)
this.opt.themeConfig = config
if (!notRender) {
// 检查改变的是否是节点大小无关的主题属性
let res = checkIsNodeSizeIndependenceConfig(changedConfig)
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.emit('before_update_config', this.opt)
this.opt = this.handleOpt(merge.all([defaultOpt, this.opt, opt]))
this.emit('after_update_config', this.opt)
}
// 获取当前布局结构
getLayout() {
return this.opt.layout
}
// 设置布局结构
setLayout(layout, notRender = false) {
// 检查布局配置
if (!layoutValueList.includes(layout)) {
layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
}
this.opt.layout = layout
this.view.reset()
this.renderer.setLayout()
if (!notRender) {
this.render(null, CONSTANTS.CHANGE_LAYOUT)
}
this.emit('layout_change', layout)
}
// 执行命令
execCommand(...args) {
this.command.exec(...args)
}
// 更新画布数据,如果新的数据是在当前画布节点数据基础上增删改查后形成的,那么可以使用该方法来更新画布数据
updateData(data) {
this.renderer.setData(data)
this.render()
this.command.addHistory()
}
// 动态设置思维导图数据,纯节点数据
setData(data) {
data = this.handleData(data)
this.opt.data = data
this.execCommand('CLEAR_ACTIVE_NODE')
this.command.clearHistory()
this.command.addHistory()
this.renderer.setData(data)
this.reRender(() => {}, CONSTANTS.SET_DATA)
this.emit('set_data', 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.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) {
try {
if (!this.doExport) {
throw new Error('请注册Export插件')
}
let result = await this.doExport.export(...args)
return result
} catch (error) {
this.opt.errorHandler(ERROR_TYPES.EXPORT_ERROR, error)
}
}
// 转换位置
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
}
const isReadonly = mode === CONSTANTS.MODE.READONLY
if (isReadonly === this.opt.readonly) return
this.opt.readonly = isReadonly
if (this.opt.readonly) {
// 取消当前激活的元素
this.execCommand('CLEAR_ACTIVE_NODE')
}
this.emit('mode_change', mode)
}
// 获取svg数据
getSvgData({
paddingX = 0,
paddingY = 0,
ignoreWatermark = false,
addContentToHeader,
addContentToFooter,
node
} = {}) {
const { watermarkConfig, openPerformance } = this.opt
// 如果开启了性能模式,那么需要先渲染所有节点
if (openPerformance) {
this.renderer.forceLoadNode(node)
}
const { cssTextList, header, headerHeight, footer, footerHeight } =
handleGetSvgDataExtraContent({
addContentToHeader,
addContentToFooter
})
const svg = this.svg
const draw = this.draw
// 保存原始信息
const origWidth = svg.width()
const origHeight = svg.height()
const origTransform = draw.transform()
const elRect = this.elRect
// 去除放大缩小的变换效果
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 需要裁减的区域
let clipData = null
if (node) {
clipData = getNodeTreeBoundingRect(
node,
rect.x,
rect.y,
paddingX,
paddingY
)
}
// 内边距
const fixHeight = 0
rect.width += paddingX * 2
rect.height += paddingY * 2 + fixHeight + headerHeight + footerHeight
draw.translate(paddingX, paddingY)
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
// 克隆一份数据
let clone = svg.clone()
// 是否存在水印
const hasWatermark = this.watermark && this.watermark.hasWatermark()
if (!ignoreWatermark && hasWatermark) {
this.watermark.isInExport = true
// 是否是仅导出时需要水印
const { onlyExport } = watermarkConfig
// 是否需要重新绘制水印
const needReDrawWatermark =
rect.width > origWidth || rect.height > origHeight
// 如果实际图形宽高超出了屏幕宽高,且存在水印的话需要重新绘制水印,否则会出现超出部分没有水印的问题
if (needReDrawWatermark) {
this.width = rect.width
this.height = rect.height
this.watermark.onResize()
clone = svg.clone()
this.width = origWidth
this.height = origHeight
this.watermark.onResize()
} else if (onlyExport) {
// 如果是仅导出时需要水印,那么需要进行绘制
this.watermark.onResize()
clone = svg.clone()
}
// 如果是仅导出时需要水印,需要清除
if (onlyExport) {
this.watermark.clear()
}
this.watermark.isInExport = false
}
// 添加必要的样式
;[cssContent, ...cssTextList].forEach(s => {
clone.add(SVG(`<style>${s}</style>`))
})
// 附加内容
if (header && headerHeight > 0) {
clone.findOne('.smm-container').translate(0, headerHeight)
header.width(rect.width)
header.y(paddingY)
clone.add(header, 0)
}
if (footer && footerHeight > 0) {
footer.width(rect.width)
footer.y(rect.height - paddingY - footerHeight)
clone.add(footer)
}
// 修正defs里定义的元素的id因为clone时defs里的元素的id会继续递增导致和内容中引用的id对不上
const defs = svg.find('defs')
const defs2 = clone.find('defs')
defs.forEach((def, defIndex) => {
const def2 = defs2[defIndex]
if (!def2) return
const children = def.children()
const children2 = def2.children()
for (let i = 0; i < children.length; i++) {
const child = children[i]
const child2 = children2[i]
if (child && child2) {
child2.attr('id', child.attr('id'))
}
}
})
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight)
draw.transform(origTransform)
return {
svg: clone, // 思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组
svgHTML: clone.svg(), // svg字符串
clipData,
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
})
}
// 销毁
destroy() {
this.emit('beforeDestroy')
// 清除节点编辑框
this.renderer.textEdit.hideEditTextBox()
this.renderer.textEdit.removeTextEditEl()
// 移除插件
;[...MindMap.pluginList].forEach(plugin => {
if (
this[plugin.instanceName] &&
this[plugin.instanceName].beforePluginDestroy
) {
this[plugin.instanceName].beforePluginDestroy()
}
this[plugin.instanceName] = null
})
// 解绑事件
this.event.unbind()
// 移除画布节点
this.svg.remove()
// 去除给容器元素设置的背景样式
Style.removeBackgroundStyle(this.el)
// 移除给容器元素添加的类名
this.el.classList.remove('smm-mind-map-container')
this.el.innerHTML = ''
this.el = null
this.removeCss()
}
}
// 插件列表
MindMap.pluginList = []
MindMap.usePlugin = (plugin, opt = {}) => {
if (MindMap.hasPlugin(plugin) !== -1) return MindMap
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