ngx-vflow 0.6.0 → 0.8.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 (42) hide show
  1. package/esm2022/lib/vflow/components/background/background.component.mjs +66 -0
  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 +32 -84
  5. package/esm2022/lib/vflow/components/vflow/vflow.component.mjs +21 -10
  6. package/esm2022/lib/vflow/directives/connection-controller.directive.mjs +91 -12
  7. package/esm2022/lib/vflow/directives/space-point-context.directive.mjs +11 -5
  8. package/esm2022/lib/vflow/interfaces/connection-settings.interface.mjs +1 -1
  9. package/esm2022/lib/vflow/interfaces/connection.internal.interface.mjs +2 -0
  10. package/esm2022/lib/vflow/models/connection.model.mjs +28 -6
  11. package/esm2022/lib/vflow/models/handle.model.mjs +1 -1
  12. package/esm2022/lib/vflow/services/flow-entities.service.mjs +2 -2
  13. package/esm2022/lib/vflow/services/flow-status.service.mjs +7 -7
  14. package/esm2022/lib/vflow/services/handle.service.mjs +1 -1
  15. package/esm2022/lib/vflow/types/background.type.mjs +2 -0
  16. package/esm2022/lib/vflow/types/connection-mode.type.mjs +2 -0
  17. package/esm2022/lib/vflow/utils/adjust-direction.mjs +30 -0
  18. package/esm2022/lib/vflow/utils/id.mjs +5 -0
  19. package/esm2022/lib/vflow/utils/resizable.mjs +3 -3
  20. package/esm2022/lib/vflow/vflow.module.mjs +6 -3
  21. package/esm2022/public-api.mjs +3 -1
  22. package/fesm2022/ngx-vflow.mjs +409 -250
  23. package/fesm2022/ngx-vflow.mjs.map +1 -1
  24. package/lib/vflow/components/background/background.component.d.ts +20 -0
  25. package/lib/vflow/components/node/node.component.d.ts +4 -13
  26. package/lib/vflow/components/vflow/vflow.component.d.ts +9 -2
  27. package/lib/vflow/directives/connection-controller.directive.d.ts +6 -0
  28. package/lib/vflow/directives/space-point-context.directive.d.ts +1 -0
  29. package/lib/vflow/interfaces/connection-settings.interface.d.ts +2 -0
  30. package/lib/vflow/interfaces/connection.internal.interface.d.ts +8 -0
  31. package/lib/vflow/models/connection.model.d.ts +5 -2
  32. package/lib/vflow/models/edge.model.d.ts +1 -17
  33. package/lib/vflow/models/handle.model.d.ts +1 -1
  34. package/lib/vflow/services/flow-status.service.d.ts +7 -18
  35. package/lib/vflow/types/background.type.d.ts +24 -0
  36. package/lib/vflow/types/connection-mode.type.d.ts +1 -0
  37. package/lib/vflow/utils/adjust-direction.d.ts +11 -0
  38. package/lib/vflow/utils/id.d.ts +1 -0
  39. package/lib/vflow/utils/resizable.d.ts +1 -2
  40. package/lib/vflow/vflow.module.d.ts +12 -11
  41. package/package.json +1 -1
  42. package/public-api.d.ts +2 -0
@@ -1,10 +1,10 @@
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, NgZone, Component, ChangeDetectionStrategy, ViewChild, ContentChild, NgModule } from '@angular/core';
4
+ import { signal, Injectable, inject, ElementRef, Directive, computed, effect, untracked, Input, TemplateRef, EventEmitter, Output, DestroyRef, runInInjectionContext, Injector, Component, ChangeDetectionStrategy, HostListener, ViewChild, 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, asyncScheduler, zip, 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';
@@ -59,13 +59,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
59
59
  }] });
60
60
 
61
61
  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);
62
+ constructor(settings) {
63
+ this.settings = settings;
64
+ this.curve = settings.curve ?? 'bezier';
65
+ this.type = settings.type ?? 'default';
66
+ this.mode = settings.mode ?? 'strict';
67
+ const validatorsToRun = this.getValidators(settings);
68
+ this.validator = (connection) => validatorsToRun.every((v) => v(connection));
69
+ }
70
+ getValidators(settings) {
71
+ const validators = [];
72
+ validators.push(notSelfValidator);
73
+ if (this.mode === 'loose') {
74
+ validators.push(hasSourceAndTargetHandleValidator);
75
+ }
76
+ if (settings.validator) {
77
+ validators.push(settings.validator);
78
+ }
79
+ return validators;
67
80
  }
68
81
  }
82
+ /**
83
+ * Internal validator that not allows self connections
84
+ */
85
+ const notSelfValidator = (connection) => {
86
+ return connection.source !== connection.target;
87
+ };
88
+ const hasSourceAndTargetHandleValidator = (connection) => {
89
+ return connection.sourceHandle !== undefined && connection.targetHandle !== undefined;
90
+ };
69
91
 
