ngx-vflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/esm2022/lib/vflow/components/connection/connection.component.mjs +100 -0
- package/esm2022/lib/vflow/components/defs/defs.component.mjs +19 -0
- package/esm2022/lib/vflow/components/edge/edge.component.mjs +42 -0
- package/esm2022/lib/vflow/components/edge-label/edge-label.component.mjs +58 -0
- package/esm2022/lib/vflow/components/node/node.component.mjs +146 -0
- package/esm2022/lib/vflow/components/vflow/vflow.component.mjs +183 -0
- package/esm2022/lib/vflow/directives/changes-controller.directive.mjs +34 -0
- package/esm2022/lib/vflow/directives/connection-controller.directive.mjs +36 -0
- package/esm2022/lib/vflow/directives/map-context.directive.mjs +62 -0
- package/esm2022/lib/vflow/directives/reference.directive.mjs +16 -0
- package/esm2022/lib/vflow/directives/root-svg-context.directive.mjs +26 -0
- package/esm2022/lib/vflow/directives/space-point-context.directive.mjs +29 -0
- package/esm2022/lib/vflow/directives/template.directive.mjs +58 -0
- package/esm2022/lib/vflow/interfaces/connection-settings.interface.mjs +2 -0
- package/esm2022/lib/vflow/interfaces/connection.interface.mjs +2 -0
- package/esm2022/lib/vflow/interfaces/edge-label.interface.mjs +2 -0
- package/esm2022/lib/vflow/interfaces/edge.interface.mjs +2 -0
- package/esm2022/lib/vflow/interfaces/handle-positions.interface.mjs +2 -0
- package/esm2022/lib/vflow/interfaces/marker.interface.mjs +2 -0
- package/esm2022/lib/vflow/interfaces/node.interface.mjs +2 -0
- package/esm2022/lib/vflow/interfaces/path-data.interface.mjs +2 -0
- package/esm2022/lib/vflow/interfaces/point.interface.mjs +2 -0
- package/esm2022/lib/vflow/interfaces/template-context.interface.mjs +2 -0
- package/esm2022/lib/vflow/interfaces/viewport.interface.mjs +2 -0
- package/esm2022/lib/vflow/math/edge-path/bezier-path.mjs +77 -0
- package/esm2022/lib/vflow/math/edge-path/straigh-path.mjs +18 -0
- package/esm2022/lib/vflow/math/point-on-line-by-ratio.mjs +12 -0
- package/esm2022/lib/vflow/models/connection.model.mjs +9 -0
- package/esm2022/lib/vflow/models/edge-label.model.mjs +8 -0
- package/esm2022/lib/vflow/models/edge.model.mjs +30 -0
- package/esm2022/lib/vflow/models/flow.model.mjs +16 -0
- package/esm2022/lib/vflow/models/node.model.mjs +90 -0
- package/esm2022/lib/vflow/services/draggable.service.mjs +66 -0
- package/esm2022/lib/vflow/services/edge-changes.service.mjs +38 -0
- package/esm2022/lib/vflow/services/flow-entities.service.mjs +49 -0
- package/esm2022/lib/vflow/services/flow-status.service.mjs +39 -0
- package/esm2022/lib/vflow/services/node-changes.service.mjs +32 -0
- package/esm2022/lib/vflow/services/viewport.service.mjs +32 -0
- package/esm2022/lib/vflow/types/edge-change.type.mjs +2 -0
- package/esm2022/lib/vflow/types/node-change.type.mjs +2 -0
- package/esm2022/lib/vflow/types/position.type.mjs +2 -0
- package/esm2022/lib/vflow/types/using-points.type.mjs +2 -0
- package/esm2022/lib/vflow/types/viewport-change-type.type.mjs +2 -0
- package/esm2022/lib/vflow/utils/add-nodes-to-edges.mjs +11 -0
- package/esm2022/lib/vflow/utils/hash.mjs +7 -0
- package/esm2022/lib/vflow/utils/is-defined.mjs +4 -0
- package/esm2022/lib/vflow/utils/reference-keeper.mjs +31 -0
- package/esm2022/lib/vflow/utils/round.mjs +2 -0
- package/esm2022/lib/vflow/vflow.module.mjs +68 -0
- package/esm2022/ngx-vflow.mjs +5 -0
- package/esm2022/public-api.mjs +23 -0
- package/fesm2022/ngx-vflow.mjs +1332 -0
- package/fesm2022/ngx-vflow.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/vflow/components/connection/connection.component.d.ts +20 -0
- package/lib/vflow/components/defs/defs.component.d.ts +8 -0
- package/lib/vflow/components/edge/edge.component.d.ts +16 -0
- package/lib/vflow/components/edge-label/edge-label.component.d.ts +31 -0
- package/lib/vflow/components/node/node.component.d.ts +53 -0
- package/lib/vflow/components/vflow/vflow.component.d.ts +73 -0
- package/lib/vflow/directives/changes-controller.directive.d.ts +16 -0
- package/lib/vflow/directives/connection-controller.directive.d.ts +11 -0
- package/lib/vflow/directives/map-context.directive.d.ts +19 -0
- package/lib/vflow/directives/reference.directive.d.ts +6 -0
- package/lib/vflow/directives/root-svg-context.directive.d.ts +7 -0
- package/lib/vflow/directives/space-point-context.directive.d.ts +14 -0
- package/lib/vflow/directives/template.directive.d.ts +28 -0
- package/lib/vflow/interfaces/connection-settings.interface.d.ts +10 -0
- package/lib/vflow/interfaces/connection.interface.d.ts +4 -0
- package/lib/vflow/interfaces/edge-label.interface.d.ts +6 -0
- package/lib/vflow/interfaces/edge.interface.d.ts +18 -0
- package/lib/vflow/interfaces/handle-positions.interface.d.ts +5 -0
- package/lib/vflow/interfaces/marker.interface.d.ts +9 -0
- package/lib/vflow/interfaces/node.interface.d.ts +16 -0
- package/lib/vflow/interfaces/path-data.interface.d.ts +8 -0
- package/lib/vflow/interfaces/point.interface.d.ts +4 -0
- package/lib/vflow/interfaces/template-context.interface.d.ts +10 -0
- package/lib/vflow/interfaces/viewport.interface.d.ts +9 -0
- package/lib/vflow/math/edge-path/bezier-path.d.ts +5 -0
- package/lib/vflow/math/edge-path/straigh-path.d.ts +4 -0
- package/lib/vflow/math/point-on-line-by-ratio.d.ts +7 -0
- package/lib/vflow/models/connection.model.d.ts +9 -0
- package/lib/vflow/models/edge-label.model.d.ts +9 -0
- package/lib/vflow/models/edge.model.d.ts +17 -0
- package/lib/vflow/models/flow.model.d.ts +14 -0
- package/lib/vflow/models/node.model.d.ts +70 -0
- package/lib/vflow/services/draggable.service.d.ts +32 -0
- package/lib/vflow/services/edge-changes.service.d.ts +22 -0
- package/lib/vflow/services/flow-entities.service.d.ts +15 -0
- package/lib/vflow/services/flow-status.service.d.ts +43 -0
- package/lib/vflow/services/node-changes.service.d.ts +26 -0
- package/lib/vflow/services/viewport.service.d.ts +24 -0
- package/lib/vflow/types/edge-change.type.d.ts +14 -0
- package/lib/vflow/types/node-change.type.d.ts +16 -0
- package/lib/vflow/types/position.type.d.ts +1 -0
- package/lib/vflow/types/using-points.type.d.ts +5 -0
- package/lib/vflow/types/viewport-change-type.type.d.ts +3 -0
- package/lib/vflow/utils/add-nodes-to-edges.d.ts +3 -0
- package/lib/vflow/utils/hash.d.ts +1 -0
- package/lib/vflow/utils/is-defined.d.ts +1 -0
- package/lib/vflow/utils/reference-keeper.d.ts +14 -0
- package/lib/vflow/utils/round.d.ts +1 -0
- package/lib/vflow/vflow.module.d.ts +18 -0
- package/package.json +25 -0
- package/public-api.d.ts +18 -0
|
@@ -0,0 +1,1332 @@
|
|
|
1
|
+
import * as i1 from '@angular/common';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { signal, Injectable, inject, ElementRef, Directive, effect, Input, TemplateRef, computed, EventEmitter, Output, untracked, Component, ChangeDetectionStrategy, ViewChild, HostListener, Injector, runInInjectionContext, ContentChild, NgModule } from '@angular/core';
|
|
5
|
+
import { select } from 'd3-selection';
|
|
6
|
+
import { zoomIdentity, zoom } from 'd3-zoom';
|
|
7
|
+
import { drag } from 'd3-drag';
|
|
8
|
+
import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
9
|
+
import { switchMap, merge, skip, map, pairwise, filter, observeOn, asyncScheduler, tap, fromEvent } from 'rxjs';
|
|
10
|
+
import { path } from 'd3-path';
|
|
11
|
+
|
|
12
|
+
class ViewportService {
|
|
13
|
+
constructor() {
|
|
14
|
+
/**
|
|
15
|
+
* Internal signal that accepts value from user by lib api
|
|
16
|
+
* When this signal changes, lib sets new view state and update readableViewport signal
|
|
17
|
+
*/
|
|
18
|
+
this.writableViewport = signal({
|
|
19
|
+
changeType: 'initial',
|
|
20
|
+
state: ViewportService.getDefaultViewport()
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Public signal with viewport state. User can directly read from this signal. It's updated by:
|
|
24
|
+
* - user events on flow
|
|
25
|
+
* - writableViewport signal
|
|
26
|
+
*/
|
|
27
|
+
this.readableViewport = signal(ViewportService.getDefaultViewport());
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The default value used by d3, just copy it here
|
|
31
|
+
*
|
|
32
|
+
* @returns default viewport value
|
|
33
|
+
*/
|
|
34
|
+
static getDefaultViewport() { return { zoom: 1, x: 0, y: 0 }; }
|
|
35
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ViewportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
36
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ViewportService }); }
|
|
37
|
+
}
|
|
38
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ViewportService, decorators: [{
|
|
39
|
+
type: Injectable
|
|
40
|
+
}] });
|
|
41
|
+
|
|
42
|
+
function isDefined(data) {
|
|
43
|
+
return data !== undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class RootSvgReferenceDirective {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.element = inject(ElementRef).nativeElement;
|
|
49
|
+
}
|
|
50
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootSvgReferenceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
51
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: RootSvgReferenceDirective, selector: "svg[rootSvgRef]", ngImport: i0 }); }
|
|
52
|
+
}
|
|
53
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootSvgReferenceDirective, decorators: [{
|
|
54
|
+
type: Directive,
|
|
55
|
+
args: [{
|
|
56
|
+
selector: 'svg[rootSvgRef]'
|
|
57
|
+
}]
|
|
58
|
+
}] });
|
|
59
|
+
|
|
60
|
+
class MapContextDirective {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.rootSvg = inject(RootSvgReferenceDirective).element;
|
|
63
|
+
this.host = inject(ElementRef).nativeElement;
|
|
64
|
+
this.viewportService = inject(ViewportService);
|
|
65
|
+
this.rootSvgSelection = select(this.rootSvg);
|
|
66
|
+
this.zoomableSelection = select(this.host);
|
|
67
|
+
// under the hood this effect triggers handleZoom, so error throws without this flag
|
|
68
|
+
// TODO: hack with timer fixes wrong node scaling (handle positions not matched with content size)
|
|
69
|
+
this.manualViewportChangeEffect = effect(() => setTimeout(() => {
|
|
70
|
+
const viewport = this.viewportService.writableViewport();
|
|
71
|
+
const state = viewport.state;
|
|
72
|
+
if (viewport.changeType === 'initial') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// If only zoom provided
|
|
76
|
+
if (isDefined(state.zoom) && (!isDefined(state.x) && !isDefined(state.y))) {
|
|
77
|
+
this.rootSvgSelection.call(this.zoomBehavior.scaleTo, state.zoom);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// If only pan provided
|
|
81
|
+
if ((isDefined(state.x) && isDefined(state.y)) && !isDefined(state.zoom)) {
|
|
82
|
+
this.rootSvgSelection.call(this.zoomBehavior.translateTo, state.x, state.y);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// If whole viewort state provided
|
|
86
|
+
if (isDefined(state.x) && isDefined(state.y) && isDefined(state.zoom)) {
|
|
87
|
+
this.rootSvgSelection.call(this.zoomBehavior.transform, zoomIdentity.translate(state.x, state.y).scale(state.zoom));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}), { allowSignalWrites: true });
|
|
91
|
+
this.handleZoom = ({ transform }) => {
|
|
92
|
+
// update public signal for user to read
|
|
93
|
+
this.viewportService.readableViewport.set({ zoom: transform.k, x: transform.x, y: transform.y });
|
|
94
|
+
this.zoomableSelection.attr('transform', transform.toString());
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
ngOnInit() {
|
|
98
|
+
this.zoomBehavior = zoom()
|
|
99
|
+
.scaleExtent([this.minZoom, this.maxZoom])
|
|
100
|
+
.on('zoom', this.handleZoom);
|
|
101
|
+
this.rootSvgSelection.call(this.zoomBehavior);
|
|
102
|
+
}
|
|
103
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MapContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
104
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: MapContextDirective, selector: "g[mapContext]", inputs: { minZoom: "minZoom", maxZoom: "maxZoom" }, ngImport: i0 }); }
|
|
105
|
+
}
|
|
106
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MapContextDirective, decorators: [{
|
|
107
|
+
type: Directive,
|
|
108
|
+
args: [{ selector: 'g[mapContext]' }]
|
|
109
|
+
}], propDecorators: { minZoom: [{
|
|
110
|
+
type: Input
|
|
111
|
+
}], maxZoom: [{
|
|
112
|
+
type: Input
|
|
113
|
+
}] } });
|
|
114
|
+
|
|
115
|
+
const round = (num) => Math.round(num * 100) / 100;
|
|
116
|
+
|
|
117
|
+
class DraggableService {
|
|
118
|
+
/**
|
|
119
|
+
* Enable or disable draggable behavior for element.
|
|
120
|
+
* model contains draggable flag which declares if draggable should be enabled or not
|
|
121
|
+
*
|
|
122
|
+
* @param element target element for toggling draggable
|
|
123
|
+
* @param model model with data for this element
|
|
124
|
+
*/
|
|
125
|
+
toggleDraggable(element, model) {
|
|
126
|
+
const d3Element = select(element);
|
|
127
|
+
const behavior = model.draggable
|
|
128
|
+
? this.getDragBehavior(model)
|
|
129
|
+
: this.getIgnoreDragBehavior();
|
|
130
|
+
d3Element.call(behavior);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* TODO: not shure if this work, need to check
|
|
134
|
+
*
|
|
135
|
+
* @param element
|
|
136
|
+
*/
|
|
137
|
+
destroy(element) {
|
|
138
|
+
select(element).on('.drag', null);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Node drag behavior. Updated node's coordinate according to dragging
|
|
142
|
+
*
|
|
143
|
+
* @param model
|
|
144
|
+
* @returns
|
|
145
|
+
*/
|
|
146
|
+
getDragBehavior(model) {
|
|
147
|
+
let deltaX;
|
|
148
|
+
let deltaY;
|
|
149
|
+
return drag()
|
|
150
|
+
.on('start', (event) => {
|
|
151
|
+
deltaX = model.point().x - event.x;
|
|
152
|
+
deltaY = model.point().y - event.y;
|
|
153
|
+
})
|
|
154
|
+
.on('drag', (event) => {
|
|
155
|
+
model.point.set({
|
|
156
|
+
x: round(event.x + deltaX),
|
|
157
|
+
y: round(event.y + deltaY)
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Specify ignoring drag behavior. It's responsible for not moving the map when user tries to drag node
|
|
163
|
+
* with disabled drag behavior
|
|
164
|
+
*/
|
|
165
|
+
getIgnoreDragBehavior() {
|
|
166
|
+
return drag()
|
|
167
|
+
.on('drag', (event) => {
|
|
168
|
+
event.sourceEvent.stopPropagation();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DraggableService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
172
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DraggableService }); }
|
|
173
|
+
}
|
|
174
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DraggableService, decorators: [{
|
|
175
|
+
type: Injectable
|
|
176
|
+
}] });
|
|
177
|
+
|
|
178
|
+
class EdgeTemplateDirective {
|
|
179
|
+
constructor() {
|
|
180
|
+
this.templateRef = inject(TemplateRef);
|
|
181
|
+
}
|
|
182
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
183
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: EdgeTemplateDirective, selector: "ng-template[edge]", ngImport: i0 }); }
|
|
184
|
+
}
|
|
185
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeTemplateDirective, decorators: [{
|
|
186
|
+
type: Directive,
|
|
187
|
+
args: [{ selector: 'ng-template[edge]' }]
|
|
188
|
+
}] });
|
|
189
|
+
class ConnectionTemplateDirective {
|
|
190
|
+
constructor() {
|
|
191
|
+
this.templateRef = inject(TemplateRef);
|
|
192
|
+
}
|
|
193
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ConnectionTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
194
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ConnectionTemplateDirective, selector: "ng-template[connection]", ngImport: i0 }); }
|
|
195
|
+
}
|
|
196
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ConnectionTemplateDirective, decorators: [{
|
|
197
|
+
type: Directive,
|
|
198
|
+
args: [{ selector: 'ng-template[connection]' }]
|
|
199
|
+
}] });
|
|
200
|
+
class EdgeLabelHtmlTemplateDirective {
|
|
201
|
+
constructor() {
|
|
202
|
+
this.templateRef = inject(TemplateRef);
|
|
203
|
+
}
|
|
204
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeLabelHtmlTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
205
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: EdgeLabelHtmlTemplateDirective, selector: "ng-template[edgeLabelHtml]", ngImport: i0 }); }
|
|
206
|
+
}
|
|
207
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeLabelHtmlTemplateDirective, decorators: [{
|
|
208
|
+
type: Directive,
|
|
209
|
+
args: [{ selector: 'ng-template[edgeLabelHtml]' }]
|
|
210
|
+
}] });
|
|
211
|
+
class NodeHtmlTemplateDirective {
|
|
212
|
+
constructor() {
|
|
213
|
+
this.templateRef = inject(TemplateRef);
|
|
214
|
+
}
|
|
215
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeHtmlTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
216
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: NodeHtmlTemplateDirective, selector: "ng-template[nodeHtml]", ngImport: i0 }); }
|
|
217
|
+
}
|
|
218
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeHtmlTemplateDirective, decorators: [{
|
|
219
|
+
type: Directive,
|
|
220
|
+
args: [{ selector: 'ng-template[nodeHtml]' }]
|
|
221
|
+
}] });
|
|
222
|
+
class HandleTemplateDirective {
|
|
223
|
+
constructor() {
|
|
224
|
+
this.templateRef = inject(TemplateRef);
|
|
225
|
+
}
|
|
226
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
227
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: HandleTemplateDirective, selector: "ng-template[handle]", ngImport: i0 }); }
|
|
228
|
+
}
|
|
229
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleTemplateDirective, decorators: [{
|
|
230
|
+
type: Directive,
|
|
231
|
+
args: [{ selector: 'ng-template[handle]' }]
|
|
232
|
+
}] });
|
|
233
|
+
|
|
234
|
+
function addNodesToEdges(nodes, edges) {
|
|
235
|
+
const nodesById = nodes.reduce((acc, n) => {
|
|
236
|
+
acc[n.node.id] = n;
|
|
237
|
+
return acc;
|
|
238
|
+
}, {});
|
|
239
|
+
edges.forEach(e => {
|
|
240
|
+
e.source = nodesById[e.edge.source];
|
|
241
|
+
e.target = nodesById[e.edge.target];
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
class FlowModel {
|
|
246
|
+
constructor() {
|
|
247
|
+
/**
|
|
248
|
+
* Global setting with handle positions. Nodes derive this value
|
|
249
|
+
*/
|
|
250
|
+
this.handlePositions = signal({ source: 'right', target: 'left' });
|
|
251
|
+
/**
|
|
252
|
+
* @see {VflowComponent.view}
|
|
253
|
+
*/
|
|
254
|
+
this.view = signal([400, 400]);
|
|
255
|
+
this.flowWidth = computed(() => this.view() === 'auto' ? '100%' : this.view()[0]);
|
|
256
|
+
this.flowHeight = computed(() => this.view() === 'auto' ? '100%' : this.view()[1]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
class FlowStatusService {
|
|
261
|
+
constructor() {
|
|
262
|
+
this.status = signal({ state: 'idle', payload: null });
|
|
263
|
+
}
|
|
264
|
+
setIdleStatus() {
|
|
265
|
+
this.status.set({ state: 'idle', payload: null });
|
|
266
|
+
}
|
|
267
|
+
setConnectionStartStatus(sourceNode) {
|
|
268
|
+
this.status.set({ state: 'connection-start', payload: { sourceNode } });
|
|
269
|
+
}
|
|
270
|
+
setConnectionValidationStatus(sourceNode, targetNode, valid) {
|
|
271
|
+
this.status.set({ state: 'connection-validation', payload: { sourceNode, targetNode, valid } });
|
|
272
|
+
}
|
|
273
|
+
setConnectionEndStatus(sourceNode, targetNode) {
|
|
274
|
+
this.status.set({ state: 'connection-end', payload: { sourceNode, targetNode } });
|
|
275
|
+
}
|
|
276
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowStatusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
277
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowStatusService }); }
|
|
278
|
+
}
|
|
279
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowStatusService, decorators: [{
|
|
280
|
+
type: Injectable
|
|
281
|
+
}] });
|
|
282
|
+
/**
|
|
283
|
+
* Batch status changes together to call them one after another
|
|
284
|
+
*
|
|
285
|
+
* @param changes list of set[FlowStatus.state]Status() calls
|
|
286
|
+
*/
|
|
287
|
+
function batchStatusChanges(...changes) {
|
|
288
|
+
if (changes.length) {
|
|
289
|
+
const [firstChange, ...restChanges] = changes;
|
|
290
|
+
// first change is sync
|
|
291
|
+
firstChange();
|
|
292
|
+
// without timer, subscribed effects/comuted signals only get latest value
|
|
293
|
+
restChanges.forEach(change => setTimeout(() => change()));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
class ConnectionModel {
|
|
298
|
+
constructor(connection) {
|
|
299
|
+
this.connection = connection;
|
|
300
|
+
this.curve = connection.curve ?? 'bezier';
|
|
301
|
+
this.type = connection.type ?? 'default';
|
|
302
|
+
this.validator = connection.validator ?? (() => true);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function hashCode(str) {
|
|
307
|
+
return str.split('').reduce((a, b) => {
|
|
308
|
+
a = ((a << 5) - a) + b.charCodeAt(0);
|
|
309
|
+
return a & a;
|
|
310
|
+
}, 0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
class FlowEntitiesService {
|
|
314
|
+
constructor() {
|
|
315
|
+
this.nodes = signal([], {
|
|
316
|
+
// empty arrays considered equal, other arrays may not be equal
|
|
317
|
+
equal: (a, b) => !a.length && !b.length ? true : a === b
|
|
318
|
+
});
|
|
319
|
+
this.edges = signal([], {
|
|
320
|
+
// empty arrays considered equal, other arrays may not be equal
|
|
321
|
+
equal: (a, b) => !a.length && !b.length ? true : a === b
|
|
322
|
+
});
|
|
323
|
+
this.connection = signal(new ConnectionModel({}));
|
|
324
|
+
this.markers = computed(() => {
|
|
325
|
+
const markersMap = new Map();
|
|
326
|
+
this.validEdges().forEach(e => {
|
|
327
|
+
if (e.edge.markers?.start) {
|
|
328
|
+
const hash = hashCode(JSON.stringify(e.edge.markers.start));
|
|
329
|
+
markersMap.set(hash, e.edge.markers.start);
|
|
330
|
+
}
|
|
331
|
+
if (e.edge.markers?.end) {
|
|
332
|
+
const hash = hashCode(JSON.stringify(e.edge.markers.end));
|
|
333
|
+
markersMap.set(hash, e.edge.markers.end);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
const connectionMarker = this.connection().connection.marker;
|
|
337
|
+
if (connectionMarker) {
|
|
338
|
+
const hash = hashCode(JSON.stringify(connectionMarker));
|
|
339
|
+
markersMap.set(hash, connectionMarker);
|
|
340
|
+
}
|
|
341
|
+
return markersMap;
|
|
342
|
+
});
|
|
343
|
+
this.validEdges = computed(() => {
|
|
344
|
+
const nodes = this.nodes();
|
|
345
|
+
return this.edges().filter(e => nodes.includes(e.source) && nodes.includes(e.target));
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
getNode(id) {
|
|
349
|
+
return this.nodes().find(({ node }) => node.id === id);
|
|
350
|
+
}
|
|
351
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowEntitiesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
352
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowEntitiesService }); }
|
|
353
|
+
}
|
|
354
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowEntitiesService, decorators: [{
|
|
355
|
+
type: Injectable
|
|
356
|
+
}] });
|
|
357
|
+
|
|
358
|
+
class ConnectionControllerDirective {
|
|
359
|
+
constructor() {
|
|
360
|
+
this.onConnect = new EventEmitter();
|
|
361
|
+
this.statusService = inject(FlowStatusService);
|
|
362
|
+
this.flowEntitiesService = inject(FlowEntitiesService);
|
|
363
|
+
this.connectEffect = effect(() => {
|
|
364
|
+
const status = this.statusService.status();
|
|
365
|
+
if (status.state === 'connection-end') {
|
|
366
|
+
const sourceModel = status.payload.sourceNode;
|
|
367
|
+
const targetModel = status.payload.targetNode;
|
|
368
|
+
const source = sourceModel.node.id;
|
|
369
|
+
const target = targetModel.node.id;
|
|
370
|
+
const connection = this.flowEntitiesService.connection();
|
|
371
|
+
if (connection.validator({ source, target })) {
|
|
372
|
+
this.onConnect.emit({ source, target });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}, { allowSignalWrites: true });
|
|
376
|
+
}
|
|
377
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ConnectionControllerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
378
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ConnectionControllerDirective, isStandalone: true, selector: "[connectionController]", outputs: { onConnect: "onConnect" }, ngImport: i0 }); }
|
|
379
|
+
}
|
|
380
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ConnectionControllerDirective, decorators: [{
|
|
381
|
+
type: Directive,
|
|
382
|
+
args: [{
|
|
383
|
+
selector: '[connectionController]',
|
|
384
|
+
standalone: true
|
|
385
|
+
}]
|
|
386
|
+
}], propDecorators: { onConnect: [{
|
|
387
|
+
type: Output
|
|
388
|
+
}] } });
|
|
389
|
+
|
|
390
|
+
class NodeModel {
|
|
391
|
+
constructor(node) {
|
|
392
|
+
this.node = node;
|
|
393
|
+
this.point = signal({ x: 0, y: 0 });
|
|
394
|
+
this.point$ = toObservable(this.point);
|
|
395
|
+
this.size = signal({ width: 0, height: 0 });
|
|
396
|
+
this.pointTransform = computed(() => `translate(${this.point().x}, ${this.point().y})`);
|
|
397
|
+
this.sourceOffset = computed(() => {
|
|
398
|
+
const { width, height } = this.size();
|
|
399
|
+
switch (this.sourcePosition()) {
|
|
400
|
+
case 'left': return { x: 0, y: height / 2 };
|
|
401
|
+
case 'right': return { x: width, y: height / 2 };
|
|
402
|
+
case 'top': return { x: width / 2, y: 0 };
|
|
403
|
+
case 'bottom': return { x: width / 2, y: height };
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
this.targetOffset = computed(() => {
|
|
407
|
+
const { width, height } = this.size();
|
|
408
|
+
switch (this.targetPosition()) {
|
|
409
|
+
case 'left': return { x: 0, y: (height / 2) };
|
|
410
|
+
case 'right': return { x: width, y: height / 2 };
|
|
411
|
+
case 'top': return { x: width / 2, y: 0 };
|
|
412
|
+
case 'bottom': return { x: width / 2, y: height };
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
this.sourcePointAbsolute = computed(() => {
|
|
416
|
+
return {
|
|
417
|
+
x: this.point().x + this.sourceOffset().x + this.sourceHandleOffset().x,
|
|
418
|
+
y: this.point().y + this.sourceOffset().y + this.sourceHandleOffset().y
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
this.targetPointAbsolute = computed(() => {
|
|
422
|
+
return {
|
|
423
|
+
x: this.point().x + this.targetOffset().x + this.targetHandleOffset().x,
|
|
424
|
+
y: this.point().y + this.targetOffset().y + this.targetHandleOffset().y
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
// Now source and handle positions derived from parent flow
|
|
428
|
+
this.sourcePosition = computed(() => this.flow.handlePositions().source);
|
|
429
|
+
this.targetPosition = computed(() => this.flow.handlePositions().target);
|
|
430
|
+
this.sourceHandleSize = signal({ width: 0, height: 0 });
|
|
431
|
+
this.targetHandleSize = signal({ width: 0, height: 0 });
|
|
432
|
+
this.sourceHandleOffset = computed(() => {
|
|
433
|
+
switch (this.sourcePosition()) {
|
|
434
|
+
case 'left': return { x: -(this.sourceHandleSize().width / 2), y: 0 };
|
|
435
|
+
case 'right': return { x: this.sourceHandleSize().width / 2, y: 0 };
|
|
436
|
+
case 'top': return { x: 0, y: -(this.sourceHandleSize().height / 2) };
|
|
437
|
+
case 'bottom': return { x: 0, y: this.sourceHandleSize().height / 2 };
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
this.targetHandleOffset = computed(() => {
|
|
441
|
+
switch (this.targetPosition()) {
|
|
442
|
+
case 'left': return { x: -(this.targetHandleSize().width / 2), y: 0 };
|
|
443
|
+
case 'right': return { x: this.targetHandleSize().width / 2, y: 0 };
|
|
444
|
+
case 'top': return { x: 0, y: -(this.targetHandleSize().height / 2) };
|
|
445
|
+
case 'bottom': return { x: 0, y: this.targetHandleSize().height / 2 };
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
this.sourceOffsetAligned = computed(() => {
|
|
449
|
+
return {
|
|
450
|
+
x: this.sourceOffset().x - (this.sourceHandleSize().width / 2),
|
|
451
|
+
y: this.sourceOffset().y - (this.sourceHandleSize().height / 2),
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
this.targetOffsetAligned = computed(() => {
|
|
455
|
+
return {
|
|
456
|
+
x: this.targetOffset().x - (this.targetHandleSize().width / 2),
|
|
457
|
+
y: this.targetOffset().y - (this.targetHandleSize().height / 2),
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
this.draggable = true;
|
|
461
|
+
// disabled for configuration for now
|
|
462
|
+
this.magnetRadius = 20;
|
|
463
|
+
this.point.set(node.point);
|
|
464
|
+
if (isDefined(node.draggable))
|
|
465
|
+
this.draggable = node.draggable;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Bind parent flow model to node
|
|
469
|
+
*
|
|
470
|
+
* @param flow parent flow
|
|
471
|
+
*/
|
|
472
|
+
bindFlow(flow) {
|
|
473
|
+
this.flow = flow;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
class EdgeLabelModel {
|
|
478
|
+
constructor(edgeLabel) {
|
|
479
|
+
this.edgeLabel = edgeLabel;
|
|
480
|
+
this.size = signal({ width: 0, height: 0 });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Get point on line
|
|
486
|
+
*
|
|
487
|
+
* https://math.stackexchange.com/questions/563566/how-do-i-find-the-middle1-2-1-3-1-4-etc-of-a-line
|
|
488
|
+
*/
|
|
489
|
+
function getPointOnLineByRatio(start, end, ratio) {
|
|
490
|
+
return {
|
|
491
|
+
x: (1 - ratio) * start.x + ratio * end.x,
|
|
492
|
+
y: (1 - ratio) * start.y + ratio * end.y,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function straightPath(source, target, usingPoints = [false, false, false]) {
|
|
497
|
+
const [start, center, end] = usingPoints;
|
|
498
|
+
const nullPoint = { x: 0, y: 0 };
|
|
499
|
+
const path$1 = path();
|
|
500
|
+
path$1.moveTo(source.x, source.y);
|
|
501
|
+
path$1.lineTo(target.x, target.y);
|
|
502
|
+
return {
|
|
503
|
+
path: path$1.toString(),
|
|
504
|
+
points: {
|
|
505
|
+
start: start ? getPointOnLineByRatio(source, target, .15) : nullPoint,
|
|
506
|
+
center: center ? getPointOnLineByRatio(source, target, .50) : nullPoint,
|
|
507
|
+
end: end ? getPointOnLineByRatio(source, target, .85) : nullPoint,
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function bezierPath(source, target, sourcePosition, targetPosition, usingPoints = [false, false, false]) {
|
|
513
|
+
const path$1 = path();
|
|
514
|
+
path$1.moveTo(source.x, source.y);
|
|
515
|
+
const distanceVector = { x: source.x - target.x, y: source.y - target.y };
|
|
516
|
+
const firstControl = calcControlPoint(source, sourcePosition, distanceVector);
|
|
517
|
+
const secondControl = calcControlPoint(target, targetPosition, distanceVector);
|
|
518
|
+
path$1.bezierCurveTo(firstControl.x, firstControl.y, secondControl.x, secondControl.y, target.x, target.y);
|
|
519
|
+
return getPathData(path$1, source, target, firstControl, secondControl, usingPoints);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Calculate control point based on provided point
|
|
523
|
+
*
|
|
524
|
+
* @param point relative this point control point is gonna be computed (the source or the target)
|
|
525
|
+
* @param pointPosition position of {point} on block
|
|
526
|
+
* @param distanceVector transmits the distance between the source and the target as x and y coordinates
|
|
527
|
+
*/
|
|
528
|
+
function calcControlPoint(point, pointPosition, distanceVector) {
|
|
529
|
+
const factorPoint = { x: 0, y: 0 };
|
|
530
|
+
switch (pointPosition) {
|
|
531
|
+
case 'top':
|
|
532
|
+
factorPoint.y = 1;
|
|
533
|
+
break;
|
|
534
|
+
case 'bottom':
|
|
535
|
+
factorPoint.y = -1;
|
|
536
|
+
break;
|
|
537
|
+
case 'right':
|
|
538
|
+
factorPoint.x = 1;
|
|
539
|
+
break;
|
|
540
|
+
case 'left':
|
|
541
|
+
factorPoint.x = -1;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
// TODO: explain name
|
|
545
|
+
const fullDistanceVector = {
|
|
546
|
+
x: distanceVector.x * Math.abs(factorPoint.x),
|
|
547
|
+
y: distanceVector.y * Math.abs(factorPoint.y),
|
|
548
|
+
};
|
|
549
|
+
// TODO: probably need to make this configurable
|
|
550
|
+
const curvature = 0.25;
|
|
551
|
+
// thanks colleagues from react/svelte world
|
|
552
|
+
// https://github.com/xyflow/xyflow/blob/f0117939bae934447fa7f232081f937169ee23b5/packages/system/src/utils/edges/bezier-edge.ts#L56
|
|
553
|
+
const controlOffset = curvature * 25 * Math.sqrt(Math.abs(fullDistanceVector.x + fullDistanceVector.y));
|
|
554
|
+
return {
|
|
555
|
+
x: point.x + factorPoint.x * controlOffset,
|
|
556
|
+
y: point.y - factorPoint.y * controlOffset,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function getPathData(path, source, target, firstControl, secondControl, usingPoints) {
|
|
560
|
+
const [start, center, end] = usingPoints;
|
|
561
|
+
const nullPoint = { x: 0, y: 0 };
|
|
562
|
+
return {
|
|
563
|
+
path: path.toString(),
|
|
564
|
+
points: {
|
|
565
|
+
start: start
|
|
566
|
+
? getPointOnBezier(source, target, firstControl, secondControl, 0.1)
|
|
567
|
+
: nullPoint,
|
|
568
|
+
center: center
|
|
569
|
+
? getPointOnBezier(source, target, firstControl, secondControl, 0.5)
|
|
570
|
+
: nullPoint,
|
|
571
|
+
end: end
|
|
572
|
+
? getPointOnBezier(source, target, firstControl, secondControl, 0.9)
|
|
573
|
+
: nullPoint,
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Get point on bezier curve by ratio
|
|
579
|
+
*/
|
|
580
|
+
function getPointOnBezier(sourcePoint, targetPoint, controlPoint1, controlPoint2, ratio) {
|
|
581
|
+
const fromSourceToFirstControl = getPointOnLineByRatio(sourcePoint, controlPoint1, ratio);
|
|
582
|
+
const fromFirstControlToSecond = getPointOnLineByRatio(controlPoint1, controlPoint2, ratio);
|
|
583
|
+
const fromSecondControlToTarget = getPointOnLineByRatio(controlPoint2, targetPoint, ratio);
|
|
584
|
+
return getPointOnLineByRatio(getPointOnLineByRatio(fromSourceToFirstControl, fromFirstControlToSecond, ratio), getPointOnLineByRatio(fromFirstControlToSecond, fromSecondControlToTarget, ratio), ratio);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
class EdgeModel {
|
|
588
|
+
constructor(edge) {
|
|
589
|
+
this.edge = edge;
|
|
590
|
+
this.path = computed(() => {
|
|
591
|
+
const source = this.source.sourcePointAbsolute();
|
|
592
|
+
const target = this.target.targetPointAbsolute();
|
|
593
|
+
switch (this.curve) {
|
|
594
|
+
case 'straight':
|
|
595
|
+
return straightPath(source, target, this.usingPoints);
|
|
596
|
+
case 'bezier':
|
|
597
|
+
return bezierPath(source, target, this.source.sourcePosition(), this.target.targetPosition(), this.usingPoints);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
this.edgeLabels = {};
|
|
601
|
+
this.type = edge.type ?? 'default';
|
|
602
|
+
this.curve = edge.curve ?? 'bezier';
|
|
603
|
+
if (edge.edgeLabels?.start)
|
|
604
|
+
this.edgeLabels.start = new EdgeLabelModel(edge.edgeLabels.start);
|
|
605
|
+
if (edge.edgeLabels?.center)
|
|
606
|
+
this.edgeLabels.center = new EdgeLabelModel(edge.edgeLabels.center);
|
|
607
|
+
if (edge.edgeLabels?.end)
|
|
608
|
+
this.edgeLabels.end = new EdgeLabelModel(edge.edgeLabels.end);
|
|
609
|
+
this.usingPoints = [!!this.edgeLabels.start, !!this.edgeLabels.center, !!this.edgeLabels.end];
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
class ReferenceKeeper {
|
|
614
|
+
/**
|
|
615
|
+
* Create new models for new node references and keep old models for old node references
|
|
616
|
+
*/
|
|
617
|
+
static nodes(newNodes, oldNodeModels) {
|
|
618
|
+
const oldNodesMap = new Map();
|
|
619
|
+
oldNodeModels.forEach(model => oldNodesMap.set(model.node, model));
|
|
620
|
+
return newNodes.map(newNode => {
|
|
621
|
+
if (oldNodesMap.has(newNode))
|
|
622
|
+
return oldNodesMap.get(newNode);
|
|
623
|
+
else
|
|
624
|
+
return new NodeModel(newNode);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Create new models for new edge references and keep old models for old edge references
|
|
629
|
+
*/
|
|
630
|
+
static edges(newEdges, oldEdgeModels) {
|
|
631
|
+
const oldEdgesMap = new Map();
|
|
632
|
+
oldEdgeModels.forEach(model => oldEdgesMap.set(model.edge, model));
|
|
633
|
+
return newEdges.map(newEdge => {
|
|
634
|
+
if (oldEdgesMap.has(newEdge))
|
|
635
|
+
return oldEdgesMap.get(newEdge);
|
|
636
|
+
else
|
|
637
|
+
return new EdgeModel(newEdge);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
class NodesChangeService {
|
|
643
|
+
constructor() {
|
|
644
|
+
this.entitiesService = inject(FlowEntitiesService);
|
|
645
|
+
this.nodesPositionChange$ = toObservable(this.entitiesService.nodes)
|
|
646
|
+
.pipe(
|
|
647
|
+
// Check for nodes list change and watch for specific node from this list change its position
|
|
648
|
+
switchMap((nodes) => merge(...nodes.map(node => node.point$.pipe(
|
|
649
|
+
// skip initial position from signal
|
|
650
|
+
skip(1), map(() => node))))),
|
|
651
|
+
// For now it's a single node, later this list will also be filled
|
|
652
|
+
// with child node position changes
|
|
653
|
+
map(changedNode => [
|
|
654
|
+
{ type: 'position', id: changedNode.node.id, point: changedNode.point() }
|
|
655
|
+
]));
|
|
656
|
+
this.nodeAddChange$ = toObservable(this.entitiesService.nodes)
|
|
657
|
+
.pipe(pairwise(), map(([oldList, newList]) => newList.filter(node => !oldList.includes(node))), filter((nodes) => !!nodes.length), map((nodes) => nodes.map(node => ({ type: 'add', id: node.node.id }))));
|
|
658
|
+
this.nodeRemoveChange$ = toObservable(this.entitiesService.nodes)
|
|
659
|
+
.pipe(pairwise(), map(([oldList, newList]) => oldList.filter(node => !newList.includes(node))), filter((nodes) => !!nodes.length), map((nodes) => nodes.map(node => ({ type: 'remove', id: node.node.id }))));
|
|
660
|
+
this.changes$ = merge(this.nodesPositionChange$, this.nodeAddChange$, this.nodeRemoveChange$);
|
|
661
|
+
}
|
|
662
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodesChangeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
663
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodesChangeService }); }
|
|
664
|
+
}
|
|
665
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodesChangeService, decorators: [{
|
|
666
|
+
type: Injectable
|
|
667
|
+
}] });
|
|
668
|
+
|
|
669
|
+
class EdgeChangesService {
|
|
670
|
+
constructor() {
|
|
671
|
+
this.entitiesService = inject(FlowEntitiesService);
|
|
672
|
+
this.edgeDetachedChange$ = toObservable(computed(() => {
|
|
673
|
+
const nodes = this.entitiesService.nodes();
|
|
674
|
+
const edges = untracked(this.entitiesService.edges);
|
|
675
|
+
return edges.filter(({ source, target }) => !nodes.includes(source) || !nodes.includes(target));
|
|
676
|
+
})).pipe(
|
|
677
|
+
// TODO check why there are 2 emits from single call inside computed
|
|
678
|
+
filter(edges => !!edges.length), map((edges) => edges.map(({ edge }) => ({ type: 'detached', id: edge.id }))));
|
|
679
|
+
this.edgeAddChange$ = toObservable(this.entitiesService.edges)
|
|
680
|
+
.pipe(pairwise(), map(([oldList, newList]) => {
|
|
681
|
+
return newList.filter(edge => !oldList.includes(edge));
|
|
682
|
+
}), filter(edges => !!edges.length), map((edges) => edges.map(({ edge }) => ({ type: 'add', id: edge.id }))));
|
|
683
|
+
this.edgeRemoveChange$ = toObservable(this.entitiesService.edges)
|
|
684
|
+
.pipe(pairwise(), map(([oldList, newList]) => {
|
|
685
|
+
return oldList.filter(edge => !newList.includes(edge));
|
|
686
|
+
}), filter(edges => !!edges.length), map((edges) => edges.map(({ edge }) => ({ type: 'remove', id: edge.id }))));
|
|
687
|
+
this.changes$ = merge(this.edgeDetachedChange$, this.edgeAddChange$, this.edgeRemoveChange$)
|
|
688
|
+
.pipe(
|
|
689
|
+
// this fixes the case when user gets 'deteched' changes
|
|
690
|
+
// and tries to delete these edges inside stream
|
|
691
|
+
// angular may ignore this change because [edges] input changed
|
|
692
|
+
// right after [nodes] input change
|
|
693
|
+
observeOn(asyncScheduler));
|
|
694
|
+
}
|
|
695
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeChangesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
696
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeChangesService }); }
|
|
697
|
+
}
|
|
698
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeChangesService, decorators: [{
|
|
699
|
+
type: Injectable
|
|
700
|
+
}] });
|
|
701
|
+
|
|
702
|
+
class ChangesControllerDirective {
|
|
703
|
+
constructor() {
|
|
704
|
+
this.nodesChangeService = inject(NodesChangeService);
|
|
705
|
+
this.edgesChangeService = inject(EdgeChangesService);
|
|
706
|
+
this.onNodesChange = new EventEmitter();
|
|
707
|
+
this.onEdgesChange = new EventEmitter();
|
|
708
|
+
this.nodesChangeProxySubscription = this.nodesChangeService.changes$
|
|
709
|
+
.pipe(tap((changes) => this.onNodesChange.emit(changes)), takeUntilDestroyed())
|
|
710
|
+
.subscribe();
|
|
711
|
+
this.edgesChangeProxySubscription = this.edgesChangeService.changes$
|
|
712
|
+
.pipe(tap((changes) => this.onEdgesChange.emit(changes)), takeUntilDestroyed())
|
|
713
|
+
.subscribe();
|
|
714
|
+
}
|
|
715
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ChangesControllerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
716
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ChangesControllerDirective, isStandalone: true, selector: "[changesController]", outputs: { onNodesChange: "onNodesChange", onEdgesChange: "onEdgesChange" }, ngImport: i0 }); }
|
|
717
|
+
}
|
|
718
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ChangesControllerDirective, decorators: [{
|
|
719
|
+
type: Directive,
|
|
720
|
+
args: [{
|
|
721
|
+
selector: '[changesController]',
|
|
722
|
+
standalone: true
|
|
723
|
+
}]
|
|
724
|
+
}], propDecorators: { onNodesChange: [{
|
|
725
|
+
type: Output
|
|
726
|
+
}], onEdgesChange: [{
|
|
727
|
+
type: Output
|
|
728
|
+
}] } });
|
|
729
|
+
|
|
730
|
+
class NodeComponent {
|
|
731
|
+
constructor() {
|
|
732
|
+
this.draggableService = inject(DraggableService);
|
|
733
|
+
this.flowStatusService = inject(FlowStatusService);
|
|
734
|
+
this.flowEntitiesService = inject(FlowEntitiesService);
|
|
735
|
+
this.hostRef = inject(ElementRef);
|
|
736
|
+
this.showMagnet = computed(() => this.flowStatusService.status().state === 'connection-start' ||
|
|
737
|
+
this.flowStatusService.status().state === 'connection-validation');
|
|
738
|
+
this.defaultHandleStrokeWidth = 2;
|
|
739
|
+
this.sourceHanldeState = signal('idle');
|
|
740
|
+
this.targetHandleState = signal('idle');
|
|
741
|
+
this.sourceHanldeStateReadonly = this.sourceHanldeState.asReadonly();
|
|
742
|
+
this.targetHanldeStateReadonly = this.targetHandleState.asReadonly();
|
|
743
|
+
}
|
|
744
|
+
ngOnInit() {
|
|
745
|
+
this.draggableService.toggleDraggable(this.hostRef.nativeElement, this.nodeModel);
|
|
746
|
+
}
|
|
747
|
+
ngAfterViewInit() {
|
|
748
|
+
// TODO remove microtask
|
|
749
|
+
queueMicrotask(() => {
|
|
750
|
+
if (this.nodeModel.node.type === 'default') {
|
|
751
|
+
const { width, height } = this.nodeContentRef.nativeElement.getBBox();
|
|
752
|
+
this.nodeModel.size.set({ width, height });
|
|
753
|
+
}
|
|
754
|
+
if (this.nodeModel.node.type === 'html-template') {
|
|
755
|
+
const width = this.htmlWrapperRef.nativeElement.clientWidth;
|
|
756
|
+
const height = this.htmlWrapperRef.nativeElement.clientHeight;
|
|
757
|
+
this.nodeModel.size.set({ width, height });
|
|
758
|
+
}
|
|
759
|
+
this.setSourceHandleSize();
|
|
760
|
+
this.setTargetHandleSize();
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
ngOnDestroy() {
|
|
764
|
+
this.draggableService.destroy(this.hostRef.nativeElement);
|
|
765
|
+
}
|
|
766
|
+
startConnection(event) {
|
|
767
|
+
// ignore drag by stopping propagation
|
|
768
|
+
event.stopPropagation();
|
|
769
|
+
this.flowStatusService.setConnectionStartStatus(this.nodeModel);
|
|
770
|
+
}
|
|
771
|
+
endConnection() {
|
|
772
|
+
const status = this.flowStatusService.status();
|
|
773
|
+
if (status.state === 'connection-validation') {
|
|
774
|
+
const sourceNode = status.payload.sourceNode;
|
|
775
|
+
const targetNode = this.nodeModel;
|
|
776
|
+
batchStatusChanges(
|
|
777
|
+
// call to create connection
|
|
778
|
+
() => this.flowStatusService.setConnectionEndStatus(sourceNode, targetNode),
|
|
779
|
+
// when connection created, we need go back to idle status
|
|
780
|
+
() => this.flowStatusService.setIdleStatus());
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* TODO srp
|
|
785
|
+
*/
|
|
786
|
+
validateTargetHandle() {
|
|
787
|
+
const status = this.flowStatusService.status();
|
|
788
|
+
if (status.state === 'connection-start') {
|
|
789
|
+
const sourceNode = status.payload.sourceNode;
|
|
790
|
+
const targetNode = this.nodeModel;
|
|
791
|
+
const source = sourceNode.node.id;
|
|
792
|
+
const target = targetNode.node.id;
|
|
793
|
+
const valid = this.flowEntitiesService.connection().validator({ source, target });
|
|
794
|
+
this.targetHandleState.set(valid ? 'valid' : 'invalid');
|
|
795
|
+
this.flowStatusService.setConnectionValidationStatus(sourceNode, targetNode, valid);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* TODO srp
|
|
800
|
+
*/
|
|
801
|
+
resetValidateTargetHandle() {
|
|
802
|
+
this.targetHandleState.set('idle');
|
|
803
|
+
// drop back to start status
|
|
804
|
+
const status = this.flowStatusService.status();
|
|
805
|
+
if (status.state === 'connection-validation') {
|
|
806
|
+
this.flowStatusService.setConnectionStartStatus(status.payload.sourceNode);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
getHandleContext(type) {
|
|
810
|
+
if (type === 'source') {
|
|
811
|
+
return {
|
|
812
|
+
$implicit: {
|
|
813
|
+
point: this.nodeModel.sourceOffset,
|
|
814
|
+
alignedPoint: this.nodeModel.sourceOffsetAligned,
|
|
815
|
+
state: this.sourceHanldeStateReadonly
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
$implicit: {
|
|
821
|
+
point: this.nodeModel.targetOffset,
|
|
822
|
+
alignedPoint: this.nodeModel.targetOffsetAligned,
|
|
823
|
+
state: this.targetHanldeStateReadonly
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
setSourceHandleSize() {
|
|
828
|
+
// if handle template provided, we don't know its stroke so it's 0
|
|
829
|
+
const strokeWidth = this.handleTemplate ? 0 : (2 * this.defaultHandleStrokeWidth);
|
|
830
|
+
const sourceBox = this.sourceHandleRef.nativeElement.getBBox({ stroke: true });
|
|
831
|
+
this.nodeModel.sourceHandleSize.set({
|
|
832
|
+
width: sourceBox.width + strokeWidth,
|
|
833
|
+
height: sourceBox.height + strokeWidth
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
setTargetHandleSize() {
|
|
837
|
+
const strokeWidth = this.handleTemplate ? 0 : (2 * this.defaultHandleStrokeWidth);
|
|
838
|
+
const targetBox = this.targetHandleRef.nativeElement.getBBox({ stroke: true });
|
|
839
|
+
this.nodeModel.targetHandleSize.set({
|
|
840
|
+
width: targetBox.width + strokeWidth,
|
|
841
|
+
height: targetBox.height + strokeWidth
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
845
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: NodeComponent, selector: "g[node]", inputs: { nodeModel: "nodeModel", nodeHtmlTemplate: "nodeHtmlTemplate", handleTemplate: "handleTemplate" }, viewQueries: [{ propertyName: "nodeContentRef", first: true, predicate: ["nodeContent"], descendants: true }, { propertyName: "htmlWrapperRef", first: true, predicate: ["htmlWrapper"], descendants: true }, { propertyName: "sourceHandleRef", first: true, predicate: ["sourceHandle"], descendants: true }, { propertyName: "targetHandleRef", first: true, predicate: ["targetHandle"], descendants: true }], ngImport: i0, template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n #nodeContent\n width=\"100\"\n height=\"50\"\n>\n <div class=\"default-node\" [innerHTML]=\"nodeModel.node.text ?? ''\"></div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n *ngTemplateOutlet=\"nodeHtmlTemplate; context: { $implicit: { node: nodeModel.node } }\"\n />\n </div>\n</svg:foreignObject>\n\n<ng-container *ngIf=\"handleTemplate\">\n <svg:g #sourceHandle (mousedown)=\"startConnection($event)\">\n <ng-container *ngTemplateOutlet=\"handleTemplate; context: getHandleContext('source')\" />\n </svg:g>\n\n <svg:g #targetHandle>\n <ng-container *ngTemplateOutlet=\"handleTemplate; context: getHandleContext('target')\" />\n </svg:g>\n</ng-container>\n\n<ng-container *ngIf=\"!handleTemplate\">\n <svg:circle\n #sourceHandle\n [attr.cx]=\"nodeModel.sourceOffset().x\"\n [attr.cy]=\"nodeModel.sourceOffset().y\"\n [attr.stroke-width]=\"defaultHandleStrokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mousedown)=\"startConnection($event)\"\n />\n\n <svg:circle\n #targetHandle\n [attr.cx]=\"nodeModel.targetOffset().x\"\n [attr.cy]=\"nodeModel.targetOffset().y\"\n [attr.stroke-width]=\"defaultHandleStrokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mouseup)=\"endConnection()\"\n />\n</ng-container>\n\n<svg:circle\n *ngIf=\"showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"nodeModel.targetOffset().x\"\n [attr.cy]=\"nodeModel.targetOffset().y\"\n (mouseup)=\"endConnection(); resetValidateTargetHandle()\"\n (mouseover)=\"validateTargetHandle()\"\n (mouseout)=\"resetValidateTargetHandle()\"\n/>\n\n\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{max-width:100px;max-height:100px;width:100px;height:50px;border:2px solid black;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
846
|
+
}
|
|
847
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, decorators: [{
|
|
848
|
+
type: Component,
|
|
849
|
+
args: [{ selector: 'g[node]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n #nodeContent\n width=\"100\"\n height=\"50\"\n>\n <div class=\"default-node\" [innerHTML]=\"nodeModel.node.text ?? ''\"></div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n *ngTemplateOutlet=\"nodeHtmlTemplate; context: { $implicit: { node: nodeModel.node } }\"\n />\n </div>\n</svg:foreignObject>\n\n<ng-container *ngIf=\"handleTemplate\">\n <svg:g #sourceHandle (mousedown)=\"startConnection($event)\">\n <ng-container *ngTemplateOutlet=\"handleTemplate; context: getHandleContext('source')\" />\n </svg:g>\n\n <svg:g #targetHandle>\n <ng-container *ngTemplateOutlet=\"handleTemplate; context: getHandleContext('target')\" />\n </svg:g>\n</ng-container>\n\n<ng-container *ngIf=\"!handleTemplate\">\n <svg:circle\n #sourceHandle\n [attr.cx]=\"nodeModel.sourceOffset().x\"\n [attr.cy]=\"nodeModel.sourceOffset().y\"\n [attr.stroke-width]=\"defaultHandleStrokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mousedown)=\"startConnection($event)\"\n />\n\n <svg:circle\n #targetHandle\n [attr.cx]=\"nodeModel.targetOffset().x\"\n [attr.cy]=\"nodeModel.targetOffset().y\"\n [attr.stroke-width]=\"defaultHandleStrokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mouseup)=\"endConnection()\"\n />\n</ng-container>\n\n<svg:circle\n *ngIf=\"showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"nodeModel.targetOffset().x\"\n [attr.cy]=\"nodeModel.targetOffset().y\"\n (mouseup)=\"endConnection(); resetValidateTargetHandle()\"\n (mouseover)=\"validateTargetHandle()\"\n (mouseout)=\"resetValidateTargetHandle()\"\n/>\n\n\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{max-width:100px;max-height:100px;width:100px;height:50px;border:2px solid black;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}\n"] }]
|
|
850
|
+
}], propDecorators: { nodeModel: [{
|
|
851
|
+
type: Input
|
|
852
|
+
}], nodeHtmlTemplate: [{
|
|
853
|
+
type: Input
|
|
854
|
+
}], handleTemplate: [{
|
|
855
|
+
type: Input
|
|
856
|
+
}], nodeContentRef: [{
|
|
857
|
+
type: ViewChild,
|
|
858
|
+
args: ['nodeContent']
|
|
859
|
+
}], htmlWrapperRef: [{
|
|
860
|
+
type: ViewChild,
|
|
861
|
+
args: ['htmlWrapper']
|
|
862
|
+
}], sourceHandleRef: [{
|
|
863
|
+
type: ViewChild,
|
|
864
|
+
args: ['sourceHandle']
|
|
865
|
+
}], targetHandleRef: [{
|
|
866
|
+
type: ViewChild,
|
|
867
|
+
args: ['targetHandle']
|
|
868
|
+
}] } });
|
|
869
|
+
|
|
870
|
+
class EdgeLabelComponent {
|
|
871
|
+
constructor() {
|
|
872
|
+
/**
|
|
873
|
+
* Centered point of label
|
|
874
|
+
*
|
|
875
|
+
* TODO: maybe put it into EdgeLabelModel
|
|
876
|
+
*/
|
|
877
|
+
this.edgeLabelPoint = computed(() => {
|
|
878
|
+
const point = this.pointSignal();
|
|
879
|
+
const { width, height } = this.model.size();
|
|
880
|
+
return {
|
|
881
|
+
x: point.x - (width / 2),
|
|
882
|
+
y: point.y - (height / 2)
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
this.pointSignal = signal({ x: 0, y: 0 });
|
|
886
|
+
}
|
|
887
|
+
set point(point) { this.pointSignal.set(point); }
|
|
888
|
+
ngAfterViewInit() {
|
|
889
|
+
queueMicrotask(() => {
|
|
890
|
+
// this is a fix for visual artifact in chrome that for some reason adresses only for edge label.
|
|
891
|
+
// the bug reproduces if edgeLabelWrapperRef size fully matched the size of parent foreignObject
|
|
892
|
+
const MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME = 2;
|
|
893
|
+
const width = this.edgeLabelWrapperRef.nativeElement.clientWidth + MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME;
|
|
894
|
+
const height = this.edgeLabelWrapperRef.nativeElement.clientHeight + MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME;
|
|
895
|
+
this.model.size.set({ width, height });
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
getLabelContext() {
|
|
899
|
+
return {
|
|
900
|
+
$implicit: {
|
|
901
|
+
edge: this.edgeModel.edge,
|
|
902
|
+
label: this.model.edgeLabel
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeLabelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
907
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: EdgeLabelComponent, selector: "g[edgeLabel]", inputs: { model: "model", edgeModel: "edgeModel", point: "point", htmlTemplate: "htmlTemplate" }, viewQueries: [{ propertyName: "edgeLabelWrapperRef", first: true, predicate: ["edgeLabelWrapper"], descendants: true }], ngImport: i0, template: "<svg:foreignObject\n [attr.x]=\"edgeLabelPoint().x\"\n [attr.y]=\"edgeLabelPoint().y\"\n [attr.width]=\"model.size().width\"\n [attr.height]=\"model.size().height\"\n *ngIf=\"model.edgeLabel.type === 'html-template' && htmlTemplate\"\n>\n <div #edgeLabelWrapper class=\"edge-label-wrapper\">\n <ng-container\n *ngTemplateOutlet=\"htmlTemplate; context: getLabelContext()\"\n />\n </div>\n</svg:foreignObject>\n", styles: [".edge-label-wrapper{width:max-content;margin-top:1px;margin-left:1px}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
908
|
+
}
|
|
909
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeLabelComponent, decorators: [{
|
|
910
|
+
type: Component,
|
|
911
|
+
args: [{ selector: 'g[edgeLabel]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg:foreignObject\n [attr.x]=\"edgeLabelPoint().x\"\n [attr.y]=\"edgeLabelPoint().y\"\n [attr.width]=\"model.size().width\"\n [attr.height]=\"model.size().height\"\n *ngIf=\"model.edgeLabel.type === 'html-template' && htmlTemplate\"\n>\n <div #edgeLabelWrapper class=\"edge-label-wrapper\">\n <ng-container\n *ngTemplateOutlet=\"htmlTemplate; context: getLabelContext()\"\n />\n </div>\n</svg:foreignObject>\n", styles: [".edge-label-wrapper{width:max-content;margin-top:1px;margin-left:1px}\n"] }]
|
|
912
|
+
}], propDecorators: { model: [{
|
|
913
|
+
type: Input
|
|
914
|
+
}], edgeModel: [{
|
|
915
|
+
type: Input
|
|
916
|
+
}], point: [{
|
|
917
|
+
type: Input
|
|
918
|
+
}], htmlTemplate: [{
|
|
919
|
+
type: Input
|
|
920
|
+
}], edgeLabelWrapperRef: [{
|
|
921
|
+
type: ViewChild,
|
|
922
|
+
args: ['edgeLabelWrapper']
|
|
923
|
+
}] } });
|
|
924
|
+
|
|
925
|
+
class EdgeComponent {
|
|
926
|
+
constructor() {
|
|
927
|
+
this.markerStartUrl = computed(() => {
|
|
928
|
+
const marker = this.model.edge.markers?.start;
|
|
929
|
+
return marker ? `url(#${hashCode(JSON.stringify(marker))})` : '';
|
|
930
|
+
});
|
|
931
|
+
this.markerEndUrl = computed(() => {
|
|
932
|
+
const marker = this.model.edge.markers?.end;
|
|
933
|
+
return marker ? `url(#${hashCode(JSON.stringify(marker))})` : '';
|
|
934
|
+
});
|
|
935
|
+
this.defaultColor = 'rgb(177, 177, 183)';
|
|
936
|
+
}
|
|
937
|
+
ngOnInit() {
|
|
938
|
+
this.edgeContext = {
|
|
939
|
+
$implicit: {
|
|
940
|
+
// TODO: check if edge could change
|
|
941
|
+
edge: this.model.edge,
|
|
942
|
+
path: computed(() => this.model.path().path),
|
|
943
|
+
markerStart: this.markerStartUrl,
|
|
944
|
+
markerEnd: this.markerEndUrl
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
949
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: EdgeComponent, selector: "g[edge]", inputs: { model: "model", edgeTemplate: "edgeTemplate", edgeLabelHtmlTemplate: "edgeLabelHtmlTemplate" }, ngImport: i0, template: "<svg:path\n *ngIf=\"model.type === 'default'\"\n [attr.d]=\"model.path().path\"\n [attr.marker-start]=\"markerStartUrl()\"\n [attr.marker-end]=\"markerEndUrl()\"\n [attr.stroke]=\"defaultColor\"\n stroke-width=\"2\"\n fill=\"none\"\n/>\n\n<ng-container *ngIf=\"model.type === 'template' && edgeTemplate\">\n <ng-container *ngTemplateOutlet=\"edgeTemplate; context: edgeContext\"></ng-container>\n</ng-container>\n\n<ng-container *ngIf=\"model.edgeLabels?.start as label\">\n <svg:g edgeLabel\n [model]=\"label\"\n [point]=\"model.path().points.start\"\n [edgeModel]=\"model\"\n [htmlTemplate]=\"edgeLabelHtmlTemplate\"\n />\n</ng-container>\n\n<ng-container *ngIf=\"model.edgeLabels?.center as label\">\n <svg:g edgeLabel\n [model]=\"label\"\n [point]=\"model.path().points.center\"\n [edgeModel]=\"model\"\n [htmlTemplate]=\"edgeLabelHtmlTemplate\"\n />\n</ng-container>\n\n<ng-container *ngIf=\"model.edgeLabels?.end as label\">\n <svg:g edgeLabel\n [model]=\"label\"\n [point]=\"model.path().points.end\"\n [edgeModel]=\"model\"\n [htmlTemplate]=\"edgeLabelHtmlTemplate\"\n />\n</ng-container>\n", dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: EdgeLabelComponent, selector: "g[edgeLabel]", inputs: ["model", "edgeModel", "point", "htmlTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
950
|
+
}
|
|
951
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeComponent, decorators: [{
|
|
952
|
+
type: Component,
|
|
953
|
+
args: [{ selector: 'g[edge]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg:path\n *ngIf=\"model.type === 'default'\"\n [attr.d]=\"model.path().path\"\n [attr.marker-start]=\"markerStartUrl()\"\n [attr.marker-end]=\"markerEndUrl()\"\n [attr.stroke]=\"defaultColor\"\n stroke-width=\"2\"\n fill=\"none\"\n/>\n\n<ng-container *ngIf=\"model.type === 'template' && edgeTemplate\">\n <ng-container *ngTemplateOutlet=\"edgeTemplate; context: edgeContext\"></ng-container>\n</ng-container>\n\n<ng-container *ngIf=\"model.edgeLabels?.start as label\">\n <svg:g edgeLabel\n [model]=\"label\"\n [point]=\"model.path().points.start\"\n [edgeModel]=\"model\"\n [htmlTemplate]=\"edgeLabelHtmlTemplate\"\n />\n</ng-container>\n\n<ng-container *ngIf=\"model.edgeLabels?.center as label\">\n <svg:g edgeLabel\n [model]=\"label\"\n [point]=\"model.path().points.center\"\n [edgeModel]=\"model\"\n [htmlTemplate]=\"edgeLabelHtmlTemplate\"\n />\n</ng-container>\n\n<ng-container *ngIf=\"model.edgeLabels?.end as label\">\n <svg:g edgeLabel\n [model]=\"label\"\n [point]=\"model.path().points.end\"\n [edgeModel]=\"model\"\n [htmlTemplate]=\"edgeLabelHtmlTemplate\"\n />\n</ng-container>\n" }]
|
|
954
|
+
}], propDecorators: { model: [{
|
|
955
|
+
type: Input
|
|
956
|
+
}], edgeTemplate: [{
|
|
957
|
+
type: Input
|
|
958
|
+
}], edgeLabelHtmlTemplate: [{
|
|
959
|
+
type: Input
|
|
960
|
+
}] } });
|
|
961
|
+
|
|
962
|
+
class SpacePointContextDirective {
|
|
963
|
+
constructor() {
|
|
964
|
+
/**
|
|
965
|
+
* Signal with current mouse position in svg space
|
|
966
|
+
*/
|
|
967
|
+
this.svgCurrentSpacePoint = computed(() => {
|
|
968
|
+
const movement = this.mouseMovement();
|
|
969
|
+
const point = this.rootSvg.createSVGPoint();
|
|
970
|
+
point.x = movement.x;
|
|
971
|
+
point.y = movement.y;
|
|
972
|
+
return point.matrixTransform(this.host.getScreenCTM().inverse());
|
|
973
|
+
});
|
|
974
|
+
this.rootSvg = inject(RootSvgReferenceDirective).element;
|
|
975
|
+
this.host = inject(ElementRef).nativeElement;
|
|
976
|
+
this.mouseMovement = toSignal(fromEvent(this.rootSvg, 'mousemove').pipe(map(event => ({ x: event.clientX, y: event.clientY }))), { initialValue: { x: 0, y: 0 } });
|
|
977
|
+
}
|
|
978
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpacePointContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
979
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: SpacePointContextDirective, selector: "g[spacePointContext]", ngImport: i0 }); }
|
|
980
|
+
}
|
|
981
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpacePointContextDirective, decorators: [{
|
|
982
|
+
type: Directive,
|
|
983
|
+
args: [{ selector: 'g[spacePointContext]' }]
|
|
984
|
+
}] });
|
|
985
|
+
|
|
986
|
+
class ConnectionComponent {
|
|
987
|
+
constructor() {
|
|
988
|
+
this.flowStatusService = inject(FlowStatusService);
|
|
989
|
+
this.spacePointContext = inject(SpacePointContextDirective);
|
|
990
|
+
this.path = computed(() => {
|
|
991
|
+
const status = this.flowStatusService.status();
|
|
992
|
+
if (status.state === 'connection-start') {
|
|
993
|
+
const sourceNode = status.payload.sourceNode;
|
|
994
|
+
const sourcePoint = sourceNode.sourcePointAbsolute();
|
|
995
|
+
const targetPoint = this.spacePointContext.svgCurrentSpacePoint();
|
|
996
|
+
switch (this.model.curve) {
|
|
997
|
+
case 'straight': return straightPath(sourcePoint, targetPoint).path;
|
|
998
|
+
case 'bezier': return bezierPath(sourcePoint, targetPoint, sourceNode.sourcePosition(), sourceNode.targetPosition()).path;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (status.state === 'connection-validation') {
|
|
1002
|
+
const sourcePoint = status.payload.sourceNode.sourcePointAbsolute();
|
|
1003
|
+
// ignore magnet if validation failed
|
|
1004
|
+
const targetPoint = status.payload.valid
|
|
1005
|
+
? status.payload.targetNode.targetPointAbsolute()
|
|
1006
|
+
: this.spacePointContext.svgCurrentSpacePoint();
|
|
1007
|
+
switch (this.model.curve) {
|
|
1008
|
+
case 'straight': return straightPath(sourcePoint, targetPoint).path;
|
|
1009
|
+
case 'bezier': return bezierPath(sourcePoint, targetPoint, status.payload.sourceNode.sourcePosition(), status.payload.sourceNode.targetPosition()).path;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return null;
|
|
1013
|
+
});
|
|
1014
|
+
this.markerUrl = computed(() => {
|
|
1015
|
+
const marker = this.model.connection.marker;
|
|
1016
|
+
if (marker) {
|
|
1017
|
+
return `url(#${hashCode(JSON.stringify(marker))})`;
|
|
1018
|
+
}
|
|
1019
|
+
return '';
|
|
1020
|
+
});
|
|
1021
|
+
this.defaultColor = 'rgb(177, 177, 183)';
|
|
1022
|
+
}
|
|
1023
|
+
getContext() {
|
|
1024
|
+
return {
|
|
1025
|
+
$implicit: {
|
|
1026
|
+
path: this.path,
|
|
1027
|
+
marker: this.markerUrl
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ConnectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1032
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ConnectionComponent, selector: "g[connection]", inputs: { model: "model", template: "template" }, ngImport: i0, template: `
|
|
1033
|
+
<ng-container *ngIf="model.type === 'default'">
|
|
1034
|
+
<svg:path
|
|
1035
|
+
*ngIf="path() as path"
|
|
1036
|
+
[attr.d]="path"
|
|
1037
|
+
[attr.marker-end]="markerUrl()"
|
|
1038
|
+
[attr.stroke]="defaultColor"
|
|
1039
|
+
fill="none"
|
|
1040
|
+
stroke-width="2"
|
|
1041
|
+
/>
|
|
1042
|
+
</ng-container>
|
|
1043
|
+
|
|
1044
|
+
<ng-container *ngIf="model.type === 'template' && template">
|
|
1045
|
+
<ng-container *ngTemplateOutlet="template; context: getContext()" />
|
|
1046
|
+
</ng-container>
|
|
1047
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1048
|
+
}
|
|
1049
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ConnectionComponent, decorators: [{
|
|
1050
|
+
type: Component,
|
|
1051
|
+
args: [{
|
|
1052
|
+
selector: 'g[connection]',
|
|
1053
|
+
template: `
|
|
1054
|
+
<ng-container *ngIf="model.type === 'default'">
|
|
1055
|
+
<svg:path
|
|
1056
|
+
*ngIf="path() as path"
|
|
1057
|
+
[attr.d]="path"
|
|
1058
|
+
[attr.marker-end]="markerUrl()"
|
|
1059
|
+
[attr.stroke]="defaultColor"
|
|
1060
|
+
fill="none"
|
|
1061
|
+
stroke-width="2"
|
|
1062
|
+
/>
|
|
1063
|
+
</ng-container>
|
|
1064
|
+
|
|
1065
|
+
<ng-container *ngIf="model.type === 'template' && template">
|
|
1066
|
+
<ng-container *ngTemplateOutlet="template; context: getContext()" />
|
|
1067
|
+
</ng-container>
|
|
1068
|
+
`,
|
|
1069
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
1070
|
+
}]
|
|
1071
|
+
}], propDecorators: { model: [{
|
|
1072
|
+
type: Input,
|
|
1073
|
+
args: [{ required: true }]
|
|
1074
|
+
}], template: [{
|
|
1075
|
+
type: Input
|
|
1076
|
+
}] } });
|
|
1077
|
+
|
|
1078
|
+
class DefsComponent {
|
|
1079
|
+
constructor() {
|
|
1080
|
+
this.markers = new Map();
|
|
1081
|
+
this.defaultColor = 'rgb(177, 177, 183)';
|
|
1082
|
+
}
|
|
1083
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DefsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1084
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: DefsComponent, selector: "defs[flowDefs]", inputs: { markers: "markers" }, ngImport: i0, template: "<svg:marker\n *ngFor=\"let marker of markers | keyvalue\"\n [attr.id]=\"marker.key\"\n [attr.markerWidth]=\"marker.value.width ?? 16.5\"\n [attr.markerHeight]=\"marker.value.height ?? 16.5\"\n [attr.orient]=\"marker.value.orient ?? 'auto-start-reverse'\"\n viewBox=\"-10 -10 20 20\"\n [attr.markerUnits]=\"marker.value.markerUnits ?? 'userSpaceOnUse'\"\n refX=\"0\"\n refY=\"0\"\n>\n <polyline\n *ngIf=\"marker.value.type === 'arrow-closed' || !marker.value.type\"\n class=\"marker__arrow_closed\"\n [style.stroke]=\"marker.value.color ?? defaultColor\"\n [style.stroke-width]=\"marker.value.strokeWidth ?? 2\"\n [style.fill]=\"marker.value.color ?? defaultColor\"\n points=\"-5,-4 1,0 -5,4 -5,-4\"\n />\n\n <polyline\n *ngIf=\"marker.value.type === 'arrow'\"\n class=\"marker__arrow_default\"\n [style.stroke]=\"marker.value.color ?? defaultColor\"\n [style.stroke-width]=\"marker.value.strokeWidth ?? 2\"\n points=\"-5,-4 0,0 -5,4\"\n />\n</svg:marker>\n", styles: [".marker__arrow_default{stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;fill:none}.marker__arrow_closed{stroke-linecap:round;stroke-linejoin:round}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1085
|
+
}
|
|
1086
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DefsComponent, decorators: [{
|
|
1087
|
+
type: Component,
|
|
1088
|
+
args: [{ selector: 'defs[flowDefs]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg:marker\n *ngFor=\"let marker of markers | keyvalue\"\n [attr.id]=\"marker.key\"\n [attr.markerWidth]=\"marker.value.width ?? 16.5\"\n [attr.markerHeight]=\"marker.value.height ?? 16.5\"\n [attr.orient]=\"marker.value.orient ?? 'auto-start-reverse'\"\n viewBox=\"-10 -10 20 20\"\n [attr.markerUnits]=\"marker.value.markerUnits ?? 'userSpaceOnUse'\"\n refX=\"0\"\n refY=\"0\"\n>\n <polyline\n *ngIf=\"marker.value.type === 'arrow-closed' || !marker.value.type\"\n class=\"marker__arrow_closed\"\n [style.stroke]=\"marker.value.color ?? defaultColor\"\n [style.stroke-width]=\"marker.value.strokeWidth ?? 2\"\n [style.fill]=\"marker.value.color ?? defaultColor\"\n points=\"-5,-4 1,0 -5,4 -5,-4\"\n />\n\n <polyline\n *ngIf=\"marker.value.type === 'arrow'\"\n class=\"marker__arrow_default\"\n [style.stroke]=\"marker.value.color ?? defaultColor\"\n [style.stroke-width]=\"marker.value.strokeWidth ?? 2\"\n points=\"-5,-4 0,0 -5,4\"\n />\n</svg:marker>\n", styles: [".marker__arrow_default{stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;fill:none}.marker__arrow_closed{stroke-linecap:round;stroke-linejoin:round}\n"] }]
|
|
1089
|
+
}], propDecorators: { markers: [{
|
|
1090
|
+
type: Input,
|
|
1091
|
+
args: [{ required: true }]
|
|
1092
|
+
}] } });
|
|
1093
|
+
|
|
1094
|
+
// TODO: too general purpose nane
|
|
1095
|
+
class RootSvgContextDirective {
|
|
1096
|
+
constructor() {
|
|
1097
|
+
this.flowStatusService = inject(FlowStatusService);
|
|
1098
|
+
}
|
|
1099
|
+
// TODO: check for multiple instances on page
|
|
1100
|
+
resetConnection() {
|
|
1101
|
+
const status = this.flowStatusService.status();
|
|
1102
|
+
if (status.state === 'connection-start') {
|
|
1103
|
+
this.flowStatusService.setIdleStatus();
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootSvgContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1107
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: RootSvgContextDirective, selector: "svg[rootSvgContext]", host: { listeners: { "document:mouseup": "resetConnection()" } }, ngImport: i0 }); }
|
|
1108
|
+
}
|
|
1109
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootSvgContextDirective, decorators: [{
|
|
1110
|
+
type: Directive,
|
|
1111
|
+
args: [{ selector: 'svg[rootSvgContext]' }]
|
|
1112
|
+
}], propDecorators: { resetConnection: [{
|
|
1113
|
+
type: HostListener,
|
|
1114
|
+
args: ['document:mouseup']
|
|
1115
|
+
}] } });
|
|
1116
|
+
|
|
1117
|
+
const connectionControllerHostDirective = {
|
|
1118
|
+
directive: ConnectionControllerDirective,
|
|
1119
|
+
outputs: ['onConnect']
|
|
1120
|
+
};
|
|
1121
|
+
const changesControllerHostDirective = {
|
|
1122
|
+
directive: ChangesControllerDirective,
|
|
1123
|
+
outputs: ['onNodesChange', 'onEdgesChange']
|
|
1124
|
+
};
|
|
1125
|
+
class VflowComponent {
|
|
1126
|
+
constructor() {
|
|
1127
|
+
// #region DI
|
|
1128
|
+
this.viewportService = inject(ViewportService);
|
|
1129
|
+
this.flowEntitiesService = inject(FlowEntitiesService);
|
|
1130
|
+
this.nodesChangeService = inject(NodesChangeService);
|
|
1131
|
+
this.edgesChangeService = inject(EdgeChangesService);
|
|
1132
|
+
this.injector = inject(Injector);
|
|
1133
|
+
this.minZoom = 0.5;
|
|
1134
|
+
this.maxZoom = 3;
|
|
1135
|
+
this.background = '#FFFFFF';
|
|
1136
|
+
// #endregion
|
|
1137
|
+
// #region SIGNAL_API
|
|
1138
|
+
this.viewport = this.viewportService.readableViewport.asReadonly();
|
|
1139
|
+
this.nodesChange = toSignal(this.nodesChangeService.changes$, { initialValue: [] });
|
|
1140
|
+
this.edgesChange = toSignal(this.edgesChangeService.changes$, { initialValue: [] });
|
|
1141
|
+
// #endregion
|
|
1142
|
+
// #region RX_API
|
|
1143
|
+
this.viewportChanges$ = toObservable(this.viewportService.readableViewport)
|
|
1144
|
+
.pipe(skip(1)); // skip default value that set by signal
|
|
1145
|
+
this.nodesChange$ = this.nodesChangeService.changes$;
|
|
1146
|
+
this.edgesChange$ = this.edgesChangeService.changes$;
|
|
1147
|
+
// #endregion
|
|
1148
|
+
// TODO: probably better to make it injectable
|
|
1149
|
+
this.flowModel = new FlowModel();
|
|
1150
|
+
this.markers = this.flowEntitiesService.markers;
|
|
1151
|
+
}
|
|
1152
|
+
// #endregion
|
|
1153
|
+
// #region SETTINGS
|
|
1154
|
+
/**
|
|
1155
|
+
* Size for flow view
|
|
1156
|
+
*
|
|
1157
|
+
* accepts
|
|
1158
|
+
* - absolute size in format [width, height] or
|
|
1159
|
+
* - 'auto' to compute size based on parent element size
|
|
1160
|
+
*/
|
|
1161
|
+
set view(view) {
|
|
1162
|
+
this.flowModel.view.set(view);
|
|
1163
|
+
}
|
|
1164
|
+
set handlePositions(handlePositions) {
|
|
1165
|
+
this.flowModel.handlePositions.set(handlePositions);
|
|
1166
|
+
}
|
|
1167
|
+
set connection(connection) { this.flowEntitiesService.connection.set(connection); }
|
|
1168
|
+
get connection() { return this.flowEntitiesService.connection(); }
|
|
1169
|
+
// #endregion
|
|
1170
|
+
// #region MAIN_INPUTS
|
|
1171
|
+
set nodes(newNodes) {
|
|
1172
|
+
const newModels = runInInjectionContext(this.injector, () => ReferenceKeeper.nodes(newNodes, this.flowEntitiesService.nodes()));
|
|
1173
|
+
// TODO better to solve this by DI
|
|
1174
|
+
bindFlowToNodes(this.flowModel, newModels);
|
|
1175
|
+
// quick and dirty binding nodes to edges
|
|
1176
|
+
addNodesToEdges(newModels, this.flowEntitiesService.edges());
|
|
1177
|
+
this.flowEntitiesService.nodes.set(newModels);
|
|
1178
|
+
}
|
|
1179
|
+
get nodeModels() { return this.flowEntitiesService.nodes(); }
|
|
1180
|
+
set edges(newEdges) {
|
|
1181
|
+
const newModels = ReferenceKeeper.edges(newEdges, this.flowEntitiesService.edges());
|
|
1182
|
+
// quick and dirty binding nodes to edges
|
|
1183
|
+
addNodesToEdges(this.nodeModels, newModels);
|
|
1184
|
+
this.flowEntitiesService.edges.set(newModels);
|
|
1185
|
+
}
|
|
1186
|
+
get edgeModels() { return this.flowEntitiesService.validEdges(); }
|
|
1187
|
+
// #region METHODS_API
|
|
1188
|
+
viewportTo(viewport) {
|
|
1189
|
+
this.viewportService.writableViewport.set({ changeType: 'absolute', state: viewport });
|
|
1190
|
+
}
|
|
1191
|
+
zoomTo(zoom) {
|
|
1192
|
+
this.viewportService.writableViewport.set({ changeType: 'absolute', state: { zoom } });
|
|
1193
|
+
}
|
|
1194
|
+
panTo(point) {
|
|
1195
|
+
this.viewportService.writableViewport.set({ changeType: 'absolute', state: point });
|
|
1196
|
+
}
|
|
1197
|
+
getNode(id) {
|
|
1198
|
+
return this.flowEntitiesService.getNode(id)?.node;
|
|
1199
|
+
}
|
|
1200
|
+
// #endregion
|
|
1201
|
+
trackNodes(idx, { node }) {
|
|
1202
|
+
return node.id;
|
|
1203
|
+
}
|
|
1204
|
+
trackEdges(idx, { edge }) {
|
|
1205
|
+
return edge.id;
|
|
1206
|
+
}
|
|
1207
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1208
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "16.2.12", type: VflowComponent, selector: "vflow", inputs: { view: "view", minZoom: "minZoom", maxZoom: "maxZoom", handlePositions: "handlePositions", background: "background", connection: ["connection", "connection", (settings) => new ConnectionModel(settings)], nodes: "nodes", edges: "edges" }, providers: [
|
|
1209
|
+
DraggableService,
|
|
1210
|
+
ViewportService,
|
|
1211
|
+
FlowStatusService,
|
|
1212
|
+
FlowEntitiesService,
|
|
1213
|
+
NodesChangeService,
|
|
1214
|
+
EdgeChangesService
|
|
1215
|
+
], queries: [{ propertyName: "nodeHtmlDirective", first: true, predicate: NodeHtmlTemplateDirective, descendants: true }, { propertyName: "edgeTemplateDirective", first: true, predicate: EdgeTemplateDirective, descendants: true }, { propertyName: "edgeLabelHtmlDirective", first: true, predicate: EdgeLabelHtmlTemplateDirective, descendants: true }, { propertyName: "connectionTemplateDirective", first: true, predicate: ConnectionTemplateDirective, descendants: true }, { propertyName: "handleTemplateDirective", first: true, predicate: HandleTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "mapContext", first: true, predicate: MapContextDirective, descendants: true }], hostDirectives: [{ directive: ConnectionControllerDirective, outputs: ["onConnect", "onConnect"] }, { directive: ChangesControllerDirective, outputs: ["onNodesChange", "onNodesChange", "onEdgesChange", "onEdgesChange"] }], ngImport: i0, template: "<svg:svg\n rootSvgRef\n rootSvgContext\n class=\"root-svg\"\n #flow\n [style.backgroundColor]=\"background\"\n [attr.width]=\"flowModel.flowWidth()\"\n [attr.height]=\"flowModel.flowHeight()\"\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <svg:g\n mapContext\n spacePointContext\n [minZoom]=\"minZoom\"\n [maxZoom]=\"maxZoom\"\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels; trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nodeModels; trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeHtmlTemplate]=\"nodeHtmlDirective?.templateRef\"\n [handleTemplate]=\"handleTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </svg:g>\n\n</svg:svg>\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["nodeModel", "nodeHtmlTemplate", "handleTemplate"] }, { kind: "component", type: EdgeComponent, selector: "g[edge]", inputs: ["model", "edgeTemplate", "edgeLabelHtmlTemplate"] }, { kind: "component", type: ConnectionComponent, selector: "g[connection]", inputs: ["model", "template"] }, { kind: "component", type: DefsComponent, selector: "defs[flowDefs]", inputs: ["markers"] }, { kind: "directive", type: SpacePointContextDirective, selector: "g[spacePointContext]" }, { kind: "directive", type: MapContextDirective, selector: "g[mapContext]", inputs: ["minZoom", "maxZoom"] }, { kind: "directive", type: RootSvgReferenceDirective, selector: "svg[rootSvgRef]" }, { kind: "directive", type: RootSvgContextDirective, selector: "svg[rootSvgContext]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1216
|
+
}
|
|
1217
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, decorators: [{
|
|
1218
|
+
type: Component,
|
|
1219
|
+
args: [{ selector: 'vflow', changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1220
|
+
DraggableService,
|
|
1221
|
+
ViewportService,
|
|
1222
|
+
FlowStatusService,
|
|
1223
|
+
FlowEntitiesService,
|
|
1224
|
+
NodesChangeService,
|
|
1225
|
+
EdgeChangesService
|
|
1226
|
+
], hostDirectives: [
|
|
1227
|
+
connectionControllerHostDirective,
|
|
1228
|
+
changesControllerHostDirective
|
|
1229
|
+
], template: "<svg:svg\n rootSvgRef\n rootSvgContext\n class=\"root-svg\"\n #flow\n [style.backgroundColor]=\"background\"\n [attr.width]=\"flowModel.flowWidth()\"\n [attr.height]=\"flowModel.flowHeight()\"\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <svg:g\n mapContext\n spacePointContext\n [minZoom]=\"minZoom\"\n [maxZoom]=\"maxZoom\"\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels; trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nodeModels; trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeHtmlTemplate]=\"nodeHtmlDirective?.templateRef\"\n [handleTemplate]=\"handleTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </svg:g>\n\n</svg:svg>\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}\n"] }]
|
|
1230
|
+
}], propDecorators: { view: [{
|
|
1231
|
+
type: Input
|
|
1232
|
+
}], minZoom: [{
|
|
1233
|
+
type: Input
|
|
1234
|
+
}], maxZoom: [{
|
|
1235
|
+
type: Input
|
|
1236
|
+
}], handlePositions: [{
|
|
1237
|
+
type: Input
|
|
1238
|
+
}], background: [{
|
|
1239
|
+
type: Input
|
|
1240
|
+
}], connection: [{
|
|
1241
|
+
type: Input,
|
|
1242
|
+
args: [{ transform: (settings) => new ConnectionModel(settings) }]
|
|
1243
|
+
}], nodes: [{
|
|
1244
|
+
type: Input,
|
|
1245
|
+
args: [{ required: true }]
|
|
1246
|
+
}], edges: [{
|
|
1247
|
+
type: Input
|
|
1248
|
+
}], nodeHtmlDirective: [{
|
|
1249
|
+
type: ContentChild,
|
|
1250
|
+
args: [NodeHtmlTemplateDirective]
|
|
1251
|
+
}], edgeTemplateDirective: [{
|
|
1252
|
+
type: ContentChild,
|
|
1253
|
+
args: [EdgeTemplateDirective]
|
|
1254
|
+
}], edgeLabelHtmlDirective: [{
|
|
1255
|
+
type: ContentChild,
|
|
1256
|
+
args: [EdgeLabelHtmlTemplateDirective]
|
|
1257
|
+
}], connectionTemplateDirective: [{
|
|
1258
|
+
type: ContentChild,
|
|
1259
|
+
args: [ConnectionTemplateDirective]
|
|
1260
|
+
}], handleTemplateDirective: [{
|
|
1261
|
+
type: ContentChild,
|
|
1262
|
+
args: [HandleTemplateDirective]
|
|
1263
|
+
}], mapContext: [{
|
|
1264
|
+
type: ViewChild,
|
|
1265
|
+
args: [MapContextDirective]
|
|
1266
|
+
}] } });
|
|
1267
|
+
function bindFlowToNodes(flow, nodes) {
|
|
1268
|
+
nodes.forEach(n => n.bindFlow(flow));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const components = [
|
|
1272
|
+
VflowComponent,
|
|
1273
|
+
NodeComponent,
|
|
1274
|
+
EdgeComponent,
|
|
1275
|
+
EdgeLabelComponent,
|
|
1276
|
+
ConnectionComponent,
|
|
1277
|
+
DefsComponent
|
|
1278
|
+
];
|
|
1279
|
+
const directives = [
|
|
1280
|
+
SpacePointContextDirective,
|
|
1281
|
+
MapContextDirective,
|
|
1282
|
+
RootSvgReferenceDirective,
|
|
1283
|
+
RootSvgContextDirective,
|
|
1284
|
+
];
|
|
1285
|
+
const templateDirectives = [
|
|
1286
|
+
NodeHtmlTemplateDirective,
|
|
1287
|
+
EdgeLabelHtmlTemplateDirective,
|
|
1288
|
+
EdgeTemplateDirective,
|
|
1289
|
+
ConnectionTemplateDirective,
|
|
1290
|
+
HandleTemplateDirective
|
|
1291
|
+
];
|
|
1292
|
+
class VflowModule {
|
|
1293
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
1294
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.2.12", ngImport: i0, type: VflowModule, declarations: [VflowComponent,
|
|
1295
|
+
NodeComponent,
|
|
1296
|
+
EdgeComponent,
|
|
1297
|
+
EdgeLabelComponent,
|
|
1298
|
+
ConnectionComponent,
|
|
1299
|
+
DefsComponent, SpacePointContextDirective,
|
|
1300
|
+
MapContextDirective,
|
|
1301
|
+
RootSvgReferenceDirective,
|
|
1302
|
+
RootSvgContextDirective, NodeHtmlTemplateDirective,
|
|
1303
|
+
EdgeLabelHtmlTemplateDirective,
|
|
1304
|
+
EdgeTemplateDirective,
|
|
1305
|
+
ConnectionTemplateDirective,
|
|
1306
|
+
HandleTemplateDirective], imports: [CommonModule], exports: [VflowComponent, NodeHtmlTemplateDirective,
|
|
1307
|
+
EdgeLabelHtmlTemplateDirective,
|
|
1308
|
+
EdgeTemplateDirective,
|
|
1309
|
+
ConnectionTemplateDirective,
|
|
1310
|
+
HandleTemplateDirective] }); }
|
|
1311
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowModule, imports: [CommonModule] }); }
|
|
1312
|
+
}
|
|
1313
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowModule, decorators: [{
|
|
1314
|
+
type: NgModule,
|
|
1315
|
+
args: [{
|
|
1316
|
+
imports: [CommonModule],
|
|
1317
|
+
exports: [
|
|
1318
|
+
VflowComponent,
|
|
1319
|
+
...templateDirectives
|
|
1320
|
+
],
|
|
1321
|
+
declarations: [...components, ...directives, ...templateDirectives],
|
|
1322
|
+
}]
|
|
1323
|
+
}] });
|
|
1324
|
+
|
|
1325
|
+
// Modules
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Generated bundle index. Do not edit.
|
|
1329
|
+
*/
|
|
1330
|
+
|
|
1331
|
+
export { ChangesControllerDirective, ConnectionControllerDirective, ConnectionTemplateDirective, EdgeLabelHtmlTemplateDirective, EdgeTemplateDirective, HandleTemplateDirective, NodeHtmlTemplateDirective, VflowComponent, VflowModule };
|
|
1332
|
+
//# sourceMappingURL=ngx-vflow.mjs.map
|