ngx-vflow 0.1.14 → 0.2.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 (30) hide show
  1. package/esm2022/lib/vflow/components/connection/connection.component.mjs +27 -7
  2. package/esm2022/lib/vflow/components/handle/handle.component.mjs +49 -0
  3. package/esm2022/lib/vflow/components/node/node.component.mjs +26 -60
  4. package/esm2022/lib/vflow/components/vflow/vflow.component.mjs +5 -8
  5. package/esm2022/lib/vflow/directives/connection-controller.directive.mjs +5 -3
  6. package/esm2022/lib/vflow/interfaces/connection.interface.mjs +1 -1
  7. package/esm2022/lib/vflow/models/edge.model.mjs +32 -5
  8. package/esm2022/lib/vflow/models/flow.model.mjs +3 -1
  9. package/esm2022/lib/vflow/models/handle.model.mjs +47 -0
  10. package/esm2022/lib/vflow/models/node.model.mjs +21 -60
  11. package/esm2022/lib/vflow/services/flow-status.service.mjs +7 -7
  12. package/esm2022/lib/vflow/services/handle.service.mjs +19 -0
  13. package/esm2022/lib/vflow/types/handle-type.type.mjs +2 -0
  14. package/esm2022/lib/vflow/vflow.module.mjs +7 -2
  15. package/esm2022/public-api.mjs +2 -1
  16. package/fesm2022/ngx-vflow.mjs +229 -143
  17. package/fesm2022/ngx-vflow.mjs.map +1 -1
  18. package/lib/vflow/components/handle/handle.component.d.ts +23 -0
  19. package/lib/vflow/components/node/node.component.d.ts +8 -25
  20. package/lib/vflow/components/vflow/vflow.component.d.ts +3 -4
  21. package/lib/vflow/interfaces/connection.interface.d.ts +2 -0
  22. package/lib/vflow/models/flow.model.d.ts +2 -0
  23. package/lib/vflow/models/handle.model.d.ts +24 -0
  24. package/lib/vflow/models/node.model.d.ts +4 -40
  25. package/lib/vflow/services/flow-status.service.d.ts +9 -3
  26. package/lib/vflow/services/handle.service.d.ts +22 -0
  27. package/lib/vflow/types/handle-type.type.d.ts +1 -0
  28. package/lib/vflow/vflow.module.d.ts +9 -8
  29. package/package.json +1 -1
  30. package/public-api.d.ts +1 -0
@@ -1,7 +1,7 @@
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, Component, ChangeDetectionStrategy, ViewChild, HostListener, Injector, runInInjectionContext, ContentChild, NgModule } 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';
5
5
  import { select } from 'd3-selection';
6
6
  import { zoomIdentity, zoom } from 'd3-zoom';
7
7
  import { drag } from 'd3-drag';
