UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

870 lines (744 loc) 22.9 kB
import { FunctionExt, ObjectExt } from '../util' import { Cell } from '../model/cell' import { Node } from '../model/node' import { Edge } from '../model/edge' import { Model } from '../model/model' import { View } from '../view/view' import { Markup } from '../view/markup' import { CellView } from '../view/cell' import { NodeView } from '../view/node' import { EdgeView } from '../view/edge' import { Widget } from '../addon/common' import { Knob } from '../addon/knob' import { MiniMap } from '../addon/minimap' import { Snapline } from '../addon/snapline' import { Scroller } from '../addon/scroller' import { Selection } from '../addon/selection' import { Clipboard } from '../addon/clipboard' import { Transform } from '../addon/transform' import { HTML } from '../shape/standard/html' import { Edge as StandardEdge } from '../shape/standard/edge' import { Base } from './base' import { Graph } from './graph' import { Options } from './options' import { Renderer } from './renderer' import { GraphView } from './view' import { DefsManager } from './defs' import { GridManager } from './grid' import { CoordManager } from './coord' import { SnaplineManager } from './snapline' import { ScrollerManager } from './scroller' import { ClipboardManager } from './clipboard' import { HighlightManager } from './highlight' import { TransformManager } from './transform' import { SelectionManager } from './selection' import { BackgroundManager } from './background' import { HistoryManager } from './history' import { MiniMapManager } from './minimap' import { Keyboard } from './keyboard' import { MouseWheel } from './mousewheel' import { PrintManager } from './print' import { FormatManager } from './format' import { PortManager } from '../model/port' import { Rectangle } from '../geometry' import { KnobManager } from './knob' import { PanningManager } from './panning' import { SizeManager } from './size' namespace Decorator { export function hook(nullable?: boolean, hookName?: string | null) { return ( target: Hook, methodName: string, descriptor: PropertyDescriptor, ) => { const raw = descriptor.value const name = hookName || methodName descriptor.value = function (this: Hook, ...args: any[]) { const hook = (this.options as any)[name] if (hook != null) { this.getNativeValue = raw.bind(this, ...args) const ret = FunctionExt.call(hook, this.graph, ...args) this.getNativeValue = null if (ret != null || (nullable === true && ret === null)) { return ret } } return raw.call(this, ...args) } } } export function after(hookName?: string | null) { return ( target: Hook, methodName: string, descriptor: PropertyDescriptor, ) => { const raw = descriptor.value const name = hookName || methodName descriptor.value = function (this: Hook, ...args: any[]) { let ret = raw.call(this, ...args) const hook = (this.options as any)[name] if (hook != null) { ret = FunctionExt.call(hook, this.graph, ...args) && ret } return ret } } } } export class Hook extends Base implements Hook.IHook { /** * Get the native value of hooked method. */ public getNativeValue: (<T>() => T | null) | null @Decorator.hook() createModel() { if (this.options.model) { return this.options.model } const model = new Model() model.graph = this.graph return model } @Decorator.hook() createView() { return new GraphView(this.graph) } @Decorator.hook() createRenderer() { return new Renderer(this.graph) } @Decorator.hook() createDefsManager() { return new DefsManager(this.graph) } @Decorator.hook() createGridManager() { return new GridManager(this.graph) } @Decorator.hook() createCoordManager() { return new CoordManager(this.graph) } @Decorator.hook() createKnobManager() { return new KnobManager(this.graph) } @Decorator.hook() createTransform(node: Node, widgetOptions?: Widget.Options) { const options = this.getTransformOptions(node) if (options.resizable || options.rotatable) { return new Transform({ node, graph: this.graph, ...options, ...widgetOptions, }) } if (options.clearAll) { Transform.removeInstances(this.graph) } return null } @Decorator.hook() createKnob(node: Node, widgetOptions?: Widget.Options) { const options = Options.parseOptionGroup<Options.KnobRaw>( this.graph, node, this.options.knob, ) const localOptions = { ...options, ...widgetOptions, } if (localOptions.clearAll) { Knob.removeInstances(this.graph) } localOptions.clearAll = false const knob = node.prop('knob') as Knob.Metadata | Knob.Metadata[] const widgets: Knob[] = [] const meta = Array.isArray(knob) ? knob : [knob] meta.forEach((knob, index) => { if (knob) { if (knob.enabled === false) { return } if ( typeof knob.enabled === 'function' && knob.enabled.call(this.graph, node) === false ) { return } } else { return } if (options.enabled) { widgets.push( new Knob({ node, index, graph: this.graph, ...localOptions, }), ) } }) return widgets } protected getTransformOptions(node: Node) { const resizing = Options.parseOptionGroup<Options.ResizingRaw>( this.graph, node, this.options.resizing, ) const rotating = Options.parseOptionGroup<Options.RotatingRaw>( this.graph, node, this.options.rotating, ) const transforming = Options.parseOptionGroup<Options.TransformingRaw>( this.graph, node, this.options.transforming, ) const options: Transform.Options = { ...transforming, resizable: resizing.enabled, minWidth: resizing.minWidth, maxWidth: resizing.maxWidth, minHeight: resizing.minHeight, maxHeight: resizing.maxHeight, orthogonalResizing: resizing.orthogonal, restrictedResizing: resizing.restrict != null ? resizing.restrict : resizing.restricted, autoScrollOnResizing: resizing.autoScroll, preserveAspectRatio: resizing.preserveAspectRatio, allowReverse: resizing.allowReverse, rotatable: rotating.enabled, rotateGrid: rotating.grid, } return options } @Decorator.hook() createTransformManager() { return new TransformManager(this.graph) } @Decorator.hook() createHighlightManager() { return new HighlightManager(this.graph) } @Decorator.hook() createBackgroundManager() { return new BackgroundManager(this.graph) } @Decorator.hook() createClipboard() { return new Clipboard() } @Decorator.hook() createClipboardManager() { return new ClipboardManager(this.graph) } @Decorator.hook() createSnapline() { return new Snapline({ graph: this.graph, ...this.options.snapline }) } @Decorator.hook() createSnaplineManager() { return new SnaplineManager(this.graph) } @Decorator.hook() createSelection() { return new Selection({ graph: this.graph, ...this.options.selecting }) } @Decorator.hook() createSelectionManager() { return new SelectionManager(this.graph) } @Decorator.hook() // eslint-disable-next-line allowRubberband(e: JQuery.MouseDownEvent) { return true } @Decorator.hook() createHistoryManager() { return new HistoryManager({ graph: this.graph, ...this.options.history }) } @Decorator.hook() createScroller() { if (this.options.scroller.enabled) { return new Scroller({ graph: this.graph, ...this.options.scroller }) } return null } @Decorator.hook() createScrollerManager() { return new ScrollerManager(this.graph) } @Decorator.hook() // eslint-disable-next-line allowPanning(e: JQuery.MouseDownEvent) { return true } @Decorator.hook() createMiniMap() { const { enabled, ...options } = this.options.minimap if (enabled) { return new MiniMap({ graph: this.graph, ...options, }) } return null } @Decorator.hook() createMiniMapManager() { return new MiniMapManager(this.graph) } @Decorator.hook() createKeyboard() { return new Keyboard({ graph: this.graph, ...this.options.keyboard }) } @Decorator.hook() createMouseWheel() { return new MouseWheel({ graph: this.graph, ...this.options.mousewheel }) } @Decorator.hook() createPrintManager() { return new PrintManager(this.graph) } @Decorator.hook() createFormatManager() { return new FormatManager(this.graph) } @Decorator.hook() createPanningManager() { return new PanningManager(this.graph) } @Decorator.hook() createSizeManager() { return new SizeManager(this.graph) } protected allowConnectToBlank(edge: Edge) { const options = this.options.connecting const allowBlank = options.allowBlank != null ? options.allowBlank : options.dangling if (typeof allowBlank !== 'function') { return !!allowBlank } const edgeView = this.graph.findViewByCell(edge) as EdgeView const sourceCell = edge.getSourceCell() const targetCell = edge.getTargetCell() const sourceView = this.graph.findViewByCell(sourceCell) const targetView = this.graph.findViewByCell(targetCell) return FunctionExt.call(allowBlank, this.graph, { edge, edgeView, sourceCell, targetCell, sourceView, targetView, sourcePort: edge.getSourcePortId(), targetPort: edge.getTargetPortId(), sourceMagnet: edgeView.sourceMagnet, targetMagnet: edgeView.targetMagnet, }) } validateEdge( edge: Edge, type: Edge.TerminalType, initialTerminal: Edge.TerminalData, ) { if (!this.allowConnectToBlank(edge)) { const sourceId = edge.getSourceCellId() const targetId = edge.getTargetCellId() if (!(sourceId && targetId)) { return false } } const validate = this.options.connecting.validateEdge if (validate) { return FunctionExt.call(validate, this.graph, { edge, type, previous: initialTerminal, }) } return true } validateMagnet( cellView: CellView, magnet: Element, e: JQuery.MouseDownEvent | JQuery.MouseEnterEvent, ) { if (magnet.getAttribute('magnet') !== 'passive') { const validate = this.options.connecting.validateMagnet if (validate) { return FunctionExt.call(validate, this.graph, { e, magnet, view: cellView, cell: cellView.cell, }) } return true } return false } getDefaultEdge(sourceView: CellView, sourceMagnet: Element) { let edge: Edge | undefined | null | void const create = this.options.connecting.createEdge if (create) { edge = FunctionExt.call(create, this.graph, { sourceMagnet, sourceView, sourceCell: sourceView.cell, }) } if (edge == null) { edge = new StandardEdge() } return edge as Edge } validateConnection( sourceView: CellView | null | undefined, sourceMagnet: Element | null | undefined, targetView: CellView | null | undefined, targetMagnet: Element | null | undefined, terminalType: Edge.TerminalType, edgeView?: EdgeView | null | undefined, candidateTerminal?: Edge.TerminalCellData | null | undefined, ) { const options = this.options.connecting const allowLoop = options.allowLoop const allowNode = options.allowNode const allowEdge = options.allowEdge const allowPort = options.allowPort const allowMulti = options.allowMulti != null ? options.allowMulti : options.multi const validate = options.validateConnection const edge = edgeView ? edgeView.cell : null const terminalView = terminalType === 'target' ? targetView : sourceView const terminalMagnet = terminalType === 'target' ? targetMagnet : sourceMagnet let valid = true const doValidate = ( validate: (this: Graph, args: Options.ValidateConnectionArgs) => boolean, ) => { const sourcePort = terminalType === 'source' ? candidateTerminal ? candidateTerminal.port : null : edge ? edge.getSourcePortId() : null const targetPort = terminalType === 'target' ? candidateTerminal ? candidateTerminal.port : null : edge ? edge.getTargetPortId() : null return FunctionExt.call(validate, this.graph, { edge, edgeView, sourceView, targetView, sourcePort, targetPort, sourceMagnet, targetMagnet, sourceCell: sourceView ? sourceView.cell : null, targetCell: targetView ? targetView.cell : null, type: terminalType, }) } if (allowLoop != null) { if (typeof allowLoop === 'boolean') { if (!allowLoop && sourceView === targetView) { valid = false } } else { valid = doValidate(allowLoop) } } if (valid && allowPort != null) { if (typeof allowPort === 'boolean') { if (!allowPort && terminalMagnet) { valid = false } } else { valid = doValidate(allowPort) } } if (valid && allowEdge != null) { if (typeof allowEdge === 'boolean') { if (!allowEdge && EdgeView.isEdgeView(terminalView)) { valid = false } } else { valid = doValidate(allowEdge) } } if (valid && allowNode != null) { if (typeof allowNode === 'boolean') { if (!allowNode && terminalView != null) { if (NodeView.isNodeView(terminalView) && terminalMagnet == null) { valid = false } } } else { valid = doValidate(allowNode) } } if (valid && allowMulti != null && edgeView) { const edge = edgeView.cell const source = terminalType === 'source' ? candidateTerminal : (edge.getSource() as Edge.TerminalCellData) const target = terminalType === 'target' ? candidateTerminal : (edge.getTarget() as Edge.TerminalCellData) const terminalCell = candidateTerminal ? this.graph.getCellById(candidateTerminal.cell) : null if (source && target && source.cell && target.cell && terminalCell) { if (typeof allowMulti === 'function') { valid = doValidate(allowMulti) } else { const connectedEdges = this.model.getConnectedEdges(terminalCell, { outgoing: terminalType === 'source', incoming: terminalType === 'target', }) if (connectedEdges.length) { if (allowMulti === 'withPort') { const exist = connectedEdges.some((link) => { const s = link.getSource() as Edge.TerminalCellData const t = link.getTarget() as Edge.TerminalCellData return ( s && t && s.cell === source.cell && t.cell === target.cell && s.port != null && s.port === source.port && t.port != null && t.port === target.port ) }) if (exist) { valid = false } } else if (!allowMulti) { const exist = connectedEdges.some((link) => { const s = link.getSource() as Edge.TerminalCellData const t = link.getTarget() as Edge.TerminalCellData return ( s && t && s.cell === source.cell && t.cell === target.cell ) }) if (exist) { valid = false } } } } } } if (valid && validate != null) { valid = doValidate(validate) } return valid } getRestrictArea(view?: NodeView): Rectangle.RectangleLike | null { const restrict = this.options.translating.restrict const area = typeof restrict === 'function' ? FunctionExt.call(restrict, this.graph, view!) : restrict if (typeof area === 'number') { return this.graph.transform.getGraphArea().inflate(area) } if (area === true) { return this.graph.transform.getGraphArea() } return area || null } @Decorator.after() onViewUpdated( view: CellView, flag: number, options: Renderer.RequestViewUpdateOptions, ) { if (flag & Renderer.FLAG_INSERT || options.mounting) { return } this.graph.renderer.requestConnectedEdgesUpdate(view, options) } @Decorator.after() onViewPostponed( view: CellView, flag: number, options: Renderer.UpdateViewOptions, // eslint-disable-line ) { return this.graph.renderer.forcePostponedViewUpdate(view, flag) } @Decorator.hook() getCellView( cell: Cell, // eslint-disable-line ): null | undefined | typeof CellView | (new (...args: any[]) => CellView) { return null } @Decorator.hook(true) createCellView(cell: Cell) { const options = { graph: this.graph } const ctor = this.getCellView(cell) if (ctor) { return new ctor(cell, options) // eslint-disable-line new-cap } const view = cell.view if (view != null && typeof view === 'string') { const def = CellView.registry.get(view) if (def) { return new def(cell, options) // eslint-disable-line new-cap } return CellView.registry.onNotFound(view) } if (cell.isNode()) { return new NodeView(cell, options) } if (cell.isEdge()) { return new EdgeView(cell, options) } return null } @Decorator.hook() getHTMLComponent(node: HTML): HTMLElement | string | null | undefined { let ret = node.getHTML() if (typeof ret === 'string') { ret = HTML.componentRegistry.get(ret) || ret } if (ObjectExt.isPlainObject(ret)) { ret = (ret as HTML.UpdatableComponent).render } if (typeof ret === 'function') { return FunctionExt.call(ret, this.graph, node) } return ret as HTML.Elem } @Decorator.hook() shouldUpdateHTMLComponent(node: HTML): boolean { let html = node.getHTML() if (typeof html === 'string') { html = HTML.componentRegistry.get(html) || html } if (ObjectExt.isPlainObject(html)) { const shouldUpdate = (html as HTML.UpdatableComponent) .shouldComponentUpdate if (typeof shouldUpdate === 'function') { return FunctionExt.call(shouldUpdate, this.graph, node) } return !!shouldUpdate } return false } @Decorator.hook() onEdgeLabelRendered(args: Hook.OnEdgeLabelRenderedArgs) {} // eslint-disable-line @Decorator.hook() onPortRendered(args: Hook.OnPortRenderedArgs) {} // eslint-disable-line @Decorator.hook() onToolItemCreated(args: Hook.OnToolItemCreatedArgs) {} // eslint-disable-line } export namespace Hook { type CreateManager<T> = (this: Graph) => T type CreateManagerWidthNode<T> = (this: Graph, node: Node) => T type CreateManagerWidthOptions<T, Options> = ( this: Graph, options: Options, ) => T export interface OnEdgeLabelRenderedArgs { edge: Edge label: Edge.Label container: Element selectors: Markup.Selectors } export interface OnPortRenderedArgs { node: Node port: PortManager.Port container: Element selectors?: Markup.Selectors labelContainer: Element labelSelectors?: Markup.Selectors contentContainer: Element contentSelectors?: Markup.Selectors } export interface OnToolItemCreatedArgs { name: string cell: Cell view: CellView tool: View } export interface IHook { createView: CreateManager<GraphView> createModel: CreateManager<Model> createRenderer: CreateManager<Renderer> createDefsManager: CreateManager<DefsManager> createGridManager: CreateManager<GridManager> createCoordManager: CreateManager<CoordManager> createHighlightManager: CreateManager<HighlightManager> createBackgroundManager: CreateManager<BackgroundManager> createSizeManager: CreateManager<SizeManager> createTransform: CreateManagerWidthNode<Transform | null> createTransformManager: CreateManager<TransformManager> createClipboard: CreateManager<Clipboard> createClipboardManager: CreateManager<ClipboardManager> createSnapline: CreateManager<Snapline> createSnaplineManager: CreateManager<SnaplineManager> createSelection: CreateManager<Selection> createSelectionManager: CreateManager<SelectionManager> allowRubberband: (e: JQuery.MouseDownEvent) => boolean createHistoryManager: CreateManagerWidthOptions< HistoryManager, HistoryManager.Options > createScroller: CreateManager<Scroller | null> createScrollerManager: CreateManager<ScrollerManager> allowPanning: (e: JQuery.MouseDownEvent) => boolean createMiniMap: CreateManager<MiniMap | null> createMiniMapManager: CreateManager<MiniMapManager> createKeyboard: CreateManager<Keyboard> createMouseWheel: CreateManager<MouseWheel> createPrintManager: CreateManager<PrintManager> createFormatManager: CreateManager<FormatManager> createPanningManager: CreateManager<PanningManager> createCellView(this: Graph, cell: Cell): CellView | null | undefined getCellView( this: Graph, cell: Cell, ): null | undefined | typeof CellView | (new (...args: any[]) => CellView) getHTMLComponent( this: Graph, node: HTML, ): HTMLElement | string | null | undefined shouldUpdateHTMLComponent(this: Graph, node: HTML): boolean onViewUpdated: ( this: Graph, view: CellView, flag: number, options: Renderer.RequestViewUpdateOptions, ) => void onViewPostponed: ( this: Graph, view: CellView, flag: number, options: Renderer.UpdateViewOptions, ) => boolean onEdgeLabelRendered(this: Graph, args: OnEdgeLabelRenderedArgs): void onPortRendered(this: Graph, args: OnPortRenderedArgs): void onToolItemCreated(this: Graph, args: OnToolItemCreatedArgs): void } }