723 lines
20 KiB
TypeScript
723 lines
20 KiB
TypeScript
|
|
import View from './core/view/View'
|
|||
|
|
import Event from './core/event/Event'
|
|||
|
|
import Render from './core/render/Render'
|
|||
|
|
import merge from 'deepmerge'
|
|||
|
|
import { theme, ThemeConfig } from './theme'
|
|||
|
|
import Style from './core/render/node/Style'
|
|||
|
|
import KeyCommand from './core/command/KeyCommand'
|
|||
|
|
import Command from './core/command/Command'
|
|||
|
|
import BatchExecution from './utils/BatchExecution'
|
|||
|
|
import {
|
|||
|
|
layoutValueList,
|
|||
|
|
CONSTANTS,
|
|||
|
|
ERROR_TYPES,
|
|||
|
|
cssContent
|
|||
|
|
} from './constants/constant'
|
|||
|
|
import { SVG } from '@svgdotjs/svg.js'
|
|||
|
|
import {
|
|||
|
|
simpleDeepClone,
|
|||
|
|
getObjectChangedProps,
|
|||
|
|
isUndef,
|
|||
|
|
handleGetSvgDataExtraContent,
|
|||
|
|
getNodeTreeBoundingRect,
|
|||
|
|
mergeTheme
|
|||
|
|
} from './utils'
|
|||
|
|
import {
|
|||
|
|
defaultTheme,
|
|||
|
|
checkIsNodeSizeIndependenceConfig
|
|||
|
|
} from './theme'
|
|||
|
|
import { defaultOpt } from './constants/defaultOptions'
|
|||
|
|
import { Node } from "./types"
|
|||
|
|
|
|||
|
|
// 思维导图
|
|||
|
|
interface MindMapOptions {
|
|||
|
|
el?: HTMLElement;
|
|||
|
|
data?: Node;
|
|||
|
|
layout?: string;
|
|||
|
|
theme?: string;
|
|||
|
|
themeConfig?: ThemeConfig;
|
|||
|
|
fit?: boolean;
|
|||
|
|
addHistoryOnInit?: boolean;
|
|||
|
|
associativeLineIsAlwaysAboveNode?: boolean;
|
|||
|
|
[key: string]: any;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export class MindMap {
|
|||
|
|
static instanceCount: number = 0;
|
|||
|
|
static pluginList: any[] = [];
|
|||
|
|
public opt: MindMapOptions;
|
|||
|
|
private el: HTMLElement;
|
|||
|
|
private width: number;
|
|||
|
|
private height: number;
|
|||
|
|
private initWidth: number;
|
|||
|
|
private initHeight: number;
|
|||
|
|
private cssEl: HTMLStyleElement | null;
|
|||
|
|
private cssTextMap: Record<string, string>;
|
|||
|
|
private nodeInnerPrefixList: string[];
|
|||
|
|
private svg: any; // SVG.js type
|
|||
|
|
private draw: any;
|
|||
|
|
private lineDraw: any;
|
|||
|
|
private nodeDraw: any;
|
|||
|
|
private associativeLineDraw: any;
|
|||
|
|
private otherDraw: any;
|
|||
|
|
private event: Event;
|
|||
|
|
private keyCommand: KeyCommand;
|
|||
|
|
private command: Command;
|
|||
|
|
private renderer: Render;
|
|||
|
|
private view: View;
|
|||
|
|
private batchExecution: BatchExecution;
|
|||
|
|
private elRect: DOMRect;
|
|||
|
|
private doExport?: any;
|
|||
|
|
private themeConfig?: ThemeConfig
|
|||
|
|
private demonstrate?: any
|
|||
|
|
private watermark?: any
|
|||
|
|
private commonCaches?: any
|
|||
|
|
public richText?: any
|
|||
|
|
static usePlugin = (plugin: any, opt = {}) => {
|
|||
|
|
if (MindMap.hasPlugin(plugin) !== -1) return MindMap
|
|||
|
|
plugin.pluginOpt = opt
|
|||
|
|
MindMap.pluginList.push(plugin)
|
|||
|
|
return MindMap
|
|||
|
|
}
|
|||
|
|
static hasPlugin = (plugin: any) => {
|
|||
|
|
return MindMap.pluginList.findIndex(item => {
|
|||
|
|
return item === plugin
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
// 定义新主题
|
|||
|
|
static defineTheme = (name: string, config = {}) => {
|
|||
|
|
if ((theme as any)[name]) {
|
|||
|
|
return new Error('该主题名称已存在')
|
|||
|
|
}
|
|||
|
|
(theme as any)[name] = mergeTheme(defaultTheme, config)
|
|||
|
|
}
|
|||
|
|
// 移除主题
|
|||
|
|
static removeTheme = (name: string) => {
|
|||
|
|
if ((theme as any)[name]) {
|
|||
|
|
(theme as any)[name] = null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
constructor(opt: Partial<MindMapOptions> = {}) {
|
|||
|
|
MindMap.instanceCount++;
|
|||
|
|
// Merge options
|
|||
|
|
this.opt = this.handleOpt(merge(defaultOpt, opt));
|
|||
|
|
// Preprocess node data
|
|||
|
|
this.opt.data = this.handleData(this.opt.data);
|
|||
|
|
|
|||
|
|
// Container element
|
|||
|
|
this.el = this.opt.el;
|
|||
|
|
console.log(opt)
|
|||
|
|
if (!this.el) throw new Error('Missing container element el');
|
|||
|
|
|
|||
|
|
// Get container size position information
|
|||
|
|
this.getElRectInfo();
|
|||
|
|
|
|||
|
|
// Initial canvas size
|
|||
|
|
this.initWidth = this.width;
|
|||
|
|
this.initHeight = this.height;
|
|||
|
|
|
|||
|
|
// Required CSS styles
|
|||
|
|
this.cssEl = null;
|
|||
|
|
this.cssTextMap = {};
|
|||
|
|
this.nodeInnerPrefixList = [];
|
|||
|
|
|
|||
|
|
// Initialize canvas
|
|||
|
|
this.initContainer();
|
|||
|
|
|
|||
|
|
// Initialize theme
|
|||
|
|
this.initTheme();
|
|||
|
|
|
|||
|
|
// Initialize cache data
|
|||
|
|
this.initCache();
|
|||
|
|
|
|||
|
|
// Event class
|
|||
|
|
this.event = new Event({
|
|||
|
|
mindMap: this
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Key command class
|
|||
|
|
this.keyCommand = new KeyCommand({
|
|||
|
|
mindMap: this
|
|||
|
|
});
|
|||
|
|
// Command class
|
|||
|
|
this.command = new Command({
|
|||
|
|
mindMap: this
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Render class
|
|||
|
|
this.renderer = new Render({
|
|||
|
|
mindMap: this
|
|||
|
|
});
|
|||
|
|
// View operation class
|
|||
|
|
this.view = new View({
|
|||
|
|
mindMap: this
|
|||
|
|
});
|
|||
|
|
// Batch execution class
|
|||
|
|
this.batchExecution = new BatchExecution();
|
|||
|
|
// Register plugins
|
|||
|
|
MindMap.pluginList.forEach(plugin => {
|
|||
|
|
this.initPlugin(plugin);
|
|||
|
|
});
|
|||
|
|
// Add required CSS styles
|
|||
|
|
this.addCss();
|
|||
|
|
|
|||
|
|
// Initial render
|
|||
|
|
this.render(this.opt.fit ? () => this.view.fit() : () => { });
|
|||
|
|
|
|||
|
|
// Add initial data to history stack
|
|||
|
|
if (this.opt.addHistoryOnInit && this.opt.data) {
|
|||
|
|
this.command.addHistory();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private handleOpt(opt: MindMapOptions): MindMapOptions {
|
|||
|
|
// Check layout configuration
|
|||
|
|
if (!layoutValueList.includes(opt.layout)) {
|
|||
|
|
opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE;
|
|||
|
|
}
|
|||
|
|
// Check theme configuration
|
|||
|
|
opt.theme = opt.theme && (theme as any)[opt.theme] ? opt.theme : 'default';
|
|||
|
|
return opt;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private handleData(data: any): any {
|
|||
|
|
if (isUndef(data) || Object.keys(data).length <= 0) return null;
|
|||
|
|
data = simpleDeepClone(data || {});
|
|||
|
|
// Root node cannot be collapsed
|
|||
|
|
if (data.data && !data.data.expand) {
|
|||
|
|
data.data.expand = true;
|
|||
|
|
}
|
|||
|
|
return data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private initContainer(): void {
|
|||
|
|
const { associativeLineIsAlwaysAboveNode } = this.opt;
|
|||
|
|
// Add a class name to the container element
|
|||
|
|
this.el.classList.add('smm-mind-map-container');
|
|||
|
|
|
|||
|
|
const createAssociativeLineDraw = (): void => {
|
|||
|
|
this.associativeLineDraw = this.draw.group();
|
|||
|
|
this.associativeLineDraw.addClass('smm-associative-line-container');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Canvas
|
|||
|
|
this.svg = SVG().addTo(this.el).size(this.width, this.height);
|
|||
|
|
|
|||
|
|
// Container
|
|||
|
|
this.draw = this.svg.group();
|
|||
|
|
this.draw.addClass('smm-container');
|
|||
|
|
// Node connection line container
|
|||
|
|
this.lineDraw = this.draw.group();
|
|||
|
|
this.lineDraw.addClass('smm-line-container');
|
|||
|
|
// Default below nodes
|
|||
|
|
if (!associativeLineIsAlwaysAboveNode) {
|
|||
|
|
createAssociativeLineDraw();
|
|||
|
|
}
|
|||
|
|
// Node container
|
|||
|
|
this.nodeDraw = this.draw.group();
|
|||
|
|
this.nodeDraw.addClass('smm-node-container');
|
|||
|
|
// Association lines always above nodes
|
|||
|
|
if (associativeLineIsAlwaysAboveNode) {
|
|||
|
|
createAssociativeLineDraw();
|
|||
|
|
}
|
|||
|
|
// Container for other content
|
|||
|
|
this.otherDraw = this.draw.group();
|
|||
|
|
this.otherDraw.addClass('smm-other-container');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清空各容器
|
|||
|
|
clearDraw() {
|
|||
|
|
this.lineDraw.clear()
|
|||
|
|
this.associativeLineDraw.clear()
|
|||
|
|
this.nodeDraw.clear()
|
|||
|
|
this.otherDraw.clear()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// Class methods with type annotations
|
|||
|
|
appendCss(key: string, str: string): void {
|
|||
|
|
this.cssTextMap[key] = str;
|
|||
|
|
this.removeCss();
|
|||
|
|
this.addCss();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
removeAppendCss(key: string): void {
|
|||
|
|
if (this.cssTextMap[key]) {
|
|||
|
|
delete this.cssTextMap[key];
|
|||
|
|
this.removeCss();
|
|||
|
|
this.addCss();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
joinCss(): string {
|
|||
|
|
return (
|
|||
|
|
cssContent +
|
|||
|
|
Object.keys(this.cssTextMap)
|
|||
|
|
.map(key => this.cssTextMap[key])
|
|||
|
|
.join('\n')
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addCss(): void {
|
|||
|
|
this.cssEl = document.createElement('style');
|
|||
|
|
this.cssEl.type = 'text/css';
|
|||
|
|
this.cssEl.innerHTML = this.joinCss();
|
|||
|
|
document.head.appendChild(this.cssEl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
removeCss(): void {
|
|||
|
|
if (this.cssEl) document.head.removeChild(this.cssEl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
render(callback?: (() => void) | null, source: string = ''): void {
|
|||
|
|
this.batchExecution.push('render', () => {
|
|||
|
|
this.initTheme();
|
|||
|
|
this.renderer.render(callback, source);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
reRender(callback?: (() => void) | null, source: string = ''): void {
|
|||
|
|
this.renderer.reRender = true;
|
|||
|
|
this.renderer.clearCache();
|
|||
|
|
this.clearDraw();
|
|||
|
|
this.render(callback, source);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getElRectInfo(): void {
|
|||
|
|
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(): void {
|
|||
|
|
const oldWidth = this.width;
|
|||
|
|
const oldHeight = this.height;
|
|||
|
|
this.getElRectInfo();
|
|||
|
|
this.svg.size(this.width, this.height);
|
|||
|
|
if (oldWidth !== this.width || oldHeight !== this.height) {
|
|||
|
|
if (this.demonstrate) {
|
|||
|
|
if (!this.demonstrate.isInDemonstrate) {
|
|||
|
|
this.render();
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
this.render();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
this.emit('resize');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
on(event: string, fn: (...args: any[]) => void): void {
|
|||
|
|
this.event.on(event, fn);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
emit(event: string, ...args: any[]): void {
|
|||
|
|
this.event.emit(event, ...args);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
off(event: string, fn: (...args: any[]) => void): void {
|
|||
|
|
this.event.off(event, fn);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
initCache(): void {
|
|||
|
|
this.commonCaches = {
|
|||
|
|
measureCustomNodeContentSizeEl: null,
|
|||
|
|
measureRichtextNodeTextSizeEl: null
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
initTheme(): void {
|
|||
|
|
this.themeConfig = mergeTheme(
|
|||
|
|
(theme as any)[this.opt.theme] || theme.default,
|
|||
|
|
this.opt.themeConfig
|
|||
|
|
);
|
|||
|
|
Style.setBackgroundStyle(this.el, this.themeConfig);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setTheme(theme: string, notRender: boolean = false): void {
|
|||
|
|
this.execCommand('CLEAR_ACTIVE_NODE');
|
|||
|
|
this.opt.theme = theme;
|
|||
|
|
if (!notRender) {
|
|||
|
|
this.render(null, CONSTANTS.CHANGE_THEME);
|
|||
|
|
}
|
|||
|
|
this.emit('view_theme_change', theme);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getTheme(): string {
|
|||
|
|
return this.opt.theme;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setThemeConfig(config: ThemeConfig, notRender: boolean = false): void {
|
|||
|
|
const changedConfig = getObjectChangedProps(this.themeConfig, config);
|
|||
|
|
this.opt.themeConfig = config;
|
|||
|
|
if (!notRender) {
|
|||
|
|
const res = checkIsNodeSizeIndependenceConfig(changedConfig);
|
|||
|
|
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 获取自定义主题配置
|
|||
|
|
getCustomThemeConfig() {
|
|||
|
|
return this.opt.themeConfig
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取某个主题配置值
|
|||
|
|
getThemeConfig(prop?: keyof ThemeConfig): ThemeConfig | any {
|
|||
|
|
return prop === undefined ? this.themeConfig : this.themeConfig[prop];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取配置
|
|||
|
|
getConfig(prop?: keyof MindMapOptions): MindMapOptions | any {
|
|||
|
|
return prop === undefined ? this.opt : this.opt[prop];
|
|||
|
|
}
|
|||
|
|
// 更新配置
|
|||
|
|
updateConfig(opt: Partial<MindMapOptions> = {}) {
|
|||
|
|
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)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取当前布局结构
|
|||
|
|
getLayout() {
|
|||
|
|
return this.opt.layout
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置布局结构
|
|||
|
|
setLayout(layout: any, 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: [name: string, ...args: any[]]) {
|
|||
|
|
this.command.exec(...args)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新画布数据,如果新的数据是在当前画布节点数据基础上增删改查后形成的,那么可以使用该方法来更新画布数据
|
|||
|
|
updateData(data: any) {
|
|||
|
|
this.emit('before_update_data', data)
|
|||
|
|
this.renderer.setData(data)
|
|||
|
|
this.render()
|
|||
|
|
this.command.addHistory()
|
|||
|
|
this.emit('update_data', data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 动态设置思维导图数据,纯节点数据
|
|||
|
|
setData(data: any) {
|
|||
|
|
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(() => { }, CONSTANTS.SET_DATA)
|
|||
|
|
this.emit('set_data', data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 动态设置思维导图数据,包括节点数据、布局、主题、视图
|
|||
|
|
setFullData(data: any) {
|
|||
|
|
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?: boolean): any {
|
|||
|
|
const nodeData = this.command.getCopyData();
|
|||
|
|
let data: Record<string, unknown> = {};
|
|||
|
|
|
|||
|
|
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: any) {
|
|||
|
|
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: number, y: number): { x: number; y: number } {
|
|||
|
|
return {
|
|||
|
|
x: x - this.elRect.left,
|
|||
|
|
y: y - this.elRect.top
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 设置只读模式、编辑模式
|
|||
|
|
setMode(mode: any) {
|
|||
|
|
if (![CONSTANTS.MODE.READONLY, CONSTANTS.MODE.EDIT].includes(mode)) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
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.execCommand('CLEAR_ACTIVE_NODE')
|
|||
|
|
}
|
|||
|
|
this.opt.readonly = isReadonly
|
|||
|
|
// 切换为编辑模式时,如果历史记录堆栈是空的,那么进行一次入栈操作
|
|||
|
|
if (!isReadonly && this.command.history.length <= 0) {
|
|||
|
|
this.command.originAddHistory()
|
|||
|
|
}
|
|||
|
|
this.emit('mode_change', mode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取svg数据
|
|||
|
|
getSvgData({
|
|||
|
|
paddingX = 0,
|
|||
|
|
paddingY = 0,
|
|||
|
|
ignoreWatermark = false,
|
|||
|
|
addContentToHeader,
|
|||
|
|
addContentToFooter,
|
|||
|
|
node
|
|||
|
|
}: {
|
|||
|
|
paddingX?: number;
|
|||
|
|
paddingY?: number;
|
|||
|
|
ignoreWatermark?: boolean;
|
|||
|
|
addContentToHeader?: () => void;
|
|||
|
|
addContentToFooter?: () => void;
|
|||
|
|
node?: any;
|
|||
|
|
} = {}) {
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
// 添加必要的样式
|
|||
|
|
[this.joinCss(), ...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: any, defIndex: number) => {
|
|||
|
|
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: any, opt: any) {
|
|||
|
|
let index = MindMap.hasPlugin(plugin)
|
|||
|
|
if (index === -1) {
|
|||
|
|
MindMap.usePlugin(plugin, opt)
|
|||
|
|
}
|
|||
|
|
this.initPlugin(plugin)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移除插件
|
|||
|
|
removePlugin(plugin: any) {
|
|||
|
|
let index = MindMap.hasPlugin(plugin)
|
|||
|
|
if (index !== -1) {
|
|||
|
|
MindMap.pluginList.splice(index, 1)
|
|||
|
|
if ((this as any)[plugin.instanceName]) {
|
|||
|
|
if ((this as any)[plugin.instanceName].beforePluginRemove) {
|
|||
|
|
(this as any)[plugin.instanceName].beforePluginRemove()
|
|||
|
|
}
|
|||
|
|
delete (this as any)[plugin.instanceName]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 实例化插件
|
|||
|
|
initPlugin(plugin: any) {
|
|||
|
|
if ((this as any)[plugin.instanceName]) return
|
|||
|
|
(this as any)[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 as any)[plugin.instanceName] &&
|
|||
|
|
(this as any)[plugin.instanceName].beforePluginDestroy
|
|||
|
|
) {
|
|||
|
|
(this as any)[plugin.instanceName].beforePluginDestroy()
|
|||
|
|
}
|
|||
|
|
(this as any)[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.instanceCount--
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
export * from "./types"
|
|||
|
|
|