@@ -246,6 +246,8 @@ class FlowModel {
246
246
  constructor() {
247
247
  /**
248
248
  * Global setting with handle positions. Nodes derive this value
249
+ *
250
+ * @deprecated
249
251
  */
250
252
  this.handlePositions = signal({ source: 'right', target: 'left' });
251
253
  /**
@@ -264,14 +266,14 @@ class FlowStatusService {
264
266
  setIdleStatus() {
265
267
  this.status.set({ state: 'idle', payload: null });
266
268
  }
267
- setConnectionStartStatus(sourceNode) {
268
- this.status.set({ state: 'connection-start', payload: { sourceNode } });
269
+ setConnectionStartStatus(sourceNode, sourceHandle) {
270
+ this.status.set({ state: 'connection-start', payload: { sourceNode, sourceHandle } });
269
271
  }
270
- setConnectionValidationStatus(sourceNode, targetNode, valid) {
271
- this.status.set({ state: 'connection-validation', payload: { sourceNode, targetNode, valid } });
272
+ setConnectionValidationStatus(valid, sourceNode, targetNode, sourceHandle, targetHandle) {
273
+ this.status.set({ state: 'connection-validation', payload: { sourceNode, targetNode, sourceHandle, targetHandle, valid } });
272
274
  }
273
- setConnectionEndStatus(sourceNode, targetNode) {
274
- this.status.set({ state: 'connection-end', payload: { sourceNode, targetNode } });
275
+ setConnectionEndStatus(sourceNode, targetNode, sourceHandle, targetHandle) {
276
+ this.status.set({ state: 'connection-end', payload: { sourceNode, targetNode, sourceHandle, targetHandle } });
275
277
  }
276
278
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowStatusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
277
279
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FlowStatusService }); }
@@ -375,9 +377,11 @@ class ConnectionControllerDirective {
375
377
  const targetModel = status.payload.targetNode;
376
378
  const source = sourceModel.node.id;
377
379
  const target = targetModel.node.id;
380
+ const sourceHandle = status.payload.sourceHandle.rawHandle.id;
381
+ const targetHandle = status.payload.targetHandle.rawHandle.id;
378
382
  const connection = this.flowEntitiesService.connection();
379
- if (connection.validator({ source, target })) {
380
- this.onConnect.emit({ source, target });
383
+ if (connection.validator({ source, target, sourceHandle, targetHandle })) {
384
+ this.onConnect.emit({ source, target, sourceHandle, targetHandle });
381
385
  }
382
386
  }
383
387
  }, { allowSignalWrites: true });
@@ -395,76 +399,82 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
395
399
  type: Output
396
400
  }] } });
397
401
 
398
- class NodeModel {
399
- constructor(node) {
400
- this.node = node;
401
- this.point = signal({ x: 0, y: 0 });
402
- this.point$ = toObservable(this.point);
403
- this.size = signal({ width: 0, height: 0 });
404
- this.pointTransform = computed(() => `translate(${this.point().x}, ${this.point().y})`);
405
- this.sourceOffset = computed(() => {
406
- const { width, height } = this.size();
407
- switch (this.sourcePosition()) {
408
- case 'left': return { x: 0, y: height / 2 };
409
- case 'right': return { x: width, y: height / 2 };
410
- case 'top': return { x: width / 2, y: 0 };
411
- case 'bottom': return { x: width / 2, y: height };
412
- }
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)
413
410
  });
414
- this.targetOffset = computed(() => {
415
- const { width, height } = this.size();
416
- switch (this.targetPosition()) {
417
- case 'left': return { x: 0, y: (height / 2) };
418
- case 'right': return { x: width, y: height / 2 };
419
- case 'top': return { x: width / 2, y: 0 };
420
- case 'bottom': return { x: width / 2, y: height };
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
+ };
421
429
  }
422
430
  });
423
- this.sourcePointAbsolute = computed(() => {
424
- return {
425
- x: this.point().x + this.sourceOffset().x + this.sourceHandleOffset().x,
426
- y: this.point().y + this.sourceOffset().y + this.sourceHandleOffset().y
427
- };
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
+ }
428
438
  });
429
- this.targetPointAbsolute = computed(() => {
439
+ this.pointAbsolute = computed(() => {
430
440
  return {
431
- x: this.point().x + this.targetOffset().x + this.targetHandleOffset().x,
432
- y: this.point().y + this.targetOffset().y + this.targetHandleOffset().y
441
+ x: this.parentNode.point().x + this.offset().x + this.sizeOffset().x,
442
+ y: this.parentNode.point().y + this.offset().y + this.sizeOffset().y,
433
443
  };
434
444
  });
445
+ }
446
+ }
447
+
448
+ class NodeModel {
449
+ constructor(node) {
450
+ this.node = node;
451
+ this.point = signal({ x: 0, y: 0 });
452
+ this.point$ = toObservable(this.point);
453
+ this.size = signal({ width: 0, height: 0 });
454
+ this.pointTransform = computed(() => `translate(${this.point().x}, ${this.point().y})`);
435
455
  // Now source and handle positions derived from parent flow
436
456
  this.sourcePosition = computed(() => this.flow.handlePositions().source);
437
457
  this.targetPosition = computed(() => this.flow.handlePositions().target);
438
- this.sourceHandleSize = signal({ width: 0, height: 0 });
439
- this.targetHandleSize = signal({ width: 0, height: 0 });
440
- this.sourceHandleOffset = computed(() => {
441
- switch (this.sourcePosition()) {
442
- case 'left': return { x: -(this.sourceHandleSize().width / 2), y: 0 };
443
- case 'right': return { x: this.sourceHandleSize().width / 2, y: 0 };
444
- case 'top': return { x: 0, y: -(this.sourceHandleSize().height / 2) };
445
- case 'bottom': return { x: 0, y: this.sourceHandleSize().height / 2 };
446
- }
447
- });
448
- this.targetHandleOffset = computed(() => {
449
- switch (this.targetPosition()) {
450
- case 'left': return { x: -(this.targetHandleSize().width / 2), y: 0 };
451
- case 'right': return { x: this.targetHandleSize().width / 2, y: 0 };
452
- case 'top': return { x: 0, y: -(this.targetHandleSize().height / 2) };
453
- case 'bottom': return { x: 0, y: this.targetHandleSize().height / 2 };
458
+ this.handles = computed(() => {
459
+ if (this.node.type === 'html-template') {
460
+ return this.rawHandles().map((handle => new HandleModel(handle, this)));
454
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
+ ];
455
476
  });
456
- this.sourceOffsetAligned = computed(() => {
457
- return {
458
- x: this.sourceOffset().x - (this.sourceHandleSize().width / 2),
459
- y: this.sourceOffset().y - (this.sourceHandleSize().height / 2),
460
- };
461
- });
462
- this.targetOffsetAligned = computed(() => {
463
- return {
464
- x: this.targetOffset().x - (this.targetHandleSize().width / 2),
465
- y: this.targetOffset().y - (this.targetHandleSize().height / 2),
466
- };
467
- });
477
+ this.rawHandles = signal([]);
468
478
  this.draggable = true;
469
479
  // disabled for configuration for now
470
480
  this.magnetRadius = 20;
@@ -596,13 +606,40 @@ class EdgeModel {
596
606
  constructor(edge) {
597
607
  this.edge = edge;
598
608
  this.path = computed(() => {
599
- const source = this.source.sourcePointAbsolute();
600
- const target = this.target.targetPointAbsolute();
609
+ let source;
610
+ if (this.edge.sourceHandle) {
611
+ source = this.source.handles()
612
+ .find(handle => handle.rawHandle.id === this.edge.sourceHandle);
613
+ }
614
+ else {
615
+ source = this.source.handles()
616
+ .find(handle => handle.rawHandle.type === 'source');
617
+ }
618
+ let target;
619
+ if (this.edge.targetHandle) {
620
+ target = this.target.handles()
621
+ .find(handle => handle.rawHandle.id === this.edge.targetHandle);
622
+ }
623
+ else {
624
+ target = this.target.handles()
625
+ .find(handle => handle.rawHandle.type === 'target');
626
+ }
627
+ // TODO: don't like this
628
+ if (!source || !target) {
629
+ return {
630
+ path: '',
631
+ points: {
632
+ start: { x: 0, y: 0 },
633
+ center: { x: 0, y: 0 },
634
+ end: { x: 0, y: 0 }
635
+ }
636
+ };
637
+ }
601
638
  switch (this.curve) {
602
639
  case 'straight':
603
- return straightPath(source, target, this.usingPoints);
640
+ return straightPath(source.pointAbsolute(), target.pointAbsolute(), this.usingPoints);
604
641
  case 'bezier':
605
- return bezierPath(source, target, this.source.sourcePosition(), this.target.targetPosition(), this.usingPoints);
642
+ return bezierPath(source.pointAbsolute(), target.pointAbsolute(), source.rawHandle.position, target.rawHandle.position, this.usingPoints);
606
643
  }
607
644
  });
608
645
  this.edgeLabels = {};
@@ -741,21 +778,40 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
741
778
  type: Output
742
779
  }] } });
743
780
 
781
+ class HandleService {
782
+ constructor() {
783
+ this.handles = signal([]);
784
+ }
785
+ createHandle(newHandle) {
786
+ this.handles.update(handles => [...handles, newHandle]);
787
+ }
788
+ destroyHandle(handleToDestoy) {
789
+ this.handles.update(handles => handles.filter(handle => handle !== handleToDestoy));
790
+ }
791
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
792
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService }); }
793
+ }
794
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleService, decorators: [{
795
+ type: Injectable
796
+ }] });
797
+
744
798
  class NodeComponent {
745
799
  constructor() {
800
+ this.handleService = inject(HandleService);
801
+ this.injector = inject(Injector);
746
802
  this.draggableService = inject(DraggableService);
747
803
  this.flowStatusService = inject(FlowStatusService);
748
804
  this.flowEntitiesService = inject(FlowEntitiesService);
749
805
  this.hostRef = inject(ElementRef);
750
806
  this.showMagnet = computed(() => this.flowStatusService.status().state === 'connection-start' ||
751
807
  this.flowStatusService.status().state === 'connection-validation');
752
- this.defaultHandleStrokeWidth = 2;
753
808
  this.sourceHanldeState = signal('idle');
754
809
  this.targetHandleState = signal('idle');
755
- this.sourceHanldeStateReadonly = this.sourceHanldeState.asReadonly();
756
- this.targetHanldeStateReadonly = this.targetHandleState.asReadonly();
757
810
  }
758
811
  ngOnInit() {
812
+ runInInjectionContext(this.injector, () => {
813
+ effect(() => this.nodeModel.rawHandles.set(this.handleService.handles()), { allowSignalWrites: true });
814
+ });
759
815
  this.draggableService.toggleDraggable(this.hostRef.nativeElement, this.nodeModel);
760
816
  }
761
817
  ngAfterViewInit() {
@@ -770,26 +826,26 @@ class NodeComponent {
770
826
  const height = this.htmlWrapperRef.nativeElement.clientHeight;
771
827
  this.nodeModel.size.set({ width, height });
772
828
  }
773
- this.setSourceHandleSize();
774
- this.setTargetHandleSize();
775
829
  });
776
830
  }
777
831
  ngOnDestroy() {
778
832
  this.draggableService.destroy(this.hostRef.nativeElement);
779
833
  }
780
- startConnection(event) {
834
+ startConnection(event, handle) {
781
835
  // ignore drag by stopping propagation
782
836
  event.stopPropagation();
783
- this.flowStatusService.setConnectionStartStatus(this.nodeModel);
837
+ this.flowStatusService.setConnectionStartStatus(this.nodeModel, handle);
784
838
  }
785
839
  endConnection() {
786
840
  const status = this.flowStatusService.status();
787
841
  if (status.state === 'connection-validation') {
788
842
  const sourceNode = status.payload.sourceNode;
789
843
  const targetNode = this.nodeModel;
844
+ const sourceHandle = status.payload.sourceHandle;
845
+ const targetHandle = status.payload.targetHandle;
790
846
  batchStatusChanges(
791
847
  // call to create connection
792
- () => this.flowStatusService.setConnectionEndStatus(sourceNode, targetNode),
848
+ () => this.flowStatusService.setConnectionEndStatus(sourceNode, targetNode, sourceHandle, targetHandle),
793
849
  // when connection created, we need go back to idle status
794
850
  () => this.flowStatusService.setIdleStatus());
795
851
  }
@@ -797,16 +853,22 @@ class NodeComponent {
797
853
  /**
798
854
  * TODO srp
799
855
  */
800
- validateTargetHandle() {
856
+ validateTargetHandle(targetHandle) {
801
857
  const status = this.flowStatusService.status();
802
858
  if (status.state === 'connection-start') {
803
859
  const sourceNode = status.payload.sourceNode;
804
- const targetNode = this.nodeModel;
860
+ const sourceHandle = status.payload.sourceHandle;
805
861
  const source = sourceNode.node.id;
862
+ const targetNode = this.nodeModel;
806
863
  const target = targetNode.node.id;
807
- const valid = this.flowEntitiesService.connection().validator({ source, target });
864
+ const valid = this.flowEntitiesService.connection().validator({
865
+ source,
866
+ target,
867
+ sourceHandle: sourceHandle.rawHandle.id,
868
+ targetHandle: targetHandle.rawHandle.id
869
+ });
808
870
  this.targetHandleState.set(valid ? 'valid' : 'invalid');
809
- this.flowStatusService.setConnectionValidationStatus(sourceNode, targetNode, valid);
871
+ this.flowStatusService.setConnectionValidationStatus(valid, sourceNode, targetNode, sourceHandle, targetHandle);
810
872
  }
811
873
  }
812
874
  /**
@@ -817,68 +879,25 @@ class NodeComponent {
817
879
  // drop back to start status
818
880
  const status = this.flowStatusService.status();
819
881
  if (status.state === 'connection-validation') {
820
- this.flowStatusService.setConnectionStartStatus(status.payload.sourceNode);
821
- }
822
- }
823
- getHandleContext(type) {
824
- if (type === 'source') {
825
- return {
826
- $implicit: {
827
- point: this.nodeModel.sourceOffset,
828
- alignedPoint: this.nodeModel.sourceOffsetAligned,
829
- state: this.sourceHanldeStateReadonly
830
- }
831
- };
882
+ this.flowStatusService.setConnectionStartStatus(status.payload.sourceNode, status.payload.sourceHandle);
832
883
  }
833
- return {
834
- $implicit: {
835
- point: this.nodeModel.targetOffset,
836
- alignedPoint: this.nodeModel.targetOffsetAligned,
837
- state: this.targetHanldeStateReadonly
838
- }
839
- };
840
- }
841
- setSourceHandleSize() {
842
- // if handle template provided, we don't know its stroke so it's 0
843
- const strokeWidth = this.handleTemplate ? 0 : (2 * this.defaultHandleStrokeWidth);
844
- const sourceBox = this.sourceHandleRef.nativeElement.getBBox({ stroke: true });
845
- this.nodeModel.sourceHandleSize.set({
846
- width: sourceBox.width + strokeWidth,
847
- height: sourceBox.height + strokeWidth
848
- });
849
- }
850
- setTargetHandleSize() {
851
- const strokeWidth = this.handleTemplate ? 0 : (2 * this.defaultHandleStrokeWidth);
852
- const targetBox = this.targetHandleRef.nativeElement.getBBox({ stroke: true });
853
- this.nodeModel.targetHandleSize.set({
854
- width: targetBox.width + strokeWidth,
855
- height: targetBox.height + strokeWidth
856
- });
857
884
  }
858
885
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
859
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: NodeComponent, selector: "g[node]", inputs: { nodeModel: "nodeModel", nodeHtmlTemplate: "nodeHtmlTemplate", handleTemplate: "handleTemplate" }, viewQueries: [{ propertyName: "nodeContentRef", first: true, predicate: ["nodeContent"], descendants: true }, { propertyName: "htmlWrapperRef", first: true, predicate: ["htmlWrapper"], descendants: true }, { propertyName: "sourceHandleRef", first: true, predicate: ["sourceHandle"], descendants: true }, { propertyName: "targetHandleRef", first: true, predicate: ["targetHandle"], descendants: true }], ngImport: i0, template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n #nodeContent\n width=\"100\"\n height=\"50\"\n>\n <div class=\"default-node\" [innerHTML]=\"nodeModel.node.text ?? ''\"></div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n *ngTemplateOutlet=\"nodeHtmlTemplate; context: { $implicit: { node: nodeModel.node } }\"\n />\n </div>\n</svg:foreignObject>\n\n<ng-container *ngIf=\"handleTemplate\">\n <svg:g #sourceHandle (mousedown)=\"startConnection($event)\">\n <ng-container *ngTemplateOutlet=\"handleTemplate; context: getHandleContext('source')\" />\n </svg:g>\n\n <svg:g #targetHandle>\n <ng-container *ngTemplateOutlet=\"handleTemplate; context: getHandleContext('target')\" />\n </svg:g>\n</ng-container>\n\n<ng-container *ngIf=\"!handleTemplate\">\n <svg:circle\n #sourceHandle\n [attr.cx]=\"nodeModel.sourceOffset().x\"\n [attr.cy]=\"nodeModel.sourceOffset().y\"\n [attr.stroke-width]=\"defaultHandleStrokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mousedown)=\"startConnection($event)\"\n />\n\n <svg:circle\n #targetHandle\n [attr.cx]=\"nodeModel.targetOffset().x\"\n [attr.cy]=\"nodeModel.targetOffset().y\"\n [attr.stroke-width]=\"defaultHandleStrokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mouseup)=\"endConnection()\"\n />\n</ng-container>\n\n<svg:circle\n *ngIf=\"showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"nodeModel.targetOffset().x\"\n [attr.cy]=\"nodeModel.targetOffset().y\"\n (mouseup)=\"endConnection(); resetValidateTargetHandle()\"\n (mouseover)=\"validateTargetHandle()\"\n (mouseout)=\"resetValidateTargetHandle()\"\n/>\n\n\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{max-width:100px;max-height:100px;width:100px;height:50px;border:2px solid black;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
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 }); }
860
887
  }
861
888
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, decorators: [{
862
889
  type: Component,
863
- args: [{ selector: 'g[node]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n #nodeContent\n width=\"100\"\n height=\"50\"\n>\n <div class=\"default-node\" [innerHTML]=\"nodeModel.node.text ?? ''\"></div>\n</svg:foreignObject>\n\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeHtmlTemplate\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n>\n <div #htmlWrapper class=\"wrapper\">\n <ng-container\n *ngTemplateOutlet=\"nodeHtmlTemplate; context: { $implicit: { node: nodeModel.node } }\"\n />\n </div>\n</svg:foreignObject>\n\n<ng-container *ngIf=\"handleTemplate\">\n <svg:g #sourceHandle (mousedown)=\"startConnection($event)\">\n <ng-container *ngTemplateOutlet=\"handleTemplate; context: getHandleContext('source')\" />\n </svg:g>\n\n <svg:g #targetHandle>\n <ng-container *ngTemplateOutlet=\"handleTemplate; context: getHandleContext('target')\" />\n </svg:g>\n</ng-container>\n\n<ng-container *ngIf=\"!handleTemplate\">\n <svg:circle\n #sourceHandle\n [attr.cx]=\"nodeModel.sourceOffset().x\"\n [attr.cy]=\"nodeModel.sourceOffset().y\"\n [attr.stroke-width]=\"defaultHandleStrokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mousedown)=\"startConnection($event)\"\n />\n\n <svg:circle\n #targetHandle\n [attr.cx]=\"nodeModel.targetOffset().x\"\n [attr.cy]=\"nodeModel.targetOffset().y\"\n [attr.stroke-width]=\"defaultHandleStrokeWidth\"\n r=\"5\"\n stroke=\"white\"\n fill=\"black\"\n (mouseup)=\"endConnection()\"\n />\n</ng-container>\n\n<svg:circle\n *ngIf=\"showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"nodeModel.targetOffset().x\"\n [attr.cy]=\"nodeModel.targetOffset().y\"\n (mouseup)=\"endConnection(); resetValidateTargetHandle()\"\n (mouseover)=\"validateTargetHandle()\"\n (mouseout)=\"resetValidateTargetHandle()\"\n/>\n\n\n", styles: [".wrapper{width:max-content}.magnet{opacity:0}.default-node{max-width:100px;max-height:100px;width:100px;height:50px;border:2px solid black;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}\n"] }]
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"] }]
864
891
  }], propDecorators: { nodeModel: [{
865
892
  type: Input
866
893
  }], nodeHtmlTemplate: [{
867
894
  type: Input
868
- }], handleTemplate: [{
869
- type: Input
870
895
  }], nodeContentRef: [{
871
896
  type: ViewChild,
872
897
  args: ['nodeContent']
873
898
  }], htmlWrapperRef: [{
874
899
  type: ViewChild,
875
900
  args: ['htmlWrapper']
876
- }], sourceHandleRef: [{
877
- type: ViewChild,
878
- args: ['sourceHandle']
879
- }], targetHandleRef: [{
880
- type: ViewChild,
881
- args: ['targetHandle']
882
901
  }] } });
883
902
 
884
903
  class EdgeLabelComponent {
@@ -1004,23 +1023,31 @@ class ConnectionComponent {
1004
1023
  this.path = computed(() => {
1005
1024
  const status = this.flowStatusService.status();
1006
1025
  if (status.state === 'connection-start') {
1007
- const sourceNode = status.payload.sourceNode;
1008
- const sourcePoint = sourceNode.sourcePointAbsolute();
1026
+ const sourceHandle = status.payload.sourceHandle;
1027
+ const sourcePoint = sourceHandle.pointAbsolute();
1028
+ const sourcePosition = sourceHandle.rawHandle.position;
1009
1029
  const targetPoint = this.spacePointContext.svgCurrentSpacePoint();
1030
+ const targetPosition = getOppositePostion(sourceHandle.rawHandle.position);
1010
1031
  switch (this.model.curve) {
1011
1032
  case 'straight': return straightPath(sourcePoint, targetPoint).path;
1012
- case 'bezier': return bezierPath(sourcePoint, targetPoint, sourceNode.sourcePosition(), sourceNode.targetPosition()).path;
1033
+ case 'bezier': return bezierPath(sourcePoint, targetPoint, sourcePosition, targetPosition).path;
1013
1034
  }
1014
1035
  }
1015
1036
  if (status.state === 'connection-validation') {
1016
- const sourcePoint = status.payload.sourceNode.sourcePointAbsolute();
1037
+ const sourceHandle = status.payload.sourceHandle;
1038
+ const sourcePoint = sourceHandle.pointAbsolute();
1039
+ const sourcePosition = sourceHandle.rawHandle.position;
1040
+ const targetHandle = status.payload.targetHandle;
1017
1041
  // ignore magnet if validation failed
1018
1042
  const targetPoint = status.payload.valid
1019
- ? status.payload.targetNode.targetPointAbsolute()
1043
+ ? targetHandle.pointAbsolute()
1020
1044
  : this.spacePointContext.svgCurrentSpacePoint();
1045
+ const targetPosition = status.payload.valid
1046
+ ? targetHandle.rawHandle.position
1047
+ : getOppositePostion(sourceHandle.rawHandle.position);
1021
1048
  switch (this.model.curve) {
1022
1049
  case 'straight': return straightPath(sourcePoint, targetPoint).path;
1023
- case 'bezier': return bezierPath(sourcePoint, targetPoint, status.payload.sourceNode.sourcePosition(), status.payload.sourceNode.targetPosition()).path;
1050
+ case 'bezier': return bezierPath(sourcePoint, targetPoint, sourcePosition, targetPosition).path;
1024
1051
  }
1025
1052
  }
1026
1053
  return null;
@@ -1088,6 +1115,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1088
1115
  }], template: [{
1089
1116
  type: Input
1090
1117
  }] } });
1118
+ function getOppositePostion(position) {
1119
+ switch (position) {
1120
+ case 'top':
1121
+ return 'bottom';
1122
+ case 'bottom':
1123
+ return 'top';
1124
+ case 'left':
1125
+ return 'right';
1126
+ case 'right':
1127
+ return 'left';
1128
+ }
1129
+ }
1091
1130
 
1092
1131
  class DefsComponent {
1093
1132
  constructor() {
@@ -1208,7 +1247,7 @@ class VflowComponent {
1208
1247
  * For example, if you want to archieve right to left direction
1209
1248
  * then you need to pass these positions { source: 'left', target: 'right' }
1210
1249
  *
1211
- * ! Be carefult using this field, it may depricate in future releases !
1250
+ * @deprecated
1212
1251
  */
1213
1252
  set handlePositions(handlePositions) {
1214
1253
  this.flowModel.handlePositions.set(handlePositions);
@@ -1292,7 +1331,7 @@ class VflowComponent {
1292
1331
  FlowEntitiesService,
1293
1332
  NodesChangeService,
1294
1333
  EdgeChangesService
1295
- ], queries: [{ propertyName: "nodeHtmlDirective", first: true, predicate: NodeHtmlTemplateDirective, descendants: true }, { propertyName: "edgeTemplateDirective", first: true, predicate: EdgeTemplateDirective, descendants: true }, { propertyName: "edgeLabelHtmlDirective", first: true, predicate: EdgeLabelHtmlTemplateDirective, descendants: true }, { propertyName: "connectionTemplateDirective", first: true, predicate: ConnectionTemplateDirective, descendants: true }, { propertyName: "handleTemplateDirective", first: true, predicate: HandleTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "mapContext", first: true, predicate: MapContextDirective, descendants: true }], hostDirectives: [{ directive: ConnectionControllerDirective, outputs: ["onConnect", "onConnect"] }, { directive: ChangesControllerDirective, outputs: ["onNodesChange", "onNodesChange", "onEdgesChange", "onEdgesChange"] }], ngImport: i0, template: "<svg:svg\n rootSvgRef\n rootSvgContext\n class=\"root-svg\"\n #flow\n [style.backgroundColor]=\"background\"\n [attr.width]=\"flowModel.flowWidth()\"\n [attr.height]=\"flowModel.flowHeight()\"\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <svg:g\n mapContext\n spacePointContext\n [minZoom]=\"minZoom\"\n [maxZoom]=\"maxZoom\"\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels; trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nodeModels; trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeHtmlTemplate]=\"nodeHtmlDirective?.templateRef\"\n [handleTemplate]=\"handleTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </svg:g>\n\n</svg:svg>\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}: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", "handleTemplate"] }, { kind: "component", type: EdgeComponent, selector: "g[edge]", inputs: ["model", "edgeTemplate", "edgeLabelHtmlTemplate"] }, { kind: "component", type: ConnectionComponent, selector: "g[connection]", inputs: ["model", "template"] }, { kind: "component", type: DefsComponent, selector: "defs[flowDefs]", inputs: ["markers"] }, { kind: "directive", type: SpacePointContextDirective, selector: "g[spacePointContext]" }, { kind: "directive", type: MapContextDirective, selector: "g[mapContext]", inputs: ["minZoom", "maxZoom"] }, { kind: "directive", type: RootSvgReferenceDirective, selector: "svg[rootSvgRef]" }, { kind: "directive", type: RootSvgContextDirective, selector: "svg[rootSvgContext]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1334
+ ], 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", "onEdgesChange", "onEdgesChange"] }], ngImport: i0, template: "<svg:svg\n rootSvgRef\n rootSvgContext\n class=\"root-svg\"\n #flow\n [style.backgroundColor]=\"background\"\n [attr.width]=\"flowModel.flowWidth()\"\n [attr.height]=\"flowModel.flowHeight()\"\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <svg:g\n mapContext\n spacePointContext\n [minZoom]=\"minZoom\"\n [maxZoom]=\"maxZoom\"\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels; trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nodeModels; trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeHtmlTemplate]=\"nodeHtmlDirective?.templateRef\"\n [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]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1296
1335
  }
1297
1336
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, decorators: [{
1298
1337
  type: Component,
@@ -1306,7 +1345,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1306
1345
  ], hostDirectives: [
1307
1346
  connectionControllerHostDirective,
1308
1347
  changesControllerHostDirective
1309
- ], template: "<svg:svg\n rootSvgRef\n rootSvgContext\n class=\"root-svg\"\n #flow\n [style.backgroundColor]=\"background\"\n [attr.width]=\"flowModel.flowWidth()\"\n [attr.height]=\"flowModel.flowHeight()\"\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <svg:g\n mapContext\n spacePointContext\n [minZoom]=\"minZoom\"\n [maxZoom]=\"maxZoom\"\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels; trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nodeModels; trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeHtmlTemplate]=\"nodeHtmlDirective?.templateRef\"\n [handleTemplate]=\"handleTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </svg:g>\n\n</svg:svg>\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}\n"] }]
1348
+ ], template: "<svg:svg\n rootSvgRef\n rootSvgContext\n class=\"root-svg\"\n #flow\n [style.backgroundColor]=\"background\"\n [attr.width]=\"flowModel.flowWidth()\"\n [attr.height]=\"flowModel.flowHeight()\"\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <svg:g\n mapContext\n spacePointContext\n [minZoom]=\"minZoom\"\n [maxZoom]=\"maxZoom\"\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <!-- Edges -->\n <svg:g\n *ngFor=\"let model of edgeModels; trackBy: trackEdges\"\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective?.templateRef\"\n />\n\n <!-- Nodes -->\n <svg:g\n *ngFor=\"let model of nodeModels; trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeHtmlTemplate]=\"nodeHtmlDirective?.templateRef\"\n [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"] }]
1310
1349
  }], propDecorators: { view: [{
1311
1350
  type: Input
1312
1351
  }], minZoom: [{
@@ -1337,9 +1376,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1337
1376
  }], connectionTemplateDirective: [{
1338
1377
  type: ContentChild,
1339
1378
  args: [ConnectionTemplateDirective]
1340
- }], handleTemplateDirective: [{
1341
- type: ContentChild,
1342
- args: [HandleTemplateDirective]
1343
1379
  }], mapContext: [{
1344
1380
  type: ViewChild,
1345
1381
  args: [MapContextDirective]
@@ -1348,12 +1384,59 @@ function bindFlowToNodes(flow, nodes) {
1348
1384
  nodes.forEach(n => n.bindFlow(flow));
1349
1385
  }
1350
1386
 
1387
+ class HandleComponent {
1388
+ constructor() {
1389
+ this.handleService = inject(HandleService);
1390
+ this.element = inject(ElementRef).nativeElement;
1391
+ }
1392
+ 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
+ });
1403
+ }
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
+ };
1416
+ }
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: "" }); }
1419
+ }
1420
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, decorators: [{
1421
+ type: Component,
1422
+ args: [{ selector: 'handle', template: "" }]
1423
+ }], propDecorators: { position: [{
1424
+ type: Input,
1425
+ args: [{ required: true }]
1426
+ }], type: [{
1427
+ type: Input,
1428
+ args: [{ required: true }]
1429
+ }], id: [{
1430
+ type: Input
1431
+ }] } });
1432
+
1351
1433
  const components = [
1352
1434
  VflowComponent,
1353
1435
  NodeComponent,
1354
1436
  EdgeComponent,
1355
1437
  EdgeLabelComponent,
1356
1438
  ConnectionComponent,
1439
+ HandleComponent,
1357
1440
  DefsComponent
1358
1441
  ];
