ngx-vflow 0.2.2 → 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 (31) 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 +22 -25
  3. package/esm2022/lib/vflow/components/node/node.component.mjs +57 -22
  4. package/esm2022/lib/vflow/components/vflow/vflow.component.mjs +3 -3
  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 +15 -11
  9. package/esm2022/lib/vflow/models/handle.model.mjs +32 -3
  10. package/esm2022/lib/vflow/models/node.model.mjs +3 -22
  11. package/esm2022/lib/vflow/services/edge-changes.service.mjs +2 -2
  12. package/esm2022/lib/vflow/services/flow-entities.service.mjs +2 -2
  13. package/esm2022/lib/vflow/services/handle.service.mjs +3 -4
  14. package/esm2022/lib/vflow/utils/add-nodes-to-edges.mjs +3 -3
  15. package/esm2022/lib/vflow/utils/resizable.mjs +11 -0
  16. package/esm2022/lib/vflow/vflow.module.mjs +5 -2
  17. package/fesm2022/ngx-vflow.mjs +262 -142
  18. package/fesm2022/ngx-vflow.mjs.map +1 -1
  19. package/lib/vflow/components/handle/handle.component.d.ts +5 -4
  20. package/lib/vflow/components/node/node.component.d.ts +7 -6
  21. package/lib/vflow/components/vflow/vflow.component.d.ts +2 -2
  22. package/lib/vflow/decorators/microtask.decorator.d.ts +1 -0
  23. package/lib/vflow/decorators/run-in-injection-context.decorator.d.ts +8 -0
  24. package/lib/vflow/directives/handle-size-controller.directive.d.ts +10 -0
  25. package/lib/vflow/models/edge.model.d.ts +2 -2
  26. package/lib/vflow/models/handle.model.d.ts +30 -2
  27. package/lib/vflow/models/node.model.d.ts +2 -2
  28. package/lib/vflow/services/handle.service.d.ts +3 -6
  29. package/lib/vflow/utils/resizable.d.ts +3 -0
  30. package/lib/vflow/vflow.module.d.ts +4 -3
  31. 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, Component, ChangeDetectionStrategy, ViewChild, HostListener, runInInjectionContext, 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, zip, distinctUntilChanged, 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,7 +345,7 @@ 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) {
@@ -402,54 +403,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
402
403
  type: Output
403
404
  }] } });
404
405
 