70
92
  function hashCode(str) {
71
93
  return str.split('').reduce((a, b) => {
@@ -97,7 +119,7 @@ class FlowEntitiesService {
97
119
  markersMap.set(hash, e.edge.markers.end);
98
120
  }
99
121
  });
100
- const connectionMarker = this.connection().connection.marker;
122
+ const connectionMarker = this.connection().settings.marker;
101
123
  if (connectionMarker) {
102
124
  const hash = hashCode(JSON.stringify(connectionMarker));
103
125
  markersMap.set(hash, connectionMarker);
@@ -385,14 +407,14 @@ class FlowStatusService {
385
407
  setIdleStatus() {
386
408
  this.status.set({ state: 'idle', payload: null });
387
409
  }
388
- setConnectionStartStatus(sourceNode, sourceHandle) {
389
- this.status.set({ state: 'connection-start', payload: { sourceNode, sourceHandle } });
410
+ setConnectionStartStatus(source, sourceHandle) {
411
+ this.status.set({ state: 'connection-start', payload: { source, sourceHandle } });
390
412
  }
391
- setConnectionValidationStatus(valid, sourceNode, targetNode, sourceHandle, targetHandle) {
392
- this.status.set({ state: 'connection-validation', payload: { sourceNode, targetNode, sourceHandle, targetHandle, valid } });
413
+ setConnectionValidationStatus(valid, source, target, sourceHandle, targetHandle) {
414
+ this.status.set({ state: 'connection-validation', payload: { source, target, sourceHandle, targetHandle, valid } });
393
415
  }
394
- setConnectionEndStatus(sourceNode, targetNode, sourceHandle, targetHandle) {
395
- this.status.set({ state: 'connection-end', payload: { sourceNode, targetNode, sourceHandle, targetHandle } });
416
+ setConnectionEndStatus(source, target, sourceHandle, targetHandle) {
417
+ this.status.set({ state: 'connection-end', payload: { source, target, sourceHandle, targetHandle } });
396
418
  }
397
419
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowStatusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
398
420
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowStatusService }); }
@@ -415,6 +437,36 @@ function batchStatusChanges(...changes) {
415
437
  }
416
438
  }
417
439
 
440
+ /**
441
+ * This function contains a hack-y behavior.
442
+ * If the handles are of the same type (source-source or target-target),
443
+ * it returns nodes where source === target and
444
+ * handles where sourceHandle === targetHandle
445
+ *
446
+ * This leads to that notSelfValidator returns false for these cases,
447
+ * exactly what we need for strict connection type
448
+ */
449
+ function adjustDirection(connection) {
450
+ const result = {};
451
+ if (connection.sourceHandle.rawHandle.type === 'source') {
452
+ result.source = connection.source;
453
+ result.sourceHandle = connection.sourceHandle;
454
+ }
455
+ else {
456
+ result.source = connection.target;
457
+ result.sourceHandle = connection.targetHandle;
458
+ }
459
+ if (connection.targetHandle.rawHandle.type === 'target') {
460
+ result.target = connection.target;
461
+ result.targetHandle = connection.targetHandle;
462
+ }
463
+ else {
464
+ result.target = connection.source;
465
+ result.targetHandle = connection.sourceHandle;
466
+ }
467
+ return result;
468
+ }
469
+
418
470
  class ConnectionControllerDirective {
419
471
  constructor() {
420
472
  /**
@@ -431,18 +483,96 @@ class ConnectionControllerDirective {
431
483
  this.connectEffect = effect(() => {
432
484
  const status = this.statusService.status();
433
485
  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 });
486
+ let source = status.payload.source;
487
+ let target = status.payload.target;
488
+ let sourceHandle = status.payload.sourceHandle;
489
+ let targetHandle = status.payload.targetHandle;
490
+ if (this.isStrictMode()) {
491
+ const adjusted = adjustDirection({
492
+ source: status.payload.source,
493
+ sourceHandle: status.payload.sourceHandle,
494
+ target: status.payload.target,
495
+ targetHandle: status.payload.targetHandle
496
+ });
497
+ source = adjusted.source;
498
+ target = adjusted.target;
499
+ sourceHandle = adjusted.sourceHandle;
500
+ targetHandle = adjusted.targetHandle;
501
+ }
502
+ const sourceId = source.node.id;
503
+ const targetId = target.node.id;
504
+ const sourceHandleId = sourceHandle.rawHandle.id;
505
+ const targetHandleId = targetHandle.rawHandle.id;
506
+ const connectionModel = this.flowEntitiesService.connection();
507
+ const connection = {
508
+ source: sourceId, target: targetId,
509
+ sourceHandle: sourceHandleId, targetHandle: targetHandleId
510
+ };
511
+ if (connectionModel.validator(connection)) {
512
+ this.onConnect.emit(connection);
443
513
  }
444
514
  }
445
515
  }, { allowSignalWrites: true });
516
+ this.isStrictMode = computed(() => this.flowEntitiesService.connection().mode === 'strict');
517
+ }
518
+ startConnection(handle) {
519
+ this.statusService.setConnectionStartStatus(handle.parentNode, handle);
520
+ }
521
+ validateConnection(handle) {
522
+ const status = this.statusService.status();
523
+ if (status.state === 'connection-start') {
524
+ let source = status.payload.source;
525
+ let target = handle.parentNode;
526
+ let sourceHandle = status.payload.sourceHandle;
527
+ let targetHandle = handle;
528
+ if (this.isStrictMode()) {
529
+ // swap direction (if needed) according to actual source and target of strict mode
530
+ const adjusted = adjustDirection({
531
+ source: status.payload.source,
532
+ sourceHandle: status.payload.sourceHandle,
533
+ target: handle.parentNode,
534
+ targetHandle: handle
535
+ });
536
+ source = adjusted.source;
537
+ target = adjusted.target;
538
+ sourceHandle = adjusted.sourceHandle;
539
+ targetHandle = adjusted.targetHandle;
540
+ }
541
+ const valid = this.flowEntitiesService.connection().validator({
542
+ source: source.node.id,
543
+ target: target.node.id,
544
+ sourceHandle: sourceHandle.rawHandle.id,
545
+ targetHandle: targetHandle.rawHandle.id
546
+ });
547
+ // TODO: check how react flow handles highlight of handle
548
+ // if direction changes
549
+ handle.state.set(valid ? 'valid' : 'invalid');
550
+ // status is about how we draw connection, so we don't need
551
+ // swapped diretion here
552
+ this.statusService.setConnectionValidationStatus(valid, status.payload.source, handle.parentNode, status.payload.sourceHandle, handle);
553
+ }
554
+ }
555
+ resetValidateConnection(targetHandle) {
556
+ targetHandle.state.set('idle');
557
+ // drop back to start status
558
+ const status = this.statusService.status();
559
+ if (status.state === 'connection-validation') {
560
+ this.statusService.setConnectionStartStatus(status.payload.source, status.payload.sourceHandle);
561
+ }
562
+ }
563
+ endConnection(handle) {
564
+ const status = this.statusService.status();
565
+ if (status.state === 'connection-validation') {
566
+ const source = status.payload.source;
567
+ const sourceHandle = status.payload.sourceHandle;
568
+ const target = status.payload.target;
569
+ const targetHandle = status.payload.targetHandle;
570
+ batchStatusChanges(
571
+ // call to create connection
572
+ () => this.statusService.setConnectionEndStatus(source, target, sourceHandle, targetHandle),
573
+ // when connection created, we need go back to idle status
574
+ () => this.statusService.setIdleStatus());
575
+ }
446
576
  }
447
577
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ConnectionControllerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
448
578
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ConnectionControllerDirective, isStandalone: true, selector: "[connectionController]", outputs: { onConnect: "onConnect" }, ngImport: i0 }); }
@@ -1040,6 +1170,79 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1040
1170
  type: Injectable
1041
1171
  }] });
1042
1172
 
1173
+ class RootPointerDirective {
1174
+ constructor() {
1175
+ this.host = inject(ElementRef).nativeElement;
1176
+ this.initialTouch$ = new Subject();
1177
+ // TODO: do not emit if mouse not down
1178
+ this.mouseMovement$ = fromEvent(this.host, 'mousemove').pipe(map(event => ({
1179
+ x: event.clientX,
1180
+ y: event.clientY,
1181
+ originalEvent: event
1182
+ })), observeOn(animationFrameScheduler), share());
1183
+ this.touchMovement$ = merge(this.initialTouch$, fromEvent(this.host, 'touchmove')).pipe(tap((event) => event.preventDefault()), map((originalEvent) => {
1184
+ const x = originalEvent.touches[0]?.clientX ?? 0;
1185
+ const y = originalEvent.touches[0]?.clientY ?? 0;
1186
+ const target = document.elementFromPoint(x, y);
1187
+ return { x, y, target, originalEvent };
1188
+ }), observeOn(animationFrameScheduler), share());
1189
+ this.touchEnd$ = fromEvent(this.host, 'touchend').pipe(map((originalEvent) => {
1190
+ const x = originalEvent.changedTouches[0]?.clientX ?? 0;
1191
+ const y = originalEvent.changedTouches[0]?.clientY ?? 0;
1192
+ const target = document.elementFromPoint(x, y);
1193
+ return { x, y, target, originalEvent };
1194
+ }), share());
1195
+ this.pointerMovement$ = merge(this.mouseMovement$, this.touchMovement$);
1196
+ }
1197
+ /**
1198
+ * We should know when user started a touch in order to not
1199
+ * show old touch position when connection creation is started
1200
+ */
1201
+ setInitialTouch(event) {
1202
+ this.initialTouch$.next(event);
1203
+ }
1204
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootPointerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1205
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: RootPointerDirective, selector: "svg[rootPointer]", ngImport: i0 }); }
1206
+ }
1207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RootPointerDirective, decorators: [{
1208
+ type: Directive,
1209
+ args: [{ selector: 'svg[rootPointer]' }]
1210
+ }] });
1211
+
1212
+ class SpacePointContextDirective {
1213
+ constructor() {
1214
+ this.pointerMovementDirective = inject(RootPointerDirective);
1215
+ this.rootSvg = inject(RootSvgReferenceDirective).element;
1216
+ this.host = inject(ElementRef).nativeElement;
1217
+ /**
1218
+ * Signal with current mouse position in svg space
1219
+ */
1220
+ this.svgCurrentSpacePoint = computed(() => {
1221
+ const movement = this.pointerMovement();
1222
+ if (!movement) {
1223
+ return { x: 0, y: 0 };
1224
+ }
1225
+ return this.documentPointToFlowPoint({
1226
+ x: movement.x,
1227
+ y: movement.y
1228
+ });
1229
+ });
1230
+ this.pointerMovement = toSignal(this.pointerMovementDirective.pointerMovement$);
1231
+ }
1232
+ documentPointToFlowPoint(documentPoint) {
1233
+ const point = this.rootSvg.createSVGPoint();
1234
+ point.x = documentPoint.x;
1235
+ point.y = documentPoint.y;
1236
+ return point.matrixTransform(this.host.getScreenCTM().inverse());
1237
+ }
1238
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpacePointContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1239
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: SpacePointContextDirective, selector: "g[spacePointContext]", ngImport: i0 }); }
1240
+ }
1241
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpacePointContextDirective, decorators: [{
1242
+ type: Directive,
1243
+ args: [{ selector: 'g[spacePointContext]' }]
1244
+ }] });
1245
+
1043
1246
  class HandleService {
1044
1247
  constructor() {
1045
1248
  this.node = signal(null);
@@ -1063,6 +1266,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1063
1266
  type: Injectable
1064
1267
  }] });
