ngx-vflow 2.0.0 → 2.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.
@@ -759,7 +759,7 @@ class DraggableService {
759
759
  moveNode(model, point) {
760
760
  const parent = model.parent();
761
761
  // keep node in bounds of parent
762
- if (parent) {
762
+ if (model.extent() === 'parent' && parent) {
763
763
  point.x = Math.min(parent.width() - model.width(), point.x);
764
764
  point.x = Math.max(0, point.x);
765
765
  point.y = Math.min(parent.height() - model.height(), point.y);
@@ -1059,6 +1059,7 @@ const NODE_DEFAULTS = {
1059
1059
  height: 50,
1060
1060
  draggable: true,
1061
1061
  parentId: null,
1062
+ extent: 'parent',
1062
1063
  preview: { style: {} },
1063
1064
  selected: false,
1064
1065
  color: '#1b262c',
@@ -1093,10 +1094,11 @@ function createBaseNode(node, useDefaults) {
1093
1094
  return {
1094
1095
  id: node.id,
1095
1096
  point: signal(node.point),
1096
- draggable: signal(node.draggable ?? NODE_DEFAULTS.draggable),
1097
- parentId: signal(node.parentId ?? NODE_DEFAULTS.parentId),
1098
- preview: signal(node.preview ?? NODE_DEFAULTS.preview),
1099
- selected: signal(node.selected ?? NODE_DEFAULTS.selected),
1097
+ draggable: signal(isDefined(node.draggable) ? node.draggable : NODE_DEFAULTS.draggable),
1098
+ parentId: signal(isDefined(node.parentId) ? node.parentId : NODE_DEFAULTS.parentId),
1099
+ extent: signal(isDefined(node.extent) ? node.extent : NODE_DEFAULTS.extent),
1100
+ preview: signal(isDefined(node.preview) ? node.preview : NODE_DEFAULTS.preview),
1101
+ selected: signal(isDefined(node.selected) ? node.selected : NODE_DEFAULTS.selected),
1100
1102
  };
1101
1103
  }
1102
1104
  else {
@@ -1105,6 +1107,7 @@ function createBaseNode(node, useDefaults) {
1105
1107
  point: signal(node.point),
1106
1108
  draggable: isDefined(node.draggable) ? signal(node.draggable) : undefined,
1107
1109
  parentId: isDefined(node.parentId) ? signal(node.parentId) : undefined,
1110
+ extent: isDefined(node.extent) ? signal(node.extent) : undefined,
1108
1111
  preview: isDefined(node.preview) ? signal(node.preview) : undefined,
1109
1112
  selected: isDefined(node.selected) ? signal(node.selected) : undefined,
1110
1113
  };
@@ -1413,6 +1416,7 @@ class NodeModel {
1413
1416
  this.renderOrder = signal(0);
1414
1417
  this.selected = signal(false);
1415
1418
  this.preview = signal({ style: {} });
1419
+ this.extent = signal(NODE_DEFAULTS.extent);
1416
1420
  this.globalPoint = computed(() => {
1417
1421
  let parent = this.parent();
1418
1422
  let x = this.point().x;
@@ -1499,6 +1503,9 @@ class NodeModel {
1499
1503
  if (rawNode.selected) {
1500
1504
  this.selected = rawNode.selected;
1501
1505
  }
1506
+ if (rawNode.extent) {
1507
+ this.extent = rawNode.extent;
1508
+ }
1502
1509
  if (rawNode.type === 'default-group' && rawNode.color) {
1503
1510
  this.color = rawNode.color;
1504
1511
  }
@@ -1569,18 +1576,18 @@ function createEdge(edge, options = { useDefaults: true }) {
1569
1576
  if (options.useDefaults) {
1570
1577
  return {
1571
1578
  id: edge.id,
1572
- type: edge.type ?? EDGE_DEFAULTS.type,
1573
1579
  source: edge.source,
1574
1580
  target: edge.target,
1575
- sourceHandle: edge.sourceHandle ?? '',
1576
- targetHandle: edge.targetHandle ?? '',
1577
- curve: signal(edge.curve ?? EDGE_DEFAULTS.curve),
1578
- data: signal(edge.data ?? EDGE_DEFAULTS.data),
1579
- edgeLabels: signal(edge.edgeLabels ?? EDGE_DEFAULTS.edgeLabels),
1580
- markers: signal(edge.markers ?? EDGE_DEFAULTS.markers),
1581
- reconnectable: signal(edge.reconnectable ?? EDGE_DEFAULTS.reconnectable),
1582
- floating: signal(edge.floating ?? EDGE_DEFAULTS.floating),
1583
- selected: signal(edge.selected ?? EDGE_DEFAULTS.selected),
1581
+ type: isDefined(edge.type) ? edge.type : EDGE_DEFAULTS.type,
1582
+ sourceHandle: isDefined(edge.sourceHandle) ? edge.sourceHandle : '',
1583
+ targetHandle: isDefined(edge.targetHandle) ? edge.targetHandle : '',
1584
+ curve: signal(isDefined(edge.curve) ? edge.curve : EDGE_DEFAULTS.curve),
1585
+ data: signal(isDefined(edge.data) ? edge.data : EDGE_DEFAULTS.data),
1586
+ edgeLabels: signal(isDefined(edge.edgeLabels) ? edge.edgeLabels : EDGE_DEFAULTS.edgeLabels),
1587
+ markers: signal(isDefined(edge.markers) ? edge.markers : EDGE_DEFAULTS.markers),
1588
+ reconnectable: signal(isDefined(edge.reconnectable) ? edge.reconnectable : EDGE_DEFAULTS.reconnectable),
1589
+ floating: signal(isDefined(edge.floating) ? edge.floating : EDGE_DEFAULTS.floating),
1590
+ selected: signal(isDefined(edge.selected) ? edge.selected : EDGE_DEFAULTS.selected),
1584
1591
  };
1585
1592
  }
1586
1593
  else {
@@ -3307,9 +3314,10 @@ function constrainRect(rect, model, side, minWidth, minHeight, maxWidth, maxHeig
3307
3314
  }
3308
3315
 
3309
3316
  class HandleModel {
3310
- constructor(rawHandle, parentNode) {
3317
+ constructor(rawHandle, parentNode, batchingService) {
3311
3318
  this.rawHandle = rawHandle;
3312
3319
  this.parentNode = parentNode;
3320
+ this.batchingService = batchingService;
3313
3321
  this.strokeWidth = 2;
3314
3322
  /**
3315
3323
  * Pre-computed size for default handle, changed dynamically
@@ -3332,10 +3340,15 @@ class HandleModel {
3332
3340
  initialValue: { width: 0, height: 0 },
3333
3341
  });
3334
3342
  // TODO: for some reason toLazySignal breaks unit tests, so we use toSignal here
3335
- this.hostPosition = toSignal(this.updateHostSizeAndPosition$.pipe(map(() => ({
3336
- x: this.hostReference instanceof HTMLElement ? this.hostReference.offsetLeft : 0, // for now just 0 for group nodes
3337
- y: this.hostReference instanceof HTMLElement ? this.hostReference.offsetTop : 0, // for now just 0 for group nodes
3338
- }))), {
3343
+ this.hostPosition = toSignal(this.updateHostSizeAndPosition$.pipe(map(() => {
3344
+ const offsets = this.hostReference instanceof HTMLElement
3345
+ ? this.batchingService.getElementOffsets(this.hostReference)
3346
+ : undefined;
3347
+ return {
3348
+ x: offsets ? offsets.offsetLeft : 0, // for now just 0 for group nodes
3349
+ y: offsets ? offsets.offsetTop : 0, // for now just 0 for group nodes
3350
+ };
3351
+ })), {
3339
3352
  initialValue: { x: 0, y: 0 },
3340
3353
  });
3341
3354
  this.hostOffset = computed(() => {
@@ -3383,16 +3396,29 @@ class HandleModel {
3383
3396
  node: this.parentNode.rawNode,
3384
3397
  },
3385
3398
  };
3399
+ if (this.hostReference instanceof HTMLElement) {
3400
+ this.batchingService.addElementCache(this.hostReference);
3401
+ }
3402
+ }
3403
+ onDestroy() {
3404
+ if (this.hostReference instanceof HTMLElement) {
3405
+ this.batchingService.removeElementCache(this.hostReference);
3406
+ }
3386
3407
  }
3387
3408
  updateHost() {
3409
+ this.batchingService.markCacheAsDirty();
3388
3410
  this.updateHostSizeAndPosition$.next();
3389
3411
  }
3390
3412
  getHostSize() {
3413
+ //TODO only get the hist ref width once ?
3391
3414
  if (this.hostReference instanceof HTMLElement) {
3392
- return {
3393
- width: this.hostReference.offsetWidth,
3394
- height: this.hostReference.offsetHeight,
3395
- };
3415
+ const offsets = this.batchingService.getElementOffsets(this.hostReference);
3416
+ if (offsets) {
3417
+ return {
3418
+ width: offsets.offsetWidth,
3419
+ height: offsets.offsetHeight,
3420
+ };
3421
+ }
3396
3422
  }
3397
3423
  else if (this.hostReference instanceof SVGGraphicsElement) {
3398
3424
  return this.hostReference.getBBox();
@@ -3401,12 +3427,102 @@ class HandleModel {
3401
3427
  }
3402
3428
  }
3403
3429
 
3430
+ class OffsetBatchingCacheService {
3431
+ constructor() {
3432
+ this.elementOffsetCache = new Map();
3433
+ this.cacheIsDirty = true;
3434
+ this.minMsBetweenDirty = 16; //1000 ms/second to get 60fps = ~16ms
3435
+ this.lastDirty = undefined;
3436
+ }
3437
+ addElementCache(element) {
3438
+ this.elementOffsetCache.set(element, undefined);
3439
+ this.markCacheAsDirty();
3440
+ }
3441
+ removeElementCache(element) {
3442
+ this.elementOffsetCache.delete(element);
3443
+ }
3444
+ getElementOffsets(requestedElement) {
3445
+ let requestedCache = undefined;
3446
+ const cachedOffset = this.elementOffsetCache.get(requestedElement);
3447
+ if (cachedOffset === undefined) {
3448
+ this.addElementCache(requestedElement);
3449
+ }
3450
+ else {
3451
+ requestedCache = cachedOffset;
3452
+ }
3453
+ //When something request to get the offset of a given element, compute the cache of all the elements of interest until we get the next dirty request.
3454
+ if (this.cacheIsDirty) {
3455
+ for (const { [0]: element } of this.elementOffsetCache) {
3456
+ const offsetWidth = element.offsetWidth;
3457
+ const offsetHeight = element.offsetHeight;
3458
+ const offsetLeft = element.offsetLeft;
3459
+ const offsetTop = element.offsetTop;
3460
+ const cacheEntry = { offsetWidth, offsetHeight, offsetLeft, offsetTop };
3461
+ this.elementOffsetCache.set(element, cacheEntry);
3462
+ if (element === requestedElement) {
3463
+ requestedCache = cacheEntry;
3464
+ }
3465
+ }
3466
+ this.cacheIsDirty = false;
3467
+ }
3468
+ return requestedCache;
3469
+ }
3470
+ markCacheAsDirty() {
3471
+ const now = new Date();
3472
+ if (this.lastDirty === undefined) {
3473
+ this.cacheIsDirty = true;
3474
+ this.lastDirty = now;
3475
+ return;
3476
+ }
3477
+ //force the cache ttl to at minimum 16ms before considering it dirty
3478
+ const msSinceLastDirty = now.getTime() - this.lastDirty?.getTime();
3479
+ if (msSinceLastDirty > this.minMsBetweenDirty) {
3480
+ this.cacheIsDirty = true;
3481
+ this.lastDirty = now;
3482
+ }
3483
+ }
3484
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: OffsetBatchingCacheService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3485
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: OffsetBatchingCacheService }); }
3486
+ }
3487
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: OffsetBatchingCacheService, decorators: [{
3488
+ type: Injectable
3489
+ }] });
3490
+
3491
+ class RequestAnimationFrameBatchingService {
3492
+ constructor() {
3493
+ this.callbacks = [];
3494
+ this.requestAnimationFrameStarted = false;
3495
+ }
3496
+ batchAnimationFrame(callback) {
3497
+ this.callbacks.push(callback);
3498
+ if (!this.requestAnimationFrameStarted) {
3499
+ this.requestAnimationFrameStarted = true;
3500
+ requestAnimationFrame(() => {
3501
+ this.callbacks.map((x) => {
3502
+ if (x) {
3503
+ x();
3504
+ }
3505
+ });
3506
+ this.callbacks = [];
3507
+ this.requestAnimationFrameStarted = false;
3508
+ });
3509
+ }
3510
+ }
3511
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: RequestAnimationFrameBatchingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3512
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: RequestAnimationFrameBatchingService }); }
3513
+ }
3514
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: RequestAnimationFrameBatchingService, decorators: [{
3515
+ type: Injectable
3516
+ }] });
3517
+
3404
3518
  class HandleComponent {
3405
3519
  constructor() {
3406
3520
  this.injector = inject(Injector);
3407
3521
  this.handleService = inject(HandleService);
3408
3522
  this.element = inject(ElementRef).nativeElement;
3409
3523
  this.destroyRef = inject(DestroyRef);
3524
+ this.requestAnimationFrameBatchingService = inject(RequestAnimationFrameBatchingService);
3525
+ this.offsetBatchingCacheService = inject(OffsetBatchingCacheService);
3410
3526
  /**
3411
3527
  * At what side of node this component should be placed
3412
3528
  */
@@ -3435,10 +3551,15 @@ class HandleComponent {
3435
3551
  template: this.template(),
3436
3552
  userOffsetX: this.offsetX(),
3437
3553
  userOffsetY: this.offsetY(),
3438
- }, node);
3554
+ }, node, this.offsetBatchingCacheService);
3439
3555
  this.handleService.createHandle(model);
3440
- requestAnimationFrame(() => model.updateHost());
3441
- this.destroyRef.onDestroy(() => this.handleService.destroyHandle(model));
3556
+ this.requestAnimationFrameBatchingService.batchAnimationFrame(() => {
3557
+ model.updateHost();
3558
+ });
3559
+ this.destroyRef.onDestroy(() => {
3560
+ this.handleService.destroyHandle(model);
3561
+ model.onDestroy();
3562
+ });
3442
3563
  }
3443
3564
  });
3444
3565
  }
@@ -3450,19 +3571,77 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
3450
3571
  args: [{ standalone: true, selector: 'handle', changeDetection: ChangeDetectionStrategy.OnPush, template: "" }]
3451
3572
  }] });
3452
3573
 
3574
+ class ResizeObserverService {
3575
+ constructor() {
3576
+ this.zone = inject(NgZone);
3577
+ this.thingsToObserve = new Map();
3578
+ this.resizeObserver = new ResizeObserver((entries) => {
3579
+ this.zone.run(() => {
3580
+ for (const entry of entries) {
3581
+ const callbacks = this.thingsToObserve.get(entry.target);
3582
+ if (callbacks !== undefined) {
3583
+ callbacks.forEach((c) => c(entry));
3584
+ }
3585
+ }
3586
+ });
3587
+ });
3588
+ }
3589
+ addObserver(element, callback) {
3590
+ const callbacks = this.thingsToObserve.get(element);
3591
+ if (callbacks === undefined) {
3592
+ this.thingsToObserve.set(element, [callback]);
3593
+ }
3594
+ else {
3595
+ callbacks.push(callback);
3596
+ }
3597
+ this.resizeObserver.observe(element);
3598
+ }
3599
+ removeObserver(element) {
3600
+ this.thingsToObserve.delete(element);
3601
+ if (this.resizeObserver) {
3602
+ this.resizeObserver.unobserve(element);
3603
+ }
3604
+ }
3605
+ ngOnDestroy() {
3606
+ this.resizeObserver.disconnect();
3607
+ }
3608
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ResizeObserverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3609
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ResizeObserverService }); }
3610
+ }
3611
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: ResizeObserverService, decorators: [{
3612
+ type: Injectable
3613
+ }], ctorParameters: () => [] });
3614
+
3453
3615
  class NodeHandlesControllerDirective {
3454
3616
  constructor() {
3455
3617
  this.nodeAccessor = inject(NodeAccessorService);
3456
- this.zone = inject(NgZone);
3457
3618
  this.destroyRef = inject(DestroyRef);
3458
3619
  this.hostElementRef = inject(ElementRef);
3620
+ this.resizeObserverService = inject(ResizeObserverService);
3621
+ this.requestAnimationFrameBatchingService = inject(RequestAnimationFrameBatchingService);
3459
3622
  }
3460
3623
  ngOnInit() {
3461
3624
  const model = this.nodeAccessor.model();
3625
+ let isTrackingHostElement = false;
3462
3626
  model.handles$
3463
- .pipe(switchMap((handles) => resizable([...handles.map((h) => h.hostReference), this.hostElementRef.nativeElement], this.zone).pipe(map(() => handles))), tap((handles) => {
3627
+ .pipe(pairwise(), tap(([previousHandles, currentHandles]) => {
3628
+ const handlesToRemove = previousHandles.filter((prev) => currentHandles.find((curr) => curr.hostReference === prev.hostReference) === undefined);
3629
+ handlesToRemove.forEach((h) => this.resizeObserverService.removeObserver(h.hostReference));
3630
+ const handlesToAdd = currentHandles.filter((curr) => previousHandles.find((prev) => curr.hostReference === prev.hostReference) === undefined);
3631
+ if (!isTrackingHostElement) {
3632
+ this.resizeObserverService.addObserver(this.hostElementRef.nativeElement, () => {
3633
+ currentHandles.forEach((h) => h.updateHost());
3634
+ });
3635
+ isTrackingHostElement = true;
3636
+ }
3637
+ handlesToAdd.forEach((h) => this.resizeObserverService.addObserver(h.hostReference, () => {
3638
+ currentHandles.forEach((h) => h.updateHost());
3639
+ }));
3640
+ //Here we need this to be in a requestAnimationFrame otherwise the handle can still be present in the dom which throws off the offset cache
3641
+ this.requestAnimationFrameBatchingService.batchAnimationFrame(() => {
3642
+ currentHandles.forEach((h) => h.updateHost());
3643
+ });
3464
3644
  // TODO (performance) inspect how to avoid calls of this when flow initially rendered
3465
- handles.forEach((h) => h.updateHost());
3466
3645
  }), takeUntilDestroyed(this.destroyRef))
3467
3646
  .subscribe();
3468
3647
  }
@@ -3483,19 +3662,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
3483
3662
  class NodeResizeControllerDirective {
3484
3663
  constructor() {
3485
3664
  this.nodeAccessor = inject(NodeAccessorService);
3486
- this.zone = inject(NgZone);
3487
- this.destroyRef = inject(DestroyRef);
3665
+ this.resizeObserverService = inject(ResizeObserverService);
3488
3666
  this.hostElementRef = inject(ElementRef);
3489
3667
  }
3490
3668
  ngOnInit() {
3491
3669
  const model = this.nodeAccessor.model();
3492
- const host = this.hostElementRef.nativeElement;
3493
- merge(resizable([host], this.zone))
3494
- .pipe(startWith(null), filter(() => !model.resizing()), tap(() => {
3495
- model.width.set(host.clientWidth);
3496
- model.height.set(host.clientHeight);
3497
- }), takeUntilDestroyed(this.destroyRef))
3498
- .subscribe();
3670
+ this.resizeObserverService.addObserver(this.hostElementRef.nativeElement, (resizeEntry) => {
3671
+ model.width.set(resizeEntry.target.clientWidth);
3672
+ model.height.set(resizeEntry.target.clientHeight);
3673
+ });
3674
+ }
3675
+ ngOnDestroy() {
3676
+ this.resizeObserverService.removeObserver(this.hostElementRef.nativeElement);
3499
3677
  }
3500
3678
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NodeResizeControllerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
3501
3679
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.17", type: NodeResizeControllerDirective, isStandalone: true, selector: "[nodeResizeController]", ngImport: i0 }); }
@@ -3749,6 +3927,8 @@ const defaultBg = '#fff';
3749
3927
  const defaultGap = 20;
3750
3928
  const defaultDotSize = 2;
3751
3929
  const defaultDotColor = 'rgb(177, 177, 183)';
3930
+ const defaultGridSize = 20;
3931
+ const defaultStrokeWidth = 2;
3752
3932
  const defaultImageScale = 0.1;
3753
3933
  const defaultRepeated = true;
3754
3934
  class BackgroundComponent {
@@ -3757,21 +3937,16 @@ class BackgroundComponent {
3757
3937
  this.rootSvg = inject(RootSvgReferenceDirective).element;
3758
3938
  this.settingsService = inject(FlowSettingsService);
3759
3939
  this.backgroundSignal = this.settingsService.background;
3760
- // DOTS PATTERN
3761
- this.scaledGap = computed(() => {
3762
- const background = this.backgroundSignal();
3763
- if (background.type === 'dots') {
3764
- const zoom = this.viewportService.readableViewport().zoom;
3765
- return zoom * (background.gap ?? defaultGap);
3766
- }
3767
- return 0;
3940
+ this.x = computed(() => {
3941
+ return this.viewportService.readableViewport().x % this.scaledGap();
3942
+ });
3943
+ this.y = computed(() => {
3944
+ return this.viewportService.readableViewport().y % this.scaledGap();
3768
3945
  });
3769
- this.x = computed(() => this.viewportService.readableViewport().x % this.scaledGap());
3770
- this.y = computed(() => this.viewportService.readableViewport().y % this.scaledGap());
3771
3946
  this.patternColor = computed(() => {
3772
- const bg = this.backgroundSignal();
3773
- if (bg.type === 'dots') {
3774
- return bg.color ?? defaultDotColor;
3947
+ const background = this.backgroundSignal();
3948
+ if (background.type === 'dots' || background.type === 'grid') {
3949
+ return background.color ?? defaultDotColor;
3775
3950
  }
3776
3951
  return defaultDotColor;
3777
3952
  });
@@ -3780,6 +3955,29 @@ class BackgroundComponent {
3780
3955
  if (background.type === 'dots') {
3781
3956
  return (this.viewportService.readableViewport().zoom * (background.size ?? defaultDotSize)) / 2;
3782
3957
  }
3958
+ if (background.type === 'grid') {
3959
+ return this.viewportService.readableViewport().zoom * (background.size ?? defaultGridSize);
3960
+ }
3961
+ return 0;
3962
+ });
3963
+ this.scaledGap = computed(() => {
3964
+ const background = this.backgroundSignal();
3965
+ const zoom = this.viewportService.readableViewport().zoom;
3966
+ if (background.type === 'dots') {
3967
+ return zoom * (background.gap ?? defaultGap);
3968
+ }
3969
+ if (background.type === 'grid') {
3970
+ return zoom * (background.size ?? defaultGridSize);
3971
+ }
3972
+ return 0;
3973
+ });
3974
+ // GRID PATTERN
3975
+ this.strokeWidth = computed(() => {
3976
+ const background = this.backgroundSignal();
3977
+ if (background.type === 'grid') {
3978
+ const zoom = this.viewportService.readableViewport().zoom;
3979
+ return zoom * ((background.strokeWidth ?? defaultStrokeWidth) / 2);
3980
+ }
3783
3981
  return 0;
3784
3982
  });
3785
3983
  // IMAGE PATTERN
@@ -3837,17 +4035,20 @@ class BackgroundComponent {
3837
4035
  if (background.type === 'dots') {
3838
4036
  this.rootSvg.style.backgroundColor = background.backgroundColor ?? defaultBg;
3839
4037
  }
4038
+ if (background.type === 'grid') {
4039
+ this.rootSvg.style.backgroundColor = background.backgroundColor ?? defaultBg;
4040
+ }
3840
4041
  if (background.type === 'solid') {
3841
4042
  this.rootSvg.style.backgroundColor = background.color;
3842
4043
  }
3843
4044
  });
3844
4045
  }
3845
4046
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: BackgroundComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3846
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: BackgroundComponent, isStandalone: true, selector: "g[background]", ngImport: i0, template: "@if (backgroundSignal().type === 'dots') {\n <svg:pattern\n patternUnits=\"userSpaceOnUse\"\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\">\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\" />\n </svg:pattern>\n\n <svg:rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" [attr.fill]=\"patternUrl\" />\n}\n\n@if (backgroundSignal().type === 'image') {\n @if (repeated()) {\n <svg:pattern\n patternUnits=\"userSpaceOnUse\"\n [attr.id]=\"patternId\"\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\">\n <svg:image [attr.href]=\"bgImageSrc()\" [attr.width]=\"scaledImageWidth()\" [attr.height]=\"scaledImageHeight()\" />\n </svg:pattern>\n\n <svg:rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" [attr.fill]=\"patternUrl\" />\n }\n\n @if (!repeated()) {\n <svg:image\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n [attr.href]=\"bgImageSrc()\" />\n }\n}\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4047
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: BackgroundComponent, isStandalone: true, selector: "g[background]", ngImport: i0, template: "@if (backgroundSignal().type === 'dots') {\n <svg:pattern\n patternUnits=\"userSpaceOnUse\"\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\">\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\" />\n </svg:pattern>\n\n <svg:rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" [attr.fill]=\"patternUrl\" />\n}\n\n@if (backgroundSignal().type === 'image') {\n @if (repeated()) {\n <svg:pattern\n patternUnits=\"userSpaceOnUse\"\n [attr.id]=\"patternId\"\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\">\n <svg:image [attr.href]=\"bgImageSrc()\" [attr.width]=\"scaledImageWidth()\" [attr.height]=\"scaledImageHeight()\" />\n </svg:pattern>\n\n <svg:rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" [attr.fill]=\"patternUrl\" />\n }\n\n @if (!repeated()) {\n <svg:image\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n [attr.href]=\"bgImageSrc()\" />\n }\n}\n\n@if (backgroundSignal().type === 'grid') {\n <svg:pattern\n patternUnits=\"userSpaceOnUse\"\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"patternSize()\"\n [attr.height]=\"patternSize()\">\n <svg:path\n fill=\"none\"\n [attr.d]=\"'M ' + patternSize() + ' 0 L 0 0 0 ' + patternSize()\"\n [attr.stroke]=\"patternColor()\"\n [attr.stroke-width]=\"strokeWidth()\" />\n </svg:pattern>\n\n <svg:rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" [attr.fill]=\"patternUrl\" />\n}\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3847
4048
  }
3848
4049
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: BackgroundComponent, decorators: [{
3849
4050
  type: Component,
3850
- args: [{ standalone: true, selector: 'g[background]', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (backgroundSignal().type === 'dots') {\n <svg:pattern\n patternUnits=\"userSpaceOnUse\"\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\">\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\" />\n </svg:pattern>\n\n <svg:rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" [attr.fill]=\"patternUrl\" />\n}\n\n@if (backgroundSignal().type === 'image') {\n @if (repeated()) {\n <svg:pattern\n patternUnits=\"userSpaceOnUse\"\n [attr.id]=\"patternId\"\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\">\n <svg:image [attr.href]=\"bgImageSrc()\" [attr.width]=\"scaledImageWidth()\" [attr.height]=\"scaledImageHeight()\" />\n </svg:pattern>\n\n <svg:rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" [attr.fill]=\"patternUrl\" />\n }\n\n @if (!repeated()) {\n <svg:image\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n [attr.href]=\"bgImageSrc()\" />\n }\n}\n" }]
4051
+ args: [{ standalone: true, selector: 'g[background]', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (backgroundSignal().type === 'dots') {\n <svg:pattern\n patternUnits=\"userSpaceOnUse\"\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\">\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\" />\n </svg:pattern>\n\n <svg:rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" [attr.fill]=\"patternUrl\" />\n}\n\n@if (backgroundSignal().type === 'image') {\n @if (repeated()) {\n <svg:pattern\n patternUnits=\"userSpaceOnUse\"\n [attr.id]=\"patternId\"\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\">\n <svg:image [attr.href]=\"bgImageSrc()\" [attr.width]=\"scaledImageWidth()\" [attr.height]=\"scaledImageHeight()\" />\n </svg:pattern>\n\n <svg:rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" [attr.fill]=\"patternUrl\" />\n }\n\n @if (!repeated()) {\n <svg:image\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n [attr.href]=\"bgImageSrc()\" />\n }\n}\n\n@if (backgroundSignal().type === 'grid') {\n <svg:pattern\n patternUnits=\"userSpaceOnUse\"\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"patternSize()\"\n [attr.height]=\"patternSize()\">\n <svg:path\n fill=\"none\"\n [attr.d]=\"'M ' + patternSize() + ' 0 L 0 0 0 ' + patternSize()\"\n [attr.stroke]=\"patternColor()\"\n [attr.stroke-width]=\"strokeWidth()\" />\n </svg:pattern>\n\n <svg:rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" [attr.fill]=\"patternUrl\" />\n}\n" }]
3851
4052
  }], ctorParameters: () => [] });
3852
4053
  function createImage(url) {
3853
4054
  const image = new Image();
@@ -4263,8 +4464,16 @@ class AlignmentHelperComponent {
4263
4464
  .pipe(filter(isNodeDragEndStatus), map((status) => status.payload.node), map((node) => [node, this.intersections()]), tap(([node, intersections]) => {
4264
4465
  if (intersections) {
4265
4466
  const snapped = { x: intersections.snappedX, y: intersections.snappedY };
4266
- const parentIfExists = node.parent() ? [node.parent()] : [];
4267
- node.setPoint(getSpacePoints(snapped, parentIfExists)[0]);
4467
+ const parent = node.parent();
4468
+ if (parent) {
4469
+ node.setPoint({
4470
+ x: snapped.x - parent.globalPoint().x,
4471
+ y: snapped.y - parent.globalPoint().y,
4472
+ });
4473
+ }
4474
+ else {
4475
+ node.setPoint(snapped);
4476
+ }
4268
4477
  }
4269
4478
  }), takeUntilDestroyed())
4270
4479
  .subscribe();
@@ -4680,6 +4889,9 @@ class VflowComponent {
4680
4889
  OverlaysService,
4681
4890
  { provide: PreviewFlowRenderStrategyService, useClass: ViewportPreviewFlowRenderStrategyService },
4682
4891
  FlowRenderingService,
4892
+ ResizeObserverService,
4893
+ OffsetBatchingCacheService,
4894
+ RequestAnimationFrameBatchingService,
4683
4895
  ], queries: [{ propertyName: "nodeTemplateDirective", first: true, predicate: NodeHtmlTemplateDirective, descendants: true, isSignal: true }, { propertyName: "nodeSvgTemplateDirective", first: true, predicate: NodeSvgTemplateDirective, descendants: true, isSignal: true }, { propertyName: "groupNodeTemplateDirective", first: true, predicate: GroupNodeTemplateDirective, descendants: true, isSignal: true }, { propertyName: "edgeTemplateDirective", first: true, predicate: EdgeTemplateDirective, descendants: true, isSignal: true }, { propertyName: "edgeLabelHtmlDirective", first: true, predicate: EdgeLabelHtmlTemplateDirective, descendants: true, isSignal: true }, { propertyName: "connectionTemplateDirective", first: true, predicate: ConnectionTemplateDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "mapContext", first: true, predicate: MapContextDirective, descendants: true, isSignal: true }, { propertyName: "spacePointContext", first: true, predicate: SpacePointContextDirective, descendants: true, isSignal: true }], hostDirectives: [{ directive: ChangesControllerDirective, outputs: ["nodesChanges", "nodesChanges", "nodesChanges.position", "nodesChanges.position", "nodesChanges.size", "nodesChanges.size", "nodesChanges.add", "nodesChanges.add", "nodesChanges.remove", "nodesChanges.remove", "nodesChanges.select", "nodesChanges.select", "edgesChanges", "edgesChanges", "edgesChanges.detached", "edgesChanges.detached", "edgesChanges.add", "edgesChanges.add", "edgesChanges.remove", "edgesChanges.remove", "edgesChanges.select", "edgesChanges.select"] }], ngImport: i0, template: "<svg:svg #flow rootSvgRef rootSvgContext rootPointer flowSizeController class=\"root-svg\">\n <defs flowDefs [markers]=\"markers()\" />\n\n <g background />\n\n <svg:g mapContext spacePointContext autoPan>\n @if (alignmentHelper(); as alignmentHelper) {\n @if (alignmentHelper === true) {\n <svg:g alignmentHelper />\n } @else {\n <svg:g alignmentHelper [tolerance]=\"alignmentHelper.tolerance\" [lineColor]=\"alignmentHelper.lineColor\" />\n }\n }\n\n <!-- Connection -->\n <svg:g connection [model]=\"connection\" [template]=\"connectionTemplateDirective()?.templateRef\" />\n\n @if (flowOptimization().detachedGroupsLayer) {\n <!-- Groups -->\n @for (model of groups(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n <!-- Edges -->\n @for (model of edgeModels(); track trackEdges($index, model)) {\n <svg:g\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective()?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective()?.templateRef\" />\n }\n <!-- Nodes -->\n @for (model of nonGroups(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective()?.templateRef\"\n [nodeSvgTemplate]=\"nodeSvgTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n }\n\n @if (!flowOptimization().detachedGroupsLayer) {\n <!-- Edges -->\n @for (model of edgeModels(); track trackEdges($index, model)) {\n <svg:g\n edge\n [model]=\"model\"\n [edgeTemplate]=\"edgeTemplateDirective()?.templateRef\"\n [edgeLabelHtmlTemplate]=\"edgeLabelHtmlDirective()?.templateRef\" />\n }\n\n @for (model of nodeModels(); track trackNodes($index, model)) {\n <svg:g\n node\n [model]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective()?.templateRef\"\n [nodeSvgTemplate]=\"nodeSvgTemplateDirective()?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective()?.templateRef\"\n [attr.transform]=\"model.pointTransform()\" />\n }\n }\n </svg:g>\n\n <!-- Minimap -->\n @if (minimap(); as minimap) {\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n }\n</svg:svg>\n\n@if (flowOptimization().virtualization) {\n <canvas previewFlow class=\"preview-flow\" [width]=\"flowWidth()\" [height]=\"flowHeight()\"></canvas>\n}\n", styles: [":host{display:grid;grid-template-columns:1fr;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}.root-svg{grid-row-start:1;grid-column-start:1}.preview-flow{pointer-events:none;grid-row-start:1;grid-column-start:1}\n"], dependencies: [{ kind: "directive", type: RootSvgReferenceDirective, selector: "svg[rootSvgRef]" }, { kind: "directive", type: RootSvgContextDirective, selector: "svg[rootSvgContext]" }, { kind: "directive", type: RootPointerDirective, selector: "svg[rootPointer]" }, { kind: "directive", type: FlowSizeControllerDirective, selector: "svg[flowSizeController]" }, { kind: "component", type: DefsComponent, selector: "defs[flowDefs]", inputs: ["markers"] }, { kind: "component", type: BackgroundComponent, selector: "g[background]" }, { kind: "directive", type: MapContextDirective, selector: "g[mapContext]" }, { kind: "directive", type: SpacePointContextDirective, selector: "g[spacePointContext]" }, { kind: "component", type: ConnectionComponent, selector: "g[connection]", inputs: ["model", "template"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["model", "nodeTemplate", "nodeSvgTemplate", "groupNodeTemplate"] }, { kind: "component", type: EdgeComponent, selector: "g[edge]", inputs: ["model", "edgeTemplate", "edgeLabelHtmlTemplate"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: PreviewFlowComponent, selector: "canvas[previewFlow]", inputs: ["width", "height"] }, { kind: "component", type: AlignmentHelperComponent, selector: "g[alignmentHelper]", inputs: ["tolerance", "lineColor"] }, { kind: "directive", type: AutoPanDirective, selector: "[autoPan]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4684
4896
  }
4685
4897
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: VflowComponent, decorators: [{
@@ -4700,6 +4912,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
4700
4912
  OverlaysService,
4701
4913
  { provide: PreviewFlowRenderStrategyService, useClass: ViewportPreviewFlowRenderStrategyService },
4702
4914
  FlowRenderingService,
4915
+ ResizeObserverService,
4916
+ OffsetBatchingCacheService,
4917
+ RequestAnimationFrameBatchingService,
4703
4918
  ], hostDirectives: [changesControllerHostDirective], imports: [
4704
4919
  RootSvgReferenceDirective,
4705
4920
  RootSvgContextDirective,