@reactodia/workspace
Version:
Reactodia Workspace -- library for visual interaction with graphs in a form of a diagram.
256 lines (232 loc) • 8.19 kB
text/typescript
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,
});
}
}