1065
1268
 
1269
+ function resizable(elems) {
1270
+ return new Observable((subscriber) => {
1271
+ let ro = new ResizeObserver((entries) => {
1272
+ subscriber.next(entries);
1273
+ });
1274
+ elems.forEach(e => ro.observe(e));
1275
+ return () => ro.disconnect();
1276
+ });
1277
+ }
1278
+
1279
+ function InjectionContext(target, key, descriptor) {
1280
+ const originalMethod = descriptor.value;
1281
+ descriptor.value = function (...args) {
1282
+ if (implementsWithInjector(this)) {
1283
+ return runInInjectionContext(this.injector, () => originalMethod.apply(this, args));
1284
+ }
1285
+ else {
1286
+ throw new Error('Class that contains decorated method must extends WithInjectorDirective class');
1287
+ }
1288
+ };
1289
+ // Return the modified descriptor
1290
+ return descriptor;
1291
+ }
1292
+ const implementsWithInjector = (instance) => {
1293
+ return 'injector' in instance && 'get' in instance.injector;
1294
+ };
1295
+
1296
+ function Microtask(target, key, descriptor) {
1297
+ const originalMethod = descriptor.value;
1298
+ descriptor.value = function (...args) {
1299
+ queueMicrotask(() => {
1300
+ originalMethod?.apply(this, args);
1301
+ });
1302
+ };
1303
+ // Return the modified descriptor
1304
+ return descriptor;
1305
+ }
1306
+
1066
1307
  class HandleModel {
1067
1308
  constructor(rawHandle, parentNode) {
1068
1309
  this.rawHandle = rawHandle;
@@ -1138,43 +1379,46 @@ class HandleModel {
1138
1379
  }
1139
1380
  }
1140
1381
 
1141
- function resizable(elems, zone) {
1142
- return new Observable((subscriber) => {
1143
- let ro = new ResizeObserver((entries) => {
1144
- zone.run(() => 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;
1382
+ class HandleComponent {
1383
+ constructor() {
1384
+ this.injector = inject(Injector);
1385
+ this.handleService = inject(HandleService);
1386
+ this.element = inject(ElementRef).nativeElement;
1387
+ }
1388
+ ngOnInit() {
1389
+ this.model = new HandleModel({
1390
+ position: this.position,
1391
+ type: this.type,
1392
+ id: this.id,
1393
+ parentReference: this.element.parentElement,
1394
+ template: this.template
1395
+ }, this.handleService.node());
1396
+ this.handleService.createHandle(this.model);
1397
+ requestAnimationFrame(() => this.model.updateParent());
1398
+ }
1399
+ ngOnDestroy() {
1400
+ this.handleService.destroyHandle(this.model);
1401
+ }
1402
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1403
+ 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
1404
  }
1405
+ __decorate([
1406
+ InjectionContext
1407
+ ], HandleComponent.prototype, "ngOnInit", null);
1408
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, decorators: [{
1409
+ type: Component,
1410
+ args: [{ selector: 'handle', changeDetection: ChangeDetectionStrategy.OnPush, template: "" }]
1411
+ }], propDecorators: { position: [{
1412
+ type: Input,
1413
+ args: [{ required: true }]
1414
+ }], type: [{
1415
+ type: Input,
1416
+ args: [{ required: true }]
1417
+ }], id: [{
1418
+ type: Input
1419
+ }], template: [{
1420
+ type: Input
1421
+ }], ngOnInit: [] } });
1178
1422
 
1179
1423
  class HandleSizeControllerDirective {
1180
1424
  constructor() {
@@ -1212,45 +1456,6 @@ function getChildStrokeWidth(element) {
1212
1456
  return 0;
1213
1457
  }
1214
1458
 
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
1459
  class PointerDirective {
1255
1460
  constructor() {
1256
1461
  this.hostElement = inject(ElementRef).nativeElement;
@@ -1334,31 +1539,29 @@ class NodeComponent {
1334
1539
  constructor() {
1335
1540
  this.injector = inject(Injector);
1336
1541
  this.handleService = inject(HandleService);
1337
- this.zone = inject(NgZone);
1338
1542
  this.draggableService = inject(DraggableService);
1339
1543
  this.flowStatusService = inject(FlowStatusService);
1340
- this.flowEntitiesService = inject(FlowEntitiesService);
1341
1544
  this.nodeRenderingService = inject(NodeRenderingService);
1342
1545
  this.flowSettingsService = inject(FlowSettingsService);
1343
1546
  this.selectionService = inject(SelectionService);
1344
1547
  this.hostRef = inject(ElementRef);
1548
+ this.connectionController = inject(ConnectionControllerDirective);
1345
1549
  this.showMagnet = computed(() => this.flowStatusService.status().state === 'connection-start' ||
1346
1550
  this.flowStatusService.status().state === 'connection-validation');
1347
1551
  this.styleWidth = computed(() => `${this.nodeModel.size().width}px`);
1348
1552
  this.styleHeight = computed(() => `${this.nodeModel.size().height}px`);
1349
- this.subscription = new Subscription();
1350
1553
  }
1351
1554
  ngOnInit() {
1352
1555
  this.handleService.node.set(this.nodeModel);
1353
1556
  this.draggableService.toggleDraggable(this.hostRef.nativeElement, this.nodeModel);
1354
- const sub = this.nodeModel.handles$
1355
- .pipe(switchMap((handles) => resizable(handles.map(h => h.parentReference), this.zone)
1356
- .pipe(map(() => handles))), tap((handles) => handles.forEach(h => h.updateParent())))
1557
+ this.nodeModel.handles$
1558
+ .pipe(switchMap((handles) => resizable(handles.map(h => h.parentReference)).pipe(map(() => handles))), tap((handles) => {
1559
+ // TODO (performance) inspect how to avoid calls of this when flow initially rendered
1560
+ handles.forEach(h => h.updateParent());
1561
+ }), takeUntilDestroyed())
1357
1562
  .subscribe();
1358
- this.subscription.add(sub);
1359
1563
  }
1360
1564
  ngAfterViewInit() {
1361
- this.setInitialHandles();
1362
1565
  if (this.nodeModel.node.type === 'default') {
1363
1566
  this.nodeModel.size.set({
1364
1567
  width: this.nodeModel.node.width ?? NodeModel.defaultTypeSize.width,
@@ -1366,69 +1569,30 @@ class NodeComponent {
1366
1569
  });
1367
1570
  }
1368
1571
  if (this.nodeModel.node.type === 'html-template' || this.nodeModel.isComponentType) {
1369
- const sub = resizable([this.htmlWrapperRef.nativeElement], this.zone)
1572
+ resizable([this.htmlWrapperRef.nativeElement])
1370
1573
  .pipe(startWith(null), tap(() => {
1371
1574
  const width = this.htmlWrapperRef.nativeElement.clientWidth;
1372
1575
  const height = this.htmlWrapperRef.nativeElement.clientHeight;
1373
1576
  this.nodeModel.size.set({ width, height });
1374
- })).subscribe();
1375
- this.subscription.add(sub);
1577
+ }), takeUntilDestroyed()).subscribe();
1376
1578
  }
1377
1579
  }
1378
1580
  ngOnDestroy() {
1379
1581
  this.draggableService.destroy(this.hostRef.nativeElement);
1380
- this.subscription.unsubscribe();
1381
1582
  }
1382
1583
  startConnection(event, handle) {
1383
1584
  // ignore drag by stopping propagation
1384
1585
  event.stopPropagation();
1385
- this.flowStatusService.setConnectionStartStatus(this.nodeModel, handle);
1586
+ this.connectionController.startConnection(handle);
1386
1587
  }
1387
- endConnection() {
1388
- const status = this.flowStatusService.status();
1389
- if (status.state === 'connection-validation') {
1390
- const sourceNode = status.payload.sourceNode;
1391
- const targetNode = this.nodeModel;
1392
- const sourceHandle = status.payload.sourceHandle;
1393
- const targetHandle = status.payload.targetHandle;
1394
- batchStatusChanges(
1395
- // call to create connection
1396
- () => this.flowStatusService.setConnectionEndStatus(sourceNode, targetNode, sourceHandle, targetHandle),
1397
- // when connection created, we need go back to idle status
1398
- () => this.flowStatusService.setIdleStatus());
1399
- }
1588
+ validateConnection(handle) {
1589
+ this.connectionController.validateConnection(handle);
1400
1590
  }
1401
- /**
1402
- * TODO srp
1403
- */
1404
- validateTargetHandle(targetHandle) {
1405
- const status = this.flowStatusService.status();
1406
- if (status.state === 'connection-start') {
1407
- const sourceNode = status.payload.sourceNode;
1408
- const sourceHandle = status.payload.sourceHandle;
1409
- const source = sourceNode.node.id;
1410
- const targetNode = this.nodeModel;
1411
- const target = targetNode.node.id;
1412
- const valid = this.flowEntitiesService.connection().validator({
1413
- source,
1414
- target,
1415
- sourceHandle: sourceHandle.rawHandle.id,
1416
- targetHandle: targetHandle.rawHandle.id
1417
- });
1418
- targetHandle.state.set(valid ? 'valid' : 'invalid');
1419
- this.flowStatusService.setConnectionValidationStatus(valid, sourceNode, targetNode, sourceHandle, targetHandle);
1420
- }
1591
+ resetValidateConnection(targetHandle) {
1592
+ this.connectionController.resetValidateConnection(targetHandle);
1421
1593
  }
1422
- /**
1423
- * TODO srp
1424
- */
1425
- resetValidateTargetHandle(targetHandle) {
1426
- targetHandle.state.set('idle');
1427
- // drop back to start status
1428
- const status = this.flowStatusService.status();
1429
- if (status.state === 'connection-validation') {
1430
- this.flowStatusService.setConnectionStartStatus(status.payload.sourceNode, status.payload.sourceHandle);
1431
- }
1594
+ endConnection(handle) {
1595
+ this.connectionController.endConnection(handle);
1432
1596
  }
1433
1597
  pullNode() {
1434
1598
  this.nodeRenderingService.pullNode(this.nodeModel);
@@ -1438,32 +1602,20 @@ class NodeComponent {
1438
1602
  this.selectionService.select(this.nodeModel);
1439
1603
  }
1440
1604
  }
1441
- setInitialHandles() {
1442
- if (this.nodeModel.node.type === 'default') {
1443
- this.handleService.createHandle(new HandleModel({
1444
- position: this.nodeModel.sourcePosition(),
1445
- type: 'source',
1446
- parentReference: this.htmlWrapperRef.nativeElement
1447
- }, this.nodeModel));
1448
- this.handleService.createHandle(new HandleModel({
1449
- position: this.nodeModel.targetPosition(),
1450
- type: 'target',
1451
- parentReference: this.htmlWrapperRef.nativeElement
1452
- }, this.nodeModel));
1453
- }
1454
- }
1455
1605
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1456
- 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 }); }
1606
+ 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 }); }
1457
1607
  }
1458
1608
  __decorate([
1459
- Microtask
1460
- ], NodeComponent.prototype, "ngAfterViewInit", null);
1609
+ InjectionContext
1610
+ ], NodeComponent.prototype, "ngOnInit", null);
1461
1611
  __decorate([
1612
+ Microtask // TODO (performance) check if we need microtask here
1613
+ ,
1462
1614
  InjectionContext
1463
- ], NodeComponent.prototype, "setInitialHandles", null);
1615
+ ], NodeComponent.prototype, "ngAfterViewInit", null);
1464
1616
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, decorators: [{
1465
1617
  type: Component,
1466
- 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"] }]
1618
+ 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"] }]
1467
1619
  }], propDecorators: { nodeModel: [{
1468
1620
  type: Input
1469
1621
  }], nodeHtmlTemplate: [{
@@ -1474,7 +1626,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1474
1626
  }], htmlWrapperRef: [{
1475
1627
  type: ViewChild,
1476
1628
  args: ['htmlWrapper']
1477
- }], ngAfterViewInit: [], setInitialHandles: [] } });
1629
+ }], ngOnInit: [], ngAfterViewInit: [] } });
1478
1630
 
