ngx-vflow 0.2.1 → 0.3.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 (35) hide show
  1. package/esm2022/lib/vflow/components/edge-label/edge-label.component.mjs +13 -10
  2. package/esm2022/lib/vflow/components/handle/handle.component.mjs +26 -28
  3. package/esm2022/lib/vflow/components/node/node.component.mjs +58 -25
  4. package/esm2022/lib/vflow/components/vflow/vflow.component.mjs +10 -4
  5. package/esm2022/lib/vflow/decorators/microtask.decorator.mjs +11 -0
  6. package/esm2022/lib/vflow/decorators/run-in-injection-context.decorator.mjs +26 -0
  7. package/esm2022/lib/vflow/directives/handle-size-controller.directive.mjs +38 -0
  8. package/esm2022/lib/vflow/models/edge.model.mjs +36 -6
  9. package/esm2022/lib/vflow/models/handle.model.mjs +36 -5
  10. package/esm2022/lib/vflow/models/node.model.mjs +3 -22
  11. package/esm2022/lib/vflow/services/edge-changes.service.mjs +14 -7
  12. package/esm2022/lib/vflow/services/flow-entities.service.mjs +5 -2
  13. package/esm2022/lib/vflow/services/handle.service.mjs +10 -4
  14. package/esm2022/lib/vflow/services/node-changes.service.mjs +6 -3
  15. package/esm2022/lib/vflow/types/edge-change.type.mjs +1 -1
  16. package/esm2022/lib/vflow/utils/add-nodes-to-edges.mjs +3 -3
  17. package/esm2022/lib/vflow/utils/resizable.mjs +11 -0
  18. package/esm2022/lib/vflow/vflow.module.mjs +5 -2
  19. package/fesm2022/ngx-vflow.mjs +317 -146
  20. package/fesm2022/ngx-vflow.mjs.map +1 -1
  21. package/lib/vflow/components/handle/handle.component.d.ts +8 -4
  22. package/lib/vflow/components/node/node.component.d.ts +7 -6
  23. package/lib/vflow/components/vflow/vflow.component.d.ts +6 -2
  24. package/lib/vflow/decorators/microtask.decorator.d.ts +1 -0
  25. package/lib/vflow/decorators/run-in-injection-context.decorator.d.ts +8 -0
  26. package/lib/vflow/directives/handle-size-controller.directive.d.ts +10 -0
  27. package/lib/vflow/models/edge.model.d.ts +21 -3
  28. package/lib/vflow/models/handle.model.d.ts +33 -0
  29. package/lib/vflow/models/node.model.d.ts +2 -3
  30. package/lib/vflow/services/flow-entities.service.d.ts +1 -0
  31. package/lib/vflow/services/handle.service.d.ts +8 -10
  32. package/lib/vflow/types/edge-change.type.d.ts +3 -0
  33. package/lib/vflow/utils/resizable.d.ts +3 -0
  34. package/lib/vflow/vflow.module.d.ts +4 -3
  35. package/package.json +1 -1
@@ -1,13 +1,14 @@
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, effect, Input, TemplateRef, computed, EventEmitter, Output, untracked, Injector, runInInjectionContext, Component, ChangeDetectionStrategy, ViewChild, HostListener, ContentChild, NgModule } from '@angular/core';
4
+ import { signal, Injectable, inject, ElementRef, Directive, effect, Input, TemplateRef, computed, EventEmitter, Output, untracked, runInInjectionContext, Injector, NgZone, Component, ChangeDetectionStrategy, ViewChild, HostListener, ContentChild, NgModule } from '@angular/core';
5
5
  import { select } from 'd3-selection';
6
6
  import { zoomIdentity, zoom } from 'd3-zoom';
7
7
  import { drag } from 'd3-drag';
8
8
  import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
9
- import { switchMap, merge, skip, map, pairwise, filter, observeOn, asyncScheduler, tap, fromEvent } from 'rxjs';
9
+ import { switchMap, merge, skip, map, pairwise, filter, observeOn, asyncScheduler, zip, distinctUntilChanged, tap, Subject, Observable, Subscription, startWith, fromEvent } from 'rxjs';
10
10
  import { path } from 'd3-path';
11
+ import { __decorate } from 'tslib';
11
12
 
12
13
  class ViewportService {
13
14
  constructor() {
@@ -237,8 +238,8 @@ function addNodesToEdges(nodes, edges) {
237
238
  return acc;
238
239
  }, {});
239
240
  edges.forEach(e => {
240
- e.source = nodesById[e.edge.source];
241
- e.target = nodesById[e.edge.target];
241
+ e.source.set(nodesById[e.edge.source]);
242
+ e.target.set(nodesById[e.edge.target]);
242
243
  });
243
244
  }
244
245
 
@@ -344,12 +345,15 @@ class FlowEntitiesService {
344
345
  });
345
346
  this.validEdges = computed(() => {
346
347
  const nodes = this.nodes();
347
- return this.edges().filter(e => nodes.includes(e.source) && nodes.includes(e.target));
348
+ return this.edges().filter(e => nodes.includes(e.source()) && nodes.includes(e.target()));
348
349
  });
