ngx-vflow 0.14.1 → 0.16.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 (47) hide show
  1. package/esm2022/lib/vflow/components/background/background.component.mjs +72 -4
  2. package/esm2022/lib/vflow/components/connection/connection.component.mjs +6 -1
  3. package/esm2022/lib/vflow/components/custom-node-base/custom-node-base.component.mjs +5 -7
  4. package/esm2022/lib/vflow/components/handle/handle.component.mjs +16 -14
  5. package/esm2022/lib/vflow/components/node/node.component.mjs +8 -3
  6. package/esm2022/lib/vflow/components/vflow/vflow.component.mjs +12 -6
  7. package/esm2022/lib/vflow/directives/drag-handle.directive.mjs +27 -0
  8. package/esm2022/lib/vflow/directives/map-context.directive.mjs +24 -17
  9. package/esm2022/lib/vflow/interfaces/edge.interface.mjs +1 -1
  10. package/esm2022/lib/vflow/interfaces/optimization.interface.mjs +1 -1
  11. package/esm2022/lib/vflow/math/edge-path/smooth-step-path.mjs +170 -0
  12. package/esm2022/lib/vflow/models/edge.model.mjs +6 -1
  13. package/esm2022/lib/vflow/models/node.model.mjs +2 -1
  14. package/esm2022/lib/vflow/models/toolbar.model.mjs +36 -0
  15. package/esm2022/lib/vflow/public-components/minimap/minimap.component.mjs +8 -2
  16. package/esm2022/lib/vflow/public-components/node-toolbar/node-toolbar.component.mjs +66 -0
  17. package/esm2022/lib/vflow/services/draggable.service.mjs +13 -15
  18. package/esm2022/lib/vflow/services/node-rendering.service.mjs +8 -1
  19. package/esm2022/lib/vflow/services/overlays.service.mjs +34 -0
  20. package/esm2022/lib/vflow/testing-utils/provide-custom-node-mocks.mjs +67 -0
  21. package/esm2022/lib/vflow/types/background.type.mjs +1 -1
  22. package/esm2022/lib/vflow/utils/is-group-node.mjs +4 -0
  23. package/esm2022/lib/vflow/vflow.module.mjs +17 -5
  24. package/esm2022/public-api.mjs +5 -1
  25. package/fesm2022/ngx-vflow.mjs +554 -65
  26. package/fesm2022/ngx-vflow.mjs.map +1 -1
  27. package/lib/vflow/components/background/background.component.d.ts +13 -0
  28. package/lib/vflow/components/handle/handle.component.d.ts +3 -3
  29. package/lib/vflow/components/node/node.component.d.ts +2 -0
  30. package/lib/vflow/components/vflow/vflow.component.d.ts +2 -0
  31. package/lib/vflow/directives/drag-handle.directive.d.ts +8 -0
  32. package/lib/vflow/directives/map-context.directive.d.ts +3 -2
  33. package/lib/vflow/interfaces/edge.interface.d.ts +1 -1
  34. package/lib/vflow/interfaces/optimization.interface.d.ts +13 -0
  35. package/lib/vflow/math/edge-path/smooth-step-path.d.ts +5 -0
  36. package/lib/vflow/models/node.model.d.ts +1 -0
  37. package/lib/vflow/models/toolbar.model.d.ts +19 -0
  38. package/lib/vflow/public-components/node-toolbar/node-toolbar.component.d.ts +22 -0
  39. package/lib/vflow/services/draggable.service.d.ts +0 -5
  40. package/lib/vflow/services/node-rendering.service.d.ts +2 -0
  41. package/lib/vflow/services/overlays.service.d.ts +11 -0
  42. package/lib/vflow/testing-utils/provide-custom-node-mocks.d.ts +2 -0
  43. package/lib/vflow/types/background.type.d.ts +24 -1
  44. package/lib/vflow/utils/is-group-node.d.ts +2 -0
  45. package/lib/vflow/vflow.module.d.ts +14 -12
  46. package/package.json +3 -3
  47. package/public-api.d.ts +3 -0
@@ -4,7 +4,7 @@ import * as i0 from '@angular/core';
4
4
  import { signal, computed, Injectable, inject, ElementRef, Directive, effect, untracked, TemplateRef, EventEmitter, Output, DestroyRef, Input, runInInjectionContext, Component, Injector, ChangeDetectionStrategy, HostListener, ViewChild, NgZone, ContentChild, NgModule } from '@angular/core';
5
5
  import { select } from 'd3-selection';
6
6
  import { zoomIdentity, zoom } from 'd3-zoom';
7
- import { switchMap, merge, fromEvent, tap, Subject, observeOn, animationFrameScheduler, skip, map, pairwise, filter, distinctUntilChanged, asyncScheduler, zip, share, Observable, startWith } from 'rxjs';
7
+ import { switchMap, merge, fromEvent, tap, Subject, observeOn, animationFrameScheduler, skip, map, pairwise, filter, distinctUntilChanged, asyncScheduler, zip, share, Observable, startWith, of } from 'rxjs';
8
8
  import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
9
9
  import { drag } from 'd3-drag';
10
10
  import { __decorate } from 'tslib';
@@ -406,30 +406,37 @@ class MapContextDirective {
406
406
  this.viewportService.readableViewport.set(mapTransformToViewportState(transform));
407
407
  this.zoomableSelection.attr('transform', transform.toString());
408
408
  };
409
+ this.handleZoomStart = ({ transform }) => {
410
+ this.viewportForSelection = {
411
+ start: mapTransformToViewportState(transform)
412
+ };
413
+ };
414
+ this.handleZoomEnd = ({ transform, sourceEvent }) => {
415
+ this.viewportForSelection = {
416
+ ...this.viewportForSelection,
417
+ end: mapTransformToViewportState(transform),
418
+ target: evTarget(sourceEvent)
419
+ };
420
+ this.selectionService.setViewport(this.viewportForSelection);
421
+ };
422
+ this.filterCondition = (event) => {
423
+ if (event.type === 'mousedown' || event.type === 'touchstart') {
424
+ return event.target.closest('.vflow-node') === null;
425
+ }
426
+ return true;
427
+ };
409
428
  }
410
429
  ngOnInit() {
411
430
  this.zoomBehavior = zoom()
412
431
  .scaleExtent([this.flowSettingsService.minZoom(), this.flowSettingsService.maxZoom()])
413
- .on('start', (event) => this.onD3zoomStart(event))
414
- .on('zoom', (event) => this.handleZoom(event))
415
- .on('end', (event) => this.onD3zoomEnd(event));
432
+ .filter(this.filterCondition)
433
+ .on('start', this.handleZoomStart)
434
+ .on('zoom', this.handleZoom)
435
+ .on('end', this.handleZoomEnd);
416
436
  this.rootSvgSelection
417
437
  .call(this.zoomBehavior)
418
438
  .on('dblclick.zoom', null);
419
439
  }
420
- onD3zoomStart({ transform }) {
421
- this.viewportForSelection = {
422
- start: mapTransformToViewportState(transform)
423
- };
424
- }
425
- onD3zoomEnd({ transform, sourceEvent }) {
426
- this.viewportForSelection = {
427
- ...this.viewportForSelection,
428
- end: mapTransformToViewportState(transform),
429
- target: evTarget(sourceEvent)
430
- };
431
- this.selectionService.setViewport(this.viewportForSelection);
432
- }
433
440
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MapContextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
434
441
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: MapContextDirective, selector: "g[mapContext]", ngImport: i0 }); }
435
442
  }
