UNPKG

@reactodia/workspace

Version:

Reactodia Workspace -- library for visual interaction with graphs in a form of a diagram.

256 lines (232 loc) 8.19 kB
import * as React from 'react'; import { Events, EventSource, EventObserver, PropertyChange } from '../coreUtils/events'; import { TemplateProperties } from '../data/schema'; import type { CanvasApi, CanvasDropEvent, CanvasWidgetDescription, } from './canvasApi'; import type { ElementTemplate, LinkTemplate, RenameLinkProvider } from './customization'; import { Element, Link } from './elements'; import type { LayoutFunction } from './layout'; /** * Event data for {@link SharedCanvasState} events. * * @see {@link SharedCanvasState} */ export interface SharedCanvasStateEvents { /** * Triggered on {@link SharedCanvasState.highlighter} property change. */ changeHighlight: PropertyChange< SharedCanvasState, CellHighlighter | undefined >; /** * Triggered on {@link SharedCanvasState.widgets} property change. */ changeWidgets: PropertyChange< SharedCanvasState, ReadonlyMap<string, CanvasWidgetDescription> >; /** * Triggered on a request to find all canvases using this state. */ findCanvas: FindCanvasEvent; /** * Triggered when all rendering-related state should be disposed. */ dispose: { /** * Event source (shared canvas state). */ readonly source: SharedCanvasState; }; } /** * Event data for a request to find all canvases using this state. * * @see {@link SharedCanvasStateEvents.findCanvas} */ export interface FindCanvasEvent { /** * Collects found canvas instances. */ readonly canvases: CanvasApi[]; } /** * For a each diagram cell tells whether it should be highlighted or blurred. */ export type CellHighlighter = (item: Element | Link) => boolean; /** @hidden */ export interface SharedCanvasStateOptions { defaultElementTemplate: ElementTemplate; defaultLinkTemplate: LinkTemplate; defaultLayout: LayoutFunction; renameLinkProvider?: RenameLinkProvider; } /** * Stores common state and settings for multiple canvases. * * @category Core */ export class SharedCanvasState { private readonly listener = new EventObserver(); private readonly source = new EventSource<SharedCanvasStateEvents>(); /** * Event for the shared canvas state. */ readonly events: Events<SharedCanvasStateEvents> = this.source; private disposed = false; private _canvasWidgets: ReadonlyMap<string, CanvasWidgetDescription>; private dropOnPaperHandler: ((e: CanvasDropEvent) => void) | undefined; private _highlighter: CellHighlighter | undefined; /** * Default element template to use as a fallback. */ readonly defaultElementTemplate: ElementTemplate; /** * Default link template to use as a fallback. */ readonly defaultLinkTemplate: LinkTemplate; /** * Default layout algorithm function to use if it's not specified explicitly. */ readonly defaultLayout: LayoutFunction; /** * A strategy to rename diagram links (change labels). */ readonly renameLinkProvider: RenameLinkProvider | undefined; /** @hidden */ constructor(options: SharedCanvasStateOptions) { const { defaultElementTemplate, defaultLinkTemplate, defaultLayout, renameLinkProvider, } = options; this._canvasWidgets = new Map(); this.defaultElementTemplate = defaultElementTemplate; this.defaultLinkTemplate = defaultLinkTemplate; this.defaultLayout = defaultLayout; this.renameLinkProvider = renameLinkProvider; } /** @hidden */ dispose(): void { if (this.disposed) { return; } this.source.trigger('dispose', {source: this}); this.listener.stopListening(); this.disposed = true; } /** * Returns all canvases that use this shared state. */ findAllCanvases(): CanvasApi[] { const event: FindCanvasEvent = {canvases: []}; this.source.trigger('findCanvas', event); return event.canvases; } /** * Returns any canvas that uses this shared state or `undefined` if none found. */ findAnyCanvas(): CanvasApi | undefined { const canvases = this.findAllCanvases(); return canvases.length > 0 ? canvases[0] : undefined; } /** * Live collection of canvas widgets rendered on each canvas. */ get widgets(): ReadonlyMap<string, CanvasWidgetDescription> { return this._canvasWidgets; } /** * Adds, changes or removes a canvas widget from being rendered on the canvases. * * @param key unique key for a widget * @param widget widget description with a target widget layer to render on * or `null` to remove the widget */ setCanvasWidget(key: string, widget: CanvasWidgetDescription | null): void { const previous = this._canvasWidgets; const nextWidgets = new Map(previous); if (widget) { const description: CanvasWidgetDescription = { element: React.cloneElement(widget.element, {key}), attachment: widget.attachment, }; nextWidgets.set(key, description); } else { nextWidgets.delete(key); } this._canvasWidgets = nextWidgets; this.source.trigger('changeWidgets', {source: this, previous}); } /** * Sets the handler for the next drop event from drag-and-drop operation on a canvas. * * **Experimental**: this feature will likely change in the future. */ setHandlerForNextDropOnPaper(handler: ((e: CanvasDropEvent) => void) | undefined): void { this.dropOnPaperHandler = handler; } /** * Tries to run previously set drop handler on a canvas, * then removes the handler if it was set. * * **Experimental**: this feature will likely change in the future. * * @returns `true` if a handler was set, otherwise `false` */ tryHandleDropOnPaper(e: CanvasDropEvent): boolean { const {dropOnPaperHandler} = this; if (dropOnPaperHandler) { this.dropOnPaperHandler = undefined; e.sourceEvent.preventDefault(); dropOnPaperHandler(e); return true; } return false; } /** * Returns active highlight for the diagram cells. * * **Experimental**: this feature will likely change in the future. */ get highlighter(): CellHighlighter | undefined { return this._highlighter; } /** * Sets or removes an active highlight for the diagram cells. * * **Experimental**: this feature will likely change in the future. */ setHighlighter(value: CellHighlighter | undefined): void { const previous = this._highlighter; if (previous === value) { return; } this._highlighter = value; this.source.trigger('changeHighlight', {source: this, previous}); } } /** * A strategy to rename diagram links which stores changed link label * in the link template state. * * @see {@link TemplateProperties.CustomLabel} */ export class RenameLinkToLinkStateProvider implements RenameLinkProvider { canRename(link: Link): boolean { return true; } getLabel(link: Link): string | undefined { const {linkState} = link; if ( linkState && Object.prototype.hasOwnProperty.call(linkState, TemplateProperties.CustomLabel) ) { const customLabel = linkState[TemplateProperties.CustomLabel]; if (typeof customLabel === 'string') { return customLabel; } } return undefined; } setLabel(link: Link, label: string): void { link.setLinkState({ ...link.linkState, [TemplateProperties.CustomLabel]: label.length === 0 ? undefined : label, }); } }