405
- class HandleModel {
406
- constructor(rawHandle, parentNode) {
407
- this.rawHandle = rawHandle;
408
- this.parentNode = parentNode;
409
- this.strokeWidth = 2;
410
- this.size = signal({
411
- width: 10 + (2 * this.strokeWidth),
412
- height: 10 + (2 * this.strokeWidth)
413
- });
414
- this.offset = computed(() => {
415
- switch (this.rawHandle.position) {
416
- case 'left': return {
417
- x: 0,
418
- y: this.parentPosition().y + (this.parentSize().height / 2)
419
- };
420
- case 'right': return {
421
- x: this.parentNode.size().width,
422
- y: this.parentPosition().y + (this.parentSize().height / 2)
423
- };
424
- case 'top': return {
425
- x: this.parentPosition().x + (this.parentSize().width / 2),
426
- y: 0
427
- };
428
- case 'bottom': return {
429
- x: this.parentPosition().x + this.parentSize().width / 2,
430
- y: this.parentNode.size().height
431
- };
432
- }
433
- });
434
- this.sizeOffset = computed(() => {
435
- switch (this.rawHandle.position) {
436
- case 'left': return { x: -(this.size().width / 2), y: 0 };
437
- case 'right': return { x: this.size().width / 2, y: 0 };
438
- case 'top': return { x: 0, y: -(this.size().height / 2) };
439
- case 'bottom': return { x: 0, y: this.size().height / 2 };
440
- }
441
- });
442
- this.pointAbsolute = computed(() => {
443
- return {
444
- x: this.parentNode.point().x + this.offset().x + this.sizeOffset().x,
445
- y: this.parentNode.point().y + this.offset().y + this.sizeOffset().y,
446
- };
447
- });
448
- this.parentSize = signal(this.rawHandle.parentSize);
449
- this.parentPosition = signal(this.rawHandle.parentPosition);
450
- }
451
- }
452
-
453
406
  class NodeModel {
454
407
  constructor(node) {
455
408
  this.node = node;
@@ -460,26 +413,8 @@ class NodeModel {
460
413
  // Now source and handle positions derived from parent flow
461
414
  this.sourcePosition = computed(() => this.flow.handlePositions().source);
462
415
  this.targetPosition = computed(() => this.flow.handlePositions().target);
463
- this.handles = computed(() => {
464
- if (this.node.type === 'html-template') {
465
- return this.rawHandles();
466
- }
467
- return [
468
- new HandleModel({
469
- position: this.sourcePosition(),
470
- type: 'source',
471
- parentPosition: { x: 0, y: 0 },
472
- parentSize: this.size()
473
- }, this),
474
- new HandleModel({
475
- position: this.targetPosition(),
476
- type: 'target',
477
- parentPosition: { x: 0, y: 0 },
478
- parentSize: this.size()
479
- }, this),
480
- ];
481
- });
482
- this.rawHandles = signal([]);
416
+ this.handles = signal([]);
417
+ this.handles$ = toObservable(this.handles);
483
418
  this.draggable = true;
484
419
  // disabled for configuration for now
485
420
  this.magnetRadius = 20;
@@ -610,26 +545,30 @@ function getPointOnBezier(sourcePoint, targetPoint, controlPoint1, controlPoint2
610
545
  class EdgeModel {
611
546
  constructor(edge) {
612
547
  this.edge = edge;
548
+ this.source = signal(undefined);
549
+ this.target = signal(undefined);
613
550
  this.detached = computed(() => {
614
- if (!this.source || !this.target) {
551
+ const source = this.source();
552
+ const target = this.target();
553
+ if (!source || !target) {
615
554
  return true;
616
555
  }
617
556
  let existsSourceHandle = false;
618
557
  let existsTargetHandle = false;
619
558
  if (this.edge.sourceHandle) {
620
- existsSourceHandle = !!this.source.handles()
559
+ existsSourceHandle = !!source.handles()
621
560
  .find(handle => handle.rawHandle.id === this.edge.sourceHandle);
622
561
  }
623
562
  else {
624
- existsSourceHandle = !!this.source.handles()
563
+ existsSourceHandle = !!source.handles()
625
564
  .find(handle => handle.rawHandle.type === 'source');
626
565
  }
627
566
  if (this.edge.targetHandle) {
628
- existsTargetHandle = !!this.target.handles()
567
+ existsTargetHandle = !!target.handles()
629
568
  .find(handle => handle.rawHandle.id === this.edge.targetHandle);
630
569
  }
631
570
  else {
632
- existsTargetHandle = !!this.target.handles()
571
+ existsTargetHandle = !!target.handles()
633
572
  .find(handle => handle.rawHandle.type === 'target');
634
573
  }
635
574
  return !existsSourceHandle || !existsTargetHandle;
@@ -638,20 +577,20 @@ class EdgeModel {
638
577
  this.path = computed(() => {
639
578
  let source;
640
579
  if (this.edge.sourceHandle) {
641
- source = this.source.handles()
580
+ source = this.source()?.handles()
642
581
  .find(handle => handle.rawHandle.id === this.edge.sourceHandle);
643
582
  }
644
583
  else {
645
- source = this.source.handles()
584
+ source = this.source()?.handles()
646
585
  .find(handle => handle.rawHandle.type === 'source');
647
586
  }
648
587
  let target;
649
588
  if (this.edge.targetHandle) {
650
- target = this.target.handles()
589
+ target = this.target()?.handles()
651
590
  .find(handle => handle.rawHandle.id === this.edge.targetHandle);
652
591
  }
653
592
  else {
654
- target = this.target.handles()
593
+ target = this.target()?.handles()
655
594
  .find(handle => handle.rawHandle.type === 'target');
656
595
  }
657
596
  // TODO: don't like this
@@ -752,7 +691,7 @@ class EdgeChangesService {
752
691
  this.edgeDetachedChange$ = merge(toObservable(computed(() => {
753
692
  const nodes = this.entitiesService.nodes();
754
693
  const edges = untracked(this.entitiesService.edges);
755
- return edges.filter(({ source, target }) => !nodes.includes(source) || !nodes.includes(target));
694
+ return edges.filter(({ source, target }) => !nodes.includes(source()) || !nodes.includes(target()));
756
695
  })), toObservable(this.entitiesService.edges).pipe(switchMap((edges) => {
757
696
  return zip(...edges.map(e => e.detached$.pipe(map(() => e))));
758
697
  }), map((edges) => edges.filter(e => e.detached())),
@@ -825,14 +764,13 @@ class HandleService {
825
764
  createHandle(newHandle) {
826
765
  const node = this.node();
827
766
  if (node) {
828
- node.rawHandles.update(handles => [...handles, newHandle]);
767
+ node.handles.update(handles => [...handles, newHandle]);
829
768
  }
830
769
  }
831
770
  destroyHandle(handleToDestoy) {
832
771
  const node = this.node();
833
- // TODO: microtask
834
772
  if (node) {
835
- queueMicrotask(() => node.rawHandles.update(handles => handles.filter(handle => handle !== handleToDestoy)));
773
+ node.handles.update(handles => handles.filter(handle => handle !== handleToDestoy));
836
774
  }
837
775
  }
838
776
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
@@ -842,39 +780,203 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
842
780
  type: Injectable
843
781
  }] });
844
782
 
845
- 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 {
882
+ constructor() {
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 {
846
940
  constructor() {
941
+ super(...arguments);
847
942
  this.handleService = inject(HandleService);
848
- this.injector = inject(Injector);
943
+ this.zone = inject(NgZone);
849
944
  this.draggableService = inject(DraggableService);
850
945
  this.flowStatusService = inject(FlowStatusService);
851
946
  this.flowEntitiesService = inject(FlowEntitiesService);
852
947
  this.hostRef = inject(ElementRef);
853
948
  this.showMagnet = computed(() => this.flowStatusService.status().state === 'connection-start' ||
854
949
  this.flowStatusService.status().state === 'connection-validation');
855
- this.sourceHanldeState = signal('idle');
856
- this.targetHandleState = signal('idle');
950
+ this.subscription = new Subscription();
857
951
  }
858
952
  ngOnInit() {
859
953
  this.handleService.node.set(this.nodeModel);
860
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);
861
960
  }
862
961
  ngAfterViewInit() {
863
- // TODO remove microtask
864
- queueMicrotask(() => {
865
- if (this.nodeModel.node.type === 'default') {
866
- const { width, height } = this.nodeContentRef.nativeElement.getBBox();
867
- this.nodeModel.size.set({ width, height });
868
- }
869
- 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(() => {
870
970
  const width = this.htmlWrapperRef.nativeElement.clientWidth;
871
971
  const height = this.htmlWrapperRef.nativeElement.clientHeight;
872
972
  this.nodeModel.size.set({ width, height });
873
- }
874
- });
973
+ })).subscribe();
974
+ this.subscription.add(sub);
975
+ }
875
976
  }
876
977
  ngOnDestroy() {
877
978
  this.draggableService.destroy(this.hostRef.nativeElement);
979
+ this.subscription.unsubscribe();
878
980
  }
879
981
  startConnection(event, handle) {
880
982
  // ignore drag by stopping propagation
@@ -912,27 +1014,47 @@ class NodeComponent {
912
1014
  sourceHandle: sourceHandle.rawHandle.id,
913
1015
  targetHandle: targetHandle.rawHandle.id
914
1016
  });
915
- this.targetHandleState.set(valid ? 'valid' : 'invalid');
1017
+ targetHandle.state.set(valid ? 'valid' : 'invalid');
916
1018
  this.flowStatusService.setConnectionValidationStatus(valid, sourceNode, targetNode, sourceHandle, targetHandle);
917
1019
  }
918
1020
  }
919
1021
  /**
920
1022
  * TODO srp
921
1023
  */
922
- resetValidateTargetHandle() {
923
- this.targetHandleState.set('idle');
1024
+ resetValidateTargetHandle(targetHandle) {
1025
+ targetHandle.state.set('idle');
924
1026
  // drop back to start status
925
1027
  const status = this.flowStatusService.status();
926
1028
  if (status.state === 'connection-validation') {
927
1029
  this.flowStatusService.setConnectionStartStatus(status.payload.sourceNode, status.payload.sourceHandle);
928
1030
  }
929
1031
  }
930
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
931
- 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 }); }
932
1048
  }
1049
+ __decorate([
1050
+ Microtask
1051
+ ], NodeComponent.prototype, "ngAfterViewInit", null);
1052
+ __decorate([
1053
+ InjectionContext
1054
+ ], NodeComponent.prototype, "setInitialHandles", null);
933
1055
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, decorators: [{
934
1056
  type: Component,
935
- 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"] }]
936
1058
  }], propDecorators: { nodeModel: [{
937
1059
  type: Input
938
1060
  }], nodeHtmlTemplate: [{
@@ -943,7 +1065,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
943
1065
  }], htmlWrapperRef: [{
944
1066
  type: ViewChild,
945
1067
  args: ['htmlWrapper']
946
- }] } });
1068
+ }], ngAfterViewInit: [], setInitialHandles: [] } });
947
1069
 
