@metadev/daga-angular
Version:
Diagramming engine for editing models on the Web. Made by Metadev.
903 lines (896 loc) • 137 kB
JavaScript
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')