ngx-vflow 1.1.2 → 1.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.
@@ -165,6 +165,7 @@ class FlowSettingsService {
165
165
  this.minZoom = signal(0.5);
166
166
  this.maxZoom = signal(3);
167
167
  this.background = signal({ type: 'solid', color: '#fff' });
168
+ this.snapGrid = signal([1, 1]);
168
169
  }
169
170
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FlowSettingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
170
171
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FlowSettingsService }); }
@@ -459,6 +460,7 @@ const round = (num) => Math.round(num * 100) / 100;
459
460
  class DraggableService {
460
461
  constructor() {
461
462
  this.entitiesService = inject(FlowEntitiesService);
463
+ this.settingsService = inject(FlowSettingsService);
462
464
  }
463
465
  /**
464
466
  * Enable draggable behavior for element.
@@ -517,7 +519,7 @@ class DraggableService {
517
519
  x: round(event.x + initialPositions[index].x),
518
520
  y: round(event.y + initialPositions[index].y),
519
521
  };
520
- moveNode(model, point);
522
+ this.moveNode(model, point);
521
523
  });
522
524
  });
523
525
  }
@@ -530,22 +532,42 @@ class DraggableService {
530
532
  : // we only can move current node if it's not selected
531
533
  [model];
532
534
  }
535
+ /**
536
+ * @todo make it unit testable
537
+ */
538
+ moveNode(model, point) {
539
+ point = this.alignToGrid(point);
540
+ const parent = model.parent();
541
+ // keep node in bounds of parent
542
+ if (parent) {
543
+ point.x = Math.min(parent.width() - model.width(), point.x);
544
+ point.x = Math.max(0, point.x);
545
+ point.y = Math.min(parent.height() - model.height(), point.y);
546
+ point.y = Math.max(0, point.y);
547
+ }
548
+ model.setPoint(point);
549
+ }
550
+ /**
551
+ * @todo make it unit testable
552
+ */
553
+ alignToGrid(point) {
554
+ const [snapX, snapY] = this.settingsService.snapGrid();
555
+ if (snapX > 1) {
556
+ point.x = align(point.x, snapX);
557
+ }
558
+ if (snapY > 1) {
559
+ point.y = align(point.y, snapY);
560
+ }
561
+ return point;
562
+ }
533
563
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DraggableService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
534
564
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DraggableService }); }
535
565
  }
536
566
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DraggableService, decorators: [{
537
567
  type: Injectable
538
568
  }] });