349
350
  }
350
351
  getNode(id) {
351
352
  return this.nodes().find(({ node }) => node.id === id);
352
353
  }
354
+ getDetachedEdges() {
355
+ return this.edges().filter(e => e.detached());
356
+ }
353
357
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowEntitiesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
354
358
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowEntitiesService }); }
355
359
  }
@@ -399,52 +403,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
399
403
  type: Output
400
404
  }] } });
401
405
 
402
- class HandleModel {
403
- constructor(rawHandle, parentNode) {
404
- this.rawHandle = rawHandle;
405
- this.parentNode = parentNode;
406
- this.strokeWidth = 2;
407
- this.size = signal({
408
- width: 10 + (2 * this.strokeWidth),
409
- height: 10 + (2 * this.strokeWidth)
410
- });
411
- this.offset = computed(() => {
412
- switch (this.rawHandle.position) {
413
- case 'left': return {
414
- x: 0,
415
- y: this.rawHandle.parentPosition().y + (this.rawHandle.parentSize().height / 2)
416
- };
417
- case 'right': return {
418
- x: this.parentNode.size().width,
419
- y: this.rawHandle.parentPosition().y + (this.rawHandle.parentSize().height / 2)
420
- };
421
- case 'top': return {
422
- x: this.rawHandle.parentPosition().x + (this.rawHandle.parentSize().width / 2),
423
- y: 0
424
- };
425
- case 'bottom': return {
426
- x: this.rawHandle.parentPosition().x + this.rawHandle.parentSize().width / 2,
427
- y: this.parentNode.size().height
428
- };
429
- }
430
- });
431
- this.sizeOffset = computed(() => {
432
- switch (this.rawHandle.position) {
433
- case 'left': return { x: -(this.size().width / 2), y: 0 };
434
- case 'right': return { x: this.size().width / 2, y: 0 };
435
- case 'top': return { x: 0, y: -(this.size().height / 2) };
436
- case 'bottom': return { x: 0, y: this.size().height / 2 };
437
- }
438
- });
439
- this.pointAbsolute = computed(() => {
440
- return {
441
- x: this.parentNode.point().x + this.offset().x + this.sizeOffset().x,
442
- y: this.parentNode.point().y + this.offset().y + this.sizeOffset().y,
443
- };
444
- });
445
- }
446
- }
447
-
448
406
  class NodeModel {
449
407
  constructor(node) {
450
408
  this.node = node;
@@ -455,26 +413,8 @@ class NodeModel {
455
413
  // Now source and handle positions derived from parent flow
456
414
  this.sourcePosition = computed(() => this.flow.handlePositions().source);
457
415
  this.targetPosition = computed(() => this.flow.handlePositions().target);
458
- this.handles = computed(() => {
459
- if (this.node.type === 'html-template') {
460
- return this.rawHandles().map((handle => new HandleModel(handle, this)));
461
- }
462
- return [
463
- new HandleModel({
464
- position: this.sourcePosition(),
465
- type: 'source',
466
- parentPosition: signal({ x: 0, y: 0 }),
467
- parentSize: signal(this.size())
468
- }, this),
469
- new HandleModel({
470
- position: this.targetPosition(),
471
- type: 'target',
472
- parentPosition: signal({ x: 0, y: 0 }),
473
- parentSize: signal(this.size())
474
- }, this),
475
- ];
476
- });
477
- this.rawHandles = signal([]);
416
+ this.handles = signal([]);
417
+ this.handles$ = toObservable(this.handles);
478
418
  this.draggable = true;
479
419
  // disabled for configuration for now
480
420
  this.magnetRadius = 20;
@@ -605,23 +545,52 @@ function getPointOnBezier(sourcePoint, targetPoint, controlPoint1, controlPoint2
605
545
  class EdgeModel {
606
546
  constructor(edge) {
607
547
  this.edge = edge;
548
+ this.source = signal(undefined);
549
+ this.target = signal(undefined);
550
+ this.detached = computed(() => {
551
+ const source = this.source();
552
+ const target = this.target();
553
+ if (!source || !target) {
554
+ return true;
555
+ }
556
+ let existsSourceHandle = false;
557
+ let existsTargetHandle = false;
558
+ if (this.edge.sourceHandle) {
559
+ existsSourceHandle = !!source.handles()
560
+ .find(handle => handle.rawHandle.id === this.edge.sourceHandle);
561
+ }
562
+ else {
563
+ existsSourceHandle = !!source.handles()
564
+ .find(handle => handle.rawHandle.type === 'source');
565
+ }
566
+ if (this.edge.targetHandle) {
567
+ existsTargetHandle = !!target.handles()
568
+ .find(handle => handle.rawHandle.id === this.edge.targetHandle);
569
+ }
570
+ else {
571
+ existsTargetHandle = !!target.handles()
572
+ .find(handle => handle.rawHandle.type === 'target');
573
+ }
574
+ return !existsSourceHandle || !existsTargetHandle;
575
+ });
576
+ this.detached$ = toObservable(this.detached);
608
577
  this.path = computed(() => {
609
578
  let source;
610
579
  if (this.edge.sourceHandle) {
611
- source = this.source.handles()
580
+ source = this.source()?.handles()
612
581
  .find(handle => handle.rawHandle.id === this.edge.sourceHandle);
613
582
  }
614
583
  else {
615
- source = this.source.handles()
584
+ source = this.source()?.handles()
616
585
  .find(handle => handle.rawHandle.type === 'source');
617
586
  }
618
587
  let target;
619
588
  if (this.edge.targetHandle) {
620
- target = this.target.handles()
589
+ target = this.target()?.handles()
621
590
  .find(handle => handle.rawHandle.id === this.edge.targetHandle);
622
591
  }
623
592
  else {
624
- target = this.target.handles()
593
+ target = this.target()?.handles()
625
594
  .find(handle => handle.rawHandle.type === 'target');
626
595
  }
627
596
  // TODO: don't like this
@@ -702,7 +671,10 @@ class NodesChangeService {
702
671
  .pipe(pairwise(), map(([oldList, newList]) => newList.filter(node => !oldList.includes(node))), filter((nodes) => !!nodes.length), map((nodes) => nodes.map(node => ({ type: 'add', id: node.node.id }))));
703
672
  this.nodeRemoveChange$ = toObservable(this.entitiesService.nodes)
704
673
  .pipe(pairwise(), map(([oldList, newList]) => oldList.filter(node => !newList.includes(node))), filter((nodes) => !!nodes.length), map((nodes) => nodes.map(node => ({ type: 'remove', id: node.node.id }))));
705
- this.changes$ = merge(this.nodesPositionChange$, this.nodeAddChange$, this.nodeRemoveChange$);
674
+ this.changes$ = merge(this.nodesPositionChange$, this.nodeAddChange$, this.nodeRemoveChange$).pipe(
675
+ // this fixes a bug when on fire node event change,
676
+ // you can't get valid list of detached edges
677
+ observeOn(asyncScheduler));
706
678
  }
707
679
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodesChangeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
708
680
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodesChangeService }); }
@@ -711,16 +683,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
711
683
  type: Injectable
712
684
  }] });
713
685
 
686
+ const haveSameContents = (a, b) => a.length === b.length &&
687
+ [...new Set([...a, ...b])].every(v => a.filter(e => e === v).length === b.filter(e => e === v).length);
714
688
  class EdgeChangesService {
715
689
  constructor() {
716
690
  this.entitiesService = inject(FlowEntitiesService);
717
- this.edgeDetachedChange$ = toObservable(computed(() => {
691
+ this.edgeDetachedChange$ = merge(toObservable(computed(() => {
718
692
  const nodes = this.entitiesService.nodes();
719
693
  const edges = untracked(this.entitiesService.edges);
720
- return edges.filter(({ source, target }) => !nodes.includes(source) || !nodes.includes(target));
721
- })).pipe(
722
- // TODO check why there are 2 emits from single call inside computed
723
- filter(edges => !!edges.length), map((edges) => edges.map(({ edge }) => ({ type: 'detached', id: edge.id }))));
694
+ return edges.filter(({ source, target }) => !nodes.includes(source()) || !nodes.includes(target()));
695
+ })), toObservable(this.entitiesService.edges).pipe(switchMap((edges) => {
696
+ return zip(...edges.map(e => e.detached$.pipe(map(() => e))));
697
+ }), map((edges) => edges.filter(e => e.detached())),
698
+ // TODO check why there are 2 emits
699
+ skip(2))).pipe(
700
+ // here we check if 2 approaches to detect detached edges emits same
701
+ // and same values (this may happen on node delete)
702
+ distinctUntilChanged(haveSameContents), filter(edges => !!edges.length), map((edges) => edges.map(({ edge }) => ({ type: 'detached', id: edge.id }))));
724
703
  this.edgeAddChange$ = toObservable(this.entitiesService.edges)
725
704
  .pipe(pairwise(), map(([oldList, newList]) => {
726
705
  return newList.filter(edge => !oldList.includes(edge));
@@ -780,13 +759,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
780
759
 
781
760
  class HandleService {
782
761
  constructor() {
783
- this.handles = signal([]);
762
+ this.node = signal(null);
784
763
  }
785
764
  createHandle(newHandle) {
786
- this.handles.update(handles => [...handles, newHandle]);
765
+ const node = this.node();
766
+ if (node) {
767
+ node.handles.update(handles => [...handles, newHandle]);
768
+ }
787
769
  }
788
770
  destroyHandle(handleToDestoy) {
789
- this.handles.update(handles => handles.filter(handle => handle !== handleToDestoy));
771
+ const node = this.node();
772
+ if (node) {
773
+ node.handles.update(handles => handles.filter(handle => handle !== handleToDestoy));
774
+ }
790
775
  }
791
776
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
792
777
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService }); }
@@ -795,41 +780,203 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
795
780
  type: Injectable
796
781
  }] });
