ngx-vflow 0.7.0 → 0.9.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.
Files changed (56) hide show
  1. package/esm2022/lib/vflow/components/background/background.component.mjs +4 -4
  2. package/esm2022/lib/vflow/components/connection/connection.component.mjs +2 -2
  3. package/esm2022/lib/vflow/components/handle/handle.component.mjs +5 -5
  4. package/esm2022/lib/vflow/components/node/node.component.mjs +31 -82
  5. package/esm2022/lib/vflow/components/vflow/vflow.component.mjs +32 -16
  6. package/esm2022/lib/vflow/directives/connection-controller.directive.mjs +91 -12
  7. package/esm2022/lib/vflow/directives/flow-size-controller.directive.mjs +36 -0
  8. package/esm2022/lib/vflow/directives/map-context.directive.mjs +16 -12
  9. package/esm2022/lib/vflow/directives/space-point-context.directive.mjs +11 -5
  10. package/esm2022/lib/vflow/interfaces/box.mjs +2 -0
  11. package/esm2022/lib/vflow/interfaces/connection-settings.interface.mjs +1 -1
  12. package/esm2022/lib/vflow/interfaces/connection.internal.interface.mjs +2 -0
  13. package/esm2022/lib/vflow/interfaces/fit-view-options.interface.mjs +2 -0
  14. package/esm2022/lib/vflow/interfaces/rect.mjs +2 -0
  15. package/esm2022/lib/vflow/interfaces/viewport.interface.mjs +1 -1
  16. package/esm2022/lib/vflow/models/connection.model.mjs +28 -6
  17. package/esm2022/lib/vflow/models/handle.model.mjs +1 -1
  18. package/esm2022/lib/vflow/services/flow-entities.service.mjs +2 -2
  19. package/esm2022/lib/vflow/services/flow-settings.service.mjs +12 -4
  20. package/esm2022/lib/vflow/services/flow-status.service.mjs +7 -7
  21. package/esm2022/lib/vflow/services/handle.service.mjs +1 -1
  22. package/esm2022/lib/vflow/services/node-changes.service.mjs +3 -3
  23. package/esm2022/lib/vflow/services/viewport.service.mjs +26 -3
  24. package/esm2022/lib/vflow/types/connection-mode.type.mjs +2 -0
  25. package/esm2022/lib/vflow/utils/adjust-direction.mjs +30 -0
  26. package/esm2022/lib/vflow/utils/nodes.mjs +36 -0
  27. package/esm2022/lib/vflow/utils/viewport.mjs +15 -0
  28. package/esm2022/lib/vflow/vflow.module.mjs +6 -3
  29. package/esm2022/public-api.mjs +3 -1
  30. package/fesm2022/ngx-vflow.mjs +573 -362
  31. package/fesm2022/ngx-vflow.mjs.map +1 -1
  32. package/lib/vflow/components/node/node.component.d.ts +4 -12
  33. package/lib/vflow/components/vflow/vflow.component.d.ts +10 -4
  34. package/lib/vflow/directives/connection-controller.directive.d.ts +6 -0
  35. package/lib/vflow/directives/flow-size-controller.directive.d.ts +10 -0
  36. package/lib/vflow/directives/map-context.directive.d.ts +9 -3
  37. package/lib/vflow/directives/space-point-context.directive.d.ts +1 -0
  38. package/lib/vflow/interfaces/box.d.ts +6 -0
  39. package/lib/vflow/interfaces/connection-settings.interface.d.ts +2 -0
  40. package/lib/vflow/interfaces/connection.internal.interface.d.ts +8 -0
  41. package/lib/vflow/interfaces/fit-view-options.interface.d.ts +15 -0
  42. package/lib/vflow/interfaces/rect.d.ts +6 -0
  43. package/lib/vflow/interfaces/viewport.interface.d.ts +1 -0
  44. package/lib/vflow/models/connection.model.d.ts +5 -2
  45. package/lib/vflow/models/edge.model.d.ts +17 -1
  46. package/lib/vflow/models/handle.model.d.ts +1 -1
  47. package/lib/vflow/services/flow-settings.service.d.ts +10 -2
  48. package/lib/vflow/services/flow-status.service.d.ts +7 -18
  49. package/lib/vflow/services/viewport.service.d.ts +5 -0
  50. package/lib/vflow/types/connection-mode.type.d.ts +1 -0
  51. package/lib/vflow/utils/adjust-direction.d.ts +11 -0
  52. package/lib/vflow/utils/nodes.d.ts +3 -0
  53. package/lib/vflow/utils/viewport.d.ts +4 -0
  54. package/lib/vflow/vflow.module.d.ts +4 -3
  55. package/package.json +1 -1
  56. package/public-api.d.ts +2 -0
@@ -1,71 +1,81 @@
1
1
  import * as i1 from '@angular/common';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { signal, Injectable, inject, ElementRef, Directive, computed, effect, untracked, Input, TemplateRef, EventEmitter, Output, DestroyRef, runInInjectionContext, HostListener, Injector, Component, ChangeDetectionStrategy, ViewChild, ContentChild, NgModule } from '@angular/core';
4
+ import { signal, computed, Injectable, inject, ElementRef, Directive, effect, untracked, TemplateRef, EventEmitter, Output, DestroyRef, Input, runInInjectionContext, Injector, Component, ChangeDetectionStrategy, HostListener, ViewChild, HostBinding, ContentChild, NgModule } from '@angular/core';
5
5
  import { select } from 'd3-selection';
6
6
  import { zoomIdentity, zoom } from 'd3-zoom';
7
- import { Subject, tap, merge, BehaviorSubject, observeOn, animationFrameScheduler, switchMap, skip, map, pairwise, filter, distinctUntilChanged, asyncScheduler, zip, Observable, fromEvent, share, Subscription, startWith } from 'rxjs';
7
+ import { Subject, tap, merge, BehaviorSubject, observeOn, animationFrameScheduler, switchMap, skip, map, pairwise, filter, distinctUntilChanged, zip, asyncScheduler, fromEvent, share, Observable, startWith } from 'rxjs';
8
8
  import { takeUntilDestroyed, toSignal, toObservable } from '@angular/core/rxjs-interop';
9
9
  import { drag } from 'd3-drag';
10
10
  import { path } from 'd3-path';
11
11
  import { __decorate } from 'tslib';
12
12
 
13
- class ViewportService {
14
- constructor() {
15
- /**
16
- * Internal signal that accepts value from user by lib api
17
- * When this signal changes, lib sets new view state and update readableViewport signal
18
- */
19
- this.writableViewport = signal({
20
- changeType: 'initial',
21
- state: ViewportService.getDefaultViewport()
22
- });
23
- /**
24
- * Public signal with viewport state. User can directly read from this signal. It's updated by:
25
- * - user events on flow
26
- * - writableViewport signal
27
- */
28
- this.readableViewport = signal(ViewportService.getDefaultViewport());
29
- }
30
- /**
31
- * The default value used by d3, just copy it here
32
- *
33
- * @returns default viewport value
34
- */
35
- static getDefaultViewport() { return { zoom: 1, x: 0, y: 0 }; }
36
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ViewportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
37
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ViewportService }); }
13
+ function getNodesBounds(nodes) {
14
+ if (nodes.length === 0) {
15
+ return { x: 0, y: 0, width: 0, height: 0 };
16
+ }
17
+ let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity };
18
+ nodes.forEach(node => {
19
+ const nodeBox = nodeToBox(node);
20
+ box = getBoundsOfBoxes(box, nodeBox);
21
+ });
22
+ return boxToRect(box);
38
23
  }
39
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ViewportService, decorators: [{
40
- type: Injectable
41
- }] });
42
-
43
- function isDefined(data) {
44
- return data !== undefined;
24
+ function nodeToBox(node) {
25
+ return {
26
+ x: node.point().x,
27
+ y: node.point().y,
28
+ x2: node.point().x + node.size().width,
29
+ y2: node.point().y + node.size().height,
30
+ };
45
31
  }
46
-
47
- class RootSvgReferenceDirective {
48
- constructor() {
49
- this.element = inject(ElementRef).nativeElement;
50
- }
51
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootSvgReferenceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
52
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: RootSvgReferenceDirective, selector: "svg[rootSvgRef]", ngImport: i0 }); }
32
+ function boxToRect({ x, y, x2, y2 }) {
33
+ return {
34
+ x,
35
+ y,
36
+ width: x2 - x,
37
+ height: y2 - y,
38
+ };
39
+ }
40
+ function getBoundsOfBoxes(box1, box2) {
41
+ return {
42
+ x: Math.min(box1.x, box2.x),
43
+ y: Math.min(box1.y, box2.y),
44
+ x2: Math.max(box1.x2, box2.x2),
45
+ y2: Math.max(box1.y2, box2.y2),
46
+ };
53
47
  }
54
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootSvgReferenceDirective, decorators: [{
55
- type: Directive,
56
- args: [{
57
- selector: 'svg[rootSvgRef]'
58
- }]
59
- }] });
60
48
 
