@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
|