797
782
 
798
- class NodeComponent {
783
+ class HandleModel {
784
+ constructor(rawHandle, parentNode) {
785
+ this.rawHandle = rawHandle;
786
+ this.parentNode = parentNode;
787
+ this.strokeWidth = 2;
788
+ /**
789
+ * Pre-computed size for default handle, changed dynamically
790
+ * for custom handles
791
+ */
792
+ this.size = signal({
793
+ width: 10 + (2 * this.strokeWidth),
794
+ height: 10 + (2 * this.strokeWidth)
795
+ });
796
+ this.offset = computed(() => {
797
+ switch (this.rawHandle.position) {
798
+ case 'left': return {
799
+ x: 0,
800
+ y: this.parentPosition().y + (this.parentSize().height / 2)
801
+ };
802
+ case 'right': return {
803
+ x: this.parentNode.size().width,
804
+ y: this.parentPosition().y + (this.parentSize().height / 2)
805
+ };
806
+ case 'top': return {
807
+ x: this.parentPosition().x + (this.parentSize().width / 2),
808
+ y: 0
809
+ };
810
+ case 'bottom': return {
811
+ x: this.parentPosition().x + this.parentSize().width / 2,
812
+ y: this.parentNode.size().height
813
+ };
814
+ }
815
+ });
816
+ this.sizeOffset = computed(() => {
817
+ switch (this.rawHandle.position) {
818
+ case 'left': return { x: -(this.size().width / 2), y: 0 };
819
+ case 'right': return { x: this.size().width / 2, y: 0 };
820
+ case 'top': return { x: 0, y: -(this.size().height / 2) };
821
+ case 'bottom': return { x: 0, y: this.size().height / 2 };
822
+ }
823
+ });
824
+ this.pointAbsolute = computed(() => {
825
+ return {
826
+ x: this.parentNode.point().x + this.offset().x + this.sizeOffset().x,
827
+ y: this.parentNode.point().y + this.offset().y + this.sizeOffset().y,
828
+ };
829
+ });
830
+ this.state = signal('idle');
831
+ this.updateParentSizeAndPosition$ = new Subject();
832
+ this.parentSize = toSignal(this.updateParentSizeAndPosition$.pipe(map(() => ({
833
+ width: this.parentReference.offsetWidth,
834
+ height: this.parentReference.offsetHeight
835
+ }))), {
836
+ initialValue: { width: 0, height: 0 }
837
+ });
838
+ this.parentPosition = toSignal(this.updateParentSizeAndPosition$.pipe(map(() => ({
839
+ x: this.parentReference.offsetLeft,
840
+ y: this.parentReference.offsetTop
841
+ }))), {
842
+ initialValue: { x: 0, y: 0 }
843
+ });
844
+ this.parentReference = this.rawHandle.parentReference;
845
+ this.template = this.rawHandle.template;
846
+ this.templateContext = {
847
+ $implicit: {
848
+ point: this.offset,
849
+ state: this.state
850
+ }
851
+ };
852
+ }
853
+ updateParent() {
854
+ this.updateParentSizeAndPosition$.next();
855
+ }
856
+ }
857
+
858
+ function resizable(elems, zone) {
859
+ return new Observable((subscriber) => {
860
+ let ro = new ResizeObserver((entries) => {
861
+ zone.run(() => subscriber.next(entries));
862
+ });
863
+ elems.forEach(e => ro.observe(e));
864
+ return () => ro.disconnect();
865
+ });
866
+ }
867
+
868
+ function InjectionContext(target, key, descriptor) {
869
+ const originalMethod = descriptor.value;
870
+ descriptor.value = function (...args) {
871
+ if (this instanceof WithInjectorDirective) {
872
+ return runInInjectionContext(this.injector, () => originalMethod.apply(this, args));
873
+ }
874
+ else {
875
+ throw new Error('Class that contains decorated method must extends WithInjectorDirective class');
876
+ }
877
+ };
878
+ // Return the modified descriptor
879
+ return descriptor;
880
+ }
881
+ class WithInjectorDirective {
799
882
  constructor() {
800
- this.handleService = inject(HandleService);
801
883
  this.injector = inject(Injector);
884
+ }
885
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: WithInjectorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
886
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: WithInjectorDirective, ngImport: i0 }); }
887
+ }
888
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: WithInjectorDirective, decorators: [{
889
+ type: Directive
890
+ }] });
891
+
892
+ function Microtask(target, key, descriptor) {
893
+ const originalMethod = descriptor.value;
894
+ descriptor.value = function (...args) {
895
+ queueMicrotask(() => {
896
+ originalMethod?.apply(this, args);
897
+ });
898
+ };
899
+ // Return the modified descriptor
900
+ return descriptor;
901
+ }
902
+
903
+ class HandleSizeControllerDirective {
904
+ constructor() {
905
+ this.handleWrapper = inject(ElementRef);
906
+ }
907
+ ngAfterViewInit() {
908
+ const element = this.handleWrapper.nativeElement;
909
+ const rect = element.getBBox();
910
+ const stroke = getChildStrokeWidth(element);
911
+ this.handleModel.size.set({
912
+ width: rect.width + stroke,
913
+ height: rect.height + stroke
914
+ });
915
+ }
916
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleSizeControllerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
917
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: HandleSizeControllerDirective, selector: "[handleSizeController]", inputs: { handleModel: ["handleSizeController", "handleModel"] }, ngImport: i0 }); }
918
+ }
919
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleSizeControllerDirective, decorators: [{
920
+ type: Directive,
921
+ args: [{ selector: '[handleSizeController]' }]
922
+ }], propDecorators: { handleModel: [{
923
+ type: Input,
924
+ args: [{ required: true, alias: 'handleSizeController' }]
925
+ }] } });
926
+ function getChildStrokeWidth(element) {
927
+ const child = element.firstElementChild;
928
+ if (child) {
929
+ const stroke = getComputedStyle(child).strokeWidth;
930
+ const strokeAsNumber = Number(stroke.replace('px', ''));
931
+ if (isNaN(strokeAsNumber)) {
932
+ return 0;
933
+ }
934
+ return strokeAsNumber;
935
+ }
936
+ return 0;
937
+ }
938
+
939
+ class NodeComponent extends WithInjectorDirective {
940
+ constructor() {
941
+ super(...arguments);
942
+ this.handleService = inject(HandleService);
943
+ this.zone = inject(NgZone);
802
944
  this.draggableService = inject(DraggableService);
803
945
  this.flowStatusService = inject(FlowStatusService);
804
946
  this.flowEntitiesService = inject(FlowEntitiesService);
805
947
  this.hostRef = inject(ElementRef);
806
948
  this.showMagnet = computed(() => this.flowStatusService.status().state === 'connection-start' ||
807
949
  this.flowStatusService.status().state === 'connection-validation');
808
- this.sourceHanldeState = signal('idle');
809
- this.targetHandleState = signal('idle');
950
+ this.subscription = new Subscription();
810
951
  }
811
952
  ngOnInit() {
812
- runInInjectionContext(this.injector, () => {
813
- effect(() => this.nodeModel.rawHandles.set(this.handleService.handles()), { allowSignalWrites: true });
814
- });
953
+ this.handleService.node.set(this.nodeModel);
815
954
  this.draggableService.toggleDraggable(this.hostRef.nativeElement, this.nodeModel);
955
+ const sub = this.nodeModel.handles$
956
+ .pipe(switchMap((handles) => resizable(handles.map(h => h.parentReference), this.zone)
957
+ .pipe(map(() => handles))), tap((handles) => handles.forEach(h => h.updateParent())))
958
+ .subscribe();
959
+ this.subscription.add(sub);
816
960
  }
817
961
  ngAfterViewInit() {
818
- // TODO remove microtask
819
- queueMicrotask(() => {
820
- if (this.nodeModel.node.type === 'default') {
821
- const { width, height } = this.nodeContentRef.nativeElement.getBBox();
822
- this.nodeModel.size.set({ width, height });
823
- }
824
- if (this.nodeModel.node.type === 'html-template') {
962
+ this.setInitialHandles();
963
+ if (this.nodeModel.node.type === 'default') {
964
+ const { width, height } = this.nodeContentRef.nativeElement.getBBox();
965
+ this.nodeModel.size.set({ width, height });
966
+ }
967
+ if (this.nodeModel.node.type === 'html-template') {
968
+ const sub = resizable([this.htmlWrapperRef.nativeElement], this.zone)
969
+ .pipe(startWith(null), tap(() => {
825
970
  const width = this.htmlWrapperRef.nativeElement.clientWidth;
826
971
  const height = this.htmlWrapperRef.nativeElement.clientHeight;
827
972
  this.nodeModel.size.set({ width, height });
828
- }
829
- });
973
+ })).subscribe();
974
+ this.subscription.add(sub);
975
+ }
830
976
  }
831
977
  ngOnDestroy() {
832
978
  this.draggableService.destroy(this.hostRef.nativeElement);
979
+ this.subscription.unsubscribe();
833
980
  }
834
981
  startConnection(event, handle) {
835
982
  // ignore drag by stopping propagation
@@ -867,27 +1014,47 @@ class NodeComponent {
867
1014
  sourceHandle: sourceHandle.rawHandle.id,
868
1015
  targetHandle: targetHandle.rawHandle.id
869
1016
  });
870
- this.targetHandleState.set(valid ? 'valid' : 'invalid');
1017
+ targetHandle.state.set(valid ? 'valid' : 'invalid');
871
1018
  this.flowStatusService.setConnectionValidationStatus(valid, sourceNode, targetNode, sourceHandle, targetHandle);
872
1019
  }