1479
1631
  class EdgeLabelComponent {
1480
1632
  constructor() {
@@ -1579,34 +1731,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1579
1731
  type: Input
1580
1732
  }] } });
1581
1733
 
1582
- class SpacePointContextDirective {
1583
- constructor() {
1584
- this.pointerMovementDirective = inject(RootPointerDirective);
1585
- this.rootSvg = inject(RootSvgReferenceDirective).element;
1586
- this.host = inject(ElementRef).nativeElement;
1587
- /**
1588
- * Signal with current mouse position in svg space
1589
- */
1590
- this.svgCurrentSpacePoint = computed(() => {
1591
- const movement = this.pointerMovement();
1592
- if (!movement) {
1593
- return { x: 0, y: 0 };
1594
- }
1595
- const point = this.rootSvg.createSVGPoint();
1596
- point.x = movement.x;
1597
- point.y = movement.y;
1598
- return point.matrixTransform(this.host.getScreenCTM().inverse());
1599
- });
1600
- this.pointerMovement = toSignal(this.pointerMovementDirective.pointerMovement$);
1601
- }
1602
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpacePointContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1603
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: SpacePointContextDirective, selector: "g[spacePointContext]", ngImport: i0 }); }
1604
- }
1605
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SpacePointContextDirective, decorators: [{
1606
- type: Directive,
1607
- args: [{ selector: 'g[spacePointContext]' }]
1608
- }] });
1609
-
1610
1734
  class ConnectionComponent {
1611
1735
  constructor() {
1612
1736
  this.flowStatusService = inject(FlowStatusService);
@@ -1644,7 +1768,7 @@ class ConnectionComponent {
1644
1768
  return null;
1645
1769
  });
1646
1770
  this.markerUrl = computed(() => {
1647
- const marker = this.model.connection.marker;
1771
+ const marker = this.model.settings.marker;
1648
1772
  if (marker) {
1649
1773
  return `url(#${hashCode(JSON.stringify(marker))})`;
1650
1774
  }
@@ -1735,6 +1859,71 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1735
1859
  args: [{ required: true }]
1736
1860
  }] } });
