UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

1,869 lines (1,578 loc) 45.6 kB
/* eslint-disable no-underscore-dangle */ import { NonUndefined } from 'utility-types' import { ArrayExt, StringExt, ObjectExt, FunctionExt } from '../util' import { Rectangle, Point } from '../geometry' import { KeyValue, Size } from '../types' import { Knob } from '../addon/knob' import { Basecoat } from '../common' import { Attr } from '../registry' import { Markup, CellView } from '../view' import { Graph } from '../graph' import { Model } from './model' import { Animation } from './animation' import { PortManager } from './port' import { Store } from './store' import { Node } from './node' import { Edge } from './edge' export class Cell< Properties extends Cell.Properties = Cell.Properties, > extends Basecoat<Cell.EventArgs> { // #region static protected static markup: Markup protected static defaults: Cell.Defaults = {} protected static attrHooks: Attr.Definitions = {} protected static propHooks: Cell.PropHook[] = [] public static config<C extends Cell.Config = Cell.Config>(presets: C) { const { markup, propHooks, attrHooks, ...others } = presets if (markup != null) { this.markup = markup } if (propHooks) { this.propHooks = this.propHooks.slice() if (Array.isArray(propHooks)) { this.propHooks.push(...propHooks) } else if (typeof propHooks === 'function') { this.propHooks.push(propHooks) } else { Object.keys(propHooks).forEach((name) => { const hook = propHooks[name] if (typeof hook === 'function') { this.propHooks.push(hook) } }) } } if (attrHooks) { this.attrHooks = { ...this.attrHooks, ...attrHooks } } this.defaults = ObjectExt.merge({}, this.defaults, others) } public static getMarkup() { return this.markup } public static getDefaults<T extends Cell.Defaults = Cell.Defaults>( raw?: boolean, ): T { return (raw ? this.defaults : ObjectExt.cloneDeep(this.defaults)) as T } public static getAttrHooks() { return this.attrHooks } public static applyPropHooks( cell: Cell, metadata: Cell.Metadata, ): Cell.Metadata { return this.propHooks.reduce((memo, hook) => { return hook ? FunctionExt.call(hook, cell, memo) : memo }, metadata) } // #endregion protected get [Symbol.toStringTag]() { return Cell.toStringTag } public readonly id: string protected readonly store: Store<Cell.Properties> protected readonly animation: Animation protected _model: Model | null // eslint-disable-line protected _parent: Cell | null // eslint-disable-line protected _children: Cell[] | null // eslint-disable-line constructor(metadata: Cell.Metadata = {}) { super() const ctor = this.constructor as typeof Cell const defaults = ctor.getDefaults(true) const props = ObjectExt.merge( {}, this.preprocess(defaults), this.preprocess(metadata), ) this.id = props.id || StringExt.uuid() this.store = new Store(props) this.animation = new Animation(this) this.setup() this.init() this.postprocess(metadata) } init() {} // #region model get model() { return this._model } set model(model: Model | null) { if (this._model !== model) { this._model = model } } // #endregion protected preprocess( metadata: Cell.Metadata, ignoreIdCheck?: boolean, ): Properties { const id = metadata.id const ctor = this.constructor as typeof Cell const props = ctor.applyPropHooks(this, metadata) if (id == null && ignoreIdCheck !== true) { props.id = StringExt.uuid() } return props as Properties } protected postprocess(metadata: Cell.Metadata) {} // eslint-disable-line protected setup() { this.store.on('change:*', (metadata) => { const { key, current, previous, options } = metadata this.notify('change:*', { key, options, current, previous, cell: this, }) this.notify(`change:${key}` as keyof Cell.EventArgs, { options, current, previous, cell: this, }) const type = key as Edge.TerminalType if (type === 'source' || type === 'target') { this.notify(`change:terminal`, { type, current, previous, options, cell: this, }) } }) this.store.on('changed', ({ options }) => this.notify('changed', { options, cell: this }), ) } notify<Key extends keyof Cell.EventArgs>( name: Key, args: Cell.EventArgs[Key], ): this notify(name: Exclude<string, keyof Cell.EventArgs>, args: any): this notify<Key extends keyof Cell.EventArgs>( name: Key, args: Cell.EventArgs[Key], ) { this.trigger(name, args) const model = this.model if (model) { model.notify(`cell:${name}`, args) if (this.isNode()) { model.notify(`node:${name}`, { ...args, node: this }) } else if (this.isEdge()) { model.notify(`edge:${name}`, { ...args, edge: this }) } } return this } isNode(): this is Node { return false } isEdge(): this is Edge { return false } isSameStore(cell: Cell) { return this.store === cell.store } get view() { return this.store.get('view') } get shape() { return this.store.get('shape', '') } // #region get/set getProp(): Properties getProp<K extends keyof Properties>(key: K): Properties[K] getProp<K extends keyof Properties>( key: K, defaultValue: Properties[K], ): NonUndefined<Properties[K]> getProp<T>(key: string): T getProp<T>(key: string, defaultValue: T): T getProp(key?: string, defaultValue?: any) { if (key == null) { return this.store.get() } return this.store.get(key, defaultValue) } setProp<K extends keyof Properties>( key: K, value: Properties[K] | null | undefined | void, options?: Cell.SetOptions, ): this setProp(key: string, value: any, options?: Cell.SetOptions): this setProp(props: Partial<Properties>, options?: Cell.SetOptions): this setProp( key: string | Partial<Properties>, value?: any, options?: Cell.SetOptions, ) { if (typeof key === 'string') { this.store.set(key, value, options) } else { const props = this.preprocess(key, true) this.store.set(ObjectExt.merge({}, this.getProp(), props), value) this.postprocess(key) } return this } removeProp<K extends keyof Properties>( key: K | K[], options?: Cell.SetOptions, ): this removeProp(key: string | string[], options?: Cell.SetOptions): this removeProp(options?: Cell.SetOptions): this removeProp( key?: string | string[] | Cell.SetOptions, options?: Cell.SetOptions, ) { if (typeof key === 'string' || Array.isArray(key)) { this.store.removeByPath(key, options) } else { this.store.remove(options) } return this } hasChanged(): boolean hasChanged<K extends keyof Properties>(key: K | null): boolean hasChanged(key: string | null): boolean hasChanged(key?: string | null) { return key == null ? this.store.hasChanged() : this.store.hasChanged(key) } getPropByPath<T>(path: string | string[]) { return this.store.getByPath<T>(path) } setPropByPath( path: string | string[], value: any, options: Cell.SetByPathOptions = {}, ) { if (this.model) { // update inner reference if (path === 'children') { this._children = value ? value .map((id: string) => this.model!.getCell(id)) .filter((child: Cell) => child != null) : null } else if (path === 'parent') { this._parent = value ? this.model.getCell(value) : null } } this.store.setByPath(path, value, options) return this } removePropByPath(path: string | string[], options: Cell.SetOptions = {}) { const paths = Array.isArray(path) ? path : path.split('/') // Once a property is removed from the `attrs` the CellView will // recognize a `dirty` flag and re-render itself in order to remove // the attribute from SVGElement. if (paths[0] === 'attrs') { options.dirty = true } this.store.removeByPath(paths, options) return this } prop(): Properties prop<K extends keyof Properties>(key: K): Properties[K] prop<T>(key: string): T prop<T>(path: string[]): T prop<K extends keyof Properties>( key: K, value: Properties[K] | null | undefined | void, options?: Cell.SetOptions, ): this prop(key: string, value: any, options?: Cell.SetOptions): this prop(path: string[], value: any, options?: Cell.SetOptions): this prop(props: Partial<Properties>, options?: Cell.SetOptions): this prop( key?: string | string[] | Partial<Properties>, value?: any, options?: Cell.SetOptions, ) { if (key == null) { return this.getProp() } if (typeof key === 'string' || Array.isArray(key)) { if (arguments.length === 1) { return this.getPropByPath(key) } if (value == null) { return this.removePropByPath(key, options || {}) } return this.setPropByPath(key, value, options || {}) } return this.setProp(key, value || {}) } previous<K extends keyof Properties>(name: K): Properties[K] | undefined previous<T>(name: string): T | undefined previous(name: string) { return this.store.getPrevious(name as keyof Cell.Properties) } // #endregion // #region zIndex get zIndex() { return this.getZIndex() } set zIndex(z: number | undefined | null) { if (z == null) { this.removeZIndex() } else { this.setZIndex(z) } } getZIndex() { return this.store.get('zIndex') } setZIndex(z: number, options: Cell.SetOptions = {}) { this.store.set('zIndex', z, options) return this } removeZIndex(options: Cell.SetOptions = {}) { this.store.remove('zIndex', options) return this } toFront(options: Cell.ToFrontOptions = {}) { const model = this.model if (model) { let z = model.getMaxZIndex() let cells: Cell[] if (options.deep) { cells = this.getDescendants({ deep: true, breadthFirst: true }) cells.unshift(this) } else { cells = [this] } z = z - cells.length + 1 const count = model.total() let changed = model.indexOf(this) !== count - cells.length if (!changed) { changed = cells.some((cell, index) => cell.getZIndex() !== z + index) } if (changed) { this.batchUpdate('to-front', () => { z += cells.length cells.forEach((cell, index) => { cell.setZIndex(z + index, options) }) }) } } return this } toBack(options: Cell.ToBackOptions = {}) { const model = this.model if (model) { let z = model.getMinZIndex() let cells: Cell[] if (options.deep) { cells = this.getDescendants({ deep: true, breadthFirst: true }) cells.unshift(this) } else { cells = [this] } let changed = model.indexOf(this) !== 0 if (!changed) { changed = cells.some((cell, index) => cell.getZIndex() !== z + index) } if (changed) { this.batchUpdate('to-back', () => { z -= cells.length cells.forEach((cell, index) => { cell.setZIndex(z + index, options) }) }) } } return this } // #endregion // #region markup get markup() { return this.getMarkup() } set markup(value: Markup | undefined | null) { if (value == null) { this.removeMarkup() } else { this.setMarkup(value) } } getMarkup() { let markup = this.store.get('markup') if (markup == null) { const ctor = this.constructor as typeof Cell markup = ctor.getMarkup() } return markup } setMarkup(markup: Markup, options: Cell.SetOptions = {}) { this.store.set('markup', markup, options) return this } removeMarkup(options: Cell.SetOptions = {}) { this.store.remove('markup', options) return this } // #endregion // #region attrs get attrs() { return this.getAttrs() } set attrs(value: Attr.CellAttrs | null | undefined) { if (value == null) { this.removeAttrs() } else { this.setAttrs(value) } } getAttrs() { const result = this.store.get('attrs') return result ? { ...result } : {} } setAttrs( attrs: Attr.CellAttrs | null | undefined, options: Cell.SetAttrOptions = {}, ) { if (attrs == null) { this.removeAttrs(options) } else { const set = (attrs: Attr.CellAttrs) => this.store.set('attrs', attrs, options) if (options.overwrite === true) { set(attrs) } else { const prev = this.getAttrs() if (options.deep === false) { set({ ...prev, ...attrs }) } else { set(ObjectExt.merge({}, prev, attrs)) } } } return this } replaceAttrs(attrs: Attr.CellAttrs, options: Cell.SetOptions = {}) { return this.setAttrs(attrs, { ...options, overwrite: true }) } updateAttrs(attrs: Attr.CellAttrs, options: Cell.SetOptions = {}) { return this.setAttrs(attrs, { ...options, deep: false }) } removeAttrs(options: Cell.SetOptions = {}) { this.store.remove('attrs', options) return this } getAttrDefinition(attrName: string) { if (!attrName) { return null } const ctor = this.constructor as typeof Cell const hooks = ctor.getAttrHooks() || {} let definition = hooks[attrName] || Attr.registry.get(attrName) if (!definition) { const name = StringExt.camelCase(attrName) definition = hooks[name] || Attr.registry.get(name) } return definition || null } getAttrByPath(): Attr.CellAttrs getAttrByPath<T>(path: string | string[]): T getAttrByPath<T>(path?: string | string[]) { if (path == null || path === '') { return this.getAttrs() } return this.getPropByPath<T>(this.prefixAttrPath(path)) } setAttrByPath( path: string | string[], value: Attr.ComplexAttrValue, options: Cell.SetOptions = {}, ) { this.setPropByPath(this.prefixAttrPath(path), value, options) return this } removeAttrByPath(path: string | string[], options: Cell.SetOptions = {}) { this.removePropByPath(this.prefixAttrPath(path), options) return this } protected prefixAttrPath(path: string | string[]) { return Array.isArray(path) ? ['attrs'].concat(path) : `attrs/${path}` } attr(): Attr.CellAttrs attr<T>(path: string | string[]): T attr( path: string | string[], value: Attr.ComplexAttrValue | null, options?: Cell.SetOptions, ): this attr(attrs: Attr.CellAttrs, options?: Cell.SetAttrOptions): this attr( path?: string | string[] | Attr.CellAttrs, value?: Attr.ComplexAttrValue | Cell.SetOptions, options?: Cell.SetOptions, ) { if (path == null) { return this.getAttrByPath() } if (typeof path === 'string' || Array.isArray(path)) { if (arguments.length === 1) { return this.getAttrByPath(path) } if (value == null) { return this.removeAttrByPath(path, options || {}) } return this.setAttrByPath( path, value as Attr.ComplexAttrValue, options || {}, ) } return this.setAttrs(path, (value || {}) as Cell.SetOptions) } // #endregion // #region visible get visible() { return this.isVisible() } set visible(value: boolean) { this.setVisible(value) } setVisible(visible: boolean, options: Cell.SetOptions = {}) { this.store.set('visible', visible, options) return this } isVisible() { return this.store.get('visible') !== false } show(options: Cell.SetOptions = {}) { if (!this.isVisible()) { this.setVisible(true, options) } return this } hide(options: Cell.SetOptions = {}) { if (this.isVisible()) { this.setVisible(false, options) } return this } toggleVisible(visible: boolean, options?: Cell.SetOptions): this toggleVisible(options?: Cell.SetOptions): this toggleVisible( isVisible?: boolean | Cell.SetOptions, options: Cell.SetOptions = {}, ) { const visible = typeof isVisible === 'boolean' ? isVisible : !this.isVisible() const localOptions = typeof isVisible === 'boolean' ? options : isVisible if (visible) { this.show(localOptions) } else { this.hide(localOptions) } return this } // #endregion // #region data get data(): Properties['data'] { return this.getData() } set data(val: Properties['data']) { this.setData(val) } getData<T = Properties['data']>(): T { return this.store.get<T>('data') } setData<T = Properties['data']>(data: T, options: Cell.SetDataOptions = {}) { if (data == null) { this.removeData(options) } else { const set = (data: T) => this.store.set('data', data, options) if (options.overwrite === true) { set(data) } else { const prev = this.getData<Record<string, any>>() if (options.deep === false) { set(typeof data === 'object' ? { ...prev, ...data } : data) } else { set(ObjectExt.merge({}, prev, data)) } } } return this } replaceData<T = Properties['data']>(data: T, options: Cell.SetOptions = {}) { return this.setData(data, { ...options, overwrite: true }) } updateData<T = Properties['data']>(data: T, options: Cell.SetOptions = {}) { return this.setData(data, { ...options, deep: false }) } removeData(options: Cell.SetOptions = {}) { this.store.remove('data', options) return this } // #endregion // #region parent children get parent(): Cell | null { return this.getParent() } get children() { return this.getChildren() } getParentId() { return this.store.get('parent') } getParent() { let parent = this._parent if (parent == null && this.store) { const parentId = this.getParentId() if (parentId != null && this.model) { parent = this.model.getCell(parentId) this._parent = parent } } return parent } getChildren() { let children = this._children if (children == null) { const childrenIds = this.store.get('children') if (childrenIds && childrenIds.length && this.model) { children = childrenIds .map((id) => this.model?.getCell(id)) .filter((cell) => cell != null) as Cell[] this._children = children } } return children ? [...children] : null } hasParent() { return this.parent != null } isParentOf(child: Cell | null): boolean { return child != null && child.getParent() === this } isChildOf(parent: Cell | null): boolean { return parent != null && this.getParent() === parent } eachChild( iterator: (child: Cell, index: number, children: Cell[]) => void, context?: any, ) { if (this.children) { this.children.forEach(iterator, context) } return this } filterChild( filter: (cell: Cell, index: number, arr: Cell[]) => boolean, context?: any, ): Cell[] { return this.children ? this.children.filter(filter, context) : [] } getChildCount() { return this.children == null ? 0 : this.children.length } getChildIndex(child: Cell) { return this.children == null ? -1 : this.children.indexOf(child) } getChildAt(index: number) { return this.children != null && index >= 0 ? this.children[index] : null } getAncestors(options: { deep?: boolean } = {}): Cell[] { const ancestors: Cell[] = [] let parent = this.getParent() while (parent) { ancestors.push(parent) parent = options.deep !== false ? parent.getParent() : null } return ancestors } getDescendants(options: Cell.GetDescendantsOptions = {}): Cell[] { if (options.deep !== false) { // breadth first if (options.breadthFirst) { const cells = [] const queue = this.getChildren() || [] while (queue.length > 0) { const parent = queue.shift()! const children = parent.getChildren() cells.push(parent) if (children) { queue.push(...children) } } return cells } // depth first { const cells = this.getChildren() || [] cells.forEach((cell) => { cells.push(...cell.getDescendants(options)) }) return cells } } return this.getChildren() || [] } isDescendantOf( ancestor: Cell | null, options: { deep?: boolean } = {}, ): boolean { if (ancestor == null) { return false } if (options.deep !== false) { let current = this.getParent() while (current) { if (current === ancestor) { return true } current = current.getParent() } return false } return this.isChildOf(ancestor) } isAncestorOf( descendant: Cell | null, options: { deep?: boolean } = {}, ): boolean { if (descendant == null) { return false } return descendant.isDescendantOf(this, options) } contains(cell: Cell | null) { return this.isAncestorOf(cell) } getCommonAncestor(...cells: (Cell | null | undefined)[]): Cell | null { return Cell.getCommonAncestor(this, ...cells) } setParent(parent: Cell | null, options: Cell.SetOptions = {}) { this._parent = parent if (parent) { this.store.set('parent', parent.id, options) } else { this.store.remove('parent', options) } return this } setChildren(children: Cell[] | null, options: Cell.SetOptions = {}) { this._children = children if (children != null) { this.store.set( 'children', children.map((child) => child.id), options, ) } else { this.store.remove('children', options) } return this } unembed(child: Cell, options: Cell.SetOptions = {}) { const children = this.children if (children != null && child != null) { const index = this.getChildIndex(child) if (index !== -1) { children.splice(index, 1) child.setParent(null, options) this.setChildren(children, options) } } return this } embed(child: Cell, options: Cell.SetOptions = {}) { child.addTo(this, options) return this } addTo(model: Model, options?: Cell.SetOptions): this addTo(graph: Graph, options?: Cell.SetOptions): this addTo(parent: Cell, options?: Cell.SetOptions): this addTo(target: Model | Graph | Cell, options: Cell.SetOptions = {}) { if (Cell.isCell(target)) { target.addChild(this, options) } else { target.addCell(this, options) } return this } insertTo(parent: Cell, index?: number, options: Cell.SetOptions = {}) { parent.insertChild(this, index, options) return this } addChild(child: Cell | null, options: Cell.SetOptions = {}) { return this.insertChild(child, undefined, options) } insertChild( child: Cell | null, index?: number, options: Cell.SetOptions = {}, ): this { if (child != null && child !== this) { const oldParent = child.getParent() const changed = this !== oldParent let pos = index if (pos == null) { pos = this.getChildCount() if (!changed) { pos -= 1 } } // remove from old parent if (oldParent) { const children = oldParent.getChildren() if (children) { const index = children.indexOf(child) if (index >= 0) { child.setParent(null, options) children.splice(index, 1) oldParent.setChildren(children, options) } } } let children = this.children if (children == null) { children = [] children.push(child) } else { children.splice(pos, 0, child) } child.setParent(this, options) this.setChildren(children, options) if (changed && this.model) { const incomings = this.model.getIncomingEdges(this) const outgoings = this.model.getOutgoingEdges(this) if (incomings) { incomings.forEach((edge) => edge.updateParent(options)) } if (outgoings) { outgoings.forEach((edge) => edge.updateParent(options)) } } if (this.model) { this.model.addCell(child, options) } } return this } removeFromParent(options: Cell.RemoveOptions = {}) { const parent = this.getParent() if (parent != null) { const index = parent.getChildIndex(this) parent.removeChildAt(index, options) } return this } removeChild(child: Cell, options: Cell.RemoveOptions = {}) { const index = this.getChildIndex(child) return this.removeChildAt(index, options) } removeChildAt(index: number, options: Cell.RemoveOptions = {}) { const child = this.getChildAt(index) const children = this.children if (children != null && child != null) { this.unembed(child, options) child.remove(options) } return child } remove(options: Cell.RemoveOptions = {}) { this.batchUpdate('remove', () => { const parent = this.getParent() if (parent) { parent.removeChild(this, options) } if (options.deep !== false) { this.eachChild((child) => child.remove(options)) } if (this.model) { this.model.removeCell(this, options) } }) return this } // #endregion // #region transition transition<K extends keyof Properties>( path: K, target: Properties[K], options?: Animation.StartOptions<Properties[K]>, delim?: string, ): () => void transition<T extends Animation.TargetValue>( path: string | string[], target: T, options?: Animation.StartOptions<T>, delim?: string, ): () => void transition<T extends Animation.TargetValue>( path: string | string[], target: T, options: Animation.StartOptions<T> = {}, delim = '/', ) { return this.animation.start(path, target, options, delim) } stopTransition<T extends Animation.TargetValue>( path: string | string[], options?: Animation.StopOptions<T>, delim = '/', ) { this.animation.stop(path, options, delim) return this } getTransitions() { return this.animation.get() } // #endregion // #region transform // eslint-disable-next-line translate(tx: number, ty: number, options?: Cell.TranslateOptions) { return this } scale( sx: number, // eslint-disable-line sy: number, // eslint-disable-line origin?: Point | Point.PointLike, // eslint-disable-line options?: Node.SetOptions, // eslint-disable-line ) { return this } // #endregion // #region tools addTools( items: Cell.ToolItem | Cell.ToolItem[], options?: Cell.AddToolOptions, ): void addTools( items: Cell.ToolItem | Cell.ToolItem[], name: string, options?: Cell.AddToolOptions, ): void addTools( items: Cell.ToolItem | Cell.ToolItem[], obj?: string | Cell.AddToolOptions, options?: Cell.AddToolOptions, ) { const toolItems = Array.isArray(items) ? items : [items] const name = typeof obj === 'string' ? obj : null const config = typeof obj === 'object' ? obj : typeof options === 'object' ? options : {} if (config.reset) { return this.setTools( { name, items: toolItems, local: config.local }, config, ) } let tools = ObjectExt.cloneDeep(this.getTools()) if (tools == null || name == null || tools.name === name) { if (tools == null) { tools = {} as Cell.Tools } if (!tools.items) { tools.items = [] } tools.name = name tools.items = [...tools.items, ...toolItems] return this.setTools({ ...tools }, config) } } setTools(tools?: Cell.ToolsLoose | null, options: Cell.SetOptions = {}) { if (tools == null) { this.removeTools() } else { this.store.set('tools', Cell.normalizeTools(tools), options) } return this } getTools(): Cell.Tools | null { return this.store.get<Cell.Tools>('tools') } removeTools(options: Cell.SetOptions = {}) { this.store.remove('tools', options) return this } hasTools(name?: string) { const tools = this.getTools() if (tools == null) { return false } if (name == null) { return true } return tools.name === name } hasTool(name: string) { const tools = this.getTools() if (tools == null) { return false } return tools.items.some((item) => typeof item === 'string' ? item === name : item.name === name, ) } removeTool(name: string, options?: Cell.SetOptions): this removeTool(index: number, options?: Cell.SetOptions): this removeTool(nameOrIndex: string | number, options: Cell.SetOptions = {}) { const tools = ObjectExt.cloneDeep(this.getTools()) if (tools) { let updated = false const items = tools.items.slice() const remove = (index: number) => { items.splice(index, 1) updated = true } if (typeof nameOrIndex === 'number') { remove(nameOrIndex) } else { for (let i = items.length - 1; i >= 0; i -= 1) { const item = items[i] const exist = typeof item === 'string' ? item === nameOrIndex : item.name === nameOrIndex if (exist) { remove(i) } } } if (updated) { tools.items = items this.setTools(tools, options) } } return this } // #endregion // #region common // eslint-disable-next-line getBBox(options?: { deep?: boolean }) { return new Rectangle() } // eslint-disable-next-line getConnectionPoint(edge: Edge, type: Edge.TerminalType) { return new Point() } toJSON( options: Cell.ToJSONOptions = {}, ): this extends Node ? Node.Properties : this extends Edge ? Edge.Properties : Properties { const props = { ...this.store.get() } const toString = Object.prototype.toString const cellType = this.isNode() ? 'node' : this.isEdge() ? 'edge' : 'cell' if (!props.shape) { const ctor = this.constructor throw new Error( `Unable to serialize ${cellType} missing "shape" prop, check the ${cellType} "${ ctor.name || toString.call(ctor) }"`, ) } const ctor = this.constructor as typeof Cell const diff = options.diff === true const attrs = props.attrs || {} const presets = ctor.getDefaults(true) as Properties // When `options.diff` is `true`, we should process the custom options, // such as `width`, `height` etc. to ensure the comparing work correctly. const defaults = diff ? this.preprocess(presets, true) : presets const defaultAttrs = defaults.attrs || {} const finalAttrs: Attr.CellAttrs = {} Object.keys(props).forEach((key) => { const val = props[key] if ( val != null && !Array.isArray(val) && typeof val === 'object' && !ObjectExt.isPlainObject(val) ) { throw new Error( `Can only serialize ${cellType} with plain-object props, but got a "${toString.call( val, )}" type of key "${key}" on ${cellType} "${this.id}"`, ) } if (key !== 'attrs' && key !== 'shape' && diff) { const preset = defaults[key] if (ObjectExt.isEqual(val, preset)) { delete props[key] } } }) Object.keys(attrs).forEach((key) => { const attr = attrs[key] const defaultAttr = defaultAttrs[key] Object.keys(attr).forEach((name) => { const value = attr[name] as KeyValue const defaultValue = defaultAttr ? defaultAttr[name] : null if ( value != null && typeof value === 'object' && !Array.isArray(value) ) { Object.keys(value).forEach((subName) => { const subValue = value[subName] if ( defaultAttr == null || defaultValue == null || !ObjectExt.isObject(defaultValue) || !ObjectExt.isEqual(defaultValue[subName], subValue) ) { if (finalAttrs[key] == null) { finalAttrs[key] = {} } if (finalAttrs[key][name] == null) { finalAttrs[key][name] = {} } const tmp = finalAttrs[key][name] as KeyValue tmp[subName] = subValue } }) } else if ( defaultAttr == null || !ObjectExt.isEqual(defaultValue, value) ) { // `value` is not an object, default attribute with `key` does not // exist or it is different than the attribute value set on the cell. if (finalAttrs[key] == null) { finalAttrs[key] = {} } finalAttrs[key][name] = value as any } }) }) const finalProps = { ...props, attrs: ObjectExt.isEmpty(finalAttrs) ? undefined : finalAttrs, } if (finalProps.attrs == null) { delete finalProps.attrs } const ret = finalProps as any if (ret.angle === 0) { delete ret.angle } return ObjectExt.cloneDeep(ret) } clone( options: Cell.CloneOptions = {}, ): this extends Node ? Node : this extends Edge ? Edge : Cell { if (!options.deep) { const data = { ...this.store.get() } if (!options.keepId) { delete data.id } delete data.parent delete data.children const ctor = this.constructor as typeof Cell return new ctor(data) as any // eslint-disable-line new-cap } // Deep cloning. Clone the cell itself and all its children. const map = Cell.deepClone(this) return map[this.id] as any } findView(graph: Graph): CellView | null { return graph.renderer.findViewByCell(this) } // #endregion // #region batch startBatch( name: Model.BatchName, data: KeyValue = {}, model: Model | null = this.model, ) { this.notify('batch:start', { name, data, cell: this }) if (model) { model.startBatch(name, { ...data, cell: this }) } return this } stopBatch( name: Model.BatchName, data: KeyValue = {}, model: Model | null = this.model, ) { if (model) { model.stopBatch(name, { ...data, cell: this }) } this.notify('batch:stop', { name, data, cell: this }) return this } batchUpdate<T>(name: Model.BatchName, execute: () => T, data?: KeyValue): T { // The model is null after cell was removed(remove batch). // So we should temp save model to trigger pairing batch event. const model = this.model this.startBatch(name, data, model) const result = execute() this.stopBatch(name, data, model) return result } // #endregion // #region IDisposable @Basecoat.dispose() dispose() { this.removeFromParent() this.store.dispose() } // #endregion } export namespace Cell { export interface Common { view?: string shape?: string markup?: Markup attrs?: Attr.CellAttrs zIndex?: number visible?: boolean data?: any knob?: Knob.Metadata | Knob.Metadata[] } export interface Defaults extends Common {} export interface Metadata extends Common, KeyValue { id?: string tools?: ToolsLoose } export interface Properties extends Defaults, Metadata { parent?: string children?: string[] tools?: Tools } } export namespace Cell { export type ToolItem = | string | { name: string args?: any } export interface Tools { name?: string | null local?: boolean items: ToolItem[] } export type ToolsLoose = ToolItem | ToolItem[] | Tools export function normalizeTools(raw: ToolsLoose): Tools { if (typeof raw === 'string') { return { items: [raw] } } if (Array.isArray(raw)) { return { items: raw } } if ((raw as Tools).items) { return raw as Tools } return { items: [raw as ToolItem], } } } export namespace Cell { export interface SetOptions extends Store.SetOptions {} export interface MutateOptions extends Store.MutateOptions {} export interface RemoveOptions extends SetOptions { deep?: boolean } export interface SetAttrOptions extends SetOptions { deep?: boolean overwrite?: boolean } export interface SetDataOptions extends SetOptions { deep?: boolean overwrite?: boolean } export interface SetByPathOptions extends Store.SetByPathOptions {} export interface ToFrontOptions extends SetOptions { deep?: boolean } export interface ToBackOptions extends ToFrontOptions {} export interface TranslateOptions extends SetOptions { tx?: number ty?: number translateBy?: string | number } export interface AddToolOptions extends SetOptions { reset?: boolean local?: boolean } export interface GetDescendantsOptions { deep?: boolean breadthFirst?: boolean } export interface ToJSONOptions { diff?: boolean } export interface CloneOptions { deep?: boolean keepId?: boolean } } export namespace Cell { export interface EventArgs { 'transition:start': Animation.CallbackArgs<Animation.TargetValue> 'transition:progress': Animation.ProgressArgs<Animation.TargetValue> 'transition:complete': Animation.CallbackArgs<Animation.TargetValue> 'transition:stop': Animation.StopArgs<Animation.TargetValue> 'transition:finish': Animation.CallbackArgs<Animation.TargetValue> // common 'change:*': ChangeAnyKeyArgs 'change:attrs': ChangeArgs<Attr.CellAttrs> 'change:zIndex': ChangeArgs<number> 'change:markup': ChangeArgs<Markup> 'change:visible': ChangeArgs<boolean> 'change:parent': ChangeArgs<string> 'change:children': ChangeArgs<string[]> 'change:tools': ChangeArgs<Tools> 'change:view': ChangeArgs<string> 'change:data': ChangeArgs<any> // node 'change:size': NodeChangeArgs<Size> 'change:angle': NodeChangeArgs<number> 'change:position': NodeChangeArgs<Point.PointLike> 'change:ports': NodeChangeArgs<PortManager.Port[]> 'change:portMarkup': NodeChangeArgs<Markup> 'change:portLabelMarkup': NodeChangeArgs<Markup> 'change:portContainerMarkup': NodeChangeArgs<Markup> 'ports:removed': { cell: Cell node: Node removed: PortManager.Port[] } 'ports:added': { cell: Cell node: Node added: PortManager.Port[] } // edge 'change:source': EdgeChangeArgs<Edge.TerminalData> 'change:target': EdgeChangeArgs<Edge.TerminalData> 'change:terminal': EdgeChangeArgs<Edge.TerminalData> & { type: Edge.TerminalType } 'change:router': EdgeChangeArgs<Edge.RouterData> 'change:connector': EdgeChangeArgs<Edge.ConnectorData> 'change:vertices': EdgeChangeArgs<Point.PointLike[]> 'change:labels': EdgeChangeArgs<Edge.Label[]> 'change:defaultLabel': EdgeChangeArgs<Edge.Label> 'change:toolMarkup': EdgeChangeArgs<Markup> 'change:doubleToolMarkup': EdgeChangeArgs<Markup> 'change:vertexMarkup': EdgeChangeArgs<Markup> 'change:arrowheadMarkup': EdgeChangeArgs<Markup> 'vertexs:added': { cell: Cell edge: Edge added: Point.PointLike[] } 'vertexs:removed': { cell: Cell edge: Edge removed: Point.PointLike[] } 'labels:added': { cell: Cell edge: Edge added: Edge.Label[] } 'labels:removed': { cell: Cell edge: Edge removed: Edge.Label[] } 'batch:start': { name: Model.BatchName data: KeyValue cell: Cell } 'batch:stop': { name: Model.BatchName data: KeyValue cell: Cell } changed: { cell: Cell options: MutateOptions } added: { cell: Cell index: number options: Cell.SetOptions } removed: { cell: Cell index: number options: Cell.RemoveOptions } } interface ChangeAnyKeyArgs<T extends keyof Properties = keyof Properties> { key: T current: Properties[T] previous: Properties[T] options: MutateOptions cell: Cell } export interface ChangeArgs<T> { cell: Cell current?: T previous?: T options: MutateOptions } interface NodeChangeArgs<T> extends ChangeArgs<T> { node: Node } interface EdgeChangeArgs<T> extends ChangeArgs<T> { edge: Edge } } export namespace Cell { export const toStringTag = `X6.${Cell.name}` export function isCell(instance: any): instance is Cell { if (instance == null) { return false } if (instance instanceof Cell) { return true } const tag = instance[Symbol.toStringTag] const cell = instance as Cell if ( (tag == null || tag === toStringTag) && typeof cell.isNode === 'function' && typeof cell.isEdge === 'function' && typeof cell.prop === 'function' && typeof cell.attr === 'function' ) { return true } return false } } export namespace Cell { export function getCommonAncestor( ...cells: (Cell | null | undefined)[] ): Cell | null { const ancestors = cells .filter((cell) => cell != null) .map((cell) => cell!.getAncestors()) .sort((a, b) => { return a.length - b.length }) const first = ancestors.shift()! return ( first.find((cell) => ancestors.every((item) => item.includes(cell))) || null ) } export interface GetCellsBBoxOptions { deep?: boolean } export function getCellsBBox( cells: Cell[], options: GetCellsBBoxOptions = {}, ) { let bbox: Rectangle | null = null for (let i = 0, ii = cells.length; i < ii; i += 1) { const cell = cells[i] let rect = cell.getBBox(options) if (rect) { if (cell.isNode()) { const angle = cell.getAngle() if (angle != null && angle !== 0) { rect = rect.bbox(angle) } } bbox = bbox == null ? rect : bbox.union(rect) } } return bbox } export function deepClone(cell: Cell) { const cells = [cell, ...cell.getDescendants({ deep: true })] return Cell.cloneCells(cells) } export function cloneCells(cells: Cell[]) { const inputs = ArrayExt.uniq(cells) const cloneMap = inputs.reduce<KeyValue<Cell>>((map, cell) => { map[cell.id] = cell.clone() return map }, {}) inputs.forEach((cell) => { const clone = cloneMap[cell.id] if (clone.isEdge()) { const sourceId = clone.getSourceCellId() const targetId = clone.getTargetCellId() if (sourceId && cloneMap[sourceId]) { // Source is a node and the node is among the clones. // Then update the source of the cloned edge. clone.setSource({ ...clone.getSource(), cell: cloneMap[sourceId].id, }) } if (targetId && cloneMap[targetId]) { // Target is a node and the node is among the clones. // Then update the target of the cloned edge. clone.setTarget({ ...clone.getTarget(), cell: cloneMap[targetId].id, }) } } // Find the parent of the original cell const parent = cell.getParent() if (parent && cloneMap[parent.id]) { clone.setParent(cloneMap[parent.id]) } // Find the children of the original cell const children = cell.getChildren() if (children && children.length) { const embeds = children.reduce<Cell[]>((memo, child) => { // Embedded cells that are not being cloned can not be carried // over with other embedded cells. if (cloneMap[child.id]) { memo.push(cloneMap[child.id]) } return memo }, []) if (embeds.length > 0) { clone.setChildren(embeds) } } }) return cloneMap } } export namespace Cell { export type Definition = typeof Cell export type PropHook<M extends Metadata = Metadata, C extends Cell = Cell> = ( this: C, metadata: M, ) => M export type PropHooks<M extends Metadata = Metadata, C extends Cell = Cell> = | KeyValue<PropHook<M, C>> | PropHook<M, C> | PropHook<M, C>[] export interface Config<M extends Metadata = Metadata, C extends Cell = Cell> extends Defaults, KeyValue { constructorName?: string overwrite?: boolean propHooks?: PropHooks<M, C> attrHooks?: Attr.Definitions } } export namespace Cell { Cell.config({ propHooks({ tools, ...metadata }) { if (tools) { metadata.tools = normalizeTools(tools) } return metadata }, }) }