873
1020
  }
874
1021
  /**
875
1022
  * TODO srp
876
1023
  */
877
- resetValidateTargetHandle() {
878
- this.targetHandleState.set('idle');
1024
+ resetValidateTargetHandle(targetHandle) {
1025
+ targetHandle.state.set('idle');
879
1026
  // drop back to start status
880
1027
  const status = this.flowStatusService.status();
881
1028
  if (status.state === 'connection-validation') {
882
1029
  this.flowStatusService.setConnectionStartStatus(status.payload.sourceNode, status.payload.sourceHandle);
883
1030
  }
884
1031
  }
885
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
886
- 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 #nodeContent\n width=\"100\"\n height=\"50\"\n>\n <div class=\"default-node\" [innerHTML]=\"nodeModel.node.text ?? ''\"></div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngTemplateOutlet]=\"nodeHtmlTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<ng-container *ngFor=\"let handle of nodeModel.handles()\">\n <svg:circle\n #sourceHandle\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mousedown)=\"handle.rawHandle.type === 'source' ? startConnection($event, handle) : null\"\n (mouseup)=\"handle.rawHandle.type === 'target' ? endConnection() : null\"\n />\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 (mouseup)=\"endConnection(); resetValidateTargetHandle()\"\n (mouseover)=\"validateTargetHandle(handle)\"\n (mouseout)=\"resetValidateTargetHandle()\"\n />\n</ng-container>\n\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{max-width:100px;max-height:100px;width:100px;height:50px;border:2px solid black;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}\n"], dependencies: [{ kind: "directive", type: i1.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1032
+ setInitialHandles() {
1033
+ if (this.nodeModel.node.type === 'default') {
1034
+ this.handleService.createHandle(new HandleModel({
1035
+ position: this.nodeModel.sourcePosition(),
1036
+ type: 'source',
1037
+ parentReference: this.htmlWrapperRef.nativeElement
1038
+ }, this.nodeModel));
1039
+ this.handleService.createHandle(new HandleModel({
1040
+ position: this.nodeModel.targetPosition(),
1041
+ type: 'target',
1042
+ parentReference: this.htmlWrapperRef.nativeElement
1043
+ }, this.nodeModel));
1044
+ }
1045
+ }
1046
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
1047
+ 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 }], usesInheritance: true, ngImport: i0, template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n #nodeContent\n width=\"100\"\n height=\"50\"\n>\n <div #htmlWrapper class=\"default-node\" [innerHTML]=\"nodeModel.node.text ?? ''\"></div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngTemplateOutlet]=\"nodeHtmlTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node } }\"\n [ngTemplateOutletInjector]=\"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 [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mousedown)=\"handle.rawHandle.type === 'source' ? startConnection($event, handle) : null\"\n (mouseup)=\"handle.rawHandle.type === 'target' ? endConnection() : null\"\n />\n\n <svg:g\n *ngIf=\"handle.template\"\n [handleSizeController]=\"handle\"\n (mousedown)=\"handle.rawHandle.type === 'source' ? startConnection($event, handle) : null\"\n (mouseup)=\"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 (mouseup)=\"endConnection(); resetValidateTargetHandle(handle)\"\n (mouseover)=\"validateTargetHandle(handle)\"\n (mouseout)=\"resetValidateTargetHandle(handle)\"\n />\n</ng-container>\n\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{max-width:100px;max-height:100px;width:100px;height:50px;border:2px solid black;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}\n"], dependencies: [{ kind: "directive", type: i1.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
887
1048
  }