1737
1861
 
1862
+ function id() {
1863
+ const randomLetter = String.fromCharCode(65 + Math.floor(Math.random() * 26));
1864
+ return randomLetter + Date.now();
1865
+ }
1866
+
1867
+ const defaultBg = '#fff';
1868
+ const defaultGap = 20;
1869
+ const defaultDotSize = 2;
1870
+ const defaultDotColor = 'rgb(177, 177, 183)';
1871
+ class BackgroundComponent {
1872
+ set background(value) {
1873
+ this.backgroundSignal.set(value);
1874
+ }
1875
+ constructor() {
1876
+ this.viewportService = inject(ViewportService);
1877
+ this.rootSvg = inject(RootSvgReferenceDirective).element;
1878
+ this.backgroundSignal = signal({ type: 'solid', color: defaultBg });
1879
+ this.scaledGap = computed(() => {
1880
+ const background = this.backgroundSignal();
1881
+ if (background.type === 'dots') {
1882
+ const zoom = this.viewportService.readableViewport().zoom;
1883
+ return zoom * (background.gap ?? defaultGap);
1884
+ }
1885
+ return 0;
1886
+ });
1887
+ this.x = computed(() => this.viewportService.readableViewport().x % this.scaledGap());
1888
+ this.y = computed(() => this.viewportService.readableViewport().y % this.scaledGap());
1889
+ this.patternColor = computed(() => this.backgroundSignal().color ?? defaultDotColor);
1890
+ this.patternSize = computed(() => {
1891
+ const background = this.backgroundSignal();
1892
+ if (background.type === 'dots') {
1893
+ return (this.viewportService.readableViewport().zoom * (background.size ?? defaultDotSize)) / 2;
1894
+ }
1895
+ return 0;
1896
+ });
1897
+ // Without ID there will be pattern collision for several flows on the page
1898
+ // Later pattern ID may be exposed to API
1899
+ this.patternId = id();
1900
+ this.patternUrl = `url(#${this.patternId})`;
1901
+ effect(() => {
1902
+ const background = this.backgroundSignal();
1903
+ if (background.type === 'dots') {
1904
+ this.rootSvg.style.backgroundColor = background.backgroundColor ?? defaultBg;
1905
+ }
1906
+ if (background.type === 'solid') {
1907
+ this.rootSvg.style.backgroundColor = background.color;
1908
+ }
1909
+ });
1910
+ }
1911
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: BackgroundComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1912
+ 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 }); }
1913
+ }
1914
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: BackgroundComponent, decorators: [{
1915
+ type: Component,
1916
+ 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" }]
1917
+ }], ctorParameters: function () { return []; }, propDecorators: { background: [{
1918
+ type: Input,
1919
+ args: [{ required: true, transform }]
1920
+ }] } });
1921
+ function transform(background) {
1922
+ return typeof background === 'string'
1923
+ ? { type: 'solid', color: background }
1924
+ : background;
1925
+ }
1926
+
1738
1927
  // TODO: too general purpose nane