61
49
  class ConnectionModel {
62
- constructor(connection) {
63
- this.connection = connection;
64
- this.curve = connection.curve ?? 'bezier';
65
- this.type = connection.type ?? 'default';
66
- this.validator = connection.validator ?? (() => true);
50
+ constructor(settings) {
51
+ this.settings = settings;
52
+ this.curve = settings.curve ?? 'bezier';
53
+ this.type = settings.type ?? 'default';
54
+ this.mode = settings.mode ?? 'strict';
55
+ const validatorsToRun = this.getValidators(settings);
56
+ this.validator = (connection) => validatorsToRun.every((v) => v(connection));
57
+ }
58
+ getValidators(settings) {
59
+ const validators = [];
60
+ validators.push(notSelfValidator);
61
+ if (this.mode === 'loose') {
62
+ validators.push(hasSourceAndTargetHandleValidator);
63
+ }
64
+ if (settings.validator) {
65
+ validators.push(settings.validator);
66
+ }
67
+ return validators;
67
68
  }
68
69
  }
70
+ /**
71
+ * Internal validator that not allows self connections
72
+ */
73
+ const notSelfValidator = (connection) => {
74
+ return connection.source !== connection.target;
75
+ };
76
+ const hasSourceAndTargetHandleValidator = (connection) => {
77
+ return connection.sourceHandle !== undefined && connection.targetHandle !== undefined;
78
+ };
69
79
 
70
80
  function hashCode(str) {
71
81
  return str.split('').reduce((a, b) => {
@@ -97,7 +107,7 @@ class FlowEntitiesService {
97
107
  markersMap.set(hash, e.edge.markers.end);
98
108
  }
99
109
  });
100
- const connectionMarker = this.connection().connection.marker;
110
+ const connectionMarker = this.connection().settings.marker;
101
111
  if (connectionMarker) {
102
112
  const hash = hashCode(JSON.stringify(connectionMarker));
103
113
  markersMap.set(hash, connectionMarker);
@@ -126,6 +136,119 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
126
136
  type: Injectable
127
137
  }] });
128
138
 
139
+ function getViewportForBounds(bounds, width, height, minZoom, maxZoom, padding) {
140
+ const xZoom = width / (bounds.width * (1 + padding));
141
+ const yZoom = height / (bounds.height * (1 + padding));
142
+ const zoom = Math.min(xZoom, yZoom);
143
+ const clampedZoom = clamp(zoom, minZoom, maxZoom);
144
+ const boundsCenterX = bounds.x + bounds.width / 2;
145
+ const boundsCenterY = bounds.y + bounds.height / 2;
146
+ const x = width / 2 - boundsCenterX * clampedZoom;
147
+ const y = height / 2 - boundsCenterY * clampedZoom;
148
+ return { x, y, zoom: clampedZoom };
149
+ }
150
+ function clamp(value, min = 0, max = 1) {
151
+ return Math.min(Math.max(value, min), max);
152
+ }
153
+
154
+ class FlowSettingsService {
155
+ constructor() {
156
+ this.entitiesSelectable = signal(true);
157
+ /**
158
+ * Global setting with handle positions. Nodes derive this value
159
+ *
160
+ * @deprecated
161
+ */
162
+ this.handlePositions = signal({ source: 'right', target: 'left' });
163
+ /**
164
+ * @see {VflowComponent.view}
165
+ */
166
+ this.view = signal([400, 400]);
167
+ /**
168
+ * Set based on view property. May change if view is 'auto'
169
+ */
170
+ this.computedFlowWidth = signal(0);
171
+ /**
172
+ * Set based on view property. May change if view is 'auto'
173
+ */
174
+ this.computedFlowHeight = signal(0);
175
+ this.minZoom = signal(0.5);
176
+ this.maxZoom = signal(3);
177
+ }
178
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowSettingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
179
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowSettingsService }); }
180
+ }
181
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowSettingsService, decorators: [{
182
+ type: Injectable
183
+ }] });
184
+
185
+ class ViewportService {
186
+ constructor() {
187
+ this.entitiesService = inject(FlowEntitiesService);
188
+ this.flowSettingsService = inject(FlowSettingsService);
189
+ /**
190
+ * Internal signal that accepts value from user by lib api
191
+ * When this signal changes, lib sets new view state and update readableViewport signal
192
+ */
193
+ this.writableViewport = signal({
194
+ changeType: 'initial',
195
+ state: ViewportService.getDefaultViewport(),
196
+ duration: 0
197
+ });
198
+ /**
199
+ * Public signal with viewport state. User can directly read from this signal. It's updated by:
200
+ * - user events on flow
201
+ * - writableViewport signal
202
+ */
203
+ this.readableViewport = signal(ViewportService.getDefaultViewport());
204
+ }
205
+ /**
206
+ * The default value used by d3, just copy it here
207
+ *
208
+ * @returns default viewport value
209
+ */
210
+ static getDefaultViewport() { return { zoom: 1, x: 0, y: 0 }; }
211
+ // TODO: add writableViewportWithConstraints (to apply min zoom/max zoom values)
212
+ fitView(options = { padding: .1, duration: 0, nodes: [] }) {
213
+ const nodes = this.getBoundsNodes(options.nodes ?? []);
214
+ const state = getViewportForBounds(getNodesBounds(nodes), this.flowSettingsService.computedFlowWidth(), this.flowSettingsService.computedFlowHeight(), this.flowSettingsService.minZoom(), this.flowSettingsService.maxZoom(), options.padding ?? .1);
215
+ const duration = options.duration ?? 0;
216
+ this.writableViewport.set({ changeType: 'absolute', state, duration });
217
+ }
218
+ getBoundsNodes(nodeIds) {
219
+ return !nodeIds?.length
220
+ // If nodes option not passed or the list is empty, then get fit the whole view
221
+ ? this.entitiesService.nodes()
222
+ // Otherwise fit to specific nodes
223
+ : nodeIds
224
+ .map(nodeId => this.entitiesService.nodes().find(({ node }) => node.id === nodeId))
225
+ .filter((node) => !!node);
226
+ }
227
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ViewportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
228
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ViewportService }); }
229
+ }
230
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ViewportService, decorators: [{
231
+ type: Injectable
232
+ }] });
233
+
234
+ function isDefined(data) {
235
+ return data !== undefined;
236
+ }
237
+
238
+ class RootSvgReferenceDirective {
239
+ constructor() {
240
+ this.element = inject(ElementRef).nativeElement;
241
+ }
242
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootSvgReferenceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
243
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: RootSvgReferenceDirective, selector: "svg[rootSvgRef]", ngImport: i0 }); }
244
+ }
245
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootSvgReferenceDirective, decorators: [{
246
+ type: Directive,
247
+ args: [{
248
+ selector: 'svg[rootSvgRef]'
249
+ }]
250
+ }] });
251
+
129
252
  class SelectionService {
130
253
  constructor() {
131
254
  this.flowEntitiesService = inject(FlowEntitiesService);
@@ -172,6 +295,7 @@ class MapContextDirective {
172
295
  this.host = inject(ElementRef).nativeElement;
173
296
  this.selectionService = inject(SelectionService);
174
297
  this.viewportService = inject(ViewportService);
298
+ this.flowSettingsService = inject(FlowSettingsService);
175
299
  this.rootSvgSelection = select(this.rootSvg);
176
300
  this.zoomableSelection = select(this.host);
177
301
  this.viewportForSelection = {};
@@ -184,19 +308,25 @@ class MapContextDirective {
184
308
  }
185
309
  // If only zoom provided
186
310
  if (isDefined(state.zoom) && (!isDefined(state.x) && !isDefined(state.y))) {
187
- this.rootSvgSelection.call(this.zoomBehavior.scaleTo, state.zoom);
311
+ this.rootSvgSelection
312
+ .transition().duration(viewport.duration)
313
+ .call(this.zoomBehavior.scaleTo, state.zoom);
188
314
  return;
189
315
  }
190
316
  // If only pan provided
191
317
  if ((isDefined(state.x) && isDefined(state.y)) && !isDefined(state.zoom)) {
192
318
  // remain same zoom value
193
319
  const zoom = untracked(this.viewportService.readableViewport).zoom;
194
- this.rootSvgSelection.call(this.zoomBehavior.transform, zoomIdentity.translate(state.x, state.y).scale(zoom));
320
+ this.rootSvgSelection
321
+ .transition().duration(viewport.duration)
322
+ .call(this.zoomBehavior.transform, zoomIdentity.translate(state.x, state.y).scale(zoom));
195
323
  return;
196
324
  }
197
325
  // If whole viewort state provided
198
326
  if (isDefined(state.x) && isDefined(state.y) && isDefined(state.zoom)) {
199
- this.rootSvgSelection.call(this.zoomBehavior.transform, zoomIdentity.translate(state.x, state.y).scale(state.zoom));
327
+ this.rootSvgSelection
328
+ .transition().duration(viewport.duration)
329
+ .call(this.zoomBehavior.transform, zoomIdentity.translate(state.x, state.y).scale(state.zoom));
200
330
  return;
201
331
  }
202
332
  }, { allowSignalWrites: true });
@@ -208,7 +338,7 @@ class MapContextDirective {
208
338
  }
209
339
  ngOnInit() {
210
340
  this.zoomBehavior = zoom()
211
- .scaleExtent([this.minZoom, this.maxZoom])
341
+ .scaleExtent([this.flowSettingsService.minZoom(), this.flowSettingsService.maxZoom()])
212
342
  .on('start', (event) => this.onD3zoomStart(event))
213
343
  .on('zoom', (event) => this.handleZoom(event))
214
344
  .on('end', (event) => this.onD3zoomEnd(event));
@@ -230,16 +360,12 @@ class MapContextDirective {
230
360
  this.selectionService.setViewport(this.viewportForSelection);
231
361
  }
232
362
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MapContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
233
- 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 }); }
363
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: MapContextDirective, selector: "g[mapContext]", ngImport: i0 }); }
234
364
  }