1049
+ __decorate([
1050
+ Microtask
1051
+ ], NodeComponent.prototype, "ngAfterViewInit", null);
1052
+ __decorate([
1053
+ InjectionContext
1054
+ ], NodeComponent.prototype, "setInitialHandles", null);
888
1055
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, decorators: [{
889
1056
  type: Component,
890
- args: [{ selector: 'g[node]', changeDetection: ChangeDetectionStrategy.OnPush, providers: [HandleService], template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n #nodeContent\n width=\"100\"\n height=\"50\"\n>\n <div class=\"default-node\" [innerHTML]=\"nodeModel.node.text ?? ''\"></div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngTemplateOutlet]=\"nodeHtmlTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<ng-container *ngFor=\"let handle of nodeModel.handles()\">\n <svg:circle\n #sourceHandle\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mousedown)=\"handle.rawHandle.type === 'source' ? startConnection($event, handle) : null\"\n (mouseup)=\"handle.rawHandle.type === 'target' ? endConnection() : null\"\n />\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 (mouseup)=\"endConnection(); resetValidateTargetHandle()\"\n (mouseover)=\"validateTargetHandle(handle)\"\n (mouseout)=\"resetValidateTargetHandle()\"\n />\n</ng-container>\n\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{max-width:100px;max-height:100px;width:100px;height:50px;border:2px solid black;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}\n"] }]
1057
+ args: [{ selector: 'g[node]', changeDetection: ChangeDetectionStrategy.OnPush, providers: [HandleService], template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n #nodeContent\n width=\"100\"\n height=\"50\"\n>\n <div #htmlWrapper class=\"default-node\" [innerHTML]=\"nodeModel.node.text ?? ''\"></div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n [ngTemplateOutlet]=\"nodeHtmlTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node } }\"\n [ngTemplateOutletInjector]=\"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 [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mousedown)=\"handle.rawHandle.type === 'source' ? startConnection($event, handle) : null\"\n (mouseup)=\"handle.rawHandle.type === 'target' ? endConnection() : null\"\n />\n\n <svg:g\n *ngIf=\"handle.template\"\n [handleSizeController]=\"handle\"\n (mousedown)=\"handle.rawHandle.type === 'source' ? startConnection($event, handle) : null\"\n (mouseup)=\"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 (mouseup)=\"endConnection(); resetValidateTargetHandle(handle)\"\n (mouseover)=\"validateTargetHandle(handle)\"\n (mouseout)=\"resetValidateTargetHandle(handle)\"\n />\n</ng-container>\n\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{max-width:100px;max-height:100px;width:100px;height:50px;border:2px solid black;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}\n"] }]
891
1058
  }], propDecorators: { nodeModel: [{
892
1059
  type: Input
893
1060
  }], nodeHtmlTemplate: [{
@@ -898,7 +1065,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
898
1065
  }], htmlWrapperRef: [{
899
1066
  type: ViewChild,
900
1067
  args: ['htmlWrapper']
901
- }] } });
1068
+ }], ngAfterViewInit: [], setInitialHandles: [] } });
902
1069
 