948
1070
  class EdgeLabelComponent {
949
1071
  constructor() {
@@ -964,14 +1086,12 @@ class EdgeLabelComponent {
964
1086
  }
965
1087
  set point(point) { this.pointSignal.set(point); }
966
1088
  ngAfterViewInit() {
967
- queueMicrotask(() => {
968
- // this is a fix for visual artifact in chrome that for some reason adresses only for edge label.
969
- // the bug reproduces if edgeLabelWrapperRef size fully matched the size of parent foreignObject
970
- const MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME = 2;
971
- const width = this.edgeLabelWrapperRef.nativeElement.clientWidth + MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME;
972
- const height = this.edgeLabelWrapperRef.nativeElement.clientHeight + MAGIC_VALUE_TO_FIX_GLITCH_IN_CHROME;
973
- this.model.size.set({ width, height });
974
- });
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 });
975
1095
  }
976
1096
  getLabelContext() {
977
1097
  return {
@@ -984,6 +1104,9 @@ class EdgeLabelComponent {
984
1104
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeLabelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
985
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 }); }
986
1106
  }
1107
+ __decorate([
1108
+ Microtask
1109
+ ], EdgeLabelComponent.prototype, "ngAfterViewInit", null);
987
1110
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EdgeLabelComponent, decorators: [{
988
1111
  type: Component,
989
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"] }]
@@ -998,7 +1121,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
998
1121
  }], edgeLabelWrapperRef: [{
999
1122
  type: ViewChild,
1000
1123
  args: ['edgeLabelWrapper']
1001
- }] } });
1124
+ }], ngAfterViewInit: [] } });
1002
1125
 