1739
1928
  class RootSvgContextDirective {
1740
1929
  constructor() {
@@ -1816,9 +2005,9 @@ class VflowComponent {
1816
2005
  */
1817
2006
  this.maxZoom = 3;
1818
2007
  /**
1819
- * Background color for flow
2008
+ * Background for flow
1820
2009
  */
1821
- this.background = '#FFFFFF';
2010
+ this.background = '#fff';
1822
2011
  this.nodeModels = computed(() => this.nodeRenderingService.nodes());
1823
2012
  this.edgeModels = computed(() => this.flowEntitiesService.validEdges());
1824
2013
  // #endregion
@@ -1958,6 +2147,12 @@ class VflowComponent {
1958
2147
  getDetachedEdges() {
1959
2148
  return this.flowEntitiesService.getDetachedEdges().map(e => e.edge);
1960
2149
  }
2150
+ /**
2151
+ * Convert point received from document to point on the flow
2152
+ */
2153
+ documentPointToFlowPoint(point) {
2154
+ return this.spacePointContext.documentPointToFlowPoint(point);
2155
+ }
1961
2156
  // #endregion
1962
2157
  trackNodes(idx, { node }) {
1963
2158
  return node;
@@ -1977,7 +2172,7 @@ class VflowComponent {
1977
2172
  SelectionService,
1978
2173
  FlowSettingsService,
1979
2174
  ComponentEventBusService
1980
- ], 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 [style.backgroundColor]=\"background\"\n [attr.width]=\"flowWidth()\"\n [attr.height]=\"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 [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: "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 }); }
2175
+ ], 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 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 }); }
1981
2176
  }