903
1070
  class EdgeLabelComponent {
904
1071
  constructor() {
@@ -919,14 +1086,12 @@ class EdgeLabelComponent {
919
1086
  }
920
1087
  set point(point) { this.pointSignal.set(point); }
921
1088
  ngAfterViewInit() {
922
- queueMicrotask(() => {
923
- // this is a fix for visual artifact in chrome that for some reason adresses only for edge label.
924
- // the bug reproduces if edgeLabelWrapperRef size fully matched the size of parent foreignObject
925
- const MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME = 2;
926
- const width = this.edgeLabelWrapperRef.nativeElement.clientWidth + MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME;
927
- const height = this.edgeLabelWrapperRef.nativeElement.clientHeight + MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME;
928
- this.model.size.set({ width, height });
929
- });
1089
+ // this is a fix for visual artifact in chrome that for some reason adresses only for edge label.
1090
+ // the bug reproduces if edgeLabelWrapperRef size fully matched the size of parent foreignObject
1091
+ const MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME = 2;
1092
+ const width = this.edgeLabelWrapperRef.nativeElement.clientWidth + MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME;
1093
+ const height = this.edgeLabelWrapperRef.nativeElement.clientHeight + MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME;
1094
+ this.model.size.set({ width, height });
930
1095
  }
931
1096
  getLabelContext() {
932
1097
  return {
@@ -939,6 +1104,9 @@ class EdgeLabelComponent {
939
1104
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeLabelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
940
1105
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: EdgeLabelComponent, selector: "g[edgeLabel]", inputs: { model: "model", edgeModel: "edgeModel", point: "point", htmlTemplate: "htmlTemplate" }, viewQueries: [{ propertyName: "edgeLabelWrapperRef", first: true, predicate: ["edgeLabelWrapper"], descendants: true }], ngImport: i0, template: "<svg:foreignObject\n [attr.x]=\"edgeLabelPoint().x\"\n [attr.y]=\"edgeLabelPoint().y\"\n [attr.width]=\"model.size().width\"\n [attr.height]=\"model.size().height\"\n *ngIf=\"model.edgeLabel.type === 'html-template' && htmlTemplate\"\n>\n <div #edgeLabelWrapper class=\"edge-label-wrapper\">\n <ng-container\n *ngTemplateOutlet=\"htmlTemplate; context: getLabelContext()\"\n />\n </div>\n</svg:foreignObject>\n", styles: [".edge-label-wrapper{width:max-content;margin-top:1px;margin-left:1px}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
941
1106
  }
1107
+ __decorate([
1108
+ Microtask
1109
+ ], EdgeLabelComponent.prototype, "ngAfterViewInit", null);
942
1110
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeLabelComponent, decorators: [{
943
1111
  type: Component,
944
1112
  args: [{ selector: 'g[edgeLabel]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg:foreignObject\n [attr.x]=\"edgeLabelPoint().x\"\n [attr.y]=\"edgeLabelPoint().y\"\n [attr.width]=\"model.size().width\"\n [attr.height]=\"model.size().height\"\n *ngIf=\"model.edgeLabel.type === 'html-template' && htmlTemplate\"\n>\n <div #edgeLabelWrapper class=\"edge-label-wrapper\">\n <ng-container\n *ngTemplateOutlet=\"htmlTemplate; context: getLabelContext()\"\n />\n </div>\n</svg:foreignObject>\n", styles: [".edge-label-wrapper{width:max-content;margin-top:1px;margin-left:1px}\n"] }]
@@ -953,7 +1121,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
953
1121
  }], edgeLabelWrapperRef: [{
954
1122
  type: ViewChild,
955
1123
  args: ['edgeLabelWrapper']
956
- }] } });
1124
+ }], ngAfterViewInit: [] } });
957
1125
 
