ngx-vflow 0.13.0-0 → 0.13.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 (26) hide show
  1. package/esm2022/lib/vflow/components/node/node.component.mjs +3 -3
  2. package/esm2022/lib/vflow/components/vflow/vflow.component.mjs +13 -4
  3. package/esm2022/lib/vflow/directives/root-pointer.directive.mjs +1 -1
  4. package/esm2022/lib/vflow/directives/selectable.directive.mjs +5 -2
  5. package/esm2022/lib/vflow/public-components/resizable/resizable.component.mjs +124 -180
  6. package/esm2022/lib/vflow/services/draggable.service.mjs +40 -19
  7. package/esm2022/lib/vflow/services/keyboard.service.mjs +45 -0
  8. package/esm2022/lib/vflow/services/node-changes.service.mjs +6 -7
  9. package/esm2022/lib/vflow/services/selection.service.mjs +12 -5
  10. package/esm2022/lib/vflow/types/keyboard-action.type.mjs +2 -0
  11. package/esm2022/lib/vflow/utils/get-os.mjs +24 -0
  12. package/esm2022/public-api.mjs +2 -1
  13. package/fesm2022/ngx-vflow.mjs +257 -214
  14. package/fesm2022/ngx-vflow.mjs.map +1 -1
  15. package/lib/vflow/components/vflow/vflow.component.d.ts +4 -1
  16. package/lib/vflow/directives/root-pointer.directive.d.ts +10 -16
  17. package/lib/vflow/directives/space-point-context.directive.d.ts +1 -15
  18. package/lib/vflow/models/edge.model.d.ts +17 -1
  19. package/lib/vflow/public-components/resizable/resizable.component.d.ts +6 -11
  20. package/lib/vflow/services/draggable.service.d.ts +2 -0
  21. package/lib/vflow/services/keyboard.service.d.ts +11 -0
  22. package/lib/vflow/services/selection.service.d.ts +1 -0
  23. package/lib/vflow/types/keyboard-action.type.d.ts +2 -0
  24. package/lib/vflow/utils/get-os.d.ts +1 -0
  25. package/package.json +1 -1
  26. package/public-api.d.ts +1 -0
@@ -4,8 +4,8 @@ import * as i0 from '@angular/core';
4
4
  import { signal, computed, Injectable, inject, ElementRef, Directive, effect, untracked, TemplateRef, EventEmitter, Output, DestroyRef, Input, runInInjectionContext, Injector, Component, 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 { Subject, tap, merge, observeOn, animationFrameScheduler, switchMap, skip, map, pairwise, filter, distinctUntilChanged, asyncScheduler, zip, fromEvent, share, Observable, startWith } from 'rxjs';
8
- import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
7
+ import { switchMap, merge, fromEvent, tap, Subject, observeOn, animationFrameScheduler, skip, map, pairwise, filter, distinctUntilChanged, asyncScheduler, zip, share, Observable, startWith } from 'rxjs';
8
+ import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
9
9
  import { drag } from 'd3-drag';
10
10
  import { path } from 'd3-path';
11
11
  import { __decorate } from 'tslib';
@@ -249,9 +249,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
249
249
  }]
250
250
  }] });
251
251
 