@@ -458,8 +465,8 @@ class DraggableService {
458
465
  * @param model model with data for this element
459
466
  */
460
467
  enable(element, model) {
461
- const d3Element = select(element);
462
- d3Element.call(this.getDragBehavior(model));
468
+ select(element)
469
+ .call(this.getDragBehavior(model));
463
470
  }
464
471
  /**
465
472
  * Disable draggable behavior for element.
@@ -468,8 +475,8 @@ class DraggableService {
468
475
  * @param model model with data for this element
469
476
  */
470
477
  disable(element) {
471
- const d3Element = select(element);
472
- d3Element.call(this.getIgnoreDragBehavior());
478
+ select(element)
479
+ .call(drag().on('drag', null));
473
480
  }
474
481
  /**
475
482
  * TODO: not shure if this work, need to check
@@ -488,7 +495,15 @@ class DraggableService {
488
495
  getDragBehavior(model) {
489
496
  let dragNodes = [];
490
497
  let initialPositions = [];
498
+ const filterCondition = (event) => {
499
+ // if there is at least one drag handle, we should check if we are dragging it
500
+ if (model.dragHandlesCount()) {
501
+ return !!event.target.closest('.vflow-drag-handle');
502
+ }
503
+ return true;
504
+ };
491
505
  return drag()
506
+ .filter(filterCondition)
492
507
  .on('start', (event) => {
493
508
  dragNodes = this.getDragNodes(model);
494
509
  initialPositions = dragNodes.map(node => ({
@@ -506,16 +521,6 @@ class DraggableService {
506
521
  });
507
522
  });
508
523
  }
509
- /**
510
- * Specify ignoring drag behavior. It's responsible for not moving the map when user tries to drag node
511
- * with disabled drag behavior
512
- */
513
- getIgnoreDragBehavior() {
514
- return drag()
515
- .on('drag', (event) => {
516
- event.sourceEvent.stopPropagation();
517
- });
518
- }
519
524
  getDragNodes(model) {
520
525
  return model.selected()
521
526
  ? this.entitiesService
@@ -825,7 +830,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
825
830
 
826
831
  class CustomNodeBaseComponent {
827
832
  constructor() {
828
- this.eventBus = inject(ComponentEventBusService, { optional: true });
833
+ this.eventBus = inject(ComponentEventBusService);
829
834
  this.destroyRef = inject(DestroyRef);
830
835
  /**
831
836
  * Signal with selected state of node
@@ -837,11 +842,9 @@ class CustomNodeBaseComponent {
837
842
  this.selected.set(value);
838
843
  }
839
844
  ngOnInit() {
840
- if (this.eventBus) {
841
- this.trackEvents()
842
- .pipe(takeUntilDestroyed(this.destroyRef))
843
- .subscribe();
844
- }
845
+ this.trackEvents()
846
+ .pipe(takeUntilDestroyed(this.destroyRef))
847
+ .subscribe();
845
848
  }
846
849
  trackEvents() {
847
850
  const props = Object.getOwnPropertyNames(this);
@@ -980,6 +983,7 @@ class NodeModel {
980
983
  this.handles = signal([]);
981
984
  this.handles$ = toObservable(this.handles);
982
985
  this.draggable = signal(true);
986
+ this.dragHandlesCount = signal(0);
983
987
  // disabled for configuration for now
984
988
  this.magnetRadius = 20;
985
989
  // TODO: not sure if we need to statically store it
@@ -1209,6 +1213,176 @@ function getPointOnBezier(sourcePoint, targetPoint, sourceControl, targetControl
1209
1213
  return getPointOnLineByRatio(getPointOnLineByRatio(fromSourceToFirstControl, fromFirstControlToSecond, ratio), getPointOnLineByRatio(fromFirstControlToSecond, fromSecondControlToTarget, ratio), ratio);
1210
1214
  }
1211
1215
 
1216
+ const handleDirections = {
1217
+ left: { x: -1, y: 0 },
1218
+ right: { x: 1, y: 0 },
1219
+ top: { x: 0, y: -1 },
1220
+ bottom: { x: 0, y: 1 },
1221
+ };
1222
+ function getEdgeCenter(source, target) {
1223
+ const xOffset = Math.abs(target.x - source.x) / 2;
1224
+ const centerX = target.x < source.x ? target.x + xOffset : target.x - xOffset;
1225
+ const yOffset = Math.abs(target.y - source.y) / 2;
1226
+ const centerY = target.y < source.y ? target.y + yOffset : target.y - yOffset;
1227
+ return [centerX, centerY, xOffset, yOffset];
1228
+ }
1229
+ const getDirection = ({ source, sourcePosition = 'bottom', target, }) => {
1230
+ if (sourcePosition === 'left' || sourcePosition === 'right') {
1231
+ return source.x < target.x ? { x: 1, y: 0 } : { x: -1, y: 0 };
1232
+ }
1233
+ return source.y < target.y ? { x: 0, y: 1 } : { x: 0, y: -1 };
1234
+ };
1235
+ const distance = (a, b) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
1236
+ // ith this function we try to mimic a orthogonal edge routing behaviour
1237
+ // It's not as good as a real orthogonal edge routing but it's faster and good enough as a default for step and smooth step edges
1238
+ function getPoints({ source, sourcePosition = 'bottom', target, targetPosition = 'top', offset, }) {
1239
+ const sourceDir = handleDirections[sourcePosition];
1240
+ const targetDir = handleDirections[targetPosition];
1241
+ const sourceGapped = { x: source.x + sourceDir.x * offset, y: source.y + sourceDir.y * offset };
1242
+ const targetGapped = { x: target.x + targetDir.x * offset, y: target.y + targetDir.y * offset };
1243
+ const dir = getDirection({
1244
+ source: sourceGapped,
1245
+ sourcePosition,
1246
+ target: targetGapped,
1247
+ });
1248
+ const dirAccessor = dir.x !== 0 ? 'x' : 'y';
1249
+ const currDir = dir[dirAccessor];
1250
+ let points = [];
1251
+ let centerX, centerY;
1252
+ const sourceGapOffset = { x: 0, y: 0 };
1253
+ const targetGapOffset = { x: 0, y: 0 };
1254
+ const [defaultCenterX, defaultCenterY] = getEdgeCenter(source, target);
1255
+ // opposite handle positions, default case
1256
+ if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
1257
+ centerX = defaultCenterX;
1258
+ centerY = defaultCenterY;
1259
+ // --->
1260
+ // |
1261
+ // >---
1262
+ const verticalSplit = [
1263
+ { x: centerX, y: sourceGapped.y },
1264
+ { x: centerX, y: targetGapped.y },
1265
+ ];
1266
+ // |
1267
+ // ---
1268
+ // |
1269
+ const horizontalSplit = [
1270
+ { x: sourceGapped.x, y: centerY },
1271
+ { x: targetGapped.x, y: centerY },
1272
+ ];
1273
+ if (sourceDir[dirAccessor] === currDir) {
1274
+ points = dirAccessor === 'x' ? verticalSplit : horizontalSplit;
1275
+ }
1276
+ else {
1277
+ points = dirAccessor === 'x' ? horizontalSplit : verticalSplit;
1278
+ }
1279
+ }
1280
+ else {
1281
+ // sourceTarget means we take x from source and y from target, targetSource is the opposite
1282
+ const sourceTarget = [{ x: sourceGapped.x, y: targetGapped.y }];
1283
+ const targetSource = [{ x: targetGapped.x, y: sourceGapped.y }];
1284
+ // this handles edges with same handle positions
1285
+ if (dirAccessor === 'x') {
1286
+ points = sourceDir.x === currDir ? targetSource : sourceTarget;
1287
+ }
1288
+ else {
1289
+ points = sourceDir.y === currDir ? sourceTarget : targetSource;
1290
+ }
1291
+ if (sourcePosition === targetPosition) {
1292
+ const diff = Math.abs(source[dirAccessor] - target[dirAccessor]);
1293
+ // if an edge goes from right to right for example (sourcePosition === targetPosition) and the distance between source.x and target.x is less than the offset, the added point and the gapped source/target will overlap. This leads to a weird edge path. To avoid this we add a gapOffset to the source/target
1294
+ if (diff <= offset) {
1295
+ const gapOffset = Math.min(offset - 1, offset - diff);
1296
+ if (sourceDir[dirAccessor] === currDir) {
1297
+ sourceGapOffset[dirAccessor] = (sourceGapped[dirAccessor] > source[dirAccessor] ? -1 : 1) * gapOffset;
1298
+ }
1299
+ else {
1300
+ targetGapOffset[dirAccessor] = (targetGapped[dirAccessor] > target[dirAccessor] ? -1 : 1) * gapOffset;
1301
+ }
1302
+ }
1303
+ }
1304
+ // these are conditions for handling mixed handle positions like Right -> Bottom for example
1305
+ if (sourcePosition !== targetPosition) {
1306
+ const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x';
1307
+ const isSameDir = sourceDir[dirAccessor] === targetDir[dirAccessorOpposite];
1308
+ const sourceGtTargetOppo = sourceGapped[dirAccessorOpposite] > targetGapped[dirAccessorOpposite];
1309
+ const sourceLtTargetOppo = sourceGapped[dirAccessorOpposite] < targetGapped[dirAccessorOpposite];
1310
+ const flipSourceTarget = (sourceDir[dirAccessor] === 1 && ((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) ||
1311
+ (sourceDir[dirAccessor] !== 1 && ((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo)));
1312
+ if (flipSourceTarget) {
1313
+ points = dirAccessor === 'x' ? sourceTarget : targetSource;
1314
+ }
1315
+ }
1316
+ const sourceGapPoint = { x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y };
1317
+ const targetGapPoint = { x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y };
1318
+ const maxXDistance = Math.max(Math.abs(sourceGapPoint.x - points[0].x), Math.abs(targetGapPoint.x - points[0].x));
1319
+ const maxYDistance = Math.max(Math.abs(sourceGapPoint.y - points[0].y), Math.abs(targetGapPoint.y - points[0].y));
1320
+ // we want to place the label on the longest segment of the edge
1321
+ if (maxXDistance >= maxYDistance) {
1322
+ centerX = (sourceGapPoint.x + targetGapPoint.x) / 2;
1323
+ centerY = points[0].y;
1324
+ }
1325
+ else {
1326
+ centerX = points[0].x;
1327
+ centerY = (sourceGapPoint.y + targetGapPoint.y) / 2;
1328
+ }
1329
+ }
1330
+ const pathPoints = [
1331
+ source,
1332
+ { x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y },
1333
+ ...points,
1334
+ { x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y },
1335
+ target,
1336
+ ];
1337
+ return [pathPoints, centerX, centerY];
1338
+ }
1339
+ function getBend(a, b, c, size) {
1340
+ const bendSize = Math.min(distance(a, b) / 2, distance(b, c) / 2, size);
1341
+ const { x, y } = b;
1342
+ // no bend
1343
+ if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) {
1344
+ return `L${x} ${y}`;
1345
+ }
1346
+ // first segment is horizontal
1347
+ if (a.y === y) {
1348
+ const xDir = a.x < c.x ? -1 : 1;
1349
+ const yDir = a.y < c.y ? 1 : -1;
1350
+ return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`;
1351
+ }
1352
+ const xDir = a.x < c.x ? 1 : -1;
1353
+ const yDir = a.y < c.y ? -1 : 1;
1354
+ return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
1355
+ }
1356
+ function smoothStepPath(source, target, sourcePosition, targetPosition, borderRadius = 5) {
1357
+ const [points, labelX, labelY] = getPoints({
1358
+ source,
1359
+ sourcePosition,
1360
+ target,
1361
+ targetPosition,
1362
+ offset: 20
1363
+ });
1364
+ const path = points.reduce((res, p, i) => {
1365
+ let segment = '';
1366
+ if (i > 0 && i < points.length - 1) {
1367
+ segment = getBend(points[i - 1], p, points[i + 1], borderRadius);
1368
+ }
1369
+ else {
1370
+ segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`;
1371
+ }
1372
+ res += segment;
1373
+ return res;
1374
+ }, '');
1375
+ return {
1376
+ path,
1377
+ points: {
1378
+ // TODO start and end points temporary unavailable for this path
1379
+ start: { x: labelX, y: labelY },
1380
+ center: { x: labelX, y: labelY },
1381
+ end: { x: labelX, y: labelY },
1382
+ }
1383
+ };
1384
+ }
1385
+
1212
1386
  class EdgeModel {
1213
1387
  constructor(edge) {
1214
1388
  this.edge = edge;
@@ -1278,6 +1452,10 @@ class EdgeModel {
1278
1452
  return straightPath(source.pointAbsolute(), target.pointAbsolute(), this.usingPoints);
1279
1453
  case 'bezier':
1280
1454
  return bezierPath(source.pointAbsolute(), target.pointAbsolute(), source.rawHandle.position, target.rawHandle.position, this.usingPoints);
1455
+ case 'smooth-step':
1456
+ return smoothStepPath(source.pointAbsolute(), target.pointAbsolute(), source.rawHandle.position, target.rawHandle.position);
1457
+ case 'step':
1458
+ return smoothStepPath(source.pointAbsolute(), target.pointAbsolute(), source.rawHandle.position, target.rawHandle.position, 0);
1281
1459
  }
1282
1460
  });
1283
1461
  this.edgeLabels = {};
@@ -1554,6 +1732,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
1554
1732
  args: ['onEdgesChange.select.many']
1555
1733
  }] } });
1556
1734
 
1735
+ function isGroupNode(node) {
1736
+ return node.node.type === 'default-group' || node.node.type === 'template-group';
1737
+ }
1738
+
1557
1739
  class NodeRenderingService {
1558
1740
  constructor() {
1559
1741
  this.flowEntitiesService = inject(FlowEntitiesService);
@@ -1561,6 +1743,12 @@ class NodeRenderingService {
1561
1743
  return this.flowEntitiesService.nodes()
1562
1744
  .sort((aNode, bNode) => aNode.renderOrder() - bNode.renderOrder());
1563
1745
  });
1746
+ this.groups = computed(() => {
1747
+ return this.nodes().filter(n => isGroupNode(n));
1748
+ });
1749
+ this.nonGroups = computed(() => {
1750
+ return this.nodes().filter(n => !isGroupNode(n));
1751
+ });
1564
1752
  this.maxOrder = computed(() => {
1565
1753
  return Math.max(...this.flowEntitiesService.nodes().map((n) => n.renderOrder()));
1566
1754
  });
@@ -1689,6 +1877,36 @@ function Microtask(target, key, descriptor) {
1689
1877
  return descriptor;
1690
1878
  }
1691
1879
 
1880
+ class OverlaysService {
1881
+ constructor() {
1882
+ this.toolbars = signal([]);
1883
+ this.nodeToolbars = computed(() => {
1884
+ const map = new Map();
1885
+ this.toolbars().forEach((toolbar) => {
1886
+ map.set(toolbar.node, toolbar);
1887
+ });
1888
+ return map;
1889
+ });
1890
+ }
1891
+ addToolbar(toolbar) {
1892
+ this.toolbars.update((toolbars) => [...toolbars, toolbar]);
1893
+ }
1894
+ removeToolbar(toolbar) {
1895
+ this.toolbars.update((toolbars) => toolbars.filter(t => t !== toolbar));
1896
+ }
1897
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: OverlaysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1898
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: OverlaysService }); }
1899
+ }
1900
+ __decorate([
1901
+ Microtask
1902
+ ], OverlaysService.prototype, "addToolbar", null);
1903
+ __decorate([
1904
+ Microtask
1905
+ ], OverlaysService.prototype, "removeToolbar", null);
1906
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: OverlaysService, decorators: [{
1907
+ type: Injectable
1908
+ }], propDecorators: { addToolbar: [], removeToolbar: [] } });
1909
+
1692
1910
  class HandleService {
1693
1911
  constructor() {
1694
1912
  this.node = signal(null);
@@ -1866,20 +2084,22 @@ class HandleComponent {
1866
2084
  this.injector = inject(Injector);
1867
2085
  this.handleService = inject(HandleService);
1868
2086
  this.element = inject(ElementRef).nativeElement;
2087
+ this.destroyRef = inject(DestroyRef);
1869
2088
  }
1870
2089
  ngOnInit() {
1871
- this.model = new HandleModel({
1872
- position: this.position,
1873
- type: this.type,
1874
- id: this.id,
1875
- parentReference: this.element.parentElement,
1876
- template: this.template
1877
- }, this.handleService.node());
1878
- this.handleService.createHandle(this.model);
1879
- requestAnimationFrame(() => this.model.updateParent());
1880
- }
1881
- ngOnDestroy() {
1882
- this.handleService.destroyHandle(this.model);
2090
+ const node = this.handleService.node();
2091
+ if (node) {
2092
+ this.model = new HandleModel({
2093
+ position: this.position,
2094
+ type: this.type,
2095
+ id: this.id,
2096
+ parentReference: this.element.parentElement,
2097
+ template: this.template
2098
+ }, node);
2099
+ this.handleService.createHandle(this.model);
2100
+ requestAnimationFrame(() => this.model.updateParent());
2101
+ this.destroyRef.onDestroy(() => this.handleService.destroyHandle(this.model));
2102
+ }
1883
2103
  }
1884
2104
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HandleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1885
2105
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: HandleComponent, selector: "handle", inputs: { position: "position", type: "type", id: "id", template: "template" }, ngImport: i0, template: "", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
@@ -2225,11 +2445,13 @@ class NodeComponent {
2225
2445
  this.hostRef = inject(ElementRef);
2226
2446
  this.connectionController = inject(ConnectionControllerDirective);
2227
2447
  this.nodeAccessor = inject(NodeAccessorService);
2448
+ this.overlaysService = inject(OverlaysService);
2228
2449
  this.zone = inject(NgZone);
2229
2450
  this.showMagnet = computed(() => this.flowStatusService.status().state === 'connection-start' ||
2230
2451
  this.flowStatusService.status().state === 'connection-validation');
2231
2452
  this.styleWidth = computed(() => `${this.nodeModel.size().width}px`);
2232
2453
  this.styleHeight = computed(() => `${this.nodeModel.size().height}px`);
2454
+ this.toolbar = computed(() => this.overlaysService.nodeToolbars().get(this.nodeModel));
2233
2455
  }
2234
2456
  ngOnInit() {
2235
2457
  this.nodeAccessor.model.set(this.nodeModel);
@@ -2287,7 +2509,7 @@ class NodeComponent {
2287
2509
  }
2288
2510
  }
2289
2511
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2290
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: NodeComponent, selector: "g[node]", inputs: { nodeModel: "nodeModel", nodeTemplate: "nodeTemplate", groupNodeTemplate: "groupNodeTemplate" }, providers: [HandleService, NodeAccessorService], viewQueries: [{ propertyName: "nodeContentRef", first: true, predicate: ["nodeContent"], descendants: true }, { propertyName: "htmlWrapperRef", first: true, predicate: ["htmlWrapper"], descendants: true }], ngImport: i0, template: "<!-- Default node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n class=\"selectable\"\n #nodeContent\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode(); selectNode()\"\n>\n <default-node\n #htmlWrapper\n [selected]=\"nodeModel.selected()\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n [style.max-width]=\"styleWidth()\"\n [style.max-height]=\"styleHeight()\"\n >\n <div [outerHTML]=\"nodeModel.text()\"></div>\n\n <handle type=\"source\" [position]=\"nodeModel.sourcePosition()\" />\n <handle type=\"target\" [position]=\"nodeModel.targetPosition()\" />\n </default-node>\n</svg:foreignObject>\n\n<!-- Template node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeTemplate\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode()\"\n>\n <div\n #htmlWrapper\n class=\"wrapper\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n >\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<!-- Component node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.isComponentType\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode()\"\n>\n <div\n #htmlWrapper\n class=\"wrapper\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n >\n <ng-container\n [ngComponentOutlet]=\"$any(nodeModel.node.type)\"\n [ngComponentOutletInputs]=\"nodeModel.componentTypeInputs()\"\n [ngComponentOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<!-- Default group node -->\n<svg:rect\n *ngIf=\"nodeModel.node.type === 'default-group'\"\n [resizable]=\"nodeModel.resizable()\"\n [gap]=\"3\"\n [resizerColor]=\"nodeModel.color()\"\n class=\"default-group-node\"\n rx=\"5\"\n ry=\"5\"\n [class.default-group-node_selected]=\"nodeModel.selected()\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n [style.stroke]=\"nodeModel.color()\"\n [style.fill]=\"nodeModel.color()\"\n (pointerStart)=\"pullNode(); selectNode()\"\n/>\n\n<!-- Template group node -->\n<svg:g\n *ngIf=\"nodeModel.node.type === 'template-group' && groupNodeTemplate\"\n class=\"selectable\"\n (pointerStart)=\"pullNode()\"\n>\n <ng-container\n [ngTemplateOutlet]=\"groupNodeTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected, width: nodeModel.width, height: nodeModel.height } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n</svg:g>\n\n<!-- Resizer -->\n<ng-container *ngIf=\"nodeModel.resizerTemplate() as template\">\n <ng-container *ngIf=\"nodeModel.resizable()\">\n <ng-template [ngTemplateOutlet]=\"template\" />\n </ng-container>\n</ng-container>\n\n<!-- Handles -->\n<ng-container *ngFor=\"let handle of nodeModel.handles()\">\n <svg:circle\n *ngIf=\"!handle.template\"\n class=\"default-handle\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n />\n\n <svg:g\n *ngIf=\"handle.template\"\n [handleSizeController]=\"handle\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n >\n <ng-container *ngTemplateOutlet=\"handle.template; context: handle.templateContext\" />\n </svg:g>\n\n <svg:circle\n *ngIf=\"showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n (pointerEnd)=\"endConnection(handle); resetValidateConnection(handle)\"\n (pointerOver)=\"validateConnection(handle)\"\n (pointerOut)=\"resetValidateConnection(handle)\"\n />\n</ng-container>\n", styles: [".magnet{opacity:0}.wrapper{display:table-cell}.default-group-node{stroke-width:1.5px;fill-opacity:.05}.default-group-node_selected{stroke-width:2px}.default-handle{stroke:#fff;fill:#1b262c}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { kind: "component", type: DefaultNodeComponent, selector: "default-node", inputs: ["selected"] }, { kind: "component", type: HandleComponent, selector: "handle", inputs: ["position", "type", "id", "template"] }, { kind: "component", type: ResizableComponent, selector: "[resizable]", inputs: ["resizable", "resizerColor", "gap"] }, { kind: "directive", type: HandleSizeControllerDirective, selector: "[handleSizeController]", inputs: ["handleSizeController"] }, { kind: "directive", type: PointerDirective, selector: "[pointerStart], [pointerEnd], [pointerOver], [pointerOut]", outputs: ["pointerOver", "pointerOut", "pointerStart", "pointerEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2512
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: NodeComponent, selector: "g[node]", inputs: { nodeModel: "nodeModel", nodeTemplate: "nodeTemplate", groupNodeTemplate: "groupNodeTemplate" }, host: { classAttribute: "vflow-node" }, providers: [HandleService, NodeAccessorService], viewQueries: [{ propertyName: "nodeContentRef", first: true, predicate: ["nodeContent"], descendants: true }, { propertyName: "htmlWrapperRef", first: true, predicate: ["htmlWrapper"], descendants: true }], ngImport: i0, template: "<!-- Default node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n class=\"selectable\"\n #nodeContent\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode(); selectNode()\"\n>\n <default-node\n #htmlWrapper\n [selected]=\"nodeModel.selected()\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n [style.max-width]=\"styleWidth()\"\n [style.max-height]=\"styleHeight()\"\n >\n <div [outerHTML]=\"nodeModel.text()\"></div>\n\n <handle type=\"source\" [position]=\"nodeModel.sourcePosition()\" />\n <handle type=\"target\" [position]=\"nodeModel.targetPosition()\" />\n </default-node>\n</svg:foreignObject>\n\n<!-- Template node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeTemplate\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode()\"\n>\n <div\n #htmlWrapper\n class=\"wrapper\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n >\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<!-- Component node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.isComponentType\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode()\"\n>\n <div\n #htmlWrapper\n class=\"wrapper\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n >\n <ng-container\n [ngComponentOutlet]=\"$any(nodeModel.node.type)\"\n [ngComponentOutletInputs]=\"nodeModel.componentTypeInputs()\"\n [ngComponentOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<!-- Default group node -->\n<svg:rect\n *ngIf=\"nodeModel.node.type === 'default-group'\"\n [resizable]=\"nodeModel.resizable()\"\n [gap]=\"3\"\n [resizerColor]=\"nodeModel.color()\"\n class=\"default-group-node\"\n rx=\"5\"\n ry=\"5\"\n [class.default-group-node_selected]=\"nodeModel.selected()\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n [style.stroke]=\"nodeModel.color()\"\n [style.fill]=\"nodeModel.color()\"\n (pointerStart)=\"pullNode(); selectNode()\"\n/>\n\n<!-- Template group node -->\n<svg:g\n *ngIf=\"nodeModel.node.type === 'template-group' && groupNodeTemplate\"\n class=\"selectable\"\n (pointerStart)=\"pullNode()\"\n>\n <ng-container\n [ngTemplateOutlet]=\"groupNodeTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected, width: nodeModel.width, height: nodeModel.height } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n</svg:g>\n\n<!-- Resizer -->\n<ng-container *ngIf=\"nodeModel.resizerTemplate() as template\">\n <ng-container *ngIf=\"nodeModel.resizable()\">\n <ng-template [ngTemplateOutlet]=\"template\" />\n </ng-container>\n</ng-container>\n\n<!-- Handles -->\n<ng-container *ngFor=\"let handle of nodeModel.handles()\">\n <svg:circle\n *ngIf=\"!handle.template\"\n class=\"default-handle\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n />\n\n <svg:g\n *ngIf=\"handle.template\"\n [handleSizeController]=\"handle\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n >\n <ng-container *ngTemplateOutlet=\"handle.template; context: handle.templateContext\" />\n </svg:g>\n\n <svg:circle\n *ngIf=\"showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n (pointerEnd)=\"endConnection(handle); resetValidateConnection(handle)\"\n (pointerOver)=\"validateConnection(handle)\"\n (pointerOut)=\"resetValidateConnection(handle)\"\n />\n</ng-container>\n\n<!-- Toolbar -->\n<svg:foreignObject\n *ngIf=\"toolbar() as toolbar\"\n [attr.width]=\"toolbar.size().width\"\n [attr.height]=\"toolbar.size().height\"\n [attr.transform]=\"toolbar.transform()\"\n>\n <ng-container [ngTemplateOutlet]=\"toolbar.template()\" />\n</svg:foreignObject>\n", styles: [".magnet{opacity:0}.wrapper{display:table-cell}.default-group-node{stroke-width:1.5px;fill-opacity:.05}.default-group-node_selected{stroke-width:2px}.default-handle{stroke:#fff;fill:#1b262c}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { kind: "component", type: DefaultNodeComponent, selector: "default-node", inputs: ["selected"] }, { kind: "component", type: HandleComponent, selector: "handle", inputs: ["position", "type", "id", "template"] }, { kind: "component", type: ResizableComponent, selector: "[resizable]", inputs: ["resizable", "resizerColor", "gap"] }, { kind: "directive", type: HandleSizeControllerDirective, selector: "[handleSizeController]", inputs: ["handleSizeController"] }, { kind: "directive", type: PointerDirective, selector: "[pointerStart], [pointerEnd], [pointerOver], [pointerOut]", outputs: ["pointerOver", "pointerOut", "pointerStart", "pointerEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2291
2513
  }
2292
2514
  __decorate([
2293
2515
  InjectionContext
@@ -2297,7 +2519,9 @@ __decorate([
2297
2519
  ], NodeComponent.prototype, "ngAfterViewInit", null);
2298
2520
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, decorators: [{
2299
2521
  type: Component,
2300
- args: [{ selector: 'g[node]', changeDetection: ChangeDetectionStrategy.OnPush, providers: [HandleService, NodeAccessorService], template: "<!-- Default node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n class=\"selectable\"\n #nodeContent\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode(); selectNode()\"\n>\n <default-node\n #htmlWrapper\n [selected]=\"nodeModel.selected()\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n [style.max-width]=\"styleWidth()\"\n [style.max-height]=\"styleHeight()\"\n >\n <div [outerHTML]=\"nodeModel.text()\"></div>\n\n <handle type=\"source\" [position]=\"nodeModel.sourcePosition()\" />\n <handle type=\"target\" [position]=\"nodeModel.targetPosition()\" />\n </default-node>\n</svg:foreignObject>\n\n<!-- Template node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeTemplate\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode()\"\n>\n <div\n #htmlWrapper\n class=\"wrapper\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n >\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<!-- Component node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.isComponentType\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode()\"\n>\n <div\n #htmlWrapper\n class=\"wrapper\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n >\n <ng-container\n [ngComponentOutlet]=\"$any(nodeModel.node.type)\"\n [ngComponentOutletInputs]=\"nodeModel.componentTypeInputs()\"\n [ngComponentOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<!-- Default group node -->\n<svg:rect\n *ngIf=\"nodeModel.node.type === 'default-group'\"\n [resizable]=\"nodeModel.resizable()\"\n [gap]=\"3\"\n [resizerColor]=\"nodeModel.color()\"\n class=\"default-group-node\"\n rx=\"5\"\n ry=\"5\"\n [class.default-group-node_selected]=\"nodeModel.selected()\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n [style.stroke]=\"nodeModel.color()\"\n [style.fill]=\"nodeModel.color()\"\n (pointerStart)=\"pullNode(); selectNode()\"\n/>\n\n<!-- Template group node -->\n<svg:g\n *ngIf=\"nodeModel.node.type === 'template-group' && groupNodeTemplate\"\n class=\"selectable\"\n (pointerStart)=\"pullNode()\"\n>\n <ng-container\n [ngTemplateOutlet]=\"groupNodeTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected, width: nodeModel.width, height: nodeModel.height } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n</svg:g>\n\n<!-- Resizer -->\n<ng-container *ngIf=\"nodeModel.resizerTemplate() as template\">\n <ng-container *ngIf=\"nodeModel.resizable()\">\n <ng-template [ngTemplateOutlet]=\"template\" />\n </ng-container>\n</ng-container>\n\n<!-- Handles -->\n<ng-container *ngFor=\"let handle of nodeModel.handles()\">\n <svg:circle\n *ngIf=\"!handle.template\"\n class=\"default-handle\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n />\n\n <svg:g\n *ngIf=\"handle.template\"\n [handleSizeController]=\"handle\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n >\n <ng-container *ngTemplateOutlet=\"handle.template; context: handle.templateContext\" />\n </svg:g>\n\n <svg:circle\n *ngIf=\"showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n (pointerEnd)=\"endConnection(handle); resetValidateConnection(handle)\"\n (pointerOver)=\"validateConnection(handle)\"\n (pointerOut)=\"resetValidateConnection(handle)\"\n />\n</ng-container>\n", styles: [".magnet{opacity:0}.wrapper{display:table-cell}.default-group-node{stroke-width:1.5px;fill-opacity:.05}.default-group-node_selected{stroke-width:2px}.default-handle{stroke:#fff;fill:#1b262c}\n"] }]
2522
+ args: [{ selector: 'g[node]', changeDetection: ChangeDetectionStrategy.OnPush, providers: [HandleService, NodeAccessorService], host: {
2523
+ 'class': 'vflow-node',
2524
+ }, template: "<!-- Default node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'default'\"\n class=\"selectable\"\n #nodeContent\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode(); selectNode()\"\n>\n <default-node\n #htmlWrapper\n [selected]=\"nodeModel.selected()\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n [style.max-width]=\"styleWidth()\"\n [style.max-height]=\"styleHeight()\"\n >\n <div [outerHTML]=\"nodeModel.text()\"></div>\n\n <handle type=\"source\" [position]=\"nodeModel.sourcePosition()\" />\n <handle type=\"target\" [position]=\"nodeModel.targetPosition()\" />\n </default-node>\n</svg:foreignObject>\n\n<!-- Template node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.node.type === 'html-template' && nodeTemplate\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode()\"\n>\n <div\n #htmlWrapper\n class=\"wrapper\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n >\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<!-- Component node -->\n<svg:foreignObject\n *ngIf=\"nodeModel.isComponentType\"\n class=\"selectable\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n (pointerStart)=\"pullNode()\"\n>\n <div\n #htmlWrapper\n class=\"wrapper\"\n [style.width]=\"styleWidth()\"\n [style.height]=\"styleHeight()\"\n >\n <ng-container\n [ngComponentOutlet]=\"$any(nodeModel.node.type)\"\n [ngComponentOutletInputs]=\"nodeModel.componentTypeInputs()\"\n [ngComponentOutletInjector]=\"injector\"\n />\n </div>\n</svg:foreignObject>\n\n<!-- Default group node -->\n<svg:rect\n *ngIf=\"nodeModel.node.type === 'default-group'\"\n [resizable]=\"nodeModel.resizable()\"\n [gap]=\"3\"\n [resizerColor]=\"nodeModel.color()\"\n class=\"default-group-node\"\n rx=\"5\"\n ry=\"5\"\n [class.default-group-node_selected]=\"nodeModel.selected()\"\n [attr.width]=\"nodeModel.size().width\"\n [attr.height]=\"nodeModel.size().height\"\n [style.stroke]=\"nodeModel.color()\"\n [style.fill]=\"nodeModel.color()\"\n (pointerStart)=\"pullNode(); selectNode()\"\n/>\n\n<!-- Template group node -->\n<svg:g\n *ngIf=\"nodeModel.node.type === 'template-group' && groupNodeTemplate\"\n class=\"selectable\"\n (pointerStart)=\"pullNode()\"\n>\n <ng-container\n [ngTemplateOutlet]=\"groupNodeTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: { node: nodeModel.node, selected: nodeModel.selected, width: nodeModel.width, height: nodeModel.height } }\"\n [ngTemplateOutletInjector]=\"injector\"\n />\n</svg:g>\n\n<!-- Resizer -->\n<ng-container *ngIf=\"nodeModel.resizerTemplate() as template\">\n <ng-container *ngIf=\"nodeModel.resizable()\">\n <ng-template [ngTemplateOutlet]=\"template\" />\n </ng-container>\n</ng-container>\n\n<!-- Handles -->\n<ng-container *ngFor=\"let handle of nodeModel.handles()\">\n <svg:circle\n *ngIf=\"!handle.template\"\n class=\"default-handle\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n [attr.stroke-width]=\"handle.strokeWidth\"\n r=\"5\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n />\n\n <svg:g\n *ngIf=\"handle.template\"\n [handleSizeController]=\"handle\"\n (pointerStart)=\"startConnection($event, handle)\"\n (pointerEnd)=\"endConnection(handle)\"\n >\n <ng-container *ngTemplateOutlet=\"handle.template; context: handle.templateContext\" />\n </svg:g>\n\n <svg:circle\n *ngIf=\"showMagnet()\"\n class=\"magnet\"\n [attr.r]=\"nodeModel.magnetRadius\"\n [attr.cx]=\"handle.offset().x\"\n [attr.cy]=\"handle.offset().y\"\n (pointerEnd)=\"endConnection(handle); resetValidateConnection(handle)\"\n (pointerOver)=\"validateConnection(handle)\"\n (pointerOut)=\"resetValidateConnection(handle)\"\n />\n</ng-container>\n\n<!-- Toolbar -->\n<svg:foreignObject\n *ngIf=\"toolbar() as toolbar\"\n [attr.width]=\"toolbar.size().width\"\n [attr.height]=\"toolbar.size().height\"\n [attr.transform]=\"toolbar.transform()\"\n>\n <ng-container [ngTemplateOutlet]=\"toolbar.template()\" />\n</svg:foreignObject>\n", styles: [".magnet{opacity:0}.wrapper{display:table-cell}.default-group-node{stroke-width:1.5px;fill-opacity:.05}.default-group-node_selected{stroke-width:2px}.default-handle{stroke:#fff;fill:#1b262c}\n"] }]
2301
2525
  }], propDecorators: { nodeModel: [{
2302
2526
  type: Input
2303
2527
  }], nodeTemplate: [{
@@ -2430,6 +2654,8 @@ class ConnectionComponent {
2430
2654
  switch (this.model.curve) {
2431
2655
  case 'straight': return straightPath(sourcePoint, targetPoint).path;
2432
2656
  case 'bezier': return bezierPath(sourcePoint, targetPoint, sourcePosition, targetPosition).path;
2657
+ case 'smooth-step': return smoothStepPath(sourcePoint, targetPoint, sourcePosition, targetPosition).path;
2658
+ case 'step': return smoothStepPath(sourcePoint, targetPoint, sourcePosition, targetPosition, 0).path;
2433
2659
  }
2434
2660
  }
2435
2661
  if (status.state === 'connection-validation') {
@@ -2447,6 +2673,8 @@ class ConnectionComponent {
2447
2673
  switch (this.model.curve) {
2448
2674
  case 'straight': return straightPath(sourcePoint, targetPoint).path;
2449
2675
  case 'bezier': return bezierPath(sourcePoint, targetPoint, sourcePosition, targetPosition).path;
2676
+ case 'smooth-step': return smoothStepPath(sourcePoint, targetPoint, sourcePosition, targetPosition).path;
2677
+ case 'step': return smoothStepPath(sourcePoint, targetPoint, sourcePosition, targetPosition, 0).path;
2450
2678
  }
2451
2679
  }
2452
2680
  return null;
@@ -2552,12 +2780,15 @@ const defaultBg = '#fff';
2552
2780
  const defaultGap = 20;
2553
2781
  const defaultDotSize = 2;
2554
2782
  const defaultDotColor = 'rgb(177, 177, 183)';
2783
+ const defaultImageScale = 0.1;
2784
+ const defaultRepeated = true;
2555
2785
  class BackgroundComponent {
2556
2786
  constructor() {
2557
2787
  this.viewportService = inject(ViewportService);
2558
2788
  this.rootSvg = inject(RootSvgReferenceDirective).element;
2559
2789
  this.settingsService = inject(FlowSettingsService);
2560
2790
  this.backgroundSignal = this.settingsService.background;
2791
+ // DOTS PATTERN
2561
2792
  this.scaledGap = computed(() => {
2562
2793
  const background = this.backgroundSignal();
2563
2794
  if (background.type === 'dots') {
@@ -2568,7 +2799,13 @@ class BackgroundComponent {
2568
2799
  });
2569
2800
  this.x = computed(() => this.viewportService.readableViewport().x % this.scaledGap());
2570
2801
  this.y = computed(() => this.viewportService.readableViewport().y % this.scaledGap());
2571
- this.patternColor = computed(() => this.backgroundSignal().color ?? defaultDotColor);
2802
+ this.patternColor = computed(() => {
2803
+ const bg = this.backgroundSignal();
2804
+ if (bg.type === 'dots') {
2805
+ return bg.color ?? defaultDotColor;
2806
+ }
2807
+ return defaultDotColor;
2808
+ });
2572
2809
  this.patternSize = computed(() => {
2573
2810
  const background = this.backgroundSignal();
2574
2811
  if (background.type === 'dots') {
@@ -2576,6 +2813,56 @@ class BackgroundComponent {
2576
2813
  }
2577
2814
  return 0;
2578
2815
  });
2816
+ // IMAGE PATTERN
2817
+ this.bgImageSrc = computed(() => {
2818
+ const background = this.backgroundSignal();
2819
+ return background.type === 'image' ? background.src : '';
2820
+ });
2821
+ this.imageSize = toSignal(toObservable(this.backgroundSignal).pipe(switchMap(() => createImage(this.bgImageSrc())), map((image) => ({ width: image.naturalWidth, height: image.naturalHeight }))), { initialValue: { width: 0, height: 0 } });
2822
+ this.scaledImageWidth = computed(() => {
2823
+ const background = this.backgroundSignal();
2824
+ if (background.type === 'image') {
2825
+ const zoom = background.fixed ? 1 : this.viewportService.readableViewport().zoom;
2826
+ return this.imageSize().width * zoom * (background.scale ?? defaultImageScale);
2827
+ }
2828
+ return 0;
2829
+ });
2830
+ this.scaledImageHeight = computed(() => {
2831
+ const background = this.backgroundSignal();
2832
+ if (background.type === 'image') {
2833
+ const zoom = background.fixed ? 1 : this.viewportService.readableViewport().zoom;
2834
+ return this.imageSize().height * zoom * (background.scale ?? defaultImageScale);
2835
+ }
2836
+ return 0;
2837
+ });
2838
+ this.imageX = computed(() => {
2839
+ const background = this.backgroundSignal();
2840
+ if (background.type === 'image') {
2841
+ if (!background.repeat) {
2842
+ return background.fixed ? 0 : this.viewportService.readableViewport().x;
2843
+ }
2844
+ return background.fixed
2845
+ ? 0
2846
+ : this.viewportService.readableViewport().x % this.scaledImageWidth();
2847
+ }
2848
+ return 0;
2849
+ });
2850
+ this.imageY = computed(() => {
2851
+ const background = this.backgroundSignal();
2852
+ if (background.type === 'image') {
2853
+ if (!background.repeat) {
2854
+ return background.fixed ? 0 : this.viewportService.readableViewport().y;
2855
+ }
2856
+ return background.fixed
2857
+ ? 0
2858
+ : this.viewportService.readableViewport().y % this.scaledImageHeight();
2859
+ }
2860
+ return 0;
2861
+ });
2862
+ this.repeated = computed(() => {
2863
+ const background = this.backgroundSignal();
2864
+ return background.type === 'image' && (background.repeat ?? defaultRepeated);
2865
+ });
2579
2866
  // Without ID there will be pattern collision for several flows on the page
2580
2867
  // Later pattern ID may be exposed to API
2581
2868
  this.patternId = id();
@@ -2591,12 +2878,19 @@ class BackgroundComponent {
2591
2878
  });
2592
2879
  }
2593
2880
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: BackgroundComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2594
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: BackgroundComponent, selector: "g[background]", ngImport: i0, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n", dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2881
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: BackgroundComponent, selector: "g[background]", ngImport: i0, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n\n<ng-container *ngIf=\"backgroundSignal().type === 'image'\">\n <ng-container *ngIf=\"repeated()\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:image\n [attr.href]=\"bgImageSrc()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n </ng-container>\n\n <ng-container *ngIf=\"!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 </ng-container>\n</ng-container>\n", dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2595
2882
  }
2596
2883
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: BackgroundComponent, decorators: [{
2597
2884
  type: Component,
2598
- args: [{ selector: 'g[background]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n" }]
2885
+ args: [{ selector: 'g[background]', changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"backgroundSignal().type === 'dots'\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"x()\"\n [attr.y]=\"y()\"\n [attr.width]=\"scaledGap()\"\n [attr.height]=\"scaledGap()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:circle\n [attr.cx]=\"patternSize()\"\n [attr.cy]=\"patternSize()\"\n [attr.r]=\"patternSize()\"\n [attr.fill]=\"patternColor()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n</ng-container>\n\n<ng-container *ngIf=\"backgroundSignal().type === 'image'\">\n <ng-container *ngIf=\"repeated()\">\n <svg:pattern\n [attr.id]=\"patternId\"\n [attr.x]=\"imageX()\"\n [attr.y]=\"imageY()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n patternUnits=\"userSpaceOnUse\"\n >\n <svg:image\n [attr.href]=\"bgImageSrc()\"\n [attr.width]=\"scaledImageWidth()\"\n [attr.height]=\"scaledImageHeight()\"\n />\n </svg:pattern>\n\n <svg:rect\n x=\"0\"\n y=\"0\"\n width=\"100%\"\n height=\"100%\"\n [attr.fill]=\"patternUrl\"\n />\n </ng-container>\n\n <ng-container *ngIf=\"!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 </ng-container>\n</ng-container>\n" }]
2599
2886
  }], ctorParameters: function () { return []; } });
2887
+ function createImage(url) {
2888
+ const image = new Image();
2889
+ image.src = url;
2890
+ return new Promise(resolve => {
2891
+ image.onload = () => resolve(image);
2892
+ });
2893
+ }
2600
2894
 
2601
2895
  // TODO: too general purpose nane
2602
2896
  class RootSvgContextDirective {
@@ -2706,9 +3000,12 @@ class VflowComponent {
2706
3000
  this.keyboardService = inject(KeyboardService);
2707
3001
  this.injector = inject(Injector);
2708
3002
  this.optimization = {
2709
- computeLayersOnInit: true
3003
+ computeLayersOnInit: true,
3004
+ detachedGroupsLayer: false
2710
3005
  };
2711
3006
  this.nodeModels = computed(() => this.nodeRenderingService.nodes());
3007
+ this.groups = computed(() => this.nodeRenderingService.groups());
3008
+ this.nonGroups = computed(() => this.nodeRenderingService.nonGroups());
2712
3009
  this.edgeModels = computed(() => this.flowEntitiesService.validEdges());
2713
3010
  // #endregion
2714
3011
  // #region OUTPUTS
@@ -2910,8 +3207,9 @@ class VflowComponent {
2910
3207
  SelectionService,
2911
3208
  FlowSettingsService,
2912
3209
  ComponentEventBusService,
2913
- KeyboardService
2914
- ], queries: [{ propertyName: "nodeTemplateDirective", first: true, predicate: NodeHtmlTemplateDirective, descendants: true }, { propertyName: "groupNodeTemplateDirective", first: true, predicate: GroupNodeTemplateDirective, 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 }, { propertyName: "spacePointContext", first: true, predicate: SpacePointContextDirective, descendants: true }], hostDirectives: [{ directive: ConnectionControllerDirective, outputs: ["onConnect", "onConnect"] }, { directive: ChangesControllerDirective, outputs: ["onNodesChange", "onNodesChange", "onNodesChange.position", "onNodesChange.position", "onNodesChange.position.single", "onNodesChange.position.single", "onNodesChange.position.many", "onNodesChange.position.many", "onNodesChange.size", "onNodesChange.size", "onNodesChange.size.single", "onNodesChange.size.single", "onNodesChange.size.many", "onNodesChange.size.many", "onNodesChange.add", "onNodesChange.add", "onNodesChange.add.single", "onNodesChange.add.single", "onNodesChange.add.many", "onNodesChange.add.many", "onNodesChange.remove", "onNodesChange.remove", "onNodesChange.remove.single", "onNodesChange.remove.single", "onNodesChange.remove.many", "onNodesChange.remove.many", "onNodesChange.select", "onNodesChange.select", "onNodesChange.select.single", "onNodesChange.select.single", "onNodesChange.select.many", "onNodesChange.select.many", "onEdgesChange", "onEdgesChange", "onEdgesChange.detached", "onEdgesChange.detached", "onEdgesChange.detached.single", "onEdgesChange.detached.single", "onEdgesChange.detached.many", "onEdgesChange.detached.many", "onEdgesChange.add", "onEdgesChange.add", "onEdgesChange.add.single", "onEdgesChange.add.single", "onEdgesChange.add.many", "onEdgesChange.add.many", "onEdgesChange.remove", "onEdgesChange.remove", "onEdgesChange.remove.single", "onEdgesChange.remove.single", "onEdgesChange.remove.many", "onEdgesChange.remove.many", "onEdgesChange.select", "onEdgesChange.select", "onEdgesChange.select.single", "onEdgesChange.select.single", "onEdgesChange.select.many", "onEdgesChange.select.many"] }], ngImport: i0, template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n flowSizeController\n class=\"root-svg\"\n #flow\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g background />\n\n <svg:g\n mapContext\n spacePointContext\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 [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </svg:g>\n\n <!-- Minimap -->\n <ng-container *ngIf=\"minimap() as minimap\">\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n </ng-container>\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.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["nodeModel", "nodeTemplate", "groupNodeTemplate"] }, { 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: "component", type: BackgroundComponent, selector: "g[background]" }, { kind: "directive", type: SpacePointContextDirective, selector: "g[spacePointContext]" }, { kind: "directive", type: MapContextDirective, selector: "g[mapContext]" }, { 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]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3210
+ KeyboardService,
3211
+ OverlaysService
3212
+ ], queries: [{ propertyName: "nodeTemplateDirective", first: true, predicate: NodeHtmlTemplateDirective, descendants: true }, { propertyName: "groupNodeTemplateDirective", first: true, predicate: GroupNodeTemplateDirective, 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 }, { propertyName: "spacePointContext", first: true, predicate: SpacePointContextDirective, descendants: true }], hostDirectives: [{ directive: ConnectionControllerDirective, outputs: ["onConnect", "onConnect"] }, { directive: ChangesControllerDirective, outputs: ["onNodesChange", "onNodesChange", "onNodesChange.position", "onNodesChange.position", "onNodesChange.position.single", "onNodesChange.position.single", "onNodesChange.position.many", "onNodesChange.position.many", "onNodesChange.size", "onNodesChange.size", "onNodesChange.size.single", "onNodesChange.size.single", "onNodesChange.size.many", "onNodesChange.size.many", "onNodesChange.add", "onNodesChange.add", "onNodesChange.add.single", "onNodesChange.add.single", "onNodesChange.add.many", "onNodesChange.add.many", "onNodesChange.remove", "onNodesChange.remove", "onNodesChange.remove.single", "onNodesChange.remove.single", "onNodesChange.remove.many", "onNodesChange.remove.many", "onNodesChange.select", "onNodesChange.select", "onNodesChange.select.single", "onNodesChange.select.single", "onNodesChange.select.many", "onNodesChange.select.many", "onEdgesChange", "onEdgesChange", "onEdgesChange.detached", "onEdgesChange.detached", "onEdgesChange.detached.single", "onEdgesChange.detached.single", "onEdgesChange.detached.many", "onEdgesChange.detached.many", "onEdgesChange.add", "onEdgesChange.add", "onEdgesChange.add.single", "onEdgesChange.add.single", "onEdgesChange.add.many", "onEdgesChange.add.many", "onEdgesChange.remove", "onEdgesChange.remove", "onEdgesChange.remove.single", "onEdgesChange.remove.single", "onEdgesChange.remove.many", "onEdgesChange.remove.many", "onEdgesChange.select", "onEdgesChange.select", "onEdgesChange.select.single", "onEdgesChange.select.single", "onEdgesChange.select.many", "onEdgesChange.select.many"] }], ngImport: i0, template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n flowSizeController\n class=\"root-svg\"\n #flow\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g background />\n\n <svg:g\n mapContext\n spacePointContext\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <ng-container *ngIf=\"optimization.detachedGroupsLayer\">\n <!-- Groups -->\n <svg:g\n *ngFor=\"let model of groups(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\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 nonGroups(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </ng-container>\n\n <ng-container *ngIf=\"!optimization.detachedGroupsLayer\">\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 [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </ng-container>\n\n </svg:g>\n\n <!-- Minimap -->\n <ng-container *ngIf=\"minimap() as minimap\">\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n </ng-container>\n</svg:svg>\n\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.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["nodeModel", "nodeTemplate", "groupNodeTemplate"] }, { 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: "component", type: BackgroundComponent, selector: "g[background]" }, { kind: "directive", type: SpacePointContextDirective, selector: "g[spacePointContext]" }, { kind: "directive", type: MapContextDirective, selector: "g[mapContext]" }, { 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]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2915
3213
  }
2916
3214
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, decorators: [{
2917
3215
  type: Component,
@@ -2926,11 +3224,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2926
3224
  SelectionService,
2927
3225
  FlowSettingsService,
2928
3226
  ComponentEventBusService,
2929
- KeyboardService
3227
+ KeyboardService,
3228
+ OverlaysService
2930
3229
  ], hostDirectives: [
2931
3230
  connectionControllerHostDirective,
2932
3231
  changesControllerHostDirective
2933
- ], template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n flowSizeController\n class=\"root-svg\"\n #flow\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g background />\n\n <svg:g\n mapContext\n spacePointContext\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 [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </svg:g>\n\n <!-- Minimap -->\n <ng-container *ngIf=\"minimap() as minimap\">\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n </ng-container>\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"] }]
3232
+ ], template: "<svg:svg\n rootSvgRef\n rootSvgContext\n rootPointer\n flowSizeController\n class=\"root-svg\"\n #flow\n>\n <defs [markers]=\"markers()\" flowDefs />\n\n <g background />\n\n <svg:g\n mapContext\n spacePointContext\n >\n <!-- Connection -->\n <svg:g\n connection\n [model]=\"connection\"\n [template]=\"connectionTemplateDirective?.templateRef\"\n />\n\n <ng-container *ngIf=\"optimization.detachedGroupsLayer\">\n <!-- Groups -->\n <svg:g\n *ngFor=\"let model of groups(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\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 nonGroups(); trackBy: trackNodes\"\n node\n [nodeModel]=\"model\"\n [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </ng-container>\n\n <ng-container *ngIf=\"!optimization.detachedGroupsLayer\">\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 [nodeTemplate]=\"nodeTemplateDirective?.templateRef\"\n [groupNodeTemplate]=\"groupNodeTemplateDirective?.templateRef\"\n [attr.transform]=\"model.pointTransform()\"\n />\n </ng-container>\n\n </svg:g>\n\n <!-- Minimap -->\n <ng-container *ngIf=\"minimap() as minimap\">\n <ng-container [ngTemplateOutlet]=\"minimap.template()\" />\n </ng-container>\n</svg:svg>\n\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}\n"] }]
2934
3233
  }], propDecorators: { view: [{
2935
3234
  type: Input
2936
3235
  }], minZoom: [{
@@ -3043,7 +3342,13 @@ class MiniMapComponent {
3043
3342
  }
3044
3343
  return 0.2;
3045
3344
  });
3046
- this.viewportColor = computed(() => this.flowSettingsService.background().color ?? '#fff');
3345
+ this.viewportColor = computed(() => {
3346
+ const bg = this.flowSettingsService.background();
3347
+ if (bg.type === 'dots' || bg.type === 'solid') {
3348
+ return bg.color ?? '#fff';
3349
+ }
3350
+ return '#fff';
3351
+ });
3047
3352
  this.hovered = signal(false);
3048
3353
  this.minimapPoint = computed(() => {
3049
3354
  switch (this.minimapPosition()) {
@@ -3131,6 +3436,125 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
3131
3436
  args: ['minimap', { static: true }]
3132
3437
  }] } });
3133
3438
 
3439
+ class ToolbarModel {
3440
+ constructor(node) {
3441
+ this.node = node;
3442
+ this.position = signal('top');
3443
+ this.template = signal(null);
3444
+ this.offset = signal(10);
3445
+ this.point = computed(() => {
3446
+ switch (this.position()) {
3447
+ case 'top':
3448
+ return {
3449
+ x: this.node.size().width / 2 - this.size().width / 2,
3450
+ y: -this.size().height - this.offset()
3451
+ };
3452
+ case 'bottom':
3453
+ return {
3454
+ x: this.node.size().width / 2 - this.size().width / 2,
3455
+ y: this.node.size().height + this.offset()
3456
+ };
3457
+ case 'left':
3458
+ return {
3459
+ x: -this.size().width - this.offset(),
3460
+ y: this.node.size().height / 2 - this.size().height / 2
3461
+ };
3462
+ case 'right':
3463
+ return {
3464
+ x: this.node.size().width + this.offset(),
3465
+ y: this.node.size().height / 2 - this.size().height / 2
3466
+ };
3467
+ }
3468
+ });
3469
+ this.transform = computed(() => `translate(${this.point().x}, ${this.point().y})`);
3470
+ this.size = signal({ width: 0, height: 0 });
3471
+ }
3472
+ }
3473
+
3474
+ class NodeToolbarComponent {
3475
+ constructor() {
3476
+ this.overlaysService = inject(OverlaysService);
3477
+ this.nodeService = inject(NodeAccessorService);
3478
+ this.model = new ToolbarModel(this.nodeService.model());
3479
+ }
3480
+ set position(value) {
3481
+ this.model.position.set(value);
3482
+ }
3483
+ ngOnInit() {
3484
+ this.model.template.set(this.toolbarContentTemplate);
3485
+ this.overlaysService.addToolbar(this.model);
3486
+ }
3487
+ ngOnDestroy() {
3488
+ this.overlaysService.removeToolbar(this.model);
3489
+ }
3490
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3491
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: NodeToolbarComponent, selector: "node-toolbar", inputs: { position: "position" }, viewQueries: [{ propertyName: "toolbarContentTemplate", first: true, predicate: ["toolbar"], descendants: true, static: true }], ngImport: i0, template: `
3492
+ <ng-template #toolbar>
3493
+ <div class="wrapper" nodeToolbarWrapper [model]="model">
3494
+ <ng-content />
3495
+ </div>
3496
+ </ng-template>
3497
+ `, isInline: true, styles: [".wrapper{width:max-content}\n"], dependencies: [{ kind: "directive", type: i0.forwardRef(function () { return NodeToolbarWrapperDirective; }), selector: "[nodeToolbarWrapper]", inputs: ["model"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3498
+ }
3499
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeToolbarComponent, decorators: [{
3500
+ type: Component,
3501
+ args: [{ selector: 'node-toolbar', template: `
3502
+ <ng-template #toolbar>
3503
+ <div class="wrapper" nodeToolbarWrapper [model]="model">
3504
+ <ng-content />
3505
+ </div>
3506
+ </ng-template>
3507
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".wrapper{width:max-content}\n"] }]
3508
+ }], propDecorators: { position: [{
3509
+ type: Input
3510
+ }], toolbarContentTemplate: [{
3511
+ type: ViewChild,
3512
+ args: ['toolbar', { static: true }]
3513
+ }] } });
3514
+ class NodeToolbarWrapperDirective {
3515
+ constructor() {
3516
+ this.element = inject(ElementRef);
3517
+ }
3518
+ ngOnInit() {
3519
+ this.model.size.set({
3520
+ width: this.element.nativeElement.clientWidth,
3521
+ height: this.element.nativeElement.clientHeight
3522
+ });
3523
+ }
3524
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeToolbarWrapperDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
3525
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: NodeToolbarWrapperDirective, selector: "[nodeToolbarWrapper]", inputs: { model: "model" }, ngImport: i0 }); }
3526
+ }
3527
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeToolbarWrapperDirective, decorators: [{
3528
+ type: Directive,
3529
+ args: [{ selector: '[nodeToolbarWrapper]' }]
3530
+ }], propDecorators: { model: [{
3531
+ type: Input
3532
+ }] } });
3533
+
3534
+ class DragHandleDirective {
3535
+ get model() {
3536
+ return this.nodeAccessor.model();
3537
+ }
3538
+ constructor() {
3539
+ this.nodeAccessor = inject(NodeAccessorService);
3540
+ this.model.dragHandlesCount.update((count) => count + 1);
3541
+ inject(DestroyRef).onDestroy(() => {
3542
+ this.model.dragHandlesCount.update(count => count - 1);
3543
+ });
3544
+ }
3545
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DragHandleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
3546
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: DragHandleDirective, selector: "[dragHandle]", host: { classAttribute: "vflow-drag-handle" }, ngImport: i0 }); }
3547
+ }
3548
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DragHandleDirective, decorators: [{
3549
+ type: Directive,
3550
+ args: [{
3551
+ selector: '[dragHandle]',
3552
+ host: {
3553
+ 'class': 'vflow-drag-handle'
3554
+ }
3555
+ }]
3556
+ }], ctorParameters: function () { return []; } });
3557
+
3134
3558
  const components = [
3135
3559
  VflowComponent,
3136
3560
  NodeComponent,
@@ -3142,7 +3566,8 @@ const components = [
3142
3566
  DefsComponent,
3143
3567
  BackgroundComponent,
3144
3568
  ResizableComponent,
3145
- MiniMapComponent
3569
+ MiniMapComponent,
3570
+ NodeToolbarComponent
3146
3571
  ];
3147
3572
  const directives = [
3148
3573
  SpacePointContextDirective,
@@ -3151,9 +3576,11 @@ const directives = [
3151
3576
  RootSvgContextDirective,
3152
3577
  HandleSizeControllerDirective,
3153
3578
  SelectableDirective,
3579
+ DragHandleDirective,
3154
3580
  PointerDirective,
3155
3581
  RootPointerDirective,
3156
3582
  FlowSizeControllerDirective,
3583
+ NodeToolbarWrapperDirective
3157
3584
  ];
3158
3585
  const templateDirectives = [
3159
3586
  NodeHtmlTemplateDirective,
@@ -3175,15 +3602,18 @@ class VflowModule {
3175
3602
  DefsComponent,
3176
3603
  BackgroundComponent,
3177
3604
  ResizableComponent,
3178
- MiniMapComponent, SpacePointContextDirective,
3605
+ MiniMapComponent,
3606
+ NodeToolbarComponent, SpacePointContextDirective,
3179
3607
  MapContextDirective,
3180
3608
  RootSvgReferenceDirective,
3181
3609
  RootSvgContextDirective,
3182
3610
  HandleSizeControllerDirective,
3183
3611
  SelectableDirective,
3612
+ DragHandleDirective,
3184
3613
  PointerDirective,
3185
3614
  RootPointerDirective,
3186
- FlowSizeControllerDirective, NodeHtmlTemplateDirective,
3615
+ FlowSizeControllerDirective,
3616
+ NodeToolbarWrapperDirective, NodeHtmlTemplateDirective,
3187
3617
  GroupNodeTemplateDirective,
3188
3618
  EdgeLabelHtmlTemplateDirective,
3189
3619
  EdgeTemplateDirective,
@@ -3192,7 +3622,9 @@ class VflowModule {
3192
3622
  HandleComponent,
3193
3623
  ResizableComponent,
3194
3624
  SelectableDirective,
3195
- MiniMapComponent, NodeHtmlTemplateDirective,
3625
+ MiniMapComponent,
3626
+ NodeToolbarComponent,
3627
+ DragHandleDirective, NodeHtmlTemplateDirective,
3196
3628
  GroupNodeTemplateDirective,
3197
3629
  EdgeLabelHtmlTemplateDirective,
3198
3630
  EdgeTemplateDirective,
@@ -3210,17 +3642,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
3210
3642
  ResizableComponent,
3211
3643
  SelectableDirective,
3212
3644
  MiniMapComponent,
3645
+ NodeToolbarComponent,
3646
+ DragHandleDirective,
3213
3647
  ...templateDirectives
3214
3648
  ],
3215
3649
  declarations: [...components, ...directives, ...templateDirectives],
3216
3650
  }]
3217
3651
  }] });
3218
3652
 
3653
+ const mockModel = () => new NodeModel({ id: 'mock', type: 'default', point: { x: 0, y: 0 } });
3654
+ function provideCustomNodeMocks() {
3655
+ return [
3656
+ {
3657
+ provide: ComponentEventBusService,
3658
+ useValue: {
3659
+ pushEvent: () => { }
3660
+ }
3661
+ },
3662
+ {
3663
+ provide: HandleService,
3664
+ useFactory: () => ({
3665
+ node: signal(mockModel()),
3666
+ createHandle: () => { },
3667
+ destroyHandle: () => { },
3668
+ })
3669
+ },
3670
+ {
3671
+ provide: RootPointerDirective,
3672
+ useValue: {
3673
+ pointerMovement$: of({
3674
+ x: 0,
3675
+ y: 0,
3676
+ movementX: 0,
3677
+ movementY: 0,
3678
+ target: null,
3679
+ originalEvent: null
3680
+ }),
3681
+ documentPointerEnd$: of(null)
3682
+ }
3683
+ },
3684
+ {
3685
+ provide: SpacePointContextDirective,
3686
+ useValue: {
3687
+ documentPointToFlowPoint: (point) => point
3688
+ }
3689
+ },
3690
+ {
3691
+ provide: NodeAccessorService,
3692
+ useFactory: () => ({
3693
+ model: signal(mockModel())
3694
+ })
3695
+ },
3696
+ {
3697
+ provide: SelectionService,
3698
+ useValue: {
3699
+ select: () => { },
3700
+ }
3701
+ },
3702
+ FlowSettingsService,
3703
+ FlowEntitiesService,
3704
+ ViewportService
3705
+ ];
3706
+ }
3707
+
3219
3708
  // Modules
3220
3709
 
3221
3710
  /**
3222
3711
  * Generated bundle index. Do not edit.
3223
3712
  */
3224
3713
 
3225
- export { ChangesControllerDirective, ConnectionControllerDirective, ConnectionTemplateDirective, CustomDynamicNodeComponent, CustomNodeComponent, EdgeLabelHtmlTemplateDirective, EdgeTemplateDirective, GroupNodeTemplateDirective, HandleComponent, HandleTemplateDirective, MiniMapComponent, NodeHtmlTemplateDirective, ResizableComponent, SelectableDirective, VflowComponent, VflowModule, isComponentDynamicNode, isComponentStaticNode, isDefaultDynamicGroupNode, isDefaultDynamicNode, isDefaultStaticGroupNode, isDefaultStaticNode, isDynamicNode, isStaticNode, isTemplateDynamicGroupNode, isTemplateDynamicNode, isTemplateStaticGroupNode, isTemplateStaticNode };
3714
+ export { ChangesControllerDirective, ConnectionControllerDirective, ConnectionTemplateDirective, CustomDynamicNodeComponent, CustomNodeComponent, DragHandleDirective, EdgeLabelHtmlTemplateDirective, EdgeTemplateDirective, GroupNodeTemplateDirective, HandleComponent, HandleTemplateDirective, MiniMapComponent, NodeHtmlTemplateDirective, NodeToolbarComponent, NodeToolbarWrapperDirective, ResizableComponent, SelectableDirective, VflowComponent, VflowModule, isComponentDynamicNode, isComponentStaticNode, isDefaultDynamicGroupNode, isDefaultDynamicNode, isDefaultStaticGroupNode, isDefaultStaticNode, isDynamicNode, isStaticNode, isTemplateDynamicGroupNode, isTemplateDynamicNode, isTemplateStaticGroupNode, isTemplateStaticNode, provideCustomNodeMocks };
3226
3715
  //# sourceMappingURL=ngx-vflow.mjs.map