958
1126
  class EdgeComponent {
959
1127
  constructor() {
@@ -1277,7 +1445,7 @@ class VflowComponent {
1277
1445
  * Edges to render
1278
1446
  */
1279
1447
  set edges(newEdges) {
1280
- const newModels = ReferenceKeeper.edges(newEdges, this.flowEntitiesService.edges());
1448
+ const newModels = runInInjectionContext(this.injector, () => ReferenceKeeper.edges(newEdges, this.flowEntitiesService.edges()));
1281
1449
  // quick and dirty binding nodes to edges
1282
1450
  addNodesToEdges(this.nodeModels, newModels);
1283
1451
  this.flowEntitiesService.edges.set(newModels);
@@ -1316,12 +1484,18 @@ class VflowComponent {
1316
1484
  getNode(id) {
1317
1485
  return this.flowEntitiesService.getNode(id)?.node;
1318
1486
  }
1487
+ /**
1488
+ * Sync method to get detached edges
1489
+ */
1490
+ getDetachedEdges() {
1491
+ return this.flowEntitiesService.getDetachedEdges().map(e => e.edge);
1492
+ }
1319
1493
  // #endregion
1320
1494
  trackNodes(idx, { node }) {
1321
- return node.id;
1495
+ return node;
1322
1496
  }
1323
1497
  trackEdges(idx, { edge }) {
1324
- return edge.id;
1498
+ return edge;
1325
1499
  }
1326
1500
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1327
1501
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "16.2.12", type: VflowComponent, selector: "vflow", inputs: { view: "view", minZoom: "minZoom", maxZoom: "maxZoom", handlePositions: "handlePositions", background: "background", connection: ["connection", "connection", (settings) => new ConnectionModel(settings)], nodes: "nodes", edges: "edges" }, providers: [
@@ -1384,39 +1558,32 @@ function bindFlowToNodes(flow, nodes) {
1384
1558
  nodes.forEach(n => n.bindFlow(flow));
1385
1559
  }
1386
1560
 
1387
- class HandleComponent {
1561
+ class HandleComponent extends WithInjectorDirective {
1388
1562
  constructor() {
1563
+ super(...arguments);
1389
1564
  this.handleService = inject(HandleService);
1390
1565
  this.element = inject(ElementRef).nativeElement;
1391
1566
  }
1392
1567
  ngOnInit() {
1393
- queueMicrotask(() => {
1394
- const rect = this.parentRect();
1395
- this.handleService.createHandle({
1396
- position: this.position,
1397
- type: this.type,
1398
- id: this.id,
1399
- parentPosition: signal({ x: rect.x, y: rect.y }),
1400
- parentSize: signal({ width: rect.width, height: rect.height })
1401
- });
1402
- });
1568
+ this.model = new HandleModel({
1569
+ position: this.position,
1570
+ type: this.type,
1571
+ id: this.id,
1572
+ parentReference: this.element.parentElement,
1573
+ template: this.template
1574
+ }, this.handleService.node());
1575
+ this.handleService.createHandle(this.model);
1576
+ queueMicrotask(() => this.model.updateParent());
1403
1577
  }
1404
- parentRect() {
1405
- // we assume there is only one foreignObject that wraps node
1406
- const fo = this.element.closest('foreignObject');
1407
- const parent = this.element.parentElement;
1408
- const foRect = fo.getBoundingClientRect();
1409
- const parentRect = parent.getBoundingClientRect();
1410
- return {
1411
- x: parentRect.left - foRect.left,
1412
- y: parentRect.top - foRect.top,
1413
- width: parentRect.width,
1414
- height: parentRect.height
1415
- };
1578
+ ngOnDestroy() {
1579
+ this.handleService.destroyHandle(this.model);
1416
1580
  }
1417
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1418
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: HandleComponent, selector: "handle", inputs: { position: "position", type: "type", id: "id" }, ngImport: i0, template: "" }); }
1581
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
1582
+ 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" }, usesInheritance: true, ngImport: i0, template: "" }); }
1419
1583
  }
1584
+ __decorate([
1585
+ InjectionContext
1586
+ ], HandleComponent.prototype, "ngOnInit", null);
1420
1587
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, decorators: [{
1421
1588
  type: Component,
1422
1589
  args: [{ selector: 'handle', template: "" }]
@@ -1428,7 +1595,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1428
1595
  args: [{ required: true }]
1429
1596
  }], id: [{
1430
1597
  type: Input
1431
- }] } });
1598
+ }], template: [{
1599
+ type: Input
1600
+ }], ngOnInit: [] } });
1432
1601
 
1433
1602
  const components = [
1434
1603
  VflowComponent,
@@ -1444,6 +1613,7 @@ const directives = [
1444
1613
  MapContextDirective,
1445
1614
  RootSvgReferenceDirective,
1446
1615
  RootSvgContextDirective,
1616
+ HandleSizeControllerDirective
1447
1617
  ];
1448
1618
  const templateDirectives = [
1449
1619
  NodeHtmlTemplateDirective,
@@ -1463,7 +1633,8 @@ class VflowModule {
1463
1633
  DefsComponent, SpacePointContextDirective,
1464
1634
  MapContextDirective,
1465
1635
  RootSvgReferenceDirective,
1466
- RootSvgContextDirective, NodeHtmlTemplateDirective,
1636
+ RootSvgContextDirective,
1637
+ HandleSizeControllerDirective, NodeHtmlTemplateDirective,
1467
1638
  EdgeLabelHtmlTemplateDirective,
1468
1639
  EdgeTemplateDirective,
1469
1640
  ConnectionTemplateDirective,