539
- function moveNode(model, point) {
540
- const parent = model.parent();
541
- // keep node in bounds of parent
542
- if (parent) {
543
- point.x = Math.min(parent.size().width - model.size().width, point.x);
544
- point.x = Math.max(0, point.x);
545
- point.y = Math.min(parent.size().height - model.size().height, point.y);
546
- point.y = Math.max(0, point.y);
547
- }
548
- model.setPoint(point);
569
+ function align(num, constant) {
570
+ return Math.ceil(num / constant) * constant;
549
571
  }
550
572
 
551
573
  class EdgeTemplateDirective {
@@ -1758,6 +1780,7 @@ class EdgeLabelComponent {
1758
1780
  constructor() {
1759
1781
  this.zone = inject(NgZone);
1760
1782
  this.destroyRef = inject(DestroyRef);
1783
+ this.settingsService = inject(FlowSettingsService);
1761
1784
  // TODO: too many inputs
1762
1785
  this.model = input.required();
1763
1786
  this.edgeModel = input.required();
@@ -1777,12 +1800,29 @@ class EdgeLabelComponent {
1777
1800
  y: point.y - height / 2,
1778
1801
  };
1779
1802
  });
1803
+ this.edgeLabelStyle = computed(() => {
1804
+ const label = this.model().edgeLabel;
1805
+ if (label.type === 'default' && label.style) {
1806
+ const flowBackground = this.settingsService.background();
1807
+ let color = 'transparent';
1808
+ if (flowBackground.type === 'dots') {
1809
+ color = flowBackground.backgroundColor ?? '#fff';
1810
+ }
1811
+ if (flowBackground.type === 'solid') {
1812
+ color = flowBackground.color;
1813
+ }
1814
+ label.style.backgroundColor = label.style.backgroundColor ?? color;
1815
+ return label.style;
1816
+ }
1817
+ return null;
1818
+ });
1780
1819
  }
1781
1820
  ngAfterViewInit() {
1782
- resizable([this.edgeLabelWrapperRef().nativeElement], this.zone)
1821
+ const labelElement = this.edgeLabelWrapperRef().nativeElement;
1822
+ resizable([labelElement], this.zone)
1783
1823
  .pipe(startWith(null), tap(() => {
1784
- const width = this.edgeLabelWrapperRef().nativeElement.clientWidth + MAGIC_NUMBER_TO_FIX_GLITCH_IN_CHROME;
1785
- const height = this.edgeLabelWrapperRef().nativeElement.clientHeight + MAGIC_NUMBER_TO_FIX_GLITCH_IN_CHROME;
1824
+ const width = labelElement.clientWidth + MAGIC_NUMBER_TO_FIX_GLITCH_IN_CHROME;
1825
+ const height = labelElement.clientHeight + MAGIC_NUMBER_TO_FIX_GLITCH_IN_CHROME;
1786
1826
  this.model().size.set({ width, height });
1787
1827
  }), takeUntilDestroyed(this.destroyRef))
1788
1828
  .subscribe();
@@ -1796,11 +1836,11 @@ class EdgeLabelComponent {
1796
1836
  };
1797
1837
  }
1798
1838
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EdgeLabelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1799
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EdgeLabelComponent, isStandalone: true, selector: "g[edgeLabel]", inputs: { model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: true, transformFunction: null }, edgeModel: { classPropertyName: "edgeModel", publicName: "edgeModel", isSignal: true, isRequired: true, transformFunction: null }, point: { classPropertyName: "point", publicName: "point", isSignal: true, isRequired: false, transformFunction: null }, htmlTemplate: { classPropertyName: "htmlTemplate", publicName: "htmlTemplate", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "edgeLabelWrapperRef", first: true, predicate: ["edgeLabelWrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (model().edgeLabel.type === 'html-template' && htmlTemplate()) {\n @if (htmlTemplate(); as htmlTemplate) {\n <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 <div #edgeLabelWrapper class=\"edge-label-wrapper\">\n <ng-container *ngTemplateOutlet=\"htmlTemplate; context: getLabelContext()\" />\n </div>\n </svg:foreignObject>\n }\n}\n", styles: [".edge-label-wrapper{width:max-content;margin-top:1px;margin-left:1px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1839
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EdgeLabelComponent, isStandalone: true, selector: "g[edgeLabel]", inputs: { model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: true, transformFunction: null }, edgeModel: { classPropertyName: "edgeModel", publicName: "edgeModel", isSignal: true, isRequired: true, transformFunction: null }, point: { classPropertyName: "point", publicName: "point", isSignal: true, isRequired: false, transformFunction: null }, htmlTemplate: { classPropertyName: "htmlTemplate", publicName: "htmlTemplate", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "edgeLabelWrapperRef", first: true, predicate: ["edgeLabelWrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (model(); as model) {\n @if (model.edgeLabel.type === 'html-template' && htmlTemplate()) {\n @if (htmlTemplate(); as htmlTemplate) {\n <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 <div #edgeLabelWrapper class=\"edge-label-wrapper\">\n <ng-container *ngTemplateOutlet=\"htmlTemplate; context: getLabelContext()\" />\n </div>\n </svg:foreignObject>\n }\n }\n\n @if (model.edgeLabel.type === 'default') {\n <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 <div #edgeLabelWrapper class=\"edge-label-wrapper\" [style]=\"edgeLabelStyle()\">\n {{ model.edgeLabel.text }}\n </div>\n </svg:foreignObject>\n }\n}\n", styles: [".edge-label-wrapper{width:max-content;margin-top:1px;margin-left:1px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1800
1840
  }
1801
1841
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EdgeLabelComponent, decorators: [{
1802
1842
  type: Component,
1803
- args: [{ standalone: true, selector: 'g[edgeLabel]', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], template: "@if (model().edgeLabel.type === 'html-template' && htmlTemplate()) {\n @if (htmlTemplate(); as htmlTemplate) {\n <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 <div #edgeLabelWrapper class=\"edge-label-wrapper\">\n <ng-container *ngTemplateOutlet=\"htmlTemplate; context: getLabelContext()\" />\n </div>\n </svg:foreignObject>\n }\n}\n", styles: [".edge-label-wrapper{width:max-content;margin-top:1px;margin-left:1px}\n"] }]
1843
+ args: [{ standalone: true, selector: 'g[edgeLabel]', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], template: "@if (model(); as model) {\n @if (model.edgeLabel.type === 'html-template' && htmlTemplate()) {\n @if (htmlTemplate(); as htmlTemplate) {\n <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 <div #edgeLabelWrapper class=\"edge-label-wrapper\">\n <ng-container *ngTemplateOutlet=\"htmlTemplate; context: getLabelContext()\" />\n </div>\n </svg:foreignObject>\n }\n }\n\n @if (model.edgeLabel.type === 'default') {\n <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 <div #edgeLabelWrapper class=\"edge-label-wrapper\" [style]=\"edgeLabelStyle()\">\n {{ model.edgeLabel.text }}\n </div>\n </svg:foreignObject>\n }\n}\n", styles: [".edge-label-wrapper{width:max-content;margin-top:1px;margin-left:1px}\n"] }]
1804
1844
  }] });
1805
1845
 
1806
1846
  class EdgeComponent {
@@ -2945,7 +2985,7 @@ class RootSvgContextDirective {
2945
2985
  }
2946
2986
  }
2947
2987
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RootSvgContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2948
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: RootSvgContextDirective, isStandalone: true, selector: "svg[rootSvgContext]", host: { listeners: { "document:mouseup": "resetConnection()", "document:touchend": "resetConnection()" } }, ngImport: i0 }); }
2988
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: RootSvgContextDirective, isStandalone: true, selector: "svg[rootSvgContext]", host: { listeners: { "document:mouseup": "resetConnection()", "document:touchend": "resetConnection()", "contextmenu": "resetConnection()" } }, ngImport: i0 }); }
2949
2989
  }
