UNPKG

@metadev/daga-angular

Version:

Diagramming engine for editing models on the Web. Made by Metadev.

903 lines (896 loc) 137 kB
import { Side, DiagramCanvas, DiagramActions, Corner, layouts, getLocationsOfNodes, ApplyLayoutAction, Events, setCursorStyle, CursorStyle, DragEvents, filterByOnlyDescendants, AddNodeAction, generalClosedPath, DIAGRAM_FIELD_DEFAULTS, getLeftMargin, getTopMargin, Type, Property, isObject, equals, Keys, DagaImporter, DagaExporter } from '@metadev/daga'; export { ACTION_STACK_SIZE, ActionStack, AddConnectionAction, AddNodeAction, AdjacencyLayout, ApplyLayoutAction, BreadthAdjacencyLayout, BreadthLayout, ClosedShape, CollabClient, Corner, DagaExporter, DagaImporter, DiagramActionMethod, DiagramActions, DiagramCanvas, DiagramConnection, DiagramConnectionSet, DiagramConnectionType, DiagramDecorator, DiagramDecoratorSet, DiagramDoubleClickEvent, DiagramElement, DiagramElementSet, DiagramEntitySet, DiagramEvent, DiagramEvents, DiagramField, DiagramFieldSet, DiagramHighlightedEvent, DiagramModel, DiagramNode, DiagramNodeSet, DiagramNodeType, DiagramObject, DiagramObjectSet, DiagramPort, DiagramPortSet, DiagramPortType, DiagramSecondaryClickEvent, DiagramSection, DiagramSectionSet, DiagramSelectionEvent, EditFieldAction, ForceLayout, HorizontalAlign, HorizontalLayout, LineShape, LineStyle, MoveAction, PasteAction, PriorityLayout, Property, PropertySet, RemoveAction, SetGeometryAction, SetParentAction, Side, TreeLayout, Type, UpdateValuesAction, ValueSet, VerticalAlign, VerticalLayout, getLocationsOfNodes, layouts } from '@metadev/daga'; import * as i0 from '@angular/core'; import { ElementRef, Input, Component, Injectable, inject, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef, EventEmitter, Output, NgModule } from '@angular/core'; import * as d3 from 'd3'; import * as i2 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i1 from '@angular/forms'; import { FormsModule } from '@angular/forms'; import { ReplaySubject, merge, map, skip } from 'rxjs'; /** * Button used to collapse components that implement it. * @private */ class CollapseButtonComponent { constructor() { this.collapsed = false; this.disabled = false; this.direction = Side.Bottom; this.rule = 'visibility'; this.collapsedValue = 'collapse'; this.visibleValue = 'visible'; this.Side = Side; } toggleCollapse() { if (!this.disabled) { this.collapsed = !this.collapsed; let selection; if (this.collapsableSelector instanceof ElementRef) { selection = d3.select(this.collapsableSelector.nativeElement); if (this.collapsableAdditionalSelector) { selection = selection.select(this.collapsableAdditionalSelector); } } else if (this.collapsableSelector instanceof HTMLElement) { selection = d3.select(this.collapsableSelector); if (this.collapsableAdditionalSelector) { selection = selection.select(this.collapsableAdditionalSelector); } } else { selection = d3.select(this.collapsableSelector); if (this.collapsableAdditionalSelector) { selection = selection.select(this.collapsableAdditionalSelector); } } selection.style(this.rule, this.collapsed ? this.collapsedValue : this.visibleValue); } } getClass() { switch (this.direction) { case Side.Right: if (this.disabled) { return 'daga-horizontal-none'; } else if (this.collapsed) { return 'daga-horizontal-right'; } else { return 'daga-horizontal-left'; } case Side.Bottom: if (this.disabled) { return 'daga-vertical-none'; } else if (this.collapsed) { return 'daga-vertical-down'; } else { return 'daga-vertical-up'; } case Side.Left: if (this.disabled) { return 'daga-horizontal-none'; } else if (this.collapsed) { return 'daga-horizontal-left'; } else { return 'daga-horizontal-right'; } case Side.Top: if (this.disabled) { return 'daga-vertical-none'; } else if (this.collapsed) { return 'daga-vertical-up'; } else { return 'daga-vertical-down'; } } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CollapseButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.3", type: CollapseButtonComponent, isStandalone: true, selector: "daga-collapse-button", inputs: { collapsableSelector: "collapsableSelector", collapsableAdditionalSelector: "collapsableAdditionalSelector", collapsed: "collapsed", disabled: "disabled", direction: "direction", rule: "rule", collapsedValue: "collapsedValue", visibleValue: "visibleValue" }, ngImport: i0, template: "<button\r\n class=\"daga-collapse-button daga-{{ direction }}\"\r\n (click)=\"toggleCollapse()\"\r\n>\r\n <div [class]=\"getClass()\"></div>\r\n</button>\r\n" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CollapseButtonComponent, decorators: [{ type: Component, args: [{ selector: 'daga-collapse-button', template: "<button\r\n class=\"daga-collapse-button daga-{{ direction }}\"\r\n (click)=\"toggleCollapse()\"\r\n>\r\n <div [class]=\"getClass()\"></div>\r\n</button>\r\n" }] }], propDecorators: { collapsableSelector: [{ type: Input }], collapsableAdditionalSelector: [{ type: Input }], collapsed: [{ type: Input }], disabled: [{ type: Input }], direction: [{ type: Input }], rule: [{ type: Input }], collapsedValue: [{ type: Input }], visibleValue: [{ type: Input }] } }); /** * A provider for the {@link Canvas} associated with a {@link DiagramComponent} context. * @public */ class CanvasProviderService { constructor() { this.canvasSubject$ = new ReplaySubject(); /** * Subject used to track when the canvas has been updated. * @public */ this.canvas$ = this.canvasSubject$.asObservable(); this.viewInitializedSubject$ = new ReplaySubject(); /** * Subject used to track when the view of the canvas has been updated after updating the canvas. * @public */ this.viewInitialized$ = this.viewInitializedSubject$.asObservable(); } /** * Initialize the diagram canvas object of this context using the given diagram configuration. * @private * @param parentComponent A diagram editor. * @param config A diagram configuration. */ initCanvas(parentComponent, config) { this._canvas = new DiagramCanvas(parentComponent, config); this.canvasSubject$.next(this._canvas); } /** * Attach the canvas of this context to an HTML element to render it there. * @private * @param appendTo An HTML element. */ initCanvasView(appendTo) { this._canvas.initView(appendTo); this.viewInitializedSubject$.next(this._canvas); } /** * Get the current canvas of this context. * @public * @returns A canvas. */ getCanvas() { return this._canvas; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CanvasProviderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CanvasProviderService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CanvasProviderService, decorators: [{ type: Injectable }] }); /** * A provider for the {@link DiagramConfig} associated with a {@link DiagramComponent} context. * @public */ class DagaConfigurationService { constructor() { this.configSubject$ = new ReplaySubject(); /** * Subject used to track when the diagram configuration has been updated. * @public */ this.config$ = this.configSubject$.asObservable(); } /** * Set the diagram configuration of this context. * @private * @param config A diagram configuration. */ init(config) { this._config = config; this.configSubject$.next(config); } /** * Get the current diagram configuration of this context. * @public * @returns A diagram configuration. */ getConfig() { return this._config; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: DagaConfigurationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: DagaConfigurationService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: DagaConfigurationService, decorators: [{ type: Injectable }] }); /** * Buttons used to trigger diagram functionalities by the user. * @private */ class DiagramButtonsComponent { constructor() { this.#canvasProvider = inject(CanvasProviderService); this.#configService = inject(DagaConfigurationService); this.enableAction = true; this.enableFilter = false; this.enableLayout = false; this.enableSelection = true; this.enableZoom = true; this.filterOn = false; this.collapsed = true; this.animationOngoing = false; this.DiagramActions = DiagramActions; } #canvasProvider; #configService; get canvas() { return this.#canvasProvider.getCanvas(); } ngOnInit() { this.sub = this.#configService.config$.subscribe((c) => { this.init(c); }); } ngAfterViewInit() { this.recalculateSizeOfButtons(); } ngOnDestroy() { this.sub?.unsubscribe(); } init(config) { this.filterOn = false; this.collapsed = true; this.animationOngoing = false; this.location = config.components?.buttons?.location || Corner.BottomRight; this.direction = config.components?.buttons?.direction || Side.Top; this.enableAction = config.components?.buttons?.enableAction !== false; this.enableFilter = config.components?.buttons?.enableFilter === true; this.enableLayout = config.components?.buttons?.enableLayout === true; this.enableSelection = config.components?.buttons?.enableSelection !== false; this.enableZoom = config.components?.buttons?.enableZoom !== false; switch (this.direction) { case Side.Bottom: this.sizeAttribute = 'height'; this.transformFunction = 'scaleY'; this.transformOrigin = 'top'; this.marginSide = 'bottom'; break; case Side.Top: this.sizeAttribute = 'height'; this.transformFunction = 'scaleY'; this.transformOrigin = 'bottom'; this.marginSide = 'top'; break; case Side.Left: this.sizeAttribute = 'width'; this.transformFunction = 'scaleX'; this.transformOrigin = 'right'; this.marginSide = 'left'; break; case Side.Right: this.sizeAttribute = 'width'; this.transformFunction = 'scaleX'; this.transformOrigin = 'left'; this.marginSide = 'right'; break; } this.recalculateSizeOfButtons(); } recalculateSizeOfButtons() { if (this.collapsableButtons) { const numberOfCollapsableButtons = (this.enableZoom && this.canvas.canUserPerformAction(DiagramActions.Zoom) ? 1 : 0) + (this.enableAction ? 2 : 0) + (this.enableSelection ? 5 : 0) + (this.enableLayout ? 1 : 0) + (this.enableFilter ? 1 : 0); this.collapsableButtonsSize = `${4 * numberOfCollapsableButtons}rem`; d3.select(this.collapsableButtons.nativeElement) .style(`margin-${this.marginSide}`, '-1rem') .style(this.sizeAttribute, '0rem') .style('transform', `${this.transformFunction}(0)`) .style('transform-origin', this.transformOrigin); } } // Collapse functions async toggleCollapse() { if (!this.animationOngoing) { const duration = 500; const collapsableButtons = d3.select(this.collapsableButtons.nativeElement); if (this.collapsed) { this.collapsed = false; collapsableButtons .transition() .duration(duration) .ease(d3.easeLinear) .style(this.sizeAttribute, this.collapsableButtonsSize) .style('transform', `${this.transformFunction}(1)`); setTimeout(() => { this.animationOngoing = false; }, duration); } else { this.collapsed = true; collapsableButtons .transition() .duration(duration) .ease(d3.easeLinear) .style(this.sizeAttribute, '0rem') .style('transform', `${this.transformFunction}(0)`); setTimeout(() => { this.animationOngoing = false; }, duration); } } } // Button functions zoomIn() { this.canvas.zoomBy(this.canvas.zoomFactor); } zoomOut() { this.canvas.zoomBy(1 / this.canvas.zoomFactor); } center() { this.canvas.center(); } layout() { if (this.canvas.layoutFormat && this.canvas.layoutFormat in layouts) { const from = getLocationsOfNodes(this.canvas.model); layouts[this.canvas.layoutFormat].apply(this.canvas.model); const to = getLocationsOfNodes(this.canvas.model); const action = new ApplyLayoutAction(this.canvas, from, to); this.canvas.actionStack.add(action); } } filter() { this.filterOn = !this.filterOn; const priorityThresholds = this.canvas.getPriorityThresholdOptions(); if (priorityThresholds && priorityThresholds.length >= 2) { this.canvas.setPriorityThreshold(priorityThresholds[this.filterOn ? 1 : 0]); } } undo() { this.canvas.actionStack.undo(); } redo() { this.canvas.actionStack.redo(); } copySelection() { this.canvas.userSelection.copyToClipboard(); } cutSelection() { this.canvas.userSelection.cutToClipboard(); } pasteSelection() { this.canvas.userSelection.pasteFromClipboard(); } deleteSelection() { this.canvas.userSelection.removeFromModel(); } startMultipleSelection() { this.canvas.multipleSelectionOn = true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: DiagramButtonsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: DiagramButtonsComponent, isStandalone: true, selector: "daga-diagram-buttons", inputs: { location: "location", direction: "direction", enableAction: "enableAction", enableFilter: "enableFilter", enableLayout: "enableLayout", enableSelection: "enableSelection", enableZoom: "enableZoom" }, viewQueries: [{ propertyName: "collapsableButtons", first: true, predicate: ["collapsableButtons"], descendants: true }], ngImport: i0, template: "<div class=\"daga-diagram-buttons daga-{{ location }} daga-{{ direction }}\">\r\n @if (enableZoom && canvas.canUserPerformAction(DiagramActions.Zoom)) {\r\n <button\r\n class=\"daga-zoom-in\"\r\n (click)=\"zoomIn()\"\r\n >\r\n <span class=\"daga-tooltip\">Zoom in</span>\r\n </button>\r\n }\r\n @if (enableZoom && canvas.canUserPerformAction(DiagramActions.Zoom)) {\r\n <button\r\n class=\"daga-zoom-out\"\r\n (click)=\"zoomOut()\"\r\n >\r\n <span class=\"daga-tooltip\">Zoom out</span>\r\n </button>\r\n }\r\n <div #collapsableButtons class=\"daga-collapsable-buttons daga-collapsed\">\r\n @if (enableZoom && canvas.canUserPerformAction(DiagramActions.Zoom)) {\r\n <button\r\n class=\"daga-center\"\r\n (click)=\"center()\"\r\n >\r\n <span class=\"daga-tooltip\">Fit diagram to screen</span>\r\n </button>\r\n }\r\n @if (enableAction) {\r\n <button class=\"daga-undo\" (click)=\"undo()\">\r\n <span class=\"daga-tooltip\">Undo</span>\r\n </button>\r\n }\r\n @if (enableAction) {\r\n <button class=\"daga-redo\" (click)=\"redo()\">\r\n <span class=\"daga-tooltip\">Redo</span>\r\n </button>\r\n }\r\n @if (enableSelection) {\r\n <button class=\"daga-copy\" (click)=\"copySelection()\">\r\n <span class=\"daga-tooltip\">Copy</span>\r\n </button>\r\n }\r\n @if (enableSelection) {\r\n <button class=\"daga-cut\" (click)=\"cutSelection()\">\r\n <span class=\"daga-tooltip\">Cut</span>\r\n </button>\r\n }\r\n @if (enableSelection) {\r\n <button\r\n class=\"daga-multiple-selection\"\r\n [class]=\"canvas.multipleSelectionOn ? 'daga-on' : 'daga-off'\"\r\n (click)=\"startMultipleSelection()\"\r\n >\r\n <span class=\"daga-tooltip\">Multiple selection</span>\r\n </button>\r\n }\r\n @if (enableSelection) {\r\n <button\r\n class=\"daga-paste\"\r\n (click)=\"pasteSelection()\"\r\n >\r\n <span class=\"daga-tooltip\">Paste</span>\r\n </button>\r\n }\r\n @if (enableSelection) {\r\n <button\r\n class=\"daga-delete\"\r\n (click)=\"deleteSelection()\"\r\n >\r\n <span class=\"daga-tooltip\">Delete</span>\r\n </button>\r\n }\r\n @if (enableLayout) {\r\n <button class=\"daga-layout\" (click)=\"layout()\">\r\n <span class=\"daga-tooltip\">Apply layout</span>\r\n </button>\r\n }\r\n @if (enableFilter) {\r\n <button\r\n class=\"daga-filter\"\r\n [class]=\"filterOn ? 'daga-on' : 'daga-off'\"\r\n (click)=\"filter()\"\r\n >\r\n <span class=\"daga-tooltip\">Apply filter</span>\r\n </button>\r\n }\r\n </div>\r\n <button class=\"daga-more-options\" (click)=\"toggleCollapse()\">\r\n @if (!collapsed) {\r\n <span class=\"daga-tooltip\">Less options</span>\r\n }\r\n @if (collapsed) {\r\n <span class=\"daga-tooltip\">More options</span>\r\n }\r\n </button>\r\n</div>\r\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: DiagramButtonsComponent, decorators: [{ type: Component, args: [{ selector: 'daga-diagram-buttons', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"daga-diagram-buttons daga-{{ location }} daga-{{ direction }}\">\r\n @if (enableZoom && canvas.canUserPerformAction(DiagramActions.Zoom)) {\r\n <button\r\n class=\"daga-zoom-in\"\r\n (click)=\"zoomIn()\"\r\n >\r\n <span class=\"daga-tooltip\">Zoom in</span>\r\n </button>\r\n }\r\n @if (enableZoom && canvas.canUserPerformAction(DiagramActions.Zoom)) {\r\n <button\r\n class=\"daga-zoom-out\"\r\n (click)=\"zoomOut()\"\r\n >\r\n <span class=\"daga-tooltip\">Zoom out</span>\r\n </button>\r\n }\r\n <div #collapsableButtons class=\"daga-collapsable-buttons daga-collapsed\">\r\n @if (enableZoom && canvas.canUserPerformAction(DiagramActions.Zoom)) {\r\n <button\r\n class=\"daga-center\"\r\n (click)=\"center()\"\r\n >\r\n <span class=\"daga-tooltip\">Fit diagram to screen</span>\r\n </button>\r\n }\r\n @if (enableAction) {\r\n <button class=\"daga-undo\" (click)=\"undo()\">\r\n <span class=\"daga-tooltip\">Undo</span>\r\n </button>\r\n }\r\n @if (enableAction) {\r\n <button class=\"daga-redo\" (click)=\"redo()\">\r\n <span class=\"daga-tooltip\">Redo</span>\r\n </button>\r\n }\r\n @if (enableSelection) {\r\n <button class=\"daga-copy\" (click)=\"copySelection()\">\r\n <span class=\"daga-tooltip\">Copy</span>\r\n </button>\r\n }\r\n @if (enableSelection) {\r\n <button class=\"daga-cut\" (click)=\"cutSelection()\">\r\n <span class=\"daga-tooltip\">Cut</span>\r\n </button>\r\n }\r\n @if (enableSelection) {\r\n <button\r\n class=\"daga-multiple-selection\"\r\n [class]=\"canvas.multipleSelectionOn ? 'daga-on' : 'daga-off'\"\r\n (click)=\"startMultipleSelection()\"\r\n >\r\n <span class=\"daga-tooltip\">Multiple selection</span>\r\n </button>\r\n }\r\n @if (enableSelection) {\r\n <button\r\n class=\"daga-paste\"\r\n (click)=\"pasteSelection()\"\r\n >\r\n <span class=\"daga-tooltip\">Paste</span>\r\n </button>\r\n }\r\n @if (enableSelection) {\r\n <button\r\n class=\"daga-delete\"\r\n (click)=\"deleteSelection()\"\r\n >\r\n <span class=\"daga-tooltip\">Delete</span>\r\n </button>\r\n }\r\n @if (enableLayout) {\r\n <button class=\"daga-layout\" (click)=\"layout()\">\r\n <span class=\"daga-tooltip\">Apply layout</span>\r\n </button>\r\n }\r\n @if (enableFilter) {\r\n <button\r\n class=\"daga-filter\"\r\n [class]=\"filterOn ? 'daga-on' : 'daga-off'\"\r\n (click)=\"filter()\"\r\n >\r\n <span class=\"daga-tooltip\">Apply filter</span>\r\n </button>\r\n }\r\n </div>\r\n <button class=\"daga-more-options\" (click)=\"toggleCollapse()\">\r\n @if (!collapsed) {\r\n <span class=\"daga-tooltip\">Less options</span>\r\n }\r\n @if (collapsed) {\r\n <span class=\"daga-tooltip\">More options</span>\r\n }\r\n </button>\r\n</div>\r\n" }] }], propDecorators: { collapsableButtons: [{ type: ViewChild, args: ['collapsableButtons'] }], location: [{ type: Input }], direction: [{ type: Input }], enableAction: [{ type: Input }], enableFilter: [{ type: Input }], enableLayout: [{ type: Input }], enableSelection: [{ type: Input }], enableZoom: [{ type: Input }] } }); /** * Displays the errors detected by a diagram's validators. * @private * @see DiagramValidator */ class ErrorsComponent { constructor() { this.#canvasProvider = inject(CanvasProviderService); this.#cdf = inject(ChangeDetectorRef); this.errors = []; this.Side = Side; } #canvasProvider; #cdf; get canvas() { return this.#canvasProvider.getCanvas(); } ngAfterViewInit() { this.canvasSub = this.#canvasProvider.canvas$.subscribe((c) => { this.updateCanvas(c); }); } ngOnDestroy() { this.canvasSub?.unsubscribe(); this.validationSub?.unsubscribe(); } selectPanel() { return d3 .select(this.errorsContainer.nativeElement) .select('.daga-error-panel'); } updateCanvas(canvas) { this.validationSub?.unsubscribe(); this.validationSub = merge(canvas.validatorChange$, canvas.diagramChange$) .pipe(map(() => this.validate())) .subscribe(); } validate() { this.errors = []; for (const validator of this.canvas.validators) { const newErrors = validator.validate(this.canvas.model); this.errors = [...this.errors, ...newErrors]; } // need to force detection changes here for some reason... this.#cdf.detectChanges(); } showError(error) { if (error.elementId && (error.propertyNames === undefined || error.propertyNames.length === 0)) { const element = this.canvas.model.nodes.get(error.elementId) || this.canvas.model.connections.get(error.elementId); if (element) { this.canvas.userHighlight.add(element); } } else if (error.elementId && error.propertyNames !== undefined && error.propertyNames.length > 0) { this.canvas.userSelection.openInPropertyEditor(this.canvas.model.nodes.get(error.elementId) || this.canvas.model.connections.get(error.elementId)); const element = this.canvas.model.nodes.get(error.elementId) || this.canvas.model.connections.get(error.elementId); if (element) { this.canvas.userHighlight.add(element); } this.canvas.parentComponent?.propertyEditor?.highlightProperty(...error.propertyNames); } else if (!error.elementId && error.propertyNames !== undefined && error.propertyNames.length > 0) { this.canvas.userSelection.openInPropertyEditor(); this.canvas.parentComponent?.propertyEditor?.highlightProperty(...error.propertyNames); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ErrorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: ErrorsComponent, isStandalone: true, selector: "daga-errors", viewQueries: [{ propertyName: "errorsContainer", first: true, predicate: ["errors"], descendants: true }], ngImport: i0, template: "<div #errorsContainer class=\"daga-errors\">\r\n @if (errors.length === 0) {\r\n <div\r\n class=\"daga-errors-summary daga-no-errors daga-prevent-user-select\"\r\n >\r\n <span>No errors found</span>\r\n </div>\r\n }\r\n @if (errors.length > 0) {\r\n <div\r\n class=\"daga-errors-summary daga-with-errors daga-prevent-user-select\"\r\n >\r\n <span>{{ errors.length }} errors found</span>\r\n <div class=\"daga-collapse-button-container\">\r\n <daga-collapse-button\r\n [collapsableSelector]=\"errorsContainer\"\r\n [collapsableAdditionalSelector]=\"'.daga-error-panel'\"\r\n [direction]=\"Side.Top\"\r\n [rule]=\"'display'\"\r\n [collapsedValue]=\"'none'\"\r\n [visibleValue]=\"'block'\"\r\n />\r\n </div>\r\n </div>\r\n }\r\n @if (errors.length > 0) {\r\n <div class=\"daga-error-panel\">\r\n <ol>\r\n @for (error of errors; track error; let i = $index) {\r\n <li\r\n (click)=\"showError(error)\"\r\n [innerHTML]=\"error.message\"\r\n ></li>\r\n }\r\n </ol>\r\n </div>\r\n }\r\n </div>\r\n", dependencies: [{ kind: "component", type: CollapseButtonComponent, selector: "daga-collapse-button", inputs: ["collapsableSelector", "collapsableAdditionalSelector", "collapsed", "disabled", "direction", "rule", "collapsedValue", "visibleValue"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ErrorsComponent, decorators: [{ type: Component, args: [{ selector: 'daga-errors', imports: [CollapseButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div #errorsContainer class=\"daga-errors\">\r\n @if (errors.length === 0) {\r\n <div\r\n class=\"daga-errors-summary daga-no-errors daga-prevent-user-select\"\r\n >\r\n <span>No errors found</span>\r\n </div>\r\n }\r\n @if (errors.length > 0) {\r\n <div\r\n class=\"daga-errors-summary daga-with-errors daga-prevent-user-select\"\r\n >\r\n <span>{{ errors.length }} errors found</span>\r\n <div class=\"daga-collapse-button-container\">\r\n <daga-collapse-button\r\n [collapsableSelector]=\"errorsContainer\"\r\n [collapsableAdditionalSelector]=\"'.daga-error-panel'\"\r\n [direction]=\"Side.Top\"\r\n [rule]=\"'display'\"\r\n [collapsedValue]=\"'none'\"\r\n [visibleValue]=\"'block'\"\r\n />\r\n </div>\r\n </div>\r\n }\r\n @if (errors.length > 0) {\r\n <div class=\"daga-error-panel\">\r\n <ol>\r\n @for (error of errors; track error; let i = $index) {\r\n <li\r\n (click)=\"showError(error)\"\r\n [innerHTML]=\"error.message\"\r\n ></li>\r\n }\r\n </ol>\r\n </div>\r\n }\r\n </div>\r\n" }] }], propDecorators: { errorsContainer: [{ type: ViewChild, args: ['errors'] }] } }); /** * Distance in pixels by which labels must be shifted in order to be centered properly. */ const LABEL_VERTICAL_SHIFT_PX = 6; /** * Palette that the user can drag and drop nodes from. * @private * @see DiagramConfig * @see DiagramNode */ class PaletteComponent { constructor() { this.#canvasProvider = inject(CanvasProviderService); this.#configService = inject(DagaConfigurationService); this.currentCategory = ''; } #canvasProvider; #configService; get canvas() { return this.#canvasProvider.getCanvas(); } ngOnInit() { /* reload using the canvas observable instead of the config one because we're getting the type info from the model, not the configuration */ this.sub = this.#canvasProvider.canvas$.subscribe(() => { this.init(this.#configService.getConfig()); }); } ngAfterViewInit() { this.refreshPalette(); switch (this.direction) { case Side.Bottom: case Side.Top: this.selectPanel() .style('width', this.width) .select('.daga-palette-view') .style('flex-direction', 'row'); break; case Side.Left: case Side.Right: this.selectPanel() .style('height', this.width) .select('.daga-palette-view') .style('flex-direction', 'column'); break; } this.selectPanel() .select('.daga-palette-view') .style('flex-wrap', 'wrap') .style('justify-content', 'center') .style('gap', this.gap); } ngOnDestroy() { this.sub?.unsubscribe(); } init(config) { this.location = config.components?.palette?.location || Corner.TopLeft; this.direction = config.components?.palette?.direction || Side.Bottom; this.width = config.components?.palette?.width || '12rem'; this.gap = config.components?.palette?.gap || '1rem'; this.palettes = config.components?.palette?.sections || []; this.currentPalette = this.palettes[0]; this.refreshPalette(); } refreshPalette() { this.switchPalette(this.currentPalette); } switchPalette(palette) { this.currentPalette = palette; if (this.panel !== undefined) { this.selectPalette().selectAll('*').remove(); this.priorityThreshold = this.canvas.getPriorityThreshold(); if (palette.categories) { this.appendCategories(palette.categories); } if (palette.templates) { for (const template of palette.templates) { this.appendTemplate(template); } } } } selectPanel() { return d3.select(this.panel.nativeElement); } selectPalette() { return this.selectPanel().select('.daga-palette-view'); } appendCategories(categories) { const thisComponent = this.selectPalette() .append('select') .style('width', '100%') .style('height', '2rem') .style('padding', '0.5rem') .style('border-radius', '0.25rem') .style('background-color', '#f7f8fc') .style('border', `1px solid #e6e6e6`); thisComponent.append('option').attr('value', '').text('(None selected)'); for (const categoryKey in categories) { thisComponent .append('option') .attr('value', categoryKey) .text(categoryKey); } thisComponent.on(Events.Change, () => { if (this.currentCategory) { this.selectPalette() .selectAll(`.daga-template-container.daga-in-category`) .remove(); } const selectedKey = thisComponent.property('value'); this.currentCategory = selectedKey; const templatesInCategory = categories[selectedKey] || []; for (const template of templatesInCategory) { this.appendTemplate(template, 'daga-in-category'); } }); if (this.currentCategory) { // if we had a category already selected, re-select it thisComponent.property('value', this.currentCategory); thisComponent.dispatch(Events.Change); } } appendTemplate(template, classes) { if (template.templateType === 'node') { const nodeType = this.canvas.model.nodes.types.get(template.type); if (nodeType) { this.appendNodeTemplate(nodeType, template, classes); } else { console.error(`Could not find a node type called '${template.type}'`); } } else if (template.templateType === 'connection') { { const connectionType = this.canvas.model.connections.types.get(template.type); if (connectionType) { this.appendConnectionTemplate(connectionType, template, classes); } else { console.error(`Could not find a connection type called '${template.type}'`); } } } } appendNodeTemplate(type, templateConfig, classes) { if (this.priorityThreshold !== undefined && type.priority < this.priorityThreshold) { return; } const borderThickness = type.defaultLook.lookType === 'shaped-look' ? type.defaultLook.borderThickness : 0; const templateHeight = type.defaultHeight + borderThickness; const templateWidth = type.defaultWidth + borderThickness; const stretchedTemplateHeight = templateConfig.height || templateHeight; const stretchedTemplateWidth = templateConfig.width || templateWidth; let thisComponentClone; const thisComponent = this.selectPalette() .append('div') .attr('class', `daga-template-container ${classes !== undefined ? classes : ''}`) .style('width', `${stretchedTemplateWidth}px`) .style('height', `${stretchedTemplateHeight}px`) .on(Events.MouseEnter, () => { setCursorStyle(CursorStyle.Grab); }) .on(Events.MouseLeave, () => { setCursorStyle(); }) .call(d3 .drag() .on(DragEvents.Drag, (event) => { if (this.canvas.canUserPerformAction(DiagramActions.AddNode)) { const pointerCoords = this.canvas.getPointerLocationRelativeToRoot(event); if (pointerCoords.length < 2 || isNaN(pointerCoords[0]) || isNaN(pointerCoords[1])) { return; } thisComponentClone .style('left', `${pointerCoords[0] - stretchedTemplateWidth / 2}px`) .style('top', `${pointerCoords[1] - stretchedTemplateHeight / 2}px`); } }) .on(DragEvents.Start, (event) => { if (this.canvas.canUserPerformAction(DiagramActions.AddNode)) { setCursorStyle(CursorStyle.Grabbing); const pointerCoords = this.canvas.getPointerLocationRelativeToRoot(event); if (pointerCoords.length < 2 || isNaN(pointerCoords[0]) || isNaN(pointerCoords[1])) { return; } const thisComponentNodeCloned = thisComponent .node() ?.cloneNode(true); this.canvas.selectRoot().node().appendChild(thisComponentNodeCloned); thisComponentClone = d3.select(thisComponentNodeCloned); thisComponentClone .style('position', 'absolute') .style('left', `${pointerCoords[0] - stretchedTemplateWidth / 2}px`) .style('top', `${pointerCoords[1] - stretchedTemplateHeight / 2}px`) .style('z-index', 1); // when trying to place a unique node in a diagram that already has a node of that type, set cursor style to not allowed if (type.isUnique && this.canvas.model.nodes.find((n) => !n.removed && n.type.id === type.id) !== undefined) { setCursorStyle(CursorStyle.NotAllowed); } } }) .on(DragEvents.End, (event) => { if (this.canvas.canUserPerformAction(DiagramActions.AddNode)) { // take node back to its original position setCursorStyle(CursorStyle.Auto); thisComponentClone?.remove(); // try to place node if (type.isUnique && this.canvas.model.nodes.find((n) => !n.removed && n.type.id === type.id) !== undefined) { // can't place, it's unique and that node is already in the model return; } const pointerCoordsRelativeToScreen = this.canvas.getPointerLocationRelativeToScreen(event); if (pointerCoordsRelativeToScreen.length < 2 || isNaN(pointerCoordsRelativeToScreen[0]) || isNaN(pointerCoordsRelativeToScreen[1])) { // can't place, position is incorrect return; } const element = document.elementFromPoint(pointerCoordsRelativeToScreen[0], pointerCoordsRelativeToScreen[1]); if (element && !this.canvas.selectCanvasView().node()?.contains(element)) { // can't place, node hasn't been dropped on the canvas return; } const pointerCoords = this.canvas.getPointerLocationRelativeToCanvas(event); if (pointerCoords.length < 2 || isNaN(pointerCoords[0]) || isNaN(pointerCoords[1])) { // can't place, position is incorrect return; } let newNodeCoords = [ pointerCoords[0] - type.defaultWidth / 2, pointerCoords[1] - type.defaultHeight / 2 ]; if (this.canvas.snapToGrid) { newNodeCoords = this.canvas.getClosestGridPoint(newNodeCoords); } // check whether we dropped this node on a potential parent const nodesAtLocation = this.canvas.model.nodes.getAtCoordinates(pointerCoords[0], pointerCoords[1]); // filter by which nodes can have this type as a child const nodesAtLocationWhichCanHaveNodeAsAChild = nodesAtLocation.filter((n) => n.type.childrenTypes.includes(type.id)); // filter by which nodes don't have descendants in this collection const filteredNodesAtLocation = filterByOnlyDescendants(nodesAtLocationWhichCanHaveNodeAsAChild); const droppedOn = filteredNodesAtLocation[filteredNodesAtLocation.length - 1]; if (!type.canBeParentless && droppedOn === undefined) { // can't place, node must have a parent and no suitable parent has been found return; } const ancestor = droppedOn?.getLastAncestor(); const addNodeAction = new AddNodeAction(this.canvas, type, newNodeCoords, droppedOn?.id, ancestor?.id, ancestor?.getGeometry(), undefined, templateConfig.label, templateConfig.values); addNodeAction.do(); addNodeAction.toAncestorGeometry = ancestor?.getGeometry(); this.canvas.actionStack.add(addNodeAction); // reset cursor setCursorStyle(); } })) .append('svg') .attr('class', `palette-node ${type.id}`) .attr('viewBox', `0 0 ${templateWidth} ${templateHeight}`) .attr('preserveAspectRatio', 'none') .style('position', 'relative') .style('left', 0) .style('top', 0) .style('width', `${stretchedTemplateWidth}px`) .style('height', `${stretchedTemplateHeight}px`); const nodeLook = templateConfig.look || type.defaultLook; switch (nodeLook.lookType) { case 'shaped-look': thisComponent .append('path') .attr('d', generalClosedPath(nodeLook.shape, nodeLook.borderThickness / 2, nodeLook.borderThickness / 2, type.defaultWidth, type.defaultHeight)) .attr('fill', nodeLook.fillColor) .attr('stroke', nodeLook.borderColor) .attr('stroke-width', `${nodeLook.borderThickness}px`); break; case 'image-look': thisComponent .append('image') .attr('x', 0) .attr('y', 0) .attr('width', type.defaultWidth) .attr('height', type.defaultHeight) .attr('href', nodeLook.backgroundImage) .attr('preserveAspectRatio', 'none'); break; case 'stretchable-image-look': thisComponent .append('image') .attr('x', 0) .attr('y', 0) .attr('width', nodeLook.leftMargin) .attr('height', nodeLook.topMargin) .attr('href', nodeLook.backgroundImageTopLeft) .attr('preserveAspectRatio', 'none'); thisComponent .append('image') .attr('x', nodeLook.leftMargin) .attr('y', 0) .attr('width', type.defaultWidth - nodeLook.rightMargin - nodeLook.leftMargin) .attr('height', nodeLook.topMargin) .attr('href', nodeLook.backgroundImageTop) .attr('preserveAspectRatio', 'none'); thisComponent .append('image') .attr('x', type.defaultWidth - nodeLook.rightMargin) .attr('y', 0) .attr('width', nodeLook.rightMargin) .attr('height', nodeLook.topMargin) .attr('href', nodeLook.backgroundImageTopRight) .attr('preserveAspectRatio', 'none'); thisComponent .append('image') .attr('x', 0) .attr('y', nodeLook.topMargin) .attr('width', nodeLook.leftMargin) .attr('height', type.defaultHeight - nodeLook.bottomMargin - nodeLook.topMargin) .attr('href', nodeLook.backgroundImageLeft) .attr('preserveAspectRatio', 'none'); thisComponent .append('image') .attr('x', nodeLook.leftMargin) .attr('y', nodeLook.topMargin) .attr('width', type.defaultWidth - nodeLook.rightMargin - nodeLook.leftMargin) .attr('height', type.defaultHeight - nodeLook.bottomMargin - nodeLook.topMargin) .attr('href', nodeLook.backgroundImageCenter) .attr('preserveAspectRatio', 'none'); thisComponent .append('image') .attr('x', type.defaultWidth - nodeLook.rightMargin) .attr('y', nodeLook.topMargin) .attr('width', nodeLook.rightMargin) .attr('height', type.defaultHeight - nodeLook.bottomMargin - nodeLook.topMargin) .attr('href', nodeLook.backgroundImageRight) .attr('preserveAspectRatio', 'none'); thisComponent .append('image') .attr('x', 0) .attr('y', type.defaultHeight - nodeLook.bottomMargin) .attr('width', nodeLook.leftMargin) .attr('height', nodeLook.bottomMargin) .attr('href', nodeLook.backgroundImageBottomLeft) .attr('preserveAspectRatio', 'none'); thisComponent .append('image') .attr('x', nodeLook.leftMargin) .attr('y', type.defaultHeight - nodeLook.bottomMargin) .attr('width', type.defaultWidth - nodeLook.rightMargin - nodeLook.leftMargin) .attr('height', nodeLook.bottomMargin) .attr('href', nodeLook.backgroundImageBottom) .attr('preserveAspectRatio', 'none'); thisComponent .append('image') .attr('x', type.defaultWidth - nodeLook.rightMargin) .attr('y', type.defaultHeight - nodeLook.bottomMargin) .attr('width', nodeLook.rightMargin) .attr('height', nodeLook.bottomMargin) .attr('href', nodeLook.backgroundImageBottomRight) .attr('preserveAspectRatio', 'none'); } if (templateConfig.look === undefined) { if (type.decorators) { for (const decoratorConfig of type.decorators) { thisComponent .append('foreignObject') .attr('width', `${decoratorConfig.width}px`) .attr('height', `${decoratorConfig.height}px`) .attr('transform', `translate(${decoratorConfig.coords[0]},${decoratorConfig.coords[1]})`) .html(decoratorConfig.html); } } if (templateConfig.label) { const labelConfig = { ...DIAGRAM_FIELD_DEFAULTS, ...type.label, ...templateConfig.labelLook }; thisComponent .append('text') .attr('transform', `translate(${(getLeftMargin(labelConfig) + type.defaultWidth + borderThickness / 2) / 2},${(getTopMargin(labelConfig) + type.defaultHeight + borderThickness / 2) / 2})`) .attr('x', 0) .attr('y', 0) .attr('font-size', `${labelConfig.fontSize}px`) .attr('text-anchor', 'middle') .attr('font-family', labelConfig.fontFamily) .attr('font-weight', 400) .attr('fill', labelConfig.color) .attr('stroke', 'none') .style('font-kerning', 'none') .style('user-select', 'none') .text(templateConfig.label); } } } appendConnectionTemplate(type, templateConfig, classes) { const thisComponent = this.selectPalette() .append('div') .attr('class', `daga-template-container ${classes !== undefined ? classes : ''}`) .style('width', `${templateConfig.width}px`) .style('height', `${templateConfig.height}px`) .style('cursor', 'pointer')