ngx-workflow 0.0.1

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.
@@ -0,0 +1,3016 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, EventEmitter, Output, Input, ChangeDetectionStrategy, Component, signal, computed, Injectable, effect, inject, HostListener, ViewChild, Optional, Inject, NgModule } from '@angular/core';
3
+ import * as i2 from '@angular/common';
4
+ import { CommonModule } from '@angular/common';
5
+ import { toObservable } from '@angular/core/rxjs-interop';
6
+ import { Subject, animationFrameScheduler, Subscription } from 'rxjs';
7
+ import { v4 } from 'uuid';
8
+ import { throttleTime } from 'rxjs/operators';
9
+ import * as i2$1 from '@angular/forms';
10
+ import { FormsModule } from '@angular/forms';
11
+ import ELK from 'elkjs/lib/elk.bundled';
12
+
13
+ const NGX_WORKFLOW_NODE_TYPES = new InjectionToken('NGX_WORKFLOW_NODE_TYPES');
14
+
15
+ const NGX_WORKFLOW_EDGE_TYPES = new InjectionToken('NGX_WORKFLOW_EDGE_TYPES');
16
+
17
+ function getStraightPath(source, target) {
18
+ return `M ${source.x},${source.y} L ${target.x},${target.y}`;
19
+ }
20
+ function getBezierPath(source, target) {
21
+ const midX = (source.x + target.x) / 2;
22
+ return `M ${source.x},${source.y} C ${midX},${source.y} ${midX},${target.y} ${target.x},${target.y}`;
23
+ }
24
+ function getStepPath(source, target) {
25
+ const midY = (source.y + target.y) / 2;
26
+ return `M ${source.x},${source.y} L ${source.x},${midY} L ${target.x},${midY} L ${target.x},${target.y}`;
27
+ }
28
+ function getSelfLoopPath(source, handle = 'top', offset = 30) {
29
+ const { x, y } = source;
30
+ switch (handle) {
31
+ case 'top':
32
+ return `M ${x},${y} C ${x - offset},${y - offset * 2} ${x + offset},${y - offset * 2} ${x},${y}`;
33
+ case 'right':
34
+ return `M ${x},${y} C ${x + offset * 2},${y - offset} ${x + offset * 2},${y + offset} ${x},${y}`;
35
+ case 'bottom':
36
+ return `M ${x},${y} C ${x + offset},${y + offset * 2} ${x - offset},${y + offset * 2} ${x},${y}`;
37
+ case 'left':
38
+ return `M ${x},${y} C ${x - offset * 2},${y + offset} ${x - offset * 2},${y - offset} ${x},${y}`;
39
+ default:
40
+ return `M ${x},${y} C ${x - offset},${y - offset * 2} ${x + offset},${y - offset * 2} ${x},${y}`;
41
+ }
42
+ }
43
+ function getSmartEdgePath(path) {
44
+ if (path.length === 0)
45
+ return '';
46
+ let d = `M ${path[0].x},${path[0].y}`;
47
+ for (let i = 1; i < path.length; i++) {
48
+ d += ` L ${path[i].x},${path[i].y}`;
49
+ }
50
+ return d;
51
+ }
52
+ function getPolylineMidpoint(points) {
53
+ if (points.length < 2)
54
+ return points[0] || { x: 0, y: 0 };
55
+ // Calculate total length
56
+ let totalLength = 0;
57
+ const segments = [];
58
+ for (let i = 0; i < points.length - 1; i++) {
59
+ const start = points[i];
60
+ const end = points[i + 1];
61
+ const length = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2));
62
+ segments.push({ length, start, end });
63
+ totalLength += length;
64
+ }
65
+ // Find midpoint
66
+ let remainingLength = totalLength / 2;
67
+ for (const segment of segments) {
68
+ if (remainingLength <= segment.length) {
69
+ // Midpoint is on this segment
70
+ const ratio = remainingLength / segment.length;
71
+ return {
72
+ x: segment.start.x + (segment.end.x - segment.start.x) * ratio,
73
+ y: segment.start.y + (segment.end.y - segment.start.y) * ratio
74
+ };
75
+ }
76
+ remainingLength -= segment.length;
77
+ }
78
+ return points[points.length - 1];
79
+ }
80
+
81
+ class MinHeap {
82
+ heap = [];
83
+ push(node) {
84
+ this.heap.push(node);
85
+ this.bubbleUp(this.heap.length - 1);
86
+ }
87
+ pop() {
88
+ if (this.heap.length === 0)
89
+ return undefined;
90
+ const top = this.heap[0];
91
+ const bottom = this.heap.pop();
92
+ if (this.heap.length > 0 && bottom) {
93
+ this.heap[0] = bottom;
94
+ this.sinkDown(0);
95
+ }
96
+ return top;
97
+ }
98
+ size() {
99
+ return this.heap.length;
100
+ }
101
+ bubbleUp(index) {
102
+ const element = this.heap[index];
103
+ while (index > 0) {
104
+ const parentIndex = Math.floor((index - 1) / 2);
105
+ const parent = this.heap[parentIndex];
106
+ if (element.f >= parent.f)
107
+ break;
108
+ this.heap[parentIndex] = element;
109
+ this.heap[index] = parent;
110
+ index = parentIndex;
111
+ }
112
+ }
113
+ sinkDown(index) {
114
+ const length = this.heap.length;
115
+ const element = this.heap[index];
116
+ while (true) {
117
+ const leftChildIndex = 2 * index + 1;
118
+ const rightChildIndex = 2 * index + 2;
119
+ let leftChild, rightChild;
120
+ let swap = null;
121
+ if (leftChildIndex < length) {
122
+ leftChild = this.heap[leftChildIndex];
123
+ if (leftChild.f < element.f) {
124
+ swap = leftChildIndex;
125
+ }
126
+ }
127
+ if (rightChildIndex < length) {
128
+ rightChild = this.heap[rightChildIndex];
129
+ if ((swap === null && rightChild.f < element.f) ||
130
+ (swap !== null && rightChild.f < leftChild.f)) {
131
+ swap = rightChildIndex;
132
+ }
133
+ }
134
+ if (swap === null)
135
+ break;
136
+ this.heap[index] = this.heap[swap];
137
+ this.heap[swap] = element;
138
+ index = swap;
139
+ }
140
+ }
141
+ rescoreElement(node) {
142
+ const index = this.heap.indexOf(node);
143
+ if (index !== -1) {
144
+ // Since f usually decreases in A*, we bubble up
145
+ this.bubbleUp(index);
146
+ }
147
+ }
148
+ }
149
+ class PathFinder {
150
+ nodes;
151
+ graphWidth;
152
+ graphHeight;
153
+ gridSize = 20; // Size of each grid cell
154
+ grid = [];
155
+ width = 0;
156
+ height = 0;
157
+ bounds = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
158
+ constructor(nodes, graphWidth = 2000, graphHeight = 2000) {
159
+ this.nodes = nodes;
160
+ this.graphWidth = graphWidth;
161
+ this.graphHeight = graphHeight;
162
+ this.initializeGrid();
163
+ }
164
+ initializeGrid() {
165
+ // Determine bounds based on nodes, with some padding
166
+ if (this.nodes.length === 0)
167
+ return;
168
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
169
+ this.nodes.forEach(node => {
170
+ minX = Math.min(minX, node.x);
171
+ minY = Math.min(minY, node.y);
172
+ maxX = Math.max(maxX, node.x + node.width);
173
+ maxY = Math.max(maxY, node.y + node.height);
174
+ });
175
+ // Add padding
176
+ const padding = 100;
177
+ this.bounds = {
178
+ minX: Math.floor((minX - padding) / this.gridSize) * this.gridSize,
179
+ minY: Math.floor((minY - padding) / this.gridSize) * this.gridSize,
180
+ maxX: Math.ceil((maxX + padding) / this.gridSize) * this.gridSize,
181
+ maxY: Math.ceil((maxY + padding) / this.gridSize) * this.gridSize
182
+ };
183
+ this.width = Math.ceil((this.bounds.maxX - this.bounds.minX) / this.gridSize);
184
+ this.height = Math.ceil((this.bounds.maxY - this.bounds.minY) / this.gridSize);
185
+ // Initialize grid
186
+ this.grid = [];
187
+ for (let y = 0; y < this.height; y++) {
188
+ const row = [];
189
+ for (let x = 0; x < this.width; x++) {
190
+ row.push({
191
+ x,
192
+ y,
193
+ walkable: true,
194
+ g: 0,
195
+ h: 0,
196
+ f: 0,
197
+ parent: null,
198
+ opened: false,
199
+ closed: false
200
+ });
201
+ }
202
+ this.grid.push(row);
203
+ }
204
+ // Mark obstacles
205
+ this.nodes.forEach(node => {
206
+ const startX = Math.floor((node.x - this.bounds.minX) / this.gridSize);
207
+ const startY = Math.floor((node.y - this.bounds.minY) / this.gridSize);
208
+ const endX = Math.ceil((node.x + node.width - this.bounds.minX) / this.gridSize);
209
+ const endY = Math.ceil((node.y + node.height - this.bounds.minY) / this.gridSize);
210
+ for (let y = Math.max(0, startY); y < Math.min(this.height, endY); y++) {
211
+ for (let x = Math.max(0, startX); x < Math.min(this.width, endX); x++) {
212
+ this.grid[y][x].walkable = false;
213
+ }
214
+ }
215
+ });
216
+ }
217
+ findPath(start, end) {
218
+ const startGridX = Math.floor((start.x - this.bounds.minX) / this.gridSize);
219
+ const startGridY = Math.floor((start.y - this.bounds.minY) / this.gridSize);
220
+ const endGridX = Math.floor((end.x - this.bounds.minX) / this.gridSize);
221
+ const endGridY = Math.floor((end.y - this.bounds.minY) / this.gridSize);
222
+ // Check if start or end are out of bounds
223
+ if (!this.isValid(startGridX, startGridY) || !this.isValid(endGridX, endGridY)) {
224
+ return [start, end]; // Fallback to straight line
225
+ }
226
+ // Reset grid state for new search (optimization: use a search ID instead of clearing all?)
227
+ // For now, simple reset is safer, but O(W*H).
228
+ // Optimization: Only reset nodes we touch?
229
+ // Let's iterate over the grid to reset. This is slow if grid is huge.
230
+ // Better: Use a 'visitedToken' on GridNode and increment it per search.
231
+ // But I can't easily change GridNode interface without touching everything.
232
+ // Let's stick to resetting for now, but maybe optimize later.
233
+ // Actually, since we create a NEW PathFinder on drag stop, the grid is fresh.
234
+ // But findPath is called multiple times (once per edge).
235
+ // So we MUST reset.
236
+ // Optimization: Keep a list of visited nodes and reset only them.
237
+ // We'll use a `resetList` to track modified nodes and reset them at the end.
238
+ const resetList = [];
239
+ const startNode = this.grid[startGridY][startGridX];
240
+ const endNode = this.grid[endGridY][endGridX];
241
+ // Temporarily make start and end walkable
242
+ const startWasWalkable = startNode.walkable;
243
+ const endWasWalkable = endNode.walkable;
244
+ startNode.walkable = true;
245
+ endNode.walkable = true;
246
+ resetList.push(startNode, endNode);
247
+ const openList = new MinHeap();
248
+ startNode.g = 0;
249
+ startNode.h = Math.abs(startNode.x - endNode.x) + Math.abs(startNode.y - endNode.y);
250
+ startNode.f = startNode.g + startNode.h;
251
+ startNode.opened = true;
252
+ openList.push(startNode);
253
+ resetList.push(startNode);
254
+ let pathFound = false;
255
+ while (openList.size() > 0) {
256
+ const currentNode = openList.pop();
257
+ currentNode.closed = true;
258
+ if (currentNode === endNode) {
259
+ pathFound = true;
260
+ break;
261
+ }
262
+ const neighbors = this.getNeighbors(currentNode);
263
+ for (const neighbor of neighbors) {
264
+ if (neighbor.closed || !neighbor.walkable) {
265
+ continue;
266
+ }
267
+ const gScore = currentNode.g + 1;
268
+ if (!neighbor.opened || gScore < neighbor.g) {
269
+ neighbor.g = gScore;
270
+ neighbor.h = Math.abs(neighbor.x - endNode.x) + Math.abs(neighbor.y - endNode.y);
271
+ neighbor.f = neighbor.g + neighbor.h;
272
+ neighbor.parent = currentNode;
273
+ if (!neighbor.opened) {
274
+ neighbor.opened = true;
275
+ openList.push(neighbor);
276
+ resetList.push(neighbor);
277
+ }
278
+ else {
279
+ openList.rescoreElement(neighbor);
280
+ }
281
+ }
282
+ }
283
+ }
284
+ let resultPath = [start, end];
285
+ if (pathFound) {
286
+ const path = [];
287
+ let curr = endNode;
288
+ while (curr) {
289
+ path.unshift({
290
+ x: curr.x * this.gridSize + this.bounds.minX + this.gridSize / 2,
291
+ y: curr.y * this.gridSize + this.bounds.minY + this.gridSize / 2
292
+ });
293
+ curr = curr.parent;
294
+ }
295
+ path[0] = start;
296
+ path[path.length - 1] = end;
297
+ resultPath = this.simplifyPath(path);
298
+ }
299
+ // Cleanup
300
+ startNode.walkable = startWasWalkable;
301
+ endNode.walkable = endWasWalkable;
302
+ // Reset modified nodes
303
+ for (const node of resetList) {
304
+ node.g = 0;
305
+ node.h = 0;
306
+ node.f = 0;
307
+ node.parent = null;
308
+ node.opened = false;
309
+ node.closed = false;
310
+ }
311
+ return resultPath;
312
+ }
313
+ isValid(x, y) {
314
+ return x >= 0 && x < this.width && y >= 0 && y < this.height;
315
+ }
316
+ getNeighbors(node) {
317
+ const neighbors = [];
318
+ const dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]]; // 4-directional
319
+ for (const [dx, dy] of dirs) {
320
+ const newX = node.x + dx;
321
+ const newY = node.y + dy;
322
+ if (this.isValid(newX, newY)) {
323
+ neighbors.push(this.grid[newY][newX]);
324
+ }
325
+ }
326
+ return neighbors;
327
+ }
328
+ simplifyPath(path) {
329
+ if (path.length <= 2)
330
+ return path;
331
+ const simplified = [path[0]];
332
+ let direction = {
333
+ x: Math.sign(path[1].x - path[0].x),
334
+ y: Math.sign(path[1].y - path[0].y)
335
+ };
336
+ for (let i = 1; i < path.length - 1; i++) {
337
+ const nextPoint = path[i + 1];
338
+ const currentPoint = path[i];
339
+ const newDirection = {
340
+ x: Math.sign(nextPoint.x - currentPoint.x),
341
+ y: Math.sign(nextPoint.y - currentPoint.y)
342
+ };
343
+ // If direction changes, add the turning point
344
+ if (newDirection.x !== direction.x || newDirection.y !== direction.y) {
345
+ simplified.push(currentPoint);
346
+ direction = newDirection;
347
+ }
348
+ }
349
+ simplified.push(path[path.length - 1]);
350
+ return simplified;
351
+ }
352
+ }
353
+
354
+ class ZoomControlsComponent {
355
+ zoom = 1;
356
+ minZoom = 0.1;
357
+ maxZoom = 10;
358
+ zoomIn = new EventEmitter();
359
+ zoomOut = new EventEmitter();
360
+ fitView = new EventEmitter();
361
+ resetZoom = new EventEmitter();
362
+ get zoomPercent() {
363
+ return Math.round(this.zoom * 100);
364
+ }
365
+ onZoomIn() {
366
+ this.zoomIn.emit();
367
+ }
368
+ onZoomOut() {
369
+ this.zoomOut.emit();
370
+ }
371
+ onFitView() {
372
+ this.fitView.emit();
373
+ }
374
+ onResetZoom() {
375
+ this.resetZoom.emit();
376
+ }
377
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ZoomControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
378
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: ZoomControlsComponent, isStandalone: true, selector: "ngx-workflow-zoom-controls", inputs: { zoom: "zoom", minZoom: "minZoom", maxZoom: "maxZoom" }, outputs: { zoomIn: "zoomIn", zoomOut: "zoomOut", fitView: "fitView", resetZoom: "resetZoom" }, ngImport: i0, template: "<div class=\"ngx-workflow__zoom-controls\">\r\n <button class=\"ngx-workflow__zoom-button\" (click)=\"onZoomIn()\" [disabled]=\"zoom >= maxZoom\" title=\"Zoom In\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\">\r\n <path d=\"M8 3v10M3 8h10\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" />\r\n </svg>\r\n </button>\r\n\r\n <button class=\"ngx-workflow__zoom-button\" (click)=\"onZoomOut()\" [disabled]=\"zoom <= minZoom\" title=\"Zoom Out\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\">\r\n <path d=\"M3 8h10\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" />\r\n </svg>\r\n </button>\r\n\r\n <button class=\"ngx-workflow__zoom-button\" (click)=\"onFitView()\" title=\"Fit View\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\">\r\n <rect x=\"2\" y=\"2\" width=\"12\" height=\"12\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" />\r\n <path d=\"M5 5L11 11M11 5L5 11\" stroke=\"currentColor\" stroke-width=\"1.5\" />\r\n </svg>\r\n </button>\r\n\r\n <button class=\"ngx-workflow__zoom-button\" (click)=\"onResetZoom()\" title=\"Reset Zoom (100%)\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\">\r\n <text x=\"8\" y=\"12\" text-anchor=\"middle\" font-size=\"10\" fill=\"currentColor\">1:1</text>\r\n </svg>\r\n </button>\r\n\r\n <div class=\"ngx-workflow__zoom-level\">{{ zoomPercent }}%</div>\r\n</div>", styles: [".ngx-workflow__zoom-controls{position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;gap:4px;background:var(--ngx-workflow-zoom-controls-bg, #fff);border:1px solid var(--ngx-workflow-zoom-controls-border, #ddd);border-radius:8px;padding:8px;box-shadow:0 2px 8px #0000001a;z-index:10;-webkit-user-select:none;user-select:none}.ngx-workflow__zoom-button{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:var(--ngx-workflow-zoom-button-bg, #f5f5f5);border:1px solid var(--ngx-workflow-zoom-button-border, #ddd);border-radius:4px;cursor:pointer;transition:all .2s ease;color:var(--ngx-workflow-zoom-button-color, #333);padding:0}.ngx-workflow__zoom-button:hover:not(:disabled){background:var(--ngx-workflow-zoom-button-hover-bg, #e8e8e8);border-color:var(--ngx-workflow-zoom-button-hover-border, #bbb)}.ngx-workflow__zoom-button:active:not(:disabled){background:var(--ngx-workflow-zoom-button-active-bg, #ddd);transform:scale(.95)}.ngx-workflow__zoom-button:disabled{opacity:.4;cursor:not-allowed}.ngx-workflow__zoom-button svg{display:block}.ngx-workflow__zoom-level{text-align:center;font-size:11px;font-weight:600;color:var(--ngx-workflow-zoom-level-color, #666);padding:4px 0;border-top:1px solid var(--ngx-workflow-zoom-controls-border, #ddd);margin-top:4px;min-width:40px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
379
+ }
380
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ZoomControlsComponent, decorators: [{
381
+ type: Component,
382
+ args: [{ selector: 'ngx-workflow-zoom-controls', imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ngx-workflow__zoom-controls\">\r\n <button class=\"ngx-workflow__zoom-button\" (click)=\"onZoomIn()\" [disabled]=\"zoom >= maxZoom\" title=\"Zoom In\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\">\r\n <path d=\"M8 3v10M3 8h10\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" />\r\n </svg>\r\n </button>\r\n\r\n <button class=\"ngx-workflow__zoom-button\" (click)=\"onZoomOut()\" [disabled]=\"zoom <= minZoom\" title=\"Zoom Out\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\">\r\n <path d=\"M3 8h10\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" />\r\n </svg>\r\n </button>\r\n\r\n <button class=\"ngx-workflow__zoom-button\" (click)=\"onFitView()\" title=\"Fit View\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\">\r\n <rect x=\"2\" y=\"2\" width=\"12\" height=\"12\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" />\r\n <path d=\"M5 5L11 11M11 5L5 11\" stroke=\"currentColor\" stroke-width=\"1.5\" />\r\n </svg>\r\n </button>\r\n\r\n <button class=\"ngx-workflow__zoom-button\" (click)=\"onResetZoom()\" title=\"Reset Zoom (100%)\">\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\">\r\n <text x=\"8\" y=\"12\" text-anchor=\"middle\" font-size=\"10\" fill=\"currentColor\">1:1</text>\r\n </svg>\r\n </button>\r\n\r\n <div class=\"ngx-workflow__zoom-level\">{{ zoomPercent }}%</div>\r\n</div>", styles: [".ngx-workflow__zoom-controls{position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;gap:4px;background:var(--ngx-workflow-zoom-controls-bg, #fff);border:1px solid var(--ngx-workflow-zoom-controls-border, #ddd);border-radius:8px;padding:8px;box-shadow:0 2px 8px #0000001a;z-index:10;-webkit-user-select:none;user-select:none}.ngx-workflow__zoom-button{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:var(--ngx-workflow-zoom-button-bg, #f5f5f5);border:1px solid var(--ngx-workflow-zoom-button-border, #ddd);border-radius:4px;cursor:pointer;transition:all .2s ease;color:var(--ngx-workflow-zoom-button-color, #333);padding:0}.ngx-workflow__zoom-button:hover:not(:disabled){background:var(--ngx-workflow-zoom-button-hover-bg, #e8e8e8);border-color:var(--ngx-workflow-zoom-button-hover-border, #bbb)}.ngx-workflow__zoom-button:active:not(:disabled){background:var(--ngx-workflow-zoom-button-active-bg, #ddd);transform:scale(.95)}.ngx-workflow__zoom-button:disabled{opacity:.4;cursor:not-allowed}.ngx-workflow__zoom-button svg{display:block}.ngx-workflow__zoom-level{text-align:center;font-size:11px;font-weight:600;color:var(--ngx-workflow-zoom-level-color, #666);padding:4px 0;border-top:1px solid var(--ngx-workflow-zoom-controls-border, #ddd);margin-top:4px;min-width:40px}\n"] }]
383
+ }], propDecorators: { zoom: [{
384
+ type: Input
385
+ }], minZoom: [{
386
+ type: Input
387
+ }], maxZoom: [{
388
+ type: Input
389
+ }], zoomIn: [{
390
+ type: Output
391
+ }], zoomOut: [{
392
+ type: Output
393
+ }], fitView: [{
394
+ type: Output
395
+ }], resetZoom: [{
396
+ type: Output
397
+ }] } });
398
+
399
+ class UndoRedoService {
400
+ undoStack = signal([], ...(ngDevMode ? [{ debugName: "undoStack" }] : []));
401
+ redoStack = signal([], ...(ngDevMode ? [{ debugName: "redoStack" }] : []));
402
+ constructor() { }
403
+ // Saves the current state of the diagram to the undo stack
404
+ saveState(currentState) {
405
+ this.undoStack.update((stack) => [...stack, currentState]);
406
+ this.redoStack.set([]); // Clear redo stack on new action
407
+ // console.log('State saved');
408
+ // console.log('Undo Stack size:', this.undoStack().length);
409
+ }
410
+ // Undoes the last action
411
+ undo(currentState) {
412
+ const previousState = this.undoStack().pop();
413
+ if (previousState) {
414
+ this.redoStack.update((stack) => [...stack, currentState]);
415
+ console.log('Undo performed. Undo Stack size:', this.undoStack().length);
416
+ console.log('Redo Stack size:', this.redoStack().length);
417
+ return previousState;
418
+ }
419
+ console.log('Nothing to undo.');
420
+ return undefined;
421
+ }
422
+ // Redoes the last undone action
423
+ redo(currentState) {
424
+ const nextState = this.redoStack().pop();
425
+ if (nextState) {
426
+ this.undoStack.update((stack) => [...stack, currentState]);
427
+ console.log('Redo performed. Undo Stack size:', this.undoStack().length);
428
+ console.log('Redo Stack size:', this.redoStack().length);
429
+ return nextState;
430
+ }
431
+ console.log('Nothing to redo.');
432
+ return undefined;
433
+ }
434
+ canUndo = computed(() => this.undoStack().length > 0, ...(ngDevMode ? [{ debugName: "canUndo" }] : []));
435
+ canRedo = computed(() => this.redoStack().length > 0, ...(ngDevMode ? [{ debugName: "canRedo" }] : []));
436
+ clearStacks() {
437
+ this.undoStack.set([]);
438
+ this.redoStack.set([]);
439
+ }
440
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: UndoRedoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
441
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: UndoRedoService, providedIn: 'root' });
442
+ }
443
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: UndoRedoService, decorators: [{
444
+ type: Injectable,
445
+ args: [{
446
+ providedIn: 'root',
447
+ }]
448
+ }], ctorParameters: () => [] });
449
+
450
+ class DiagramStateService {
451
+ undoRedoService;
452
+ nodes = signal([], ...(ngDevMode ? [{ debugName: "nodes" }] : []));
453
+ edges = signal([], ...(ngDevMode ? [{ debugName: "edges" }] : []));
454
+ tempEdges = signal([], ...(ngDevMode ? [{ debugName: "tempEdges" }] : []));
455
+ viewport = signal({ x: 0, y: 0, zoom: 1 }, ...(ngDevMode ? [{ debugName: "viewport" }] : []));
456
+ alignmentGuides = signal([], ...(ngDevMode ? [{ debugName: "alignmentGuides" }] : []));
457
+ searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : []));
458
+ filterType = signal(null, ...(ngDevMode ? [{ debugName: "filterType" }] : []));
459
+ containerDimensions = signal({ width: 0, height: 0 }, ...(ngDevMode ? [{ debugName: "containerDimensions" }] : []));
460
+ // Computed signals
461
+ selectedNodes = computed(() => this.nodes().filter((n) => n.selected), ...(ngDevMode ? [{ debugName: "selectedNodes" }] : []));
462
+ selectedEdges = computed(() => this.edges().filter((e) => e.selected), ...(ngDevMode ? [{ debugName: "selectedEdges" }] : []));
463
+ viewNodes = computed(() => {
464
+ const nodes = this.nodes();
465
+ const query = this.searchQuery().toLowerCase();
466
+ const type = this.filterType();
467
+ return nodes.map(node => {
468
+ const matchesSearch = !query ||
469
+ (node.label && node.label.toLowerCase().includes(query)) ||
470
+ (node.data && JSON.stringify(node.data).toLowerCase().includes(query));
471
+ const matchesType = !type || node.type === type;
472
+ const isMatch = matchesSearch && matchesType;
473
+ // If there is a search/filter active, highlight matches and dim non-matches
474
+ // If no search/filter, reset (no highlight, no dim)
475
+ const isActive = !!query || !!type;
476
+ return {
477
+ ...node,
478
+ highlighted: isActive && isMatch,
479
+ dimmed: isActive && !isMatch
480
+ };
481
+ });
482
+ }, ...(ngDevMode ? [{ debugName: "viewNodes" }] : []));
483
+ visibleNodes = computed(() => {
484
+ const nodes = this.viewNodes();
485
+ const viewport = this.viewport();
486
+ const dimensions = this.containerDimensions();
487
+ const buffer = 500; // Buffer in pixels
488
+ if (dimensions.width === 0 || dimensions.height === 0) {
489
+ return nodes; // Render all if dimensions not set
490
+ }
491
+ const minX = -viewport.x / viewport.zoom - buffer;
492
+ const maxX = (-viewport.x + dimensions.width) / viewport.zoom + buffer;
493
+ const minY = -viewport.y / viewport.zoom - buffer;
494
+ const maxY = (-viewport.y + dimensions.height) / viewport.zoom + buffer;
495
+ return nodes.filter(node => {
496
+ const nodeX = node.position.x;
497
+ const nodeY = node.position.y;
498
+ const nodeWidth = node.width || 150; // Default width
499
+ const nodeHeight = node.height || 60; // Default height
500
+ return (nodeX + nodeWidth >= minX &&
501
+ nodeX <= maxX &&
502
+ nodeY + nodeHeight >= minY &&
503
+ nodeY <= maxY);
504
+ });
505
+ }, ...(ngDevMode ? [{ debugName: "visibleNodes" }] : []));
506
+ lodLevel = computed(() => {
507
+ const zoom = this.viewport().zoom;
508
+ if (zoom < 0.4)
509
+ return 'low';
510
+ if (zoom < 0.8)
511
+ return 'medium';
512
+ return 'high';
513
+ }, ...(ngDevMode ? [{ debugName: "lodLevel" }] : []));
514
+ // Reference to the main SVG element, set by DiagramComponent
515
+ el;
516
+ dragEnd = new EventEmitter();
517
+ nodesChange = new EventEmitter();
518
+ edgesChange = new EventEmitter();
519
+ viewportChange = new EventEmitter();
520
+ nodeClick = new EventEmitter();
521
+ edgeClick = new EventEmitter();
522
+ connect = new EventEmitter();
523
+ dragStart = new EventEmitter();
524
+ // Internal subjects for batched updates
525
+ nodeUpdates$ = new Subject();
526
+ edgeUpdates$ = new Subject();
527
+ viewportUpdates$ = new Subject();
528
+ constructor(undoRedoService) {
529
+ this.undoRedoService = undoRedoService;
530
+ // Sync internal subjects to signals (for batched updates)
531
+ this.nodeUpdates$
532
+ .pipe(throttleTime(0, animationFrameScheduler, { leading: true, trailing: true }))
533
+ .subscribe((nodes) => {
534
+ this.nodes.set(nodes);
535
+ });
536
+ this.edgeUpdates$
537
+ .pipe(throttleTime(0, animationFrameScheduler, { leading: true, trailing: true }))
538
+ .subscribe((edges) => {
539
+ this.edges.set(edges);
540
+ });
541
+ this.viewportUpdates$
542
+ .pipe(throttleTime(0, animationFrameScheduler, { leading: true, trailing: true }))
543
+ .subscribe((viewport) => {
544
+ this.viewport.set(viewport);
545
+ });
546
+ // Emit changes whenever signals update
547
+ effect(() => {
548
+ this.nodesChange.emit(this.nodes());
549
+ });
550
+ effect(() => {
551
+ this.edgesChange.emit(this.edges());
552
+ });
553
+ effect(() => {
554
+ this.viewportChange.emit(this.viewport());
555
+ });
556
+ }
557
+ // Helper to get current state for undo/redo
558
+ getCurrentState() {
559
+ return {
560
+ nodes: [...this.nodes()],
561
+ edges: [...this.edges()],
562
+ viewport: { ...this.viewport() },
563
+ };
564
+ }
565
+ // Public method to retrieve the current diagram state (for export)
566
+ getDiagramState() {
567
+ return this.getCurrentState();
568
+ }
569
+ /**
570
+ * Replace the entire diagram state (nodes, edges, viewport) with the given state.
571
+ * Used for importing diagram JSON.
572
+ */
573
+ setDiagramState(state) {
574
+ // Save current state for undo
575
+ this.undoRedoService.saveState(this.getCurrentState());
576
+ // Replace signals
577
+ this.nodes.set(state.nodes.map(n => ({ ...n, selected: false, dragging: false, draggable: true })));
578
+ this.edges.set(state.edges.map(e => ({ ...e, selected: false })));
579
+ this.viewport.set(state.viewport);
580
+ // Emit changes
581
+ this.nodesChange.emit(this.nodes());
582
+ this.edgesChange.emit(this.edges());
583
+ this.viewportChange.emit(this.viewport());
584
+ }
585
+ // Apply state from undo/redo service
586
+ applyState(state) {
587
+ this.nodes.set(state.nodes);
588
+ this.edges.set(state.edges);
589
+ this.viewport.set(state.viewport);
590
+ }
591
+ undo() {
592
+ const currentState = this.getCurrentState();
593
+ const previousState = this.undoRedoService.undo(currentState);
594
+ if (previousState) {
595
+ this.applyState(previousState);
596
+ }
597
+ }
598
+ redo() {
599
+ const currentState = this.getCurrentState();
600
+ const nextState = this.undoRedoService.redo(currentState);
601
+ if (nextState) {
602
+ this.applyState(nextState);
603
+ }
604
+ }
605
+ onResizeStart(node) {
606
+ this.undoRedoService.saveState(this.getCurrentState());
607
+ }
608
+ onResizeEnd(node) {
609
+ // Optional: emit resize end event if needed in the future
610
+ }
611
+ // --- Edge Management ---
612
+ addEdge(edge) {
613
+ console.log('DiagramStateService.addEdge: start', edge);
614
+ this.undoRedoService.saveState(this.getCurrentState());
615
+ this.edges.update((currentEdges) => [...currentEdges, { ...edge, selected: false }]);
616
+ this.connect.emit({
617
+ source: edge.source,
618
+ sourceHandle: edge.sourceHandle,
619
+ target: edge.target,
620
+ targetHandle: edge.targetHandle,
621
+ });
622
+ console.log('DiagramStateService.addEdge: end');
623
+ }
624
+ updateEdge(id, changes) {
625
+ this.undoRedoService.saveState(this.getCurrentState());
626
+ this.edges.update((currentEdges) => currentEdges.map((edge) => {
627
+ if (edge.id === id) {
628
+ return { ...edge, ...changes };
629
+ }
630
+ return edge;
631
+ }));
632
+ }
633
+ removeEdge(id) {
634
+ this.undoRedoService.saveState(this.getCurrentState());
635
+ this.edges.update((currentEdges) => currentEdges.filter((edge) => edge.id !== id));
636
+ this.tempEdges.update((currentTempEdges) => currentTempEdges.filter((edge) => edge.id !== id)); // Remove from tempEdges as well
637
+ }
638
+ // --- Temporary Edge (Preview Edge) Management ---
639
+ addTempEdge(edge) {
640
+ this.tempEdges.update((currentTempEdges) => [...currentTempEdges, edge]);
641
+ }
642
+ updateTempEdgeTarget(id, targetPosition) {
643
+ this.tempEdges.update((currentTempEdges) => currentTempEdges.map((edge) => edge.id === id ? { ...edge, targetX: targetPosition.x, targetY: targetPosition.y } : edge));
644
+ }
645
+ // --- Viewport Management ---
646
+ setViewport(viewport) {
647
+ this.viewport.update((currentViewport) => ({ ...currentViewport, ...viewport }));
648
+ }
649
+ // --- Selection Management ---
650
+ selectNodes(nodeIds, multi = false) {
651
+ this.nodes.update((currentNodes) => currentNodes.map((node) => ({
652
+ ...node,
653
+ selected: multi
654
+ ? nodeIds.includes(node.id)
655
+ ? !node.selected
656
+ : node.selected
657
+ : nodeIds.includes(node.id),
658
+ })));
659
+ }
660
+ clearSelection() {
661
+ this.nodes.update((currentNodes) => currentNodes.map((node) => ({ ...node, selected: false })));
662
+ this.edges.update((currentEdges) => currentEdges.map((edge) => ({ ...edge, selected: false })));
663
+ }
664
+ selectAll() {
665
+ this.nodes.update((currentNodes) => currentNodes.map((node) => ({ ...node, selected: true })));
666
+ }
667
+ multiSelect(nodeId) {
668
+ this.nodes.update((currentNodes) => currentNodes.map((node) => ({
669
+ ...node,
670
+ selected: node.id === nodeId ? !node.selected : node.selected,
671
+ })));
672
+ }
673
+ deleteSelectedElements() {
674
+ this.undoRedoService.saveState(this.getCurrentState());
675
+ const nodesToDelete = this.selectedNodes().map((node) => node.id);
676
+ const edgesToDelete = this.selectedEdges().map((edge) => edge.id);
677
+ // Remove nodes and associated edges
678
+ this.nodes.update((currentNodes) => currentNodes.filter((node) => !nodesToDelete.includes(node.id)));
679
+ this.edges.update((currentEdges) => currentEdges.filter((edge) => !edgesToDelete.includes(edge.id) &&
680
+ !nodesToDelete.includes(edge.source) &&
681
+ !nodesToDelete.includes(edge.target)));
682
+ // Clear selection after deletion
683
+ this.clearSelection();
684
+ console.log('Selected elements deleted');
685
+ }
686
+ // --- Alignment & Distribution ---
687
+ alignNodes(alignment) {
688
+ const selectedNodes = this.selectedNodes();
689
+ if (selectedNodes.length < 2)
690
+ return;
691
+ this.undoRedoService.saveState(this.getCurrentState());
692
+ let targetValue;
693
+ switch (alignment) {
694
+ case 'left':
695
+ targetValue = Math.min(...selectedNodes.map((n) => n.position.x));
696
+ break;
697
+ case 'center': {
698
+ const minX = Math.min(...selectedNodes.map((n) => n.position.x));
699
+ const maxX = Math.max(...selectedNodes.map((n) => n.position.x + (n.width || 170)));
700
+ targetValue = minX + (maxX - minX) / 2;
701
+ break;
702
+ }
703
+ case 'right':
704
+ targetValue = Math.max(...selectedNodes.map((n) => n.position.x + (n.width || 170)));
705
+ break;
706
+ case 'top':
707
+ targetValue = Math.min(...selectedNodes.map((n) => n.position.y));
708
+ break;
709
+ case 'middle': {
710
+ const minY = Math.min(...selectedNodes.map((n) => n.position.y));
711
+ const maxY = Math.max(...selectedNodes.map((n) => n.position.y + (n.height || 60)));
712
+ targetValue = minY + (maxY - minY) / 2;
713
+ break;
714
+ }
715
+ case 'bottom':
716
+ targetValue = Math.max(...selectedNodes.map((n) => n.position.y + (n.height || 60)));
717
+ break;
718
+ }
719
+ this.nodes.update((currentNodes) => currentNodes.map((node) => {
720
+ if (!node.selected)
721
+ return node;
722
+ const newPos = { ...node.position };
723
+ const width = node.width || 170;
724
+ const height = node.height || 60;
725
+ switch (alignment) {
726
+ case 'left':
727
+ newPos.x = targetValue;
728
+ break;
729
+ case 'center':
730
+ newPos.x = targetValue - width / 2;
731
+ break;
732
+ case 'right':
733
+ newPos.x = targetValue - width;
734
+ break;
735
+ case 'top':
736
+ newPos.y = targetValue;
737
+ break;
738
+ case 'middle':
739
+ newPos.y = targetValue - height / 2;
740
+ break;
741
+ case 'bottom':
742
+ newPos.y = targetValue - height;
743
+ break;
744
+ }
745
+ return { ...node, position: newPos };
746
+ }));
747
+ }
748
+ distributeNodes(distribution) {
749
+ const selectedNodes = this.selectedNodes();
750
+ if (selectedNodes.length < 3)
751
+ return;
752
+ this.undoRedoService.saveState(this.getCurrentState());
753
+ // Sort nodes by position
754
+ const sortedNodes = [...selectedNodes].sort((a, b) => {
755
+ return distribution === 'horizontal' ? a.position.x - b.position.x : a.position.y - b.position.y;
756
+ });
757
+ const firstNode = sortedNodes[0];
758
+ const lastNode = sortedNodes[sortedNodes.length - 1];
759
+ if (distribution === 'horizontal') {
760
+ const totalSpan = lastNode.position.x - firstNode.position.x;
761
+ const step = totalSpan / (sortedNodes.length - 1);
762
+ this.nodes.update(nodes => nodes.map(node => {
763
+ const index = sortedNodes.findIndex(n => n.id === node.id);
764
+ if (index === -1)
765
+ return node;
766
+ return {
767
+ ...node,
768
+ position: {
769
+ ...node.position,
770
+ x: firstNode.position.x + (step * index)
771
+ }
772
+ };
773
+ }));
774
+ }
775
+ else {
776
+ const totalSpan = lastNode.position.y - firstNode.position.y;
777
+ const step = totalSpan / (sortedNodes.length - 1);
778
+ this.nodes.update(nodes => nodes.map(node => {
779
+ const index = sortedNodes.findIndex(n => n.id === node.id);
780
+ if (index === -1)
781
+ return node;
782
+ return {
783
+ ...node,
784
+ position: {
785
+ ...node.position,
786
+ y: firstNode.position.y + (step * index)
787
+ }
788
+ };
789
+ }));
790
+ }
791
+ }
792
+ // --- Internal batching methods for components to use ---
793
+ batchUpdateNodes(updatedNodes) {
794
+ this.nodeUpdates$.next(updatedNodes);
795
+ }
796
+ batchUpdateEdges(updatedEdges) {
797
+ this.edgeUpdates$.next(updatedEdges);
798
+ }
799
+ batchUpdateViewport(updatedViewport) {
800
+ this.viewportUpdates$.next(updatedViewport);
801
+ }
802
+ // --- Event Emitters triggered by components ---
803
+ onNodeClick(node) {
804
+ this.nodeClick.emit(node);
805
+ }
806
+ onEdgeClick(edge) {
807
+ this.edgeClick.emit(edge);
808
+ }
809
+ onDragStart(node) {
810
+ this.undoRedoService.saveState(this.getCurrentState()); // Save state before drag starts
811
+ this.dragStart.emit(node);
812
+ this.updateNode(node.id, { dragging: true });
813
+ }
814
+ onDragEnd(node) {
815
+ this.dragEnd.emit(node);
816
+ this.updateNode(node.id, { dragging: false });
817
+ this.alignmentGuides.set([]); // Clear alignment guides
818
+ // State is already saved at dragStart, so no need to save again here unless
819
+ // a single drag operation is considered a single undoable action.
820
+ // If multiple small state changes during drag need to be undone as one,
821
+ // then the saveState logic here would be different (e.g., debounced save).
822
+ }
823
+ // --- Clipboard Operations ---
824
+ clipboard = { nodes: [], edges: [] };
825
+ copy() {
826
+ const selectedNodes = this.selectedNodes();
827
+ if (selectedNodes.length === 0)
828
+ return;
829
+ const selectedNodeIds = new Set(selectedNodes.map((n) => n.id));
830
+ // Find edges that are connected ONLY to selected nodes
831
+ const internalEdges = this.edges().filter((edge) => selectedNodeIds.has(edge.source) && selectedNodeIds.has(edge.target));
832
+ // Deep copy to avoid reference issues
833
+ this.clipboard = {
834
+ nodes: JSON.parse(JSON.stringify(selectedNodes)),
835
+ edges: JSON.parse(JSON.stringify(internalEdges)),
836
+ };
837
+ console.log('Copied to clipboard:', this.clipboard);
838
+ }
839
+ paste() {
840
+ if (this.clipboard.nodes.length === 0)
841
+ return;
842
+ this.undoRedoService.saveState(this.getCurrentState());
843
+ const idMap = new Map();
844
+ const newNodes = [];
845
+ const newEdges = [];
846
+ // Create new nodes with new IDs and offset position
847
+ this.clipboard.nodes.forEach((node) => {
848
+ const newId = v4();
849
+ idMap.set(node.id, newId);
850
+ newNodes.push({
851
+ ...node,
852
+ id: newId,
853
+ position: { x: node.position.x + 20, y: node.position.y + 20 },
854
+ selected: true, // Select pasted nodes
855
+ });
856
+ });
857
+ // Create new edges with updated source/target IDs
858
+ this.clipboard.edges.forEach((edge) => {
859
+ const newSource = idMap.get(edge.source);
860
+ const newTarget = idMap.get(edge.target);
861
+ if (newSource && newTarget) {
862
+ newEdges.push({
863
+ ...edge,
864
+ id: v4(),
865
+ source: newSource,
866
+ target: newTarget,
867
+ selected: true, // Select pasted edges
868
+ });
869
+ }
870
+ });
871
+ // Deselect existing elements
872
+ this.clearSelection();
873
+ // Add new elements
874
+ this.nodes.update((nodes) => [...nodes, ...newNodes]);
875
+ this.edges.update((edges) => [...edges, ...newEdges]);
876
+ }
877
+ cut() {
878
+ this.copy();
879
+ this.deleteSelectedElements();
880
+ }
881
+ duplicate() {
882
+ this.copy();
883
+ this.paste();
884
+ }
885
+ // --- Grouping Operations ---
886
+ groupNodes(nodeIds) {
887
+ if (nodeIds.length < 2)
888
+ return;
889
+ this.undoRedoService.saveState(this.getCurrentState());
890
+ const nodesToGroup = this.nodes().filter((n) => nodeIds.includes(n.id));
891
+ if (nodesToGroup.length === 0)
892
+ return;
893
+ // Calculate bounding box
894
+ const minX = Math.min(...nodesToGroup.map((n) => n.position.x));
895
+ const minY = Math.min(...nodesToGroup.map((n) => n.position.y));
896
+ const maxX = Math.max(...nodesToGroup.map((n) => n.position.x + (n.width || 150)));
897
+ const maxY = Math.max(...nodesToGroup.map((n) => n.position.y + (n.height || 60)));
898
+ const padding = 20;
899
+ const groupNode = {
900
+ id: v4(),
901
+ type: 'group',
902
+ position: { x: minX - padding, y: minY - padding },
903
+ width: maxX - minX + padding * 2,
904
+ height: maxY - minY + padding * 2,
905
+ data: { label: 'Group' },
906
+ expanded: true,
907
+ selected: true,
908
+ };
909
+ // Update children to point to parent
910
+ // Note: We are keeping absolute coordinates for now, so no position update needed for children
911
+ const updatedChildren = nodesToGroup.map((n) => ({ ...n, parentId: groupNode.id, selected: false }));
912
+ const otherNodes = this.nodes().filter((n) => !nodeIds.includes(n.id));
913
+ this.nodes.set([...otherNodes, groupNode, ...updatedChildren]);
914
+ this.clearSelection();
915
+ this.selectNodes([groupNode.id]);
916
+ }
917
+ ungroupNodes(groupId) {
918
+ const groupNode = this.nodes().find((n) => n.id === groupId);
919
+ if (!groupNode || groupNode.type !== 'group')
920
+ return;
921
+ this.undoRedoService.saveState(this.getCurrentState());
922
+ // Remove parentId from children
923
+ const updatedNodes = this.nodes().map((n) => {
924
+ if (n.parentId === groupId) {
925
+ const { parentId, ...rest } = n;
926
+ return { ...rest, selected: true };
927
+ }
928
+ return n;
929
+ });
930
+ // Remove group node
931
+ this.nodes.set(updatedNodes.filter((n) => n.id !== groupId));
932
+ }
933
+ addNode(node) {
934
+ this.undoRedoService.saveState(this.getCurrentState());
935
+ this.nodes.update((nodes) => [...nodes, { ...node, selected: false }]);
936
+ }
937
+ removeNode(id) {
938
+ this.undoRedoService.saveState(this.getCurrentState());
939
+ this.nodes.update((nodes) => nodes.filter((n) => n.id !== id));
940
+ this.edges.update((edges) => edges.filter((e) => e.source !== id && e.target !== id));
941
+ }
942
+ updateNode(id, changes) {
943
+ if (!changes.dragging && !changes.position) {
944
+ this.undoRedoService.saveState(this.getCurrentState());
945
+ }
946
+ this.nodes.update((nodes) => nodes.map((n) => {
947
+ if (n.id === id) {
948
+ return { ...n, ...changes };
949
+ }
950
+ return n;
951
+ }));
952
+ }
953
+ resizeNode(id, width, height, position) {
954
+ this.nodes.update((nodes) => nodes.map((n) => {
955
+ if (n.id === id) {
956
+ return { ...n, width, height, ...(position ? { position } : {}) };
957
+ }
958
+ return n;
959
+ }));
960
+ }
961
+ toggleGroup(groupId) {
962
+ this.nodes.update((nodes) => nodes.map((n) => {
963
+ if (n.id === groupId) {
964
+ return { ...n, expanded: !n.expanded };
965
+ }
966
+ return n;
967
+ }));
968
+ }
969
+ // Move node and handle group children
970
+ moveNode(id, newPosition) {
971
+ // console.log('moveNode', id, newPosition);
972
+ const node = this.nodes().find((n) => n.id === id);
973
+ if (!node)
974
+ return;
975
+ // Snapping Logic
976
+ let snappedX = newPosition.x;
977
+ let snappedY = newPosition.y;
978
+ const guides = [];
979
+ const SNAP_DISTANCE = 5;
980
+ const GRID_SIZE = 20;
981
+ // 1. Snap to Grid (lower priority)
982
+ snappedX = Math.round(snappedX / GRID_SIZE) * GRID_SIZE;
983
+ snappedY = Math.round(snappedY / GRID_SIZE) * GRID_SIZE;
984
+ // 2. Snap to Nodes (higher priority)
985
+ // Only snap if dragging a single node (for simplicity for now)
986
+ const otherNodes = this.nodes().filter(n => n.id !== id && n.parentId === node.parentId && !n.selected);
987
+ let snappedXNode = false;
988
+ let snappedYNode = false;
989
+ // Horizontal Alignment (aligning Y coordinates)
990
+ // Check Top, Center, Bottom
991
+ const myHeight = node.height || 150; // Default height
992
+ const myCenterY = newPosition.y + myHeight / 2;
993
+ const myBottomY = newPosition.y + myHeight;
994
+ for (const other of otherNodes) {
995
+ const otherHeight = other.height || 150;
996
+ const otherY = other.position.y;
997
+ const otherCenterY = otherY + otherHeight / 2;
998
+ const otherBottomY = otherY + otherHeight;
999
+ // Top to Top
1000
+ if (Math.abs(newPosition.y - otherY) < SNAP_DISTANCE) {
1001
+ snappedY = otherY;
1002
+ snappedYNode = true;
1003
+ guides.push({ type: 'horizontal', position: otherY, start: Math.min(newPosition.x, other.position.x), end: Math.max(newPosition.x + (node.width || 150), other.position.x + (other.width || 150)) });
1004
+ }
1005
+ // Top to Bottom
1006
+ else if (Math.abs(newPosition.y - otherBottomY) < SNAP_DISTANCE) {
1007
+ snappedY = otherBottomY;
1008
+ snappedYNode = true;
1009
+ guides.push({ type: 'horizontal', position: otherBottomY, start: Math.min(newPosition.x, other.position.x), end: Math.max(newPosition.x + (node.width || 150), other.position.x + (other.width || 150)) });
1010
+ }
1011
+ // Center to Center
1012
+ else if (Math.abs(myCenterY - otherCenterY) < SNAP_DISTANCE) {
1013
+ snappedY = otherCenterY - myHeight / 2;
1014
+ snappedYNode = true;
1015
+ guides.push({ type: 'horizontal', position: otherCenterY, start: Math.min(newPosition.x, other.position.x), end: Math.max(newPosition.x + (node.width || 150), other.position.x + (other.width || 150)) });
1016
+ }
1017
+ // Bottom to Top
1018
+ else if (Math.abs(myBottomY - otherY) < SNAP_DISTANCE) {
1019
+ snappedY = otherY - myHeight;
1020
+ snappedYNode = true;
1021
+ guides.push({ type: 'horizontal', position: otherY, start: Math.min(newPosition.x, other.position.x), end: Math.max(newPosition.x + (node.width || 150), other.position.x + (other.width || 150)) });
1022
+ }
1023
+ // Bottom to Bottom
1024
+ else if (Math.abs(myBottomY - otherBottomY) < SNAP_DISTANCE) {
1025
+ snappedY = otherBottomY - myHeight;
1026
+ snappedYNode = true;
1027
+ guides.push({ type: 'horizontal', position: otherBottomY, start: Math.min(newPosition.x, other.position.x), end: Math.max(newPosition.x + (node.width || 150), other.position.x + (other.width || 150)) });
1028
+ }
1029
+ if (snappedYNode)
1030
+ break; // Snap to first match
1031
+ }
1032
+ // Vertical Alignment (aligning X coordinates)
1033
+ // Check Left, Center, Right
1034
+ const myWidth = node.width || 150; // Default width
1035
+ const myCenterX = newPosition.x + myWidth / 2;
1036
+ const myRightX = newPosition.x + myWidth;
1037
+ for (const other of otherNodes) {
1038
+ const otherWidth = other.width || 150;
1039
+ const otherX = other.position.x;
1040
+ const otherCenterX = otherX + otherWidth / 2;
1041
+ const otherRightX = otherX + otherWidth;
1042
+ // Left to Left
1043
+ if (Math.abs(newPosition.x - otherX) < SNAP_DISTANCE) {
1044
+ snappedX = otherX;
1045
+ snappedXNode = true;
1046
+ guides.push({ type: 'vertical', position: otherX, start: Math.min(newPosition.y, other.position.y), end: Math.max(newPosition.y + (node.height || 60), other.position.y + (other.height || 60)) });
1047
+ }
1048
+ // Left to Right
1049
+ else if (Math.abs(newPosition.x - otherRightX) < SNAP_DISTANCE) {
1050
+ snappedX = otherRightX;
1051
+ snappedXNode = true;
1052
+ guides.push({ type: 'vertical', position: otherRightX, start: Math.min(newPosition.y, other.position.y), end: Math.max(newPosition.y + (node.height || 60), other.position.y + (other.height || 60)) });
1053
+ }
1054
+ // Center to Center
1055
+ else if (Math.abs(myCenterX - otherCenterX) < SNAP_DISTANCE) {
1056
+ snappedX = otherCenterX - myWidth / 2;
1057
+ snappedXNode = true;
1058
+ guides.push({ type: 'vertical', position: otherCenterX, start: Math.min(newPosition.y, other.position.y), end: Math.max(newPosition.y + (node.height || 60), other.position.y + (other.height || 60)) });
1059
+ }
1060
+ // Right to Left
1061
+ else if (Math.abs(myRightX - otherX) < SNAP_DISTANCE) {
1062
+ snappedX = otherX - myWidth;
1063
+ snappedXNode = true;
1064
+ guides.push({ type: 'vertical', position: otherX, start: Math.min(newPosition.y, other.position.y), end: Math.max(newPosition.y + (node.height || 60), other.position.y + (other.height || 60)) });
1065
+ }
1066
+ // Right to Right
1067
+ else if (Math.abs(myRightX - otherRightX) < SNAP_DISTANCE) {
1068
+ snappedX = otherRightX - myWidth;
1069
+ snappedXNode = true;
1070
+ guides.push({ type: 'vertical', position: otherRightX, start: Math.min(newPosition.y, other.position.y), end: Math.max(newPosition.y + (node.height || 60), other.position.y + (other.height || 60)) });
1071
+ }
1072
+ if (snappedXNode)
1073
+ break;
1074
+ }
1075
+ this.alignmentGuides.set(guides);
1076
+ const finalPosition = { x: snappedX, y: snappedY };
1077
+ const dx = finalPosition.x - node.position.x;
1078
+ const dy = finalPosition.y - node.position.y;
1079
+ if (dx === 0 && dy === 0)
1080
+ return;
1081
+ // Update the moved node
1082
+ const updatedNodes = this.nodes().map((n) => {
1083
+ if (n.id === id) {
1084
+ return { ...n, position: finalPosition };
1085
+ }
1086
+ return n;
1087
+ });
1088
+ // If it's a group, move all children recursively
1089
+ if (node.type === 'group') {
1090
+ this.moveChildren(id, dx, dy, updatedNodes);
1091
+ }
1092
+ this.nodes.set(updatedNodes);
1093
+ }
1094
+ moveChildren(parentId, dx, dy, nodes, visited = new Set()) {
1095
+ if (visited.has(parentId)) {
1096
+ console.warn('Cycle detected in group hierarchy, stopping recursion for', parentId);
1097
+ return;
1098
+ }
1099
+ visited.add(parentId);
1100
+ for (let i = 0; i < nodes.length; i++) {
1101
+ if (nodes[i].parentId === parentId) {
1102
+ nodes[i] = {
1103
+ ...nodes[i],
1104
+ position: {
1105
+ x: nodes[i].position.x + dx,
1106
+ y: nodes[i].position.y + dy,
1107
+ },
1108
+ };
1109
+ // Recursively move children of children (nested groups)
1110
+ if (nodes[i].type === 'group') {
1111
+ this.moveChildren(nodes[i].id, dx, dy, nodes, visited);
1112
+ }
1113
+ }
1114
+ }
1115
+ }
1116
+ // Move multiple nodes (batch movement)
1117
+ moveNodes(moves) {
1118
+ console.log('moveNodes', moves.length);
1119
+ const GRID_SIZE = 20;
1120
+ let currentNodes = [...this.nodes()];
1121
+ this.alignmentGuides.set([]); // Clear guides during batch move for now
1122
+ moves.forEach(move => {
1123
+ const node = currentNodes.find(n => n.id === move.id);
1124
+ if (!node)
1125
+ return;
1126
+ // Simple grid snap for batch move
1127
+ const snappedX = Math.round(move.position.x / GRID_SIZE) * GRID_SIZE;
1128
+ const snappedY = Math.round(move.position.y / GRID_SIZE) * GRID_SIZE;
1129
+ const finalPosition = { x: snappedX, y: snappedY };
1130
+ const dx = finalPosition.x - node.position.x;
1131
+ const dy = finalPosition.y - node.position.y;
1132
+ // Update parent node
1133
+ currentNodes = currentNodes.map(n => n.id === move.id ? { ...n, position: finalPosition } : n);
1134
+ // If group, move children
1135
+ if (node.type === 'group') {
1136
+ this.moveChildren(node.id, dx, dy, currentNodes);
1137
+ }
1138
+ });
1139
+ this.nodes.set(currentNodes);
1140
+ }
1141
+ setSearchQuery(query) {
1142
+ this.searchQuery.set(query);
1143
+ }
1144
+ setFilterType(type) {
1145
+ this.filterType.set(type);
1146
+ }
1147
+ setZoom(zoom) {
1148
+ this.viewport.update(v => ({ ...v, zoom }));
1149
+ }
1150
+ setContainerDimensions(dimensions) {
1151
+ this.containerDimensions.set(dimensions);
1152
+ }
1153
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DiagramStateService, deps: [{ token: UndoRedoService }], target: i0.ɵɵFactoryTarget.Injectable });
1154
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DiagramStateService, providedIn: 'root' });
1155
+ }
1156
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DiagramStateService, decorators: [{
1157
+ type: Injectable,
1158
+ args: [{
1159
+ providedIn: 'root',
1160
+ }]
1161
+ }], ctorParameters: () => [{ type: UndoRedoService }] });
1162
+
1163
+ class MinimapComponent {
1164
+ diagramStateService;
1165
+ nodeColor = '#e2e2e2';
1166
+ nodeClass = '';
1167
+ // Initialize signals from service
1168
+ nodes;
1169
+ viewport;
1170
+ // Minimap dimensions
1171
+ width = 200;
1172
+ height = 150;
1173
+ constructor(diagramStateService) {
1174
+ this.diagramStateService = diagramStateService;
1175
+ this.nodes = this.diagramStateService.nodes;
1176
+ this.viewport = this.diagramStateService.viewport;
1177
+ }
1178
+ // Computed properties for rendering
1179
+ viewBox = computed(() => {
1180
+ const nodes = this.nodes();
1181
+ if (nodes.length === 0)
1182
+ return '0 0 100 100';
1183
+ const bounds = this.getBounds(nodes);
1184
+ const padding = 50;
1185
+ return `${bounds.minX - padding} ${bounds.minY - padding} ${bounds.width + padding * 2} ${bounds.height + padding * 2}`;
1186
+ }, ...(ngDevMode ? [{ debugName: "viewBox" }] : []));
1187
+ viewportIndicator = computed(() => {
1188
+ const v = this.viewport();
1189
+ const nodes = this.nodes();
1190
+ if (nodes.length === 0)
1191
+ return { x: 0, y: 0, width: 0, height: 0 };
1192
+ // We need to map the current viewport to the minimap coordinate system
1193
+ // This is a bit complex because the viewport transform is applied to the main diagram
1194
+ // and we need to show which part of the diagram is currently visible.
1195
+ // For now, let's just return a placeholder.
1196
+ // Real implementation requires knowing the diagram container size which we might need to get from service
1197
+ return { x: -v.x / v.zoom, y: -v.y / v.zoom, width: 1000 / v.zoom, height: 800 / v.zoom };
1198
+ }, ...(ngDevMode ? [{ debugName: "viewportIndicator" }] : []));
1199
+ getBounds(nodes) {
1200
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1201
+ nodes.forEach(node => {
1202
+ minX = Math.min(minX, node.position.x);
1203
+ minY = Math.min(minY, node.position.y);
1204
+ maxX = Math.max(maxX, node.position.x + (node.width || 170));
1205
+ maxY = Math.max(maxY, node.position.y + (node.height || 60));
1206
+ });
1207
+ return {
1208
+ minX,
1209
+ minY,
1210
+ width: maxX - minX,
1211
+ height: maxY - minY
1212
+ };
1213
+ }
1214
+ onMinimapClick(event) {
1215
+ const svg = event.currentTarget;
1216
+ const rect = svg.getBoundingClientRect();
1217
+ // Click position relative to minimap
1218
+ const clickX = event.clientX - rect.left;
1219
+ const clickY = event.clientY - rect.top;
1220
+ // Convert to SVG coordinates
1221
+ // We need to parse the viewBox to know the scale
1222
+ const vb = this.viewBox().split(' ').map(parseFloat);
1223
+ const vbX = vb[0];
1224
+ const vbY = vb[1];
1225
+ const vbW = vb[2];
1226
+ const vbH = vb[3];
1227
+ const scaleX = vbW / rect.width;
1228
+ const scaleY = vbH / rect.height;
1229
+ const svgX = vbX + clickX * scaleX;
1230
+ const svgY = vbY + clickY * scaleY;
1231
+ // Center the viewport on this point
1232
+ // New viewport x = -svgX * zoom + viewportWidth / 2
1233
+ // We need the diagram dimensions to center perfectly, but for now we can approximate
1234
+ // or just move the top-left to this point.
1235
+ // Let's try to center.
1236
+ const currentZoom = this.viewport().zoom;
1237
+ // Assuming a default diagram size or getting it from service would be better
1238
+ // For now, let's just move to that position
1239
+ const newX = -svgX * currentZoom + 400; // 400 is approx half width
1240
+ const newY = -svgY * currentZoom + 300; // 300 is approx half height
1241
+ this.diagramStateService.setViewport({
1242
+ x: newX,
1243
+ y: newY,
1244
+ zoom: currentZoom
1245
+ });
1246
+ }
1247
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: MinimapComponent, deps: [{ token: DiagramStateService }], target: i0.ɵɵFactoryTarget.Component });
1248
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: MinimapComponent, isStandalone: true, selector: "ngx-workflow-minimap", inputs: { nodeColor: "nodeColor", nodeClass: "nodeClass" }, ngImport: i0, template: "<div class=\"ngx-workflow__minimap\">\r\n <svg [attr.viewBox]=\"viewBox()\" class=\"ngx-workflow__minimap-svg\" (click)=\"onMinimapClick($event)\">\r\n <rect *ngFor=\"let node of nodes()\" [attr.x]=\"node.position.x\" [attr.y]=\"node.position.y\"\r\n [attr.width]=\"node.width || 170\" [attr.height]=\"node.height || 60\" [attr.fill]=\"nodeColor\" [class]=\"nodeClass\"\r\n class=\"ngx-workflow__minimap-node\" />\r\n <rect class=\"ngx-workflow__minimap-viewport\" [attr.x]=\"viewportIndicator().x\" [attr.y]=\"viewportIndicator().y\"\r\n [attr.width]=\"viewportIndicator().width\" [attr.height]=\"viewportIndicator().height\" />\r\n </svg>\r\n</div>", styles: [".ngx-workflow__minimap{position:absolute;bottom:20px;right:20px;width:200px;height:150px;background-color:#fff;border:1px solid #e2e2e2;border-radius:4px;box-shadow:0 2px 4px #0000001a;overflow:hidden;z-index:5}.ngx-workflow__minimap-svg{width:100%;height:100%;display:block}.ngx-workflow__minimap-node{stroke:none}.ngx-workflow__minimap-viewport{fill:#0000ff1a;stroke:#00f;stroke-width:1px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1249
+ }
1250
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: MinimapComponent, decorators: [{
1251
+ type: Component,
1252
+ args: [{ selector: 'ngx-workflow-minimap', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ngx-workflow__minimap\">\r\n <svg [attr.viewBox]=\"viewBox()\" class=\"ngx-workflow__minimap-svg\" (click)=\"onMinimapClick($event)\">\r\n <rect *ngFor=\"let node of nodes()\" [attr.x]=\"node.position.x\" [attr.y]=\"node.position.y\"\r\n [attr.width]=\"node.width || 170\" [attr.height]=\"node.height || 60\" [attr.fill]=\"nodeColor\" [class]=\"nodeClass\"\r\n class=\"ngx-workflow__minimap-node\" />\r\n <rect class=\"ngx-workflow__minimap-viewport\" [attr.x]=\"viewportIndicator().x\" [attr.y]=\"viewportIndicator().y\"\r\n [attr.width]=\"viewportIndicator().width\" [attr.height]=\"viewportIndicator().height\" />\r\n </svg>\r\n</div>", styles: [".ngx-workflow__minimap{position:absolute;bottom:20px;right:20px;width:200px;height:150px;background-color:#fff;border:1px solid #e2e2e2;border-radius:4px;box-shadow:0 2px 4px #0000001a;overflow:hidden;z-index:5}.ngx-workflow__minimap-svg{width:100%;height:100%;display:block}.ngx-workflow__minimap-node{stroke:none}.ngx-workflow__minimap-viewport{fill:#0000ff1a;stroke:#00f;stroke-width:1px}\n"] }]
1253
+ }], ctorParameters: () => [{ type: DiagramStateService }], propDecorators: { nodeColor: [{
1254
+ type: Input
1255
+ }], nodeClass: [{
1256
+ type: Input
1257
+ }] } });
1258
+
1259
+ class BackgroundComponent {
1260
+ variant = 'dots';
1261
+ gap = 20;
1262
+ size = 1;
1263
+ color = '#81818a';
1264
+ backgroundColor = '#f0f0f0';
1265
+ diagramStateService = inject(DiagramStateService);
1266
+ // Computed property for pattern transform based on viewport
1267
+ patternTransform = computed(() => {
1268
+ const viewport = this.diagramStateService.viewport();
1269
+ return `translate(${viewport.x}, ${viewport.y}) scale(${viewport.zoom})`;
1270
+ }, ...(ngDevMode ? [{ debugName: "patternTransform" }] : []));
1271
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: BackgroundComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1272
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: BackgroundComponent, isStandalone: true, selector: "ngx-workflow-background", inputs: { variant: "variant", gap: "gap", size: "size", color: "color", backgroundColor: "backgroundColor" }, ngImport: i0, template: "<svg class=\"ngx-workflow__background\" [attr.width]=\"'100%'\" [attr.height]=\"'100%'\">\r\n <defs>\r\n <!-- Dots Pattern -->\r\n <pattern *ngIf=\"variant === 'dots'\" id=\"ngx-workflow__pattern-dots\" [attr.x]=\"0\" [attr.y]=\"0\" [attr.width]=\"gap\"\r\n [attr.height]=\"gap\" patternUnits=\"userSpaceOnUse\" [attr.patternTransform]=\"patternTransform()\">\r\n <circle [attr.cx]=\"size\" [attr.cy]=\"size\" [attr.r]=\"size\" [attr.fill]=\"color\" />\r\n </pattern>\r\n\r\n <!-- Lines Pattern -->\r\n <pattern *ngIf=\"variant === 'lines'\" id=\"ngx-workflow__pattern-lines\" [attr.x]=\"0\" [attr.y]=\"0\" [attr.width]=\"gap\"\r\n [attr.height]=\"gap\" patternUnits=\"userSpaceOnUse\" [attr.patternTransform]=\"patternTransform()\">\r\n <path [attr.d]=\"'M ' + gap + ' 0 L 0 0 0 ' + gap\" [attr.fill]=\"'none'\" [attr.stroke]=\"color\"\r\n [attr.stroke-width]=\"size\" />\r\n </pattern>\r\n\r\n <!-- Cross Pattern -->\r\n <pattern *ngIf=\"variant === 'cross'\" id=\"ngx-workflow__pattern-cross\" [attr.x]=\"0\" [attr.y]=\"0\" [attr.width]=\"gap\"\r\n [attr.height]=\"gap\" patternUnits=\"userSpaceOnUse\" [attr.patternTransform]=\"patternTransform()\">\r\n <path [attr.d]=\"'M ' + gap + ' 0 L 0 0 0 ' + gap\" [attr.fill]=\"'none'\" [attr.stroke]=\"color\"\r\n [attr.stroke-width]=\"size\" />\r\n <path [attr.d]=\"'M 0 ' + (gap/2) + ' L ' + gap + ' ' + (gap/2) + ' M ' + (gap/2) + ' 0 L ' + (gap/2) + ' ' + gap\"\r\n [attr.fill]=\"'none'\" [attr.stroke]=\"color\" [attr.stroke-width]=\"size\" />\r\n </pattern>\r\n </defs>\r\n\r\n <rect width=\"100%\" height=\"100%\" [attr.fill]=\"backgroundColor\" />\r\n <rect width=\"100%\" height=\"100%\" [attr.fill]=\"'url(#ngx-workflow__pattern-' + variant + ')'\" />\r\n</svg>", styles: [".ngx-workflow__background{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
1273
+ }
1274
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: BackgroundComponent, decorators: [{
1275
+ type: Component,
1276
+ args: [{ selector: 'ngx-workflow-background', standalone: true, imports: [CommonModule], template: "<svg class=\"ngx-workflow__background\" [attr.width]=\"'100%'\" [attr.height]=\"'100%'\">\r\n <defs>\r\n <!-- Dots Pattern -->\r\n <pattern *ngIf=\"variant === 'dots'\" id=\"ngx-workflow__pattern-dots\" [attr.x]=\"0\" [attr.y]=\"0\" [attr.width]=\"gap\"\r\n [attr.height]=\"gap\" patternUnits=\"userSpaceOnUse\" [attr.patternTransform]=\"patternTransform()\">\r\n <circle [attr.cx]=\"size\" [attr.cy]=\"size\" [attr.r]=\"size\" [attr.fill]=\"color\" />\r\n </pattern>\r\n\r\n <!-- Lines Pattern -->\r\n <pattern *ngIf=\"variant === 'lines'\" id=\"ngx-workflow__pattern-lines\" [attr.x]=\"0\" [attr.y]=\"0\" [attr.width]=\"gap\"\r\n [attr.height]=\"gap\" patternUnits=\"userSpaceOnUse\" [attr.patternTransform]=\"patternTransform()\">\r\n <path [attr.d]=\"'M ' + gap + ' 0 L 0 0 0 ' + gap\" [attr.fill]=\"'none'\" [attr.stroke]=\"color\"\r\n [attr.stroke-width]=\"size\" />\r\n </pattern>\r\n\r\n <!-- Cross Pattern -->\r\n <pattern *ngIf=\"variant === 'cross'\" id=\"ngx-workflow__pattern-cross\" [attr.x]=\"0\" [attr.y]=\"0\" [attr.width]=\"gap\"\r\n [attr.height]=\"gap\" patternUnits=\"userSpaceOnUse\" [attr.patternTransform]=\"patternTransform()\">\r\n <path [attr.d]=\"'M ' + gap + ' 0 L 0 0 0 ' + gap\" [attr.fill]=\"'none'\" [attr.stroke]=\"color\"\r\n [attr.stroke-width]=\"size\" />\r\n <path [attr.d]=\"'M 0 ' + (gap/2) + ' L ' + gap + ' ' + (gap/2) + ' M ' + (gap/2) + ' 0 L ' + (gap/2) + ' ' + gap\"\r\n [attr.fill]=\"'none'\" [attr.stroke]=\"color\" [attr.stroke-width]=\"size\" />\r\n </pattern>\r\n </defs>\r\n\r\n <rect width=\"100%\" height=\"100%\" [attr.fill]=\"backgroundColor\" />\r\n <rect width=\"100%\" height=\"100%\" [attr.fill]=\"'url(#ngx-workflow__pattern-' + variant + ')'\" />\r\n</svg>", styles: [".ngx-workflow__background{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}\n"] }]
1277
+ }], propDecorators: { variant: [{
1278
+ type: Input
1279
+ }], gap: [{
1280
+ type: Input
1281
+ }], size: [{
1282
+ type: Input
1283
+ }], color: [{
1284
+ type: Input
1285
+ }], backgroundColor: [{
1286
+ type: Input
1287
+ }] } });
1288
+
1289
+ class AlignmentControlsComponent {
1290
+ diagramStateService;
1291
+ // Only show controls if more than 1 node is selected
1292
+ showControls = computed(() => this.diagramStateService.selectedNodes().length > 1, ...(ngDevMode ? [{ debugName: "showControls" }] : []));
1293
+ // Only show distribution controls if more than 2 nodes are selected
1294
+ showDistribution = computed(() => this.diagramStateService.selectedNodes().length > 2, ...(ngDevMode ? [{ debugName: "showDistribution" }] : []));
1295
+ constructor(diagramStateService) {
1296
+ this.diagramStateService = diagramStateService;
1297
+ }
1298
+ align(alignment) {
1299
+ this.diagramStateService.alignNodes(alignment);
1300
+ }
1301
+ distribute(distribution) {
1302
+ this.diagramStateService.distributeNodes(distribution);
1303
+ }
1304
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AlignmentControlsComponent, deps: [{ token: DiagramStateService }], target: i0.ɵɵFactoryTarget.Component });
1305
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: AlignmentControlsComponent, isStandalone: true, selector: "ngx-workflow-alignment-controls", ngImport: i0, template: "<div class=\"ngx-workflow__alignment-controls\" *ngIf=\"showControls()\">\r\n <div class=\"ngx-workflow__alignment-group\">\r\n <button (click)=\"align('left')\" title=\"Align Left\">Left</button>\r\n <button (click)=\"align('center')\" title=\"Align Center\">Center</button>\r\n <button (click)=\"align('right')\" title=\"Align Right\">Right</button>\r\n </div>\r\n <div class=\"ngx-workflow__alignment-group\">\r\n <button (click)=\"align('top')\" title=\"Align Top\">Top</button>\r\n <button (click)=\"align('middle')\" title=\"Align Middle\">Middle</button>\r\n <button (click)=\"align('bottom')\" title=\"Align Bottom\">Bottom</button>\r\n </div>\r\n <div class=\"ngx-workflow__alignment-group\" *ngIf=\"showDistribution()\">\r\n <button (click)=\"distribute('horizontal')\" title=\"Distribute Horizontally\">Dist H</button>\r\n <button (click)=\"distribute('vertical')\" title=\"Distribute Vertically\">Dist V</button>\r\n </div>\r\n</div>", styles: [".ngx-workflow__alignment-controls{position:absolute;top:20px;left:50%;transform:translate(-50%);background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:8px;display:flex;gap:12px;box-shadow:0 4px 6px #0000001a;z-index:10}.ngx-workflow__alignment-group{display:flex;gap:4px;align-items:center}.ngx-workflow__alignment-group:not(:last-child){padding-right:12px;border-right:1px solid #e0e0e0}button{background:none;border:1px solid transparent;border-radius:4px;padding:4px 8px;font-size:12px;cursor:pointer;color:#555;transition:all .2s}button:hover{background:#f5f5f5;border-color:#ddd;color:#333}button:active{background:#e0e0e0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1306
+ }
1307
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AlignmentControlsComponent, decorators: [{
1308
+ type: Component,
1309
+ args: [{ selector: 'ngx-workflow-alignment-controls', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ngx-workflow__alignment-controls\" *ngIf=\"showControls()\">\r\n <div class=\"ngx-workflow__alignment-group\">\r\n <button (click)=\"align('left')\" title=\"Align Left\">Left</button>\r\n <button (click)=\"align('center')\" title=\"Align Center\">Center</button>\r\n <button (click)=\"align('right')\" title=\"Align Right\">Right</button>\r\n </div>\r\n <div class=\"ngx-workflow__alignment-group\">\r\n <button (click)=\"align('top')\" title=\"Align Top\">Top</button>\r\n <button (click)=\"align('middle')\" title=\"Align Middle\">Middle</button>\r\n <button (click)=\"align('bottom')\" title=\"Align Bottom\">Bottom</button>\r\n </div>\r\n <div class=\"ngx-workflow__alignment-group\" *ngIf=\"showDistribution()\">\r\n <button (click)=\"distribute('horizontal')\" title=\"Distribute Horizontally\">Dist H</button>\r\n <button (click)=\"distribute('vertical')\" title=\"Distribute Vertically\">Dist V</button>\r\n </div>\r\n</div>", styles: [".ngx-workflow__alignment-controls{position:absolute;top:20px;left:50%;transform:translate(-50%);background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:8px;display:flex;gap:12px;box-shadow:0 4px 6px #0000001a;z-index:10}.ngx-workflow__alignment-group{display:flex;gap:4px;align-items:center}.ngx-workflow__alignment-group:not(:last-child){padding-right:12px;border-right:1px solid #e0e0e0}button{background:none;border:1px solid transparent;border-radius:4px;padding:4px 8px;font-size:12px;cursor:pointer;color:#555;transition:all .2s}button:hover{background:#f5f5f5;border-color:#ddd;color:#333}button:active{background:#e0e0e0}\n"] }]
1310
+ }], ctorParameters: () => [{ type: DiagramStateService }] });
1311
+
1312
+ class PropertiesSidebarComponent {
1313
+ node = null;
1314
+ close = new EventEmitter();
1315
+ change = new EventEmitter();
1316
+ updateLabel(label) {
1317
+ if (!this.node)
1318
+ return;
1319
+ this.change.emit({ label });
1320
+ }
1321
+ updateWidth(width) {
1322
+ this.change.emit({ width });
1323
+ }
1324
+ updateHeight(height) {
1325
+ this.change.emit({ height });
1326
+ }
1327
+ updateX(x) {
1328
+ if (!this.node)
1329
+ return;
1330
+ this.change.emit({ position: { x, y: this.node.position.y } });
1331
+ }
1332
+ updateY(y) {
1333
+ if (!this.node)
1334
+ return;
1335
+ this.change.emit({ position: { x: this.node.position.x, y } });
1336
+ }
1337
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: PropertiesSidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1338
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: PropertiesSidebarComponent, isStandalone: true, selector: "ngx-workflow-properties-sidebar", inputs: { node: "node" }, outputs: { close: "close", change: "change" }, ngImport: i0, template: "<div class=\"ngx-workflow__sidebar\" [class.open]=\"!!node\">\r\n <div class=\"ngx-workflow__sidebar-header\">\r\n <h3>Node Properties</h3>\r\n <button class=\"ngx-workflow__close-btn\" (click)=\"close.emit()\">\u00D7</button>\r\n </div>\r\n\r\n <div class=\"ngx-workflow__sidebar-content\" *ngIf=\"node\">\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>ID</label>\r\n <input type=\"text\" [value]=\"node.id\" disabled class=\"ngx-workflow__input disabled\">\r\n </div>\r\n\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>Label</label>\r\n <input type=\"text\" [ngModel]=\"node.label\" (ngModelChange)=\"updateLabel($event)\" class=\"ngx-workflow__input\">\r\n </div>\r\n\r\n <div class=\"ngx-workflow__form-row\">\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>X</label>\r\n <input type=\"number\" [ngModel]=\"node.position.x\" (ngModelChange)=\"updateX($event)\"\r\n class=\"ngx-workflow__input\">\r\n </div>\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>Y</label>\r\n <input type=\"number\" [ngModel]=\"node.position.y\" (ngModelChange)=\"updateY($event)\"\r\n class=\"ngx-workflow__input\">\r\n </div>\r\n </div>\r\n\r\n <div class=\"ngx-workflow__form-row\">\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>Width</label>\r\n <input type=\"number\" [ngModel]=\"node.width || 170\" (ngModelChange)=\"updateWidth($event)\"\r\n class=\"ngx-workflow__input\">\r\n </div>\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>Height</label>\r\n <input type=\"number\" [ngModel]=\"node.height || 60\" (ngModelChange)=\"updateHeight($event)\"\r\n class=\"ngx-workflow__input\">\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: [".ngx-workflow__sidebar{position:absolute;top:20px;right:20px;width:300px;background:var(--ngx-workflow-glass-bg);-webkit-backdrop-filter:var(--ngx-workflow-glass-blur);backdrop-filter:var(--ngx-workflow-glass-blur);border:1px solid var(--ngx-workflow-border);border-radius:12px;box-shadow:var(--ngx-workflow-shadow-lg);padding:0;transform:translate(120%);transition:transform .3s cubic-bezier(.4,0,.2,1);z-index:50;overflow:hidden}.ngx-workflow__sidebar.open{transform:translate(0)}.ngx-workflow__sidebar-header{display:flex;justify-content:space-between;align-items:center;padding:16px;border-bottom:1px solid var(--ngx-workflow-border);background:#ffffff80}.ngx-workflow__sidebar-header h3{margin:0;font-size:16px;font-weight:600;color:var(--ngx-workflow-text-primary)}.ngx-workflow__sidebar-content{padding:16px;display:flex;flex-direction:column;gap:16px}.ngx-workflow__close-btn{background:none;border:none;font-size:20px;color:var(--ngx-workflow-text-secondary);cursor:pointer;padding:4px;border-radius:4px;line-height:1}.ngx-workflow__close-btn:hover{background:#0000000d;color:var(--ngx-workflow-text-primary)}.ngx-workflow__form-group{display:flex;flex-direction:column;gap:6px}.ngx-workflow__form-group label{font-size:12px;font-weight:500;color:var(--ngx-workflow-text-secondary)}.ngx-workflow__form-row{display:flex;gap:12px}.ngx-workflow__form-row .ngx-workflow__form-group{flex:1}.ngx-workflow__input{padding:8px 12px;border-radius:6px;border:1px solid var(--ngx-workflow-border);background:var(--ngx-workflow-surface);color:var(--ngx-workflow-text-primary);font-size:14px;outline:none;transition:all .2s ease;width:100%;box-sizing:border-box}.ngx-workflow__input:focus{border-color:var(--ngx-workflow-primary);box-shadow:0 0 0 2px #3b82f61a}.ngx-workflow__input.disabled{background:#00000005;color:var(--ngx-workflow-text-secondary);cursor:not-allowed}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
1339
+ }
1340
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: PropertiesSidebarComponent, decorators: [{
1341
+ type: Component,
1342
+ args: [{ selector: 'ngx-workflow-properties-sidebar', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"ngx-workflow__sidebar\" [class.open]=\"!!node\">\r\n <div class=\"ngx-workflow__sidebar-header\">\r\n <h3>Node Properties</h3>\r\n <button class=\"ngx-workflow__close-btn\" (click)=\"close.emit()\">\u00D7</button>\r\n </div>\r\n\r\n <div class=\"ngx-workflow__sidebar-content\" *ngIf=\"node\">\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>ID</label>\r\n <input type=\"text\" [value]=\"node.id\" disabled class=\"ngx-workflow__input disabled\">\r\n </div>\r\n\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>Label</label>\r\n <input type=\"text\" [ngModel]=\"node.label\" (ngModelChange)=\"updateLabel($event)\" class=\"ngx-workflow__input\">\r\n </div>\r\n\r\n <div class=\"ngx-workflow__form-row\">\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>X</label>\r\n <input type=\"number\" [ngModel]=\"node.position.x\" (ngModelChange)=\"updateX($event)\"\r\n class=\"ngx-workflow__input\">\r\n </div>\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>Y</label>\r\n <input type=\"number\" [ngModel]=\"node.position.y\" (ngModelChange)=\"updateY($event)\"\r\n class=\"ngx-workflow__input\">\r\n </div>\r\n </div>\r\n\r\n <div class=\"ngx-workflow__form-row\">\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>Width</label>\r\n <input type=\"number\" [ngModel]=\"node.width || 170\" (ngModelChange)=\"updateWidth($event)\"\r\n class=\"ngx-workflow__input\">\r\n </div>\r\n <div class=\"ngx-workflow__form-group\">\r\n <label>Height</label>\r\n <input type=\"number\" [ngModel]=\"node.height || 60\" (ngModelChange)=\"updateHeight($event)\"\r\n class=\"ngx-workflow__input\">\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: [".ngx-workflow__sidebar{position:absolute;top:20px;right:20px;width:300px;background:var(--ngx-workflow-glass-bg);-webkit-backdrop-filter:var(--ngx-workflow-glass-blur);backdrop-filter:var(--ngx-workflow-glass-blur);border:1px solid var(--ngx-workflow-border);border-radius:12px;box-shadow:var(--ngx-workflow-shadow-lg);padding:0;transform:translate(120%);transition:transform .3s cubic-bezier(.4,0,.2,1);z-index:50;overflow:hidden}.ngx-workflow__sidebar.open{transform:translate(0)}.ngx-workflow__sidebar-header{display:flex;justify-content:space-between;align-items:center;padding:16px;border-bottom:1px solid var(--ngx-workflow-border);background:#ffffff80}.ngx-workflow__sidebar-header h3{margin:0;font-size:16px;font-weight:600;color:var(--ngx-workflow-text-primary)}.ngx-workflow__sidebar-content{padding:16px;display:flex;flex-direction:column;gap:16px}.ngx-workflow__close-btn{background:none;border:none;font-size:20px;color:var(--ngx-workflow-text-secondary);cursor:pointer;padding:4px;border-radius:4px;line-height:1}.ngx-workflow__close-btn:hover{background:#0000000d;color:var(--ngx-workflow-text-primary)}.ngx-workflow__form-group{display:flex;flex-direction:column;gap:6px}.ngx-workflow__form-group label{font-size:12px;font-weight:500;color:var(--ngx-workflow-text-secondary)}.ngx-workflow__form-row{display:flex;gap:12px}.ngx-workflow__form-row .ngx-workflow__form-group{flex:1}.ngx-workflow__input{padding:8px 12px;border-radius:6px;border:1px solid var(--ngx-workflow-border);background:var(--ngx-workflow-surface);color:var(--ngx-workflow-text-primary);font-size:14px;outline:none;transition:all .2s ease;width:100%;box-sizing:border-box}.ngx-workflow__input:focus{border-color:var(--ngx-workflow-primary);box-shadow:0 0 0 2px #3b82f61a}.ngx-workflow__input.disabled{background:#00000005;color:var(--ngx-workflow-text-secondary);cursor:not-allowed}\n"] }]
1343
+ }], propDecorators: { node: [{
1344
+ type: Input
1345
+ }], close: [{
1346
+ type: Output
1347
+ }], change: [{
1348
+ type: Output
1349
+ }] } });
1350
+
1351
+ // Helper function to get a node from the array
1352
+ function getNode(id, nodes) {
1353
+ return nodes.find(n => n.id === id);
1354
+ }
1355
+ // Helper function to determine handle position based on node and handle id/type
1356
+ function getHandleAbsolutePosition(node, handleId) {
1357
+ const nodeWidth = node.width || 170;
1358
+ const nodeHeight = node.height || 60;
1359
+ let offsetX = 0;
1360
+ let offsetY = 0;
1361
+ switch (handleId) {
1362
+ case 'top':
1363
+ offsetX = nodeWidth / 2;
1364
+ offsetY = 0;
1365
+ break;
1366
+ case 'right':
1367
+ offsetX = nodeWidth;
1368
+ offsetY = nodeHeight / 2;
1369
+ break;
1370
+ case 'bottom':
1371
+ offsetX = nodeWidth / 2;
1372
+ offsetY = nodeHeight;
1373
+ break;
1374
+ case 'left':
1375
+ offsetX = 0;
1376
+ offsetY = nodeHeight / 2;
1377
+ break;
1378
+ default: // Center of the node if no specific handle
1379
+ offsetX = nodeWidth / 2;
1380
+ offsetY = nodeHeight / 2;
1381
+ }
1382
+ return {
1383
+ x: node.position.x + offsetX,
1384
+ y: node.position.y + offsetY
1385
+ };
1386
+ }
1387
+ class DiagramComponent {
1388
+ el;
1389
+ renderer;
1390
+ ngZone;
1391
+ cdRef;
1392
+ diagramStateService;
1393
+ nodeTypes;
1394
+ // Trigger rebuild
1395
+ svgRef;
1396
+ // Input properties for declarative usage
1397
+ initialNodes = [];
1398
+ initialEdges = [];
1399
+ initialViewport;
1400
+ showZoomControls = true;
1401
+ // Input for showing/hiding minimap
1402
+ showMinimap = true;
1403
+ // Input for background configuration
1404
+ showBackground = true;
1405
+ backgroundVariant = 'dots';
1406
+ backgroundGap = 20;
1407
+ backgroundSize = 1;
1408
+ backgroundColor = '#81818a';
1409
+ backgroundBgColor = '#f0f0f0';
1410
+ // Output events
1411
+ nodeClick = new EventEmitter();
1412
+ edgeClick = new EventEmitter();
1413
+ connect = new EventEmitter();
1414
+ nodesChange = new EventEmitter();
1415
+ edgesChange = new EventEmitter();
1416
+ nodeDoubleClick = new EventEmitter();
1417
+ // Sidebar State
1418
+ selectedNodeForEditing = null;
1419
+ viewport;
1420
+ nodes;
1421
+ viewNodes;
1422
+ filteredNodes;
1423
+ edges;
1424
+ tempEdges;
1425
+ alignmentGuides;
1426
+ // Expose Math to the template
1427
+ Math = Math;
1428
+ _pathFinder = null;
1429
+ pathCache = new Map();
1430
+ dragAnimationFrameId = null;
1431
+ unlistenPointerMove = null;
1432
+ unlistenPointerUp = null;
1433
+ unlistenPointerLeave = null;
1434
+ pathPointsCache = new Map();
1435
+ isPanning = false;
1436
+ lastPanPosition = { x: 0, y: 0 };
1437
+ subscriptions = new Subscription();
1438
+ // Lasso selection properties
1439
+ isSelecting = false;
1440
+ selectionStart = { x: 0, y: 0 };
1441
+ selectionEnd = { x: 0, y: 0 };
1442
+ // Node Dragging
1443
+ isDraggingNode = false;
1444
+ draggingNode = null;
1445
+ draggingNodes = []; // All nodes being dragged (for multi-select)
1446
+ startNodePosition = { x: 0, y: 0 };
1447
+ startNodePositions = new Map(); // Initial positions for multi-drag
1448
+ startPointerPosition = { x: 0, y: 0 };
1449
+ // Connection (Handle)
1450
+ isConnecting = false;
1451
+ currentPreviewEdgeId = null;
1452
+ currentTargetHandle = null;
1453
+ connectingSourceNodeId = null;
1454
+ connectingSourceHandleId = undefined;
1455
+ // Resizing
1456
+ isResizing = false;
1457
+ resizingNode = null;
1458
+ resizeHandle = null;
1459
+ startResizePosition = { x: 0, y: 0 };
1460
+ startNodeDimensions = { width: 0, height: 0, x: 0, y: 0 };
1461
+ // Edge Updating
1462
+ isUpdatingEdge = false;
1463
+ updatingEdge = null;
1464
+ updatingEdgeHandle = null;
1465
+ // Edge Label Editing
1466
+ editingEdgeId = null;
1467
+ updatePathFinder(nodes) {
1468
+ this.pathCache.clear();
1469
+ this.pathPointsCache.clear();
1470
+ this._pathFinder = new PathFinder(nodes.map(n => ({
1471
+ id: n.id,
1472
+ x: n.position.x,
1473
+ y: n.position.y,
1474
+ width: n.width || this.defaultNodeWidth,
1475
+ height: n.height || this.defaultNodeHeight
1476
+ })));
1477
+ }
1478
+ onEdgeDoubleClick(event, edge) {
1479
+ event.stopPropagation();
1480
+ event.preventDefault();
1481
+ this.editingEdgeId = edge.id;
1482
+ }
1483
+ onNodeDoubleClick(event, node) {
1484
+ console.log('Node double clicked:', node);
1485
+ event.stopPropagation();
1486
+ this.selectedNodeForEditing = node;
1487
+ this.nodeDoubleClick.emit(node);
1488
+ this.cdRef.detectChanges();
1489
+ }
1490
+ onDiagramDoubleClick(event) {
1491
+ let target = event.target;
1492
+ // If target is SVG (likely due to capture), find the actual element under cursor
1493
+ if (target === this.svgRef.nativeElement || target.classList.contains('ngx-workflow__background')) {
1494
+ const element = document.elementFromPoint(event.clientX, event.clientY);
1495
+ if (element) {
1496
+ target = element;
1497
+ }
1498
+ }
1499
+ const nodeElement = target.closest('.ngx-workflow__node');
1500
+ if (nodeElement) {
1501
+ const nodeId = nodeElement.dataset['id'];
1502
+ const node = this.nodes().find(n => n.id === nodeId);
1503
+ if (node) {
1504
+ this.onNodeDoubleClick(event, node);
1505
+ }
1506
+ }
1507
+ }
1508
+ closeSidebar() {
1509
+ this.selectedNodeForEditing = null;
1510
+ }
1511
+ onPropertiesChange(changes) {
1512
+ if (this.selectedNodeForEditing) {
1513
+ this.diagramStateService.updateNode(this.selectedNodeForEditing.id, changes);
1514
+ // Update local reference to keep sidebar in sync
1515
+ this.selectedNodeForEditing = { ...this.selectedNodeForEditing, ...changes };
1516
+ }
1517
+ }
1518
+ updateEdgeLabel(edge, newLabel) {
1519
+ this.diagramStateService.updateEdge(edge.id, { label: newLabel });
1520
+ this.editingEdgeId = null;
1521
+ }
1522
+ onEdgeLabelBlur() {
1523
+ this.editingEdgeId = null;
1524
+ }
1525
+ // Default node dimensions
1526
+ defaultNodeWidth = 170;
1527
+ defaultNodeHeight = 60;
1528
+ // Input for custom connection validation (optional)
1529
+ connectionValidator;
1530
+ // Input for node resizing (global toggle)
1531
+ nodesResizable = true;
1532
+ // Helper to check if a connection is allowed
1533
+ isValidConnection(sourceId, targetId) {
1534
+ // console.log('isValidConnection checking:', sourceId, targetId);
1535
+ // Prevent duplicate edges between same source and target
1536
+ const existing = this.edges().some(e => e.source === sourceId && e.target === targetId);
1537
+ if (existing) {
1538
+ // console.log('isValidConnection: existing edge found');
1539
+ return false;
1540
+ }
1541
+ // Use custom validator if provided
1542
+ if (this.connectionValidator) {
1543
+ return this.connectionValidator(sourceId, targetId);
1544
+ }
1545
+ return true;
1546
+ }
1547
+ constructor(el, // Host element
1548
+ renderer, ngZone, cdRef, diagramStateService, nodeTypes) {
1549
+ this.el = el;
1550
+ this.renderer = renderer;
1551
+ this.ngZone = ngZone;
1552
+ this.cdRef = cdRef;
1553
+ this.diagramStateService = diagramStateService;
1554
+ this.nodeTypes = nodeTypes;
1555
+ this.nodes$ = toObservable(this.diagramStateService.nodes);
1556
+ }
1557
+ get nodeTypeKeys() {
1558
+ return this.nodeTypes ? Object.keys(this.nodeTypes) : [];
1559
+ }
1560
+ nodes$;
1561
+ resizeObserver;
1562
+ ngOnInit() {
1563
+ this.diagramStateService.el = this.svgRef;
1564
+ this.viewport = this.diagramStateService.viewport;
1565
+ this.nodes = this.diagramStateService.nodes;
1566
+ this.filteredNodes = this.diagramStateService.visibleNodes; // Use visibleNodes for rendering
1567
+ this.edges = this.diagramStateService.edges;
1568
+ this.tempEdges = this.diagramStateService.tempEdges;
1569
+ this.alignmentGuides = this.diagramStateService.alignmentGuides;
1570
+ if (this.initialNodes.length > 0) {
1571
+ this.initialNodes.forEach(node => this.diagramStateService.addNode(node));
1572
+ }
1573
+ if (this.initialEdges.length > 0) {
1574
+ // Add initial edges directly to the signal without triggering connect events
1575
+ this.diagramStateService.edges.set([...this.initialEdges]);
1576
+ }
1577
+ if (this.initialViewport) {
1578
+ this.diagramStateService.setViewport(this.initialViewport);
1579
+ }
1580
+ // Subscribe to state changes and emit events
1581
+ this.subscriptions.add(this.diagramStateService.nodeClick.subscribe((node) => this.nodeClick.emit(node)));
1582
+ this.subscriptions.add(this.diagramStateService.edgeClick.subscribe((edge) => this.edgeClick.emit(edge)));
1583
+ this.subscriptions.add(this.diagramStateService.connect.subscribe((connection) => this.connect.emit(connection)));
1584
+ this.subscriptions.add(this.nodes$.subscribe(nodes => {
1585
+ this.nodes.set(nodes);
1586
+ if (!this.isDraggingNode) {
1587
+ this.updatePathFinder(nodes);
1588
+ this.nodesChange.emit(nodes);
1589
+ }
1590
+ }));
1591
+ this.subscriptions.add(this.diagramStateService.edgesChange.subscribe((edges) => this.edgesChange.emit(edges)));
1592
+ // Initialize ResizeObserver
1593
+ this.resizeObserver = new ResizeObserver(entries => {
1594
+ for (const entry of entries) {
1595
+ const { width, height } = entry.contentRect;
1596
+ this.diagramStateService.setContainerDimensions({ width, height });
1597
+ }
1598
+ });
1599
+ // We need to observe the container, but we only have el (host) or svgRef.
1600
+ // Let's observe the host element.
1601
+ this.resizeObserver.observe(this.el.nativeElement);
1602
+ this.ngZone.runOutsideAngular(() => {
1603
+ this.unlistenPointerMove = this.renderer.listen(this.svgRef.nativeElement, 'pointermove', (event) => {
1604
+ this.onPointerMove(event);
1605
+ });
1606
+ this.unlistenPointerUp = this.renderer.listen(this.svgRef.nativeElement, 'pointerup', (event) => {
1607
+ this.onPointerUp(event);
1608
+ });
1609
+ this.unlistenPointerLeave = this.renderer.listen(this.svgRef.nativeElement, 'pointerleave', (event) => {
1610
+ this.onPointerLeave(event);
1611
+ });
1612
+ });
1613
+ }
1614
+ ngOnDestroy() {
1615
+ this.subscriptions.unsubscribe();
1616
+ if (this.resizeObserver) {
1617
+ this.resizeObserver.disconnect();
1618
+ }
1619
+ if (this.unlistenPointerMove)
1620
+ this.unlistenPointerMove();
1621
+ if (this.unlistenPointerUp)
1622
+ this.unlistenPointerUp();
1623
+ if (this.unlistenPointerLeave)
1624
+ this.unlistenPointerLeave();
1625
+ }
1626
+ get lodLevel() {
1627
+ return this.diagramStateService.lodLevel();
1628
+ }
1629
+ ngOnChanges(changes) {
1630
+ // Handle changes to input properties after initialization
1631
+ if (changes['initialNodes'] && !changes['initialNodes'].firstChange) {
1632
+ if (this.isDraggingNode || this.draggingNode) {
1633
+ return;
1634
+ }
1635
+ const currentNodes = this.nodes();
1636
+ if (this.initialNodes === currentNodes) {
1637
+ return;
1638
+ }
1639
+ const currentNodeIds = new Set(currentNodes.map(n => n.id));
1640
+ const newNodeIds = new Set(this.initialNodes.map(n => n.id));
1641
+ currentNodes.forEach(node => {
1642
+ if (!newNodeIds.has(node.id)) {
1643
+ this.diagramStateService.removeNode(node.id);
1644
+ }
1645
+ });
1646
+ this.initialNodes.forEach(node => {
1647
+ if (!currentNodeIds.has(node.id)) {
1648
+ this.diagramStateService.addNode(node);
1649
+ }
1650
+ else {
1651
+ const currentNode = currentNodes.find(n => n.id === node.id);
1652
+ if (currentNode && JSON.stringify(currentNode) !== JSON.stringify(node)) {
1653
+ this.diagramStateService.updateNode(node.id, node);
1654
+ }
1655
+ }
1656
+ });
1657
+ }
1658
+ if (changes['initialEdges'] && !changes['initialEdges'].firstChange) {
1659
+ const currentEdges = this.edges();
1660
+ if (this.initialEdges === currentEdges)
1661
+ return;
1662
+ if (JSON.stringify(this.initialEdges) !== JSON.stringify(currentEdges)) {
1663
+ this.diagramStateService.edges.set([...this.initialEdges]);
1664
+ }
1665
+ }
1666
+ if (changes['initialViewport'] && !changes['initialViewport'].firstChange && this.initialViewport) {
1667
+ const currentViewport = this.viewport();
1668
+ if (JSON.stringify(this.initialViewport) !== JSON.stringify(currentViewport)) {
1669
+ this.diagramStateService.setViewport(this.initialViewport);
1670
+ }
1671
+ }
1672
+ }
1673
+ get transform() {
1674
+ const v = this.viewport();
1675
+ return `translate(${v.x}, ${v.y}) scale(${v.zoom})`;
1676
+ }
1677
+ trackByNodeId(index, node) {
1678
+ return node.id;
1679
+ }
1680
+ trackByEdgeId(index, edge) {
1681
+ return edge.id;
1682
+ }
1683
+ onDeleteKeyPress(event) {
1684
+ this.diagramStateService.deleteSelectedElements();
1685
+ }
1686
+ onUndoKeyPress(event) {
1687
+ event.preventDefault(); // Prevent browser undo
1688
+ this.diagramStateService.undo();
1689
+ }
1690
+ onRedoKeyPress(event) {
1691
+ event.preventDefault(); // Prevent browser redo
1692
+ this.diagramStateService.redo();
1693
+ }
1694
+ onCopyKeyPress(event) {
1695
+ // Don't prevent default if user is typing in an input
1696
+ if (this.isInputActive(event))
1697
+ return;
1698
+ this.diagramStateService.copy();
1699
+ }
1700
+ onPasteKeyPress(event) {
1701
+ if (this.isInputActive(event))
1702
+ return;
1703
+ this.diagramStateService.paste();
1704
+ }
1705
+ onCutKeyPress(event) {
1706
+ if (this.isInputActive(event))
1707
+ return;
1708
+ this.diagramStateService.cut();
1709
+ }
1710
+ onDuplicateKeyPress(event) {
1711
+ if (this.isInputActive(event))
1712
+ return;
1713
+ event.preventDefault(); // Prevent browser bookmark
1714
+ this.diagramStateService.duplicate();
1715
+ }
1716
+ onGroupKeyPress(event) {
1717
+ if (this.isInputActive(event))
1718
+ return;
1719
+ event.preventDefault(); // Prevent browser find
1720
+ const selectedNodes = this.diagramStateService.selectedNodes();
1721
+ if (selectedNodes.length > 1) {
1722
+ this.diagramStateService.groupNodes(selectedNodes.map(n => n.id));
1723
+ }
1724
+ }
1725
+ onUngroupKeyPress(event) {
1726
+ if (this.isInputActive(event))
1727
+ return;
1728
+ event.preventDefault();
1729
+ const selectedNodes = this.diagramStateService.selectedNodes();
1730
+ if (selectedNodes.length === 1 && selectedNodes[0].type === 'group') {
1731
+ this.diagramStateService.ungroupNodes(selectedNodes[0].id);
1732
+ }
1733
+ }
1734
+ toggleGroup(event, node) {
1735
+ event.stopPropagation();
1736
+ this.diagramStateService.toggleGroup(node.id);
1737
+ }
1738
+ isInputActive(event) {
1739
+ const target = event.target;
1740
+ return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
1741
+ }
1742
+ onWheel(event) {
1743
+ event.preventDefault();
1744
+ this.ngZone.runOutsideAngular(() => {
1745
+ const svgRect = this.svgRef.nativeElement.getBoundingClientRect();
1746
+ const clientX = event.clientX;
1747
+ const clientY = event.clientY;
1748
+ const viewportBefore = this.viewport();
1749
+ const pointX = (clientX - svgRect.left - viewportBefore.x) / viewportBefore.zoom;
1750
+ const pointY = (clientY - svgRect.top - viewportBefore.y) / viewportBefore.zoom;
1751
+ const scaleFactor = 1.05;
1752
+ const newZoom = event.deltaY < 0 ? viewportBefore.zoom * scaleFactor : viewportBefore.zoom / scaleFactor;
1753
+ const clampedZoom = Math.max(0.1, Math.min(10, newZoom));
1754
+ const newX = clientX - svgRect.left - pointX * clampedZoom;
1755
+ const newY = clientY - svgRect.top - pointY * clampedZoom;
1756
+ this.diagramStateService.setViewport({ x: newX, y: newY, zoom: clampedZoom });
1757
+ });
1758
+ }
1759
+ onPointerDown(event) {
1760
+ let targetElement = event.target;
1761
+ // Check for resize handle first (highest priority)
1762
+ const resizeHandleElement = targetElement.closest('.ngx-workflow__resize-handle');
1763
+ if (resizeHandleElement) {
1764
+ const nodeElement = resizeHandleElement.closest('.ngx-workflow__node');
1765
+ if (nodeElement) {
1766
+ const nodeId = nodeElement.dataset['id'];
1767
+ const node = this.nodes().find(n => n.id === nodeId);
1768
+ const handle = resizeHandleElement.dataset['handle'];
1769
+ if (node && handle && (node.resizable !== false) && this.nodesResizable) {
1770
+ this.startResizing(event, node, handle);
1771
+ return;
1772
+ }
1773
+ }
1774
+ }
1775
+ let handleElement = targetElement.closest('.ngx-workflow__handle');
1776
+ const nodeElement = targetElement.closest('.ngx-workflow__node');
1777
+ if (event.button !== 0)
1778
+ return;
1779
+ if (handleElement && handleElement.dataset['type'] === 'source') {
1780
+ // Start Connecting
1781
+ this.startConnecting(event, handleElement);
1782
+ }
1783
+ else if (nodeElement) {
1784
+ // Start Dragging Node
1785
+ const nodeId = nodeElement.dataset['id'];
1786
+ const node = this.nodes().find(n => n.id === nodeId);
1787
+ if (node && node.draggable) {
1788
+ this.startDraggingNode(event, node);
1789
+ }
1790
+ // Select Node
1791
+ if (node) {
1792
+ this.diagramStateService.onNodeClick(node);
1793
+ this.diagramStateService.selectNodes([node.id], event.ctrlKey || event.metaKey || event.shiftKey);
1794
+ }
1795
+ }
1796
+ else {
1797
+ // Pan or Select
1798
+ const isClickingOnCanvas = targetElement === this.svgRef.nativeElement || targetElement.classList.contains('ngx-workflow__background');
1799
+ if (isClickingOnCanvas) {
1800
+ if (event.shiftKey) {
1801
+ this.startSelecting(event);
1802
+ }
1803
+ else {
1804
+ this.startPanning(event);
1805
+ }
1806
+ }
1807
+ }
1808
+ }
1809
+ onPointerMove(event) {
1810
+ // console.log('onPointerMove', { dragging: this.isDraggingNode, connecting: this.isConnecting, resizing: this.isResizing });
1811
+ if (this.isResizing) {
1812
+ this.resize(event);
1813
+ }
1814
+ else if (this.isUpdatingEdge) {
1815
+ this.updateEdge(event);
1816
+ }
1817
+ else if (this.isConnecting) {
1818
+ this.updateConnection(event);
1819
+ }
1820
+ else if (this.isDraggingNode) {
1821
+ this.dragNode(event);
1822
+ }
1823
+ else if (this.isPanning) {
1824
+ this.pan(event);
1825
+ }
1826
+ else if (this.isSelecting) {
1827
+ this.updateSelection(event);
1828
+ }
1829
+ }
1830
+ onPointerUp(event) {
1831
+ if (this.isResizing) {
1832
+ this.stopResizing(event);
1833
+ }
1834
+ else if (this.isUpdatingEdge) {
1835
+ this.stopUpdatingEdge(event);
1836
+ }
1837
+ else if (this.isConnecting) {
1838
+ this.finishConnecting(event);
1839
+ }
1840
+ else if (this.isDraggingNode) {
1841
+ this.stopDraggingNode(event);
1842
+ }
1843
+ else if (this.isPanning) {
1844
+ this.stopPanning(event);
1845
+ }
1846
+ else if (this.isSelecting) {
1847
+ this.stopSelecting(event);
1848
+ }
1849
+ }
1850
+ onPointerLeave(event) {
1851
+ if (this.isResizing || this.isUpdatingEdge || this.isPanning || this.isSelecting || this.isDraggingNode || this.isConnecting) {
1852
+ this.onPointerUp(event);
1853
+ }
1854
+ }
1855
+ // --- Connecting Logic ---
1856
+ startConnecting(event, handleElement) {
1857
+ event.stopPropagation();
1858
+ event.preventDefault();
1859
+ this.isConnecting = true;
1860
+ this.svgRef.nativeElement.setPointerCapture(event.pointerId);
1861
+ const nodeId = handleElement.dataset['nodeid'];
1862
+ const handleId = handleElement.dataset['handleid'];
1863
+ if (!nodeId)
1864
+ return;
1865
+ this.connectingSourceNodeId = nodeId;
1866
+ this.connectingSourceHandleId = handleId;
1867
+ const previewEdgeId = `preview-${v4()}`;
1868
+ this.currentPreviewEdgeId = previewEdgeId;
1869
+ const viewport = this.viewport();
1870
+ const diagramSvgEl = this.svgRef.nativeElement;
1871
+ const handleScreenCoords = handleElement.getBoundingClientRect();
1872
+ const diagramScreenCoords = diagramSvgEl.getBoundingClientRect();
1873
+ const sourceX = (handleScreenCoords.x + handleScreenCoords.width / 2 - diagramScreenCoords.x - viewport.x) / viewport.zoom;
1874
+ const sourceY = (handleScreenCoords.y + handleScreenCoords.height / 2 - diagramScreenCoords.y - viewport.y) / viewport.zoom;
1875
+ const newTempEdge = {
1876
+ id: previewEdgeId,
1877
+ source: nodeId,
1878
+ sourceHandle: handleId,
1879
+ target: 'preview-target',
1880
+ targetHandle: undefined,
1881
+ type: 'straight',
1882
+ animated: true,
1883
+ style: { stroke: 'blue', strokeWidth: '2' },
1884
+ sourceX: sourceX,
1885
+ sourceY: sourceY,
1886
+ targetX: sourceX,
1887
+ targetY: sourceY,
1888
+ };
1889
+ this.diagramStateService.addTempEdge(newTempEdge);
1890
+ }
1891
+ updateConnection(event) {
1892
+ if (!this.currentPreviewEdgeId)
1893
+ return;
1894
+ this.ngZone.runOutsideAngular(() => {
1895
+ const diagramSvgEl = this.svgRef.nativeElement;
1896
+ const diagramScreenCoords = diagramSvgEl.getBoundingClientRect();
1897
+ const viewport = this.viewport();
1898
+ const currentPointerX = (event.clientX - diagramScreenCoords.x - viewport.x) / viewport.zoom;
1899
+ const currentPointerY = (event.clientY - diagramScreenCoords.y - viewport.y) / viewport.zoom;
1900
+ this.diagramStateService.updateTempEdgeTarget(this.currentPreviewEdgeId, { x: currentPointerX, y: currentPointerY });
1901
+ // Use geometric distance check instead of elementFromPoint to avoid pointer capture issues
1902
+ let closestHandle = null;
1903
+ let minDistance = 20; // Detection radius
1904
+ const nodes = this.nodes();
1905
+ for (const node of nodes) {
1906
+ const handles = ['top', 'right', 'bottom', 'left'];
1907
+ for (const handleId of handles) {
1908
+ const handlePos = getHandleAbsolutePosition(node, handleId);
1909
+ const dist = Math.hypot(handlePos.x - currentPointerX, handlePos.y - currentPointerY);
1910
+ if (dist < minDistance) {
1911
+ minDistance = dist;
1912
+ closestHandle = { nodeId: node.id, handleId: handleId };
1913
+ }
1914
+ }
1915
+ }
1916
+ this.clearTargetHandleHighlight();
1917
+ if (closestHandle) {
1918
+ const targetNodeId = closestHandle.nodeId;
1919
+ const targetHandleId = closestHandle.handleId;
1920
+ // Allow connecting to any handle on a different node OR same node (self-loop)
1921
+ // console.log('updateConnection: handle found', targetNodeId, this.connectingSourceNodeId);
1922
+ if (targetNodeId && this.isValidConnection(this.connectingSourceNodeId, targetNodeId)) {
1923
+ this.currentTargetHandle = { nodeId: targetNodeId, handleId: targetHandleId, type: 'target' };
1924
+ // We need to find the handle element to highlight it
1925
+ // This is a bit expensive but necessary for visual feedback
1926
+ const handleEl = this.el.nativeElement.querySelector(`.ngx-workflow__handle[data-nodeid="${targetNodeId}"][data-handleid="${targetHandleId}"]`);
1927
+ if (handleEl) {
1928
+ this.renderer.addClass(handleEl, 'ngx-workflow__handle--valid-target');
1929
+ }
1930
+ }
1931
+ else {
1932
+ this.currentTargetHandle = null;
1933
+ }
1934
+ }
1935
+ else {
1936
+ this.currentTargetHandle = null;
1937
+ }
1938
+ });
1939
+ }
1940
+ finishConnecting(event) {
1941
+ console.log('finishConnecting: start');
1942
+ event.stopPropagation();
1943
+ event.preventDefault();
1944
+ this.isConnecting = false;
1945
+ this.svgRef.nativeElement.releasePointerCapture(event.pointerId);
1946
+ this.clearTargetHandleHighlight();
1947
+ if (this.currentPreviewEdgeId) {
1948
+ console.log('finishConnecting: removing preview edge');
1949
+ this.diagramStateService.removeEdge(this.currentPreviewEdgeId);
1950
+ }
1951
+ if (this.currentTargetHandle && this.connectingSourceNodeId) {
1952
+ const sourceId = this.connectingSourceNodeId;
1953
+ const targetId = this.currentTargetHandle.nodeId;
1954
+ console.log('finishConnecting: attempting connection', { sourceId, targetId });
1955
+ if (this.isValidConnection(sourceId, targetId)) {
1956
+ const newEdge = {
1957
+ id: v4(),
1958
+ source: sourceId,
1959
+ sourceHandle: this.connectingSourceHandleId,
1960
+ target: targetId,
1961
+ targetHandle: this.currentTargetHandle.handleId,
1962
+ // type: 'bezier', // Removed to use default smart routing
1963
+ };
1964
+ console.log('finishConnecting: adding edge', newEdge);
1965
+ this.diagramStateService.addEdge(newEdge);
1966
+ }
1967
+ else {
1968
+ console.log('finishConnecting: invalid connection');
1969
+ // Visual feedback for invalid connection: flash source node
1970
+ const sourceNodeEl = this.el.nativeElement.querySelector(`[data-nodeid="${sourceId}"]`);
1971
+ if (sourceNodeEl) {
1972
+ this.renderer.addClass(sourceNodeEl, 'invalid-connection');
1973
+ setTimeout(() => this.renderer.removeClass(sourceNodeEl, 'invalid-connection'), 1000);
1974
+ }
1975
+ }
1976
+ }
1977
+ this.currentPreviewEdgeId = null;
1978
+ this.currentTargetHandle = null;
1979
+ this.connectingSourceNodeId = null;
1980
+ this.connectingSourceHandleId = undefined;
1981
+ console.log('finishConnecting: end');
1982
+ }
1983
+ clearTargetHandleHighlight() {
1984
+ const activeHighlights = document.querySelectorAll('.ngx-workflow__handle--valid-target');
1985
+ activeHighlights.forEach(el => this.renderer.removeClass(el, 'ngx-workflow__handle--valid-target'));
1986
+ }
1987
+ // --- Dragging Logic ---
1988
+ startDraggingNode(event, node) {
1989
+ event.stopPropagation();
1990
+ this.isDraggingNode = true;
1991
+ this.draggingNode = node;
1992
+ this.startNodePosition = { x: node.position.x, y: node.position.y };
1993
+ this.startPointerPosition = { x: event.clientX, y: event.clientY };
1994
+ this.svgRef.nativeElement.setPointerCapture(event.pointerId);
1995
+ // Check if this node is part of a multi-selection
1996
+ const selectedNodes = this.nodes().filter(n => n.selected);
1997
+ if (selectedNodes.length > 1 && node.selected) {
1998
+ // Multi-node drag: store all selected nodes and their positions
1999
+ this.draggingNodes = selectedNodes;
2000
+ this.startNodePositions.clear();
2001
+ selectedNodes.forEach(n => {
2002
+ this.startNodePositions.set(n.id, { x: n.position.x, y: n.position.y });
2003
+ });
2004
+ }
2005
+ else {
2006
+ // Single node drag
2007
+ this.draggingNodes = [node];
2008
+ this.startNodePositions.clear();
2009
+ this.startNodePositions.set(node.id, { x: node.position.x, y: node.position.y });
2010
+ }
2011
+ this.diagramStateService.onDragStart(node);
2012
+ console.log('startDraggingNode: started for', node.id);
2013
+ }
2014
+ dragNode(event) {
2015
+ if (!this.draggingNode)
2016
+ return;
2017
+ event.stopPropagation();
2018
+ if (this.dragAnimationFrameId) {
2019
+ cancelAnimationFrame(this.dragAnimationFrameId);
2020
+ }
2021
+ this.dragAnimationFrameId = requestAnimationFrame(() => {
2022
+ if (!this.draggingNode)
2023
+ return;
2024
+ const zoom = this.viewport().zoom;
2025
+ const deltaX = (event.clientX - this.startPointerPosition.x) / zoom;
2026
+ const deltaY = (event.clientY - this.startPointerPosition.y) / zoom;
2027
+ if (this.draggingNodes.length > 1) {
2028
+ // Multi-node drag: move all selected nodes by the same delta
2029
+ const moves = this.draggingNodes.map(node => {
2030
+ const startPos = this.startNodePositions.get(node.id);
2031
+ return {
2032
+ id: node.id,
2033
+ position: {
2034
+ x: startPos.x + deltaX,
2035
+ y: startPos.y + deltaY
2036
+ }
2037
+ };
2038
+ });
2039
+ this.diagramStateService.moveNodes(moves);
2040
+ }
2041
+ else {
2042
+ // Single node drag
2043
+ const newPosition = {
2044
+ x: this.startNodePosition.x + deltaX,
2045
+ y: this.startNodePosition.y + deltaY,
2046
+ };
2047
+ this.diagramStateService.moveNode(this.draggingNode.id, newPosition);
2048
+ }
2049
+ this.cdRef.detectChanges();
2050
+ this.dragAnimationFrameId = null;
2051
+ });
2052
+ }
2053
+ stopDraggingNode(event) {
2054
+ if (!this.draggingNode)
2055
+ return;
2056
+ event.stopPropagation();
2057
+ this.isDraggingNode = false;
2058
+ this.updatePathFinder(this.nodes());
2059
+ if (this.dragAnimationFrameId) {
2060
+ cancelAnimationFrame(this.dragAnimationFrameId);
2061
+ this.dragAnimationFrameId = null;
2062
+ }
2063
+ this.svgRef.nativeElement.releasePointerCapture(event.pointerId);
2064
+ // Trigger onDragEnd for all dragged nodes
2065
+ this.draggingNodes.forEach(node => {
2066
+ this.diagramStateService.onDragEnd(node);
2067
+ });
2068
+ console.log('stopDraggingNode: stopped');
2069
+ this.draggingNode = null;
2070
+ this.draggingNodes = [];
2071
+ this.startNodePositions.clear();
2072
+ // Emit the final state after drag is complete
2073
+ this.nodesChange.emit(this.nodes());
2074
+ }
2075
+ // --- Resizing Logic ---
2076
+ startResizing(event, node, handle) {
2077
+ event.stopPropagation();
2078
+ this.isResizing = true;
2079
+ this.resizingNode = node;
2080
+ this.resizeHandle = handle;
2081
+ this.startResizePosition = { x: event.clientX, y: event.clientY };
2082
+ this.startNodeDimensions = {
2083
+ width: node.width || this.defaultNodeWidth,
2084
+ height: node.height || this.defaultNodeHeight,
2085
+ x: node.position.x,
2086
+ y: node.position.y
2087
+ };
2088
+ this.svgRef.nativeElement.setPointerCapture(event.pointerId);
2089
+ this.diagramStateService.onResizeStart(node);
2090
+ }
2091
+ resize(event) {
2092
+ if (!this.resizingNode || !this.resizeHandle)
2093
+ return;
2094
+ event.stopPropagation();
2095
+ const resizingNode = this.resizingNode;
2096
+ const resizeHandle = this.resizeHandle;
2097
+ this.ngZone.runOutsideAngular(() => {
2098
+ const zoom = this.viewport().zoom;
2099
+ const deltaX = (event.clientX - this.startResizePosition.x) / zoom;
2100
+ const deltaY = (event.clientY - this.startResizePosition.y) / zoom;
2101
+ let newWidth = this.startNodeDimensions.width;
2102
+ let newHeight = this.startNodeDimensions.height;
2103
+ let newX = this.startNodeDimensions.x;
2104
+ let newY = this.startNodeDimensions.y;
2105
+ // Calculate new dimensions based on handle
2106
+ switch (resizeHandle) {
2107
+ case 'se': // Southeast - resize from bottom-right
2108
+ newWidth = this.startNodeDimensions.width + deltaX;
2109
+ newHeight = this.startNodeDimensions.height + deltaY;
2110
+ break;
2111
+ case 'sw': // Southwest - resize from bottom-left
2112
+ newWidth = this.startNodeDimensions.width - deltaX;
2113
+ newHeight = this.startNodeDimensions.height + deltaY;
2114
+ newX = this.startNodeDimensions.x + deltaX;
2115
+ break;
2116
+ case 'ne': // Northeast - resize from top-right
2117
+ newWidth = this.startNodeDimensions.width + deltaX;
2118
+ newHeight = this.startNodeDimensions.height - deltaY;
2119
+ newY = this.startNodeDimensions.y + deltaY;
2120
+ break;
2121
+ case 'nw': // Northwest - resize from top-left
2122
+ newWidth = this.startNodeDimensions.width - deltaX;
2123
+ newHeight = this.startNodeDimensions.height - deltaY;
2124
+ newX = this.startNodeDimensions.x + deltaX;
2125
+ newY = this.startNodeDimensions.y + deltaY;
2126
+ break;
2127
+ }
2128
+ // Apply constraints
2129
+ const minWidth = resizingNode.minWidth || 50;
2130
+ const minHeight = resizingNode.minHeight || 30;
2131
+ const maxWidth = resizingNode.maxWidth || 500;
2132
+ const maxHeight = resizingNode.maxHeight || 500;
2133
+ newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth));
2134
+ newHeight = Math.max(minHeight, Math.min(maxHeight, newHeight));
2135
+ // Adjust position if constrained (for nw, ne, sw handles)
2136
+ if (resizeHandle === 'nw' || resizeHandle === 'sw') {
2137
+ const widthDiff = newWidth - (this.startNodeDimensions.width - deltaX);
2138
+ newX = this.startNodeDimensions.x + deltaX - widthDiff;
2139
+ }
2140
+ if (resizeHandle === 'nw' || resizeHandle === 'ne') {
2141
+ const heightDiff = newHeight - (this.startNodeDimensions.height - deltaY);
2142
+ newY = this.startNodeDimensions.y + deltaY - heightDiff;
2143
+ }
2144
+ // Update node
2145
+ this.diagramStateService.resizeNode(resizingNode.id, newWidth, newHeight, { x: newX, y: newY });
2146
+ });
2147
+ }
2148
+ stopResizing(event) {
2149
+ if (!this.resizingNode)
2150
+ return;
2151
+ event.stopPropagation();
2152
+ this.isResizing = false;
2153
+ this.svgRef.nativeElement.releasePointerCapture(event.pointerId);
2154
+ this.diagramStateService.onResizeEnd(this.resizingNode);
2155
+ this.resizingNode = null;
2156
+ this.resizeHandle = null;
2157
+ }
2158
+ // --- Edge Updating Logic ---
2159
+ startUpdatingEdge(event, edge, handleType) {
2160
+ event.stopPropagation();
2161
+ this.isUpdatingEdge = true;
2162
+ this.updatingEdge = edge;
2163
+ this.updatingEdgeHandle = handleType;
2164
+ this.svgRef.nativeElement.setPointerCapture(event.pointerId);
2165
+ const tempEdgeId = `temp-update-${edge.id}`;
2166
+ this.currentPreviewEdgeId = tempEdgeId;
2167
+ const sourceNode = this.nodes().find(n => n.id === edge.source);
2168
+ const targetNode = this.nodes().find(n => n.id === edge.target);
2169
+ if (!sourceNode || !targetNode)
2170
+ return;
2171
+ const sourcePos = getHandleAbsolutePosition(sourceNode, edge.sourceHandle);
2172
+ const targetPos = getHandleAbsolutePosition(targetNode, edge.targetHandle);
2173
+ let startX, startY, endX, endY;
2174
+ if (handleType === 'source') {
2175
+ startX = targetPos.x;
2176
+ startY = targetPos.y;
2177
+ endX = sourcePos.x;
2178
+ endY = sourcePos.y;
2179
+ }
2180
+ else {
2181
+ startX = sourcePos.x;
2182
+ startY = sourcePos.y;
2183
+ endX = targetPos.x;
2184
+ endY = targetPos.y;
2185
+ }
2186
+ this.diagramStateService.addTempEdge({
2187
+ id: tempEdgeId,
2188
+ source: edge.source,
2189
+ sourceHandle: edge.sourceHandle,
2190
+ target: edge.target,
2191
+ targetHandle: edge.targetHandle,
2192
+ type: edge.type || 'bezier',
2193
+ animated: edge.animated,
2194
+ sourceX: startX,
2195
+ sourceY: startY,
2196
+ targetX: endX,
2197
+ targetY: endY,
2198
+ style: edge.style,
2199
+ markerEnd: edge.markerEnd
2200
+ });
2201
+ }
2202
+ updateEdge(event) {
2203
+ if (!this.updatingEdge || !this.currentPreviewEdgeId)
2204
+ return;
2205
+ event.stopPropagation();
2206
+ this.ngZone.runOutsideAngular(() => {
2207
+ const diagramRect = this.svgRef.nativeElement.getBoundingClientRect();
2208
+ const viewport = this.viewport();
2209
+ const point = {
2210
+ x: (event.clientX - diagramRect.left - viewport.x) / viewport.zoom,
2211
+ y: (event.clientY - diagramRect.top - viewport.y) / viewport.zoom
2212
+ };
2213
+ this.diagramStateService.updateTempEdgeTarget(this.currentPreviewEdgeId, point);
2214
+ const element = document.elementFromPoint(event.clientX, event.clientY);
2215
+ const handle = element?.closest('.ngx-workflow__handle');
2216
+ if (handle) {
2217
+ const nodeId = handle.getAttribute('data-nodeid');
2218
+ const handleId = handle.getAttribute('data-handleid');
2219
+ const type = handle.getAttribute('data-type');
2220
+ if (nodeId && type) {
2221
+ this.currentTargetHandle = { nodeId, handleId: handleId || undefined, type: type };
2222
+ }
2223
+ }
2224
+ else {
2225
+ this.currentTargetHandle = null;
2226
+ }
2227
+ });
2228
+ }
2229
+ stopUpdatingEdge(event) {
2230
+ if (!this.updatingEdge)
2231
+ return;
2232
+ event.stopPropagation();
2233
+ this.isUpdatingEdge = false;
2234
+ this.svgRef.nativeElement.releasePointerCapture(event.pointerId);
2235
+ if (this.currentPreviewEdgeId) {
2236
+ this.diagramStateService.removeEdge(this.currentPreviewEdgeId);
2237
+ this.currentPreviewEdgeId = null;
2238
+ }
2239
+ }
2240
+ // --- Panning Logic ---
2241
+ startPanning(event) {
2242
+ this.isPanning = true;
2243
+ this.lastPanPosition = { x: event.clientX, y: event.clientY };
2244
+ this.renderer.setStyle(this.svgRef.nativeElement, 'cursor', 'grabbing');
2245
+ this.svgRef.nativeElement.setPointerCapture(event.pointerId);
2246
+ this.diagramStateService.clearSelection();
2247
+ }
2248
+ pan(event) {
2249
+ this.ngZone.runOutsideAngular(() => {
2250
+ const deltaX = event.clientX - this.lastPanPosition.x;
2251
+ const deltaY = event.clientY - this.lastPanPosition.y;
2252
+ const currentViewport = this.viewport();
2253
+ this.diagramStateService.setViewport({
2254
+ x: currentViewport.x + deltaX,
2255
+ y: currentViewport.y + deltaY,
2256
+ zoom: currentViewport.zoom,
2257
+ });
2258
+ this.lastPanPosition = { x: event.clientX, y: event.clientY };
2259
+ });
2260
+ }
2261
+ stopPanning(event) {
2262
+ this.isPanning = false;
2263
+ this.renderer.setStyle(this.svgRef.nativeElement, 'cursor', 'grab');
2264
+ this.svgRef.nativeElement.releasePointerCapture(event.pointerId);
2265
+ }
2266
+ // --- Selection Logic ---
2267
+ startSelecting(event) {
2268
+ this.isSelecting = true;
2269
+ this.selectionStart = this.getDiagramCoordinates(event.clientX, event.clientY);
2270
+ this.selectionEnd = { ...this.selectionStart };
2271
+ this.svgRef.nativeElement.setPointerCapture(event.pointerId);
2272
+ }
2273
+ updateSelection(event) {
2274
+ this.ngZone.runOutsideAngular(() => {
2275
+ this.selectionEnd = this.getDiagramCoordinates(event.clientX, event.clientY);
2276
+ });
2277
+ }
2278
+ stopSelecting(event) {
2279
+ this.isSelecting = false;
2280
+ this.svgRef.nativeElement.releasePointerCapture(event.pointerId);
2281
+ this.performLassoSelection();
2282
+ }
2283
+ getDiagramCoordinates(clientX, clientY) {
2284
+ const svgRect = this.svgRef.nativeElement.getBoundingClientRect();
2285
+ const viewport = this.viewport();
2286
+ const x = (clientX - svgRect.left - viewport.x) / viewport.zoom;
2287
+ const y = (clientY - svgRect.top - viewport.y) / viewport.zoom;
2288
+ return { x, y };
2289
+ }
2290
+ performLassoSelection() {
2291
+ const minX = Math.min(this.selectionStart.x, this.selectionEnd.x);
2292
+ const maxX = Math.max(this.selectionStart.x, this.selectionEnd.x);
2293
+ const minY = Math.min(this.selectionStart.y, this.selectionEnd.y);
2294
+ const maxY = Math.max(this.selectionStart.y, this.selectionEnd.y);
2295
+ const selectedNodeIds = [];
2296
+ this.nodes().forEach((node) => {
2297
+ const nodeX = node.position.x;
2298
+ const nodeY = node.position.y;
2299
+ const nodeWidth = node.width || this.defaultNodeWidth;
2300
+ const nodeHeight = node.height || this.defaultNodeHeight;
2301
+ if (nodeX < maxX &&
2302
+ nodeX + nodeWidth > minX &&
2303
+ nodeY < maxY &&
2304
+ nodeY + nodeHeight > minY) {
2305
+ selectedNodeIds.push(node.id);
2306
+ }
2307
+ });
2308
+ this.diagramStateService.clearSelection();
2309
+ this.diagramStateService.selectNodes(selectedNodeIds, false);
2310
+ }
2311
+ // --- Edge Logic ---
2312
+ getEdgePath(edge, isTemporary = false) {
2313
+ const nodes = this.nodes();
2314
+ let sourcePos;
2315
+ let targetPos;
2316
+ if (isTemporary && 'sourceX' in edge && 'sourceY' in edge && 'targetX' in edge && 'targetY' in edge) {
2317
+ sourcePos = { x: edge.sourceX, y: edge.sourceY };
2318
+ targetPos = { x: edge.targetX, y: edge.targetY };
2319
+ }
2320
+ else {
2321
+ const sourceNode = getNode(edge.source, nodes);
2322
+ const targetNode = getNode(edge.target, nodes);
2323
+ if (!sourceNode || !targetNode) {
2324
+ return 'M 0 0';
2325
+ }
2326
+ sourcePos = getHandleAbsolutePosition(sourceNode, edge.sourceHandle);
2327
+ targetPos = getHandleAbsolutePosition(targetNode, edge.targetHandle);
2328
+ if (sourceNode.id === targetNode.id) {
2329
+ return getSelfLoopPath(sourcePos, edge.sourceHandle);
2330
+ }
2331
+ }
2332
+ // Use smart routing if type is 'smart' or not specified (default)
2333
+ // But respect explicit 'straight' type if user wants simple straight line
2334
+ if ((edge.type === 'smart' || !edge.type) && !isTemporary) {
2335
+ const cacheKey = `${edge.id}-${sourcePos.x},${sourcePos.y}-${targetPos.x},${targetPos.y}`;
2336
+ if (this.pathCache.has(cacheKey)) {
2337
+ return this.pathCache.get(cacheKey);
2338
+ }
2339
+ try {
2340
+ if (!this._pathFinder) {
2341
+ this.updatePathFinder(this.nodes());
2342
+ }
2343
+ const path = this._pathFinder.findPath(sourcePos, targetPos);
2344
+ const d = getSmartEdgePath(path);
2345
+ this.pathCache.set(cacheKey, d);
2346
+ return d;
2347
+ }
2348
+ catch (e) {
2349
+ console.warn('Pathfinding failed, falling back to straight path', e);
2350
+ return getStraightPath(sourcePos, targetPos);
2351
+ }
2352
+ }
2353
+ switch (edge.type) {
2354
+ case 'bezier': return getBezierPath(sourcePos, targetPos);
2355
+ case 'step': return getStepPath(sourcePos, targetPos);
2356
+ case 'straight': return getStraightPath(sourcePos, targetPos);
2357
+ default: return getStraightPath(sourcePos, targetPos);
2358
+ }
2359
+ }
2360
+ getMarkerUrl(marker) {
2361
+ if (!marker)
2362
+ return null;
2363
+ // Support built-in markers or custom marker IDs
2364
+ if (marker === 'arrow' || marker === 'arrowclosed' || marker === 'dot') {
2365
+ return `url(#ngx-workflow__${marker})`;
2366
+ }
2367
+ return `url(#${marker})`;
2368
+ }
2369
+ getEdgeLabelPosition(edge) {
2370
+ const nodes = this.nodes();
2371
+ const sourceNode = getNode(edge.source, nodes);
2372
+ const targetNode = getNode(edge.target, nodes);
2373
+ if (!sourceNode || !targetNode) {
2374
+ return { x: 0, y: 0 };
2375
+ }
2376
+ const sourcePos = getHandleAbsolutePosition(sourceNode, edge.sourceHandle);
2377
+ const targetPos = getHandleAbsolutePosition(targetNode, edge.targetHandle);
2378
+ if (edge.type === 'smart' || !edge.type) {
2379
+ try {
2380
+ // For label position, we can re-use the cached path if available,
2381
+ // but we need the points, not the string.
2382
+ // For now, let's just re-calculate or maybe cache points too?
2383
+ // Re-calculating for label might be okay if getEdgePath is cached,
2384
+ // but ideally we cache the points.
2385
+ // Let's optimize this later if needed, or cache points instead of string.
2386
+ // Optimization: Cache points instead of string
2387
+ const cacheKey = `${edge.id}-${sourcePos.x},${sourcePos.y}-${targetPos.x},${targetPos.y}-points`;
2388
+ let path;
2389
+ if (this.pathPointsCache.has(cacheKey)) {
2390
+ path = this.pathPointsCache.get(cacheKey);
2391
+ }
2392
+ else {
2393
+ if (!this._pathFinder) {
2394
+ this.updatePathFinder(this.nodes());
2395
+ }
2396
+ path = this._pathFinder.findPath(sourcePos, targetPos);
2397
+ this.pathPointsCache.set(cacheKey, path);
2398
+ }
2399
+ return getPolylineMidpoint(path);
2400
+ }
2401
+ catch (e) {
2402
+ console.warn('Pathfinding failed for label position', e);
2403
+ }
2404
+ }
2405
+ // Return midpoint of the edge
2406
+ return {
2407
+ x: (sourcePos.x + targetPos.x) / 2,
2408
+ y: (sourcePos.y + targetPos.y) / 2
2409
+ };
2410
+ }
2411
+ onEdgeClick(event, edge) {
2412
+ event.stopPropagation();
2413
+ event.preventDefault();
2414
+ this.diagramStateService.onEdgeClick(edge);
2415
+ const isMultiSelect = event.ctrlKey || event.metaKey || event.shiftKey;
2416
+ // Clear node selection when selecting edges
2417
+ this.diagramStateService.nodes.update(nodes => nodes.map(n => ({ ...n, selected: false })));
2418
+ // Toggle edge selection
2419
+ this.diagramStateService.edges.update(edges => edges.map(e => ({
2420
+ ...e,
2421
+ selected: e.id === edge.id
2422
+ ? !e.selected
2423
+ : (isMultiSelect ? e.selected : false)
2424
+ })));
2425
+ }
2426
+ // --- Node Logic ---
2427
+ getCustomNodeComponent(type) {
2428
+ if (type && this.nodeTypes && this.nodeTypes[type]) {
2429
+ return this.nodeTypes[type];
2430
+ }
2431
+ return null;
2432
+ }
2433
+ zoomIn() {
2434
+ const currentViewport = this.viewport();
2435
+ const newZoom = Math.min(currentViewport.zoom * 1.2, 10);
2436
+ this.diagramStateService.setViewport({
2437
+ ...currentViewport,
2438
+ zoom: newZoom
2439
+ });
2440
+ }
2441
+ zoomOut() {
2442
+ const currentViewport = this.viewport();
2443
+ const newZoom = Math.max(currentViewport.zoom / 1.2, 0.1);
2444
+ this.diagramStateService.setViewport({
2445
+ ...currentViewport,
2446
+ zoom: newZoom
2447
+ });
2448
+ }
2449
+ resetZoom() {
2450
+ const currentViewport = this.viewport();
2451
+ this.diagramStateService.setViewport({
2452
+ ...currentViewport,
2453
+ zoom: 1
2454
+ });
2455
+ }
2456
+ fitView() {
2457
+ const nodes = this.nodes();
2458
+ if (nodes.length === 0)
2459
+ return;
2460
+ // Calculate bounds of all nodes
2461
+ let minX = Infinity;
2462
+ let minY = Infinity;
2463
+ let maxX = -Infinity;
2464
+ let maxY = -Infinity;
2465
+ nodes.forEach(node => {
2466
+ const width = node.width || this.defaultNodeWidth;
2467
+ const height = node.height || this.defaultNodeHeight;
2468
+ minX = Math.min(minX, node.position.x);
2469
+ minY = Math.min(minY, node.position.y);
2470
+ maxX = Math.max(maxX, node.position.x + width);
2471
+ maxY = Math.max(maxY, node.position.y + height);
2472
+ });
2473
+ const boundsWidth = maxX - minX;
2474
+ const boundsHeight = maxY - minY;
2475
+ // Get SVG dimensions
2476
+ const svgRect = this.svgRef.nativeElement.getBoundingClientRect();
2477
+ const padding = 50; // Padding around nodes
2478
+ // Calculate zoom to fit
2479
+ const zoomX = (svgRect.width - padding * 2) / boundsWidth;
2480
+ const zoomY = (svgRect.height - padding * 2) / boundsHeight;
2481
+ const zoom = Math.min(zoomX, zoomY, 2); // Max zoom of 2x for fit view
2482
+ // Calculate center position
2483
+ const x = (svgRect.width - boundsWidth * zoom) / 2 - minX * zoom;
2484
+ const y = (svgRect.height - boundsHeight * zoom) / 2 - minY * zoom;
2485
+ this.diagramStateService.setViewport({ x, y, zoom });
2486
+ }
2487
+ /**
2488
+ * Returns the current state of the diagram (nodes, edges, viewport).
2489
+ */
2490
+ getDiagramState() {
2491
+ return this.diagramStateService.getDiagramState();
2492
+ }
2493
+ /**
2494
+ * Sets the state of the diagram.
2495
+ */
2496
+ setDiagramState(state) {
2497
+ this.diagramStateService.setDiagramState(state);
2498
+ }
2499
+ /**
2500
+ * Exports the diagram as an SVG file.
2501
+ * @param fileName The name of the file to download (default: 'diagram.svg')
2502
+ * @param download Whether to trigger a download (default: true)
2503
+ * @returns The SVG string
2504
+ */
2505
+ exportToSVG(fileName = 'diagram.svg', download = true) {
2506
+ const svgElement = this.svgRef.nativeElement;
2507
+ // Clone the SVG to avoid modifying the live diagram
2508
+ const clone = svgElement.cloneNode(true);
2509
+ // Get the bounding box of the content (nodes and edges)
2510
+ // We need to calculate this manually because getBBox() on the clone won't work if it's not in the DOM
2511
+ const nodes = this.nodes();
2512
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2513
+ if (nodes.length > 0) {
2514
+ nodes.forEach(node => {
2515
+ minX = Math.min(minX, node.position.x);
2516
+ minY = Math.min(minY, node.position.y);
2517
+ maxX = Math.max(maxX, node.position.x + (node.width || this.defaultNodeWidth));
2518
+ maxY = Math.max(maxY, node.position.y + (node.height || this.defaultNodeHeight));
2519
+ });
2520
+ }
2521
+ else {
2522
+ minX = 0;
2523
+ minY = 0;
2524
+ maxX = 100;
2525
+ maxY = 100;
2526
+ }
2527
+ // Add some padding
2528
+ const padding = 20;
2529
+ minX -= padding;
2530
+ minY -= padding;
2531
+ maxX += padding;
2532
+ maxY += padding;
2533
+ const width = maxX - minX;
2534
+ const height = maxY - minY;
2535
+ // Set the viewBox to the content bounds
2536
+ clone.setAttribute('viewBox', `${minX} ${minY} ${width} ${height}`);
2537
+ clone.setAttribute('width', `${width}`);
2538
+ clone.setAttribute('height', `${height}`);
2539
+ // Remove the transform from the viewport group in the clone to reset zoom/pan
2540
+ // The viewport group is the first child g element
2541
+ const viewportGroup = clone.querySelector('.ngx-workflow__viewport');
2542
+ if (viewportGroup) {
2543
+ viewportGroup.removeAttribute('transform');
2544
+ }
2545
+ // Serialize the SVG
2546
+ const serializer = new XMLSerializer();
2547
+ let svgString = serializer.serializeToString(clone);
2548
+ // Add XML declaration
2549
+ if (!svgString.match(/^<xml/)) {
2550
+ svgString = '<?xml version="1.0" encoding="utf-8"?>\n' + svgString;
2551
+ }
2552
+ if (download) {
2553
+ const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
2554
+ const url = URL.createObjectURL(blob);
2555
+ this.downloadFile(url, fileName);
2556
+ URL.revokeObjectURL(url);
2557
+ }
2558
+ return svgString;
2559
+ }
2560
+ /**
2561
+ * Exports the diagram as a PNG image.
2562
+ * @param fileName The name of the file to download (default: 'diagram.png')
2563
+ * @param download Whether to trigger a download (default: true)
2564
+ * @returns A promise that resolves to the data URL of the PNG
2565
+ */
2566
+ async exportToPNG(fileName = 'diagram.png', download = true) {
2567
+ const svgString = this.exportToSVG(fileName, false);
2568
+ return new Promise((resolve, reject) => {
2569
+ const img = new Image();
2570
+ const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
2571
+ const url = URL.createObjectURL(svgBlob);
2572
+ img.onload = () => {
2573
+ const canvas = document.createElement('canvas');
2574
+ canvas.width = img.width;
2575
+ canvas.height = img.height;
2576
+ const ctx = canvas.getContext('2d');
2577
+ if (!ctx) {
2578
+ reject(new Error('Could not get canvas context'));
2579
+ return;
2580
+ }
2581
+ // Draw white background
2582
+ ctx.fillStyle = '#ffffff';
2583
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
2584
+ ctx.drawImage(img, 0, 0);
2585
+ const pngUrl = canvas.toDataURL('image/png');
2586
+ if (download) {
2587
+ this.downloadFile(pngUrl, fileName);
2588
+ }
2589
+ URL.revokeObjectURL(url);
2590
+ resolve(pngUrl);
2591
+ };
2592
+ img.onerror = (e) => {
2593
+ URL.revokeObjectURL(url);
2594
+ reject(e);
2595
+ };
2596
+ img.src = url;
2597
+ });
2598
+ }
2599
+ /**
2600
+ * Exports the diagram state as a JSON file.
2601
+ * @param fileName The name of the file to download (default: 'diagram.json')
2602
+ */
2603
+ exportToJSON(fileName = 'diagram.json') {
2604
+ const state = this.getDiagramState();
2605
+ const jsonString = JSON.stringify(state, null, 2);
2606
+ const blob = new Blob([jsonString], { type: 'application/json' });
2607
+ const url = URL.createObjectURL(blob);
2608
+ this.downloadFile(url, fileName);
2609
+ URL.revokeObjectURL(url);
2610
+ }
2611
+ /**
2612
+ * Triggers the file input to select a JSON file for import.
2613
+ */
2614
+ triggerImport() {
2615
+ const fileInput = document.createElement('input');
2616
+ fileInput.type = 'file';
2617
+ fileInput.accept = '.json';
2618
+ fileInput.style.display = 'none';
2619
+ fileInput.onchange = (e) => this.onFileSelected(e);
2620
+ document.body.appendChild(fileInput);
2621
+ fileInput.click();
2622
+ document.body.removeChild(fileInput);
2623
+ }
2624
+ /**
2625
+ * Handles the file selection for import.
2626
+ */
2627
+ onFileSelected(event) {
2628
+ const input = event.target;
2629
+ if (!input.files || input.files.length === 0)
2630
+ return;
2631
+ const file = input.files[0];
2632
+ const reader = new FileReader();
2633
+ reader.onload = (e) => {
2634
+ try {
2635
+ const jsonString = e.target?.result;
2636
+ const state = JSON.parse(jsonString);
2637
+ // Basic validation
2638
+ if (state.nodes && state.edges && state.viewport) {
2639
+ this.setDiagramState(state);
2640
+ }
2641
+ else {
2642
+ console.error('Invalid diagram JSON format');
2643
+ // TODO: Show user notification
2644
+ }
2645
+ }
2646
+ catch (error) {
2647
+ console.error('Error parsing JSON', error);
2648
+ // TODO: Show user notification
2649
+ }
2650
+ };
2651
+ reader.readAsText(file);
2652
+ }
2653
+ downloadFile(url, fileName) {
2654
+ const link = document.createElement('a');
2655
+ link.href = url;
2656
+ link.download = fileName;
2657
+ document.body.appendChild(link);
2658
+ link.click();
2659
+ document.body.removeChild(link);
2660
+ document.body.removeChild(link);
2661
+ }
2662
+ onSearch(event) {
2663
+ const input = event.target;
2664
+ this.diagramStateService.setSearchQuery(input.value);
2665
+ }
2666
+ onFilterType(event) {
2667
+ const select = event.target;
2668
+ this.diagramStateService.setFilterType(select.value || null);
2669
+ }
2670
+ onZoomChange(zoom) {
2671
+ this.diagramStateService.setZoom(zoom);
2672
+ }
2673
+ onMinimapViewportChange(viewport) {
2674
+ this.diagramStateService.setViewport(viewport);
2675
+ }
2676
+ getEdgeHandlePosition(edge, type) {
2677
+ const nodes = this.nodes();
2678
+ const nodeId = type === 'source' ? edge.source : edge.target;
2679
+ const handleId = type === 'source' ? edge.sourceHandle : edge.targetHandle;
2680
+ const node = nodes.find(n => n.id === nodeId);
2681
+ if (!node)
2682
+ return { x: 0, y: 0 };
2683
+ const pos = getHandleAbsolutePosition(node, handleId);
2684
+ return {
2685
+ x: isFinite(pos.x) ? pos.x : 0,
2686
+ y: isFinite(pos.y) ? pos.y : 0
2687
+ };
2688
+ }
2689
+ onKeyDown(event) {
2690
+ const target = event.target;
2691
+ // Ignore if focus is on an input or textarea
2692
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
2693
+ return;
2694
+ }
2695
+ // Delete or Backspace to remove selected elements
2696
+ if (event.key === 'Delete' || event.key === 'Backspace') {
2697
+ this.diagramStateService.deleteSelectedElements();
2698
+ }
2699
+ // Ctrl+A or Cmd+A to select all
2700
+ if ((event.ctrlKey || event.metaKey) && event.key === 'a') {
2701
+ event.preventDefault(); // Prevent default browser select all
2702
+ this.diagramStateService.selectAll();
2703
+ }
2704
+ // Undo (Ctrl+Z) and Redo (Ctrl+Y or Ctrl+Shift+Z)
2705
+ if (event.ctrlKey || event.metaKey) {
2706
+ if (event.key === 'z') {
2707
+ event.preventDefault();
2708
+ this.diagramStateService.undo();
2709
+ }
2710
+ else if (event.key === 'y' || (event.shiftKey && event.key === 'Z')) {
2711
+ event.preventDefault();
2712
+ this.diagramStateService.redo();
2713
+ }
2714
+ // Clipboard Operations
2715
+ if (event.key === 'c') {
2716
+ // Handled by onCopyKeyPress
2717
+ }
2718
+ else if (event.key === 'v') {
2719
+ // Handled by onPasteKeyPress
2720
+ }
2721
+ else if (event.key === 'x') {
2722
+ // Handled by onCutKeyPress
2723
+ }
2724
+ }
2725
+ }
2726
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DiagramComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: DiagramStateService }, { token: NGX_WORKFLOW_NODE_TYPES, optional: true }], target: i0.ɵɵFactoryTarget.Component });
2727
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: DiagramComponent, isStandalone: true, selector: "ngx-workflow-diagram", inputs: { initialNodes: "initialNodes", initialEdges: "initialEdges", initialViewport: "initialViewport", showZoomControls: "showZoomControls", showMinimap: "showMinimap", showBackground: "showBackground", backgroundVariant: "backgroundVariant", backgroundGap: "backgroundGap", backgroundSize: "backgroundSize", backgroundColor: "backgroundColor", backgroundBgColor: "backgroundBgColor", connectionValidator: "connectionValidator", nodesResizable: "nodesResizable" }, outputs: { nodeClick: "nodeClick", edgeClick: "edgeClick", connect: "connect", nodesChange: "nodesChange", edgesChange: "edgesChange", nodeDoubleClick: "nodeDoubleClick" }, host: { listeners: { "window:keydown.delete": "onDeleteKeyPress($event)", "window:keydown.control.z": "onUndoKeyPress($event)", "window:keydown.meta.z": "onUndoKeyPress($event)", "window:keydown.control.shift.z": "onRedoKeyPress($event)", "window:keydown.meta.shift.z": "onRedoKeyPress($event)", "window:keydown.control.c": "onCopyKeyPress($event)", "window:keydown.meta.c": "onCopyKeyPress($event)", "window:keydown.control.v": "onPasteKeyPress($event)", "window:keydown.meta.v": "onPasteKeyPress($event)", "window:keydown.control.x": "onCutKeyPress($event)", "window:keydown.meta.x": "onCutKeyPress($event)", "window:keydown.control.d": "onDuplicateKeyPress($event)", "window:keydown.meta.d": "onDuplicateKeyPress($event)", "window:keydown.control.g": "onGroupKeyPress($event)", "window:keydown.meta.g": "onGroupKeyPress($event)", "window:keydown.control.shift.g": "onUngroupKeyPress($event)", "window:keydown.meta.shift.g": "onUngroupKeyPress($event)", "window:keydown": "onKeyDown($event)" } }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svg"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ngx-workflow__container\" [attr.data-lod]=\"lodLevel\">\r\n <ngx-workflow-background *ngIf=\"showBackground\" [variant]=\"backgroundVariant\" [gap]=\"backgroundGap\"\r\n [size]=\"backgroundSize\" [color]=\"backgroundColor\" [backgroundColor]=\"backgroundBgColor\"></ngx-workflow-background>\r\n <svg #svg class=\"ngx-workflow__diagram\" (wheel)=\"onWheel($event)\" (pointerdown)=\"onPointerDown($event)\"\r\n (dblclick)=\"onDiagramDoubleClick($event)\">\r\n <defs>\r\n <pattern id=\"ngx-workflow__grid-pattern\" x=\"0\" y=\"0\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\r\n <circle cx=\"0.5\" cy=\"0.5\" r=\"0.5\" fill=\"#ccc\" />\r\n </pattern>\r\n\r\n <!-- Arrow marker -->\r\n <marker id=\"ngx-workflow__arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\"\r\n orient=\"auto-start-reverse\">\r\n <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"context-stroke\" />\r\n </marker>\r\n\r\n <!-- Arrowclosed marker (filled) -->\r\n <marker id=\"ngx-workflow__arrowclosed\" viewBox=\"0 0 10 10\" refX=\"10\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\"\r\n orient=\"auto-start-reverse\">\r\n <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"context-stroke\" stroke=\"context-stroke\" />\r\n </marker>\r\n\r\n <!-- Dot marker -->\r\n <marker id=\"ngx-workflow__dot\" viewBox=\"0 0 10 10\" refX=\"5\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\">\r\n <circle cx=\"5\" cy=\"5\" r=\"4\" fill=\"context-stroke\" />\r\n </marker>\r\n </defs>\r\n\r\n <rect width=\"100%\" height=\"100%\" fill=\"url(#ngx-workflow__grid-pattern)\" class=\"ngx-workflow__background\" />\r\n\r\n <g class=\"ngx-workflow__viewport\" [attr.transform]=\"transform\">\r\n <!-- Edges -->\r\n <g *ngFor=\"let edge of edges(); trackBy: trackByEdgeId\" [class.selected]=\"edge.selected\"\r\n [class.animated]=\"edge.animated\" class=\"ngx-workflow__edge\" (click)=\"onEdgeClick($event, edge)\"\r\n (dblclick)=\"onEdgeDoubleClick($event, edge)\">\r\n <!-- Edge path with markers -->\r\n <path [attr.d]=\"getEdgePath(edge)\" class=\"ngx-workflow__edge-path\" [style]=\"edge.style\"\r\n [attr.marker-start]=\"getMarkerUrl(edge.markerStart)\" [attr.marker-end]=\"getMarkerUrl(edge.markerEnd)\">\r\n </path>\r\n <!-- Invisible hitbox for easier clicking -->\r\n <path [attr.d]=\"getEdgePath(edge)\" class=\"ngx-workflow__edge-hitbox\"></path>\r\n\r\n <!-- Edge label -->\r\n <g *ngIf=\"edge.label || editingEdgeId === edge.id\" class=\"ngx-workflow__edge-label\">\r\n <foreignObject *ngIf=\"editingEdgeId === edge.id\" [attr.x]=\"getEdgeLabelPosition(edge).x - 50\"\r\n [attr.y]=\"getEdgeLabelPosition(edge).y - 15\" width=\"100\" height=\"30\">\r\n <input #edgeLabelInput type=\"text\" [value]=\"edge.label || ''\" (blur)=\"onEdgeLabelBlur()\"\r\n (keydown.enter)=\"updateEdgeLabel(edge, edgeLabelInput.value)\" class=\"ngx-workflow__edge-label-input\"\r\n autofocus>\r\n </foreignObject>\r\n\r\n <text *ngIf=\"editingEdgeId !== edge.id\" [attr.x]=\"getEdgeLabelPosition(edge).x\"\r\n [attr.y]=\"getEdgeLabelPosition(edge).y\" text-anchor=\"middle\" dominant-baseline=\"middle\"\r\n class=\"ngx-workflow__edge-label-text\" [style]=\"edge.labelStyle\">\r\n <!-- Label background -->\r\n <tspan *ngIf=\"edge.labelBgStyle\" class=\"ngx-workflow__edge-label-bg\" [style]=\"edge.labelBgStyle\"></tspan>\r\n {{ edge.label }}\r\n </text>\r\n </g>\r\n\r\n <!-- Edge Update Handles -->\r\n <g *ngIf=\"edge.selected\">\r\n <!-- Source Handle -->\r\n <circle class=\"ngx-workflow__edge-handle\" [attr.cx]=\"getEdgeHandlePosition(edge, 'source').x\"\r\n [attr.cy]=\"getEdgeHandlePosition(edge, 'source').y\" r=\"4\"\r\n (pointerdown)=\"startUpdatingEdge($event, edge, 'source')\"></circle>\r\n <!-- Target Handle -->\r\n <circle class=\"ngx-workflow__edge-handle\" [attr.cx]=\"getEdgeHandlePosition(edge, 'target').x\"\r\n [attr.cy]=\"getEdgeHandlePosition(edge, 'target').y\" r=\"4\"\r\n (pointerdown)=\"startUpdatingEdge($event, edge, 'target')\"></circle>\r\n </g>\r\n </g>\r\n\r\n <!-- Render temporary edges (previews) -->\r\n <g *ngFor=\"let tempEdge of tempEdges(); trackBy: trackByEdgeId\" [class.animated]=\"tempEdge.animated\"\r\n class=\"ngx-workflow__edge\">\r\n <path [attr.d]=\"getEdgePath(tempEdge, true)\" class=\"ngx-workflow__edge-path\" [style]=\"tempEdge.style\"></path>\r\n </g>\r\n\r\n <!-- Nodes -->\r\n <g *ngFor=\"let node of filteredNodes(); trackBy: trackByNodeId\" class=\"ngx-workflow__node\"\r\n [attr.data-id]=\"node.id\" [class.selected]=\"node.selected\" [class.dragging]=\"node.dragging\"\r\n [attr.transform]=\"'translate(' + node.position.x + ',' + node.position.y + ')'\"\r\n (dblclick)=\"onNodeDoubleClick($event, node)\">\r\n\r\n <!-- Node outline and background (visual + dragging) -->\r\n <rect [attr.width]=\"node.width || defaultNodeWidth\" [attr.height]=\"node.height || defaultNodeHeight\" rx=\"3\"\r\n ry=\"3\" class=\"ngx-workflow__node-rect\"></rect>\r\n\r\n <!-- Node Label -->\r\n <text *ngIf=\"node.type !== 'group'\" [attr.x]=\"(node.width || defaultNodeWidth) / 2\"\r\n [attr.y]=\"(node.height || defaultNodeHeight) / 2\" text-anchor=\"middle\" dominant-baseline=\"middle\"\r\n class=\"ngx-workflow__node-label\">\r\n {{ node.label }}\r\n </text>\r\n\r\n <!-- Hit area -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" cy=\"-10\" r=\"20\" fill=\"transparent\"\r\n pointer-events=\"all\"></circle>\r\n <!-- Visual -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" [attr.cy]=\"0\" r=\"6\"\r\n class=\"ngx-workflow__handle-circle\">\r\n </circle>\r\n\r\n <!-- Top Handle -->\r\n <g class=\"ngx-workflow__handle ngx-workflow__handle--source ngx-workflow__handle--target\"\r\n [attr.data-nodeid]=\"node.id\" data-handleid=\"top\" [attr.data-type]=\"'source'\">\r\n <!-- Hit area -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" cy=\"-10\" r=\"20\" fill=\"transparent\"\r\n pointer-events=\"all\"></circle>\r\n <!-- Visual -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" [attr.cy]=\"0\" r=\"6\"\r\n class=\"ngx-workflow__handle-circle\">\r\n </circle>\r\n </g>\r\n\r\n <!-- Right Handle -->\r\n <g class=\"ngx-workflow__handle ngx-workflow__handle--source ngx-workflow__handle--target\"\r\n [attr.data-nodeid]=\"node.id\" data-handleid=\"right\" [attr.data-type]=\"'source'\">\r\n <!-- Hit area -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) + 10\" [attr.cy]=\"(node.height || defaultNodeHeight) / 2\"\r\n r=\"20\" fill=\"transparent\" pointer-events=\"all\">\r\n </circle>\r\n <!-- Visual -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth)\" [attr.cy]=\"(node.height || defaultNodeHeight) / 2\" r=\"6\"\r\n class=\"ngx-workflow__handle-circle\"></circle>\r\n </g>\r\n <!-- Bottom Handle -->\r\n <g class=\"ngx-workflow__handle ngx-workflow__handle--source ngx-workflow__handle--target\"\r\n [attr.data-nodeid]=\"node.id\" data-handleid=\"bottom\" [attr.data-type]=\"'source'\">\r\n <!-- Hit area -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" [attr.cy]=\"(node.height || defaultNodeHeight) + 10\"\r\n r=\"20\" fill=\"transparent\" pointer-events=\"all\">\r\n </circle>\r\n <!-- Visual -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" [attr.cy]=\"(node.height || defaultNodeHeight)\" r=\"6\"\r\n class=\"ngx-workflow__handle-circle\"></circle>\r\n </g>\r\n <!-- Left Handle -->\r\n <g class=\"ngx-workflow__handle ngx-workflow__handle--source ngx-workflow__handle--target\"\r\n [attr.data-nodeid]=\"node.id\" data-handleid=\"left\" [attr.data-type]=\"'source'\">\r\n <!-- Hit area -->\r\n <circle cx=\"-10\" [attr.cy]=\"(node.height || defaultNodeHeight) / 2\" r=\"20\" fill=\"transparent\"\r\n pointer-events=\"all\"></circle>\r\n <!-- Visual -->\r\n <circle [attr.cx]=\"0\" [attr.cy]=\"(node.height || defaultNodeHeight) / 2\" r=\"6\"\r\n class=\"ngx-workflow__handle-circle\">\r\n </circle>\r\n </g>\r\n\r\n <!-- Resize Handles -->\r\n <g *ngIf=\"node.selected && (node.resizable !== false) && nodesResizable\">\r\n <!-- NW Handle -->\r\n <rect class=\"ngx-workflow__resize-handle\" data-handle=\"nw\" [attr.x]=\"-5\" [attr.y]=\"-5\" width=\"10\" height=\"10\">\r\n </rect>\r\n <!-- NE Handle -->\r\n <rect class=\"ngx-workflow__resize-handle\" data-handle=\"ne\" [attr.x]=\"(node.width || defaultNodeWidth) - 5\"\r\n [attr.y]=\"-5\" width=\"10\" height=\"10\"></rect>\r\n <!-- SW Handle -->\r\n <rect class=\"ngx-workflow__resize-handle\" data-handle=\"sw\" [attr.x]=\"-5\"\r\n [attr.y]=\"(node.height || defaultNodeHeight) - 5\" width=\"10\" height=\"10\"></rect>\r\n <!-- SE Handle -->\r\n <rect class=\"ngx-workflow__resize-handle\" data-handle=\"se\" [attr.x]=\"(node.width || defaultNodeWidth) - 5\"\r\n [attr.y]=\"(node.height || defaultNodeHeight) - 5\" width=\"10\" height=\"10\"></rect>\r\n </g>\r\n\r\n <!-- Group Node Template -->\r\n <g *ngIf=\"node.type === 'group'\" class=\"ngx-workflow__group-node\">\r\n <!-- Header -->\r\n <rect [attr.width]=\"node.width\" height=\"30\" class=\"ngx-workflow__group-header\"></rect>\r\n <text [attr.x]=\"10\" [attr.y]=\"20\" class=\"ngx-workflow__group-label\">{{ node.label }}</text>\r\n\r\n <!-- Expand/Collapse Toggle -->\r\n <g class=\"ngx-workflow__group-toggle\" [attr.transform]=\"'translate(' + (node.width! - 25) + ', 5)'\"\r\n (click)=\"toggleGroup($event, node)\">\r\n <rect width=\"20\" height=\"20\" rx=\"4\" class=\"ngx-workflow__group-toggle-bg\"></rect>\r\n <path [attr.d]=\"node.expanded ? 'M 5 10 L 15 10' : 'M 5 10 L 15 10 M 10 5 L 10 15'\"\r\n class=\"ngx-workflow__group-toggle-icon\"></path>\r\n </g>\r\n </g>\r\n </g>\r\n\r\n <!-- Alignment Guides -->\r\n <g *ngFor=\"let guide of alignmentGuides()\" class=\"ngx-workflow__alignment-guide\">\r\n <line *ngIf=\"guide.type === 'horizontal'\" [attr.x1]=\"guide.start\" [attr.y1]=\"guide.position\"\r\n [attr.x2]=\"guide.end\" [attr.y2]=\"guide.position\" />\r\n <line *ngIf=\"guide.type === 'vertical'\" [attr.x1]=\"guide.position\" [attr.y1]=\"guide.start\"\r\n [attr.x2]=\"guide.position\" [attr.y2]=\"guide.end\" />\r\n </g>\r\n </g>\r\n\r\n </svg>\r\n\r\n <!-- IO Controls -->\r\n <div class=\"ngx-workflow__io-controls\">\r\n <input type=\"text\" placeholder=\"Search...\" (input)=\"onSearch($event)\">\r\n <select (change)=\"onFilterType($event)\">\r\n <option value=\"\">All Types</option>\r\n <option *ngFor=\"let type of nodeTypeKeys\" [value]=\"type\">{{ type }}</option>\r\n </select>\r\n <div class=\"ngx-workflow__separator\"></div>\r\n <button (click)=\"exportToJSON()\">Export JSON</button>\r\n <button (click)=\"triggerImport()\">Import JSON</button>\r\n <button (click)=\"exportToPNG()\">Export PNG</button>\r\n <button (click)=\"exportToSVG()\">Export SVG</button>\r\n </div>\r\n\r\n <ngx-workflow-zoom-controls *ngIf=\"showZoomControls\" [zoom]=\"viewport().zoom\" [minZoom]=\"0.1\" [maxZoom]=\"4\"\r\n (zoomIn)=\"onZoomChange(viewport().zoom + 0.1)\" (zoomOut)=\"onZoomChange(viewport().zoom - 0.1)\"\r\n (fitView)=\"onZoomChange(1)\" (resetZoom)=\"onZoomChange(1)\"></ngx-workflow-zoom-controls>\r\n\r\n <ngx-workflow-alignment-controls></ngx-workflow-alignment-controls>\r\n\r\n <ngx-workflow-minimap *ngIf=\"showMinimap\"></ngx-workflow-minimap>\r\n\r\n <ngx-workflow-properties-sidebar [node]=\"selectedNodeForEditing\" (close)=\"closeSidebar()\"\r\n (change)=\"onPropertiesChange($event)\"></ngx-workflow-properties-sidebar>\r\n</div>", styles: [":host{--ngx-workflow-primary: #3b82f6;--ngx-workflow-primary-hover: #2563eb;--ngx-workflow-bg: #f8fafc;--ngx-workflow-surface: #ffffff;--ngx-workflow-border: #e2e8f0;--ngx-workflow-text-primary: #1e293b;--ngx-workflow-text-secondary: #64748b;--ngx-workflow-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / .05);--ngx-workflow-shadow-md: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--ngx-workflow-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--ngx-workflow-glass-bg: rgba(255, 255, 255, .8);--ngx-workflow-glass-border: rgba(255, 255, 255, .5);--ngx-workflow-glass-blur: blur(12px);display:block;width:100%;height:100%;font-family:Inter,system-ui,-apple-system,sans-serif}.ngx-workflow__container{position:relative;width:100%;height:100%;overflow:hidden;background-color:var(--ngx-workflow-bg)}.ngx-workflow__diagram{width:100%;height:100%;cursor:grab;position:relative;z-index:1}.ngx-workflow__diagram:active{cursor:grabbing}.ngx-workflow__background{fill:url(#ngx-workflow__grid-pattern);opacity:.6}.ngx-workflow__viewport{transform-origin:0 0}.ngx-workflow__node{cursor:grab;transition:transform .1s ease,opacity .2s ease}.ngx-workflow__node .ngx-workflow__node-rect{fill:var(--ngx-workflow-surface);stroke:var(--ngx-workflow-border);stroke-width:1.5px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.05));transition:all .2s cubic-bezier(.4,0,.2,1);pointer-events:all}.ngx-workflow__node .ngx-workflow__node-label{font-size:12px;font-weight:500;fill:var(--ngx-workflow-text-primary);pointer-events:none;-webkit-user-select:none;user-select:none}.ngx-workflow__node:hover .ngx-workflow__node-rect{stroke:var(--ngx-workflow-primary);filter:drop-shadow(0 4px 6px rgba(0,0,0,.08));transform:translateY(-1px)}.ngx-workflow__node.selected .ngx-workflow__node-rect{stroke:var(--ngx-workflow-primary);stroke-width:2px;filter:drop-shadow(0 0 0 2px rgba(59,130,246,.2))}.ngx-workflow__node.dragging{cursor:grabbing;opacity:.9}.ngx-workflow__node.dragging .ngx-workflow__node-rect{filter:drop-shadow(0 10px 15px rgba(0,0,0,.1));transform:scale(1.02)}.ngx-workflow__node.dimmed{opacity:.4}foreignObject{overflow:visible;pointer-events:none}foreignObject *{pointer-events:auto}.ngx-workflow__edge{pointer-events:none}.ngx-workflow__edge-path{fill:none;stroke:#94a3b8;stroke-width:2;transition:stroke .2s ease,stroke-width .2s ease;pointer-events:stroke;cursor:pointer}.ngx-workflow__edge-hitbox{fill:none;stroke:transparent;stroke-width:20;pointer-events:stroke;cursor:pointer}.ngx-workflow__edge:hover .ngx-workflow__edge-path{stroke:var(--ngx-workflow-primary)}.ngx-workflow__edge.selected .ngx-workflow__edge-path{stroke:var(--ngx-workflow-primary);stroke-width:3;filter:drop-shadow(0 1px 2px rgba(59,130,246,.3))}.ngx-workflow__edge.animated .ngx-workflow__edge-path{stroke-dasharray:10;animation:flowAnimation 1s linear infinite}@keyframes flowAnimation{0%{stroke-dashoffset:20}to{stroke-dashoffset:0}}.ngx-workflow__edge-label{pointer-events:none}.ngx-workflow__edge-label-text{font-family:Inter,sans-serif;font-size:12px;font-weight:500;fill:var(--ngx-workflow-text-secondary);text-anchor:middle;dominant-baseline:middle}.ngx-workflow__edge-label-bg{fill:var(--ngx-workflow-surface);rx:4;ry:4;filter:drop-shadow(0 1px 2px rgba(0,0,0,.1))}.ngx-workflow__edge-label-input{width:100%;height:100%;border:1px solid var(--ngx-workflow-primary);border-radius:4px;padding:2px 6px;font-size:12px;text-align:center;background:var(--ngx-workflow-surface);pointer-events:all;outline:none;box-shadow:var(--ngx-workflow-shadow-sm)}.ngx-workflow__handle{opacity:1;transition:transform .2s cubic-bezier(.34,1.56,.64,1);pointer-events:all}.ngx-workflow__handle-circle{fill:var(--ngx-workflow-surface);stroke:var(--ngx-workflow-text-secondary);stroke-width:1.5;transition:all .2s ease}.ngx-workflow__handle-circle:hover{fill:var(--ngx-workflow-primary);stroke:var(--ngx-workflow-primary);r:7}.ngx-workflow__handle.ngx-workflow__handle--valid-target .ngx-workflow__handle-circle{fill:#10b981!important;stroke:#059669!important;r:8;stroke-width:2}.ngx-workflow__io-controls{position:absolute;top:16px;left:16px;z-index:10;display:flex;gap:8px;padding:8px;background:var(--ngx-workflow-glass-bg);-webkit-backdrop-filter:var(--ngx-workflow-glass-blur);backdrop-filter:var(--ngx-workflow-glass-blur);border:1px solid var(--ngx-workflow-glass-border);border-radius:12px;box-shadow:var(--ngx-workflow-shadow-md);align-items:center}.ngx-workflow__io-controls input[type=text]{padding:6px 12px;border:1px solid transparent;border-radius:8px;font-size:13px;background:#0000000d;transition:all .2s ease;width:160px}.ngx-workflow__io-controls input[type=text]:focus{background:#fff;border-color:var(--ngx-workflow-primary);outline:none;box-shadow:0 0 0 2px #3b82f61a}.ngx-workflow__io-controls select{padding:6px 12px;border:1px solid transparent;border-radius:8px;font-size:13px;background:#0000000d;cursor:pointer;transition:all .2s ease}.ngx-workflow__io-controls select:hover{background:#00000014}.ngx-workflow__io-controls select:focus{outline:none;border-color:var(--ngx-workflow-primary)}.ngx-workflow__io-controls .ngx-workflow__separator{width:1px;height:24px;background:#0000001a;margin:0 4px}.ngx-workflow__io-controls button{padding:6px 12px;background:transparent;border:1px solid transparent;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;color:var(--ngx-workflow-text-primary);transition:all .2s ease}.ngx-workflow__io-controls button:hover{background:#0000000d;color:var(--ngx-workflow-primary)}.ngx-workflow__io-controls button:active{transform:translateY(1px)}.ngx-workflow__group-node .ngx-workflow__group-header{fill:#00000008;stroke:none}.ngx-workflow__group-node .ngx-workflow__group-label{font-family:Inter,sans-serif;font-size:11px;font-weight:600;fill:var(--ngx-workflow-text-secondary);text-transform:uppercase;letter-spacing:.5px}.ngx-workflow__group-node .ngx-workflow__group-toggle{cursor:pointer;opacity:.6;transition:opacity .2s}.ngx-workflow__group-node .ngx-workflow__group-toggle:hover{opacity:1}.ngx-workflow__group-node .ngx-workflow__group-toggle-bg{fill:#0000001a}.ngx-workflow__group-node .ngx-workflow__group-toggle-icon{stroke:var(--ngx-workflow-text-secondary);stroke-width:1.5;fill:none}.ngx-workflow__alignment-guide{pointer-events:none}.ngx-workflow__alignment-guide line{stroke:#ec4899;stroke-width:1;stroke-dasharray:4 4;opacity:.8}.ngx-workflow__resize-handle{fill:var(--ngx-workflow-surface);stroke:var(--ngx-workflow-primary);stroke-width:1.5;width:8px;height:8px;transition:transform .2s ease;transform-box:fill-box;transform-origin:center}.ngx-workflow__resize-handle:hover{fill:var(--ngx-workflow-primary);transform:scale(1.2)}.ngx-workflow__resize-handle[data-handle=nw]{cursor:nw-resize}.ngx-workflow__resize-handle[data-handle=ne]{cursor:ne-resize}.ngx-workflow__resize-handle[data-handle=sw]{cursor:sw-resize}.ngx-workflow__resize-handle[data-handle=se]{cursor:se-resize}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ZoomControlsComponent, selector: "ngx-workflow-zoom-controls", inputs: ["zoom", "minZoom", "maxZoom"], outputs: ["zoomIn", "zoomOut", "fitView", "resetZoom"] }, { kind: "component", type: MinimapComponent, selector: "ngx-workflow-minimap", inputs: ["nodeColor", "nodeClass"] }, { kind: "component", type: BackgroundComponent, selector: "ngx-workflow-background", inputs: ["variant", "gap", "size", "color", "backgroundColor"] }, { kind: "component", type: AlignmentControlsComponent, selector: "ngx-workflow-alignment-controls" }, { kind: "component", type: PropertiesSidebarComponent, selector: "ngx-workflow-properties-sidebar", inputs: ["node"], outputs: ["close", "change"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2728
+ }
2729
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DiagramComponent, decorators: [{
2730
+ type: Component,
2731
+ args: [{ selector: 'ngx-workflow-diagram', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [CommonModule, ZoomControlsComponent, MinimapComponent, BackgroundComponent, AlignmentControlsComponent, PropertiesSidebarComponent], template: "<div class=\"ngx-workflow__container\" [attr.data-lod]=\"lodLevel\">\r\n <ngx-workflow-background *ngIf=\"showBackground\" [variant]=\"backgroundVariant\" [gap]=\"backgroundGap\"\r\n [size]=\"backgroundSize\" [color]=\"backgroundColor\" [backgroundColor]=\"backgroundBgColor\"></ngx-workflow-background>\r\n <svg #svg class=\"ngx-workflow__diagram\" (wheel)=\"onWheel($event)\" (pointerdown)=\"onPointerDown($event)\"\r\n (dblclick)=\"onDiagramDoubleClick($event)\">\r\n <defs>\r\n <pattern id=\"ngx-workflow__grid-pattern\" x=\"0\" y=\"0\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\r\n <circle cx=\"0.5\" cy=\"0.5\" r=\"0.5\" fill=\"#ccc\" />\r\n </pattern>\r\n\r\n <!-- Arrow marker -->\r\n <marker id=\"ngx-workflow__arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\"\r\n orient=\"auto-start-reverse\">\r\n <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"context-stroke\" />\r\n </marker>\r\n\r\n <!-- Arrowclosed marker (filled) -->\r\n <marker id=\"ngx-workflow__arrowclosed\" viewBox=\"0 0 10 10\" refX=\"10\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\"\r\n orient=\"auto-start-reverse\">\r\n <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"context-stroke\" stroke=\"context-stroke\" />\r\n </marker>\r\n\r\n <!-- Dot marker -->\r\n <marker id=\"ngx-workflow__dot\" viewBox=\"0 0 10 10\" refX=\"5\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\">\r\n <circle cx=\"5\" cy=\"5\" r=\"4\" fill=\"context-stroke\" />\r\n </marker>\r\n </defs>\r\n\r\n <rect width=\"100%\" height=\"100%\" fill=\"url(#ngx-workflow__grid-pattern)\" class=\"ngx-workflow__background\" />\r\n\r\n <g class=\"ngx-workflow__viewport\" [attr.transform]=\"transform\">\r\n <!-- Edges -->\r\n <g *ngFor=\"let edge of edges(); trackBy: trackByEdgeId\" [class.selected]=\"edge.selected\"\r\n [class.animated]=\"edge.animated\" class=\"ngx-workflow__edge\" (click)=\"onEdgeClick($event, edge)\"\r\n (dblclick)=\"onEdgeDoubleClick($event, edge)\">\r\n <!-- Edge path with markers -->\r\n <path [attr.d]=\"getEdgePath(edge)\" class=\"ngx-workflow__edge-path\" [style]=\"edge.style\"\r\n [attr.marker-start]=\"getMarkerUrl(edge.markerStart)\" [attr.marker-end]=\"getMarkerUrl(edge.markerEnd)\">\r\n </path>\r\n <!-- Invisible hitbox for easier clicking -->\r\n <path [attr.d]=\"getEdgePath(edge)\" class=\"ngx-workflow__edge-hitbox\"></path>\r\n\r\n <!-- Edge label -->\r\n <g *ngIf=\"edge.label || editingEdgeId === edge.id\" class=\"ngx-workflow__edge-label\">\r\n <foreignObject *ngIf=\"editingEdgeId === edge.id\" [attr.x]=\"getEdgeLabelPosition(edge).x - 50\"\r\n [attr.y]=\"getEdgeLabelPosition(edge).y - 15\" width=\"100\" height=\"30\">\r\n <input #edgeLabelInput type=\"text\" [value]=\"edge.label || ''\" (blur)=\"onEdgeLabelBlur()\"\r\n (keydown.enter)=\"updateEdgeLabel(edge, edgeLabelInput.value)\" class=\"ngx-workflow__edge-label-input\"\r\n autofocus>\r\n </foreignObject>\r\n\r\n <text *ngIf=\"editingEdgeId !== edge.id\" [attr.x]=\"getEdgeLabelPosition(edge).x\"\r\n [attr.y]=\"getEdgeLabelPosition(edge).y\" text-anchor=\"middle\" dominant-baseline=\"middle\"\r\n class=\"ngx-workflow__edge-label-text\" [style]=\"edge.labelStyle\">\r\n <!-- Label background -->\r\n <tspan *ngIf=\"edge.labelBgStyle\" class=\"ngx-workflow__edge-label-bg\" [style]=\"edge.labelBgStyle\"></tspan>\r\n {{ edge.label }}\r\n </text>\r\n </g>\r\n\r\n <!-- Edge Update Handles -->\r\n <g *ngIf=\"edge.selected\">\r\n <!-- Source Handle -->\r\n <circle class=\"ngx-workflow__edge-handle\" [attr.cx]=\"getEdgeHandlePosition(edge, 'source').x\"\r\n [attr.cy]=\"getEdgeHandlePosition(edge, 'source').y\" r=\"4\"\r\n (pointerdown)=\"startUpdatingEdge($event, edge, 'source')\"></circle>\r\n <!-- Target Handle -->\r\n <circle class=\"ngx-workflow__edge-handle\" [attr.cx]=\"getEdgeHandlePosition(edge, 'target').x\"\r\n [attr.cy]=\"getEdgeHandlePosition(edge, 'target').y\" r=\"4\"\r\n (pointerdown)=\"startUpdatingEdge($event, edge, 'target')\"></circle>\r\n </g>\r\n </g>\r\n\r\n <!-- Render temporary edges (previews) -->\r\n <g *ngFor=\"let tempEdge of tempEdges(); trackBy: trackByEdgeId\" [class.animated]=\"tempEdge.animated\"\r\n class=\"ngx-workflow__edge\">\r\n <path [attr.d]=\"getEdgePath(tempEdge, true)\" class=\"ngx-workflow__edge-path\" [style]=\"tempEdge.style\"></path>\r\n </g>\r\n\r\n <!-- Nodes -->\r\n <g *ngFor=\"let node of filteredNodes(); trackBy: trackByNodeId\" class=\"ngx-workflow__node\"\r\n [attr.data-id]=\"node.id\" [class.selected]=\"node.selected\" [class.dragging]=\"node.dragging\"\r\n [attr.transform]=\"'translate(' + node.position.x + ',' + node.position.y + ')'\"\r\n (dblclick)=\"onNodeDoubleClick($event, node)\">\r\n\r\n <!-- Node outline and background (visual + dragging) -->\r\n <rect [attr.width]=\"node.width || defaultNodeWidth\" [attr.height]=\"node.height || defaultNodeHeight\" rx=\"3\"\r\n ry=\"3\" class=\"ngx-workflow__node-rect\"></rect>\r\n\r\n <!-- Node Label -->\r\n <text *ngIf=\"node.type !== 'group'\" [attr.x]=\"(node.width || defaultNodeWidth) / 2\"\r\n [attr.y]=\"(node.height || defaultNodeHeight) / 2\" text-anchor=\"middle\" dominant-baseline=\"middle\"\r\n class=\"ngx-workflow__node-label\">\r\n {{ node.label }}\r\n </text>\r\n\r\n <!-- Hit area -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" cy=\"-10\" r=\"20\" fill=\"transparent\"\r\n pointer-events=\"all\"></circle>\r\n <!-- Visual -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" [attr.cy]=\"0\" r=\"6\"\r\n class=\"ngx-workflow__handle-circle\">\r\n </circle>\r\n\r\n <!-- Top Handle -->\r\n <g class=\"ngx-workflow__handle ngx-workflow__handle--source ngx-workflow__handle--target\"\r\n [attr.data-nodeid]=\"node.id\" data-handleid=\"top\" [attr.data-type]=\"'source'\">\r\n <!-- Hit area -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" cy=\"-10\" r=\"20\" fill=\"transparent\"\r\n pointer-events=\"all\"></circle>\r\n <!-- Visual -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" [attr.cy]=\"0\" r=\"6\"\r\n class=\"ngx-workflow__handle-circle\">\r\n </circle>\r\n </g>\r\n\r\n <!-- Right Handle -->\r\n <g class=\"ngx-workflow__handle ngx-workflow__handle--source ngx-workflow__handle--target\"\r\n [attr.data-nodeid]=\"node.id\" data-handleid=\"right\" [attr.data-type]=\"'source'\">\r\n <!-- Hit area -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) + 10\" [attr.cy]=\"(node.height || defaultNodeHeight) / 2\"\r\n r=\"20\" fill=\"transparent\" pointer-events=\"all\">\r\n </circle>\r\n <!-- Visual -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth)\" [attr.cy]=\"(node.height || defaultNodeHeight) / 2\" r=\"6\"\r\n class=\"ngx-workflow__handle-circle\"></circle>\r\n </g>\r\n <!-- Bottom Handle -->\r\n <g class=\"ngx-workflow__handle ngx-workflow__handle--source ngx-workflow__handle--target\"\r\n [attr.data-nodeid]=\"node.id\" data-handleid=\"bottom\" [attr.data-type]=\"'source'\">\r\n <!-- Hit area -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" [attr.cy]=\"(node.height || defaultNodeHeight) + 10\"\r\n r=\"20\" fill=\"transparent\" pointer-events=\"all\">\r\n </circle>\r\n <!-- Visual -->\r\n <circle [attr.cx]=\"(node.width || defaultNodeWidth) / 2\" [attr.cy]=\"(node.height || defaultNodeHeight)\" r=\"6\"\r\n class=\"ngx-workflow__handle-circle\"></circle>\r\n </g>\r\n <!-- Left Handle -->\r\n <g class=\"ngx-workflow__handle ngx-workflow__handle--source ngx-workflow__handle--target\"\r\n [attr.data-nodeid]=\"node.id\" data-handleid=\"left\" [attr.data-type]=\"'source'\">\r\n <!-- Hit area -->\r\n <circle cx=\"-10\" [attr.cy]=\"(node.height || defaultNodeHeight) / 2\" r=\"20\" fill=\"transparent\"\r\n pointer-events=\"all\"></circle>\r\n <!-- Visual -->\r\n <circle [attr.cx]=\"0\" [attr.cy]=\"(node.height || defaultNodeHeight) / 2\" r=\"6\"\r\n class=\"ngx-workflow__handle-circle\">\r\n </circle>\r\n </g>\r\n\r\n <!-- Resize Handles -->\r\n <g *ngIf=\"node.selected && (node.resizable !== false) && nodesResizable\">\r\n <!-- NW Handle -->\r\n <rect class=\"ngx-workflow__resize-handle\" data-handle=\"nw\" [attr.x]=\"-5\" [attr.y]=\"-5\" width=\"10\" height=\"10\">\r\n </rect>\r\n <!-- NE Handle -->\r\n <rect class=\"ngx-workflow__resize-handle\" data-handle=\"ne\" [attr.x]=\"(node.width || defaultNodeWidth) - 5\"\r\n [attr.y]=\"-5\" width=\"10\" height=\"10\"></rect>\r\n <!-- SW Handle -->\r\n <rect class=\"ngx-workflow__resize-handle\" data-handle=\"sw\" [attr.x]=\"-5\"\r\n [attr.y]=\"(node.height || defaultNodeHeight) - 5\" width=\"10\" height=\"10\"></rect>\r\n <!-- SE Handle -->\r\n <rect class=\"ngx-workflow__resize-handle\" data-handle=\"se\" [attr.x]=\"(node.width || defaultNodeWidth) - 5\"\r\n [attr.y]=\"(node.height || defaultNodeHeight) - 5\" width=\"10\" height=\"10\"></rect>\r\n </g>\r\n\r\n <!-- Group Node Template -->\r\n <g *ngIf=\"node.type === 'group'\" class=\"ngx-workflow__group-node\">\r\n <!-- Header -->\r\n <rect [attr.width]=\"node.width\" height=\"30\" class=\"ngx-workflow__group-header\"></rect>\r\n <text [attr.x]=\"10\" [attr.y]=\"20\" class=\"ngx-workflow__group-label\">{{ node.label }}</text>\r\n\r\n <!-- Expand/Collapse Toggle -->\r\n <g class=\"ngx-workflow__group-toggle\" [attr.transform]=\"'translate(' + (node.width! - 25) + ', 5)'\"\r\n (click)=\"toggleGroup($event, node)\">\r\n <rect width=\"20\" height=\"20\" rx=\"4\" class=\"ngx-workflow__group-toggle-bg\"></rect>\r\n <path [attr.d]=\"node.expanded ? 'M 5 10 L 15 10' : 'M 5 10 L 15 10 M 10 5 L 10 15'\"\r\n class=\"ngx-workflow__group-toggle-icon\"></path>\r\n </g>\r\n </g>\r\n </g>\r\n\r\n <!-- Alignment Guides -->\r\n <g *ngFor=\"let guide of alignmentGuides()\" class=\"ngx-workflow__alignment-guide\">\r\n <line *ngIf=\"guide.type === 'horizontal'\" [attr.x1]=\"guide.start\" [attr.y1]=\"guide.position\"\r\n [attr.x2]=\"guide.end\" [attr.y2]=\"guide.position\" />\r\n <line *ngIf=\"guide.type === 'vertical'\" [attr.x1]=\"guide.position\" [attr.y1]=\"guide.start\"\r\n [attr.x2]=\"guide.position\" [attr.y2]=\"guide.end\" />\r\n </g>\r\n </g>\r\n\r\n </svg>\r\n\r\n <!-- IO Controls -->\r\n <div class=\"ngx-workflow__io-controls\">\r\n <input type=\"text\" placeholder=\"Search...\" (input)=\"onSearch($event)\">\r\n <select (change)=\"onFilterType($event)\">\r\n <option value=\"\">All Types</option>\r\n <option *ngFor=\"let type of nodeTypeKeys\" [value]=\"type\">{{ type }}</option>\r\n </select>\r\n <div class=\"ngx-workflow__separator\"></div>\r\n <button (click)=\"exportToJSON()\">Export JSON</button>\r\n <button (click)=\"triggerImport()\">Import JSON</button>\r\n <button (click)=\"exportToPNG()\">Export PNG</button>\r\n <button (click)=\"exportToSVG()\">Export SVG</button>\r\n </div>\r\n\r\n <ngx-workflow-zoom-controls *ngIf=\"showZoomControls\" [zoom]=\"viewport().zoom\" [minZoom]=\"0.1\" [maxZoom]=\"4\"\r\n (zoomIn)=\"onZoomChange(viewport().zoom + 0.1)\" (zoomOut)=\"onZoomChange(viewport().zoom - 0.1)\"\r\n (fitView)=\"onZoomChange(1)\" (resetZoom)=\"onZoomChange(1)\"></ngx-workflow-zoom-controls>\r\n\r\n <ngx-workflow-alignment-controls></ngx-workflow-alignment-controls>\r\n\r\n <ngx-workflow-minimap *ngIf=\"showMinimap\"></ngx-workflow-minimap>\r\n\r\n <ngx-workflow-properties-sidebar [node]=\"selectedNodeForEditing\" (close)=\"closeSidebar()\"\r\n (change)=\"onPropertiesChange($event)\"></ngx-workflow-properties-sidebar>\r\n</div>", styles: [":host{--ngx-workflow-primary: #3b82f6;--ngx-workflow-primary-hover: #2563eb;--ngx-workflow-bg: #f8fafc;--ngx-workflow-surface: #ffffff;--ngx-workflow-border: #e2e8f0;--ngx-workflow-text-primary: #1e293b;--ngx-workflow-text-secondary: #64748b;--ngx-workflow-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / .05);--ngx-workflow-shadow-md: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--ngx-workflow-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--ngx-workflow-glass-bg: rgba(255, 255, 255, .8);--ngx-workflow-glass-border: rgba(255, 255, 255, .5);--ngx-workflow-glass-blur: blur(12px);display:block;width:100%;height:100%;font-family:Inter,system-ui,-apple-system,sans-serif}.ngx-workflow__container{position:relative;width:100%;height:100%;overflow:hidden;background-color:var(--ngx-workflow-bg)}.ngx-workflow__diagram{width:100%;height:100%;cursor:grab;position:relative;z-index:1}.ngx-workflow__diagram:active{cursor:grabbing}.ngx-workflow__background{fill:url(#ngx-workflow__grid-pattern);opacity:.6}.ngx-workflow__viewport{transform-origin:0 0}.ngx-workflow__node{cursor:grab;transition:transform .1s ease,opacity .2s ease}.ngx-workflow__node .ngx-workflow__node-rect{fill:var(--ngx-workflow-surface);stroke:var(--ngx-workflow-border);stroke-width:1.5px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.05));transition:all .2s cubic-bezier(.4,0,.2,1);pointer-events:all}.ngx-workflow__node .ngx-workflow__node-label{font-size:12px;font-weight:500;fill:var(--ngx-workflow-text-primary);pointer-events:none;-webkit-user-select:none;user-select:none}.ngx-workflow__node:hover .ngx-workflow__node-rect{stroke:var(--ngx-workflow-primary);filter:drop-shadow(0 4px 6px rgba(0,0,0,.08));transform:translateY(-1px)}.ngx-workflow__node.selected .ngx-workflow__node-rect{stroke:var(--ngx-workflow-primary);stroke-width:2px;filter:drop-shadow(0 0 0 2px rgba(59,130,246,.2))}.ngx-workflow__node.dragging{cursor:grabbing;opacity:.9}.ngx-workflow__node.dragging .ngx-workflow__node-rect{filter:drop-shadow(0 10px 15px rgba(0,0,0,.1));transform:scale(1.02)}.ngx-workflow__node.dimmed{opacity:.4}foreignObject{overflow:visible;pointer-events:none}foreignObject *{pointer-events:auto}.ngx-workflow__edge{pointer-events:none}.ngx-workflow__edge-path{fill:none;stroke:#94a3b8;stroke-width:2;transition:stroke .2s ease,stroke-width .2s ease;pointer-events:stroke;cursor:pointer}.ngx-workflow__edge-hitbox{fill:none;stroke:transparent;stroke-width:20;pointer-events:stroke;cursor:pointer}.ngx-workflow__edge:hover .ngx-workflow__edge-path{stroke:var(--ngx-workflow-primary)}.ngx-workflow__edge.selected .ngx-workflow__edge-path{stroke:var(--ngx-workflow-primary);stroke-width:3;filter:drop-shadow(0 1px 2px rgba(59,130,246,.3))}.ngx-workflow__edge.animated .ngx-workflow__edge-path{stroke-dasharray:10;animation:flowAnimation 1s linear infinite}@keyframes flowAnimation{0%{stroke-dashoffset:20}to{stroke-dashoffset:0}}.ngx-workflow__edge-label{pointer-events:none}.ngx-workflow__edge-label-text{font-family:Inter,sans-serif;font-size:12px;font-weight:500;fill:var(--ngx-workflow-text-secondary);text-anchor:middle;dominant-baseline:middle}.ngx-workflow__edge-label-bg{fill:var(--ngx-workflow-surface);rx:4;ry:4;filter:drop-shadow(0 1px 2px rgba(0,0,0,.1))}.ngx-workflow__edge-label-input{width:100%;height:100%;border:1px solid var(--ngx-workflow-primary);border-radius:4px;padding:2px 6px;font-size:12px;text-align:center;background:var(--ngx-workflow-surface);pointer-events:all;outline:none;box-shadow:var(--ngx-workflow-shadow-sm)}.ngx-workflow__handle{opacity:1;transition:transform .2s cubic-bezier(.34,1.56,.64,1);pointer-events:all}.ngx-workflow__handle-circle{fill:var(--ngx-workflow-surface);stroke:var(--ngx-workflow-text-secondary);stroke-width:1.5;transition:all .2s ease}.ngx-workflow__handle-circle:hover{fill:var(--ngx-workflow-primary);stroke:var(--ngx-workflow-primary);r:7}.ngx-workflow__handle.ngx-workflow__handle--valid-target .ngx-workflow__handle-circle{fill:#10b981!important;stroke:#059669!important;r:8;stroke-width:2}.ngx-workflow__io-controls{position:absolute;top:16px;left:16px;z-index:10;display:flex;gap:8px;padding:8px;background:var(--ngx-workflow-glass-bg);-webkit-backdrop-filter:var(--ngx-workflow-glass-blur);backdrop-filter:var(--ngx-workflow-glass-blur);border:1px solid var(--ngx-workflow-glass-border);border-radius:12px;box-shadow:var(--ngx-workflow-shadow-md);align-items:center}.ngx-workflow__io-controls input[type=text]{padding:6px 12px;border:1px solid transparent;border-radius:8px;font-size:13px;background:#0000000d;transition:all .2s ease;width:160px}.ngx-workflow__io-controls input[type=text]:focus{background:#fff;border-color:var(--ngx-workflow-primary);outline:none;box-shadow:0 0 0 2px #3b82f61a}.ngx-workflow__io-controls select{padding:6px 12px;border:1px solid transparent;border-radius:8px;font-size:13px;background:#0000000d;cursor:pointer;transition:all .2s ease}.ngx-workflow__io-controls select:hover{background:#00000014}.ngx-workflow__io-controls select:focus{outline:none;border-color:var(--ngx-workflow-primary)}.ngx-workflow__io-controls .ngx-workflow__separator{width:1px;height:24px;background:#0000001a;margin:0 4px}.ngx-workflow__io-controls button{padding:6px 12px;background:transparent;border:1px solid transparent;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;color:var(--ngx-workflow-text-primary);transition:all .2s ease}.ngx-workflow__io-controls button:hover{background:#0000000d;color:var(--ngx-workflow-primary)}.ngx-workflow__io-controls button:active{transform:translateY(1px)}.ngx-workflow__group-node .ngx-workflow__group-header{fill:#00000008;stroke:none}.ngx-workflow__group-node .ngx-workflow__group-label{font-family:Inter,sans-serif;font-size:11px;font-weight:600;fill:var(--ngx-workflow-text-secondary);text-transform:uppercase;letter-spacing:.5px}.ngx-workflow__group-node .ngx-workflow__group-toggle{cursor:pointer;opacity:.6;transition:opacity .2s}.ngx-workflow__group-node .ngx-workflow__group-toggle:hover{opacity:1}.ngx-workflow__group-node .ngx-workflow__group-toggle-bg{fill:#0000001a}.ngx-workflow__group-node .ngx-workflow__group-toggle-icon{stroke:var(--ngx-workflow-text-secondary);stroke-width:1.5;fill:none}.ngx-workflow__alignment-guide{pointer-events:none}.ngx-workflow__alignment-guide line{stroke:#ec4899;stroke-width:1;stroke-dasharray:4 4;opacity:.8}.ngx-workflow__resize-handle{fill:var(--ngx-workflow-surface);stroke:var(--ngx-workflow-primary);stroke-width:1.5;width:8px;height:8px;transition:transform .2s ease;transform-box:fill-box;transform-origin:center}.ngx-workflow__resize-handle:hover{fill:var(--ngx-workflow-primary);transform:scale(1.2)}.ngx-workflow__resize-handle[data-handle=nw]{cursor:nw-resize}.ngx-workflow__resize-handle[data-handle=ne]{cursor:ne-resize}.ngx-workflow__resize-handle[data-handle=sw]{cursor:sw-resize}.ngx-workflow__resize-handle[data-handle=se]{cursor:se-resize}\n"] }]
2732
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }, { type: DiagramStateService }, { type: undefined, decorators: [{
2733
+ type: Optional
2734
+ }, {
2735
+ type: Inject,
2736
+ args: [NGX_WORKFLOW_NODE_TYPES]
2737
+ }] }], propDecorators: { svgRef: [{
2738
+ type: ViewChild,
2739
+ args: ['svg', { static: true }]
2740
+ }], initialNodes: [{
2741
+ type: Input
2742
+ }], initialEdges: [{
2743
+ type: Input
2744
+ }], initialViewport: [{
2745
+ type: Input
2746
+ }], showZoomControls: [{
2747
+ type: Input
2748
+ }], showMinimap: [{
2749
+ type: Input
2750
+ }], showBackground: [{
2751
+ type: Input
2752
+ }], backgroundVariant: [{
2753
+ type: Input
2754
+ }], backgroundGap: [{
2755
+ type: Input
2756
+ }], backgroundSize: [{
2757
+ type: Input
2758
+ }], backgroundColor: [{
2759
+ type: Input
2760
+ }], backgroundBgColor: [{
2761
+ type: Input
2762
+ }], nodeClick: [{
2763
+ type: Output
2764
+ }], edgeClick: [{
2765
+ type: Output
2766
+ }], connect: [{
2767
+ type: Output
2768
+ }], nodesChange: [{
2769
+ type: Output
2770
+ }], edgesChange: [{
2771
+ type: Output
2772
+ }], nodeDoubleClick: [{
2773
+ type: Output
2774
+ }], connectionValidator: [{
2775
+ type: Input
2776
+ }], nodesResizable: [{
2777
+ type: Input
2778
+ }], onDeleteKeyPress: [{
2779
+ type: HostListener,
2780
+ args: ['window:keydown.delete', ['$event']]
2781
+ }], onUndoKeyPress: [{
2782
+ type: HostListener,
2783
+ args: ['window:keydown.control.z', ['$event']]
2784
+ }, {
2785
+ type: HostListener,
2786
+ args: ['window:keydown.meta.z', ['$event']]
2787
+ }], onRedoKeyPress: [{
2788
+ type: HostListener,
2789
+ args: ['window:keydown.control.shift.z', ['$event']]
2790
+ }, {
2791
+ type: HostListener,
2792
+ args: ['window:keydown.meta.shift.z', ['$event']]
2793
+ }], onCopyKeyPress: [{
2794
+ type: HostListener,
2795
+ args: ['window:keydown.control.c', ['$event']]
2796
+ }, {
2797
+ type: HostListener,
2798
+ args: ['window:keydown.meta.c', ['$event']]
2799
+ }], onPasteKeyPress: [{
2800
+ type: HostListener,
2801
+ args: ['window:keydown.control.v', ['$event']]
2802
+ }, {
2803
+ type: HostListener,
2804
+ args: ['window:keydown.meta.v', ['$event']]
2805
+ }], onCutKeyPress: [{
2806
+ type: HostListener,
2807
+ args: ['window:keydown.control.x', ['$event']]
2808
+ }, {
2809
+ type: HostListener,
2810
+ args: ['window:keydown.meta.x', ['$event']]
2811
+ }], onDuplicateKeyPress: [{
2812
+ type: HostListener,
2813
+ args: ['window:keydown.control.d', ['$event']]
2814
+ }, {
2815
+ type: HostListener,
2816
+ args: ['window:keydown.meta.d', ['$event']]
2817
+ }], onGroupKeyPress: [{
2818
+ type: HostListener,
2819
+ args: ['window:keydown.control.g', ['$event']]
2820
+ }, {
2821
+ type: HostListener,
2822
+ args: ['window:keydown.meta.g', ['$event']]
2823
+ }], onUngroupKeyPress: [{
2824
+ type: HostListener,
2825
+ args: ['window:keydown.control.shift.g', ['$event']]
2826
+ }, {
2827
+ type: HostListener,
2828
+ args: ['window:keydown.meta.shift.g', ['$event']]
2829
+ }], onKeyDown: [{
2830
+ type: HostListener,
2831
+ args: ['window:keydown', ['$event']]
2832
+ }] } });
2833
+
2834
+ class RoundedRectNodeComponent {
2835
+ node;
2836
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: RoundedRectNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2837
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: RoundedRectNodeComponent, isStandalone: true, selector: "ngx-workflow-rounded-rect-node", inputs: { node: "node" }, ngImport: i0, template: `
2838
+ <svg:g class="ngx-workflow__custom-node ngx-workflow__rounded-rect-node">
2839
+ <rect
2840
+ [attr.x]="0"
2841
+ [attr.y]="0"
2842
+ [attr.width]="node.width || 170"
2843
+ [attr.height]="node.height || 60"
2844
+ rx="10"
2845
+ ry="10"
2846
+ fill="#a7f3d0"
2847
+ stroke="#065f46"
2848
+ stroke-width="1.5"
2849
+ ></rect>
2850
+ <text
2851
+ [attr.x]="(node.width || 170) / 2"
2852
+ [attr.y]="(node.height || 60) / 2"
2853
+ text-anchor="middle"
2854
+ alignment-baseline="middle"
2855
+ fill="#065f46"
2856
+ font-size="14px"
2857
+ font-family="sans-serif"
2858
+ >
2859
+ {{ node.data?.label || 'Custom Node' }}
2860
+ </text>
2861
+ </svg:g>
2862
+ `, isInline: true, styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2863
+ }
2864
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: RoundedRectNodeComponent, decorators: [{
2865
+ type: Component,
2866
+ args: [{ selector: 'ngx-workflow-rounded-rect-node', template: `
2867
+ <svg:g class="ngx-workflow__custom-node ngx-workflow__rounded-rect-node">
2868
+ <rect
2869
+ [attr.x]="0"
2870
+ [attr.y]="0"
2871
+ [attr.width]="node.width || 170"
2872
+ [attr.height]="node.height || 60"
2873
+ rx="10"
2874
+ ry="10"
2875
+ fill="#a7f3d0"
2876
+ stroke="#065f46"
2877
+ stroke-width="1.5"
2878
+ ></rect>
2879
+ <text
2880
+ [attr.x]="(node.width || 170) / 2"
2881
+ [attr.y]="(node.height || 60) / 2"
2882
+ text-anchor="middle"
2883
+ alignment-baseline="middle"
2884
+ fill="#065f46"
2885
+ font-size="14px"
2886
+ font-family="sans-serif"
2887
+ >
2888
+ {{ node.data?.label || 'Custom Node' }}
2889
+ </text>
2890
+ </svg:g>
2891
+ `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [CommonModule] }]
2892
+ }], propDecorators: { node: [{
2893
+ type: Input
2894
+ }] } });
2895
+
2896
+ class LayoutService {
2897
+ elk;
2898
+ constructor() {
2899
+ this.elk = new ELK();
2900
+ }
2901
+ // --- ELK Layout ---
2902
+ async applyElkLayout(nodes, edges, options) {
2903
+ const elkGraph = {
2904
+ id: 'root',
2905
+ layoutOptions: {
2906
+ 'elk.algorithm': 'layered',
2907
+ 'elk.direction': 'DOWN',
2908
+ 'elk.spacing.nodeNode': '75',
2909
+ 'elk.layered.nodePlacement.strategy': 'BRANDES_KOLLER',
2910
+ ...options
2911
+ },
2912
+ children: nodes.map(node => ({
2913
+ id: node.id,
2914
+ width: node.width || 170,
2915
+ height: node.height || 60,
2916
+ })),
2917
+ edges: edges.map(edge => ({
2918
+ id: edge.id,
2919
+ sources: [edge.source],
2920
+ targets: [edge.target],
2921
+ })),
2922
+ };
2923
+ try {
2924
+ const result = await this.elk.layout(elkGraph);
2925
+ const laidOutNodes = nodes.map(node => {
2926
+ const elkNode = result.children?.find((n) => n.id === node.id);
2927
+ if (elkNode && elkNode.x !== undefined && elkNode.y !== undefined) {
2928
+ return {
2929
+ ...node,
2930
+ position: {
2931
+ x: elkNode.x,
2932
+ y: elkNode.y,
2933
+ },
2934
+ };
2935
+ }
2936
+ return node; // Return original if not found or no position
2937
+ });
2938
+ return laidOutNodes;
2939
+ }
2940
+ catch (error) {
2941
+ console.error('ELK layout failed:', error);
2942
+ return nodes; // Return original nodes on error
2943
+ }
2944
+ }
2945
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: LayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2946
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: LayoutService, providedIn: 'root' });
2947
+ }
2948
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: LayoutService, decorators: [{
2949
+ type: Injectable,
2950
+ args: [{
2951
+ providedIn: 'root',
2952
+ }]
2953
+ }], ctorParameters: () => [] });
2954
+
2955
+ class NgxWorkflowModule {
2956
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: NgxWorkflowModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
2957
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.13", ngImport: i0, type: NgxWorkflowModule, imports: [CommonModule,
2958
+ DiagramComponent,
2959
+ RoundedRectNodeComponent], exports: [DiagramComponent,
2960
+ RoundedRectNodeComponent] });
2961
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: NgxWorkflowModule, providers: [
2962
+ DiagramStateService,
2963
+ LayoutService,
2964
+ UndoRedoService,
2965
+ {
2966
+ provide: NGX_WORKFLOW_NODE_TYPES,
2967
+ useValue: {
2968
+ 'rounded-rect': RoundedRectNodeComponent,
2969
+ // Add other custom node types here
2970
+ },
2971
+ },
2972
+ ], imports: [CommonModule,
2973
+ DiagramComponent,
2974
+ RoundedRectNodeComponent] });
2975
+ }
2976
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: NgxWorkflowModule, decorators: [{
2977
+ type: NgModule,
2978
+ args: [{
2979
+ imports: [
2980
+ CommonModule,
2981
+ DiagramComponent,
2982
+ RoundedRectNodeComponent, // Import custom node component if it's standalone
2983
+ ],
2984
+ declarations: [
2985
+ // Standalone components are imported, not declared.
2986
+ // If a non-standalone component was needed, it would go here.
2987
+ ],
2988
+ providers: [
2989
+ DiagramStateService,
2990
+ LayoutService,
2991
+ UndoRedoService,
2992
+ {
2993
+ provide: NGX_WORKFLOW_NODE_TYPES,
2994
+ useValue: {
2995
+ 'rounded-rect': RoundedRectNodeComponent,
2996
+ // Add other custom node types here
2997
+ },
2998
+ },
2999
+ ],
3000
+ exports: [
3001
+ DiagramComponent,
3002
+ RoundedRectNodeComponent,
3003
+ ],
3004
+ }]
3005
+ }] });
3006
+
3007
+ /*
3008
+ * Public API Surface of ngx-workflow
3009
+ */
3010
+
3011
+ /**
3012
+ * Generated bundle index. Do not edit.
3013
+ */
3014
+
3015
+ export { DiagramComponent, DiagramStateService, LayoutService, NGX_WORKFLOW_EDGE_TYPES, NGX_WORKFLOW_NODE_TYPES, NgxWorkflowModule, RoundedRectNodeComponent, UndoRedoService, getBezierPath, getPolylineMidpoint, getSelfLoopPath, getSmartEdgePath, getStepPath, getStraightPath };
3016
+ //# sourceMappingURL=ngx-workflow.mjs.map