2950
2990
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RootSvgContextDirective, decorators: [{
2951
2991
  type: Directive,
@@ -2959,6 +2999,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2959
2999
  }, {
2960
3000
  type: HostListener,
2961
3001
  args: ['document:touchend']
3002
+ }, {
3003
+ type: HostListener,
3004
+ args: ['contextmenu']
2962
3005
  }] } });
2963
3006
 
2964
3007
  const changesControllerHostDirective = {
@@ -3119,6 +3162,12 @@ class VflowComponent {
3119
3162
  get connection() {
3120
3163
  return this.flowEntitiesService.connection();
3121
3164
  }
3165
+ /**
3166
+ * Snap grid for node movement. Passes as [x, y]
3167
+ */
3168
+ set snapGrid(value) {
3169
+ this.flowSettingsService.snapGrid.set(value);
3170
+ }
3122
3171
  // #endregion
3123
3172
  // #region MAIN_INPUTS
3124
3173
  /**
@@ -3220,7 +3269,7 @@ class VflowComponent {
3220
3269
  });
3221
3270
  }
3222
3271
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: VflowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3223
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: VflowComponent, isStandalone: true, selector: "vflow", inputs: { view: { classPropertyName: "view", publicName: "view", isSignal: false, isRequired: false, transformFunction: null }, minZoom: { classPropertyName: "minZoom", publicName: "minZoom", isSignal: false, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: false, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: false, isRequired: false, transformFunction: null }, optimization: { classPropertyName: "optimization", publicName: "optimization", isSignal: true, isRequired: false, transformFunction: null }, entitiesSelectable: { classPropertyName: "entitiesSelectable", publicName: "entitiesSelectable", isSignal: false, isRequired: false, transformFunction: null }, keyboardShortcuts: { classPropertyName: "keyboardShortcuts", publicName: "keyboardShortcuts", isSignal: false, isRequired: false, transformFunction: null }, connection: { classPropertyName: "connection", publicName: "connection", isSignal: false, isRequired: false, transformFunction: (settings) => new ConnectionModel(settings) }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: false, isRequired: true, transformFunction: null }, edges: { classPropertyName: "edges", publicName: "edges", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { onComponentNodeEvent: "onComponentNodeEvent" }, providers: [
3272
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: VflowComponent, isStandalone: true, selector: "vflow", inputs: { view: { classPropertyName: "view", publicName: "view", isSignal: false, isRequired: false, transformFunction: null }, minZoom: { classPropertyName: "minZoom", publicName: "minZoom", isSignal: false, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: false, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: false, isRequired: false, transformFunction: null }, optimization: { classPropertyName: "optimization", publicName: "optimization", isSignal: true, isRequired: false, transformFunction: null }, entitiesSelectable: { classPropertyName: "entitiesSelectable", publicName: "entitiesSelectable", isSignal: false, isRequired: false, transformFunction: null }, keyboardShortcuts: { classPropertyName: "keyboardShortcuts", publicName: "keyboardShortcuts", isSignal: false, isRequired: false, transformFunction: null }, connection: { classPropertyName: "connection", publicName: "connection", isSignal: false, isRequired: false, transformFunction: (settings) => new ConnectionModel(settings) }, snapGrid: { classPropertyName: "snapGrid", publicName: "snapGrid", isSignal: false, isRequired: false, transformFunction: null }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: false, isRequired: true, transformFunction: null }, edges: { classPropertyName: "edges", publicName: "edges", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { onComponentNodeEvent: "onComponentNodeEvent" }, providers: [
3224
3273
  DraggableService,
3225
3274
  ViewportService,
3226
3275
  FlowStatusService,
@@ -3281,6 +3330,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3281
3330
  args: [{
3282
3331
  transform: (settings) => new ConnectionModel(settings),
3283
3332
  }]
3333
+ }], snapGrid: [{
3334
+ type: Input
3284
3335
  }], nodes: [{
3285
3336
  type: Input,
3286
3337
  args: [{ required: true }]