@tic-nova/ngx-interactive-org-chart 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2047 @@
1
+ import { NgStyle, NgTemplateOutlet, NgClass } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { inject, Injector, ElementRef, input, output, viewChild, signal, computed, effect, Component, afterNextRender, ContentChild } from '@angular/core';
4
+ import createPanZoom from 'panzoom';
5
+ import { trigger, transition, style, animate } from '@angular/animations';
6
+
7
+ function toggleNodeCollapse({ node, targetNode, collapse, }) {
8
+ if (node.id === targetNode) {
9
+ const newCollapse = collapse ?? !node.collapsed;
10
+ return {
11
+ ...node,
12
+ collapsed: newCollapse,
13
+ children: node.children?.map(child => setCollapseRecursively(child, newCollapse)),
14
+ };
15
+ }
16
+ if (node.children?.length) {
17
+ return {
18
+ ...node,
19
+ children: node.children.map(child => toggleNodeCollapse({ node: child, targetNode, collapse })),
20
+ };
21
+ }
22
+ return node;
23
+ }
24
+ function setCollapseRecursively(node, collapse) {
25
+ return {
26
+ ...node,
27
+ collapsed: collapse,
28
+ children: node.children?.map(child => setCollapseRecursively(child, collapse)),
29
+ };
30
+ }
31
+ function mapNodesRecursively(node, collapsed) {
32
+ const mappedChildren = node.children?.map(child => mapNodesRecursively(child, collapsed)) || [];
33
+ const descendantsCount = mappedChildren.reduce((acc, child) => acc + 1 + (child.descendantsCount ?? 0), 0);
34
+ return {
35
+ ...node,
36
+ id: node.id ?? crypto.randomUUID(),
37
+ collapsed: collapsed ?? node.collapsed ?? false,
38
+ hidden: node.hidden ?? false,
39
+ children: mappedChildren,
40
+ descendantsCount,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Essential CSS properties to copy for drag ghost elements.
46
+ */
47
+ const GHOST_ELEMENT_STYLES = [
48
+ 'background',
49
+ 'background-color',
50
+ 'background-image',
51
+ 'background-size',
52
+ 'background-position',
53
+ 'background-repeat',
54
+ 'color',
55
+ 'font-family',
56
+ 'font-size',
57
+ 'font-weight',
58
+ 'line-height',
59
+ 'text-align',
60
+ 'border',
61
+ 'border-radius',
62
+ 'border-color',
63
+ 'border-width',
64
+ 'border-style',
65
+ 'padding',
66
+ 'box-sizing',
67
+ 'display',
68
+ 'flex-direction',
69
+ 'align-items',
70
+ 'justify-content',
71
+ 'gap',
72
+ 'outline',
73
+ 'outline-color',
74
+ 'outline-width',
75
+ ];
76
+ /**
77
+ * Creates a cloned node element with copied styles for drag preview.
78
+ * This removes interactive elements and copies essential visual styles.
79
+ *
80
+ * @param nodeElement - The original node element to clone
81
+ * @returns A cloned element with copied styles
82
+ */
83
+ function cloneNodeWithStyles(nodeElement) {
84
+ // Clone the node deeply
85
+ const clone = nodeElement.cloneNode(true);
86
+ // Remove ID and draggable to avoid conflicts
87
+ clone.removeAttribute('id');
88
+ clone.removeAttribute('draggable');
89
+ // Remove interactive elements
90
+ const collapseBtn = clone.querySelector('.collapse-btn');
91
+ if (collapseBtn)
92
+ collapseBtn.remove();
93
+ const dragHandle = clone.querySelector('.drag-handle');
94
+ if (dragHandle)
95
+ dragHandle.remove();
96
+ // Get computed styles from original
97
+ const computed = window.getComputedStyle(nodeElement);
98
+ // Apply essential visual styles to clone
99
+ GHOST_ELEMENT_STYLES.forEach(prop => {
100
+ const value = computed.getPropertyValue(prop);
101
+ if (value) {
102
+ clone.style.setProperty(prop, value, 'important');
103
+ }
104
+ });
105
+ // Apply styles to nested elements
106
+ const sourceChildren = nodeElement.querySelectorAll('*');
107
+ const cloneChildren = clone.querySelectorAll('*');
108
+ sourceChildren.forEach((sourceChild, index) => {
109
+ if (cloneChildren[index]) {
110
+ const childComputed = window.getComputedStyle(sourceChild);
111
+ const cloneChild = cloneChildren[index];
112
+ GHOST_ELEMENT_STYLES.forEach(prop => {
113
+ const value = childComputed.getPropertyValue(prop);
114
+ if (value) {
115
+ cloneChild.style.setProperty(prop, value, 'important');
116
+ }
117
+ });
118
+ }
119
+ });
120
+ return clone;
121
+ }
122
+ /**
123
+ * Creates a ghost element to follow the touch during drag.
124
+ * The ghost element is wrapped in a positioned container and scaled to match the current zoom level.
125
+ *
126
+ * @param config - Configuration for creating the ghost element
127
+ * @returns The wrapper element and scaled dimensions
128
+ */
129
+ function createTouchDragGhost(config) {
130
+ const { nodeElement, currentScale, touchX, touchY } = config;
131
+ // Get the unscaled dimensions
132
+ const unscaledWidth = nodeElement.offsetWidth;
133
+ const unscaledHeight = nodeElement.offsetHeight;
134
+ // Clone the node with styles using shared helper
135
+ const ghost = cloneNodeWithStyles(nodeElement);
136
+ // Calculate the actual scaled dimensions for positioning
137
+ const scaleFactor = currentScale * 1.05;
138
+ const scaledWidth = unscaledWidth * scaleFactor;
139
+ const scaledHeight = unscaledHeight * scaleFactor;
140
+ // Create wrapper for positioning
141
+ const wrapper = document.createElement('div');
142
+ wrapper.className = 'touch-drag-ghost-wrapper';
143
+ wrapper.style.position = 'fixed';
144
+ wrapper.style.pointerEvents = 'none';
145
+ wrapper.style.zIndex = '10000';
146
+ // Position wrapper so the scaled ghost is centered on the touch point
147
+ wrapper.style.left = touchX - scaledWidth / 2 + 'px';
148
+ wrapper.style.top = touchY - scaledHeight / 2 + 'px';
149
+ // Set ghost to unscaled dimensions, then apply the zoom scale via transform
150
+ ghost.style.setProperty('position', 'relative', 'important');
151
+ ghost.style.setProperty('width', unscaledWidth + 'px', 'important');
152
+ ghost.style.setProperty('height', unscaledHeight + 'px', 'important');
153
+ ghost.style.setProperty('margin', '0', 'important');
154
+ ghost.style.setProperty('opacity', '0.9', 'important');
155
+ ghost.style.setProperty('transform-origin', 'top left', 'important');
156
+ // Apply the current zoom scale to match the visible node, then apply slight scale-up for drag effect
157
+ ghost.style.setProperty('transform', `scale(${scaleFactor})`, 'important');
158
+ ghost.style.setProperty('box-shadow', '0 15px 40px rgba(0, 0, 0, 0.4)', 'important');
159
+ ghost.style.setProperty('cursor', 'grabbing', 'important');
160
+ wrapper.appendChild(ghost);
161
+ document.body.appendChild(wrapper);
162
+ return {
163
+ wrapper,
164
+ scaledWidth,
165
+ scaledHeight,
166
+ };
167
+ }
168
+ /**
169
+ * Updates the position of a touch drag ghost element wrapper.
170
+ *
171
+ * @param wrapper - The ghost wrapper element to reposition
172
+ * @param x - The new x coordinate
173
+ * @param y - The new y coordinate
174
+ * @param scaledWidth - The scaled width of the ghost element
175
+ * @param scaledHeight - The scaled height of the ghost element
176
+ */
177
+ function updateTouchGhostPosition(wrapper, x, y, scaledWidth, scaledHeight) {
178
+ wrapper.style.left = x - scaledWidth / 2 + 'px';
179
+ wrapper.style.top = y - scaledHeight / 2 + 'px';
180
+ }
181
+ /**
182
+ * Creates and sets a custom drag image for desktop drag and drop.
183
+ * This is required for Safari to show a drag preview properly.
184
+ *
185
+ * @param event - The drag event
186
+ * @param nodeElement - The node element being dragged
187
+ * @returns Cleanup function to remove the temporary drag image
188
+ */
189
+ function setDesktopDragImage(event, nodeElement) {
190
+ if (!event.dataTransfer)
191
+ return;
192
+ // Create a styled clone for the drag image
193
+ const dragImage = cloneNodeWithStyles(nodeElement);
194
+ dragImage.style.position = 'absolute';
195
+ dragImage.style.top = '-9999px';
196
+ dragImage.style.left = '-9999px';
197
+ dragImage.style.opacity = '0.8';
198
+ document.body.appendChild(dragImage);
199
+ const rect = nodeElement.getBoundingClientRect();
200
+ const offsetX = event.clientX - rect.left;
201
+ const offsetY = event.clientY - rect.top;
202
+ event.dataTransfer.setDragImage(dragImage, offsetX, offsetY);
203
+ setTimeout(() => {
204
+ if (document.body.contains(dragImage)) {
205
+ document.body.removeChild(dragImage);
206
+ }
207
+ }, 0);
208
+ }
209
+
210
+ const DEFAULT_THEME = {
211
+ background: 'rgba(255, 255, 255, 0.95)',
212
+ borderColor: 'rgba(0, 0, 0, 0.15)',
213
+ borderRadius: '8px',
214
+ shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
215
+ nodeColor: 'rgba(0, 0, 0, 0.6)',
216
+ viewportBackground: 'rgba(59, 130, 246, 0.2)',
217
+ viewportBorderColor: 'rgb(59, 130, 246)',
218
+ viewportBorderWidth: '2px',
219
+ };
220
+ const REDRAW_DEBOUNCE_MS = 100;
221
+ const CONTENT_PADDING_RATIO = 0.9;
222
+ const CSS_VAR_REGEX = /var\((--[^)]+)\)/;
223
+ class MiniMapComponent {
224
+ #injector = inject(Injector);
225
+ #elementRef = inject(ElementRef);
226
+ panZoomInstance = input.required(...(ngDevMode ? [{ debugName: "panZoomInstance" }] : []));
227
+ chartContainer = input.required(...(ngDevMode ? [{ debugName: "chartContainer" }] : []));
228
+ position = input('bottom-right', ...(ngDevMode ? [{ debugName: "position" }] : []));
229
+ width = input(200, ...(ngDevMode ? [{ debugName: "width" }] : []));
230
+ height = input(150, ...(ngDevMode ? [{ debugName: "height" }] : []));
231
+ visible = input(true, ...(ngDevMode ? [{ debugName: "visible" }] : []));
232
+ themeOptions = input(...(ngDevMode ? [undefined, { debugName: "themeOptions" }] : []));
233
+ navigate = output();
234
+ canvasRef = viewChild('miniMapCanvas', ...(ngDevMode ? [{ debugName: "canvasRef" }] : []));
235
+ viewportRef = viewChild('viewport', ...(ngDevMode ? [{ debugName: "viewportRef" }] : []));
236
+ viewportStyle = signal({}, ...(ngDevMode ? [{ debugName: "viewportStyle" }] : []));
237
+ miniMapStyle = computed(() => {
238
+ const theme = this.themeOptions();
239
+ return {
240
+ width: `${this.width()}px`,
241
+ height: `${this.height()}px`,
242
+ backgroundColor: theme?.background ?? DEFAULT_THEME.background,
243
+ borderColor: theme?.borderColor ?? DEFAULT_THEME.borderColor,
244
+ borderRadius: theme?.borderRadius ?? DEFAULT_THEME.borderRadius,
245
+ boxShadow: theme?.shadow ?? DEFAULT_THEME.shadow,
246
+ };
247
+ }, ...(ngDevMode ? [{ debugName: "miniMapStyle" }] : []));
248
+ viewportIndicatorStyle = computed(() => {
249
+ const theme = this.themeOptions();
250
+ return {
251
+ ...this.viewportStyle(),
252
+ backgroundColor: theme?.viewportBackground ?? DEFAULT_THEME.viewportBackground,
253
+ borderColor: theme?.viewportBorderColor ?? DEFAULT_THEME.viewportBorderColor,
254
+ borderWidth: theme?.viewportBorderWidth ?? DEFAULT_THEME.viewportBorderWidth,
255
+ };
256
+ }, ...(ngDevMode ? [{ debugName: "viewportIndicatorStyle" }] : []));
257
+ nodeColor = computed(() => this.themeOptions()?.nodeColor ?? DEFAULT_THEME.nodeColor, ...(ngDevMode ? [{ debugName: "nodeColor" }] : []));
258
+ #animationFrameId = null;
259
+ #isDragging = false;
260
+ #mutationObserver = null;
261
+ #themeObserver = null;
262
+ #redrawTimeout = null;
263
+ constructor() {
264
+ this.#initializeEffects();
265
+ this.#setupThemeObserver();
266
+ }
267
+ ngOnDestroy() {
268
+ this.#cleanup();
269
+ }
270
+ onMouseDown(event) {
271
+ event.preventDefault();
272
+ this.#isDragging = true;
273
+ const handleMouseMove = (e) => {
274
+ if (this.#isDragging) {
275
+ this.#navigateToPoint(e);
276
+ }
277
+ };
278
+ const handleMouseUp = () => {
279
+ this.#isDragging = false;
280
+ document.removeEventListener('mousemove', handleMouseMove);
281
+ document.removeEventListener('mouseup', handleMouseUp);
282
+ };
283
+ document.addEventListener('mousemove', handleMouseMove);
284
+ document.addEventListener('mouseup', handleMouseUp);
285
+ }
286
+ #initializeEffects() {
287
+ effect(() => {
288
+ const panZoom = this.panZoomInstance();
289
+ panZoom ? this.#startTracking() : this.#stopTracking();
290
+ }, { injector: this.#injector });
291
+ effect(() => {
292
+ const container = this.chartContainer();
293
+ if (container) {
294
+ this.#setupMutationObserver(container);
295
+ this.#drawMiniMap();
296
+ }
297
+ else {
298
+ this.#disconnectMutationObserver();
299
+ }
300
+ }, { injector: this.#injector });
301
+ effect(() => {
302
+ this.themeOptions();
303
+ this.#scheduleRedraw();
304
+ }, { injector: this.#injector });
305
+ }
306
+ #setupThemeObserver() {
307
+ this.#themeObserver = new MutationObserver(() => this.#scheduleRedraw());
308
+ [document.documentElement, document.body].forEach(target => {
309
+ this.#themeObserver.observe(target, {
310
+ attributes: true,
311
+ attributeFilter: ['class', 'data-theme', 'data-mode'],
312
+ });
313
+ });
314
+ }
315
+ #disconnectThemeObserver() {
316
+ this.#themeObserver?.disconnect();
317
+ this.#themeObserver = null;
318
+ }
319
+ #setupMutationObserver(container) {
320
+ this.#disconnectMutationObserver();
321
+ this.#mutationObserver = new MutationObserver(() => this.#scheduleRedraw());
322
+ this.#mutationObserver.observe(container, {
323
+ childList: true,
324
+ subtree: true,
325
+ attributes: true,
326
+ attributeFilter: ['class', 'style'],
327
+ });
328
+ }
329
+ #disconnectMutationObserver() {
330
+ this.#mutationObserver?.disconnect();
331
+ this.#mutationObserver = null;
332
+ }
333
+ #scheduleRedraw() {
334
+ if (this.#redrawTimeout !== null) {
335
+ clearTimeout(this.#redrawTimeout);
336
+ }
337
+ this.#redrawTimeout = setTimeout(() => {
338
+ this.#drawMiniMap();
339
+ this.#redrawTimeout = null;
340
+ }, REDRAW_DEBOUNCE_MS);
341
+ }
342
+ #startTracking() {
343
+ if (this.#animationFrameId !== null)
344
+ return;
345
+ const update = () => {
346
+ this.#updateViewport();
347
+ this.#animationFrameId = requestAnimationFrame(update);
348
+ };
349
+ this.#animationFrameId = requestAnimationFrame(update);
350
+ }
351
+ #stopTracking() {
352
+ if (this.#animationFrameId !== null) {
353
+ cancelAnimationFrame(this.#animationFrameId);
354
+ this.#animationFrameId = null;
355
+ }
356
+ }
357
+ #drawMiniMap() {
358
+ const canvas = this.canvasRef()?.nativeElement;
359
+ const container = this.chartContainer();
360
+ if (!canvas || !container)
361
+ return;
362
+ const ctx = canvas.getContext('2d');
363
+ if (!ctx)
364
+ return;
365
+ canvas.width = this.width();
366
+ canvas.height = this.height();
367
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
368
+ const contentBounds = this.#getContentBounds(container);
369
+ if (!contentBounds)
370
+ return;
371
+ const { scale, offsetX, offsetY } = this.#calculateMiniMapTransform(canvas, contentBounds);
372
+ const transform = this.#getCurrentTransform();
373
+ this.#drawNodes(ctx, container, contentBounds, scale, offsetX, offsetY, transform);
374
+ }
375
+ #getContentBounds(container) {
376
+ const nodes = container.querySelectorAll('.node-content');
377
+ if (nodes.length === 0)
378
+ return null;
379
+ const transform = this.#getCurrentTransform();
380
+ const containerRect = container.getBoundingClientRect();
381
+ let minX = Infinity;
382
+ let minY = Infinity;
383
+ let maxX = -Infinity;
384
+ let maxY = -Infinity;
385
+ nodes.forEach(node => {
386
+ const rect = node.getBoundingClientRect();
387
+ const relX = (rect.left - containerRect.left - transform.x) / transform.scale;
388
+ const relY = (rect.top - containerRect.top - transform.y) / transform.scale;
389
+ const width = rect.width / transform.scale;
390
+ const height = rect.height / transform.scale;
391
+ minX = Math.min(minX, relX);
392
+ minY = Math.min(minY, relY);
393
+ maxX = Math.max(maxX, relX + width);
394
+ maxY = Math.max(maxY, relY + height);
395
+ });
396
+ return {
397
+ left: minX,
398
+ top: minY,
399
+ width: maxX - minX,
400
+ height: maxY - minY,
401
+ };
402
+ }
403
+ #calculateMiniMapTransform(canvas, contentBounds) {
404
+ const scaleX = canvas.width / contentBounds.width;
405
+ const scaleY = canvas.height / contentBounds.height;
406
+ const scale = Math.min(scaleX, scaleY) * CONTENT_PADDING_RATIO;
407
+ const offsetX = (canvas.width - contentBounds.width * scale) / 2;
408
+ const offsetY = (canvas.height - contentBounds.height * scale) / 2;
409
+ return { scale, offsetX, offsetY };
410
+ }
411
+ #drawNodes(ctx, container, contentBounds, scale, offsetX, offsetY, transform) {
412
+ const resolvedNodeColor = this.#resolveColor(this.nodeColor());
413
+ ctx.fillStyle = resolvedNodeColor;
414
+ ctx.strokeStyle = resolvedNodeColor;
415
+ ctx.lineWidth = 1;
416
+ const nodes = container.querySelectorAll('.node-content');
417
+ const containerRect = container.getBoundingClientRect();
418
+ nodes.forEach(node => {
419
+ const rect = node.getBoundingClientRect();
420
+ const relX = (rect.left - containerRect.left - transform.x) / transform.scale;
421
+ const relY = (rect.top - containerRect.top - transform.y) / transform.scale;
422
+ const w = rect.width / transform.scale;
423
+ const h = rect.height / transform.scale;
424
+ const x = (relX - contentBounds.left) * scale + offsetX;
425
+ const y = (relY - contentBounds.top) * scale + offsetY;
426
+ const scaledW = w * scale;
427
+ const scaledH = h * scale;
428
+ ctx.fillRect(x, y, scaledW, scaledH);
429
+ });
430
+ }
431
+ #updateViewport() {
432
+ const panZoom = this.panZoomInstance();
433
+ const container = this.chartContainer();
434
+ const canvas = this.canvasRef()?.nativeElement;
435
+ if (!panZoom || !container || !canvas)
436
+ return;
437
+ const transform = panZoom.getTransform();
438
+ const contentBounds = this.#getContentBounds(container);
439
+ if (!contentBounds)
440
+ return;
441
+ const { scale, offsetX, offsetY } = this.#calculateMiniMapTransform(canvas, contentBounds);
442
+ const containerRect = container.getBoundingClientRect();
443
+ const viewportWidth = containerRect.width / transform.scale;
444
+ const viewportHeight = containerRect.height / transform.scale;
445
+ const viewportX = -transform.x / transform.scale - contentBounds.left;
446
+ const viewportY = -transform.y / transform.scale - contentBounds.top;
447
+ const miniViewportX = viewportX * scale + offsetX;
448
+ const miniViewportY = viewportY * scale + offsetY;
449
+ const miniViewportWidth = viewportWidth * scale;
450
+ const miniViewportHeight = viewportHeight * scale;
451
+ this.viewportStyle.set({
452
+ left: `${miniViewportX}px`,
453
+ top: `${miniViewportY}px`,
454
+ width: `${miniViewportWidth}px`,
455
+ height: `${miniViewportHeight}px`,
456
+ });
457
+ }
458
+ #navigateToPoint(event) {
459
+ const canvas = this.canvasRef()?.nativeElement;
460
+ const container = this.chartContainer();
461
+ const panZoom = this.panZoomInstance();
462
+ if (!canvas || !container || !panZoom)
463
+ return;
464
+ const rect = canvas.getBoundingClientRect();
465
+ const clickX = event.clientX - rect.left;
466
+ const clickY = event.clientY - rect.top;
467
+ const contentBounds = this.#getContentBounds(container);
468
+ if (!contentBounds)
469
+ return;
470
+ const { scale, offsetX, offsetY } = this.#calculateMiniMapTransform(canvas, contentBounds);
471
+ const contentX = (clickX - offsetX) / scale + contentBounds.left;
472
+ const contentY = (clickY - offsetY) / scale + contentBounds.top;
473
+ const transform = panZoom.getTransform();
474
+ const containerRect = container.getBoundingClientRect();
475
+ const newX = -(contentX * transform.scale - containerRect.width / 2);
476
+ const newY = -(contentY * transform.scale - containerRect.height / 2);
477
+ panZoom.moveTo(newX, newY);
478
+ this.navigate.emit({ x: newX, y: newY });
479
+ }
480
+ #resolveColor(color) {
481
+ if (!color.includes('var('))
482
+ return color;
483
+ const computedStyle = getComputedStyle(this.#elementRef.nativeElement);
484
+ const match = color.match(CSS_VAR_REGEX);
485
+ if (match) {
486
+ const propertyName = match[1];
487
+ const resolvedColor = computedStyle.getPropertyValue(propertyName).trim();
488
+ return resolvedColor || color;
489
+ }
490
+ return color;
491
+ }
492
+ #getCurrentTransform() {
493
+ const panZoom = this.panZoomInstance();
494
+ return panZoom?.getTransform() ?? { scale: 1, x: 0, y: 0 };
495
+ }
496
+ #cleanup() {
497
+ this.#stopTracking();
498
+ this.#disconnectMutationObserver();
499
+ this.#disconnectThemeObserver();
500
+ if (this.#redrawTimeout !== null) {
501
+ clearTimeout(this.#redrawTimeout);
502
+ this.#redrawTimeout = null;
503
+ }
504
+ }
505
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MiniMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
506
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: MiniMapComponent, isStandalone: true, selector: "ngx-org-chart-mini-map", inputs: { panZoomInstance: { classPropertyName: "panZoomInstance", publicName: "panZoomInstance", isSignal: true, isRequired: true, transformFunction: null }, chartContainer: { classPropertyName: "chartContainer", publicName: "chartContainer", isSignal: true, isRequired: true, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, themeOptions: { classPropertyName: "themeOptions", publicName: "themeOptions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { navigate: "navigate" }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["miniMapCanvas"], descendants: true, isSignal: true }, { propertyName: "viewportRef", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (visible()) {\n <div\n class=\"mini-map-container\"\n [class]=\"'position-' + position()\"\n [ngStyle]=\"miniMapStyle()\"\n (mousedown)=\"onMouseDown($event)\"\n >\n <canvas #miniMapCanvas class=\"mini-map-canvas\"></canvas>\n <div #viewport class=\"viewport-indicator\" [ngStyle]=\"viewportIndicatorStyle()\"></div>\n </div>\n}\n", styles: [":host{display:contents}.mini-map-container{position:absolute;z-index:1000;border:2px solid;border-radius:8px;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;cursor:pointer;-webkit-user-select:none;user-select:none;overflow:hidden;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transition:opacity .3s ease}.mini-map-container:hover{opacity:1!important}.mini-map-container.position-top-left{top:16px;left:16px}.mini-map-container.position-top-right{top:16px;right:16px}.mini-map-container.position-bottom-left{bottom:16px;left:16px}.mini-map-container.position-bottom-right{bottom:16px;right:16px}.mini-map-canvas{display:block;width:100%;height:100%}.viewport-indicator{position:absolute;border:2px solid;pointer-events:none;border-radius:2px;transition:all .1s ease-out;box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
507
+ }
508
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MiniMapComponent, decorators: [{
509
+ type: Component,
510
+ args: [{ selector: 'ngx-org-chart-mini-map', standalone: true, imports: [NgStyle], template: "@if (visible()) {\n <div\n class=\"mini-map-container\"\n [class]=\"'position-' + position()\"\n [ngStyle]=\"miniMapStyle()\"\n (mousedown)=\"onMouseDown($event)\"\n >\n <canvas #miniMapCanvas class=\"mini-map-canvas\"></canvas>\n <div #viewport class=\"viewport-indicator\" [ngStyle]=\"viewportIndicatorStyle()\"></div>\n </div>\n}\n", styles: [":host{display:contents}.mini-map-container{position:absolute;z-index:1000;border:2px solid;border-radius:8px;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;cursor:pointer;-webkit-user-select:none;user-select:none;overflow:hidden;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transition:opacity .3s ease}.mini-map-container:hover{opacity:1!important}.mini-map-container.position-top-left{top:16px;left:16px}.mini-map-container.position-top-right{top:16px;right:16px}.mini-map-container.position-bottom-left{bottom:16px;left:16px}.mini-map-container.position-bottom-right{bottom:16px;right:16px}.mini-map-canvas{display:block;width:100%;height:100%}.viewport-indicator{position:absolute;border:2px solid;pointer-events:none;border-radius:2px;transition:all .1s ease-out;box-sizing:border-box}\n"] }]
511
+ }], ctorParameters: () => [], propDecorators: { panZoomInstance: [{ type: i0.Input, args: [{ isSignal: true, alias: "panZoomInstance", required: true }] }], chartContainer: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartContainer", required: true }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], themeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "themeOptions", required: false }] }], navigate: [{ type: i0.Output, args: ["navigate"] }], canvasRef: [{ type: i0.ViewChild, args: ['miniMapCanvas', { isSignal: true }] }], viewportRef: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }] } });
512
+
513
+ const DEFAULT_THEME_OPTIONS = {
514
+ node: {
515
+ background: '#ffffff',
516
+ color: '#4a4a4a',
517
+ activeOutlineColor: '#3b82f6',
518
+ outlineWidth: '2px',
519
+ shadow: '0 2px 10px rgba(0, 0, 0, 0.1)',
520
+ outlineColor: '#d1d5db',
521
+ highlightShadowColor: 'rgba(121, 59, 246, 0)',
522
+ padding: '12px 16px',
523
+ borderRadius: '8px',
524
+ containerSpacing: '20px',
525
+ activeColor: '#3b82f6',
526
+ maxWidth: 'auto',
527
+ minWidth: 'auto',
528
+ maxHeight: 'auto',
529
+ minHeight: 'auto',
530
+ dragOverOutlineColor: '#3b82f6',
531
+ },
532
+ connector: {
533
+ color: '#d1d5db',
534
+ activeColor: '#3b82f6',
535
+ borderRadius: '10px',
536
+ width: '1.5px',
537
+ },
538
+ collapseButton: {
539
+ size: '20px',
540
+ borderColor: '#d1d5db',
541
+ borderRadius: '0.25rem',
542
+ color: '#4a4a4a',
543
+ background: '#ffffff',
544
+ hoverColor: '#3b82f6',
545
+ hoverBackground: '#f3f4f6',
546
+ hoverShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
547
+ hoverTransformScale: '1.05',
548
+ focusOutline: '2px solid #3b82f6',
549
+ countFontSize: '0.75rem',
550
+ },
551
+ container: {
552
+ background: 'transparent',
553
+ border: 'none',
554
+ },
555
+ miniMap: {
556
+ background: 'rgba(255, 255, 255, 0.95)',
557
+ borderColor: 'rgba(0, 0, 0, 0.15)',
558
+ borderRadius: '8px',
559
+ shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
560
+ nodeColor: 'rgba(0, 0, 0, 0.6)',
561
+ viewportBackground: 'rgba(59, 130, 246, 0.2)',
562
+ viewportBorderColor: 'rgb(59, 130, 246)',
563
+ viewportBorderWidth: '2px',
564
+ },
565
+ };
566
+
567
+ // Constants
568
+ const RESET_DELAY = 300; // ms
569
+ const TOUCH_DRAG_THRESHOLD = 10; // pixels
570
+ const AUTO_PAN_EDGE_THRESHOLD = 0.1; // 10% of container dimensions
571
+ const AUTO_PAN_SPEED = 15; // pixels per frame
572
+ class NgxInteractiveOrgChart {
573
+ #elementRef = inject(ElementRef);
574
+ #injector = inject(Injector);
575
+ /**
576
+ * Optional template for a custom node.
577
+ * When provided, this template will be used to render each node in the org chart.
578
+ *
579
+ * @remarks
580
+ * The template context includes:
581
+ * - `$implicit`: The node data
582
+ * - `node`: The node data (alternative accessor)
583
+ *
584
+ * @example
585
+ * ```html
586
+ * <ngx-interactive-org-chart>
587
+ * <ng-template #nodeTemplate let-node="node">
588
+ * <div class="custom-node">
589
+ * <h3>{{ node.data?.name }}</h3>
590
+ * <p>{{ node.data?.age }}</p>
591
+ * </div>
592
+ * </ng-template>
593
+ * </ngx-interactive-org-chart>
594
+ * ```
595
+ */
596
+ customNodeTemplate;
597
+ /**
598
+ * Optional template for a custom drag handle.
599
+ * When provided, only this element will be draggable instead of the entire node.
600
+ *
601
+ * @remarks
602
+ * The template context includes:
603
+ * - `$implicit`: The node data
604
+ * - `node`: The node data (alternative accessor)
605
+ *
606
+ * @example
607
+ * ```html
608
+ * <ngx-interactive-org-chart [draggable]="true">
609
+ * <ng-template #dragHandleTemplate let-node="node">
610
+ * <button class="drag-handle" title="Drag to move">
611
+ * <svg><!-- Drag icon --></svg>
612
+ * </button>
613
+ * </ng-template>
614
+ * </ngx-interactive-org-chart>
615
+ * ```
616
+ */
617
+ customDragHandleTemplate;
618
+ panZoomContainer = viewChild.required('panZoomContainer');
619
+ orgChartContainer = viewChild.required('orgChartContainer');
620
+ container = viewChild.required('container');
621
+ /**
622
+ * The data for the org chart.
623
+ */
624
+ data = input.required(...(ngDevMode ? [{ debugName: "data" }] : []));
625
+ /**
626
+ * The initial zoom level for the chart.
627
+ */
628
+ initialZoom = input(...(ngDevMode ? [undefined, { debugName: "initialZoom" }] : []));
629
+ /**
630
+ * The minimum zoom level for the chart
631
+ */
632
+ minZoom = input(0.1, ...(ngDevMode ? [{ debugName: "minZoom" }] : []));
633
+ /**
634
+ * The maximum zoom level for the chart.
635
+ */
636
+ maxZoom = input(5, ...(ngDevMode ? [{ debugName: "maxZoom" }] : []));
637
+ /**
638
+ * The speed at which the chart zooms in/out on wheel or pinch.
639
+ */
640
+ zoomSpeed = input(1, ...(ngDevMode ? [{ debugName: "zoomSpeed" }] : []));
641
+ /**
642
+ * The speed at which the chart zooms in/out on double-click.
643
+ */
644
+ zoomDoubleClickSpeed = input(2, ...(ngDevMode ? [{ debugName: "zoomDoubleClickSpeed" }] : []));
645
+ /**
646
+ * Whether the nodes can be collapsed/expanded.
647
+ */
648
+ collapsible = input(true, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
649
+ /**
650
+ * The CSS class to apply to each node element.
651
+ */
652
+ nodeClass = input(...(ngDevMode ? [undefined, { debugName: "nodeClass" }] : []));
653
+ /**
654
+ * If set to `true`, all the nodes will be initially collapsed.
655
+ */
656
+ initialCollapsed = input(...(ngDevMode ? [undefined, { debugName: "initialCollapsed" }] : []));
657
+ /**
658
+ * Whether to enable RTL (right-to-left) layout.
659
+ */
660
+ isRtl = input(...(ngDevMode ? [undefined, { debugName: "isRtl" }] : []));
661
+ /**
662
+ * The layout direction of the org chart tree.
663
+ * - 'vertical': Traditional top-to-bottom tree layout
664
+ * - 'horizontal': Left-to-right tree layout
665
+ */
666
+ layout = input('vertical', ...(ngDevMode ? [{ debugName: "layout" }] : []));
667
+ /**
668
+ * Whether to focus on the node when it is collapsed and focus on its children when it is expanded.
669
+ */
670
+ focusOnCollapseOrExpand = input(false, ...(ngDevMode ? [{ debugName: "focusOnCollapseOrExpand" }] : []));
671
+ /**
672
+ * Whether to display the count of children for each node on expand/collapse button.
673
+ */
674
+ displayChildrenCount = input(true, ...(ngDevMode ? [{ debugName: "displayChildrenCount" }] : []));
675
+ /**
676
+ * The ratio of the node's width to the viewport's width when highlighting.
677
+ */
678
+ highlightZoomNodeWidthRatio = input(0.3, ...(ngDevMode ? [{ debugName: "highlightZoomNodeWidthRatio" }] : []));
679
+ /**
680
+ * The ratio of the node's height to the viewport's height when highlighting.
681
+ */
682
+ highlightZoomNodeHeightRatio = input(0.4, ...(ngDevMode ? [{ debugName: "highlightZoomNodeHeightRatio" }] : []));
683
+ /**
684
+ * Whether to enable drag and drop functionality for nodes.
685
+ * When enabled, nodes can be dragged and dropped onto other nodes.
686
+ *
687
+ * @remarks
688
+ * By default, the entire node is draggable. To use a custom drag handle instead,
689
+ * provide a `dragHandleTemplate` template with the selector `#dragHandleTemplate`.
690
+ *
691
+ * @example
692
+ * ```html
693
+ * <ngx-interactive-org-chart [draggable]="true">
694
+ * <!-- Custom drag handle template -->
695
+ * <ng-template #dragHandleTemplate let-node="node">
696
+ * <span class="drag-handle">⋮⋮</span>
697
+ * </ng-template>
698
+ * </ngx-interactive-org-chart>
699
+ * ```
700
+ */
701
+ draggable = input(false, ...(ngDevMode ? [{ debugName: "draggable" }] : []));
702
+ /**
703
+ * Predicate function to determine if a specific node can be dragged.
704
+ *
705
+ * @param node - The node to check
706
+ * @returns true if the node can be dragged, false otherwise
707
+ *
708
+ * @example
709
+ * ```typescript
710
+ * // Prevent CEO node from being dragged
711
+ * canDragNode = (node: OrgChartNode) => node.data?.role !== 'CEO';
712
+ * ```
713
+ */
714
+ canDragNode = input(...(ngDevMode ? [undefined, { debugName: "canDragNode" }] : []));
715
+ /**
716
+ * Predicate function to determine if a node can accept drops.
717
+ *
718
+ * @param draggedNode - The node being dragged
719
+ * @param targetNode - The potential drop target
720
+ * @returns true if the drop is allowed, false otherwise
721
+ *
722
+ * @example
723
+ * ```typescript
724
+ * // Don't allow employees to have subordinates
725
+ * canDropNode = (dragged: OrgChartNode, target: OrgChartNode) => {
726
+ * return target.data?.type !== 'Employee';
727
+ * };
728
+ * ```
729
+ */
730
+ canDropNode = input(...(ngDevMode ? [undefined, { debugName: "canDropNode" }] : []));
731
+ /**
732
+ * The distance in pixels from the viewport edge to trigger auto-panning during drag.
733
+ * The threshold is calculated automatically as 10% of the container dimensions for better responsiveness across different screen sizes.
734
+ * @default 0.1
735
+ */
736
+ dragEdgeThreshold = input(AUTO_PAN_EDGE_THRESHOLD, ...(ngDevMode ? [{ debugName: "dragEdgeThreshold" }] : []));
737
+ /**
738
+ * The speed of auto-panning in pixels per frame during drag.
739
+ * @default 15
740
+ */
741
+ dragAutoPanSpeed = input(AUTO_PAN_SPEED, ...(ngDevMode ? [{ debugName: "dragAutoPanSpeed" }] : []));
742
+ /**
743
+ * The minimum zoom level for the chart when highlighting a node.
744
+ */
745
+ highlightZoomMinimum = input(0.8, ...(ngDevMode ? [{ debugName: "highlightZoomMinimum" }] : []));
746
+ /**
747
+ * The theme options for the org chart.
748
+ * This allows customization of the chart's appearance, including node styles, connector styles, and
749
+ * other visual elements.
750
+ */
751
+ themeOptions = input(...(ngDevMode ? [undefined, { debugName: "themeOptions" }] : []));
752
+ /**
753
+ * Whether to show the mini map navigation tool.
754
+ * @default false
755
+ */
756
+ showMiniMap = input(false, ...(ngDevMode ? [{ debugName: "showMiniMap" }] : []));
757
+ /**
758
+ * Position of the mini map on the screen.
759
+ * @default 'bottom-right'
760
+ */
761
+ miniMapPosition = input('bottom-right', ...(ngDevMode ? [{ debugName: "miniMapPosition" }] : []));
762
+ /**
763
+ * Width of the mini map in pixels.
764
+ * @default 200
765
+ */
766
+ miniMapWidth = input(200, ...(ngDevMode ? [{ debugName: "miniMapWidth" }] : []));
767
+ /**
768
+ * Height of the mini map in pixels.
769
+ * @default 150
770
+ */
771
+ miniMapHeight = input(150, ...(ngDevMode ? [{ debugName: "miniMapHeight" }] : []));
772
+ /**
773
+ * Event emitted when a node is dropped onto another node.
774
+ * Provides the dragged node and the target node.
775
+ */
776
+ nodeDrop = output();
777
+ /**
778
+ * Event emitted when a node drag operation starts.
779
+ */
780
+ nodeDragStart = output();
781
+ /**
782
+ * Event emitted when a node drag operation ends.
783
+ */
784
+ nodeDragEnd = output();
785
+ defaultThemeOptions = DEFAULT_THEME_OPTIONS;
786
+ finalThemeOptions = computed(() => {
787
+ const themeOptions = this.themeOptions();
788
+ return {
789
+ node: {
790
+ ...this.defaultThemeOptions.node,
791
+ ...themeOptions?.node,
792
+ },
793
+ connector: {
794
+ ...this.defaultThemeOptions.connector,
795
+ ...themeOptions?.connector,
796
+ },
797
+ collapseButton: {
798
+ ...this.defaultThemeOptions.collapseButton,
799
+ ...themeOptions?.collapseButton,
800
+ },
801
+ container: {
802
+ ...this.defaultThemeOptions.container,
803
+ ...themeOptions?.container,
804
+ },
805
+ miniMap: {
806
+ ...this.defaultThemeOptions.miniMap,
807
+ ...themeOptions?.miniMap,
808
+ },
809
+ };
810
+ }, ...(ngDevMode ? [{ debugName: "finalThemeOptions" }] : []));
811
+ nodes = signal(null, ...(ngDevMode ? [{ debugName: "nodes" }] : []));
812
+ scale = signal(0, ...(ngDevMode ? [{ debugName: "scale" }] : []));
813
+ draggedNode = signal(null, ...(ngDevMode ? [{ debugName: "draggedNode" }] : []));
814
+ dragOverNode = signal(null, ...(ngDevMode ? [{ debugName: "dragOverNode" }] : []));
815
+ currentDragOverElement = signal(null, ...(ngDevMode ? [{ debugName: "currentDragOverElement" }] : []));
816
+ autoPanInterval = null;
817
+ keyboardListener = null;
818
+ touchDragState = {
819
+ active: false,
820
+ node: null,
821
+ startX: 0,
822
+ startY: 0,
823
+ currentX: 0,
824
+ currentY: 0,
825
+ dragThreshold: TOUCH_DRAG_THRESHOLD,
826
+ ghostElement: null,
827
+ ghostScaledWidth: 0,
828
+ ghostScaledHeight: 0,
829
+ };
830
+ panZoomInstance = null;
831
+ containerElement = computed(() => {
832
+ return this.container()?.nativeElement || null;
833
+ }, ...(ngDevMode ? [{ debugName: "containerElement" }] : []));
834
+ /**
835
+ * A computed property that returns the current scale of the org chart.
836
+ * @returns {number} The current scale of the org chart.
837
+ */
838
+ getScale = computed(() => this.scale(), ...(ngDevMode ? [{ debugName: "getScale" }] : []));
839
+ /**
840
+ * A computed property that flattens the org chart nodes into a single array.
841
+ * It recursively traverses the nodes and their children, returning a flat array of OrgChartNode<T>.
842
+ * This is useful for operations that require a single list of all nodes, such as searching or displaying all nodes in a list.
843
+ * @returns {OrgChartNode<T>[]} An array of all nodes in the org chart, flattened from the hierarchical structure.
844
+ */
845
+ flattenedNodes = computed(() => {
846
+ const nodes = this.nodes();
847
+ if (!nodes)
848
+ return [];
849
+ const flatten = (node) => {
850
+ const children = node.children?.flatMap(flatten) ||
851
+ [];
852
+ return [node, ...children];
853
+ };
854
+ return nodes ? flatten(nodes) : [];
855
+ }, ...(ngDevMode ? [{ debugName: "flattenedNodes" }] : []));
856
+ setNodes = effect(() => {
857
+ const data = this.data();
858
+ const initialCollapsed = this.initialCollapsed();
859
+ if (data) {
860
+ this.nodes.set(mapNodesRecursively(data, initialCollapsed));
861
+ }
862
+ }, ...(ngDevMode ? [{ debugName: "setNodes" }] : []));
863
+ ngAfterViewInit() {
864
+ this.initiatePanZoom();
865
+ this.disableChildDragging();
866
+ }
867
+ /**
868
+ * Initializes the pan-zoom functionality for the org chart.
869
+ * This method creates a new panZoom instance and sets it up with the container element.
870
+ * It also ensures that any existing panZoom instance is disposed of before creating a new one.
871
+ */
872
+ initiatePanZoom() {
873
+ if (this.panZoomInstance) {
874
+ this.panZoomInstance.dispose();
875
+ }
876
+ const container = this.panZoomContainer()?.nativeElement;
877
+ this.panZoomInstance = createPanZoom(container, {
878
+ initialZoom: this.getFitScale(),
879
+ initialX: container.offsetWidth / 2,
880
+ initialY: container.offsetHeight / 2,
881
+ enableTextSelection: false,
882
+ minZoom: this.minZoom(),
883
+ maxZoom: this.maxZoom(),
884
+ zoomSpeed: this.zoomSpeed(),
885
+ smoothScroll: true,
886
+ zoomDoubleClickSpeed: this.zoomDoubleClickSpeed(),
887
+ });
888
+ this.calculateScale();
889
+ this.panZoomInstance?.on('zoom', e => {
890
+ this.calculateScale();
891
+ });
892
+ }
893
+ /**
894
+ * Zooms in of the org chart.
895
+ * @param {Object} options - Options for zooming.
896
+ * @param {number} [options.by=10] - The percentage to zoom in or out by.
897
+ * @param {boolean} [options.relative=true] - Whether to zoom relative to the current zoom level.
898
+ * If true, zooms in by a percentage of the current zoom level.
899
+ * If false, zooms to an absolute scale.
900
+ */
901
+ zoomIn({ by, relative } = { relative: true }) {
902
+ this.zoom({ type: 'in', by, relative });
903
+ }
904
+ /**
905
+ * Zooms out of the org chart.
906
+ * @param {Object} options - Options for zooming.
907
+ * @param {number} [options.by=10] - The percentage to zoom in or out by.
908
+ * @param {boolean} [options.relative=true] - Whether to zoom relative to the current zoom level.
909
+ * If true, zooms out by a percentage of the current zoom level.
910
+ * If false, zooms to an absolute scale.
911
+ */
912
+ zoomOut({ by, relative } = { relative: true }) {
913
+ this.zoom({ type: 'out', by, relative });
914
+ }
915
+ /**
916
+ * Highlights a specific node in the org chart and pans to it.
917
+ * @param {string} nodeId - The ID of the node to highlight.
918
+ */
919
+ highlightNode(nodeId) {
920
+ this.toggleCollapseAll(false);
921
+ setTimeout(() => {
922
+ const nodeElement = this.#elementRef?.nativeElement.querySelector(`#${this.getNodeId(nodeId)}`);
923
+ this.panZoomToNode({
924
+ nodeElement,
925
+ });
926
+ }, 200);
927
+ }
928
+ /**
929
+ * Pans the view of the org chart.
930
+ * @param x The horizontal offset to pan to.
931
+ * @param y The vertical offset to pan to.
932
+ * @param smooth Whether to animate the panning.
933
+ * @returns void
934
+ */
935
+ pan(x, y, smooth) {
936
+ const container = this.orgChartContainer()?.nativeElement;
937
+ if (!container || !this.panZoomInstance) {
938
+ return;
939
+ }
940
+ const containerRect = container.getBoundingClientRect();
941
+ const panZoomRect = this.panZoomContainer()?.nativeElement.getBoundingClientRect();
942
+ const transformedX = x - containerRect.x + panZoomRect.x;
943
+ const transformedY = y - containerRect.y + panZoomRect.y;
944
+ if (smooth) {
945
+ this.panZoomInstance.smoothMoveTo(transformedX, transformedY);
946
+ }
947
+ else {
948
+ this.panZoomInstance.moveTo(transformedX, transformedY);
949
+ }
950
+ }
951
+ /**
952
+ * Resets the pan position of the org chart to center it horizontally and vertically.
953
+ * This method calculates the center position based on the container's dimensions
954
+ * and the hosting element's dimensions, then moves the panZoom instance to that position.
955
+ */
956
+ resetPan() {
957
+ const container = this.panZoomContainer()?.nativeElement;
958
+ if (!container || !this.panZoomInstance) {
959
+ return;
960
+ }
961
+ const containerRect = container.getBoundingClientRect();
962
+ const hostingElement = this.#elementRef.nativeElement;
963
+ const windowWidth = hostingElement.getBoundingClientRect().width;
964
+ const windowHeight = hostingElement.getBoundingClientRect().height;
965
+ let x = (-1 * containerRect.width) / 2 + windowWidth / 2;
966
+ if (this.layout() === 'horizontal') {
967
+ if (this.isRtl()) {
968
+ x = windowWidth - containerRect.width;
969
+ }
970
+ else {
971
+ x = 0;
972
+ }
973
+ }
974
+ const y = (-1 * containerRect.height) / 2 + windowHeight / 2;
975
+ this.panZoomInstance?.smoothMoveTo(x, y);
976
+ }
977
+ /**
978
+ * Resets the zoom level of the org chart to fit the content within the container.
979
+ * This method calculates the optimal scale to fit the content and applies it.
980
+ * @param {number} [padding=20] - Optional padding around the content when calculating the fit scale.
981
+ */
982
+ resetZoom(padding = 20) {
983
+ if (!this.panZoomInstance) {
984
+ return;
985
+ }
986
+ const container = this.panZoomContainer()?.nativeElement;
987
+ if (!container) {
988
+ return;
989
+ }
990
+ this.zoomOut({ by: this.getFitScale(padding) });
991
+ }
992
+ /**
993
+ * Resets both the pan position and zoom level of the org chart.
994
+ * This method first resets the pan position, then resets the zoom level after a short delay.
995
+ * @param {number} [padding=20] - Optional padding around the content when calculating the fit scale.
996
+ */
997
+ resetPanAndZoom(padding = 20) {
998
+ this.resetPan();
999
+ setTimeout(() => {
1000
+ this.resetZoom(padding);
1001
+ }, RESET_DELAY);
1002
+ }
1003
+ /**
1004
+ * Toggles the collapse state of all nodes in the org chart.
1005
+ * If `collapse` is provided, it will collapse or expand all nodes accordingly.
1006
+ * If not provided, it will toggle the current state of the root node.
1007
+ */
1008
+ toggleCollapseAll(collapse) {
1009
+ const nodes = this.nodes();
1010
+ if (nodes?.children?.length && this.collapsible()) {
1011
+ this.onToggleCollapse({ node: nodes, collapse });
1012
+ }
1013
+ }
1014
+ /**
1015
+ * Toggles the collapse state of a specific node in the org chart.
1016
+ * If `collapse` is provided, it will collapse or expand the node accordingly.
1017
+ * If not provided, it will toggle the current state of the node.
1018
+ * @param {Object} options - Options for toggling collapse.
1019
+ * @param {OrgChartNode<T>} options.node - The node to toggle.
1020
+ * @param {boolean} [options.collapse] - Whether to collapse or expand the node.
1021
+ * @param {boolean} [options.highlightNode=false] - Whether to highlight the node after toggling.
1022
+ * @param {boolean} [options.playAnimation=false] - Whether to play animation when highlighting.
1023
+ */
1024
+ onToggleCollapse({ node, collapse, highlightNode = false, playAnimation = false, }) {
1025
+ if (!this.collapsible()) {
1026
+ return;
1027
+ }
1028
+ const nodeId = node.id;
1029
+ const wasCollapsed = node.collapsed;
1030
+ const nodes = toggleNodeCollapse({
1031
+ node: this.nodes(),
1032
+ targetNode: nodeId,
1033
+ collapse,
1034
+ });
1035
+ this.nodes.set(nodes);
1036
+ this.panZoomInstance?.resume();
1037
+ if (highlightNode) {
1038
+ setTimeout(() => {
1039
+ const nodeElement = this.#elementRef?.nativeElement.querySelector(`#${wasCollapsed
1040
+ ? this.getNodeChildrenId(nodeId)
1041
+ : this.getNodeId(nodeId)}`);
1042
+ this.panZoomToNode({
1043
+ nodeElement,
1044
+ skipZoom: true,
1045
+ playAnimation,
1046
+ });
1047
+ }, 200); // allow the DOM finish animation before highlighting
1048
+ }
1049
+ }
1050
+ zoom({ type, by = 10, relative, }) {
1051
+ const containerEl = this.panZoomContainer()?.nativeElement;
1052
+ const containerRect = containerEl.getBoundingClientRect();
1053
+ const hostElement = this.#elementRef?.nativeElement;
1054
+ const hostElementRect = hostElement.getBoundingClientRect();
1055
+ const { scale } = this.panZoomInstance?.getTransform() ?? {
1056
+ scale: 1,
1057
+ };
1058
+ let centerX = containerRect.width / 2 + containerRect.x - hostElementRect.x;
1059
+ const centerY = containerRect.height / 2 + containerRect.y - hostElementRect.y;
1060
+ if (this.layout() === 'horizontal') {
1061
+ if (this.isRtl()) {
1062
+ centerX = containerRect.width + containerRect.x - hostElementRect.x;
1063
+ }
1064
+ else {
1065
+ centerX = 0;
1066
+ }
1067
+ }
1068
+ const newScale = relative
1069
+ ? type === 'in'
1070
+ ? scale * (1 + by / 100)
1071
+ : scale / (1 + by / 100)
1072
+ : by;
1073
+ this.panZoomInstance?.smoothZoomAbs(centerX, centerY, newScale);
1074
+ }
1075
+ panZoomToNode({ nodeElement, skipZoom, playAnimation = true, }) {
1076
+ const container = this.panZoomContainer()?.nativeElement;
1077
+ if (!container || !nodeElement || !this.panZoomInstance) {
1078
+ return;
1079
+ }
1080
+ const highlightedElements = container.querySelectorAll('.highlighted');
1081
+ highlightedElements.forEach(el => {
1082
+ el.classList.remove('highlighted');
1083
+ });
1084
+ this.panZoomInstance?.pause();
1085
+ this.panZoomInstance?.resume();
1086
+ setTimeout(() => {
1087
+ const hostElementRect = this.#elementRef.nativeElement.getBoundingClientRect();
1088
+ const nodeRect1 = nodeElement.getBoundingClientRect();
1089
+ const clientX = nodeRect1.x - nodeRect1.width / 2 - hostElementRect.x;
1090
+ const clientY = nodeRect1.y - nodeRect1.height / 2 - hostElementRect.y;
1091
+ if (!skipZoom) {
1092
+ const dynamicZoom = this.calculateOptimalZoom(nodeElement);
1093
+ this.panZoomInstance?.smoothZoomAbs(clientX, clientY, dynamicZoom);
1094
+ }
1095
+ }, 10);
1096
+ setTimeout(() => {
1097
+ const containerRect = container.getBoundingClientRect();
1098
+ const nodeRect = nodeElement.getBoundingClientRect();
1099
+ const hostingElement = this.#elementRef.nativeElement;
1100
+ const windowWidth = hostingElement.getBoundingClientRect().width;
1101
+ const windowHeight = hostingElement.getBoundingClientRect().height;
1102
+ const transformedNodeX = -1 * (nodeRect.x - containerRect.x);
1103
+ const transformedNodeY = -1 * (nodeRect.y - containerRect.y);
1104
+ const windowCenterX = windowWidth / 2;
1105
+ const windowCenterY = windowHeight / 2;
1106
+ const x = transformedNodeX + windowCenterX - nodeRect.width / 2;
1107
+ const y = transformedNodeY + windowCenterY - nodeRect.height / 2;
1108
+ this.panZoomInstance?.smoothMoveTo(x, y);
1109
+ if (playAnimation) {
1110
+ nodeElement.classList.add('highlighted');
1111
+ setTimeout(() => {
1112
+ nodeElement.classList.remove('highlighted');
1113
+ }, 2300);
1114
+ }
1115
+ }, 200); // allow some time for the zoom to take effect
1116
+ }
1117
+ getNodeId(nodeId) {
1118
+ return `node-${nodeId}`;
1119
+ }
1120
+ getNodeChildrenId(nodeId) {
1121
+ return `node-children-${nodeId}`;
1122
+ }
1123
+ /**
1124
+ * Handles the drag start event for a node.
1125
+ * @param event - The drag event
1126
+ * @param node - The node being dragged
1127
+ */
1128
+ onDragStart(event, node) {
1129
+ if (!this.draggable())
1130
+ return;
1131
+ const canDrag = this.canDragNode();
1132
+ if (canDrag && !canDrag(node)) {
1133
+ event.preventDefault();
1134
+ return;
1135
+ }
1136
+ this.draggedNode.set(node);
1137
+ this.nodeDragStart.emit(node);
1138
+ this.panZoomInstance?.pause();
1139
+ const target = event.target;
1140
+ const nodeContent = target.closest('.node-content');
1141
+ if (event.dataTransfer && nodeContent) {
1142
+ event.dataTransfer.effectAllowed = 'move';
1143
+ event.dataTransfer.setData('text/plain', node.id?.toString() || '');
1144
+ // Set custom drag image for Safari compatibility
1145
+ setDesktopDragImage(event, nodeContent);
1146
+ }
1147
+ if (nodeContent) {
1148
+ setTimeout(() => {
1149
+ nodeContent.classList.add('dragging');
1150
+ }, 0);
1151
+ }
1152
+ this.setupKeyboardListener();
1153
+ }
1154
+ /**
1155
+ * Sets up keyboard event listener for drag cancellation.
1156
+ */
1157
+ setupKeyboardListener() {
1158
+ this.removeKeyboardListener();
1159
+ this.keyboardListener = (event) => {
1160
+ if (event.key === 'Escape' && this.draggedNode()) {
1161
+ this.cancelDrag();
1162
+ }
1163
+ };
1164
+ document.addEventListener('keydown', this.keyboardListener);
1165
+ }
1166
+ /**
1167
+ * Removes the keyboard event listener.
1168
+ */
1169
+ removeKeyboardListener() {
1170
+ if (this.keyboardListener) {
1171
+ document.removeEventListener('keydown', this.keyboardListener);
1172
+ this.keyboardListener = null;
1173
+ }
1174
+ }
1175
+ cancelDrag() {
1176
+ if (!this.draggedNode())
1177
+ return;
1178
+ this.stopAutoPan();
1179
+ this.panZoomInstance?.resume();
1180
+ const allNodes = this.#elementRef.nativeElement.querySelectorAll('.node-content');
1181
+ allNodes.forEach(node => {
1182
+ node.classList.remove('drag-over');
1183
+ node.classList.remove('dragging');
1184
+ node.classList.remove('drag-not-allowed');
1185
+ });
1186
+ this.draggedNode.set(null);
1187
+ this.dragOverNode.set(null);
1188
+ this.removeKeyboardListener();
1189
+ // Clean up touch drag state if active
1190
+ if (this.touchDragState.active) {
1191
+ this.removeTouchGhostElement();
1192
+ this.removeTouchListeners();
1193
+ this.resetTouchDragState();
1194
+ }
1195
+ }
1196
+ /**
1197
+ * Handles the drag end event for a node.
1198
+ * @param event - The drag event
1199
+ * @param node - The node that was being dragged
1200
+ */
1201
+ onDragEnd(event, node) {
1202
+ if (!this.draggable())
1203
+ return;
1204
+ this.nodeDragEnd.emit(node);
1205
+ this.draggedNode.set(null);
1206
+ this.dragOverNode.set(null);
1207
+ this.currentDragOverElement.set(null);
1208
+ this.stopAutoPan();
1209
+ this.removeKeyboardListener();
1210
+ this.panZoomInstance?.resume();
1211
+ const target = event.target;
1212
+ const nodeContent = target.closest('.node-content');
1213
+ if (nodeContent) {
1214
+ nodeContent.classList.remove('dragging');
1215
+ }
1216
+ const allNodes = this.#elementRef.nativeElement.querySelectorAll('.node-content');
1217
+ allNodes.forEach(node => node.classList.remove('drag-over'));
1218
+ }
1219
+ /**
1220
+ * Handles the drag over event for a node.
1221
+ * @param event - The drag event
1222
+ * @param node - The node being dragged over
1223
+ */
1224
+ onDragOver(event, node) {
1225
+ if (!this.draggable() || !this.draggedNode())
1226
+ return;
1227
+ event.preventDefault();
1228
+ const draggedNode = this.draggedNode();
1229
+ if (!draggedNode)
1230
+ return;
1231
+ if (this.isNodeDescendant(node, draggedNode)) {
1232
+ if (event.dataTransfer) {
1233
+ event.dataTransfer.dropEffect = 'none';
1234
+ }
1235
+ return;
1236
+ }
1237
+ const canDrop = this.canDropNode();
1238
+ if (canDrop && !canDrop(draggedNode, node)) {
1239
+ if (event.dataTransfer) {
1240
+ event.dataTransfer.dropEffect = 'none';
1241
+ }
1242
+ return;
1243
+ }
1244
+ if (event.dataTransfer) {
1245
+ event.dataTransfer.dropEffect = 'move';
1246
+ }
1247
+ this.dragOverNode.set(node);
1248
+ }
1249
+ /**
1250
+ * Handles the drag over event on the container for auto-panning.
1251
+ * @param event - The drag event
1252
+ */
1253
+ onContainerDragOver(event) {
1254
+ if (!this.draggable() || !this.draggedNode())
1255
+ return;
1256
+ event.preventDefault();
1257
+ this.handleAutoPan(event);
1258
+ }
1259
+ /**
1260
+ * Handles the drag enter event for a node.
1261
+ * @param event - The drag event
1262
+ * @param node - The node being entered
1263
+ */
1264
+ onDragEnter(event, node) {
1265
+ if (!this.draggable() || !this.draggedNode())
1266
+ return;
1267
+ const draggedNode = this.draggedNode();
1268
+ if (!draggedNode)
1269
+ return;
1270
+ if (this.isNodeDescendant(node, draggedNode)) {
1271
+ return;
1272
+ }
1273
+ const canDrop = this.canDropNode();
1274
+ if (canDrop && !canDrop(draggedNode, node)) {
1275
+ return;
1276
+ }
1277
+ const target = event.target;
1278
+ const nodeElement = target.closest('.node-content');
1279
+ if (nodeElement) {
1280
+ // Remove highlight from previous element
1281
+ if (this.currentDragOverElement() &&
1282
+ this.currentDragOverElement() !== nodeElement) {
1283
+ this.currentDragOverElement()?.classList.remove('drag-over');
1284
+ }
1285
+ // Add highlight to current element
1286
+ nodeElement.classList.add('drag-over');
1287
+ this.currentDragOverElement.set(nodeElement);
1288
+ }
1289
+ }
1290
+ /**
1291
+ * Handles the drag leave event for a node.
1292
+ * @param event - The drag event
1293
+ */
1294
+ onDragLeave(event) {
1295
+ if (!this.draggable())
1296
+ return;
1297
+ const target = event.target;
1298
+ const nodeElement = target.closest('.node-content');
1299
+ // Only remove highlight if we're leaving the entire node-content element
1300
+ // Check if the event is leaving to go outside the node element
1301
+ if (nodeElement && this.currentDragOverElement() === nodeElement) {
1302
+ const rect = nodeElement.getBoundingClientRect();
1303
+ const isOutside = event.clientX < rect.left ||
1304
+ event.clientX > rect.right ||
1305
+ event.clientY < rect.top ||
1306
+ event.clientY > rect.bottom;
1307
+ if (isOutside) {
1308
+ nodeElement.classList.remove('drag-over');
1309
+ this.currentDragOverElement.set(null);
1310
+ }
1311
+ }
1312
+ }
1313
+ /**
1314
+ * Handles the drop event for a node.
1315
+ * @param event - The drag event
1316
+ * @param targetNode - The node where the drop occurred
1317
+ */
1318
+ onDrop(event, targetNode) {
1319
+ if (!this.draggable())
1320
+ return;
1321
+ event.preventDefault();
1322
+ event.stopPropagation();
1323
+ const draggedNode = this.draggedNode();
1324
+ if (!draggedNode)
1325
+ return;
1326
+ if (this.isNodeDescendant(targetNode, draggedNode)) {
1327
+ return;
1328
+ }
1329
+ const canDrop = this.canDropNode();
1330
+ if (canDrop && !canDrop(draggedNode, targetNode)) {
1331
+ return;
1332
+ }
1333
+ this.nodeDrop.emit({
1334
+ draggedNode,
1335
+ targetNode,
1336
+ });
1337
+ const target = event.target;
1338
+ const nodeElement = target.closest('.node-content');
1339
+ if (nodeElement) {
1340
+ nodeElement.classList.remove('drag-over');
1341
+ }
1342
+ this.currentDragOverElement.set(null);
1343
+ this.dragOverNode.set(null);
1344
+ this.stopAutoPan();
1345
+ this.panZoomInstance?.resume();
1346
+ afterNextRender(() => {
1347
+ this.disableChildDragging();
1348
+ }, {
1349
+ injector: this.#injector,
1350
+ });
1351
+ }
1352
+ /**
1353
+ * Checks if a node is a descendant of another node.
1354
+ * @param node - The node to check
1355
+ * @param potentialAncestor - The potential ancestor node
1356
+ * @returns true if node is a descendant of potentialAncestor
1357
+ */
1358
+ isNodeDescendant(node, potentialAncestor) {
1359
+ if (node.id === potentialAncestor.id) {
1360
+ return true;
1361
+ }
1362
+ if (!potentialAncestor.children?.length) {
1363
+ return false;
1364
+ }
1365
+ return potentialAncestor.children.some(child => this.isNodeDescendant(node, child));
1366
+ }
1367
+ /**
1368
+ * Handles the touch start event for drag on mobile devices.
1369
+ * @param event - The touch event
1370
+ * @param node - The node being touched
1371
+ */
1372
+ onTouchStart(event, node) {
1373
+ if (!this.draggable())
1374
+ return;
1375
+ const canDrag = this.canDragNode();
1376
+ if (canDrag && !canDrag(node)) {
1377
+ return;
1378
+ }
1379
+ const touch = event.touches[0];
1380
+ if (!touch)
1381
+ return;
1382
+ this.touchDragState.node = node;
1383
+ this.touchDragState.startX = touch.clientX;
1384
+ this.touchDragState.startY = touch.clientY;
1385
+ this.touchDragState.currentX = touch.clientX;
1386
+ this.touchDragState.currentY = touch.clientY;
1387
+ this.touchDragState.active = false;
1388
+ document.addEventListener('touchmove', this.onTouchMoveDocument, {
1389
+ passive: false,
1390
+ });
1391
+ document.addEventListener('touchend', this.onTouchEndDocument);
1392
+ document.addEventListener('touchcancel', this.onTouchEndDocument);
1393
+ }
1394
+ /**
1395
+ * Handles touch move event during drag on mobile devices.
1396
+ */
1397
+ onTouchMoveDocument = (event) => {
1398
+ if (!this.touchDragState.node)
1399
+ return;
1400
+ const touch = event.touches[0];
1401
+ if (!touch)
1402
+ return;
1403
+ this.touchDragState.currentX = touch.clientX;
1404
+ this.touchDragState.currentY = touch.clientY;
1405
+ if (!this.touchDragState.active) {
1406
+ const deltaX = Math.abs(touch.clientX - this.touchDragState.startX);
1407
+ const deltaY = Math.abs(touch.clientY - this.touchDragState.startY);
1408
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
1409
+ if (distance >= this.touchDragState.dragThreshold) {
1410
+ this.touchDragState.active = true;
1411
+ this.draggedNode.set(this.touchDragState.node);
1412
+ this.nodeDragStart.emit(this.touchDragState.node);
1413
+ this.setupKeyboardListener();
1414
+ this.panZoomInstance?.pause();
1415
+ this.createTouchGhostElement(this.touchDragState.node);
1416
+ const nodeElement = this.getNodeElement(this.touchDragState.node);
1417
+ if (nodeElement) {
1418
+ nodeElement.classList.add('dragging');
1419
+ }
1420
+ }
1421
+ }
1422
+ if (this.touchDragState.active) {
1423
+ event.preventDefault();
1424
+ this.updateTouchGhostPosition(touch.clientX, touch.clientY);
1425
+ this.handleTouchAutoPan(touch.clientX, touch.clientY);
1426
+ const elementUnderTouch = this.getElementUnderTouch(touch.clientX, touch.clientY);
1427
+ if (elementUnderTouch) {
1428
+ const nodeElement = elementUnderTouch.closest('.node-content');
1429
+ if (nodeElement && nodeElement.id) {
1430
+ const nodeId = this.getNodeIdFromElement(nodeElement);
1431
+ const node = this.findNodeById(nodeId);
1432
+ if (node) {
1433
+ this.handleTouchDragOver(node);
1434
+ }
1435
+ else {
1436
+ this.clearDragOverState();
1437
+ }
1438
+ }
1439
+ else {
1440
+ this.clearDragOverState();
1441
+ }
1442
+ }
1443
+ else {
1444
+ this.clearDragOverState();
1445
+ }
1446
+ }
1447
+ };
1448
+ /**
1449
+ * Handles touch end event during drag on mobile devices.
1450
+ */
1451
+ onTouchEndDocument = (event) => {
1452
+ if (!this.touchDragState.node)
1453
+ return;
1454
+ if (this.touchDragState.active) {
1455
+ let targetNode = null;
1456
+ const touch = event.changedTouches[0] ||
1457
+ (event.touches.length > 0 ? event.touches[0] : null);
1458
+ if (touch) {
1459
+ const elementUnderTouch = this.getElementUnderTouch(touch.clientX, touch.clientY);
1460
+ if (elementUnderTouch) {
1461
+ const nodeElement = elementUnderTouch.closest('.node-content');
1462
+ if (nodeElement && nodeElement.id) {
1463
+ const nodeId = this.getNodeIdFromElement(nodeElement);
1464
+ targetNode = this.findNodeById(nodeId);
1465
+ }
1466
+ }
1467
+ }
1468
+ const draggedNode = this.draggedNode();
1469
+ if (targetNode && draggedNode && targetNode.id !== draggedNode.id) {
1470
+ const isDescendant = this.isNodeDescendant(targetNode, draggedNode);
1471
+ const canDrop = this.canDropNode();
1472
+ const dropAllowed = !isDescendant && (!canDrop || canDrop(draggedNode, targetNode));
1473
+ if (dropAllowed) {
1474
+ this.nodeDrop.emit({
1475
+ draggedNode,
1476
+ targetNode,
1477
+ });
1478
+ }
1479
+ }
1480
+ this.removeTouchGhostElement();
1481
+ this.nodeDragEnd.emit(this.touchDragState.node);
1482
+ const nodeElement = this.getNodeElement(this.touchDragState.node);
1483
+ if (nodeElement) {
1484
+ nodeElement.classList.remove('dragging');
1485
+ }
1486
+ this.clearDragOverState();
1487
+ this.draggedNode.set(null);
1488
+ this.stopAutoPan();
1489
+ this.removeKeyboardListener();
1490
+ this.panZoomInstance?.resume();
1491
+ }
1492
+ this.resetTouchDragState();
1493
+ this.removeTouchListeners();
1494
+ };
1495
+ /**
1496
+ * Resets the touch drag state to its initial values.
1497
+ */
1498
+ resetTouchDragState() {
1499
+ this.touchDragState = {
1500
+ active: false,
1501
+ node: null,
1502
+ startX: 0,
1503
+ startY: 0,
1504
+ currentX: 0,
1505
+ currentY: 0,
1506
+ dragThreshold: TOUCH_DRAG_THRESHOLD,
1507
+ ghostElement: null,
1508
+ ghostScaledWidth: 0,
1509
+ ghostScaledHeight: 0,
1510
+ };
1511
+ }
1512
+ /**
1513
+ * Removes touch event listeners from the document.
1514
+ */
1515
+ removeTouchListeners() {
1516
+ document.removeEventListener('touchmove', this.onTouchMoveDocument);
1517
+ document.removeEventListener('touchend', this.onTouchEndDocument);
1518
+ document.removeEventListener('touchcancel', this.onTouchEndDocument);
1519
+ }
1520
+ /**
1521
+ * Creates a ghost element to follow the touch during drag.
1522
+ */
1523
+ createTouchGhostElement(node) {
1524
+ const nodeElement = this.getNodeElement(node);
1525
+ if (!nodeElement)
1526
+ return;
1527
+ const currentScale = this.panZoomInstance?.getTransform()?.scale ?? 1;
1528
+ const result = createTouchDragGhost({
1529
+ nodeElement,
1530
+ currentScale,
1531
+ touchX: this.touchDragState.currentX,
1532
+ touchY: this.touchDragState.currentY,
1533
+ });
1534
+ this.touchDragState.ghostElement = result.wrapper;
1535
+ this.touchDragState.ghostScaledWidth = result.scaledWidth;
1536
+ this.touchDragState.ghostScaledHeight = result.scaledHeight;
1537
+ }
1538
+ /**
1539
+ * Updates the position of the touch ghost element.
1540
+ */
1541
+ updateTouchGhostPosition(x, y) {
1542
+ if (!this.touchDragState.ghostElement)
1543
+ return;
1544
+ updateTouchGhostPosition(this.touchDragState.ghostElement, x, y, this.touchDragState.ghostScaledWidth, this.touchDragState.ghostScaledHeight);
1545
+ }
1546
+ /**
1547
+ * Removes the touch ghost element.
1548
+ */
1549
+ removeTouchGhostElement() {
1550
+ if (this.touchDragState.ghostElement) {
1551
+ this.touchDragState.ghostElement.remove();
1552
+ this.touchDragState.ghostElement = null;
1553
+ }
1554
+ }
1555
+ /**
1556
+ * Gets the element under a touch point.
1557
+ */
1558
+ getElementUnderTouch(x, y) {
1559
+ const ghost = this.touchDragState.ghostElement;
1560
+ const originalDisplay = ghost ? ghost.style.display : '';
1561
+ if (ghost) {
1562
+ ghost.style.display = 'none';
1563
+ }
1564
+ const element = document.elementFromPoint(x, y);
1565
+ if (ghost) {
1566
+ ghost.style.display = originalDisplay;
1567
+ }
1568
+ return element;
1569
+ }
1570
+ /**
1571
+ * Handles touch drag over a node.
1572
+ */
1573
+ handleTouchDragOver(node) {
1574
+ const draggedNode = this.draggedNode();
1575
+ if (!draggedNode)
1576
+ return;
1577
+ if (node.id === draggedNode.id) {
1578
+ this.clearDragOverState();
1579
+ return;
1580
+ }
1581
+ if (this.isNodeDescendant(node, draggedNode)) {
1582
+ this.clearDragOverState();
1583
+ const nodeElement = this.getNodeElement(node);
1584
+ if (nodeElement) {
1585
+ nodeElement.classList.add('drag-not-allowed');
1586
+ }
1587
+ return;
1588
+ }
1589
+ const canDrop = this.canDropNode();
1590
+ if (canDrop && !canDrop(draggedNode, node)) {
1591
+ this.clearDragOverState();
1592
+ const nodeElement = this.getNodeElement(node);
1593
+ if (nodeElement) {
1594
+ nodeElement.classList.add('drag-not-allowed');
1595
+ }
1596
+ return;
1597
+ }
1598
+ if (this.dragOverNode() !== node) {
1599
+ this.clearDragOverState();
1600
+ this.dragOverNode.set(node);
1601
+ const nodeElement = this.getNodeElement(node);
1602
+ if (nodeElement) {
1603
+ nodeElement.classList.add('drag-over');
1604
+ }
1605
+ }
1606
+ }
1607
+ /**
1608
+ * Clears all drag-over visual states.
1609
+ */
1610
+ clearDragOverState() {
1611
+ const allNodes = this.#elementRef.nativeElement.querySelectorAll('.node-content');
1612
+ allNodes.forEach(node => {
1613
+ node.classList.remove('drag-over');
1614
+ node.classList.remove('drag-not-allowed');
1615
+ });
1616
+ this.dragOverNode.set(null);
1617
+ }
1618
+ /**
1619
+ * Gets a node element by node data.
1620
+ */
1621
+ getNodeElement(node) {
1622
+ if (!node.id)
1623
+ return null;
1624
+ const nodeId = this.getNodeId(node.id);
1625
+ return this.#elementRef.nativeElement.querySelector(`#${nodeId}`);
1626
+ }
1627
+ /**
1628
+ * Gets node ID from a DOM element.
1629
+ */
1630
+ getNodeIdFromElement(element) {
1631
+ const id = element.id.replace('node-', '');
1632
+ const numId = parseInt(id, 10);
1633
+ return isNaN(numId) ? id : numId;
1634
+ }
1635
+ /**
1636
+ * Finds a node by ID in the tree.
1637
+ */
1638
+ findNodeById(id) {
1639
+ const nodes = this.nodes();
1640
+ if (!nodes)
1641
+ return null;
1642
+ const search = (node) => {
1643
+ if (node.id === id || node.id?.toString() === id.toString()) {
1644
+ return node;
1645
+ }
1646
+ if (node.children) {
1647
+ for (const child of node.children) {
1648
+ const found = search(child);
1649
+ if (found)
1650
+ return found;
1651
+ }
1652
+ }
1653
+ return null;
1654
+ };
1655
+ return search(nodes);
1656
+ }
1657
+ /**
1658
+ * Handles auto-panning during touch drag.
1659
+ */
1660
+ handleTouchAutoPan(x, y) {
1661
+ const hostElement = this.#elementRef.nativeElement;
1662
+ const rect = hostElement.getBoundingClientRect();
1663
+ const touchX = x - rect.left;
1664
+ const touchY = y - rect.top;
1665
+ const thresholdX = rect.width * this.dragEdgeThreshold();
1666
+ const thresholdY = rect.height * this.dragEdgeThreshold();
1667
+ let panX = 0;
1668
+ let panY = 0;
1669
+ if (touchX < thresholdX) {
1670
+ panX = this.dragAutoPanSpeed();
1671
+ }
1672
+ else if (touchX > rect.width - thresholdX) {
1673
+ panX = -this.dragAutoPanSpeed();
1674
+ }
1675
+ if (touchY < thresholdY) {
1676
+ panY = this.dragAutoPanSpeed();
1677
+ }
1678
+ else if (touchY > rect.height - thresholdY) {
1679
+ panY = -this.dragAutoPanSpeed();
1680
+ }
1681
+ if (panX !== 0 || panY !== 0) {
1682
+ this.startAutoPan(panX, panY);
1683
+ }
1684
+ else {
1685
+ this.stopAutoPan();
1686
+ }
1687
+ }
1688
+ handleAutoPan(event) {
1689
+ const hostElement = this.#elementRef.nativeElement;
1690
+ const rect = hostElement.getBoundingClientRect();
1691
+ const mouseX = event.clientX - rect.left;
1692
+ const mouseY = event.clientY - rect.top;
1693
+ const thresholdX = rect.width * this.dragEdgeThreshold();
1694
+ const thresholdY = rect.height * this.dragEdgeThreshold();
1695
+ let panX = 0;
1696
+ let panY = 0;
1697
+ if (mouseX < thresholdX) {
1698
+ panX = this.dragAutoPanSpeed();
1699
+ }
1700
+ else if (mouseX > rect.width - thresholdX) {
1701
+ panX = -this.dragAutoPanSpeed();
1702
+ }
1703
+ if (mouseY < thresholdY) {
1704
+ panY = this.dragAutoPanSpeed();
1705
+ }
1706
+ else if (mouseY > rect.height - thresholdY) {
1707
+ panY = -this.dragAutoPanSpeed();
1708
+ }
1709
+ if (panX !== 0 || panY !== 0) {
1710
+ this.startAutoPan(panX, panY);
1711
+ }
1712
+ else {
1713
+ this.stopAutoPan();
1714
+ }
1715
+ }
1716
+ startAutoPan(panX, panY) {
1717
+ this.stopAutoPan();
1718
+ this.autoPanInterval = window.setInterval(() => {
1719
+ if (!this.panZoomInstance)
1720
+ return;
1721
+ const transform = this.panZoomInstance.getTransform();
1722
+ const newX = transform.x + panX;
1723
+ const newY = transform.y + panY;
1724
+ this.panZoomInstance.moveTo(newX, newY);
1725
+ }, 16); // ~60fps
1726
+ }
1727
+ stopAutoPan() {
1728
+ if (this.autoPanInterval !== null) {
1729
+ clearInterval(this.autoPanInterval);
1730
+ this.autoPanInterval = null;
1731
+ }
1732
+ }
1733
+ calculateScale() {
1734
+ const transform = this.panZoomInstance?.getTransform();
1735
+ const currentScale = transform?.scale ?? 0;
1736
+ const minZoom = this.minZoom();
1737
+ const maxZoom = this.maxZoom();
1738
+ if (minZoom === maxZoom) {
1739
+ this.scale.set(0);
1740
+ return;
1741
+ }
1742
+ const ratio = (currentScale - minZoom) / (maxZoom - minZoom);
1743
+ const scalePercentage = Math.round(ratio * 10000) / 100;
1744
+ this.scale.set(scalePercentage);
1745
+ }
1746
+ getFitScale(padding = 20) {
1747
+ const hostingElement = this.#elementRef?.nativeElement;
1748
+ const contentEl = this.orgChartContainer()?.nativeElement;
1749
+ if (!hostingElement || !contentEl)
1750
+ return 1;
1751
+ const containerRect = hostingElement.getBoundingClientRect();
1752
+ const containerWidth = containerRect.width;
1753
+ const containerHeight = containerRect.height;
1754
+ // Use actual unscaled dimensions of content
1755
+ const contentWidth = contentEl.clientWidth;
1756
+ const contentHeight = contentEl.clientHeight;
1757
+ // Optional padding around the content
1758
+ const availableWidth = containerWidth - padding * 2;
1759
+ const availableHeight = containerHeight - padding * 2;
1760
+ const scaleX = availableWidth / contentWidth;
1761
+ const scaleY = availableHeight / contentHeight;
1762
+ // Never upscale beyond 1
1763
+ const fitScale = Math.min(scaleX, scaleY, 1);
1764
+ return fitScale;
1765
+ }
1766
+ /**
1767
+ * Calculates the optimal zoom level for highlighting a specific node.
1768
+ * The zoom is calculated to ensure the node is appropriately sized relative to the container,
1769
+ * while respecting the minimum and maximum zoom constraints.
1770
+ * @param {HTMLElement} nodeElement - The node element to calculate zoom for.
1771
+ * @returns {number} The optimal zoom level for the node.
1772
+ */
1773
+ calculateOptimalZoom(nodeElement) {
1774
+ const hostingElement = this.#elementRef?.nativeElement;
1775
+ if (!hostingElement || !nodeElement) {
1776
+ return 1.5; // fallback to original value
1777
+ }
1778
+ const containerRect = hostingElement.getBoundingClientRect();
1779
+ const nodeRect = nodeElement.getBoundingClientRect();
1780
+ // Calculate the current transform to get actual node dimensions
1781
+ const currentTransform = this.panZoomInstance?.getTransform();
1782
+ const currentScale = currentTransform?.scale || 1;
1783
+ // Get the actual unscaled node dimensions
1784
+ const actualNodeWidth = nodeRect.width / currentScale;
1785
+ const actualNodeHeight = nodeRect.height / currentScale;
1786
+ // Use configurable ratios for target node size
1787
+ const targetNodeWidthRatio = this.highlightZoomNodeWidthRatio();
1788
+ const targetNodeHeightRatio = this.highlightZoomNodeHeightRatio();
1789
+ // Calculate zoom levels needed for width and height constraints
1790
+ const zoomForWidth = (containerRect.width * targetNodeWidthRatio) / actualNodeWidth;
1791
+ const zoomForHeight = (containerRect.height * targetNodeHeightRatio) / actualNodeHeight;
1792
+ // Use the smaller zoom to ensure the node fits well in both dimensions
1793
+ let optimalZoom = Math.min(zoomForWidth, zoomForHeight);
1794
+ // Apply zoom constraints
1795
+ const minZoom = this.minZoom();
1796
+ const maxZoom = this.maxZoom();
1797
+ optimalZoom = Math.max(minZoom, Math.min(maxZoom, optimalZoom));
1798
+ // Ensure a configurable minimum reasonable zoom for visibility
1799
+ const minimumZoom = this.highlightZoomMinimum();
1800
+ optimalZoom = Math.max(minimumZoom, optimalZoom);
1801
+ return optimalZoom;
1802
+ }
1803
+ ngOnDestroy() {
1804
+ this.stopAutoPan();
1805
+ this.removeKeyboardListener();
1806
+ this.panZoomInstance?.dispose();
1807
+ }
1808
+ /**
1809
+ * Disables native dragging on child elements (images, SVGs, anchors) within nodes.
1810
+ * This prevents child elements from interfering with the node's drag functionality.
1811
+ * Called automatically via effect when nodes change.
1812
+ */
1813
+ disableChildDragging() {
1814
+ const hostingElement = this.#elementRef?.nativeElement;
1815
+ if (!hostingElement)
1816
+ return;
1817
+ const draggableChildren = hostingElement.querySelectorAll('.node-content-wrapper img, .node-content-wrapper a, .node-content-wrapper svg');
1818
+ draggableChildren.forEach((element) => {
1819
+ element.setAttribute('draggable', 'false');
1820
+ });
1821
+ }
1822
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgxInteractiveOrgChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
1823
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: NgxInteractiveOrgChart, isStandalone: true, selector: "ngx-interactive-org-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, initialZoom: { classPropertyName: "initialZoom", publicName: "initialZoom", isSignal: true, isRequired: false, transformFunction: null }, minZoom: { classPropertyName: "minZoom", publicName: "minZoom", isSignal: true, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: true, isRequired: false, transformFunction: null }, zoomSpeed: { classPropertyName: "zoomSpeed", publicName: "zoomSpeed", isSignal: true, isRequired: false, transformFunction: null }, zoomDoubleClickSpeed: { classPropertyName: "zoomDoubleClickSpeed", publicName: "zoomDoubleClickSpeed", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, nodeClass: { classPropertyName: "nodeClass", publicName: "nodeClass", isSignal: true, isRequired: false, transformFunction: null }, initialCollapsed: { classPropertyName: "initialCollapsed", publicName: "initialCollapsed", isSignal: true, isRequired: false, transformFunction: null }, isRtl: { classPropertyName: "isRtl", publicName: "isRtl", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, focusOnCollapseOrExpand: { classPropertyName: "focusOnCollapseOrExpand", publicName: "focusOnCollapseOrExpand", isSignal: true, isRequired: false, transformFunction: null }, displayChildrenCount: { classPropertyName: "displayChildrenCount", publicName: "displayChildrenCount", isSignal: true, isRequired: false, transformFunction: null }, highlightZoomNodeWidthRatio: { classPropertyName: "highlightZoomNodeWidthRatio", publicName: "highlightZoomNodeWidthRatio", isSignal: true, isRequired: false, transformFunction: null }, highlightZoomNodeHeightRatio: { classPropertyName: "highlightZoomNodeHeightRatio", publicName: "highlightZoomNodeHeightRatio", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null }, canDragNode: { classPropertyName: "canDragNode", publicName: "canDragNode", isSignal: true, isRequired: false, transformFunction: null }, canDropNode: { classPropertyName: "canDropNode", publicName: "canDropNode", isSignal: true, isRequired: false, transformFunction: null }, dragEdgeThreshold: { classPropertyName: "dragEdgeThreshold", publicName: "dragEdgeThreshold", isSignal: true, isRequired: false, transformFunction: null }, dragAutoPanSpeed: { classPropertyName: "dragAutoPanSpeed", publicName: "dragAutoPanSpeed", isSignal: true, isRequired: false, transformFunction: null }, highlightZoomMinimum: { classPropertyName: "highlightZoomMinimum", publicName: "highlightZoomMinimum", isSignal: true, isRequired: false, transformFunction: null }, themeOptions: { classPropertyName: "themeOptions", publicName: "themeOptions", isSignal: true, isRequired: false, transformFunction: null }, showMiniMap: { classPropertyName: "showMiniMap", publicName: "showMiniMap", isSignal: true, isRequired: false, transformFunction: null }, miniMapPosition: { classPropertyName: "miniMapPosition", publicName: "miniMapPosition", isSignal: true, isRequired: false, transformFunction: null }, miniMapWidth: { classPropertyName: "miniMapWidth", publicName: "miniMapWidth", isSignal: true, isRequired: false, transformFunction: null }, miniMapHeight: { classPropertyName: "miniMapHeight", publicName: "miniMapHeight", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeDrop: "nodeDrop", nodeDragStart: "nodeDragStart", nodeDragEnd: "nodeDragEnd" }, host: { properties: { "style.--node-background": "finalThemeOptions().node!.background", "style.--node-color": "finalThemeOptions().node!.color", "style.--node-shadow": "finalThemeOptions().node!.shadow", "style.--node-outline-color": "finalThemeOptions().node!.outlineColor", "style.--node-outline-width": "finalThemeOptions().node!.outlineWidth", "style.--node-active-outline-color": "finalThemeOptions().node!.activeOutlineColor", "style.--node-highlight-shadow-color": "finalThemeOptions().node!.highlightShadowColor", "style.--node-padding": "finalThemeOptions().node!.padding", "style.--node-border-radius": "finalThemeOptions().node!.borderRadius", "style.--node-active-color": "finalThemeOptions().node!.activeColor", "style.--node-max-width": "finalThemeOptions().node!.maxWidth", "style.--node-min-width": "finalThemeOptions().node!.minWidth", "style.--node-max-height": "finalThemeOptions().node!.maxHeight", "style.--node-min-height": "finalThemeOptions().node!.minHeight", "style.--node-drag-over-outline-color": "finalThemeOptions().node!.dragOverOutlineColor", "style.--connector-color": "finalThemeOptions().connector!.color", "style.--connector-active-color": "finalThemeOptions().connector!.activeColor", "style.--connector-border-radius": "finalThemeOptions().connector!.borderRadius", "style.--node-container-spacing": "finalThemeOptions().node!.containerSpacing", "style.--connector-width": "finalThemeOptions().connector!.width", "style.--collapse-button-size": "finalThemeOptions().collapseButton!.size", "style.--collapse-button-border-color": "finalThemeOptions().collapseButton!.borderColor", "style.--collapse-button-border-radius": "finalThemeOptions().collapseButton!.borderRadius", "style.--collapse-button-color": "finalThemeOptions().collapseButton!.color", "style.--collapse-button-background": "finalThemeOptions().collapseButton!.background", "style.--collapse-button-hover-color": "finalThemeOptions().collapseButton!.hoverColor", "style.--collapse-button-hover-background": "finalThemeOptions().collapseButton!.hoverBackground", "style.--collapse-button-hover-shadow": "finalThemeOptions().collapseButton!.hoverShadow", "style.--collapse-button-hover-transform-scale": "finalThemeOptions().collapseButton!.hoverTransformScale", "style.--collapse-button-focus-outline": "finalThemeOptions().collapseButton!.focusOutline", "style.--collapse-button-count-font-size": "finalThemeOptions().collapseButton!.countFontSize", "style.--container-background": "finalThemeOptions().container!.background", "style.--container-border": "finalThemeOptions().container!.border", "attr.data-layout": "layout()" } }, queries: [{ propertyName: "customNodeTemplate", first: true, predicate: ["nodeTemplate"], descendants: true }, { propertyName: "customDragHandleTemplate", first: true, predicate: ["dragHandleTemplate"], descendants: true }], viewQueries: [{ propertyName: "panZoomContainer", first: true, predicate: ["panZoomContainer"], descendants: true, isSignal: true }, { propertyName: "orgChartContainer", first: true, predicate: ["orgChartContainer"], descendants: true, isSignal: true }, { propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }], ngImport: i0, template: "<section\n class=\"org-chart-container\"\n #container\n [class.rtl]=\"isRtl()\"\n (dragover)=\"onContainerDragOver($event)\"\n>\n <section class=\"org-chart\" #panZoomContainer>\n @if (nodes()?.id) {\n <ul class=\"node-container\">\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{ node: nodes() }\"\n ></ng-container>\n </ul>\n }\n </section>\n\n <!-- Mini Map -->\n @if (showMiniMap()) {\n <ngx-org-chart-mini-map\n [panZoomInstance]=\"panZoomInstance\"\n [chartContainer]=\"containerElement()\"\n [position]=\"miniMapPosition()\"\n [width]=\"miniMapWidth()\"\n [height]=\"miniMapHeight()\"\n [themeOptions]=\"finalThemeOptions().miniMap\"\n />\n }\n</section>\n\n<ng-template #nodeTemplate let-node=\"node\">\n <li class=\"org-node\" [class.collapsed]=\"node.collapsed\" #orgChartContainer>\n <a\n [ngClass]=\"[node?.nodeClass ?? '', nodeClass() ?? '', 'node-content']\"\n [ngStyle]=\"node.style\"\n [id]=\"getNodeId(node.id)\"\n [attr.aria-expanded]=\"node.children?.length ? !node.collapsed : null\"\n [attr.aria-label]=\"\n node.name + (node.children?.length ? (node.collapsed ? ' (collapsed)' : ' (expanded)') : '')\n \"\n [attr.draggable]=\"draggable() && !customDragHandleTemplate ? true : null\"\n (dragstart)=\"customDragHandleTemplate ? null : onDragStart($event, node)\"\n (dragend)=\"customDragHandleTemplate ? null : onDragEnd($event, node)\"\n (dragover)=\"onDragOver($event, node)\"\n (dragenter)=\"onDragEnter($event, node)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event, node)\"\n (touchstart)=\"customDragHandleTemplate ? null : onTouchStart($event, node)\"\n >\n <div class=\"node-content-wrapper\">\n @if (customNodeTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"customNodeTemplate\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n node: node,\n }\"\n ></ng-container>\n } @else {\n <span>{{ node.name }}</span>\n }\n </div>\n @if (customDragHandleTemplate && draggable()) {\n <span\n class=\"drag-handle\"\n [attr.draggable]=\"true\"\n (dragstart)=\"onDragStart($event, node)\"\n (dragend)=\"onDragEnd($event, node)\"\n (touchstart)=\"onTouchStart($event, node)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"customDragHandleTemplate\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n node: node,\n }\"\n ></ng-container>\n </span>\n }\n @if (collapsible() && node.children?.length) {\n <button\n class=\"collapse-btn\"\n (click)=\"onToggleCollapse({ node, highlightNode: focusOnCollapseOrExpand() })\"\n [class.collapsed]=\"node.collapsed\"\n (mouseenter)=\"panZoomInstance?.pause()\"\n (mousewheel)=\"panZoomInstance?.resume()\"\n (mouseleave)=\"panZoomInstance?.resume()\"\n (pointerenter)=\"panZoomInstance?.pause()\"\n (pointerleave)=\"panZoomInstance?.resume()\"\n [attr.aria-label]=\"(node.collapsed ? 'Expand' : 'Collapse') + ' ' + node.name\"\n type=\"button\"\n >\n @if (displayChildrenCount()) {\n <small class=\"children-count\">{{ node.descendantsCount }}</small>\n }\n <svg fill=\"none\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M11.9995 16.8001C11.2995 16.8001 10.5995 16.5301 10.0695 16.0001L3.54953 9.48014C3.25953 9.19014 3.25953 8.71014 3.54953 8.42014C3.83953 8.13014 4.31953 8.13014 4.60953 8.42014L11.1295 14.9401C11.6095 15.4201 12.3895 15.4201 12.8695 14.9401L19.3895 8.42014C19.6795 8.13014 20.1595 8.13014 20.4495 8.42014C20.7395 8.71014 20.7395 9.19014 20.4495 9.48014L13.9295 16.0001C13.3995 16.5301 12.6995 16.8001 11.9995 16.8001Z\"\n fill=\"currentColor\"\n />\n </svg>\n </button>\n }\n </a>\n\n @if (node.children?.length && !node.hidden && !node.collapsed) {\n <ul\n class=\"node-container\"\n [id]=\"getNodeChildrenId(node.id)\"\n [class.collapsed]=\"node.collapsed\"\n [class.instant-animation]=\"true\"\n [attr.aria-hidden]=\"node.collapsed\"\n @toggleNode\n >\n @for (child of node.children; track child.id) {\n @if (!child.hidden) {\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{\n node: child,\n nodeTemplate: nodeTemplate,\n }\"\n ></ng-container>\n }\n }\n </ul>\n }\n </li>\n</ng-template>\n", styles: [":host{--collapse-duration: .3s;--base-delay: .1s;--animation-duration: .5s;cursor:grab}:host:active{cursor:grabbing}:host[data-layout=horizontal] .org-chart{min-width:100%}:host[data-layout=horizontal] .org-chart:first-child{display:flex}:host[data-layout=horizontal] .org-chart:first-child .node-content{align-self:center}:host[data-layout=horizontal] .org-chart:first-child .org-node{display:flex;justify-content:flex-start}:host[data-layout=horizontal] .org-chart .node-container{padding-inline-start:calc(var(--node-container-spacing) + var(--collapse-button-size) / 2);padding-block-start:0;min-width:auto;margin:0;display:flex;flex-direction:column}:host[data-layout=horizontal] .org-chart .node-container .node-container{overflow:hidden;display:flex;flex-direction:column;margin:0;min-width:auto}:host[data-layout=horizontal] .org-chart .node-container .node-container:before{inset-inline-start:0;inset-block-start:50%;border-block-start:var(--connector-width) solid var(--connector-color);height:0;width:calc(var(--node-container-spacing) + var(--collapse-button-size) / 2);animation:lineAppearX var(--animation-duration) ease-out forwards}:host[data-layout=horizontal] .org-chart .org-node{justify-content:flex-start;overflow:visible}:host[data-layout=horizontal] .org-chart .org-node:before,:host[data-layout=horizontal] .org-chart .org-node:after{inset-inline-start:0;inset-block-end:50%;border-inline-start:var(--connector-width) solid var(--connector-color);border-inline-end:0;height:50%;width:var(--node-container-spacing);animation:lineAppearY var(--animation-duration) ease-out forwards}:host[data-layout=horizontal] .org-chart .org-node:after{inset-block-end:auto;inset-block-start:50%;border-block-start:var(--connector-width) solid var(--connector-color)}:host[data-layout=horizontal] .org-chart .org-node:before{border-block-start:none}:host[data-layout=horizontal] .org-chart .org-node:only-child{padding-inline-start:.125rem;padding-block-start:var(--node-container-spacing)}:host[data-layout=horizontal] .org-chart .org-node:only-child:before,:host[data-layout=horizontal] .org-chart .org-node:only-child:after{display:none}:host[data-layout=horizontal] .org-chart .org-node:first-child:before{border:0 none}:host[data-layout=horizontal] .org-chart .org-node:first-child:after{border-end-start-radius:0;border-start-start-radius:var(--connector-border-radius);border-block-start:var(--connector-width) solid var(--connector-color)}:host[data-layout=horizontal] .org-chart .org-node:last-child:after{border:0 none}:host[data-layout=horizontal] .org-chart .org-node:last-child:before{border-block-end:var(--connector-width) solid var(--connector-color);border-block-start:none;border-end-start-radius:var(--connector-border-radius);border-start-end-radius:0}:host[data-layout=horizontal] .org-chart .node-content{white-space:nowrap}:host[data-layout=horizontal] .org-chart .collapse-btn{inset-inline-end:calc(-1 * var(--collapse-button-size) / 2);inset-block-end:unset;inset-block-start:50%;inset-inline-start:unset;transform:translateY(-50%);flex-direction:column-reverse;justify-content:center}:host[data-layout=horizontal] .org-chart .collapse-btn svg{transition:transform var(--collapse-duration) ease;transform:rotate(-90deg)}:host[data-layout=horizontal] .org-chart .collapse-btn.collapsed svg{transform:rotate(90deg)}:host[data-layout=horizontal] .org-chart .collapse-btn .children-count{padding-inline-start:0}:host[data-layout=horizontal] .rtl .org-chart .collapse-btn{inset-inline-start:calc(-1 * var(--collapse-button-size) / 2);inset-inline-end:unset}.org-chart{text-align:center;min-width:100%;width:max-content;scroll-behavior:smooth;border:var(--container-border);background:var(--container-background);cursor:grab}.org-chart:active{cursor:grabbing}.org-chart-container{display:flex;min-height:100%;max-height:100%;width:100%;overflow:hidden;position:relative}.org-chart .node-container{display:flex;flex-wrap:nowrap;justify-content:center;padding-block-start:calc(var(--node-container-spacing) + var(--collapse-button-size) / 2);padding-inline-start:0;position:relative;opacity:1;transition:max-height var(--collapse-duration) ease,opacity var(--collapse-duration) ease,padding var(--collapse-duration) ease,width var(--collapse-duration) ease;box-sizing:border-box;margin:0 auto;min-width:0}.org-chart .node-container.collapsed{max-height:0;opacity:0;padding-block-start:0}.org-chart .node-container:first-child{padding-block-start:.5rem;margin-block:0}.org-chart .node-container .node-container:before{content:\"\";position:absolute;inset-block-start:0;inset-inline-start:50%;border-inline-start:var(--connector-width) solid var(--connector-color);width:0;height:calc(var(--node-container-spacing) + var(--collapse-button-size) / 2 + var(--connector-width));opacity:0;animation:lineAppearY var(--animation-duration) ease-out forwards;animation-delay:calc(var(--base-delay) + .3s);transition:border-color .5s}.org-chart .org-node{display:inline-table;text-align:center;list-style-type:none;position:relative;padding:var(--node-container-spacing);transition:.5s;flex-shrink:0;overflow:hidden}.org-chart .org-node.removing{opacity:0;transform:translateY(1.25rem) scale(.95);transition:.5s}.org-chart .org-node:before,.org-chart .org-node:after{content:\"\";position:absolute;inset-block-start:0;inset-inline-end:50%;border-block-start:var(--connector-width) solid var(--connector-color);width:51%;height:var(--node-container-spacing);opacity:0;animation:lineAppearX var(--animation-duration) ease-out forwards;animation-delay:calc(var(--base-delay) + .2s);transition:border-color .5s}.org-chart .org-node:after{inset-inline-end:auto;inset-inline-start:50%;border-inline-start:var(--connector-width) solid var(--connector-color)}.org-chart .org-node:only-child{padding-block-start:.125rem}.org-chart .org-node:only-child:before,.org-chart .org-node:only-child:after{display:none}.org-chart .org-node:first-child:before{border:0 none}.org-chart .org-node:first-child:after{border-start-start-radius:var(--connector-border-radius);border-inline-start:var(--connector-width) solid var(--connector-color)}.org-chart .org-node:last-child:after{border:0 none}.org-chart .org-node:last-child:before{border-inline-end:var(--connector-width) solid var(--connector-color);border-start-end-radius:var(--connector-border-radius)}.org-chart .org-node .node-content{max-width:var(--node-max-width);max-height:var(--node-max-height);min-width:var(--node-min-width);min-height:var(--node-min-height);background:var(--node-background);color:var(--node-color);box-shadow:var(--node-shadow);padding:var(--node-padding);position:relative;display:inline-grid;border-radius:var(--node-border-radius);text-decoration:none;transition:.5s;outline:var(--node-outline-width) solid var(--node-outline-color);z-index:1;box-sizing:content-box}.org-chart .org-node .node-content .node-content-wrapper{display:contents}.org-chart .org-node .node-content .node-content-wrapper img,.org-chart .org-node .node-content .node-content-wrapper svg{user-select:none;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.org-chart .org-node .node-content.highlighted{animation:pulse 1.5s infinite}.org-chart .org-node .node-content.dragging{opacity:.5;cursor:grabbing}.org-chart .org-node .node-content.drag-over{outline:2px dashed var(--node-drag-over-outline-color);background-color:color-mix(in srgb,var(--node-drag-over-outline-color) 10%,var(--node-background))}.org-chart .org-node .node-content.drag-not-allowed{cursor:not-allowed!important}.org-chart .org-node .node-content[draggable=true]{cursor:move}.org-chart .org-node .node-content .drag-handle{cursor:move;display:inline-flex;align-items:center;justify-content:center}.org-chart .org-node .node-content .drag-handle:active{cursor:grabbing}.org-chart .org-node .node-content:hover{outline:var(--node-outline-width) solid var(--node-active-outline-color)}.org-chart .org-node .node-content:hover+.node-container .node-content{outline:var(--node-outline-width) solid var(--node-active-outline-color)}.org-chart .org-node .node-content:hover+.node-container:before{border-color:var(--connector-active-color)}.org-chart .org-node .node-content:hover+.node-container .org-node:before,.org-chart .org-node .node-content:hover+.node-container .org-node:after{border-color:var(--connector-active-color)}.org-chart .org-node .node-content:hover+.node-container .node-container:before{border-color:var(--connector-active-color)}.org-chart .collapse-btn{all:unset;direction:ltr;display:flex;align-items:center;justify-content:center;color:var(--collapse-button-color);background:var(--collapse-button-background);border:.0625rem solid var(--collapse-button-border-color);border-radius:var(--collapse-button-border-radius);position:absolute;inset-block-end:calc(-1 * var(--collapse-button-size) / 2);inset-inline-start:50%;transform:translate(-50%);z-index:2;cursor:pointer;transition:transform var(--collapse-duration) ease,box-shadow var(--collapse-duration) ease}.org-chart .collapse-btn svg{transition:transform var(--collapse-duration) ease;width:var(--collapse-button-size);height:var(--collapse-button-size)}.org-chart .collapse-btn.collapsed svg{transform:rotate(180deg)}.org-chart .collapse-btn .children-count{padding-inline-start:.25rem;font-size:var(--collapse-button-count-font-size)}.org-chart .collapse-btn:hover{transform:translate(-50%) scale(var(--collapse-button-hover-transform-scale));box-shadow:var(--collapse-button-hover-shadow);color:var(--collapse-button-hover-color);background:var(--collapse-button-hover-background)}.org-chart .collapse-btn:focus-visible{outline:var(--collapse-button-focus-outline);outline-offset:.0625rem}@keyframes lineAppearX{0%{opacity:0;transform:scaleX(0)}to{opacity:1;transform:scaleX(1)}}@keyframes lineAppearY{0%{opacity:0;transform:scaleY(0)}to{opacity:1;transform:scaleY(1)}}@media(prefers-reduced-motion:reduce){.org-chart{--base-delay: 0ms;--animation-duration: 0ms;--collapse-duration: 0ms}.org-chart .node-container{transition:none}.org-chart .node-container .node-container:before{opacity:1;animation:none}.org-chart .collapse-btn exc-icon{transition:none}.org-chart .org-node{opacity:1;transform:none;animation:none}.org-chart .org-node:before,.org-chart .org-node:after{opacity:1;animation:none}}@keyframes pulse{0%{box-shadow:0 0}70%{box-shadow:0 0 0 10px var(--node-highlight-shadow-color)}to{box-shadow:0 0 0 0 var(--node-highlight-shadow-color)}}:root .touch-drag-ghost-wrapper{transition:none!important;animation:none!important}:root .touch-drag-ghost-wrapper *{pointer-events:none!important;-webkit-user-select:none!important;user-select:none!important}:root .touch-drag-ghost-wrapper .collapse-btn,:root .touch-drag-ghost-wrapper .drag-handle{display:none!important}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: MiniMapComponent, selector: "ngx-org-chart-mini-map", inputs: ["panZoomInstance", "chartContainer", "position", "width", "height", "visible", "themeOptions"], outputs: ["navigate"] }], animations: [
1824
+ trigger('toggleNode', [
1825
+ transition(':enter', [
1826
+ style({ width: '0', height: '0', opacity: 0, transform: 'scale(0.8)' }),
1827
+ animate('300ms ease-out', style({ width: '*', height: '*', opacity: 1, transform: 'scale(1)' })),
1828
+ ]),
1829
+ transition(':leave', [
1830
+ style({ width: '*', height: '*' }),
1831
+ animate('300ms ease-out', style({
1832
+ width: '0',
1833
+ height: '0',
1834
+ opacity: 0,
1835
+ transform: 'scale(0.8)',
1836
+ })),
1837
+ ]),
1838
+ ]),
1839
+ ] });
1840
+ }
1841
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgxInteractiveOrgChart, decorators: [{
1842
+ type: Component,
1843
+ args: [{ standalone: true, selector: 'ngx-interactive-org-chart', imports: [NgTemplateOutlet, NgClass, NgStyle, MiniMapComponent], animations: [
1844
+ trigger('toggleNode', [
1845
+ transition(':enter', [
1846
+ style({ width: '0', height: '0', opacity: 0, transform: 'scale(0.8)' }),
1847
+ animate('300ms ease-out', style({ width: '*', height: '*', opacity: 1, transform: 'scale(1)' })),
1848
+ ]),
1849
+ transition(':leave', [
1850
+ style({ width: '*', height: '*' }),
1851
+ animate('300ms ease-out', style({
1852
+ width: '0',
1853
+ height: '0',
1854
+ opacity: 0,
1855
+ transform: 'scale(0.8)',
1856
+ })),
1857
+ ]),
1858
+ ]),
1859
+ ], host: {
1860
+ '[style.--node-background]': 'finalThemeOptions().node!.background',
1861
+ '[style.--node-color]': 'finalThemeOptions().node!.color',
1862
+ '[style.--node-shadow]': 'finalThemeOptions().node!.shadow',
1863
+ '[style.--node-outline-color]': 'finalThemeOptions().node!.outlineColor',
1864
+ '[style.--node-outline-width]': 'finalThemeOptions().node!.outlineWidth',
1865
+ '[style.--node-active-outline-color]': 'finalThemeOptions().node!.activeOutlineColor',
1866
+ '[style.--node-highlight-shadow-color]': 'finalThemeOptions().node!.highlightShadowColor',
1867
+ '[style.--node-padding]': 'finalThemeOptions().node!.padding',
1868
+ '[style.--node-border-radius]': 'finalThemeOptions().node!.borderRadius',
1869
+ '[style.--node-active-color]': 'finalThemeOptions().node!.activeColor',
1870
+ '[style.--node-max-width]': 'finalThemeOptions().node!.maxWidth',
1871
+ '[style.--node-min-width]': 'finalThemeOptions().node!.minWidth',
1872
+ '[style.--node-max-height]': 'finalThemeOptions().node!.maxHeight',
1873
+ '[style.--node-min-height]': 'finalThemeOptions().node!.minHeight',
1874
+ '[style.--node-drag-over-outline-color]': 'finalThemeOptions().node!.dragOverOutlineColor',
1875
+ '[style.--connector-color]': 'finalThemeOptions().connector!.color',
1876
+ '[style.--connector-active-color]': 'finalThemeOptions().connector!.activeColor',
1877
+ '[style.--connector-border-radius]': 'finalThemeOptions().connector!.borderRadius',
1878
+ '[style.--node-container-spacing]': 'finalThemeOptions().node!.containerSpacing',
1879
+ '[style.--connector-width]': 'finalThemeOptions().connector!.width',
1880
+ '[style.--collapse-button-size]': 'finalThemeOptions().collapseButton!.size',
1881
+ '[style.--collapse-button-border-color]': 'finalThemeOptions().collapseButton!.borderColor',
1882
+ '[style.--collapse-button-border-radius]': 'finalThemeOptions().collapseButton!.borderRadius',
1883
+ '[style.--collapse-button-color]': 'finalThemeOptions().collapseButton!.color',
1884
+ '[style.--collapse-button-background]': 'finalThemeOptions().collapseButton!.background',
1885
+ '[style.--collapse-button-hover-color]': 'finalThemeOptions().collapseButton!.hoverColor',
1886
+ '[style.--collapse-button-hover-background]': 'finalThemeOptions().collapseButton!.hoverBackground',
1887
+ '[style.--collapse-button-hover-shadow]': 'finalThemeOptions().collapseButton!.hoverShadow',
1888
+ '[style.--collapse-button-hover-transform-scale]': 'finalThemeOptions().collapseButton!.hoverTransformScale',
1889
+ '[style.--collapse-button-focus-outline]': 'finalThemeOptions().collapseButton!.focusOutline',
1890
+ '[style.--collapse-button-count-font-size]': 'finalThemeOptions().collapseButton!.countFontSize',
1891
+ '[style.--container-background]': 'finalThemeOptions().container!.background',
1892
+ '[style.--container-border]': 'finalThemeOptions().container!.border',
1893
+ '[attr.data-layout]': 'layout()',
1894
+ }, template: "<section\n class=\"org-chart-container\"\n #container\n [class.rtl]=\"isRtl()\"\n (dragover)=\"onContainerDragOver($event)\"\n>\n <section class=\"org-chart\" #panZoomContainer>\n @if (nodes()?.id) {\n <ul class=\"node-container\">\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{ node: nodes() }\"\n ></ng-container>\n </ul>\n }\n </section>\n\n <!-- Mini Map -->\n @if (showMiniMap()) {\n <ngx-org-chart-mini-map\n [panZoomInstance]=\"panZoomInstance\"\n [chartContainer]=\"containerElement()\"\n [position]=\"miniMapPosition()\"\n [width]=\"miniMapWidth()\"\n [height]=\"miniMapHeight()\"\n [themeOptions]=\"finalThemeOptions().miniMap\"\n />\n }\n</section>\n\n<ng-template #nodeTemplate let-node=\"node\">\n <li class=\"org-node\" [class.collapsed]=\"node.collapsed\" #orgChartContainer>\n <a\n [ngClass]=\"[node?.nodeClass ?? '', nodeClass() ?? '', 'node-content']\"\n [ngStyle]=\"node.style\"\n [id]=\"getNodeId(node.id)\"\n [attr.aria-expanded]=\"node.children?.length ? !node.collapsed : null\"\n [attr.aria-label]=\"\n node.name + (node.children?.length ? (node.collapsed ? ' (collapsed)' : ' (expanded)') : '')\n \"\n [attr.draggable]=\"draggable() && !customDragHandleTemplate ? true : null\"\n (dragstart)=\"customDragHandleTemplate ? null : onDragStart($event, node)\"\n (dragend)=\"customDragHandleTemplate ? null : onDragEnd($event, node)\"\n (dragover)=\"onDragOver($event, node)\"\n (dragenter)=\"onDragEnter($event, node)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event, node)\"\n (touchstart)=\"customDragHandleTemplate ? null : onTouchStart($event, node)\"\n >\n <div class=\"node-content-wrapper\">\n @if (customNodeTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"customNodeTemplate\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n node: node,\n }\"\n ></ng-container>\n } @else {\n <span>{{ node.name }}</span>\n }\n </div>\n @if (customDragHandleTemplate && draggable()) {\n <span\n class=\"drag-handle\"\n [attr.draggable]=\"true\"\n (dragstart)=\"onDragStart($event, node)\"\n (dragend)=\"onDragEnd($event, node)\"\n (touchstart)=\"onTouchStart($event, node)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"customDragHandleTemplate\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n node: node,\n }\"\n ></ng-container>\n </span>\n }\n @if (collapsible() && node.children?.length) {\n <button\n class=\"collapse-btn\"\n (click)=\"onToggleCollapse({ node, highlightNode: focusOnCollapseOrExpand() })\"\n [class.collapsed]=\"node.collapsed\"\n (mouseenter)=\"panZoomInstance?.pause()\"\n (mousewheel)=\"panZoomInstance?.resume()\"\n (mouseleave)=\"panZoomInstance?.resume()\"\n (pointerenter)=\"panZoomInstance?.pause()\"\n (pointerleave)=\"panZoomInstance?.resume()\"\n [attr.aria-label]=\"(node.collapsed ? 'Expand' : 'Collapse') + ' ' + node.name\"\n type=\"button\"\n >\n @if (displayChildrenCount()) {\n <small class=\"children-count\">{{ node.descendantsCount }}</small>\n }\n <svg fill=\"none\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M11.9995 16.8001C11.2995 16.8001 10.5995 16.5301 10.0695 16.0001L3.54953 9.48014C3.25953 9.19014 3.25953 8.71014 3.54953 8.42014C3.83953 8.13014 4.31953 8.13014 4.60953 8.42014L11.1295 14.9401C11.6095 15.4201 12.3895 15.4201 12.8695 14.9401L19.3895 8.42014C19.6795 8.13014 20.1595 8.13014 20.4495 8.42014C20.7395 8.71014 20.7395 9.19014 20.4495 9.48014L13.9295 16.0001C13.3995 16.5301 12.6995 16.8001 11.9995 16.8001Z\"\n fill=\"currentColor\"\n />\n </svg>\n </button>\n }\n </a>\n\n @if (node.children?.length && !node.hidden && !node.collapsed) {\n <ul\n class=\"node-container\"\n [id]=\"getNodeChildrenId(node.id)\"\n [class.collapsed]=\"node.collapsed\"\n [class.instant-animation]=\"true\"\n [attr.aria-hidden]=\"node.collapsed\"\n @toggleNode\n >\n @for (child of node.children; track child.id) {\n @if (!child.hidden) {\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{\n node: child,\n nodeTemplate: nodeTemplate,\n }\"\n ></ng-container>\n }\n }\n </ul>\n }\n </li>\n</ng-template>\n", styles: [":host{--collapse-duration: .3s;--base-delay: .1s;--animation-duration: .5s;cursor:grab}:host:active{cursor:grabbing}:host[data-layout=horizontal] .org-chart{min-width:100%}:host[data-layout=horizontal] .org-chart:first-child{display:flex}:host[data-layout=horizontal] .org-chart:first-child .node-content{align-self:center}:host[data-layout=horizontal] .org-chart:first-child .org-node{display:flex;justify-content:flex-start}:host[data-layout=horizontal] .org-chart .node-container{padding-inline-start:calc(var(--node-container-spacing) + var(--collapse-button-size) / 2);padding-block-start:0;min-width:auto;margin:0;display:flex;flex-direction:column}:host[data-layout=horizontal] .org-chart .node-container .node-container{overflow:hidden;display:flex;flex-direction:column;margin:0;min-width:auto}:host[data-layout=horizontal] .org-chart .node-container .node-container:before{inset-inline-start:0;inset-block-start:50%;border-block-start:var(--connector-width) solid var(--connector-color);height:0;width:calc(var(--node-container-spacing) + var(--collapse-button-size) / 2);animation:lineAppearX var(--animation-duration) ease-out forwards}:host[data-layout=horizontal] .org-chart .org-node{justify-content:flex-start;overflow:visible}:host[data-layout=horizontal] .org-chart .org-node:before,:host[data-layout=horizontal] .org-chart .org-node:after{inset-inline-start:0;inset-block-end:50%;border-inline-start:var(--connector-width) solid var(--connector-color);border-inline-end:0;height:50%;width:var(--node-container-spacing);animation:lineAppearY var(--animation-duration) ease-out forwards}:host[data-layout=horizontal] .org-chart .org-node:after{inset-block-end:auto;inset-block-start:50%;border-block-start:var(--connector-width) solid var(--connector-color)}:host[data-layout=horizontal] .org-chart .org-node:before{border-block-start:none}:host[data-layout=horizontal] .org-chart .org-node:only-child{padding-inline-start:.125rem;padding-block-start:var(--node-container-spacing)}:host[data-layout=horizontal] .org-chart .org-node:only-child:before,:host[data-layout=horizontal] .org-chart .org-node:only-child:after{display:none}:host[data-layout=horizontal] .org-chart .org-node:first-child:before{border:0 none}:host[data-layout=horizontal] .org-chart .org-node:first-child:after{border-end-start-radius:0;border-start-start-radius:var(--connector-border-radius);border-block-start:var(--connector-width) solid var(--connector-color)}:host[data-layout=horizontal] .org-chart .org-node:last-child:after{border:0 none}:host[data-layout=horizontal] .org-chart .org-node:last-child:before{border-block-end:var(--connector-width) solid var(--connector-color);border-block-start:none;border-end-start-radius:var(--connector-border-radius);border-start-end-radius:0}:host[data-layout=horizontal] .org-chart .node-content{white-space:nowrap}:host[data-layout=horizontal] .org-chart .collapse-btn{inset-inline-end:calc(-1 * var(--collapse-button-size) / 2);inset-block-end:unset;inset-block-start:50%;inset-inline-start:unset;transform:translateY(-50%);flex-direction:column-reverse;justify-content:center}:host[data-layout=horizontal] .org-chart .collapse-btn svg{transition:transform var(--collapse-duration) ease;transform:rotate(-90deg)}:host[data-layout=horizontal] .org-chart .collapse-btn.collapsed svg{transform:rotate(90deg)}:host[data-layout=horizontal] .org-chart .collapse-btn .children-count{padding-inline-start:0}:host[data-layout=horizontal] .rtl .org-chart .collapse-btn{inset-inline-start:calc(-1 * var(--collapse-button-size) / 2);inset-inline-end:unset}.org-chart{text-align:center;min-width:100%;width:max-content;scroll-behavior:smooth;border:var(--container-border);background:var(--container-background);cursor:grab}.org-chart:active{cursor:grabbing}.org-chart-container{display:flex;min-height:100%;max-height:100%;width:100%;overflow:hidden;position:relative}.org-chart .node-container{display:flex;flex-wrap:nowrap;justify-content:center;padding-block-start:calc(var(--node-container-spacing) + var(--collapse-button-size) / 2);padding-inline-start:0;position:relative;opacity:1;transition:max-height var(--collapse-duration) ease,opacity var(--collapse-duration) ease,padding var(--collapse-duration) ease,width var(--collapse-duration) ease;box-sizing:border-box;margin:0 auto;min-width:0}.org-chart .node-container.collapsed{max-height:0;opacity:0;padding-block-start:0}.org-chart .node-container:first-child{padding-block-start:.5rem;margin-block:0}.org-chart .node-container .node-container:before{content:\"\";position:absolute;inset-block-start:0;inset-inline-start:50%;border-inline-start:var(--connector-width) solid var(--connector-color);width:0;height:calc(var(--node-container-spacing) + var(--collapse-button-size) / 2 + var(--connector-width));opacity:0;animation:lineAppearY var(--animation-duration) ease-out forwards;animation-delay:calc(var(--base-delay) + .3s);transition:border-color .5s}.org-chart .org-node{display:inline-table;text-align:center;list-style-type:none;position:relative;padding:var(--node-container-spacing);transition:.5s;flex-shrink:0;overflow:hidden}.org-chart .org-node.removing{opacity:0;transform:translateY(1.25rem) scale(.95);transition:.5s}.org-chart .org-node:before,.org-chart .org-node:after{content:\"\";position:absolute;inset-block-start:0;inset-inline-end:50%;border-block-start:var(--connector-width) solid var(--connector-color);width:51%;height:var(--node-container-spacing);opacity:0;animation:lineAppearX var(--animation-duration) ease-out forwards;animation-delay:calc(var(--base-delay) + .2s);transition:border-color .5s}.org-chart .org-node:after{inset-inline-end:auto;inset-inline-start:50%;border-inline-start:var(--connector-width) solid var(--connector-color)}.org-chart .org-node:only-child{padding-block-start:.125rem}.org-chart .org-node:only-child:before,.org-chart .org-node:only-child:after{display:none}.org-chart .org-node:first-child:before{border:0 none}.org-chart .org-node:first-child:after{border-start-start-radius:var(--connector-border-radius);border-inline-start:var(--connector-width) solid var(--connector-color)}.org-chart .org-node:last-child:after{border:0 none}.org-chart .org-node:last-child:before{border-inline-end:var(--connector-width) solid var(--connector-color);border-start-end-radius:var(--connector-border-radius)}.org-chart .org-node .node-content{max-width:var(--node-max-width);max-height:var(--node-max-height);min-width:var(--node-min-width);min-height:var(--node-min-height);background:var(--node-background);color:var(--node-color);box-shadow:var(--node-shadow);padding:var(--node-padding);position:relative;display:inline-grid;border-radius:var(--node-border-radius);text-decoration:none;transition:.5s;outline:var(--node-outline-width) solid var(--node-outline-color);z-index:1;box-sizing:content-box}.org-chart .org-node .node-content .node-content-wrapper{display:contents}.org-chart .org-node .node-content .node-content-wrapper img,.org-chart .org-node .node-content .node-content-wrapper svg{user-select:none;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.org-chart .org-node .node-content.highlighted{animation:pulse 1.5s infinite}.org-chart .org-node .node-content.dragging{opacity:.5;cursor:grabbing}.org-chart .org-node .node-content.drag-over{outline:2px dashed var(--node-drag-over-outline-color);background-color:color-mix(in srgb,var(--node-drag-over-outline-color) 10%,var(--node-background))}.org-chart .org-node .node-content.drag-not-allowed{cursor:not-allowed!important}.org-chart .org-node .node-content[draggable=true]{cursor:move}.org-chart .org-node .node-content .drag-handle{cursor:move;display:inline-flex;align-items:center;justify-content:center}.org-chart .org-node .node-content .drag-handle:active{cursor:grabbing}.org-chart .org-node .node-content:hover{outline:var(--node-outline-width) solid var(--node-active-outline-color)}.org-chart .org-node .node-content:hover+.node-container .node-content{outline:var(--node-outline-width) solid var(--node-active-outline-color)}.org-chart .org-node .node-content:hover+.node-container:before{border-color:var(--connector-active-color)}.org-chart .org-node .node-content:hover+.node-container .org-node:before,.org-chart .org-node .node-content:hover+.node-container .org-node:after{border-color:var(--connector-active-color)}.org-chart .org-node .node-content:hover+.node-container .node-container:before{border-color:var(--connector-active-color)}.org-chart .collapse-btn{all:unset;direction:ltr;display:flex;align-items:center;justify-content:center;color:var(--collapse-button-color);background:var(--collapse-button-background);border:.0625rem solid var(--collapse-button-border-color);border-radius:var(--collapse-button-border-radius);position:absolute;inset-block-end:calc(-1 * var(--collapse-button-size) / 2);inset-inline-start:50%;transform:translate(-50%);z-index:2;cursor:pointer;transition:transform var(--collapse-duration) ease,box-shadow var(--collapse-duration) ease}.org-chart .collapse-btn svg{transition:transform var(--collapse-duration) ease;width:var(--collapse-button-size);height:var(--collapse-button-size)}.org-chart .collapse-btn.collapsed svg{transform:rotate(180deg)}.org-chart .collapse-btn .children-count{padding-inline-start:.25rem;font-size:var(--collapse-button-count-font-size)}.org-chart .collapse-btn:hover{transform:translate(-50%) scale(var(--collapse-button-hover-transform-scale));box-shadow:var(--collapse-button-hover-shadow);color:var(--collapse-button-hover-color);background:var(--collapse-button-hover-background)}.org-chart .collapse-btn:focus-visible{outline:var(--collapse-button-focus-outline);outline-offset:.0625rem}@keyframes lineAppearX{0%{opacity:0;transform:scaleX(0)}to{opacity:1;transform:scaleX(1)}}@keyframes lineAppearY{0%{opacity:0;transform:scaleY(0)}to{opacity:1;transform:scaleY(1)}}@media(prefers-reduced-motion:reduce){.org-chart{--base-delay: 0ms;--animation-duration: 0ms;--collapse-duration: 0ms}.org-chart .node-container{transition:none}.org-chart .node-container .node-container:before{opacity:1;animation:none}.org-chart .collapse-btn exc-icon{transition:none}.org-chart .org-node{opacity:1;transform:none;animation:none}.org-chart .org-node:before,.org-chart .org-node:after{opacity:1;animation:none}}@keyframes pulse{0%{box-shadow:0 0}70%{box-shadow:0 0 0 10px var(--node-highlight-shadow-color)}to{box-shadow:0 0 0 0 var(--node-highlight-shadow-color)}}:root .touch-drag-ghost-wrapper{transition:none!important;animation:none!important}:root .touch-drag-ghost-wrapper *{pointer-events:none!important;-webkit-user-select:none!important;user-select:none!important}:root .touch-drag-ghost-wrapper .collapse-btn,:root .touch-drag-ghost-wrapper .drag-handle{display:none!important}\n"] }]
1895
+ }], propDecorators: { customNodeTemplate: [{
1896
+ type: ContentChild,
1897
+ args: ['nodeTemplate', { static: false }]
1898
+ }], customDragHandleTemplate: [{
1899
+ type: ContentChild,
1900
+ args: ['dragHandleTemplate', { static: false }]
1901
+ }], panZoomContainer: [{ type: i0.ViewChild, args: ['panZoomContainer', { isSignal: true }] }], orgChartContainer: [{ type: i0.ViewChild, args: ['orgChartContainer', { isSignal: true }] }], container: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], initialZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialZoom", required: false }] }], minZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "minZoom", required: false }] }], maxZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxZoom", required: false }] }], zoomSpeed: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomSpeed", required: false }] }], zoomDoubleClickSpeed: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomDoubleClickSpeed", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], nodeClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeClass", required: false }] }], initialCollapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialCollapsed", required: false }] }], isRtl: [{ type: i0.Input, args: [{ isSignal: true, alias: "isRtl", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], focusOnCollapseOrExpand: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusOnCollapseOrExpand", required: false }] }], displayChildrenCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayChildrenCount", required: false }] }], highlightZoomNodeWidthRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightZoomNodeWidthRatio", required: false }] }], highlightZoomNodeHeightRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightZoomNodeHeightRatio", required: false }] }], draggable: [{ type: i0.Input, args: [{ isSignal: true, alias: "draggable", required: false }] }], canDragNode: [{ type: i0.Input, args: [{ isSignal: true, alias: "canDragNode", required: false }] }], canDropNode: [{ type: i0.Input, args: [{ isSignal: true, alias: "canDropNode", required: false }] }], dragEdgeThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragEdgeThreshold", required: false }] }], dragAutoPanSpeed: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragAutoPanSpeed", required: false }] }], highlightZoomMinimum: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightZoomMinimum", required: false }] }], themeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "themeOptions", required: false }] }], showMiniMap: [{ type: i0.Input, args: [{ isSignal: true, alias: "showMiniMap", required: false }] }], miniMapPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "miniMapPosition", required: false }] }], miniMapWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "miniMapWidth", required: false }] }], miniMapHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "miniMapHeight", required: false }] }], nodeDrop: [{ type: i0.Output, args: ["nodeDrop"] }], nodeDragStart: [{ type: i0.Output, args: ["nodeDragStart"] }], nodeDragEnd: [{ type: i0.Output, args: ["nodeDragEnd"] }] } });
1902
+
1903
+ /**
1904
+ * Moves a node from its current position to become a child of a target node.
1905
+ * This is a pure function that returns a new tree structure without modifying the original.
1906
+ *
1907
+ * @param rootNode - The root node of the tree
1908
+ * @param draggedNodeId - The ID of the node to move
1909
+ * @param targetNodeId - The ID of the node that will become the parent
1910
+ * @returns A new tree structure with the node moved, or null if the operation fails
1911
+ *
1912
+ * @example
1913
+ * ```typescript
1914
+ * const updatedTree = moveNode(currentTree, '5', '3');
1915
+ * if (updatedTree) {
1916
+ * this.data.set(updatedTree);
1917
+ * }
1918
+ * ```
1919
+ */
1920
+ function moveNode(rootNode, draggedNodeId, targetNodeId) {
1921
+ // First, find and extract the dragged node
1922
+ const draggedNode = findNode(rootNode, draggedNodeId);
1923
+ if (!draggedNode) {
1924
+ return null;
1925
+ }
1926
+ // Check if target is a descendant of dragged node (prevent circular references)
1927
+ if (isNodeDescendant(draggedNode, targetNodeId)) {
1928
+ return null;
1929
+ }
1930
+ // Remove the dragged node from its current position
1931
+ const treeWithoutNode = removeNode(rootNode, draggedNodeId);
1932
+ if (!treeWithoutNode) {
1933
+ return null;
1934
+ }
1935
+ // Add the dragged node to the target node's children
1936
+ const finalTree = addNodeToParent(treeWithoutNode, targetNodeId, draggedNode);
1937
+ return finalTree;
1938
+ }
1939
+ /**
1940
+ * Finds a node in the tree by its ID.
1941
+ *
1942
+ * @param node - The node to search in
1943
+ * @param nodeId - The ID of the node to find
1944
+ * @returns The found node or null
1945
+ */
1946
+ function findNode(node, nodeId) {
1947
+ if (node.id === nodeId) {
1948
+ return node;
1949
+ }
1950
+ if (node.children) {
1951
+ for (const child of node.children) {
1952
+ const found = findNode(child, nodeId);
1953
+ if (found) {
1954
+ return found;
1955
+ }
1956
+ }
1957
+ }
1958
+ return null;
1959
+ }
1960
+ /**
1961
+ * Checks if a node is a descendant of another node.
1962
+ *
1963
+ * @param node - The potential ancestor node
1964
+ * @param descendantId - The ID of the potential descendant
1965
+ * @returns true if the node with descendantId is a descendant of node
1966
+ */
1967
+ function isNodeDescendant(node, descendantId) {
1968
+ if (node.id === descendantId) {
1969
+ return true;
1970
+ }
1971
+ if (!node.children?.length) {
1972
+ return false;
1973
+ }
1974
+ return node.children.some(child => isNodeDescendant(child, descendantId));
1975
+ }
1976
+ /**
1977
+ * Removes a node from the tree structure.
1978
+ * Returns a new tree without the specified node.
1979
+ *
1980
+ * @param node - The root node to search in
1981
+ * @param nodeIdToRemove - The ID of the node to remove
1982
+ * @returns A new tree structure without the removed node
1983
+ */
1984
+ function removeNode(node, nodeIdToRemove) {
1985
+ // Don't remove the root node
1986
+ if (node.id === nodeIdToRemove) {
1987
+ return null;
1988
+ }
1989
+ // Create a shallow copy
1990
+ const clonedNode = {
1991
+ ...node,
1992
+ children: node.children ? [...node.children] : undefined,
1993
+ };
1994
+ if (!clonedNode.children) {
1995
+ return clonedNode;
1996
+ }
1997
+ // Filter out the node to remove and recursively process children
1998
+ const updatedChildren = clonedNode.children
1999
+ .filter(child => child.id !== nodeIdToRemove)
2000
+ .map(child => removeNode(child, nodeIdToRemove))
2001
+ .filter((child) => child !== null);
2002
+ return {
2003
+ ...clonedNode,
2004
+ children: updatedChildren.length > 0 ? updatedChildren : undefined,
2005
+ };
2006
+ }
2007
+ /**
2008
+ * Adds a node as a child of the target parent node.
2009
+ * Returns a new tree structure with the node added.
2010
+ *
2011
+ * @param node - The root node to search in
2012
+ * @param targetParentId - The ID of the node that will become the parent
2013
+ * @param nodeToAdd - The node to add as a child
2014
+ * @returns A new tree structure with the node added
2015
+ */
2016
+ function addNodeToParent(node, targetParentId, nodeToAdd) {
2017
+ if (node.id === targetParentId) {
2018
+ // Found the target parent - add the node to its children
2019
+ const currentChildren = node.children || [];
2020
+ return {
2021
+ ...node,
2022
+ children: [...currentChildren, { ...nodeToAdd }],
2023
+ };
2024
+ }
2025
+ // Recursively search in children
2026
+ if (node.children) {
2027
+ const updatedChildren = node.children
2028
+ .map(child => addNodeToParent(child, targetParentId, nodeToAdd))
2029
+ .filter((child) => child !== null);
2030
+ return {
2031
+ ...node,
2032
+ children: updatedChildren,
2033
+ };
2034
+ }
2035
+ return node;
2036
+ }
2037
+
2038
+ /*
2039
+ * Public API Surface of org-chart
2040
+ */
2041
+
2042
+ /**
2043
+ * Generated bundle index. Do not edit.
2044
+ */
2045
+
2046
+ export { MiniMapComponent, NgxInteractiveOrgChart, addNodeToParent, findNode, isNodeDescendant, moveNode, removeNode };
2047
+ //# sourceMappingURL=tic-nova-ngx-interactive-org-chart.mjs.map