235
365
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MapContextDirective, decorators: [{
236
366
  type: Directive,
237
367
  args: [{ selector: 'g[mapContext]' }]
238
- }], propDecorators: { minZoom: [{
239
- type: Input
240
- }], maxZoom: [{
241
- type: Input
242
- }] } });
368
+ }] });
243
369
  const mapTransformToViewportState = (transform) => ({ zoom: transform.k, x: transform.x, y: transform.y });
244
370
  const evTarget = (anyEvent) => {
245
371
  if (anyEvent instanceof Event && anyEvent.target instanceof Element) {
@@ -385,14 +511,14 @@ class FlowStatusService {
385
511
  setIdleStatus() {
386
512
  this.status.set({ state: 'idle', payload: null });
387
513
  }
388
- setConnectionStartStatus(sourceNode, sourceHandle) {
389
- this.status.set({ state: 'connection-start', payload: { sourceNode, sourceHandle } });
514
+ setConnectionStartStatus(source, sourceHandle) {
515
+ this.status.set({ state: 'connection-start', payload: { source, sourceHandle } });
390
516
  }
391
- setConnectionValidationStatus(valid, sourceNode, targetNode, sourceHandle, targetHandle) {
392
- this.status.set({ state: 'connection-validation', payload: { sourceNode, targetNode, sourceHandle, targetHandle, valid } });
517
+ setConnectionValidationStatus(valid, source, target, sourceHandle, targetHandle) {
518
+ this.status.set({ state: 'connection-validation', payload: { source, target, sourceHandle, targetHandle, valid } });
393
519
  }
394
- setConnectionEndStatus(sourceNode, targetNode, sourceHandle, targetHandle) {
395
- this.status.set({ state: 'connection-end', payload: { sourceNode, targetNode, sourceHandle, targetHandle } });
520
+ setConnectionEndStatus(source, target, sourceHandle, targetHandle) {
521
+ this.status.set({ state: 'connection-end', payload: { source, target, sourceHandle, targetHandle } });
396
522
  }
397
523
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowStatusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
398
524
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowStatusService }); }
@@ -415,6 +541,36 @@ function batchStatusChanges(...changes) {
415
541
  }
416
542
  }
417
543
 
544
+ /**
545
+ * This function contains a hack-y behavior.
546
+ * If the handles are of the same type (source-source or target-target),
547
+ * it returns nodes where source === target and
548
+ * handles where sourceHandle === targetHandle
549
+ *
550
+ * This leads to that notSelfValidator returns false for these cases,
551
+ * exactly what we need for strict connection type
552
+ */
553
+ function adjustDirection(connection) {
554
+ const result = {};
555
+ if (connection.sourceHandle.rawHandle.type === 'source') {
556
+ result.source = connection.source;
557
+ result.sourceHandle = connection.sourceHandle;
558
+ }
559
+ else {
560
+ result.source = connection.target;
561
+ result.sourceHandle = connection.targetHandle;
562
+ }
563
+ if (connection.targetHandle.rawHandle.type === 'target') {
564
+ result.target = connection.target;
565
+ result.targetHandle = connection.targetHandle;
566
+ }
567
+ else {
568
+ result.target = connection.source;
569
+ result.targetHandle = connection.sourceHandle;
570
+ }
571
+ return result;
572
+ }
573
+
418
574
  class ConnectionControllerDirective {
419
575
  constructor() {
420
576
  /**
@@ -431,18 +587,96 @@ class ConnectionControllerDirective {
431
587
  this.connectEffect = effect(() => {
432
588
  const status = this.statusService.status();
433
589
  if (status.state === 'connection-end') {
434
- const sourceModel = status.payload.sourceNode;
435
- const targetModel = status.payload.targetNode;
436
- const source = sourceModel.node.id;
437
- const target = targetModel.node.id;
438
- const sourceHandle = status.payload.sourceHandle.rawHandle.id;
439
- const targetHandle = status.payload.targetHandle.rawHandle.id;
440
- const connection = this.flowEntitiesService.connection();
441
- if (connection.validator({ source, target, sourceHandle, targetHandle })) {
442
- this.onConnect.emit({ source, target, sourceHandle, targetHandle });
590
+ let source = status.payload.source;
591
+ let target = status.payload.target;
592
+ let sourceHandle = status.payload.sourceHandle;
593
+ let targetHandle = status.payload.targetHandle;
594
+ if (this.isStrictMode()) {
595
+ const adjusted = adjustDirection({
596
+ source: status.payload.source,
597
+ sourceHandle: status.payload.sourceHandle,
598
+ target: status.payload.target,
599
+ targetHandle: status.payload.targetHandle
600
+ });
601
+ source = adjusted.source;
602
+ target = adjusted.target;
603
+ sourceHandle = adjusted.sourceHandle;
604
+ targetHandle = adjusted.targetHandle;
605
+ }
606
+ const sourceId = source.node.id;
607
+ const targetId = target.node.id;
608
+ const sourceHandleId = sourceHandle.rawHandle.id;
609
+ const targetHandleId = targetHandle.rawHandle.id;
610
+ const connectionModel = this.flowEntitiesService.connection();
611
+ const connection = {
612
+ source: sourceId, target: targetId,
613
+ sourceHandle: sourceHandleId, targetHandle: targetHandleId
614
+ };
615
+ if (connectionModel.validator(connection)) {
616
+ this.onConnect.emit(connection);
443
617
  }
444
618
  }
445
619
  }, { allowSignalWrites: true });
620
+ this.isStrictMode = computed(() => this.flowEntitiesService.connection().mode === 'strict');
621
+ }
622
+ startConnection(handle) {
623
+ this.statusService.setConnectionStartStatus(handle.parentNode, handle);
624
+ }
625
+ validateConnection(handle) {
626
+ const status = this.statusService.status();
627
+ if (status.state === 'connection-start') {
628
+ let source = status.payload.source;
629
+ let target = handle.parentNode;
630
+ let sourceHandle = status.payload.sourceHandle;
631
+ let targetHandle = handle;
632
+ if (this.isStrictMode()) {
633
+ // swap direction (if needed) according to actual source and target of strict mode
634
+ const adjusted = adjustDirection({
635
+ source: status.payload.source,
636
+ sourceHandle: status.payload.sourceHandle,
637
+ target: handle.parentNode,
638
+ targetHandle: handle
639
+ });
640
+ source = adjusted.source;
641
+ target = adjusted.target;
642
+ sourceHandle = adjusted.sourceHandle;
643
+ targetHandle = adjusted.targetHandle;
644
+ }
645
+ const valid = this.flowEntitiesService.connection().validator({
646
+ source: source.node.id,
647
+ target: target.node.id,
648
+ sourceHandle: sourceHandle.rawHandle.id,
649
+ targetHandle: targetHandle.rawHandle.id
650
+ });
651
+ // TODO: check how react flow handles highlight of handle
652
+ // if direction changes
653
+ handle.state.set(valid ? 'valid' : 'invalid');
654
+ // status is about how we draw connection, so we don't need
655
+ // swapped diretion here
656
+ this.statusService.setConnectionValidationStatus(valid, status.payload.source, handle.parentNode, status.payload.sourceHandle, handle);
657
+ }
658
+ }
659
+ resetValidateConnection(targetHandle) {
660
+ targetHandle.state.set('idle');
661
+ // drop back to start status
662
+ const status = this.statusService.status();
663
+ if (status.state === 'connection-validation') {
664
+ this.statusService.setConnectionStartStatus(status.payload.source, status.payload.sourceHandle);
665
+ }
666
+ }
667
+ endConnection(handle) {
668
+ const status = this.statusService.status();
669
+ if (status.state === 'connection-validation') {
670
+ const source = status.payload.source;
671
+ const sourceHandle = status.payload.sourceHandle;
672
+ const target = status.payload.target;
673
+ const targetHandle = status.payload.targetHandle;
674
+ batchStatusChanges(
675
+ // call to create connection
676
+ () => this.statusService.setConnectionEndStatus(source, target, sourceHandle, targetHandle),
677
+ // when connection created, we need go back to idle status
678
+ () => this.statusService.setIdleStatus());
679
+ }
446
680
  }
447
681
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ConnectionControllerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
448
682
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ConnectionControllerDirective, isStandalone: true, selector: "[connectionController]", outputs: { onConnect: "onConnect" }, ngImport: i0 }); }
@@ -457,29 +691,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
457
691
  type: Output
458
692
  }] } });
459
693
 
460
- class FlowSettingsService {
461
- constructor() {
462
- this.entitiesSelectable = signal(true);
463
- /**
464
- * Global setting with handle positions. Nodes derive this value
465
- *
466
- * @deprecated
467
- */
468
- this.handlePositions = signal({ source: 'right', target: 'left' });
469
- /**
470
- * @see {VflowComponent.view}
471
- */
472
- this.view = signal([400, 400]);
473
- this.flowWidth = computed(() => this.view() === 'auto' ? '100%' : this.view()[0]);
474
- this.flowHeight = computed(() => this.view() === 'auto' ? '100%' : this.view()[1]);
475
- }
476
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowSettingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
477
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowSettingsService }); }
478
- }
479
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowSettingsService, decorators: [{
480
- type: Injectable
481
- }] });
482
-
483
694
  class ComponentEventBusService {
484
695
  constructor() {
485
696
  this._event$ = new Subject();
@@ -832,7 +1043,7 @@ class NodesChangeService {
832
1043
  this.changes$ = merge(this.nodesPositionChange$, this.nodeAddChange$, this.nodeRemoveChange$, this.nodeSelectedChange$).pipe(
833
1044
  // this fixes a bug when on fire node event change,
834
1045
  // you can't get valid list of detached edges
835
- observeOn(asyncScheduler));
1046
+ observeOn(animationFrameScheduler));
836
1047
  }
837
1048
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodesChangeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
838
1049
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodesChangeService }); }
@@ -1040,34 +1251,145 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1040
1251
  type: Injectable