1982
2177
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, decorators: [{
1983
2178
  type: Component,
@@ -1995,7 +2190,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1995
2190
  ], hostDirectives: [
1996
2191
  connectionControllerHostDirective,
1997
2192
  changesControllerHostDirective
1998
- ], template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n class=\"root-svg\"\n #flow\n [style.backgroundColor]=\"background\"\n [attr.width]=\"flowWidth()\"\n [attr.height]=\"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 [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"] }]
2193
+ ], 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"] }]
1999
2194
  }], propDecorators: { view: [{
2000
2195
  type: Input
2001
2196
  }], minZoom: [{
@@ -2033,49 +2228,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2033
2228
  }], mapContext: [{
2034
2229
  type: ViewChild,
2035
2230
  args: [MapContextDirective]
2231
+ }], spacePointContext: [{
2232
+ type: ViewChild,
2233
+ args: [SpacePointContextDirective]
2036
2234
  }] } });
2037
2235
 
2038
- class HandleComponent {
2039
- constructor() {
2040
- this.injector = inject(Injector);
2041
- this.handleService = inject(HandleService);
2042
- this.element = inject(ElementRef).nativeElement;
2043
- }
2044
- ngOnInit() {
2045
- this.model = new HandleModel({
2046
- position: this.position,
2047
- type: this.type,
2048
- id: this.id,
2049
- parentReference: this.element.parentElement,
2050
- template: this.template
2051
- }, this.handleService.node());
2052
- this.handleService.createHandle(this.model);
2053
- queueMicrotask(() => this.model.updateParent());
2054
- }
2055
- ngOnDestroy() {
2056
- this.handleService.destroyHandle(this.model);
2057
- }
2058
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2059
- 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: "" }); }
2060
- }
2061
- __decorate([
2062
- InjectionContext
2063
- ], HandleComponent.prototype, "ngOnInit", null);
2064
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, decorators: [{
2065
- type: Component,
2066
- args: [{ selector: 'handle', template: "" }]
2067
- }], propDecorators: { position: [{
2068
- type: Input,
2069
- args: [{ required: true }]
2070
- }], type: [{
2071
- type: Input,
2072
- args: [{ required: true }]
2073
- }], id: [{
2074
- type: Input
2075
- }], template: [{
2076
- type: Input
2077
- }], ngOnInit: [] } });
2078
-
2079
2236
  class SelectableDirective {
2080
2237
  constructor() {
2081
2238
  this.flowSettingsService = inject(FlowSettingsService);
@@ -2116,7 +2273,8 @@ const components = [
2116
2273
  EdgeLabelComponent,
2117
2274
  ConnectionComponent,
2118
2275
  HandleComponent,
2119
- DefsComponent
2276
+ DefsComponent,
2277
+ BackgroundComponent
2120
2278
  ];
2121
2279
  const directives = [
2122
2280
  SpacePointContextDirective,
@@ -2143,7 +2301,8 @@ class VflowModule {
2143
2301
  EdgeLabelComponent,
2144
2302
  ConnectionComponent,
2145
2303
  HandleComponent,
2146
- DefsComponent, SpacePointContextDirective,
2304
+ DefsComponent,
2305
+ BackgroundComponent, SpacePointContextDirective,
2147
2306
  MapContextDirective,
2148
2307
  RootSvgReferenceDirective,
2149
2308
  RootSvgContextDirective,