UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering

511 lines (436 loc) 15.5 kB
import { ArrayExt, Basecoat, disposable } from '../common' import type { Cell, CellBaseEventArgs, CellSetOptions } from './cell' import type { Edge } from './edge' import type { Node } from './node' export class Collection extends Basecoat<CollectionEventArgs> { public length = 0 public comparator: Comparator | null private cells: Cell[] private map: { [id: string]: Cell } constructor(cells: Cell | Cell[], options: Options = {}) { super() this.comparator = options.comparator || 'zIndex' this.clean() if (cells) { this.reset(cells, { silent: true }) } } toJSON() { return this.cells.map((cell) => cell.toJSON()) } add(cells: Cell | Cell[], options?: CollectionAddOptions): this add(cells: Cell | Cell[], index: number, options?: CollectionAddOptions): this add( cells: Cell | Cell[], index?: number | CollectionAddOptions, options?: CollectionAddOptions, ) { let localIndex: number let localOptions: CollectionAddOptions if (typeof index === 'number') { localIndex = index localOptions = { merge: false, ...options } } else { localIndex = this.length localOptions = { merge: false, ...index } } if (localIndex > this.length) { localIndex = this.length } if (localIndex < 0) { localIndex += this.length + 1 } const entities = Array.isArray(cells) ? cells : [cells] const sortable = this.comparator && typeof index !== 'number' && localOptions.sort !== false const sortAttr = this.comparator || null let sort = false const added: Cell[] = [] const merged: Cell[] = [] entities.forEach((cell) => { const existing = this.get(cell) if (existing) { if (localOptions.merge && !cell.isSameStore(existing)) { existing.setProp(cell.getProp(), options) // merge merged.push(existing) if (sortable && !sort) { if (sortAttr == null || typeof sortAttr === 'function') { sort = existing.hasChanged() } else if (typeof sortAttr === 'string') { sort = existing.hasChanged(sortAttr) } else { sort = sortAttr.some((key) => existing.hasChanged(key)) } } } } else { added.push(cell) this.reference(cell) } }) if (added.length) { if (sortable) { sort = true } this.cells.splice(localIndex, 0, ...added) this.length = this.cells.length } if (sort) { this.sort({ silent: true }) } if (!localOptions.silent) { added.forEach((cell, i) => { const args = { cell, index: localIndex + i, options: localOptions, } this.trigger('added', args) if (!localOptions.dryrun) { cell.notify('added', { ...args }) } }) if (sort) { this.trigger('sorted') } if (added.length || merged.length) { this.trigger('updated', { added, merged, removed: [], options: localOptions, }) } } return this } remove(cell: Cell, options?: CollectionRemoveOptions): Cell remove(cells: Cell[], options?: CollectionRemoveOptions): Cell[] remove(cells: Cell | Cell[], options: CollectionRemoveOptions = {}) { const arr = Array.isArray(cells) ? cells : [cells] const removed = this.removeCells(arr, options) if (!options.silent && removed.length > 0) { this.trigger('updated', { options, removed, added: [], merged: [], }) } return Array.isArray(cells) ? removed : removed[0] } protected removeCells(cells: Cell[], options: CollectionRemoveOptions) { const removed = [] for (let i = 0; i < cells.length; i += 1) { const cell = this.get(cells[i]) if (cell == null) { continue } const index = this.cells.indexOf(cell) this.cells.splice(index, 1) this.length -= 1 delete this.map[cell.id] removed.push(cell) this.unreference(cell) if (!options.silent) { this.trigger('removed', { cell, index, options }) if (!options.dryrun) { cell.notify('removed', { cell, index, options }) } } if (!options.dryrun) { cell.remove() } } return removed } reset(cells: Cell | Cell[], options: CollectionSetOptions = {}) { const previous = this.cells.slice() if (!options.diff) { previous.forEach((cell) => { this.unreference(cell) cell.remove() }) this.clean() } this.add(cells, { silent: true, ...options }) if (!options.silent) { const current = this.cells.slice() this.trigger('reseted', { options, previous, current, }) const added: Cell[] = [] const removed: Cell[] = [] current.forEach((a) => { const exist = previous.some((b) => b.id === a.id) if (!exist) { added.push(a) } }) previous.forEach((a) => { const exist = current.some((b) => b.id === a.id) if (!exist) { removed.push(a) } }) this.trigger('updated', { options, added, removed, merged: [] }) } return this } push(cell: Cell, options?: CollectionSetOptions) { return this.add(cell, this.length, options) } pop(options?: CollectionSetOptions) { const cell = this.at(this.length - 1) return cell ? this.remove(cell, options) : null } unshift(cell: Cell, options?: CollectionSetOptions) { return this.add(cell, 0, options) } shift(options?: CollectionSetOptions) { const cell = this.at(0) return cell ? this.remove(cell, options) : null } get(cell?: string | number | Cell | null): Cell | null { if (cell == null) { return null } const id = typeof cell === 'string' || typeof cell === 'number' ? cell : cell.id return this.map[id] || null } has(cell: string | Cell): boolean { return this.get(cell) != null } at(index: number): Cell | null { if (index < 0) { index += this.length // eslint-disable-line } return this.cells[index] || null } first() { return this.at(0) } last() { return this.at(-1) } indexOf(cell: Cell) { return this.cells.indexOf(cell) } toArray() { return this.cells.slice() } sort(options: CollectionSetOptions = {}) { if (this.comparator != null) { this.cells = ArrayExt.sortBy(this.cells, this.comparator) if (!options.silent) { this.trigger('sorted') } } return this } clone() { const Ctor = this.constructor as { new (cells: Cell[], options: Options): Collection } return new Ctor(this.cells.slice(), { comparator: this.comparator, }) as Collection } protected reference(cell: Cell) { this.map[cell.id] = cell cell.on('*', this.notifyCellEvent, this) } protected unreference(cell: Cell) { cell.off('*', this.notifyCellEvent, this) delete this.map[cell.id] } protected notifyCellEvent<K extends keyof CellBaseEventArgs>( name: K, args: CellBaseEventArgs[K], ) { const cell = args.cell this.trigger(`cell:${name}`, args) if (cell) { if (cell.isNode()) { this.trigger(`node:${name}`, { ...args, node: cell }) } else if (cell.isEdge()) { this.trigger(`edge:${name}`, { ...args, edge: cell }) } } } protected clean() { this.length = 0 this.cells = [] this.map = {} } @disposable() dispose() { this.reset([]) } } export type Comparator = string | string[] | ((cell: Cell) => number) interface Options { comparator?: Comparator } export interface CollectionSetOptions extends CellSetOptions {} export interface CollectionRemoveOptions extends CellSetOptions { /** * The default is to remove all the associated links. * Set `disconnectEdges` option to `true` to disconnect edges * when a cell is removed. */ disconnectEdges?: boolean dryrun?: boolean } export interface CollectionAddOptions extends CollectionSetOptions { sort?: boolean merge?: boolean dryrun?: boolean } export interface CollectionEventArgs extends CellBaseEventArgs, NodeEventArgs, EdgeEventArgs { sorted?: null reseted: { current: Cell[] previous: Cell[] options: CollectionSetOptions } updated: { added: Cell[] merged: Cell[] removed: Cell[] options: CollectionSetOptions } added: { cell: Cell index: number options: CollectionAddOptions } removed: { cell: Cell index: number options: CollectionRemoveOptions } } interface NodeEventCommonArgs { node: Node } interface EdgeEventCommonArgs { edge: Edge } export interface CellEventArgs { 'cell:animation:finish': CellBaseEventArgs['animation:finish'] 'cell:animation:cancel': CellBaseEventArgs['animation:cancel'] 'cell:changed': CellBaseEventArgs['changed'] 'cell:added': CellBaseEventArgs['added'] 'cell:removed': CellBaseEventArgs['removed'] 'cell:change:*': CellBaseEventArgs['change:*'] 'cell:change:attrs': CellBaseEventArgs['change:attrs'] 'cell:change:zIndex': CellBaseEventArgs['change:zIndex'] 'cell:change:markup': CellBaseEventArgs['change:markup'] 'cell:change:visible': CellBaseEventArgs['change:visible'] 'cell:change:parent': CellBaseEventArgs['change:parent'] 'cell:change:children': CellBaseEventArgs['change:children'] 'cell:change:tools': CellBaseEventArgs['change:tools'] 'cell:change:view': CellBaseEventArgs['change:view'] 'cell:change:data': CellBaseEventArgs['change:data'] 'cell:change:size': CellBaseEventArgs['change:size'] 'cell:change:angle': CellBaseEventArgs['change:angle'] 'cell:change:position': CellBaseEventArgs['change:position'] 'cell:change:ports': CellBaseEventArgs['change:ports'] 'cell:change:portMarkup': CellBaseEventArgs['change:portMarkup'] 'cell:change:portLabelMarkup': CellBaseEventArgs['change:portLabelMarkup'] 'cell:change:portContainerMarkup': CellBaseEventArgs['change:portContainerMarkup'] 'cell:ports:added': CellBaseEventArgs['ports:added'] 'cell:ports:removed': CellBaseEventArgs['ports:removed'] 'cell:change:source': CellBaseEventArgs['change:source'] 'cell:change:target': CellBaseEventArgs['change:target'] 'cell:change:router': CellBaseEventArgs['change:router'] 'cell:change:connector': CellBaseEventArgs['change:connector'] 'cell:change:vertices': CellBaseEventArgs['change:vertices'] 'cell:change:labels': CellBaseEventArgs['change:labels'] 'cell:change:defaultLabel': CellBaseEventArgs['change:defaultLabel'] 'cell:vertexs:added': CellBaseEventArgs['vertexs:added'] 'cell:vertexs:removed': CellBaseEventArgs['vertexs:removed'] 'cell:labels:added': CellBaseEventArgs['labels:added'] 'cell:labels:removed': CellBaseEventArgs['labels:removed'] 'cell:batch:start': CellBaseEventArgs['batch:start'] 'cell:batch:stop': CellBaseEventArgs['batch:stop'] } export interface NodeEventArgs { 'node:animation:finish': CellBaseEventArgs['animation:finish'] 'node:animation:cancel': CellBaseEventArgs['animation:cancel'] 'node:changed': NodeEventCommonArgs & CellEventArgs['cell:changed'] 'node:added': NodeEventCommonArgs & CellEventArgs['cell:added'] 'node:removed': NodeEventCommonArgs & CellEventArgs['cell:removed'] 'node:change:*': NodeEventCommonArgs & CellBaseEventArgs['change:*'] 'node:change:attrs': NodeEventCommonArgs & CellBaseEventArgs['change:attrs'] 'node:change:zIndex': NodeEventCommonArgs & CellBaseEventArgs['change:zIndex'] 'node:change:markup': NodeEventCommonArgs & CellBaseEventArgs['change:markup'] 'node:change:visible': NodeEventCommonArgs & CellBaseEventArgs['change:visible'] 'node:change:parent': NodeEventCommonArgs & CellBaseEventArgs['change:parent'] 'node:change:children': NodeEventCommonArgs & CellBaseEventArgs['change:children'] 'node:change:tools': NodeEventCommonArgs & CellBaseEventArgs['change:tools'] 'node:change:view': NodeEventCommonArgs & CellBaseEventArgs['change:view'] 'node:change:data': NodeEventCommonArgs & CellBaseEventArgs['change:data'] 'node:change:size': NodeEventCommonArgs & CellBaseEventArgs['change:size'] 'node:change:position': NodeEventCommonArgs & CellBaseEventArgs['change:position'] 'node:change:angle': NodeEventCommonArgs & CellBaseEventArgs['change:angle'] 'node:change:ports': NodeEventCommonArgs & CellBaseEventArgs['change:ports'] 'node:change:portMarkup': NodeEventCommonArgs & CellBaseEventArgs['change:portMarkup'] 'node:change:portLabelMarkup': NodeEventCommonArgs & CellBaseEventArgs['change:portLabelMarkup'] 'node:change:portContainerMarkup': NodeEventCommonArgs & CellBaseEventArgs['change:portContainerMarkup'] 'node:ports:added': NodeEventCommonArgs & CellBaseEventArgs['ports:added'] 'node:ports:removed': NodeEventCommonArgs & CellBaseEventArgs['ports:removed'] 'node:batch:start': NodeEventCommonArgs & CellBaseEventArgs['batch:start'] 'node:batch:stop': NodeEventCommonArgs & CellBaseEventArgs['batch:stop'] } export interface EdgeEventArgs { 'edge:animation:finish': CellBaseEventArgs['animation:finish'] 'edge:animation:cancel': CellBaseEventArgs['animation:cancel'] 'edge:changed': EdgeEventCommonArgs & CellEventArgs['cell:changed'] 'edge:added': EdgeEventCommonArgs & CellEventArgs['cell:added'] 'edge:removed': EdgeEventCommonArgs & CellEventArgs['cell:removed'] 'edge:change:*': EdgeEventCommonArgs & CellBaseEventArgs['change:*'] 'edge:change:attrs': EdgeEventCommonArgs & CellBaseEventArgs['change:attrs'] 'edge:change:zIndex': EdgeEventCommonArgs & CellBaseEventArgs['change:zIndex'] 'edge:change:markup': EdgeEventCommonArgs & CellBaseEventArgs['change:markup'] 'edge:change:visible': EdgeEventCommonArgs & CellBaseEventArgs['change:visible'] 'edge:change:parent': EdgeEventCommonArgs & CellBaseEventArgs['change:parent'] 'edge:change:children': EdgeEventCommonArgs & CellBaseEventArgs['change:children'] 'edge:change:tools': EdgeEventCommonArgs & CellBaseEventArgs['change:tools'] 'edge:change:data': EdgeEventCommonArgs & CellBaseEventArgs['change:data'] 'edge:change:source': EdgeEventCommonArgs & CellBaseEventArgs['change:source'] 'edge:change:target': EdgeEventCommonArgs & CellBaseEventArgs['change:target'] 'edge:change:router': EdgeEventCommonArgs & CellBaseEventArgs['change:router'] 'edge:change:connector': EdgeEventCommonArgs & CellBaseEventArgs['change:connector'] 'edge:change:vertices': EdgeEventCommonArgs & CellBaseEventArgs['change:vertices'] 'edge:change:labels': EdgeEventCommonArgs & CellBaseEventArgs['change:labels'] 'edge:change:defaultLabel': EdgeEventCommonArgs & CellBaseEventArgs['change:defaultLabel'] 'edge:vertexs:added': EdgeEventCommonArgs & CellBaseEventArgs['vertexs:added'] 'edge:vertexs:removed': EdgeEventCommonArgs & CellBaseEventArgs['vertexs:removed'] 'edge:labels:added': EdgeEventCommonArgs & CellBaseEventArgs['labels:added'] 'edge:labels:removed': EdgeEventCommonArgs & CellBaseEventArgs['labels:removed'] 'edge:batch:start': EdgeEventCommonArgs & CellBaseEventArgs['batch:start'] 'edge:batch:stop': EdgeEventCommonArgs & CellBaseEventArgs['batch:stop'] }