UNPKG

@reactodia/workspace

Version:

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

460 lines (415 loc) 12.5 kB
import { EventSource, Events, PropertyChange } from '../coreUtils/events'; import { LinkTypeIri } from '../data/model'; import { TemplateState, TemplateProperties } from '../data/schema'; import { generate128BitID } from '../data/utils'; import { Vector, isPolylineEqual } from './geometry'; /** * Represents a diagram content object that can be interacted with. * * @category Core */ export type Cell = Element | Link | LinkVertex; /** * Event data for{@link Element} events. */ export interface ElementEvents { /** * Triggered on {@link Element.position} property change. */ changePosition: PropertyChange<Element, Vector>; /** * Triggered on {@link Element.elementState} property change. */ changeElementState: PropertyChange<Element, TemplateState>; /** * Triggered on a request to set DOM focus on the element. */ requestedFocus: { /** * Event source (element). */ readonly source: Element; }; /** * Triggered on a request to re-render element on a canvas. * * @see {@link Element.redraw} */ requestedRedraw: { /** * Event source (element). */ readonly source: Element; /** * Element re-render level: which cached state should be invalidated * when redrawing it on a canvas. * * @default "render" */ readonly level?: ElementRedrawLevel; }; } /** * Specifies which cached state should be invalidated * when redrawing an element on a canvas: * - `render` - force render only on a wrapping component, * skipping element template render if template type has not changed; * - `template` - full element render including its template component. * * @see {@link Element.redraw} */ export type ElementRedrawLevel = 'render' | 'template'; /** * Properties for {@link Element}. */ export interface ElementProps { /** * Unique and immutable {@link Element.id element ID}. * * If not specified, {@link Element.generateId()} is used to create one. */ id?: string; /** * Initial value for the {@link Element.position element position}. */ position?: Vector; /** * Initial value for the {@link Element.isExpanded} state. * * If specified as `true`, the value is added to the {@link Element.elementState} * with {@link TemplateProperties.Expanded} property. */ expanded?: boolean; /** * Initial value for the {@link Element.elementState element template state}. */ elementState?: TemplateState; } /** * Diagram element (graph node). * * @category Core */ export abstract class Element { /** * Event source to trigger events from derived element types. */ protected readonly source = new EventSource<ElementEvents>(); /** * Events for the graph element. */ readonly events: Events<ElementEvents> = this.source; /** * Unique and immutable element ID on the diagram. */ readonly id: string; private _position: Vector; private _elementState: TemplateState; constructor(props: ElementProps) { const { id = Element.generateId(), position = {x: 0, y: 0}, expanded = false, elementState = TemplateState.empty, } = props; this.id = id; this._position = position; this._elementState = expanded ? elementState.set(TemplateProperties.Expanded, expanded) : elementState; } /** * Generates a new unique ID for an element. */ static generateId(): string { return `urn:reactodia:e:${generate128BitID()}`; } /** * Gets the element position on the canvas in paper coordinates. */ get position(): Vector { return this._position; } /** * Sets a new value for {@link position} property. * * Triggers {@link ElementEvents.changePosition} event if new value does * not equal to the previous one. * * @see {@link RestoreGeometry} */ setPosition(value: Vector): void { const previous = this._position; const same = ( previous.x === value.x && previous.y === value.y ); if (same) { return; } this._position = value; this.source.trigger('changePosition', {source: this, previous}); } /** * Whether the element should be displayed as expanded * (as defined by the element template). * * Expanded state is stored in the {@link Element.elementState element state} * with {@link TemplateProperties.Expanded} property. */ get isExpanded(): boolean { return Boolean(this._elementState.get(TemplateProperties.Expanded)); } /** * Sets a new value for {@link isExpanded} property. * * Expanded state is stored in the {@link Element.elementState element state} * with {@link TemplateProperties.Expanded} property. * * Triggers {@link ElementEvents.changeElementState} event if new value does * not equal to the previous one. */ setExpanded(value: boolean): void { this.setElementState( this._elementState.set(TemplateProperties.Expanded, value ? true : undefined) ); } /** * Gets a serializable template-specific state for the element. */ get elementState(): TemplateState { return this._elementState; } /** * Sets a new value for {@link elementState} property. * * Triggers {@link ElementEvents.changeElementState} event if new value does * not equal to the previous one. */ setElementState(value: TemplateState): void { const previous = this._elementState; if (previous === value) { return; } this._elementState = value; this.source.trigger('changeElementState', {source: this, previous}); } /** * Focuses on the element template on a canvas (if possible). */ focus(): void { this.source.trigger('requestedFocus', {source: this}); } /** * Forces a re-render of the element displayed by a template on a canvas. * * @param level specifies which cached state should be invalidated on re-render */ redraw(level?: ElementRedrawLevel): void { this.source.trigger('requestedRedraw', {source: this, level}); } } /** * Diagram element represented by an invisible single point. * * @category Core */ export class VoidElement extends Element { constructor(props: Pick<ElementProps, 'id' | 'position'>) { super(props); } } /** * Event data for {@link Link} events. */ export interface LinkEvents { /** * Triggered on {@link Link.vertices} property change. */ changeVertices: PropertyChange<Link, ReadonlyArray<Vector>>; /** * Triggered on {@link Link.linkState} property change. */ changeLinkState: PropertyChange<Link, TemplateState>; /** * Triggered on a request to re-render link on a canvas. * * @see {@link Link.redraw} */ requestedRedraw: { /** * Event source (link). */ readonly source: Link; }; } /** * Properties for {@link Link}. */ export interface LinkProps { /** * Unique and immutable {@link Link.id link ID}. * * If not specified, {@link Link.generateId()} is used to create one. */ id?: string; /** * An immutable link {@link Link.sourceId source} ({@link Element.id element ID}). */ sourceId: string; /** * An immutable link {@link Link.targetId target} ({@link Element.id element ID}). */ targetId: string; /** * Initial value for the {@link Link.vertices link vertices (geometry)}. */ vertices?: ReadonlyArray<Vector>; /** * Initial value for the {@link Link.linkState link template state}. */ linkState?: TemplateState; } /** * Diagram link (graph edge) * * @category Core */ export abstract class Link { /** * Event source to trigger events from derived link types. */ protected readonly source = new EventSource<LinkEvents>(); /** * Events for the graph link. */ readonly events: Events<LinkEvents> = this.source; /** * Unique and immutable link ID on the diagram. */ readonly id: string; private _sourceId: string; private _targetId: string; private _vertices: ReadonlyArray<Vector>; private _linkState: TemplateState; constructor(props: LinkProps) { const { id = Link.generateId(), sourceId, targetId, vertices = [], linkState = TemplateState.empty, } = props; this.id = id; this._sourceId = sourceId; this._targetId = targetId; this._vertices = vertices; this._linkState = linkState; } /** * Generates a new unique ID for an link. */ static generateId(): string { return `urn:reactodia:l:${generate128BitID()}`; } /** * Gets an immutable link source {@link Element.id element ID}. */ get sourceId(): string { return this._sourceId; } /** * Gets an immutable link target {@link Element.id element ID}. */ get targetId(): string { return this._targetId; } /** * Gets the link type IRI. */ get typeId(): LinkTypeIri { return this.getTypeId(); } /** * Should return the link type IRI. * * For derived link types without natural type IRIs the synthetic IRIs can be * used, e.g. `my:custom:link`. */ protected abstract getTypeId(): LinkTypeIri; /** * Gets the link geometry (intermediate points in paper coordinates in order * from the link source to the target). */ get vertices(): ReadonlyArray<Vector> { return this._vertices; } /** * Sets a new value for {@link vertices} property. * * Triggers {@link LinkEvents.changeVertices} event if new geometry * does not equal to the previous one. * * @see {@link RestoreGeometry} * @see {@link restoreCapturedLinkGeometry()} */ setVertices(value: ReadonlyArray<Vector>): void { const previous = this._vertices; if (isPolylineEqual(this._vertices, value)) { return; } this._vertices = value; this.source.trigger('changeVertices', {source: this, previous}); } /** * Gets a serializable template-specific state for the link. */ get linkState(): TemplateState { return this._linkState; } /** * Sets a new value for {@link linkState} property. * * Triggers {@link LinkEvents.changeLinkState} event if new value does * not equal to the previous one. */ setLinkState(value: TemplateState): void { const previous = this._linkState; if (previous === value) { return; } this._linkState = value; this.source.trigger('changeLinkState', {source: this, previous}); } /** * Forces a re-render of the link displayed by a template on a canvas. */ redraw(): void { this.source.trigger('requestedRedraw', {source: this}); } } /** * Visibility mode for all links of a type: * - `hidden` - completely skip rendering the links; * - `visible` - display the links normally; * - `withoutLabel` - display only the link path without any labels. */ export type LinkTypeVisibility = 'hidden' | 'visible' | 'withoutLabel'; /** * Represents a convenient way to refer to a particular vertex of a diagram link. * * @category Core */ export class LinkVertex { constructor( readonly link: Link, readonly vertexIndex: number, ) {} createAt(location: Vector) { const vertices = [...this.link.vertices]; vertices.splice(this.vertexIndex, 0, location); this.link.setVertices(vertices); } moveTo(location: Vector) { const vertices = [...this.link.vertices]; vertices.splice(this.vertexIndex, 1, location); this.link.setVertices(vertices); } remove() { const vertices = [...this.link.vertices]; const [location] = vertices.splice(this.vertexIndex, 1); this.link.setVertices(vertices); } }