1041
1252
  }] });
1042
1253
 
1043
- class HandleService {
1254
+ class RootPointerDirective {
1044
1255
  constructor() {
1045
- this.node = signal(null);
1046
- }
1047
- createHandle(newHandle) {
1048
- const node = this.node();
1049
- if (node) {
1050
- node.handles.update(handles => [...handles, newHandle]);
1051
- }
1256
+ this.host = inject(ElementRef).nativeElement;
1257
+ this.initialTouch$ = new Subject();
1258
+ // TODO: do not emit if mouse not down
1259
+ this.mouseMovement$ = fromEvent(this.host, 'mousemove').pipe(map(event => ({
1260
+ x: event.clientX,
1261
+ y: event.clientY,
1262
+ originalEvent: event
1263
+ })), observeOn(animationFrameScheduler), share());
1264
+ this.touchMovement$ = merge(this.initialTouch$, fromEvent(this.host, 'touchmove')).pipe(tap((event) => event.preventDefault()), map((originalEvent) => {
1265
+ const x = originalEvent.touches[0]?.clientX ?? 0;
1266
+ const y = originalEvent.touches[0]?.clientY ?? 0;
1267
+ const target = document.elementFromPoint(x, y);
1268
+ return { x, y, target, originalEvent };
1269
+ }), observeOn(animationFrameScheduler), share());
1270
+ this.touchEnd$ = fromEvent(this.host, 'touchend').pipe(map((originalEvent) => {
1271
+ const x = originalEvent.changedTouches[0]?.clientX ?? 0;
1272
+ const y = originalEvent.changedTouches[0]?.clientY ?? 0;
1273
+ const target = document.elementFromPoint(x, y);
1274
+ return { x, y, target, originalEvent };
1275
+ }), share());
1276
+ this.pointerMovement$ = merge(this.mouseMovement$, this.touchMovement$);
1052
1277
  }
1053
- destroyHandle(handleToDestoy) {
1054
- const node = this.node();
1055
- if (node) {
1056
- node.handles.update(handles => handles.filter(handle => handle !== handleToDestoy));
1057
- }
1278
+ /**
1279
+ * We should know when user started a touch in order to not
1280
+ * show old touch position when connection creation is started
1281
+ */
1282
+ setInitialTouch(event) {
1283
+ this.initialTouch$.next(event);
1058
1284
  }
1059
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1060
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService }); }
1285
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootPointerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1286
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: RootPointerDirective, selector: "svg[rootPointer]", ngImport: i0 }); }
1061
1287
  }
1062
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService, decorators: [{
1063
- type: Injectable
1288
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootPointerDirective, decorators: [{
1289
+ type: Directive,
1290
+ args: [{ selector: 'svg[rootPointer]' }]
1064
1291
  }] });
1065
1292
 
1066
- class HandleModel {
1067
- constructor(rawHandle, parentNode) {
1068
- this.rawHandle = rawHandle;
1069
- this.parentNode = parentNode;
1070
- this.strokeWidth = 2;
1293
+ class SpacePointContextDirective {
1294
+ constructor() {
1295
+ this.pointerMovementDirective = inject(RootPointerDirective);
1296
+ this.rootSvg = inject(RootSvgReferenceDirective).element;
1297
+ this.host = inject(ElementRef).nativeElement;
1298
+ /**
1299
+ * Signal with current mouse position in svg space
1300
+ */
1301
+ this.svgCurrentSpacePoint = computed(() => {
1302
+ const movement = this.pointerMovement();
1303
+ if (!movement) {
1304
+ return { x: 0, y: 0 };
1305
+ }
1306
+ return this.documentPointToFlowPoint({
1307
+ x: movement.x,
1308
+ y: movement.y
1309
+ });
1310
+ });
1311
+ this.pointerMovement = toSignal(this.pointerMovementDirective.pointerMovement$);
1312
+ }
1313
+ documentPointToFlowPoint(documentPoint) {
1314
+ const point = this.rootSvg.createSVGPoint();
1315
+ point.x = documentPoint.x;
1316
+ point.y = documentPoint.y;
1317
+ return point.matrixTransform(this.host.getScreenCTM().inverse());
1318
+ }
1319
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpacePointContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1320
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: SpacePointContextDirective, selector: "g[spacePointContext]", ngImport: i0 }); }
1321
+ }
1322
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpacePointContextDirective, decorators: [{
1323
+ type: Directive,
1324
+ args: [{ selector: 'g[spacePointContext]' }]
1325
+ }] });
1326
+
1327
+ class HandleService {
1328
+ constructor() {
1329
+ this.node = signal(null);
1330
+ }
1331
+ createHandle(newHandle) {
1332
+ const node = this.node();
1333
+ if (node) {
1334
+ node.handles.update(handles => [...handles, newHandle]);
1335
+ }
1336
+ }
1337
+ destroyHandle(handleToDestoy) {
1338
+ const node = this.node();
1339
+ if (node) {
1340
+ node.handles.update(handles => handles.filter(handle => handle !== handleToDestoy));
1341
+ }
1342
+ }
1343
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1344
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService }); }
1345
+ }
1346
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService, decorators: [{
1347
+ type: Injectable
1348
+ }] });
1349
+
1350
+ function resizable(elems) {
1351
+ return new Observable((subscriber) => {
1352
+ let ro = new ResizeObserver((entries) => {
1353
+ subscriber.next(entries);
1354
+ });
1355
+ elems.forEach(e => ro.observe(e));
1356
+ return () => ro.disconnect();
1357
+ });
1358
+ }
1359
+
1360
+ function InjectionContext(target, key, descriptor) {
1361
+ const originalMethod = descriptor.value;
1362
+ descriptor.value = function (...args) {
1363
+ if (implementsWithInjector(this)) {
1364
+ return runInInjectionContext(this.injector, () => originalMethod.apply(this, args));
1365
+ }
1366
+ else {
1367
+ throw new Error('Class that contains decorated method must extends WithInjectorDirective class');
1368
+ }
1369
+ };
1370
+ // Return the modified descriptor
1371
+ return descriptor;
1372
+ }
1373
+ const implementsWithInjector = (instance) => {
1374
+ return 'injector' in instance && 'get' in instance.injector;
1375
+ };
1376
+
1377
+ function Microtask(target, key, descriptor) {
1378
+ const originalMethod = descriptor.value;
1379
+ descriptor.value = function (...args) {
1380
+ queueMicrotask(() => {
1381
+ originalMethod?.apply(this, args);
1382
+ });
1383
+ };
1384
+ // Return the modified descriptor
1385
+ return descriptor;
1386
+ }
1387
+
1388
+ class HandleModel {
1389
+ constructor(rawHandle, parentNode) {
1390
+ this.rawHandle = rawHandle;
1391
+ this.parentNode = parentNode;
1392
+ this.strokeWidth = 2;
1071
1393
  /**
1072
1394
  * Pre-computed size for default handle, changed dynamically
1073
1395
  * for custom handles
@@ -1138,43 +1460,46 @@ class HandleModel {
1138
1460
  }
1139
1461
  }
1140
1462
 
1141
- function resizable(elems) {
1142
- return new Observable((subscriber) => {
1143
- let ro = new ResizeObserver((entries) => {
1144
- subscriber.next(entries);
1145
- });
1146
- elems.forEach(e => ro.observe(e));
1147
- return () => ro.disconnect();
1148
- });
1149
- }
1150
-
1151
- function InjectionContext(target, key, descriptor) {
1152
- const originalMethod = descriptor.value;
1153
- descriptor.value = function (...args) {
1154
- if (implementsWithInjector(this)) {
1155
- return runInInjectionContext(this.injector, () => originalMethod.apply(this, args));
1156
- }
1157
- else {
1158
- throw new Error('Class that contains decorated method must extends WithInjectorDirective class');
1159
- }
1160
- };
1161
- // Return the modified descriptor
1162
- return descriptor;
1163
- }
1164
- const implementsWithInjector = (instance) => {
1165
- return 'injector' in instance && 'get' in instance.injector;
1166
- };
1167
-
1168
- function Microtask(target, key, descriptor) {
1169
- const originalMethod = descriptor.value;
1170
- descriptor.value = function (...args) {
1171
- queueMicrotask(() => {
1172
- originalMethod?.apply(this, args);
1173
- });
1174
- };
1175
- // Return the modified descriptor
1176
- return descriptor;
1463
+ class HandleComponent {
1464
+ constructor() {
1465
+ this.injector = inject(Injector);
1466
+ this.handleService = inject(HandleService);
1467
+ this.element = inject(ElementRef).nativeElement;
1468
+ }
1469
+ ngOnInit() {
1470
+ this.model = new HandleModel({
1471
+ position: this.position,
1472
+ type: this.type,
1473
+ id: this.id,
1474
+ parentReference: this.element.parentElement,
1475
+ template: this.template
1476
+ }, this.handleService.node());
1477
+ this.handleService.createHandle(this.model);
1478
+ requestAnimationFrame(() => this.model.updateParent());
1479
+ }
1480
+ ngOnDestroy() {
1481
+ this.handleService.destroyHandle(this.model);
1482
+ }
1483
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1484
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: HandleComponent, selector: "handle", inputs: { position: "position", type: "type", id: "id", template: "template" }, ngImport: i0, template: "", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1177
1485
  }
1486
+ __decorate([
1487
+ InjectionContext
1488
+ ], HandleComponent.prototype, "ngOnInit", null);
1489
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, decorators: [{
1490
+ type: Component,
1491
+ args: [{ selector: 'handle', changeDetection: ChangeDetectionStrategy.OnPush, template: "" }]
1492
+ }], propDecorators: { position: [{
1493
+ type: Input,
1494
+ args: [{ required: true }]
1495
+ }], type: [{
1496
+ type: Input,
1497
+ args: [{ required: true }]
1498
+ }], id: [{
1499
+ type: Input
1500
+ }], template: [{
1501
+ type: Input
1502
+ }], ngOnInit: [] } });
1178
1503
 
1179
1504
  class HandleSizeControllerDirective {
1180
1505
  constructor() {
@@ -1212,45 +1537,6 @@ function getChildStrokeWidth(element) {
1212
1537
  return 0;
1213
1538
  }
1214
1539
 
1215
- class RootPointerDirective {
1216
- constructor() {
1217
- this.host = inject(ElementRef).nativeElement;
1218
- this.initialTouch$ = new Subject();
1219
- // TODO: do not emit if mouse not down
1220
- this.mouseMovement$ = fromEvent(this.host, 'mousemove').pipe(map(event => ({
1221
- x: event.clientX,
1222
- y: event.clientY,
1223
- originalEvent: event
1224
- })), observeOn(animationFrameScheduler), share());
1225
- this.touchMovement$ = merge(this.initialTouch$, fromEvent(this.host, 'touchmove')).pipe(tap((event) => event.preventDefault()), map((originalEvent) => {
1226
- const x = originalEvent.touches[0]?.clientX ?? 0;
1227
- const y = originalEvent.touches[0]?.clientY ?? 0;
1228
- const target = document.elementFromPoint(x, y);
1229
- return { x, y, target, originalEvent };
1230
- }), observeOn(animationFrameScheduler), share());
1231
- this.touchEnd$ = fromEvent(this.host, 'touchend').pipe(map((originalEvent) => {
1232
- const x = originalEvent.changedTouches[0]?.clientX ?? 0;
1233
- const y = originalEvent.changedTouches[0]?.clientY ?? 0;
1234
- const target = document.elementFromPoint(x, y);
1235
- return { x, y, target, originalEvent };
1236
- }), share());
1237
- this.pointerMovement$ = merge(this.mouseMovement$, this.touchMovement$);
1238
- }
1239
- /**
1240
- * We should know when user started a touch in order to not
1241
- * show old touch position when connection creation is started
1242
- */
1243
- setInitialTouch(event) {
1244
- this.initialTouch$.next(event);
1245
- }
1246
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootPointerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1247
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: RootPointerDirective, selector: "svg[rootPointer]", ngImport: i0 }); }
1248
- }
1249
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootPointerDirective, decorators: [{
1250
- type: Directive,
1251
- args: [{ selector: 'svg[rootPointer]' }]
1252
- }] });
1253
-
1254
1540
  class PointerDirective {
1255
1541
  constructor() {
1256
1542
  this.hostElement = inject(ElementRef).nativeElement;
@@ -1336,28 +1622,27 @@ class NodeComponent {
1336
1622
  this.handleService = inject(HandleService);
1337
1623
  this.draggableService = inject(DraggableService);
1338
1624
  this.flowStatusService = inject(FlowStatusService);
1339
- this.flowEntitiesService = inject(FlowEntitiesService);
1340
1625
  this.nodeRenderingService = inject(NodeRenderingService);
1341
1626
  this.flowSettingsService = inject(FlowSettingsService);
1342
1627
  this.selectionService = inject(SelectionService);
1343
1628
  this.hostRef = inject(ElementRef);
1629
+ this.connectionController = inject(ConnectionControllerDirective);
1344
1630
  this.showMagnet = computed(() => this.flowStatusService.status().state === 'connection-start' ||
1345
1631
  this.flowStatusService.status().state === 'connection-validation');
1346
1632
  this.styleWidth = computed(() => `${this.nodeModel.size().width}px`);
1347
1633
  this.styleHeight = computed(() => `${this.nodeModel.size().height}px`);
1348
- this.subscription = new Subscription();
1349
1634
  }
1350
1635
  ngOnInit() {
1351
1636
  this.handleService.node.set(this.nodeModel);
1352
1637
  this.draggableService.toggleDraggable(this.hostRef.nativeElement, this.nodeModel);
1353
- const sub = this.nodeModel.handles$
1354
- .pipe(switchMap((handles) => resizable(handles.map(h => h.parentReference))
1355
- .pipe(map(() => handles))), tap((handles) => handles.forEach(h => h.updateParent())))
1638
+ this.nodeModel.handles$
1639
+ .pipe(switchMap((handles) => resizable(handles.map(h => h.parentReference)).pipe(map(() => handles))), tap((handles) => {
1640
+ // TODO (performance) inspect how to avoid calls of this when flow initially rendered
1641
+ handles.forEach(h => h.updateParent());
1642
+ }), takeUntilDestroyed())
1356
1643
  .subscribe();
1357
- this.subscription.add(sub);
1358
1644
  }
1359
1645
  ngAfterViewInit() {
1360
- this.setInitialHandles();
1361
1646
  if (this.nodeModel.node.type === 'default') {
1362
1647
  this.nodeModel.size.set({
1363
1648
  width: this.nodeModel.node.width ?? NodeModel.defaultTypeSize.width,
@@ -1365,69 +1650,30 @@ class NodeComponent {
1365
1650
  });
1366
1651
  }
1367
1652
  if (this.nodeModel.node.type === 'html-template' || this.nodeModel.isComponentType) {
1368
- const sub = resizable([this.htmlWrapperRef.nativeElement])
1653
+ resizable([this.htmlWrapperRef.nativeElement])
1369
1654
  .pipe(startWith(null), tap(() => {
1370
1655
  const width = this.htmlWrapperRef.nativeElement.clientWidth;
1371
1656
  const height = this.htmlWrapperRef.nativeElement.clientHeight;
1372
1657
  this.nodeModel.size.set({ width, height });
1373
- })).subscribe();
1374
- this.subscription.add(sub);
1658
+ }), takeUntilDestroyed()).subscribe();
1375
1659
  }
1376
1660
  }