252
+ function getOS() {
253
+ const userAgent = window.navigator.userAgent.toLowerCase();
254
+ const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos)/i;
255
+ const windowsPlatforms = /(win32|win64|windows|wince)/i;
256
+ const iosPlatforms = /(iphone|ipad|ipod)/i;
257
+ let os = null;
258
+ if (macosPlatforms.test(userAgent)) {
259
+ os = "macos";
260
+ }
261
+ else if (iosPlatforms.test(userAgent)) {
262
+ os = "ios";
263
+ }
264
+ else if (windowsPlatforms.test(userAgent)) {
265
+ os = "windows";
266
+ }
267
+ else if (/android/.test(userAgent)) {
268
+ os = "android";
269
+ }
270
+ else if (!os && /linux/.test(userAgent)) {
271
+ os = "linux";
272
+ }
273
+ return os;
274
+ }
275
+
276
+ class KeyboardService {
277
+ constructor() {
278
+ this.actions = signal({
279
+ multiSelection: [
280
+ getOS() === 'macos' ? 'MetaLeft' : 'ControlLeft',
281
+ getOS() === 'macos' ? 'MetaRight' : 'ControlRight',
282
+ ]
283
+ });
284
+ this.actionsActive = {
285
+ multiSelection: false
286
+ };
287
+ toObservable(this.actions).pipe(switchMap(() => merge(fromEvent(document, 'keydown').pipe(tap(event => {
288
+ for (const action in this.actions()) {
289
+ const keyCodes = this.actions()[action] ?? [];
290
+ if (keyCodes.includes(event.code)) {
291
+ this.actionsActive[action] = true;
292
+ }
293
+ }
294
+ })), fromEvent(document, 'keyup').pipe(tap(event => {
295
+ for (const action in this.actions()) {
296
+ const keyCodes = this.actions()[action] ?? [];
297
+ if (keyCodes.includes(event.code)) {
298
+ this.actionsActive[action] = false;
299
+ }
300
+ }
301
+ })))), takeUntilDestroyed()).subscribe();
302
+ }
303
+ setShortcuts(newActions) {
304
+ this.actions.update((actions) => ({ ...actions, ...newActions }));
305
+ }
306
+ isActiveAction(action) {
307
+ return this.actionsActive[action];
308
+ }
309
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: KeyboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
310
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: KeyboardService }); }
311
+ }
312
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: KeyboardService, decorators: [{
313
+ type: Injectable
314
+ }], ctorParameters: function () { return []; } });
315
+
252
316
  class SelectionService {
253
317
  constructor() {
254
318
  this.flowEntitiesService = inject(FlowEntitiesService);
319
+ this.keyboardService = inject(KeyboardService);
255
320
  this.viewport$ = new Subject();
256
321
  this.resetSelection = this.viewport$.pipe(tap(({ start, end, target }) => {
257
322
  if (start && end && target) {
@@ -273,10 +338,15 @@ class SelectionService {
273
338
  this.viewport$.next(viewport);
274
339
  }
275
340
  select(entity) {
276
- // undo select for previously selected nodes
277
- this.flowEntitiesService.entities()
278
- .filter(n => n.selected)
279
- .forEach(n => n.selected.set(false));
341
+ // ? May be not a responsibility of this method
342
+ // if entity already selected - do nothing
343
+ if (entity?.selected()) {
344
+ return;
345
+ }
346
+ if (!this.keyboardService.isActiveAction('multiSelection')) {
347
+ // undo select for previously selected nodes
348
+ this.flowEntitiesService.entities().forEach(n => n.selected.set(false));
349
+ }
280
350
  if (entity) {
281
351
  // select passed entity
282
352
  entity.selected.set(true);
@@ -377,6 +447,9 @@ const evTarget = (anyEvent) => {
377
447
  const round = (num) => Math.round(num * 100) / 100;
378
448
 
379
449
  class DraggableService {
450
+ constructor() {
451
+ this.entitiesService = inject(FlowEntitiesService);
452
+ }
380
453
  /**
381
454
  * Enable draggable behavior for element.
382
455
  *
@@ -412,27 +485,24 @@ class DraggableService {
412
485
  * @returns
413
486
  */
414
487
  getDragBehavior(model) {
415
- let deltaX;
416
- let deltaY;
488
+ let dragNodes = [];
489
+ let initialPositions = [];
417
490
  return drag()
418
491
  .on('start', (event) => {
419
- deltaX = model.point().x - event.x;
420
- deltaY = model.point().y - event.y;
492
+ dragNodes = this.getDragNodes(model);
493
+ initialPositions = dragNodes.map(node => ({
494
+ x: node.point().x - event.x,
495
+ y: node.point().y - event.y
496
+ }));
421
497
  })
422
498
  .on('drag', (event) => {
423
- let point = {
424
- x: round(event.x + deltaX),
425
- y: round(event.y + deltaY)
426
- };
427
- const parent = model.parent();
428
- // keep node in bounds of parent
429
- if (parent) {
430
- point.x = Math.min(parent.size().width - model.size().width, point.x);
431
- point.x = Math.max(0, point.x);
432
- point.y = Math.min(parent.size().height - model.size().height, point.y);
433
- point.y = Math.max(0, point.y);
434
- }
435
- model.setPoint(point, true);
499
+ dragNodes.forEach((model, index) => {
500
+ let point = {
501
+ x: round(event.x + initialPositions[index].x),
502
+ y: round(event.y + initialPositions[index].y)
503
+ };
504
+ moveNode(model, point);
505
+ });
436
506
  });
437
507
  }
438
508
  /**
@@ -445,12 +515,32 @@ class DraggableService {
445
515
  event.sourceEvent.stopPropagation();
446
516
  });
447
517
  }
518
+ getDragNodes(model) {
519
+ return model.selected()
520
+ ? this.entitiesService
521
+ .nodes()
522
+ // selected draggable nodes (with current node)
523
+ .filter(node => node.selected() && node.draggable())
524
+ // we only can move current node if it's not selected
525
+ : [model];
526
+ }
448
527
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DraggableService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
449
528
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DraggableService }); }
450
529
  }
451
530
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DraggableService, decorators: [{
452
531
  type: Injectable
453
532
  }] });
533
+ function moveNode(model, point) {
534
+ const parent = model.parent();
535
+ // keep node in bounds of parent
536
+ if (parent) {
537
+ point.x = Math.min(parent.size().width - model.size().width, point.x);
538
+ point.x = Math.max(0, point.x);
539
+ point.y = Math.min(parent.size().height - model.size().height, point.y);
540
+ point.y = Math.max(0, point.y);
541
+ }
542
+ model.setPoint(point, true);
543
+ }
454
544
 
455
545
  class EdgeTemplateDirective {
456
546
  constructor() {
@@ -1247,12 +1337,11 @@ class NodesChangeService {
1247
1337
  // Check for nodes list change and watch for specific node from this list change its position
1248
1338
  switchMap((nodes) => merge(...nodes.map(node => node.point$.pipe(
1249
1339
  // skip initial position from signal
1250
- skip(1), map(() => node))))),
1251
- // For now it's a single node, later this list will also be filled
1252
- // with child node position changes
1253
- map(changedNode => [
1254
- { type: 'position', id: changedNode.node.id, point: changedNode.point() }
1255
- ]));
1340
+ skip(1), map(() => node))))), map((changedNode) => {
1341
+ return this.entitiesService.nodes()
1342
+ .filter(node => node === changedNode || node.selected())
1343
+ .map(node => ({ type: 'position', id: node.node.id, point: node.point() }));
1344
+ }));
1256
1345
  this.nodeSizeChange$ = toObservable(this.entitiesService.nodes)
1257
1346
  .pipe(switchMap((nodes) => merge(...nodes.map(node => node.size$.pipe(skip(1), map(() => node))))), map(changedNode => [
1258
1347
  { type: 'size', id: changedNode.node.id, size: changedNode.size() }
@@ -1878,6 +1967,7 @@ class ResizableComponent {
1878
1967
  this.nodeAccessor = inject(NodeAccessorService);
1879
1968
  this.rootPointer = inject(RootPointerDirective);
1880
1969
  this.viewportService = inject(ViewportService);
1970
+ this.spacePointContext = inject(SpacePointContextDirective);
1881
1971
  this.hostRef = inject(ElementRef);
1882
1972
  this.resizerColor = '#2e414c';
1883
1973
  this.gap = 1.5;
@@ -1889,7 +1979,7 @@ class ResizableComponent {
1889
1979
  this.minHeight = 0;
1890
1980
  // TODO: allow reszie beside the flow
1891
1981
  this.resizeOnGlobalMouseMove = this.rootPointer.pointerMovement$
1892
- .pipe(filter(() => this.resizeSide !== null), tap((event) => this.resize(event)), takeUntilDestroyed())
1982
+ .pipe(filter(() => this.resizeSide !== null), filter((event) => event.movementX !== 0 || event.movementY !== 0), tap((event) => this.resize(event)), takeUntilDestroyed())
1893
1983
  .subscribe();
1894
1984
  this.endResizeOnGlobalMouseUp = this.rootPointer.documentPointerEnd$
1895
1985
  .pipe(tap(() => this.endResize()), takeUntilDestroyed())
@@ -1918,193 +2008,55 @@ class ResizableComponent {
1918
2008
  this.resizeSide = side;
1919
2009
  this.model.resizing.set(true);
1920
2010
  }
1921
- resize({ movementX, movementY }) {
1922
- const offsetX = round(movementX / this.zoom());
1923
- const offsetY = round(movementY / this.zoom());
1924
- switch (this.resizeSide) {
1925
- case 'left':
1926
- let x = this.model.point().x + offsetX;
1927
- x = Math.max(x, this.getMinX());
1928
- x = Math.min(x, this.getMaxX());
1929
- // TODO this fixes increasing width when current node hits the parent
1930
- if (x === this.getMinX() || x === this.getMaxX()) {
1931
- return;
1932
- }
1933
- this.model.setPoint({ x, y: this.model.point().y }, false);
1934
- this.model.size.update(({ height, width }) => {
1935
- width -= offsetX;
1936
- width = Math.max(width, this.minWidth);
1937
- width = Math.min(width, this.getMaxWidth());
1938
- return { height, width: width };
1939
- });
1940
- return;
1941
- case 'right':
1942
- this.model.size.update(({ height, width }) => {
1943
- width += offsetX;
1944
- width = Math.max(width, this.minWidth);
1945
- width = Math.min(width, this.getMaxWidth());
1946
- const bounds = getNodesBounds(this.model.children());
1947
- width = Math.max(width, bounds.x + bounds.width);
1948
- return { height, width };
1949
- });
1950
- return;
1951
- case 'top':
1952
- let y = this.model.point().y + offsetY;
1953
- y = Math.max(y, this.getMinY());
1954
- y = Math.min(y, this.getMaxY());
1955
- if (y === this.getMinY() || y === this.getMaxY()) {
1956
- return;
1957
- }
1958
- this.model.setPoint({ x: this.model.point().x, y }, false);
1959
- this.model.size.update(({ height, width }) => {
1960
- height -= offsetY;
1961
- height = Math.max(height, this.minHeight);
1962
- height = Math.min(height, this.getMaxHeight());
1963
- return { width, height };
1964
- });
1965
- return;
1966
- case 'bottom':
1967
- this.model.size.update(({ height, width }) => {
1968
- height += offsetY;
1969
- height = Math.max(height, this.minHeight);
1970
- height = Math.min(height, this.getMaxHeight());
1971
- const bounds = getNodesBounds(this.model.children());
1972
- height = Math.max(height, bounds.y + bounds.height);
1973
- return { width, height };
1974
- });
1975
- return;
1976
- case 'top-left': {
1977
- let x = this.model.point().x + offsetX;
1978
- x = Math.max(x, this.getMinX());
1979
- x = Math.min(x, this.getMaxX());
1980
- let y = this.model.point().y + offsetY;
1981
- y = Math.max(y, this.getMinY());
1982
- y = Math.min(y, this.getMaxY());
1983
- if (x === this.getMinX() || y === this.getMinY() ||
1984
- x === this.getMaxX() || y === this.getMaxY()) {
1985
- return;
1986
- }
1987
- this.model.setPoint({ x, y }, false);
1988
- this.model.size.update(({ height, width }) => {
1989
- width -= offsetX;
1990
- width = Math.max(width, this.minWidth);
1991
- width = Math.min(width, this.getMaxWidth());
1992
- height -= offsetY;
1993
- height = Math.max(height, this.minHeight);
1994
- height = Math.min(height, this.getMaxHeight());
1995
- return { height, width };
1996
- });
1997
- return;
1998
- }
1999
- case 'top-right': {
2000
- let y = this.model.point().y + offsetY;
2001
- y = Math.max(y, this.getMinY());
2002
- y = Math.min(y, this.getMaxY());
2003
- if (y === this.getMinX() || y === this.getMaxY()) {
2004
- return;
2005
- }
2006
- this.model.setPoint({ x: this.model.point().x, y }, false);
2007
- this.model.size.update(({ height, width }) => {
2008
- const bounds = getNodesBounds(this.model.children());
2009
- width += offsetX;
2010
- width = Math.max(width, this.minWidth);
2011
- width = Math.min(width, this.getMaxWidth());
2012
- width = Math.max(width, bounds.x + bounds.width);
2013
- height -= offsetY;
2014
- height = Math.max(height, this.minHeight);
2015
- height = Math.min(height, this.getMaxHeight());
2016
- return { height, width };
2017
- });
2018
- return;
2019
- }
2020
- case 'bottom-left': {
2021
- let x = this.model.point().x + offsetX;
2022
- x = Math.max(x, this.getMinX());
2023
- x = Math.min(x, this.getMaxX());
2024
- if (x === this.getMinX() || x === this.getMaxX()) {
2025
- return;
2026
- }
2027
- this.model.setPoint({ x, y: this.model.point().y }, false);
2028
- this.model.size.update(({ height, width }) => {
2029
- width -= offsetX;
2030
- width = Math.max(width, this.minWidth);
2031
- width = Math.min(width, this.getMaxWidth());
2032
- height += offsetY;
2033
- height = Math.max(height, this.minHeight);
2034
- height = Math.min(height, this.getMaxHeight());
2035
- const bounds = getNodesBounds(this.model.children());
2036
- height = Math.max(height, bounds.y + bounds.height);
2037
- return { height, width };
2038
- });
2039
- return;
2040
- }
2041
- case 'bottom-right': {
2042
- this.model.size.update(({ height, width }) => {
2043
- const bounds = getNodesBounds(this.model.children());
2044
- width += offsetX;
2045
- width = Math.max(width, this.minWidth);
2046
- width = Math.min(width, this.getMaxWidth());
2047
- width = Math.max(width, bounds.x + bounds.width);
2048
- height += offsetY;
2049
- height = Math.max(height, this.minHeight);
2050
- height = Math.min(height, this.getMaxHeight());
2051
- height = Math.max(height, bounds.y + bounds.height);
2052
- return { height, width };
2053
- });
2054
- }
2055
- }
2011
+ resize(event) {
2012
+ if (!this.resizeSide)
2013
+ return;
2014
+ if (this.isResizeConstrained(event))
2015
+ return;
2016
+ const offset = calcOffset(event.movementX, event.movementY, this.zoom());
2017
+ const { x, y, width, height } = constrainRect(applyResize(this.resizeSide, this.model, offset), this.model, this.resizeSide, this.minWidth, this.minHeight);
2018
+ this.model.setPoint({ x, y }, false);
2019
+ this.model.size.set({ width, height });
2056
2020
  }
2057
2021
  endResize() {
2058
2022
  this.resizeSide = null;
2059
2023
  this.model.resizing.set(false);
2060
2024
  }
2061
- getMaxWidth() {
2062
- const parent = this.model.parent();
2063
- if (parent) {
2064
- return parent.size().width - this.model.point().x;
2065
- }
2066
- return Infinity;
2067
- }
2068
- getMaxHeight() {
2069
- const parent = this.model.parent();
2070
- if (parent) {
2071
- return parent.size().height - this.model.point().y;
2072
- }
2073
- return Infinity;
2074
- }
2075
- getMinX() {
2076
- const parent = this.model.parent();
2077
- if (parent) {
2078
- return 0;
2025
+ isResizeConstrained({ x, y, movementX, movementY }) {
2026
+ const flowPoint = this.spacePointContext.documentPointToFlowPoint({ x, y });
2027
+ if (this.resizeSide?.includes('right')) {
2028
+ if (movementX > 0 && flowPoint.x < (this.model.point().x + this.model.size().width)) {
2029
+ return true;
2030
+ }
2031
+ if (movementX < 0 && flowPoint.x > this.model.point().x + this.model.size().width) {
2032
+ return true;
2033
+ }
2079
2034
  }
2080
- return -Infinity;
2081
- }
2082
- getMinY() {
2083
- const parent = this.model.parent();
2084
- if (parent) {
2085
- return 0;
2035
+ if (this.resizeSide?.includes('left')) {
2036
+ if (movementX < 0 && flowPoint.x > this.model.point().x) {
2037
+ return true;
2038
+ }
2039
+ if (movementX > 0 && flowPoint.x < this.model.point().x) {
2040
+ return true;
2041
+ }
2086
2042
  }
2087
- return -Infinity;
2088
- }
2089
- getMaxX() {
2090
- const x = this.model.point().x;
2091
- const width = this.model.size().width;
2092
- const children = this.model.children();
2093
- if (children) {
2094
- const bounds = getNodesBounds(children);
2095
- return x + (bounds.x + bounds.width) >= x + width ? x : (width - this.minWidth) + x;
2043
+ if (this.resizeSide?.includes('bottom')) {
2044
+ if (movementY > 0 && flowPoint.y < (this.model.point().y + this.model.size().height)) {
2045
+ return true;
2046
+ }
2047
+ if (movementY < 0 && flowPoint.y > this.model.point().y + this.model.size().height) {
2048
+ return true;
2049
+ }
2096
2050
  }
2097
- return (width - this.minWidth) + x;
2098
- }
2099
- getMaxY() {
2100
- const y = this.model.point().y;
2101
- const height = this.model.size().height;
2102
- const children = this.model.children();
2103
- if (children) {
2104
- const bounds = getNodesBounds(children);
2105
- return y + (bounds.y + bounds.height) >= y + height ? y : (height - this.minHeight) + y;
2051
+ if (this.resizeSide?.includes('top')) {
2052
+ if (movementY < 0 && flowPoint.y > this.model.point().y) {
2053
+ return true;
2054
+ }
2055
+ if (movementY > 0 && flowPoint.y < this.model.point().y) {
2056
+ return true;
2057
+ }
2106
2058
  }
2107
- return (height - this.minHeight) + y;
2059
+ return false;
2108
2060
  }
2109
2061
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ResizableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2110
2062
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ResizableComponent, selector: "[resizable]", inputs: { resizable: "resizable", resizerColor: "resizerColor", gap: "gap" }, viewQueries: [{ propertyName: "resizer", first: true, predicate: ["resizer"], descendants: true, static: true }], ngImport: i0, template: "<ng-template #resizer>\n <svg:g>\n <!-- top line -->\n <svg:line\n class=\"top\"\n [attr.x1]=\"lineGap\"\n [attr.y1]=\"-gap\"\n [attr.x2]=\"model.size().width - lineGap\"\n [attr.y2]=\"-gap\"\n [attr.stroke]=\"resizerColor\"\n stroke-width=\"2\"\n (pointerStart)=\"startResize('top', $event)\"\n />\n <!-- Left line -->\n <svg:line\n class=\"left\"\n [attr.x1]=\"-gap\"\n [attr.y1]=\"lineGap\"\n [attr.x2]=\"-gap\"\n [attr.y2]=\"model.size().height - lineGap\"\n [attr.stroke]=\"resizerColor\"\n stroke-width=\"2\"\n (pointerStart)=\"startResize('left', $event)\"\n />\n <!-- Bottom line -->\n <svg:line\n class=\"bottom\"\n [attr.x1]=\"lineGap\"\n [attr.y1]=\"model.size().height + gap\"\n [attr.x2]=\"model.size().width - lineGap\"\n [attr.y2]=\"model.size().height + gap\"\n [attr.stroke]=\"resizerColor\"\n stroke-width=\"2\"\n (pointerStart)=\"startResize('bottom', $event)\"\n />\n <!-- Right line -->\n <svg:line\n class=\"right\"\n [attr.x1]=\"model.size().width + gap\"\n [attr.y1]=\"lineGap\"\n [attr.x2]=\"model.size().width + gap\"\n [attr.y2]=\"model.size().height - lineGap\"\n [attr.stroke]=\"resizerColor\"\n stroke-width=\"2\"\n (pointerStart)=\"startResize('right', $event)\"\n />\n\n <!-- Top Left -->\n <svg:rect\n class=\"top-left\"\n [attr.x]=\"-(handleSize / 2) - gap\"\n [attr.y]=\"-(handleSize / 2) - gap\"\n [attr.width]=\"handleSize\"\n [attr.height]=\"handleSize\"\n [attr.fill]=\"resizerColor\"\n (pointerStart)=\"startResize('top-left', $event)\"\n />\n\n <!-- Top right -->\n <svg:rect\n class=\"top-right\"\n [attr.x]=\"model.size().width - (handleSize / 2) + gap\"\n [attr.y]=\"-(handleSize / 2) - gap\"\n [attr.width]=\"handleSize\"\n [attr.height]=\"handleSize\"\n [attr.fill]=\"resizerColor\"\n (pointerStart)=\"startResize('top-right', $event)\"\n />\n\n <!-- Bottom left -->\n <svg:rect\n class=\"bottom-left\"\n [attr.x]=\"-(handleSize / 2) - gap\"\n [attr.y]=\"model.size().height - (handleSize / 2) + gap\"\n [attr.width]=\"handleSize\"\n [attr.height]=\"handleSize\"\n [attr.fill]=\"resizerColor\"\n (pointerStart)=\"startResize('bottom-left', $event)\"\n />\n\n <!-- Bottom right -->\n <svg:rect\n class=\"bottom-right\"\n [attr.x]=\"model.size().width - (handleSize / 2) + gap\"\n [attr.y]=\"model.size().height - (handleSize / 2) + gap\"\n [attr.width]=\"handleSize\"\n [attr.height]=\"handleSize\"\n [attr.fill]=\"resizerColor\"\n (pointerStart)=\"startResize('bottom-right', $event)\"\n />\n </svg:g>\n</ng-template>\n\n<ng-content />\n", styles: [".top{cursor:n-resize}.left{cursor:w-resize}.right{cursor:e-resize}.bottom{cursor:s-resize}.top-left{cursor:nw-resize}.top-right{cursor:ne-resize}.bottom-left{cursor:sw-resize}.bottom-right{cursor:se-resize}\n"], dependencies: [{ kind: "directive", type: PointerDirective, selector: "[pointerStart], [pointerEnd], [pointerOver], [pointerOut]", outputs: ["pointerOver", "pointerOut", "pointerStart", "pointerEnd"] }] }); }
@@ -2125,6 +2077,86 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2125
2077
  type: ViewChild,
2126
2078
  args: ['resizer', { static: true }]
2127
2079
  }], ngAfterViewInit: [] } });
2080
+ function calcOffset(movementX, movementY, zoom) {
2081
+ return {
2082
+ offsetX: round(movementX / zoom),
2083
+ offsetY: round(movementY / zoom)
2084
+ };
2085
+ }
2086
+ function applyResize(side, model, offset) {
2087
+ const { offsetX, offsetY } = offset;
2088
+ const { x, y } = model.point();
2089
+ const { width, height } = model.size();
2090
+ // Handle each case of resizing (top, bottom, left, right, corners)
2091
+ switch (side) {
2092
+ case 'left':
2093
+ return { x: x + offsetX, y, width: width - offsetX, height };
2094
+ case 'right':
2095
+ return { x, y, width: width + offsetX, height };
2096
+ case 'top':
2097
+ return { x, y: y + offsetY, width, height: height - offsetY };
2098
+ case 'bottom':
2099
+ return { x, y, width, height: height + offsetY };
2100
+ case 'top-left':
2101
+ return { x: x + offsetX, y: y + offsetY, width: width - offsetX, height: height - offsetY };
2102
+ case 'top-right':
2103
+ return { x, y: y + offsetY, width: width + offsetX, height: height - offsetY };
2104
+ case 'bottom-left':
2105
+ return { x: x + offsetX, y, width: width - offsetX, height: height + offsetY };
2106
+ case 'bottom-right':
2107
+ return { x, y, width: width + offsetX, height: height + offsetY };
2108
+ }
2109
+ }
2110
+ function constrainRect(rect, model, side, minWidth, minHeight) {
2111
+ let { x, y, width, height } = rect;
2112
+ // 1. Prevent negative dimensions
2113
+ width = Math.max(width, 0);
2114
+ height = Math.max(height, 0);
2115
+ // 2. Apply minimum size constraints
2116
+ width = Math.max(minWidth, width);
2117
+ height = Math.max(minHeight, height);
2118
+ // Apply left/top constraints based on minimum size
2119
+ x = Math.min(x, model.point().x + model.size().width - minWidth);
2120
+ y = Math.min(y, model.point().y + model.size().height - minHeight);
2121
+ const parent = model.parent();
2122
+ // 3. Apply maximum size constraints based on parent size (if exists)
2123
+ if (parent) {
2124
+ x = Math.max(x, 0); // Left boundary of the parent
2125
+ y = Math.max(y, 0); // Top boundary of the parent
2126
+ if (x === 0) {
2127
+ width = model.point().x + model.size().width;
2128
+ }
2129
+ if (y === 0) {
2130
+ height = model.point().y + model.size().height;
2131
+ }
2132
+ width = Math.min(width, parent.size().width - model.point().x);
2133
+ height = Math.min(height, parent.size().height - model.point().y);
2134
+ }
2135
+ const bounds = getNodesBounds(model.children());
2136
+ // 4. Apply child node constraints (if children exist)
2137
+ if (bounds) {
2138
+ if (side.includes('left')) {
2139
+ x = Math.min(x, (model.point().x + model.size().width) - (bounds.x + bounds.width));
2140
+ width = Math.max(width, bounds.x + bounds.width);
2141
+ }
2142
+ if (side.includes('right')) {
2143
+ width = Math.max(width, bounds.x + bounds.width);
2144
+ }
2145
+ if (side.includes('bottom')) {
2146
+ height = Math.max(height, bounds.y + bounds.height);
2147
+ }
2148
+ if (side.includes('top')) {
2149
+ y = Math.min(y, (model.point().y + model.size().height) - (bounds.y + bounds.height));
2150
+ height = Math.max(height, bounds.y + bounds.height);
2151
+ }
2152
+ }
2153
+ return {
2154
+ x,
2155
+ y,
2156
+ width,
2157
+ height
2158
+ };
2159
+ }
2128
2160
 
2129
2161
  class HandleSizeControllerDirective {
2130
2162
  constructor() {
@@ -2236,7 +2268,7 @@ class NodeComponent {
2236
2268
  }
2237
2269
  }
2238
2270
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2239
- 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 (mousedown)=\"pullNode(); selectNode()\"\n>\n <div\n #htmlWrapper\n class=\"default-node\"\n [class.default-node_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 </div>\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 (mousedown)=\"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 (mousedown)=\"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 (mousedown)=\"pullNode(); selectNode()\"\n/>\n\n<!-- Template group node -->\n<svg:g\n *ngIf=\"nodeModel.node.type === 'template-group' && groupNodeTemplate\"\n class=\"selectable\"\n (mousedown)=\"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-node{border:1.5px solid #1b262c;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}.default-node_selected{border-width:2px}.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.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "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 }); }
2271
+ 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 <div\n #htmlWrapper\n class=\"default-node\"\n [class.default-node_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 </div>\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-node{border:1.5px solid #1b262c;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}.default-node_selected{border-width:2px}.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.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "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 }); }
2240
2272
  }
2241
2273
  __decorate([
2242
2274
  InjectionContext
@@ -2246,7 +2278,7 @@ __decorate([
2246
2278
  ], NodeComponent.prototype, "ngAfterViewInit", null);
2247
2279
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeComponent, decorators: [{
2248
2280
  type: Component,
2249
- 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 (mousedown)=\"pullNode(); selectNode()\"\n>\n <div\n #htmlWrapper\n class=\"default-node\"\n [class.default-node_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 </div>\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 (mousedown)=\"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 (mousedown)=\"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 (mousedown)=\"pullNode(); selectNode()\"\n/>\n\n<!-- Template group node -->\n<svg:g\n *ngIf=\"nodeModel.node.type === 'template-group' && groupNodeTemplate\"\n class=\"selectable\"\n (mousedown)=\"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-node{border:1.5px solid #1b262c;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}.default-node_selected{border-width:2px}.default-group-node{stroke-width:1.5px;fill-opacity:.05}.default-group-node_selected{stroke-width:2px}.default-handle{stroke:#fff;fill:#1b262c}\n"] }]
2281
+ 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 <div\n #htmlWrapper\n class=\"default-node\"\n [class.default-node_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 </div>\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-node{border:1.5px solid #1b262c;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#000;background-color:#fff}.default-node_selected{border-width:2px}.default-group-node{stroke-width:1.5px;fill-opacity:.05}.default-group-node_selected{stroke-width:2px}.default-handle{stroke:#fff;fill:#1b262c}\n"] }]
2250
2282
  }], propDecorators: { nodeModel: [{
2251
2283
  type: Input
2252
2284
  }], nodeTemplate: [{
@@ -2662,6 +2694,7 @@ class VflowComponent {
2662
2694
  this.nodeRenderingService = inject(NodeRenderingService);
2663
2695
  this.flowSettingsService = inject(FlowSettingsService);
2664
2696
  this.componentEventBusService = inject(ComponentEventBusService);
2697
+ this.keyboardService = inject(KeyboardService);
2665
2698
  this.injector = inject(Injector);
2666
2699
  /**
2667
2700
  * Background for flow
@@ -2753,6 +2786,9 @@ class VflowComponent {
2753
2786
  set entitiesSelectable(value) {
2754
2787
  this.flowSettingsService.entitiesSelectable.set(value);
2755
2788
  }
2789
+ set keyboardShortcuts(value) {
2790
+ this.keyboardService.setShortcuts(value);
2791
+ }
2756
2792
  /**
2757
2793
  * Settings for connection (it renders when user tries to create edge between nodes)
2758
2794
  *
@@ -2851,7 +2887,7 @@ class VflowComponent {
2851
2887
  }
2852
2888
  }
2853
2889
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2854
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "16.2.12", type: VflowComponent, selector: "vflow", inputs: { view: "view", minZoom: "minZoom", maxZoom: "maxZoom", handlePositions: "handlePositions", background: "background", optimization: "optimization", entitiesSelectable: "entitiesSelectable", connection: ["connection", "connection", (settings) => new ConnectionModel(settings)], nodes: "nodes", edges: "edges" }, outputs: { onComponentNodeEvent: "onComponentNodeEvent" }, providers: [
2890
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "16.2.12", type: VflowComponent, selector: "vflow", inputs: { view: "view", minZoom: "minZoom", maxZoom: "maxZoom", handlePositions: "handlePositions", background: "background", optimization: "optimization", entitiesSelectable: "entitiesSelectable", keyboardShortcuts: "keyboardShortcuts", connection: ["connection", "connection", (settings) => new ConnectionModel(settings)], nodes: "nodes", edges: "edges" }, outputs: { onComponentNodeEvent: "onComponentNodeEvent" }, providers: [
2855
2891
  DraggableService,
2856
2892
  ViewportService,
2857
2893
  FlowStatusService,
@@ -2861,7 +2897,8 @@ class VflowComponent {
2861
2897
  NodeRenderingService,
2862
2898
  SelectionService,
2863
2899
  FlowSettingsService,
2864
- ComponentEventBusService
2900
+ ComponentEventBusService,
2901
+ KeyboardService
2865
2902
  ], 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]=\"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</svg:svg>\n", styles: [":host{display:block;width:100%;height:100%;-webkit-user-select:none;user-select:none}:host ::ng-deep *{box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: NodeComponent, selector: "g[node]", inputs: ["nodeModel", "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]", inputs: ["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 }); }
2866
2903
  }
2867
2904
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VflowComponent, decorators: [{
@@ -2876,7 +2913,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2876
2913
  NodeRenderingService,
2877
2914
  SelectionService,
2878
2915
  FlowSettingsService,
2879
- ComponentEventBusService
2916
+ ComponentEventBusService,
2917
+ KeyboardService
2880
2918
  ], hostDirectives: [
2881
2919
  connectionControllerHostDirective,
2882
2920
  changesControllerHostDirective
@@ -2895,6 +2933,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2895
2933
  type: Input
2896
2934
  }], entitiesSelectable: [{
2897
2935
  type: Input
2936
+ }], keyboardShortcuts: [{
2937
+ type: Input
2898
2938
  }], connection: [{
2899
2939
  type: Input,
2900
2940
  args: [{ transform: (settings) => new ConnectionModel(settings) }]
@@ -2951,7 +2991,7 @@ class SelectableDirective {
2951
2991
  return null;
2952
2992
  }
2953
2993
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SelectableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2954
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: SelectableDirective, selector: "[selectable]", host: { listeners: { "mousedown": "onMousedown()" } }, ngImport: i0 }); }
2994
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: SelectableDirective, selector: "[selectable]", host: { listeners: { "mousedown": "onMousedown()", "touchstart": "onMousedown()" } }, ngImport: i0 }); }
2955
2995
  }
2956
2996
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SelectableDirective, decorators: [{
2957
2997
  type: Directive,
@@ -2959,6 +2999,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2959
2999
  }], propDecorators: { onMousedown: [{
2960
3000
  type: HostListener,
2961
3001
  args: ['mousedown']
3002
+ }, {
3003
+ type: HostListener,
3004
+ args: ['touchstart']
2962
3005
  }] } });
2963
3006
 
2964
3007
  const components = [