1003
1126
  class EdgeComponent {
1004
1127
  constructor() {
@@ -1369,10 +1492,10 @@ class VflowComponent {
1369
1492
  }
1370
1493
  // #endregion
1371
1494
  trackNodes(idx, { node }) {
1372
- return node.id;
1495
+ return node;
1373
1496
  }
1374
1497
  trackEdges(idx, { edge }) {
1375
- return edge.id;
1498
+ return edge;
1376
1499
  }
1377
1500
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1378
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: [
@@ -1435,39 +1558,32 @@ function bindFlowToNodes(flow, nodes) {
1435
1558
  nodes.forEach(n => n.bindFlow(flow));
1436
1559
  }
1437
1560
 
1438
- class HandleComponent {
1561
+ class HandleComponent extends WithInjectorDirective {
1439
1562
  constructor() {
1563
+ super(...arguments);
1440
1564
  this.handleService = inject(HandleService);
1441
1565
  this.element = inject(ElementRef).nativeElement;
1442
1566
  }
1443
1567
  ngOnInit() {
1444
- queueMicrotask(() => {
1445
- const { width, height, x, y } = this.parentRect();
1446
- this.model = new HandleModel({
1447
- position: this.position,
1448
- type: this.type,
1449
- id: this.id,
1450
- parentPosition: { x, y },
1451
- parentSize: { width, height }
1452
- }, this.handleService.node());
1453
- this.handleService.createHandle(this.model);
1454
- });
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());
1455
1577
  }
1456
1578
  ngOnDestroy() {
1457
1579
  this.handleService.destroyHandle(this.model);
1458
1580
  }
1459
- parentRect() {
1460
- const parent = this.element.parentElement;
1461
- return {
1462
- x: parent.offsetLeft,
1463
- y: parent.offsetTop,
1464
- width: parent.clientWidth,
1465
- height: parent.clientHeight
1466
- };
1467
- }
1468
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1469
- 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: "" }); }
1470
1583
  }
1584
+ __decorate([
1585
+ InjectionContext
1586
+ ], HandleComponent.prototype, "ngOnInit", null);
1471
1587
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, decorators: [{
1472
1588
  type: Component,
1473
1589
  args: [{ selector: 'handle', template: "" }]
@@ -1479,7 +1595,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1479
1595
  args: [{ required: true }]
1480
1596
  }], id: [{
1481
1597
  type: Input
1482
- }] } });
1598
+ }], template: [{
1599
+ type: Input
1600
+ }], ngOnInit: [] } });
1483
1601
 
1484
1602
  const components = [
1485
1603
  VflowComponent,
@@ -1495,6 +1613,7 @@ const directives = [
1495
1613
  MapContextDirective,
1496
1614
  RootSvgReferenceDirective,
1497
1615
  RootSvgContextDirective,
1616
+ HandleSizeControllerDirective
1498
1617
  ];
1499
1618
  const templateDirectives = [
1500
1619
  NodeHtmlTemplateDirective,
@@ -1514,7 +1633,8 @@ class VflowModule {
1514
1633
  DefsComponent, SpacePointContextDirective,
1515
1634
  MapContextDirective,
1516
1635
  RootSvgReferenceDirective,
1517
- RootSvgContextDirective, NodeHtmlTemplateDirective,
1636
+ RootSvgContextDirective,
1637
+ HandleSizeControllerDirective, NodeHtmlTemplateDirective,
1518
1638
  EdgeLabelHtmlTemplateDirective,
1519
1639
  EdgeTemplateDirective,
1520
1640
  ConnectionTemplateDirective,