1377
1661
  ngOnDestroy() {
1378
1662
  this.draggableService.destroy(this.hostRef.nativeElement);
1379
- this.subscription.unsubscribe();
1380
1663
  }
1381
1664
  startConnection(event, handle) {
1382
1665
  // ignore drag by stopping propagation
1383
1666
  event.stopPropagation();
1384
- this.flowStatusService.setConnectionStartStatus(this.nodeModel, handle);
1667
+ this.connectionController.startConnection(handle);
1385
1668
  }
1386
- endConnection() {
1387
- const status = this.flowStatusService.status();
1388
- if (status.state === 'connection-validation') {
1389
- const sourceNode = status.payload.sourceNode;
1390
- const targetNode = this.nodeModel;
1391
- const sourceHandle = status.payload.sourceHandle;
1392
- const targetHandle = status.payload.targetHandle;
1393
- batchStatusChanges(
1394
- // call to create connection
1395
- () => this.flowStatusService.setConnectionEndStatus(sourceNode, targetNode, sourceHandle, targetHandle),
1396
- // when connection created, we need go back to idle status
1397
- () => this.flowStatusService.setIdleStatus());
1398
- }
1669
+ validateConnection(handle) {
1670
+ this.connectionController.validateConnection(handle);
1399
1671
  }
1400
- /**
1401
- * TODO srp
1402
- */
1403
- validateTargetHandle(targetHandle) {
1404
- const status = this.flowStatusService.status();
1405
- if (status.state === 'connection-start') {
1406
- const sourceNode = status.payload.sourceNode;
1407
- const sourceHandle = status.payload.sourceHandle;
1408
- const source = sourceNode.node.id;
1409
- const targetNode = this.nodeModel;
1410
- const target = targetNode.node.id;
1411
- const valid = this.flowEntitiesService.connection().validator({
1412
- source,
1413
- target,
1414
- sourceHandle: sourceHandle.rawHandle.id,
1415
- targetHandle: targetHandle.rawHandle.id
1416
- });
1417
- targetHandle.state.set(valid ? 'valid' : 'invalid');
1418
- this.flowStatusService.setConnectionValidationStatus(valid, sourceNode, targetNode, sourceHandle, targetHandle);
1419
- }
1672
+ resetValidateConnection(targetHandle) {
1673
+ this.connectionController.resetValidateConnection(targetHandle);
1420
1674
  }
1421
- /**
1422
- * TODO srp
1423
- */
1424
- resetValidateTargetHandle(targetHandle) {
1425
- targetHandle.state.set('idle');
1426
- // drop back to start status
1427
- const status = this.flowStatusService.status();
1428
- if (status.state === 'connection-validation') {
1429
- this.flowStatusService.setConnectionStartStatus(status.payload.sourceNode, status.payload.sourceHandle);
1430
- }
1675
+ endConnection(handle) {
1676
+ this.connectionController.endConnection(handle);
1431
1677
  }
1432
1678
  pullNode() {
1433
1679
  this.nodeRenderingService.pullNode(this.nodeModel);
@@ -1437,32 +1683,20 @@ class NodeComponent {
1437
1683
  this.selectionService.select(this.nodeModel);
1438
1684
  }
1439
1685
  }
1440
- setInitialHandles() {
1441
- if (this.nodeModel.node.type === 'default') {
1442
- this.handleService.createHandle(new HandleModel({
1443
- position: this.nodeModel.sourcePosition(),
1444
- type: 'source',
1445
- parentReference: this.htmlWrapperRef.nativeElement
1446
- }, this.nodeModel));
1447
- this.handleService.createHandle(new HandleModel({
1448
- position: this.nodeModel.targetPosition(),
1449
- type: 'target',
1450
- parentReference: this.htmlWrapperRef.nativeElement
1451
- }, this.nodeModel));
1452
- }
1453
- }
1454
1686
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1455
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: NodeComponent, selector: "g[node]", inputs: { nodeModel: "nodeModel", nodeHtmlTemplate: "nodeHtmlTemplate" }, providers: [HandleService], viewQueries: [{ propertyName: "nodeContentRef", first: true, predicate: ["nodeContent"], descendants: true }, { propertyName: "htmlWrapperRef", first: true, predicate: ["htmlWrapper"], descendants: true }], ngImport: i0, template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n class=\"selectable\"\n #nodeContent\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode(); selectNode()\"\n>\n <div\n #htmlWrapper\n class=\"default-node\"\n [class.default-node_selected]=\"nodeModel.selected()\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n [style.max-width]=\"styleWidth()\"\n [style.max-height]=\"styleHeight()\"\n [innerHTML]=\"nodeModel.node.text ?? ''\"\n ></div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode()\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngTemplateOutlet]=\"nodeHtmlTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.isComponentType\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode()\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngComponentOutlet]=\"$any(nodeModel.node.type)\"\n [ngComponentOutletInputs]=\"nodeModel.componentTypeInputs()\"\n [ngComponentOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<ng-container *ngFor=\"let handle of nodeModel.handles()\">\n <svg:circle\n *ngIf=\"!handle.template\"\n class=\"default-handle\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n (pointerStart)=\"handle.rawHandle.type === 'source' ? startConnection($event, handle) : null\"\n (pointerEnd)=\"handle.rawHandle.type === 'target' ? endConnection() : null\"\n />\n\n <svg:g\n *ngIf=\"handle.template\"\n [handleSizeController]=\"handle\"\n (pointerStart)=\"handle.rawHandle.type === 'source' ? startConnection($event, handle) : null\"\n (pointerEnd)=\"handle.rawHandle.type === 'target' ? endConnection() : null\"\n >\n <ng-container *ngTemplateOutlet=\"handle.template; context: handle.templateContext\" />\n </svg:g>\n\n <svg:circle\n *ngIf=\"handle.rawHandle.type === 'target' && showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n (pointerEnd)=\"endConnection(); resetValidateTargetHandle(handle)\"\n (pointerOver)=\"validateTargetHandle(handle)\"\n (pointerOut)=\"resetValidateTargetHandle(handle)\"\n />\n</ng-container>\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{border:1.5px solid #1b262c;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}.default-node_selected{border-width:2px}.default-handle{stroke:#fff;fill:#1b262c}\n"], dependencies: [{ kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: HandleSizeControllerDirective, selector: "[handleSizeController]", inputs: ["handleSizeController"] }, { kind: "directive", type: PointerDirective, selector: "[pointerStart], [pointerEnd], [pointerOver], [pointerOut]", outputs: ["pointerOver", "pointerOut", "pointerStart", "pointerEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1687
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: NodeComponent, selector: "g[node]", inputs: { nodeModel: "nodeModel", nodeHtmlTemplate: "nodeHtmlTemplate" }, providers: [HandleService], viewQueries: [{ propertyName: "nodeContentRef", first: true, predicate: ["nodeContent"], descendants: true }, { propertyName: "htmlWrapperRef", first: true, predicate: ["htmlWrapper"], descendants: true }], ngImport: i0, template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n class=\"selectable\"\n #nodeContent\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode(); selectNode()\"\n>\n <div\n #htmlWrapper\n class=\"default-node\"\n [class.default-node_selected]=\"nodeModel.selected()\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n [style.max-width]=\"styleWidth()\"\n [style.max-height]=\"styleHeight()\"\n >\n <div [outerHTML]=\"nodeModel.node.text ?? ''\"></div>\n\n <handle type=\"source\" [position]=\"nodeModel.sourcePosition()\" />\n <handle type=\"target\" [position]=\"nodeModel.targetPosition()\" />\n </div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode()\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngTemplateOutlet]=\"nodeHtmlTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.isComponentType\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode()\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngComponentOutlet]=\"$any(nodeModel.node.type)\"\n [ngComponentOutletInputs]=\"nodeModel.componentTypeInputs()\"\n [ngComponentOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<ng-container *ngFor=\"let handle of nodeModel.handles()\">\n <svg:circle\n *ngIf=\"!handle.template\"\n class=\"default-handle\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n />\n\n <svg:g\n *ngIf=\"handle.template\"\n [handleSizeController]=\"handle\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n >\n <ng-container *ngTemplateOutlet=\"handle.template; context: handle.templateContext\" />\n </svg:g>\n\n <svg:circle\n *ngIf=\"showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n (pointerEnd)=\"endConnection(handle); resetValidateConnection(handle)\"\n (pointerOver)=\"validateConnection(handle)\"\n (pointerOut)=\"resetValidateConnection(handle)\"\n />\n</ng-container>\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{border:1.5px solid #1b262c;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}.default-node_selected{border-width:2px}.default-handle{stroke:#fff;fill:#1b262c}\n"], dependencies: [{ kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { 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: HandleComponent, selector: "handle", inputs: ["position", "type", "id", "template"] }, { kind: "directive", type: HandleSizeControllerDirective, selector: "[handleSizeController]", inputs: ["handleSizeController"] }, { kind: "directive", type: PointerDirective, selector: "[pointerStart], [pointerEnd], [pointerOver], [pointerOut]", outputs: ["pointerOver", "pointerOut", "pointerStart", "pointerEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1456
1688
  }
1457
1689
  __decorate([
1458
- Microtask
1459
- ], NodeComponent.prototype, "ngAfterViewInit", null);
1690
+ InjectionContext
1691
+ ], NodeComponent.prototype, "ngOnInit", null);
1460
1692
  __decorate([
1693
+ Microtask // TODO (performance) check if we need microtask here
1694
+ ,
1461
1695
  InjectionContext
1462
- ], NodeComponent.prototype, "setInitialHandles", null);
1696
+ ], NodeComponent.prototype, "ngAfterViewInit", null);
1463
1697
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, decorators: [{
1464
1698
  type: Component,
1465
- args: [{ selector: 'g[node]', changeDetection: ChangeDetectionStrategy.OnPush, providers: [HandleService], template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n class=\"selectable\"\n #nodeContent\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode(); selectNode()\"\n>\n <div\n #htmlWrapper\n class=\"default-node\"\n [class.default-node_selected]=\"nodeModel.selected()\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n [style.max-width]=\"styleWidth()\"\n [style.max-height]=\"styleHeight()\"\n [innerHTML]=\"nodeModel.node.text ?? ''\"\n ></div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode()\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngTemplateOutlet]=\"nodeHtmlTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.isComponentType\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode()\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngComponentOutlet]=\"$any(nodeModel.node.type)\"\n [ngComponentOutletInputs]=\"nodeModel.componentTypeInputs()\"\n [ngComponentOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<ng-container *ngFor=\"let handle of nodeModel.handles()\">\n <svg:circle\n *ngIf=\"!handle.template\"\n class=\"default-handle\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n (pointerStart)=\"handle.rawHandle.type === 'source' ? startConnection($event, handle) : null\"\n (pointerEnd)=\"handle.rawHandle.type === 'target' ? endConnection() : null\"\n />\n\n <svg:g\n *ngIf=\"handle.template\"\n [handleSizeController]=\"handle\"\n (pointerStart)=\"handle.rawHandle.type === 'source' ? startConnection($event, handle) : null\"\n (pointerEnd)=\"handle.rawHandle.type === 'target' ? endConnection() : null\"\n >\n <ng-container *ngTemplateOutlet=\"handle.template; context: handle.templateContext\" />\n </svg:g>\n\n <svg:circle\n *ngIf=\"handle.rawHandle.type === 'target' && showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n (pointerEnd)=\"endConnection(); resetValidateTargetHandle(handle)\"\n (pointerOver)=\"validateTargetHandle(handle)\"\n (pointerOut)=\"resetValidateTargetHandle(handle)\"\n />\n</ng-container>\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{border:1.5px solid #1b262c;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}.default-node_selected{border-width:2px}.default-handle{stroke:#fff;fill:#1b262c}\n"] }]
1699
+ args: [{ selector: 'g[node]', changeDetection: ChangeDetectionStrategy.OnPush, providers: [HandleService], template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n class=\"selectable\"\n #nodeContent\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode(); selectNode()\"\n>\n <div\n #htmlWrapper\n class=\"default-node\"\n [class.default-node_selected]=\"nodeModel.selected()\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n [style.max-width]=\"styleWidth()\"\n [style.max-height]=\"styleHeight()\"\n >\n <div [outerHTML]=\"nodeModel.node.text ?? ''\"></div>\n\n <handle type=\"source\" [position]=\"nodeModel.sourcePosition()\" />\n <handle type=\"target\" [position]=\"nodeModel.targetPosition()\" />\n </div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode()\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngTemplateOutlet]=\"nodeHtmlTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.isComponentType\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (mousedown)=\"pullNode()\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngComponentOutlet]=\"$any(nodeModel.node.type)\"\n [ngComponentOutletInputs]=\"nodeModel.componentTypeInputs()\"\n [ngComponentOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<ng-container *ngFor=\"let handle of nodeModel.handles()\">\n <svg:circle\n *ngIf=\"!handle.template\"\n class=\"default-handle\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n />\n\n <svg:g\n *ngIf=\"handle.template\"\n [handleSizeController]=\"handle\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n >\n <ng-container *ngTemplateOutlet=\"handle.template; context: handle.templateContext\" />\n </svg:g>\n\n <svg:circle\n *ngIf=\"showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n (pointerEnd)=\"endConnection(handle); resetValidateConnection(handle)\"\n (pointerOver)=\"validateConnection(handle)\"\n (pointerOut)=\"resetValidateConnection(handle)\"\n />\n</ng-container>\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{border:1.5px solid #1b262c;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}.default-node_selected{border-width:2px}.default-handle{stroke:#fff;fill:#1b262c}\n"] }]
1466
1700
  }], propDecorators: { nodeModel: [{
1467
1701
  type: Input
1468
1702
  }], nodeHtmlTemplate: [{
@@ -1473,7 +1707,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1473
1707
  }], htmlWrapperRef: [{
1474
1708
  type: ViewChild,
1475
1709
  args: ['htmlWrapper']
1476
- }], ngAfterViewInit: [], setInitialHandles: [] } });
1710
+ }], ngOnInit: [], ngAfterViewInit: [] } });
1477
1711
 