1359
1442
  const directives = [
@@ -1376,6 +1459,7 @@ class VflowModule {
1376
1459
  EdgeComponent,
1377
1460
  EdgeLabelComponent,
1378
1461
  ConnectionComponent,
1462
+ HandleComponent,
1379
1463
  DefsComponent, SpacePointContextDirective,
1380
1464
  MapContextDirective,
1381
1465
  RootSvgReferenceDirective,
@@ -1383,7 +1467,8 @@ class VflowModule {
1383
1467
  EdgeLabelHtmlTemplateDirective,
1384
1468
  EdgeTemplateDirective,
1385
1469
  ConnectionTemplateDirective,
1386
- HandleTemplateDirective], imports: [CommonModule], exports: [VflowComponent, NodeHtmlTemplateDirective,
1470
+ HandleTemplateDirective], imports: [CommonModule], exports: [VflowComponent,
1471
+ HandleComponent, NodeHtmlTemplateDirective,
1387
1472
  EdgeLabelHtmlTemplateDirective,
1388
1473
  EdgeTemplateDirective,
1389
1474
  ConnectionTemplateDirective,
@@ -1396,6 +1481,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1396
1481
  imports: [CommonModule],
1397
1482
  exports: [
1398
1483
  VflowComponent,
1484
+ HandleComponent,
1399
1485
  ...templateDirectives
1400
1486
  ],
1401
1487
  declarations: [...components, ...directives, ...templateDirectives],
@@ -1408,5 +1494,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1408
1494
  * Generated bundle index. Do not edit.
1409
1495
  */
1410
1496
 
1411
- export { ChangesControllerDirective, ConnectionControllerDirective, ConnectionTemplateDirective, EdgeLabelHtmlTemplateDirective, EdgeTemplateDirective, HandleTemplateDirective, NodeHtmlTemplateDirective, VflowComponent, VflowModule };
1497
+ export { ChangesControllerDirective, ConnectionControllerDirective, ConnectionTemplateDirective, EdgeLabelHtmlTemplateDirective, EdgeTemplateDirective, HandleComponent, HandleTemplateDirective, NodeHtmlTemplateDirective, VflowComponent, VflowModule };
1412
1498
  //# sourceMappingURL=ngx-vflow.mjs.map