1478
1712
  class EdgeLabelComponent {
1479
1713
  constructor() {
@@ -1578,34 +1812,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1578
1812
  type: Input
1579
1813
  }] } });
1580
1814
 
1581
- class SpacePointContextDirective {
1582
- constructor() {
1583
- this.pointerMovementDirective = inject(RootPointerDirective);
1584
- this.rootSvg = inject(RootSvgReferenceDirective).element;
1585
- this.host = inject(ElementRef).nativeElement;
1586
- /**
1587
- * Signal with current mouse position in svg space
1588
- */
1589
- this.svgCurrentSpacePoint = computed(() => {
1590
- const movement = this.pointerMovement();
1591
- if (!movement) {
1592
- return { x: 0, y: 0 };
1593
- }
1594
- const point = this.rootSvg.createSVGPoint();
1595
- point.x = movement.x;
1596
- point.y = movement.y;
1597
- return point.matrixTransform(this.host.getScreenCTM().inverse());
1598
- });
1599
- this.pointerMovement = toSignal(this.pointerMovementDirective.pointerMovement$);
1600
- }
1601
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpacePointContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1602
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: SpacePointContextDirective, selector: "g[spacePointContext]", ngImport: i0 }); }
1603
- }
1604
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpacePointContextDirective, decorators: [{
1605
- type: Directive,
1606
- args: [{ selector: 'g[spacePointContext]' }]
1607
- }] });
1608
-
1609
1815
  class ConnectionComponent {
1610
1816
  constructor() {
1611
1817
  this.flowStatusService = inject(FlowStatusService);
@@ -1643,7 +1849,7 @@ class ConnectionComponent {
1643
1849
  return null;
1644
1850
  });
1645
1851
  this.markerUrl = computed(() => {
1646
- const marker = this.model.connection.marker;
1852
+ const marker = this.model.settings.marker;
1647
1853
  if (marker) {
1648
1854
  return `url(#${hashCode(JSON.stringify(marker))})`;
1649
1855
  }
@@ -1784,11 +1990,11 @@ class BackgroundComponent {
1784
1990
  });
1785
1991
  }
1786
1992
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: BackgroundComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1787
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "16.2.12", type: BackgroundComponent, selector: "g[background]", inputs: { background: ["background", "background", transform] }, ngImport: i0, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n", dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); }
1993
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "16.2.12", type: BackgroundComponent, selector: "g[background]", inputs: { background: ["background", "background", transform] }, ngImport: i0, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n", dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1788
1994
  }
1789
1995
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: BackgroundComponent, decorators: [{
1790
1996
  type: Component,
1791
- args: [{ selector: 'g[background]', template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n" }]
1997
+ args: [{ selector: 'g[background]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n" }]
1792
1998
  }], ctorParameters: function () { return []; }, propDecorators: { background: [{
1793
1999
  type: Input,
1794
2000
  args: [{ required: true, transform }]
@@ -1825,6 +2031,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1825
2031
  args: ['document:touchend']
1826
2032
  }] } });
1827
2033
 
2034
+ class FlowSizeControllerDirective {
2035
+ constructor() {
2036
+ this.host = inject(ElementRef);
2037
+ this.flowSettingsService = inject(FlowSettingsService);
2038
+ this.flowWidth = 0;
2039
+ this.flowHeight = 0;
2040
+ effect(() => {
2041
+ const view = this.flowSettingsService.view();
2042
+ this.flowWidth = view === 'auto' ? '100%' : view[0];
2043
+ this.flowHeight = view === 'auto' ? '100%' : view[1];
2044
+ });
2045
+ resizable([this.host.nativeElement]).pipe(tap(([entry]) => {
2046
+ this.flowSettingsService.computedFlowWidth.set(entry.contentRect.width);
2047
+ this.flowSettingsService.computedFlowHeight.set(entry.contentRect.height);
2048
+ }), takeUntilDestroyed()).subscribe();
2049
+ }
2050
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowSizeControllerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2051
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: FlowSizeControllerDirective, selector: "svg[flowSizeController]", host: { properties: { "attr.width": "this.flowWidth", "attr.height": "this.flowHeight" } }, ngImport: i0 }); }
2052
+ }
2053
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowSizeControllerDirective, decorators: [{
2054
+ type: Directive,
2055
+ args: [{ selector: 'svg[flowSizeController]' }]
2056
+ }], ctorParameters: function () { return []; }, propDecorators: { flowWidth: [{
2057
+ type: HostBinding,
2058
+ args: ['attr.width']
2059
+ }], flowHeight: [{
2060
+ type: HostBinding,
2061
+ args: ['attr.height']
2062
+ }] } });
2063
+
1828
2064
  const connectionControllerHostDirective = {
1829
2065
  directive: ConnectionControllerDirective,
1830
2066
  outputs: ['onConnect']
@@ -1871,14 +2107,6 @@ class VflowComponent {
1871
2107
  this.flowSettingsService = inject(FlowSettingsService);
1872
2108
  this.componentEventBusService = inject(ComponentEventBusService);
1873
2109
  this.injector = inject(Injector);
1874
- /**
1875
- * Minimum zoom value
1876
- */
1877
- this.minZoom = 0.5;
1878
- /**
1879
- * Maximum zoom value
1880
- */
1881
- this.maxZoom = 3;
1882
2110
  /**
1883
2111
  * Background for flow
1884
2112
  */
@@ -1923,8 +2151,6 @@ class VflowComponent {
1923
2151
  */
1924
2152
  this.edgesChange$ = this.edgesChangeService.changes$;
1925
2153
  // #endregion
1926
- this.flowWidth = this.flowSettingsService.flowWidth;
1927
- this.flowHeight = this.flowSettingsService.flowHeight;
1928
2154
  this.markers = this.flowEntitiesService.markers;
1929
2155
  }
1930
2156
  // #endregion
@@ -1939,6 +2165,18 @@ class VflowComponent {
1939
2165
  set view(view) {
1940
2166
  this.flowSettingsService.view.set(view);
1941
2167
  }
2168
+ /**
2169
+ * Minimum zoom value
2170
+ */
2171
+ set minZoom(value) {
2172
+ this.flowSettingsService.minZoom.set(value);
2173
+ }
2174
+ /**
2175
+ * Maximum zoom value
2176
+ */
2177
+ set maxZoom(value) {
2178
+ this.flowSettingsService.maxZoom.set(value);
2179
+ }
1942
2180
  /**
1943
2181
  * Object that controls flow direction.
1944
2182
  *
@@ -1990,7 +2228,7 @@ class VflowComponent {
1990
2228
  * @param viewport viewport state
1991
2229
  */
1992
2230
  viewportTo(viewport) {
1993
- this.viewportService.writableViewport.set({ changeType: 'absolute', state: viewport });
2231
+ this.viewportService.writableViewport.set({ changeType: 'absolute', state: viewport, duration: 0 });
1994
2232
  }
1995
2233
  /**
1996
2234
  * Change zoom
@@ -1998,7 +2236,7 @@ class VflowComponent {
1998
2236
  * @param zoom zoom value
1999
2237
  */
2000
2238
  zoomTo(zoom) {
2001
- this.viewportService.writableViewport.set({ changeType: 'absolute', state: { zoom } });
2239
+ this.viewportService.writableViewport.set({ changeType: 'absolute', state: { zoom }, duration: 0 });
2002
2240
  }
2003
2241
  /**
2004
2242
  * Move to specified coordinate
@@ -2006,7 +2244,10 @@ class VflowComponent {
2006
2244
  * @param point point where to move
2007
2245
  */
2008
2246
  panTo(point) {
2009
- this.viewportService.writableViewport.set({ changeType: 'absolute', state: point });
2247
+ this.viewportService.writableViewport.set({ changeType: 'absolute', state: point, duration: 0 });
2248
+ }
2249
+ fitView(options) {
2250
+ this.viewportService.fitView(options);
2010
2251
  }
2011
2252
  /**
2012
2253
  * Get node by id
@@ -2022,6 +2263,12 @@ class VflowComponent {
2022
2263
  getDetachedEdges() {
2023
2264
  return this.flowEntitiesService.getDetachedEdges().map(e => e.edge);
2024
2265
  }
2266
+ /**
2267
+ * Convert point received from document to point on the flow
2268
+ */
2269
+ documentPointToFlowPoint(point) {
2270
+ return this.spacePointContext.documentPointToFlowPoint(point);
2271
+ }
2025
2272
  // #endregion
2026
2273
  trackNodes(idx, { node }) {
2027
2274
  return node;
@@ -2041,7 +2288,7 @@ class VflowComponent {
2041
2288
  SelectionService,
2042
2289
  FlowSettingsService,
2043
2290
  ComponentEventBusService
2044
- ], 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 }], viewQueries: [{ propertyName: "mapContext", first: true, predicate: MapContextDirective, descendants: true }], hostDirectives: [{ directive: ConnectionControllerDirective, outputs: ["onConnect", "onConnect"] }, { directive: ChangesControllerDirective, outputs: ["onNodesChange", "onNodesChange", "onNodesChange.position", "onNodesChange.position", "onNodesChange.position.single", "onNodesChange.position.single", "onNodesChange.position.many", "onNodesChange.position.many", "onNodesChange.add", "onNodesChange.add", "onNodesChange.add.single", "onNodesChange.add.single", "onNodesChange.add.many", "onNodesChange.add.many", "onNodesChange.remove", "onNodesChange.remove", "onNodesChange.remove.single", "onNodesChange.remove.single", "onNodesChange.remove.many", "onNodesChange.remove.many", "onNodesChange.select", "onNodesChange.select", "onNodesChange.select.single", "onNodesChange.select.single", "onNodesChange.select.many", "onNodesChange.select.many", "onEdgesChange", "onEdgesChange", "onEdgesChange.detached", "onEdgesChange.detached", "onEdgesChange.detached.single", "onEdgesChange.detached.single", "onEdgesChange.detached.many", "onEdgesChange.detached.many", "onEdgesChange.add", "onEdgesChange.add", "onEdgesChange.add.single", "onEdgesChange.add.single", "onEdgesChange.add.many", "onEdgesChange.add.many", "onEdgesChange.remove", "onEdgesChange.remove", "onEdgesChange.remove.single", "onEdgesChange.remove.single", "onEdgesChange.remove.many", "onEdgesChange.remove.many", "onEdgesChange.select", "onEdgesChange.select", "onEdgesChange.select.single", "onEdgesChange.select.single", "onEdgesChange.select.many", "onEdgesChange.select.many"] }], ngImport: i0, template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n class=\"root-svg\"\n #flow\n [attr.width]=\"flowWidth()\"\n [attr.height]=\"flowHeight()\"\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g [background]=\"background\"/>\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 [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}:host ::ng-deep *{box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["nodeModel", "nodeHtmlTemplate"] }, { 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: "component", type: BackgroundComponent, selector: "g[background]", inputs: ["background"] }, { 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]" }, { kind: "directive", type: RootPointerDirective, selector: "svg[rootPointer]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2291
+ ], 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 }], viewQueries: [{ propertyName: "mapContext", first: true, predicate: MapContextDirective, descendants: true }, { propertyName: "spacePointContext", first: true, predicate: SpacePointContextDirective, descendants: true }], hostDirectives: [{ directive: ConnectionControllerDirective, outputs: ["onConnect", "onConnect"] }, { directive: ChangesControllerDirective, outputs: ["onNodesChange", "onNodesChange", "onNodesChange.position", "onNodesChange.position", "onNodesChange.position.single", "onNodesChange.position.single", "onNodesChange.position.many", "onNodesChange.position.many", "onNodesChange.add", "onNodesChange.add", "onNodesChange.add.single", "onNodesChange.add.single", "onNodesChange.add.many", "onNodesChange.add.many", "onNodesChange.remove", "onNodesChange.remove", "onNodesChange.remove.single", "onNodesChange.remove.single", "onNodesChange.remove.many", "onNodesChange.remove.many", "onNodesChange.select", "onNodesChange.select", "onNodesChange.select.single", "onNodesChange.select.single", "onNodesChange.select.many", "onNodesChange.select.many", "onEdgesChange", "onEdgesChange", "onEdgesChange.detached", "onEdgesChange.detached", "onEdgesChange.detached.single", "onEdgesChange.detached.single", "onEdgesChange.detached.many", "onEdgesChange.detached.many", "onEdgesChange.add", "onEdgesChange.add", "onEdgesChange.add.single", "onEdgesChange.add.single", "onEdgesChange.add.many", "onEdgesChange.add.many", "onEdgesChange.remove", "onEdgesChange.remove", "onEdgesChange.remove.single", "onEdgesChange.remove.single", "onEdgesChange.remove.many", "onEdgesChange.remove.many", "onEdgesChange.select", "onEdgesChange.select", "onEdgesChange.select.single", "onEdgesChange.select.single", "onEdgesChange.select.many", "onEdgesChange.select.many"] }], ngImport: i0, template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n flowSizeController\n class=\"root-svg\"\n #flow\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g [background]=\"background\"/>\n\n <svg:g\n mapContext\n spacePointContext\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 [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}:host ::ng-deep *{box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["nodeModel", "nodeHtmlTemplate"] }, { 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: "component", type: BackgroundComponent, selector: "g[background]", inputs: ["background"] }, { kind: "directive", type: SpacePointContextDirective, selector: "g[spacePointContext]" }, { kind: "directive", type: MapContextDirective, selector: "g[mapContext]" }, { kind: "directive", type: RootSvgReferenceDirective, selector: "svg[rootSvgRef]" }, { kind: "directive", type: RootSvgContextDirective, selector: "svg[rootSvgContext]" }, { kind: "directive", type: RootPointerDirective, selector: "svg[rootPointer]" }, { kind: "directive", type: FlowSizeControllerDirective, selector: "svg[flowSizeController]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2045
2292
  }
2046
2293
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, decorators: [{
2047
2294
  type: Component,
@@ -2059,7 +2306,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2059
2306
  ], hostDirectives: [
2060
2307
  connectionControllerHostDirective,
2061
2308
  changesControllerHostDirective
2062
- ], template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n class=\"root-svg\"\n #flow\n [attr.width]=\"flowWidth()\"\n [attr.height]=\"flowHeight()\"\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g [background]=\"background\"/>\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 [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}:host ::ng-deep *{box-sizing:border-box}\n"] }]
2309
+ ], template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n flowSizeController\n class=\"root-svg\"\n #flow\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g [background]=\"background\"/>\n\n <svg:g\n mapContext\n spacePointContext\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 [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}:host ::ng-deep *{box-sizing:border-box}\n"] }]
2063
2310
  }], propDecorators: { view: [{
2064
2311
  type: Input
2065
2312
  }], minZoom: [{
@@ -2097,49 +2344,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2097
2344
  }], mapContext: [{
2098
2345
  type: ViewChild,
2099
2346
  args: [MapContextDirective]
2347
+ }], spacePointContext: [{
2348
+ type: ViewChild,
2349
+ args: [SpacePointContextDirective]
2100
2350
  }] } });
2101
2351
 
2102
- class HandleComponent {
2103
- constructor() {
2104
- this.injector = inject(Injector);
2105
- this.handleService = inject(HandleService);
2106
- this.element = inject(ElementRef).nativeElement;
2107
- }
2108
- ngOnInit() {
2109
- this.model = new HandleModel({
2110
- position: this.position,
2111
- type: this.type,
2112
- id: this.id,
2113
- parentReference: this.element.parentElement,
2114
- template: this.template
2115
- }, this.handleService.node());
2116
- this.handleService.createHandle(this.model);
2117
- queueMicrotask(() => this.model.updateParent());
2118
- }
2119
- ngOnDestroy() {
2120
- this.handleService.destroyHandle(this.model);
2121
- }
2122
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2123
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: HandleComponent, selector: "handle", inputs: { position: "position", type: "type", id: "id", template: "template" }, ngImport: i0, template: "" }); }
2124
- }
2125
- __decorate([
2126
- InjectionContext
2127
- ], HandleComponent.prototype, "ngOnInit", null);
2128
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, decorators: [{
2129
- type: Component,
2130
- args: [{ selector: 'handle', template: "" }]
2131
- }], propDecorators: { position: [{
2132
- type: Input,
2133
- args: [{ required: true }]
2134
- }], type: [{
2135
- type: Input,
2136
- args: [{ required: true }]
2137
- }], id: [{
2138
- type: Input
2139
- }], template: [{
2140
- type: Input
2141
- }], ngOnInit: [] } });
2142
-
2143
2352
  class SelectableDirective {
2144
2353
  constructor() {
2145
2354
  this.flowSettingsService = inject(FlowSettingsService);
@@ -2191,7 +2400,8 @@ const directives = [
2191
2400
  HandleSizeControllerDirective,
2192
2401
  SelectableDirective,
2193
2402
  PointerDirective,
2194
- RootPointerDirective
2403
+ RootPointerDirective,
2404
+ FlowSizeControllerDirective
2195
2405
  ];
2196
2406
  const templateDirectives = [
2197
2407
  NodeHtmlTemplateDirective,
@@ -2216,7 +2426,8 @@ class VflowModule {
2216
2426
  HandleSizeControllerDirective,
2217
2427
  SelectableDirective,
2218
2428
  PointerDirective,
2219
- RootPointerDirective, NodeHtmlTemplateDirective,
2429
+ RootPointerDirective,
2430
+ FlowSizeControllerDirective, NodeHtmlTemplateDirective,
2220
2431
  EdgeLabelHtmlTemplateDirective,
2221
2432
  EdgeTemplateDirective,
2222
2433
  ConnectionTemplateDirective,