concepto-user-controls 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/esm2022/lib/concepto-context-menu/concepto-context-menu.component.mjs +3 -7
  2. package/esm2022/lib/concepto-tree/components/tree-node/tree-node.component.mjs +301 -0
  3. package/esm2022/lib/concepto-tree/components/tree-node-checkbox/tree-node-checkbox.component.mjs +90 -0
  4. package/esm2022/lib/concepto-tree/components/tree-node-content/tree-node-content.component.mjs +65 -0
  5. package/esm2022/lib/concepto-tree/components/tree-node-expander/tree-node-expander.component.mjs +74 -0
  6. package/esm2022/lib/concepto-tree/components/tree-root/tree-root.component.mjs +230 -0
  7. package/esm2022/lib/concepto-tree/components/tree-viewport/tree-viewport.component.mjs +216 -0
  8. package/esm2022/lib/concepto-tree/concepto-tree.component.mjs +220 -0
  9. package/esm2022/lib/concepto-tree/core/models/tree-events.model.mjs +2 -0
  10. package/esm2022/lib/concepto-tree/core/models/tree-node.model.mjs +102 -0
  11. package/esm2022/lib/concepto-tree/core/models/tree-options.model.mjs +24 -0
  12. package/esm2022/lib/concepto-tree/core/models/tree.model.mjs +313 -0
  13. package/esm2022/lib/concepto-tree/core/services/tree-drag-drop.service.mjs +27 -0
  14. package/esm2022/lib/concepto-tree/directives/tree-drag.directive.mjs +69 -0
  15. package/esm2022/lib/concepto-tree/directives/tree-drop.directive.mjs +124 -0
  16. package/esm2022/lib/concepto-tree/directives/tree-node-template.directive.mjs +19 -0
  17. package/esm2022/lib/entity-comparison/components/entity-comparison.component.mjs +218 -0
  18. package/esm2022/lib/entity-comparison/core/services/entity-comparison.service.mjs +111 -0
  19. package/esm2022/public-api.mjs +4 -1
  20. package/fesm2022/concepto-user-controls.mjs +2147 -7
  21. package/fesm2022/concepto-user-controls.mjs.map +1 -1
  22. package/lib/concepto-context-menu/concepto-context-menu.component.d.ts +2 -3
  23. package/lib/concepto-tree/components/tree-node/tree-node.component.d.ts +19 -0
  24. package/lib/concepto-tree/components/tree-node-checkbox/tree-node-checkbox.component.d.ts +17 -0
  25. package/lib/concepto-tree/components/tree-node-content/tree-node-content.component.d.ts +18 -0
  26. package/lib/concepto-tree/components/tree-node-expander/tree-node-expander.component.d.ts +12 -0
  27. package/lib/concepto-tree/components/tree-root/tree-root.component.d.ts +35 -0
  28. package/lib/concepto-tree/components/tree-viewport/tree-viewport.component.d.ts +33 -0
  29. package/lib/concepto-tree/concepto-tree.component.d.ts +36 -0
  30. package/lib/concepto-tree/core/models/tree-events.model.d.ts +13 -0
  31. package/lib/concepto-tree/core/models/tree-node.model.d.ts +39 -0
  32. package/lib/concepto-tree/core/models/tree-options.model.d.ts +28 -0
  33. package/lib/concepto-tree/core/models/tree.model.d.ts +54 -0
  34. package/lib/concepto-tree/core/services/tree-drag-drop.service.d.ts +11 -0
  35. package/lib/concepto-tree/directives/tree-drag.directive.d.ts +16 -0
  36. package/lib/concepto-tree/directives/tree-drop.directive.d.ts +25 -0
  37. package/lib/concepto-tree/directives/tree-node-template.directive.d.ts +8 -0
  38. package/lib/entity-comparison/components/entity-comparison.component.d.ts +49 -0
  39. package/lib/entity-comparison/core/services/entity-comparison.service.d.ts +10 -0
  40. package/package.json +1 -1
  41. package/public-api.d.ts +3 -0
@@ -1,7 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Component, EventEmitter, Output, Input, HostListener } from '@angular/core';
2
+ import { Injectable, Component, EventEmitter, Output, Input, HostListener, signal, computed, ChangeDetectionStrategy, inject, ElementRef, HostBinding, Directive, effect, ViewChild, contentChild, TemplateRef } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { NgIf, CommonModule } from '@angular/common';
5
+ import * as i2 from '@angular/forms';
6
+ import { FormsModule } from '@angular/forms';
7
+ import { Subject, fromEvent, takeUntil } from 'rxjs';
8
+ import { trigger, state, transition, style, animate } from '@angular/animations';
9
+ import { debounceTime } from 'rxjs/operators';
5
10
 
6
11
  class ConceptoUserControlsService {
7
12
  constructor() { }
@@ -68,7 +73,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
68
73
  class ConceptoContextMenuComponent {
69
74
  elRef;
70
75
  nodes = [];
71
- itemSelected = new EventEmitter();
72
76
  menuTree = [];
73
77
  visible = false;
74
78
  pos = { x: 0, y: 0 };
@@ -112,7 +116,6 @@ class ConceptoContextMenuComponent {
112
116
  onOptionClick(option) {
113
117
  if (!option.children || option.children.length === 0) {
114
118
  console.log('Selected:', option);
115
- this.itemSelected.emit(option.NodesId);
116
119
  this.hide();
117
120
  }
118
121
  }
@@ -120,20 +123,2157 @@ class ConceptoContextMenuComponent {
120
123
  return !!option.children && option.children.length > 0;
121
124
  }
122
125
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConceptoContextMenuComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
123
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: ConceptoContextMenuComponent, isStandalone: true, selector: "concepto-context-menu", inputs: { nodes: "nodes" }, outputs: { itemSelected: "itemSelected" }, host: { listeners: { "document:click": "onOutsideClick($event)" } }, usesOnChanges: true, ngImport: i0, template: "<div class=\"context-menu\" *ngIf=\"visible\" [ngStyle]=\"{ top: pos.y + 'px', left: pos.x + 'px' }\">\r\n <ul class=\"menu-root\">\r\n <ng-container *ngFor=\"let item of menuTree\">\r\n <ng-container *ngTemplateOutlet=\"renderNode; context: { $implicit: item }\"></ng-container>\r\n </ng-container>\r\n </ul>\r\n</div>\r\n\r\n<ng-template #renderNode let-node>\r\n <li class=\"menu-item\" [class.has-children]=\"hasChildren(node)\">\r\n <div (click)=\"onOptionClick(node)\">\r\n @if (node.NodesImg) {\r\n <img [src]=\"node.NodesImg\" class=\"icon\" />\r\n }\r\n @if (!node.NodesImg) {\r\n <div class=\"icon\"></div>\r\n }\r\n {{ node.NodesText }}\r\n </div>\r\n <ul class=\"submenu\" *ngIf=\"hasChildren(node)\">\r\n <ng-container *ngFor=\"let child of node.children\">\r\n <ng-container *ngTemplateOutlet=\"renderNode; context: { $implicit: child }\"></ng-container>\r\n </ng-container>\r\n </ul>\r\n </li>\r\n</ng-template>", styles: [".context-menu{position:fixed;z-index:9999;background-color:#fff;border:1px solid #ccc;box-shadow:2px 2px 8px #00000026;min-width:200px;font-family:sans-serif}.menu-root,.submenu{list-style:none;margin:0;padding:0}.menu-item{position:relative}.menu-item>div{padding:8px 12px;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.menu-item>div:hover{background-color:#f0f0f0}.menu-item.has-children>div:after{content:\"\\25b6\";margin-left:auto;font-size:10px}.submenu{display:none;position:absolute;top:0;left:100%;border:1px solid #ccc;background-color:#fff;min-width:180px;box-shadow:2px 2px 6px #0000001a}.menu-item:hover>.submenu{display:block}.icon{width:16px;height:16px;margin-right:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
126
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: ConceptoContextMenuComponent, isStandalone: true, selector: "concepto-context-menu", inputs: { nodes: "nodes" }, host: { listeners: { "document:click": "onOutsideClick($event)" } }, usesOnChanges: true, ngImport: i0, template: "<div class=\"context-menu\" *ngIf=\"visible\" [ngStyle]=\"{ top: pos.y + 'px', left: pos.x + 'px' }\">\r\n <ul class=\"menu-root\">\r\n <ng-container *ngFor=\"let item of menuTree\">\r\n <ng-container *ngTemplateOutlet=\"renderNode; context: { $implicit: item }\"></ng-container>\r\n </ng-container>\r\n </ul>\r\n</div>\r\n\r\n<ng-template #renderNode let-node>\r\n <li class=\"menu-item\" [class.has-children]=\"hasChildren(node)\">\r\n <div (click)=\"onOptionClick(node)\">\r\n @if (node.NodesImg) {\r\n <img [src]=\"node.NodesImg\" class=\"icon\" />\r\n }\r\n @if (!node.NodesImg) {\r\n <div class=\"icon\"></div>\r\n }\r\n {{ node.NodesText }}\r\n </div>\r\n <ul class=\"submenu\" *ngIf=\"hasChildren(node)\">\r\n <ng-container *ngFor=\"let child of node.children\">\r\n <ng-container *ngTemplateOutlet=\"renderNode; context: { $implicit: child }\"></ng-container>\r\n </ng-container>\r\n </ul>\r\n </li>\r\n</ng-template>", styles: [".context-menu{position:fixed;z-index:9999;background-color:#fff;border:1px solid #ccc;box-shadow:2px 2px 8px #00000026;min-width:200px;font-family:sans-serif}.menu-root,.submenu{list-style:none;margin:0;padding:0}.menu-item{position:relative}.menu-item>div{padding:8px 12px;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.menu-item>div:hover{background-color:#f0f0f0}.menu-item.has-children>div:after{content:\"\\25b6\";margin-left:auto;font-size:10px}.submenu{display:none;position:absolute;top:0;left:100%;border:1px solid #ccc;background-color:#fff;min-width:180px;box-shadow:2px 2px 6px #0000001a}.menu-item:hover>.submenu{display:block}.icon{width:16px;height:16px;margin-right:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
124
127
  }
125
128
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConceptoContextMenuComponent, decorators: [{
126
129
  type: Component,
127
130
  args: [{ selector: 'concepto-context-menu', standalone: true, imports: [CommonModule], template: "<div class=\"context-menu\" *ngIf=\"visible\" [ngStyle]=\"{ top: pos.y + 'px', left: pos.x + 'px' }\">\r\n <ul class=\"menu-root\">\r\n <ng-container *ngFor=\"let item of menuTree\">\r\n <ng-container *ngTemplateOutlet=\"renderNode; context: { $implicit: item }\"></ng-container>\r\n </ng-container>\r\n </ul>\r\n</div>\r\n\r\n<ng-template #renderNode let-node>\r\n <li class=\"menu-item\" [class.has-children]=\"hasChildren(node)\">\r\n <div (click)=\"onOptionClick(node)\">\r\n @if (node.NodesImg) {\r\n <img [src]=\"node.NodesImg\" class=\"icon\" />\r\n }\r\n @if (!node.NodesImg) {\r\n <div class=\"icon\"></div>\r\n }\r\n {{ node.NodesText }}\r\n </div>\r\n <ul class=\"submenu\" *ngIf=\"hasChildren(node)\">\r\n <ng-container *ngFor=\"let child of node.children\">\r\n <ng-container *ngTemplateOutlet=\"renderNode; context: { $implicit: child }\"></ng-container>\r\n </ng-container>\r\n </ul>\r\n </li>\r\n</ng-template>", styles: [".context-menu{position:fixed;z-index:9999;background-color:#fff;border:1px solid #ccc;box-shadow:2px 2px 8px #00000026;min-width:200px;font-family:sans-serif}.menu-root,.submenu{list-style:none;margin:0;padding:0}.menu-item{position:relative}.menu-item>div{padding:8px 12px;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.menu-item>div:hover{background-color:#f0f0f0}.menu-item.has-children>div:after{content:\"\\25b6\";margin-left:auto;font-size:10px}.submenu{display:none;position:absolute;top:0;left:100%;border:1px solid #ccc;background-color:#fff;min-width:180px;box-shadow:2px 2px 6px #0000001a}.menu-item:hover>.submenu{display:block}.icon{width:16px;height:16px;margin-right:8px}\n"] }]
128
131
  }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { nodes: [{
129
132
  type: Input
130
- }], itemSelected: [{
131
- type: Output
132
133
  }], onOutsideClick: [{
133
134
  type: HostListener,
134
135
  args: ['document:click', ['$event']]
135
136
  }] } });
136
137
 
138
+ // lib/core/models/tree-node.model.ts
139
+ class TreeNode {
140
+ // Signals for reactive state
141
+ isExpanded = signal(false);
142
+ isActive = signal(false);
143
+ isSelected = signal(false);
144
+ isFocused = signal(false);
145
+ isHidden = signal(false);
146
+ isLoading = signal(false);
147
+ // Computed signals
148
+ isCollapsed = computed(() => !this.isExpanded());
149
+ isLeaf = computed(() => !this.hasChildren);
150
+ isVisible = computed(() => !this.isHidden());
151
+ id;
152
+ data;
153
+ parent;
154
+ children = [];
155
+ level;
156
+ path;
157
+ index;
158
+ hasChildren;
159
+ icon;
160
+ constructor(data, parent, level, index, options) {
161
+ this.data = data;
162
+ this.parent = parent;
163
+ this.level = level;
164
+ this.index = index;
165
+ this.id = data[options.idField || 'id'];
166
+ this.icon = data[options.iconField || 'icon'];
167
+ const childrenData = data[options.childrenField || 'children'];
168
+ this.hasChildren = Array.isArray(childrenData) && childrenData.length > 0;
169
+ // Initialize expanded state
170
+ if (options.isExpandedField && data[options.isExpandedField]) {
171
+ this.isExpanded.set(true);
172
+ }
173
+ // Build path
174
+ this.path = parent ? [...parent.path, this.id] : [this.id];
175
+ }
176
+ // Actions
177
+ expand() {
178
+ if (this.hasChildren) {
179
+ this.isExpanded.set(true);
180
+ }
181
+ }
182
+ collapse() {
183
+ this.isExpanded.set(false);
184
+ }
185
+ toggle() {
186
+ this.isExpanded.update(value => !value);
187
+ }
188
+ setActive(value) {
189
+ this.isActive.set(value);
190
+ }
191
+ setSelected(value) {
192
+ this.isSelected.set(value);
193
+ }
194
+ setFocus(value) {
195
+ this.isFocused.set(value);
196
+ }
197
+ setHidden(value) {
198
+ this.isHidden.set(value);
199
+ }
200
+ setLoading(value) {
201
+ this.isLoading.set(value);
202
+ }
203
+ // Helper methods
204
+ ensureVisible() {
205
+ let node = this.parent;
206
+ while (node) {
207
+ node.expand();
208
+ node = node.parent;
209
+ }
210
+ }
211
+ setActiveAndVisible() {
212
+ this.ensureVisible();
213
+ this.setActive(true);
214
+ }
215
+ // Get all ancestors
216
+ getAncestors() {
217
+ const ancestors = [];
218
+ let node = this.parent;
219
+ while (node) {
220
+ ancestors.unshift(node);
221
+ node = node.parent;
222
+ }
223
+ return ancestors;
224
+ }
225
+ // Get all descendants
226
+ getDescendants() {
227
+ const descendants = [];
228
+ const traverse = (node) => {
229
+ node.children.forEach(child => {
230
+ descendants.push(child);
231
+ traverse(child);
232
+ });
233
+ };
234
+ traverse(this);
235
+ return descendants;
236
+ }
237
+ }
238
+
239
+ // lib/core/models/tree.model.ts
240
+ class TreeModel {
241
+ options;
242
+ // Reactive state with signals
243
+ roots = signal([]);
244
+ focusedNodeId = signal(null);
245
+ expandedNodeIds = signal(new Set());
246
+ activeNodeIds = signal(new Set());
247
+ selectedNodeIds = signal(new Set());
248
+ hiddenNodeIds = signal(new Set());
249
+ // Computed signals
250
+ focusedNode = computed(() => {
251
+ const id = this.focusedNodeId();
252
+ return id ? this.getNodeById(id) : null;
253
+ });
254
+ expandedNodes = computed(() => this.getAllNodes().filter(node => this.expandedNodeIds().has(node.id)));
255
+ activeNodes = computed(() => this.getAllNodes().filter(node => this.activeNodeIds().has(node.id)));
256
+ selectedNodes = computed(() => this.getAllNodes().filter(node => this.selectedNodeIds().has(node.id)));
257
+ visibleNodes = computed(() => this.getAllNodes().filter(node => !node.isHidden()));
258
+ flattenedNodes = computed(() => {
259
+ const flattened = [];
260
+ const traverse = (nodes) => {
261
+ nodes.forEach(node => {
262
+ if (!node.isHidden()) {
263
+ flattened.push(node);
264
+ if (node.isExpanded() && node.children.length > 0) {
265
+ traverse(node.children);
266
+ }
267
+ }
268
+ });
269
+ };
270
+ traverse(this.roots());
271
+ return flattened;
272
+ });
273
+ // Event streams
274
+ events$ = new Subject();
275
+ // Virtual root for unified handling
276
+ virtualRoot = null;
277
+ // Node registry for quick lookup
278
+ nodeRegistry = new Map();
279
+ constructor(options) {
280
+ this.options = options;
281
+ // Constructor sin effects - se sincronizan en las operaciones
282
+ }
283
+ // Data management
284
+ setData(data) {
285
+ this.nodeRegistry.clear();
286
+ const roots = this.buildNodes(data, null, 0);
287
+ this.roots.set(roots);
288
+ this.emitEvent({ type: 'initialized', treeModel: this });
289
+ }
290
+ buildNodes(data, parent, level) {
291
+ return data.map((item, index) => {
292
+ const node = new TreeNode(item, parent, level, index, this.options);
293
+ this.nodeRegistry.set(node.id, node);
294
+ // Recursively build children
295
+ const childrenData = item[this.options.childrenField || 'children'];
296
+ if (Array.isArray(childrenData) && childrenData.length > 0) {
297
+ node.children = this.buildNodes(childrenData, node, level + 1);
298
+ }
299
+ return node;
300
+ });
301
+ }
302
+ update() {
303
+ // Trigger change detection by creating new signal values
304
+ this.roots.set([...this.roots()]);
305
+ this.emitEvent({ type: 'update', treeModel: this });
306
+ }
307
+ // Node lookup
308
+ getNodeById(id) {
309
+ return this.nodeRegistry.get(id) || null;
310
+ }
311
+ getNodeBy(predicate) {
312
+ return this.getAllNodes().find(predicate) || null;
313
+ }
314
+ getAllNodes() {
315
+ const nodes = [];
316
+ const traverse = (nodeList) => {
317
+ nodeList.forEach(node => {
318
+ nodes.push(node);
319
+ if (node.children.length > 0) {
320
+ traverse(node.children);
321
+ }
322
+ });
323
+ };
324
+ traverse(this.roots());
325
+ return nodes;
326
+ }
327
+ // Navigation
328
+ focusNode(node) {
329
+ if (this.focusedNode()) {
330
+ this.focusedNode().setFocus(false);
331
+ }
332
+ if (node) {
333
+ this.focusedNodeId.set(node.id);
334
+ node.setFocus(true);
335
+ this.emitEvent({ type: 'focus', node });
336
+ }
337
+ else {
338
+ this.focusedNodeId.set(null);
339
+ }
340
+ }
341
+ focusNextNode() {
342
+ const flattened = this.flattenedNodes();
343
+ const currentIndex = flattened.findIndex(n => n.id === this.focusedNodeId());
344
+ if (currentIndex < flattened.length - 1) {
345
+ this.focusNode(flattened[currentIndex + 1]);
346
+ }
347
+ }
348
+ focusPreviousNode() {
349
+ const flattened = this.flattenedNodes();
350
+ const currentIndex = flattened.findIndex(n => n.id === this.focusedNodeId());
351
+ if (currentIndex > 0) {
352
+ this.focusNode(flattened[currentIndex - 1]);
353
+ }
354
+ }
355
+ focusDrillDown() {
356
+ const focused = this.focusedNode();
357
+ if (focused) {
358
+ if (!focused.isExpanded() && focused.hasChildren) {
359
+ focused.expand();
360
+ }
361
+ else if (focused.children.length > 0) {
362
+ this.focusNode(focused.children[0]);
363
+ }
364
+ }
365
+ }
366
+ focusDrillUp() {
367
+ const focused = this.focusedNode();
368
+ if (focused) {
369
+ if (focused.isExpanded() && focused.hasChildren) {
370
+ focused.collapse();
371
+ }
372
+ else if (focused.parent) {
373
+ this.focusNode(focused.parent);
374
+ }
375
+ }
376
+ }
377
+ // Operations
378
+ expandAll() {
379
+ const ids = new Set(this.getAllNodes().map(n => n.id));
380
+ this.expandedNodeIds.set(ids);
381
+ this.emitEvent({ type: 'expandAll', treeModel: this });
382
+ }
383
+ collapseAll() {
384
+ this.expandedNodeIds.set(new Set());
385
+ this.emitEvent({ type: 'collapseAll', treeModel: this });
386
+ }
387
+ expandNode(node) {
388
+ this.expandedNodeIds.update(ids => {
389
+ const newIds = new Set(ids);
390
+ newIds.add(node.id);
391
+ return newIds;
392
+ });
393
+ node.isExpanded.set(true);
394
+ this.emitEvent({ type: 'expand', node });
395
+ }
396
+ collapseNode(node) {
397
+ this.expandedNodeIds.update(ids => {
398
+ const newIds = new Set(ids);
399
+ newIds.delete(node.id);
400
+ return newIds;
401
+ });
402
+ node.isExpanded.set(false);
403
+ this.emitEvent({ type: 'collapse', node });
404
+ }
405
+ activateNode(node, multi = false) {
406
+ if (!multi) {
407
+ this.activeNodeIds.set(new Set([node.id]));
408
+ this.getAllNodes().forEach(n => n.isActive.set(n.id === node.id));
409
+ }
410
+ else {
411
+ this.activeNodeIds.update(ids => {
412
+ const newIds = new Set(ids);
413
+ newIds.add(node.id);
414
+ return newIds;
415
+ });
416
+ node.isActive.set(true);
417
+ }
418
+ this.emitEvent({ type: 'activate', node });
419
+ }
420
+ deactivateNode(node) {
421
+ this.activeNodeIds.update(ids => {
422
+ const newIds = new Set(ids);
423
+ newIds.delete(node.id);
424
+ return newIds;
425
+ });
426
+ node.isActive.set(false);
427
+ this.emitEvent({ type: 'deactivate', node });
428
+ }
429
+ selectNode(node, multi = false) {
430
+ if (!multi) {
431
+ this.selectedNodeIds.set(new Set([node.id]));
432
+ }
433
+ else {
434
+ this.selectedNodeIds.update(ids => {
435
+ const newIds = new Set(ids);
436
+ if (ids.has(node.id)) {
437
+ newIds.delete(node.id);
438
+ }
439
+ else {
440
+ newIds.add(node.id);
441
+ }
442
+ return newIds;
443
+ });
444
+ }
445
+ this.emitEvent({ type: 'select', node });
446
+ }
447
+ // Filtering
448
+ filterNodes(filterFn, autoShow = true) {
449
+ const hiddenIds = new Set();
450
+ this.getAllNodes().forEach(node => {
451
+ const matches = filterFn(node);
452
+ if (!matches) {
453
+ hiddenIds.add(node.id);
454
+ }
455
+ else if (autoShow) {
456
+ // Show ancestors of matching nodes
457
+ node.getAncestors().forEach(ancestor => {
458
+ hiddenIds.delete(ancestor.id);
459
+ });
460
+ }
461
+ });
462
+ this.hiddenNodeIds.set(hiddenIds);
463
+ this.emitEvent({ type: 'filter', treeModel: this });
464
+ }
465
+ clearFilter() {
466
+ this.hiddenNodeIds.set(new Set());
467
+ this.emitEvent({ type: 'clearFilter', treeModel: this });
468
+ }
469
+ // Drag & Drop
470
+ canMoveNode(node, to) {
471
+ // Prevent moving to itself or descendants
472
+ if (node === to.parent)
473
+ return false;
474
+ if (to.parent.getAncestors().includes(node))
475
+ return false;
476
+ // Custom validation
477
+ if (this.options.allowDrop) {
478
+ if (typeof this.options.allowDrop === 'function') {
479
+ return this.options.allowDrop(node, to);
480
+ }
481
+ return this.options.allowDrop;
482
+ }
483
+ return true;
484
+ }
485
+ moveNode(node, to) {
486
+ if (!this.canMoveNode(node, to))
487
+ return;
488
+ // Remove from old parent
489
+ if (node.parent) {
490
+ const oldIndex = node.parent.children.indexOf(node);
491
+ if (oldIndex !== -1) {
492
+ // Create new array to trigger change detection
493
+ node.parent.children = [
494
+ ...node.parent.children.slice(0, oldIndex),
495
+ ...node.parent.children.slice(oldIndex + 1)
496
+ ];
497
+ }
498
+ }
499
+ else {
500
+ // Handle root nodes
501
+ const rootIndex = this.roots().indexOf(node);
502
+ if (rootIndex !== -1) {
503
+ this.roots.set([
504
+ ...this.roots().slice(0, rootIndex),
505
+ ...this.roots().slice(rootIndex + 1)
506
+ ]);
507
+ }
508
+ }
509
+ // Add to new parent - create new array to trigger change detection
510
+ const newChildren = [...to.parent.children];
511
+ newChildren.splice(to.index, 0, node);
512
+ to.parent.children = newChildren;
513
+ node.parent = to.parent;
514
+ // Update levels
515
+ this.updateNodeLevels(node);
516
+ this.emitEvent({ type: 'moveNode', node, to });
517
+ this.update();
518
+ }
519
+ updateNodeLevels(node) {
520
+ const newLevel = node.parent ? node.parent.level + 1 : 0;
521
+ node.level = newLevel;
522
+ node.children.forEach(child => this.updateNodeLevels(child));
523
+ }
524
+ // Async children loading
525
+ async loadChildren(node) {
526
+ if (!this.options.getChildren)
527
+ return;
528
+ node.setLoading(true);
529
+ try {
530
+ const childrenData = await this.options.getChildren(node);
531
+ node.children = this.buildNodes(childrenData, node, node.level + 1);
532
+ node.hasChildren = node.children.length > 0;
533
+ this.emitEvent({ type: 'loadChildren', node });
534
+ this.update();
535
+ }
536
+ catch (error) {
537
+ this.emitEvent({ type: 'loadChildrenError', node, error });
538
+ }
539
+ finally {
540
+ node.setLoading(false);
541
+ }
542
+ }
543
+ // Events
544
+ emitEvent(event) {
545
+ this.events$.next(event);
546
+ }
547
+ }
548
+
549
+ const DEFAULT_TREE_OPTIONS = {
550
+ idField: 'id',
551
+ childrenField: 'children',
552
+ displayField: 'name',
553
+ isExpandedField: 'isExpanded',
554
+ hasChildrenField: 'hasChildren',
555
+ iconField: 'icon',
556
+ allowDrag: false,
557
+ allowDrop: false,
558
+ useCheckbox: false,
559
+ useTriState: false,
560
+ levelPadding: 20,
561
+ useVirtualScroll: false,
562
+ nodeHeight: 22,
563
+ bufferAmount: 5,
564
+ animateExpand: false,
565
+ animateSpeed: 1,
566
+ animateAcceleration: 1.2,
567
+ scrollOnActivate: true,
568
+ rtl: false,
569
+ multiSelect: false,
570
+ useMetaKey: true,
571
+ };
572
+
573
+ // lib/components/tree-node-expander/tree-node-expander.component.ts
574
+ class TreeNodeExpanderComponent {
575
+ node;
576
+ treeModel;
577
+ toggle = new EventEmitter();
578
+ onExpanderClick(event) {
579
+ event.stopPropagation();
580
+ this.toggle.emit();
581
+ }
582
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeNodeExpanderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
583
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: TreeNodeExpanderComponent, isStandalone: true, selector: "tree-node-expander", inputs: { node: "node", treeModel: "treeModel" }, outputs: { toggle: "toggle" }, ngImport: i0, template: `
584
+ <div class="tree-node-expander"
585
+ [class.tree-node-expander-loading]="node.isLoading()"
586
+ (click)="onExpanderClick($event)">
587
+ @if (node.isLoading()) {
588
+ <span class="expander-spinner"></span>
589
+ } @else if (node.hasChildren) {
590
+ <svg
591
+ class="expander-icon"
592
+ [class.expander-icon-expanded]="node.isExpanded()"
593
+ width="16"
594
+ height="16"
595
+ viewBox="0 0 16 16">
596
+ <path d="M6 4l4 4-4 4"
597
+ fill="none"
598
+ stroke="currentColor"
599
+ stroke-width="2"
600
+ stroke-linecap="round"/>
601
+ </svg>
602
+ } @else {
603
+ <span class="expander-placeholder"></span>
604
+ }
605
+ </div>
606
+ `, isInline: true, styles: [".tree-node-expander{display:flex;align-items:center;justify-content:center;width:20px;height:20px;cursor:pointer;border-radius:3px;transition:background-color .15s;flex-shrink:0}.tree-node-expander:hover{background-color:#0000000d}.expander-icon{transition:transform .2s ease;transform:rotate(0)}.expander-icon-expanded{transform:rotate(90deg)}.expander-placeholder{width:16px;height:16px}.expander-spinner{display:inline-block;width:14px;height:14px;border:2px solid #f3f3f3;border-top:2px solid #2196f3;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
607
+ }
608
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeNodeExpanderComponent, decorators: [{
609
+ type: Component,
610
+ args: [{ selector: 'tree-node-expander', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
611
+ <div class="tree-node-expander"
612
+ [class.tree-node-expander-loading]="node.isLoading()"
613
+ (click)="onExpanderClick($event)">
614
+ @if (node.isLoading()) {
615
+ <span class="expander-spinner"></span>
616
+ } @else if (node.hasChildren) {
617
+ <svg
618
+ class="expander-icon"
619
+ [class.expander-icon-expanded]="node.isExpanded()"
620
+ width="16"
621
+ height="16"
622
+ viewBox="0 0 16 16">
623
+ <path d="M6 4l4 4-4 4"
624
+ fill="none"
625
+ stroke="currentColor"
626
+ stroke-width="2"
627
+ stroke-linecap="round"/>
628
+ </svg>
629
+ } @else {
630
+ <span class="expander-placeholder"></span>
631
+ }
632
+ </div>
633
+ `, styles: [".tree-node-expander{display:flex;align-items:center;justify-content:center;width:20px;height:20px;cursor:pointer;border-radius:3px;transition:background-color .15s;flex-shrink:0}.tree-node-expander:hover{background-color:#0000000d}.expander-icon{transition:transform .2s ease;transform:rotate(0)}.expander-icon-expanded{transform:rotate(90deg)}.expander-placeholder{width:16px;height:16px}.expander-spinner{display:inline-block;width:14px;height:14px;border:2px solid #f3f3f3;border-top:2px solid #2196f3;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }]
634
+ }], propDecorators: { node: [{
635
+ type: Input,
636
+ args: [{ required: true }]
637
+ }], treeModel: [{
638
+ type: Input,
639
+ args: [{ required: true }]
640
+ }], toggle: [{
641
+ type: Output
642
+ }] } });
643
+
644
+ // lib/components/tree-node-checkbox/tree-node-checkbox.component.ts
645
+ class TreeNodeCheckboxComponent {
646
+ node;
647
+ treeModel;
648
+ useTriState = false;
649
+ change = new EventEmitter();
650
+ isChecked = computed(() => {
651
+ return this.treeModel.selectedNodeIds().has(this.node.id);
652
+ });
653
+ isIndeterminate = computed(() => {
654
+ if (!this.useTriState || !this.node.hasChildren) {
655
+ return false;
656
+ }
657
+ const selectedIds = this.treeModel.selectedNodeIds();
658
+ const descendants = this.node.getDescendants();
659
+ if (descendants.length === 0)
660
+ return false;
661
+ const selectedCount = descendants.filter(d => selectedIds.has(d.id)).length;
662
+ return selectedCount > 0 && selectedCount < descendants.length;
663
+ });
664
+ onCheckboxClick(event) {
665
+ event.stopPropagation();
666
+ }
667
+ onCheckboxChange(event) {
668
+ event.stopPropagation();
669
+ const checked = event.target.checked;
670
+ if (this.useTriState) {
671
+ // Update node and all descendants
672
+ this.updateNodeAndDescendants(this.node, checked);
673
+ }
674
+ else {
675
+ this.change.emit(checked);
676
+ }
677
+ }
678
+ updateNodeAndDescendants(node, checked) {
679
+ this.treeModel.selectedNodeIds.update(ids => {
680
+ const newIds = new Set(ids);
681
+ if (checked) {
682
+ newIds.add(node.id);
683
+ node.getDescendants().forEach(d => newIds.add(d.id));
684
+ }
685
+ else {
686
+ newIds.delete(node.id);
687
+ node.getDescendants().forEach(d => newIds.delete(d.id));
688
+ }
689
+ return newIds;
690
+ });
691
+ this.change.emit(checked);
692
+ }
693
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeNodeCheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
694
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: TreeNodeCheckboxComponent, isStandalone: true, selector: "tree-node-checkbox", inputs: { node: "node", treeModel: "treeModel", useTriState: "useTriState" }, outputs: { change: "change" }, ngImport: i0, template: `
695
+ <div class="tree-node-checkbox"
696
+ (click)="onCheckboxClick($event)">
697
+ <input
698
+ type="checkbox"
699
+ [checked]="isChecked()"
700
+ [indeterminate]="isIndeterminate()"
701
+ (change)="onCheckboxChange($event)"
702
+ (click)="$event.stopPropagation()">
703
+ </div>
704
+ `, isInline: true, styles: [".tree-node-checkbox{display:flex;align-items:center;justify-content:center;width:20px;height:20px;flex-shrink:0}input[type=checkbox]{cursor:pointer;width:16px;height:16px;margin:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
705
+ }
706
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeNodeCheckboxComponent, decorators: [{
707
+ type: Component,
708
+ args: [{ selector: 'tree-node-checkbox', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
709
+ <div class="tree-node-checkbox"
710
+ (click)="onCheckboxClick($event)">
711
+ <input
712
+ type="checkbox"
713
+ [checked]="isChecked()"
714
+ [indeterminate]="isIndeterminate()"
715
+ (change)="onCheckboxChange($event)"
716
+ (click)="$event.stopPropagation()">
717
+ </div>
718
+ `, styles: [".tree-node-checkbox{display:flex;align-items:center;justify-content:center;width:20px;height:20px;flex-shrink:0}input[type=checkbox]{cursor:pointer;width:16px;height:16px;margin:0}\n"] }]
719
+ }], propDecorators: { node: [{
720
+ type: Input,
721
+ args: [{ required: true }]
722
+ }], treeModel: [{
723
+ type: Input,
724
+ args: [{ required: true }]
725
+ }], useTriState: [{
726
+ type: Input
727
+ }], change: [{
728
+ type: Output
729
+ }] } });
730
+
731
+ // lib/components/tree-node-content/tree-node-content.component.ts
732
+ class TreeNodeContentComponent {
733
+ node;
734
+ treeModel;
735
+ template;
736
+ displayField = 'name';
737
+ NgOnInit() {
738
+ console.log("Tree node:", this.node);
739
+ }
740
+ get templateContext() {
741
+ console.log("Tree node:", this.node);
742
+ return {
743
+ $implicit: this.node,
744
+ node: this.node,
745
+ treeModel: this.treeModel,
746
+ };
747
+ }
748
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeNodeContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
749
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: TreeNodeContentComponent, isStandalone: true, selector: "tree-node-content", inputs: { node: "node", treeModel: "treeModel", template: "template", displayField: "displayField" }, ngImport: i0, template: `
750
+ @if (template) {
751
+ <ng-container
752
+ *ngTemplateOutlet="template; context: templateContext">
753
+ </ng-container>
754
+ } @else {
755
+ <span class="tree-node-content-default">
756
+ @if (node.icon) {
757
+ <img [src]="node.icon" alt="" class="tree-node-icon" />
758
+ }
759
+ <span class="tree-node-text">{{ node.data[displayField] }}</span>
760
+ </span>
761
+ }
762
+ `, isInline: true, styles: [".tree-node-content-default{flex:1;padding:0 4px;display:flex;align-items:center;gap:6px;overflow:hidden;-webkit-user-select:none;user-select:none}.tree-node-icon{width:16px;height:16px;flex-shrink:0;object-fit:contain}.tree-node-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
763
+ }
764
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeNodeContentComponent, decorators: [{
765
+ type: Component,
766
+ args: [{ selector: 'tree-node-content', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
767
+ @if (template) {
768
+ <ng-container
769
+ *ngTemplateOutlet="template; context: templateContext">
770
+ </ng-container>
771
+ } @else {
772
+ <span class="tree-node-content-default">
773
+ @if (node.icon) {
774
+ <img [src]="node.icon" alt="" class="tree-node-icon" />
775
+ }
776
+ <span class="tree-node-text">{{ node.data[displayField] }}</span>
777
+ </span>
778
+ }
779
+ `, styles: [".tree-node-content-default{flex:1;padding:0 4px;display:flex;align-items:center;gap:6px;overflow:hidden;-webkit-user-select:none;user-select:none}.tree-node-icon{width:16px;height:16px;flex-shrink:0;object-fit:contain}.tree-node-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
780
+ }], propDecorators: { node: [{
781
+ type: Input,
782
+ args: [{ required: true }]
783
+ }], treeModel: [{
784
+ type: Input,
785
+ args: [{ required: true }]
786
+ }], template: [{
787
+ type: Input
788
+ }], displayField: [{
789
+ type: Input
790
+ }] } });
791
+
792
+ // lib/core/services/tree-drag-drop.service.ts
793
+ class TreeDragDropService {
794
+ draggedNode = signal(null);
795
+ setDraggedNode(node) {
796
+ this.draggedNode.set(node);
797
+ }
798
+ getDraggedNode() {
799
+ return this.draggedNode();
800
+ }
801
+ clearDraggedNode() {
802
+ this.draggedNode.set(null);
803
+ }
804
+ isDragging() {
805
+ return this.draggedNode() !== null;
806
+ }
807
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeDragDropService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
808
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeDragDropService, providedIn: 'root' });
809
+ }
810
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeDragDropService, decorators: [{
811
+ type: Injectable,
812
+ args: [{
813
+ providedIn: 'root',
814
+ }]
815
+ }] });
816
+
817
+ // lib/directives/tree-drag.directive.ts
818
+ class TreeDragDirective {
819
+ node;
820
+ treeModel;
821
+ options;
822
+ // Use inject() function instead of constructor injection
823
+ elementRef = inject(ElementRef);
824
+ dragDropService = inject(TreeDragDropService);
825
+ get draggable() {
826
+ if (typeof this.options.allowDrag === 'function') {
827
+ return this.options.allowDrag(this.node);
828
+ }
829
+ return this.options.allowDrag || false;
830
+ }
831
+ onDragStart(event) {
832
+ if (!this.draggable) {
833
+ event.preventDefault();
834
+ return;
835
+ }
836
+ this.dragDropService.setDraggedNode(this.node);
837
+ if (event.dataTransfer) {
838
+ event.dataTransfer.effectAllowed = 'move';
839
+ event.dataTransfer.setData('text/plain', this.node.id.toString());
840
+ // Create drag image
841
+ const dragImage = this.elementRef.nativeElement.cloneNode(true);
842
+ dragImage.style.opacity = '0.7';
843
+ document.body.appendChild(dragImage);
844
+ event.dataTransfer.setDragImage(dragImage, 0, 0);
845
+ setTimeout(() => document.body.removeChild(dragImage), 0);
846
+ }
847
+ this.elementRef.nativeElement.classList.add('tree-node-dragging');
848
+ }
849
+ onDragEnd(event) {
850
+ this.dragDropService.clearDraggedNode();
851
+ this.elementRef.nativeElement.classList.remove('tree-node-dragging');
852
+ }
853
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeDragDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
854
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: TreeDragDirective, isStandalone: true, selector: "[treeDrag]", inputs: { node: ["treeDrag", "node"], treeModel: "treeModel", options: "options" }, host: { listeners: { "dragstart": "onDragStart($event)", "dragend": "onDragEnd($event)" }, properties: { "attr.draggable": "this.draggable" } }, ngImport: i0 });
855
+ }
856
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeDragDirective, decorators: [{
857
+ type: Directive,
858
+ args: [{
859
+ selector: '[treeDrag]',
860
+ standalone: true,
861
+ }]
862
+ }], propDecorators: { node: [{
863
+ type: Input,
864
+ args: [{ required: true, alias: 'treeDrag' }]
865
+ }], treeModel: [{
866
+ type: Input,
867
+ args: [{ required: true }]
868
+ }], options: [{
869
+ type: Input,
870
+ args: [{ required: true }]
871
+ }], draggable: [{
872
+ type: HostBinding,
873
+ args: ['attr.draggable']
874
+ }], onDragStart: [{
875
+ type: HostListener,
876
+ args: ['dragstart', ['$event']]
877
+ }], onDragEnd: [{
878
+ type: HostListener,
879
+ args: ['dragend', ['$event']]
880
+ }] } });
881
+
882
+ // lib/directives/tree-drop.directive.ts
883
+ class TreeDropDirective {
884
+ node;
885
+ treeModel;
886
+ options;
887
+ dropPosition = 'inside';
888
+ // Use inject() function instead of constructor injection
889
+ elementRef = inject(ElementRef);
890
+ dragDropService = inject(TreeDragDropService);
891
+ isDraggingOver = signal(false);
892
+ canDrop = signal(false);
893
+ get isDropTarget() {
894
+ return this.isDraggingOver() && this.canDrop();
895
+ }
896
+ get isDropDisabled() {
897
+ return this.isDraggingOver() && !this.canDrop();
898
+ }
899
+ onDragEnter(event) {
900
+ event.preventDefault();
901
+ const draggedNode = this.dragDropService.getDraggedNode();
902
+ if (!draggedNode)
903
+ return;
904
+ this.isDraggingOver.set(true);
905
+ const dropTarget = this.getDropTarget();
906
+ const canDrop = this.treeModel.canMoveNode(draggedNode, dropTarget);
907
+ this.canDrop.set(canDrop);
908
+ if (canDrop && event.dataTransfer) {
909
+ event.dataTransfer.dropEffect = 'move';
910
+ }
911
+ }
912
+ onDragOver(event) {
913
+ if (this.canDrop()) {
914
+ event.preventDefault();
915
+ if (event.dataTransfer) {
916
+ event.dataTransfer.dropEffect = 'move';
917
+ }
918
+ }
919
+ }
920
+ onDragLeave(event) {
921
+ // Check if we're really leaving (not just entering a child element)
922
+ const rect = this.elementRef.nativeElement.getBoundingClientRect();
923
+ const x = event.clientX;
924
+ const y = event.clientY;
925
+ if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
926
+ this.isDraggingOver.set(false);
927
+ this.canDrop.set(false);
928
+ }
929
+ }
930
+ onDrop(event) {
931
+ event.preventDefault();
932
+ event.stopPropagation();
933
+ const draggedNode = this.dragDropService.getDraggedNode();
934
+ if (!draggedNode || !this.canDrop())
935
+ return;
936
+ const dropTarget = this.getDropTarget();
937
+ this.treeModel.moveNode(draggedNode, dropTarget);
938
+ this.isDraggingOver.set(false);
939
+ this.canDrop.set(false);
940
+ this.dragDropService.clearDraggedNode();
941
+ }
942
+ getDropTarget() {
943
+ switch (this.dropPosition) {
944
+ case 'before':
945
+ return {
946
+ parent: this.node.parent,
947
+ index: this.node.index,
948
+ };
949
+ case 'after':
950
+ return {
951
+ parent: this.node.parent,
952
+ index: this.node.index + 1,
953
+ };
954
+ case 'inside':
955
+ default:
956
+ return {
957
+ parent: this.node,
958
+ index: this.node.children.length,
959
+ };
960
+ }
961
+ }
962
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeDropDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
963
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: TreeDropDirective, isStandalone: true, selector: "[treeDrop]", inputs: { node: ["treeDrop", "node"], treeModel: "treeModel", options: "options", dropPosition: "dropPosition" }, host: { listeners: { "dragenter": "onDragEnter($event)", "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "class.tree-drop-target": "this.isDropTarget", "class.tree-drop-disabled": "this.isDropDisabled" } }, ngImport: i0 });
964
+ }
965
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeDropDirective, decorators: [{
966
+ type: Directive,
967
+ args: [{
968
+ selector: '[treeDrop]',
969
+ standalone: true,
970
+ }]
971
+ }], propDecorators: { node: [{
972
+ type: Input,
973
+ args: [{ required: true, alias: 'treeDrop' }]
974
+ }], treeModel: [{
975
+ type: Input,
976
+ args: [{ required: true }]
977
+ }], options: [{
978
+ type: Input,
979
+ args: [{ required: true }]
980
+ }], dropPosition: [{
981
+ type: Input
982
+ }], isDropTarget: [{
983
+ type: HostBinding,
984
+ args: ['class.tree-drop-target']
985
+ }], isDropDisabled: [{
986
+ type: HostBinding,
987
+ args: ['class.tree-drop-disabled']
988
+ }], onDragEnter: [{
989
+ type: HostListener,
990
+ args: ['dragenter', ['$event']]
991
+ }], onDragOver: [{
992
+ type: HostListener,
993
+ args: ['dragover', ['$event']]
994
+ }], onDragLeave: [{
995
+ type: HostListener,
996
+ args: ['dragleave', ['$event']]
997
+ }], onDrop: [{
998
+ type: HostListener,
999
+ args: ['drop', ['$event']]
1000
+ }] } });
1001
+
1002
+ // lib/components/tree-node/tree-node.component.ts
1003
+ class TreeNodeComponent {
1004
+ node;
1005
+ treeModel;
1006
+ options;
1007
+ nodeTemplate;
1008
+ paddingLeft = computed(() => {
1009
+ return this.options.levelPadding || 40;
1010
+ });
1011
+ onNodeClick(event) {
1012
+ const useMetaKey = this.options.useMetaKey !== false;
1013
+ const multiSelect = this.options.multiSelect || false;
1014
+ const isMetaKeyPressed = event.ctrlKey || event.metaKey;
1015
+ const shouldMultiSelect = multiSelect && (!useMetaKey || isMetaKeyPressed);
1016
+ this.treeModel.activateNode(this.node, shouldMultiSelect);
1017
+ this.treeModel.focusNode(this.node);
1018
+ }
1019
+ onNodeDoubleClick(event) {
1020
+ if (this.node.hasChildren) {
1021
+ this.node.toggle();
1022
+ this.treeModel.expandedNodeIds.update(ids => {
1023
+ const newIds = new Set(ids);
1024
+ if (this.node.isExpanded()) {
1025
+ newIds.add(this.node.id);
1026
+ }
1027
+ else {
1028
+ newIds.delete(this.node.id);
1029
+ }
1030
+ return newIds;
1031
+ });
1032
+ }
1033
+ }
1034
+ onNodeContextMenu(event) {
1035
+ event.preventDefault();
1036
+ // Emit context menu event
1037
+ }
1038
+ onToggleExpanded() {
1039
+ if (this.node.hasChildren) {
1040
+ this.node.toggle();
1041
+ if (this.node.isExpanded()) {
1042
+ // Load children if needed
1043
+ if (this.options.getChildren && this.node.children.length === 0) {
1044
+ this.treeModel.loadChildren(this.node);
1045
+ }
1046
+ this.treeModel.expandNode(this.node);
1047
+ }
1048
+ else {
1049
+ this.treeModel.collapseNode(this.node);
1050
+ }
1051
+ }
1052
+ }
1053
+ onCheckboxChange(checked) {
1054
+ this.treeModel.selectNode(this.node, this.options.multiSelect || false);
1055
+ }
1056
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1057
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: TreeNodeComponent, isStandalone: true, selector: "tree-node", inputs: { node: "node", treeModel: "treeModel", options: "options", nodeTemplate: "nodeTemplate" }, ngImport: i0, template: `
1058
+ <div
1059
+ class="tree-node"
1060
+ [class.tree-node-expanded]="node.isExpanded()"
1061
+ [class.tree-node-collapsed]="node.isCollapsed()"
1062
+ [class.tree-node-active]="node.isActive()"
1063
+ [class.tree-node-focused]="node.isFocused()"
1064
+ [class.tree-node-leaf]="node.isLeaf()"
1065
+ [class.tree-node-loading]="node.isLoading()"
1066
+ [style.padding-left.px]="paddingLeft()">
1067
+
1068
+ <div class="tree-node-wrapper"
1069
+ [treeDrag]="node"
1070
+ [treeModel]="treeModel"
1071
+ [options]="options">
1072
+
1073
+ <!-- Drop zone before node -->
1074
+ <div class="tree-node-drop-slot tree-node-drop-slot-before"
1075
+ [treeDrop]="node"
1076
+ [treeModel]="treeModel"
1077
+ [dropPosition]="'before'"
1078
+ [options]="options">
1079
+ </div>
1080
+
1081
+ <!-- Node content wrapper -->
1082
+ <div class="tree-node-content-wrapper"
1083
+ (click)="onNodeClick($event)"
1084
+ (dblclick)="onNodeDoubleClick($event)"
1085
+ (contextmenu)="onNodeContextMenu($event)">
1086
+
1087
+ <!-- Expander -->
1088
+ @if (node.hasChildren) {
1089
+ <tree-node-expander
1090
+ [node]="node"
1091
+ [treeModel]="treeModel"
1092
+ (toggle)="onToggleExpanded()">
1093
+ </tree-node-expander>
1094
+ }
1095
+
1096
+ <!-- Checkbox -->
1097
+ @if (options.useCheckbox) {
1098
+ <tree-node-checkbox
1099
+ [node]="node"
1100
+ [treeModel]="treeModel"
1101
+ [useTriState]="options.useTriState ?? false"
1102
+ (change)="onCheckboxChange($event)">
1103
+ </tree-node-checkbox>
1104
+ }
1105
+
1106
+ <!-- Content -->
1107
+ <tree-node-content
1108
+ [node]="node"
1109
+ [treeModel]="treeModel"
1110
+ [template]="nodeTemplate"
1111
+ [displayField]="options.displayField || 'name'">
1112
+ </tree-node-content>
1113
+ </div>
1114
+
1115
+ <!-- Drop zone after node -->
1116
+ <div class="tree-node-drop-slot tree-node-drop-slot-after"
1117
+ [treeDrop]="node"
1118
+ [treeModel]="treeModel"
1119
+ [dropPosition]="'after'"
1120
+ [options]="options">
1121
+ </div>
1122
+ </div>
1123
+
1124
+ <!-- Children container -->
1125
+ @if (node.isExpanded() && node.children.length > 0) {
1126
+ <div class="tree-node-children"
1127
+ [@expandCollapse]="node.isExpanded() ? 'expanded' : 'collapsed'">
1128
+ @for (child of node.children; track child.id) {
1129
+ <tree-node
1130
+ [node]="child"
1131
+ [treeModel]="treeModel"
1132
+ [options]="options"
1133
+ [nodeTemplate]="nodeTemplate">
1134
+ </tree-node>
1135
+ }
1136
+ </div>
1137
+ }
1138
+
1139
+ <!-- Loading indicator -->
1140
+ @if (node.isLoading()) {
1141
+ <div class="tree-node-loading">
1142
+ <span class="loading-spinner"></span>
1143
+ Loading...
1144
+ </div>
1145
+ }
1146
+ </div>
1147
+ `, isInline: true, styles: [".tree-node{position:relative}.tree-node-wrapper{position:relative;display:flex;align-items:center;min-height:22px}.tree-node-content-wrapper{display:flex;align-items:center;flex:1;cursor:pointer;border-radius:3px;padding:2px 4px;transition:background-color .15s}.tree-node-content-wrapper:hover{background-color:#f5f5f5}.tree-node-active .tree-node-content-wrapper{background-color:#e3f2fd}.tree-node-focused .tree-node-content-wrapper{outline:2px solid #2196f3;outline-offset:-2px}.tree-node-children{overflow:hidden}.tree-node-drop-slot{position:absolute;left:0;right:0;height:2px;z-index:10}.tree-node-drop-slot-before{top:-1px}.tree-node-drop-slot-after{bottom:-1px}.tree-node-loading{padding:8px 24px;display:flex;align-items:center;gap:8px;color:#666;font-size:14px}.loading-spinner{display:inline-block;width:16px;height:16px;border:2px solid #f3f3f3;border-top:2px solid #2196f3;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "component", type: TreeNodeComponent, selector: "tree-node", inputs: ["node", "treeModel", "options", "nodeTemplate"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: TreeNodeExpanderComponent, selector: "tree-node-expander", inputs: ["node", "treeModel"], outputs: ["toggle"] }, { kind: "component", type: TreeNodeCheckboxComponent, selector: "tree-node-checkbox", inputs: ["node", "treeModel", "useTriState"], outputs: ["change"] }, { kind: "component", type: TreeNodeContentComponent, selector: "tree-node-content", inputs: ["node", "treeModel", "template", "displayField"] }, { kind: "directive", type: TreeDragDirective, selector: "[treeDrag]", inputs: ["treeDrag", "treeModel", "options"] }, { kind: "directive", type: TreeDropDirective, selector: "[treeDrop]", inputs: ["treeDrop", "treeModel", "options", "dropPosition"] }], animations: [
1148
+ trigger('expandCollapse', [
1149
+ state('collapsed', style({
1150
+ height: '0',
1151
+ overflow: 'hidden',
1152
+ opacity: '0'
1153
+ })),
1154
+ state('expanded', style({
1155
+ height: '*',
1156
+ overflow: 'visible',
1157
+ opacity: '1'
1158
+ })),
1159
+ transition('collapsed <=> expanded', [
1160
+ animate('200ms ease-in-out')
1161
+ ])
1162
+ ])
1163
+ ], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1164
+ }
1165
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeNodeComponent, decorators: [{
1166
+ type: Component,
1167
+ args: [{ selector: 'tree-node', standalone: true, imports: [
1168
+ CommonModule,
1169
+ TreeNodeExpanderComponent,
1170
+ TreeNodeCheckboxComponent,
1171
+ TreeNodeContentComponent,
1172
+ TreeDragDirective,
1173
+ TreeDropDirective,
1174
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1175
+ <div
1176
+ class="tree-node"
1177
+ [class.tree-node-expanded]="node.isExpanded()"
1178
+ [class.tree-node-collapsed]="node.isCollapsed()"
1179
+ [class.tree-node-active]="node.isActive()"
1180
+ [class.tree-node-focused]="node.isFocused()"
1181
+ [class.tree-node-leaf]="node.isLeaf()"
1182
+ [class.tree-node-loading]="node.isLoading()"
1183
+ [style.padding-left.px]="paddingLeft()">
1184
+
1185
+ <div class="tree-node-wrapper"
1186
+ [treeDrag]="node"
1187
+ [treeModel]="treeModel"
1188
+ [options]="options">
1189
+
1190
+ <!-- Drop zone before node -->
1191
+ <div class="tree-node-drop-slot tree-node-drop-slot-before"
1192
+ [treeDrop]="node"
1193
+ [treeModel]="treeModel"
1194
+ [dropPosition]="'before'"
1195
+ [options]="options">
1196
+ </div>
1197
+
1198
+ <!-- Node content wrapper -->
1199
+ <div class="tree-node-content-wrapper"
1200
+ (click)="onNodeClick($event)"
1201
+ (dblclick)="onNodeDoubleClick($event)"
1202
+ (contextmenu)="onNodeContextMenu($event)">
1203
+
1204
+ <!-- Expander -->
1205
+ @if (node.hasChildren) {
1206
+ <tree-node-expander
1207
+ [node]="node"
1208
+ [treeModel]="treeModel"
1209
+ (toggle)="onToggleExpanded()">
1210
+ </tree-node-expander>
1211
+ }
1212
+
1213
+ <!-- Checkbox -->
1214
+ @if (options.useCheckbox) {
1215
+ <tree-node-checkbox
1216
+ [node]="node"
1217
+ [treeModel]="treeModel"
1218
+ [useTriState]="options.useTriState ?? false"
1219
+ (change)="onCheckboxChange($event)">
1220
+ </tree-node-checkbox>
1221
+ }
1222
+
1223
+ <!-- Content -->
1224
+ <tree-node-content
1225
+ [node]="node"
1226
+ [treeModel]="treeModel"
1227
+ [template]="nodeTemplate"
1228
+ [displayField]="options.displayField || 'name'">
1229
+ </tree-node-content>
1230
+ </div>
1231
+
1232
+ <!-- Drop zone after node -->
1233
+ <div class="tree-node-drop-slot tree-node-drop-slot-after"
1234
+ [treeDrop]="node"
1235
+ [treeModel]="treeModel"
1236
+ [dropPosition]="'after'"
1237
+ [options]="options">
1238
+ </div>
1239
+ </div>
1240
+
1241
+ <!-- Children container -->
1242
+ @if (node.isExpanded() && node.children.length > 0) {
1243
+ <div class="tree-node-children"
1244
+ [@expandCollapse]="node.isExpanded() ? 'expanded' : 'collapsed'">
1245
+ @for (child of node.children; track child.id) {
1246
+ <tree-node
1247
+ [node]="child"
1248
+ [treeModel]="treeModel"
1249
+ [options]="options"
1250
+ [nodeTemplate]="nodeTemplate">
1251
+ </tree-node>
1252
+ }
1253
+ </div>
1254
+ }
1255
+
1256
+ <!-- Loading indicator -->
1257
+ @if (node.isLoading()) {
1258
+ <div class="tree-node-loading">
1259
+ <span class="loading-spinner"></span>
1260
+ Loading...
1261
+ </div>
1262
+ }
1263
+ </div>
1264
+ `, animations: [
1265
+ trigger('expandCollapse', [
1266
+ state('collapsed', style({
1267
+ height: '0',
1268
+ overflow: 'hidden',
1269
+ opacity: '0'
1270
+ })),
1271
+ state('expanded', style({
1272
+ height: '*',
1273
+ overflow: 'visible',
1274
+ opacity: '1'
1275
+ })),
1276
+ transition('collapsed <=> expanded', [
1277
+ animate('200ms ease-in-out')
1278
+ ])
1279
+ ])
1280
+ ], styles: [".tree-node{position:relative}.tree-node-wrapper{position:relative;display:flex;align-items:center;min-height:22px}.tree-node-content-wrapper{display:flex;align-items:center;flex:1;cursor:pointer;border-radius:3px;padding:2px 4px;transition:background-color .15s}.tree-node-content-wrapper:hover{background-color:#f5f5f5}.tree-node-active .tree-node-content-wrapper{background-color:#e3f2fd}.tree-node-focused .tree-node-content-wrapper{outline:2px solid #2196f3;outline-offset:-2px}.tree-node-children{overflow:hidden}.tree-node-drop-slot{position:absolute;left:0;right:0;height:2px;z-index:10}.tree-node-drop-slot-before{top:-1px}.tree-node-drop-slot-after{bottom:-1px}.tree-node-loading{padding:8px 24px;display:flex;align-items:center;gap:8px;color:#666;font-size:14px}.loading-spinner{display:inline-block;width:16px;height:16px;border:2px solid #f3f3f3;border-top:2px solid #2196f3;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }]
1281
+ }], propDecorators: { node: [{
1282
+ type: Input,
1283
+ args: [{ required: true }]
1284
+ }], treeModel: [{
1285
+ type: Input,
1286
+ args: [{ required: true }]
1287
+ }], options: [{
1288
+ type: Input,
1289
+ args: [{ required: true }]
1290
+ }], nodeTemplate: [{
1291
+ type: Input
1292
+ }] } });
1293
+
1294
+ // lib/components/tree-viewport/tree-viewport.component.ts
1295
+ class TreeViewportComponent {
1296
+ treeModel;
1297
+ options;
1298
+ nodeTemplate;
1299
+ scrollContainer;
1300
+ destroy$ = new Subject();
1301
+ scrollTop = signal(0);
1302
+ viewportHeight = signal(0);
1303
+ flattenedNodes = computed(() => {
1304
+ return this.treeModel.flattenedNodes();
1305
+ });
1306
+ totalHeight = computed(() => {
1307
+ const nodes = this.flattenedNodes();
1308
+ const nodeHeight = this.options.nodeHeight || 22;
1309
+ if (typeof nodeHeight === 'number') {
1310
+ return nodes.length * nodeHeight;
1311
+ }
1312
+ return nodes.reduce((sum, node) => sum + nodeHeight(node), 0);
1313
+ });
1314
+ visibleRange = computed(() => {
1315
+ const scrollTop = this.scrollTop();
1316
+ const viewportHeight = this.viewportHeight();
1317
+ const nodes = this.flattenedNodes();
1318
+ const nodeHeight = this.options.nodeHeight || 22;
1319
+ const bufferAmount = this.options.bufferAmount || 5;
1320
+ let startIndex = 0;
1321
+ let accumulatedHeight = 0;
1322
+ // Find start index
1323
+ for (let i = 0; i < nodes.length; i++) {
1324
+ const height = typeof nodeHeight === 'number'
1325
+ ? nodeHeight
1326
+ : nodeHeight(nodes[i]);
1327
+ if (accumulatedHeight + height > scrollTop) {
1328
+ startIndex = Math.max(0, i - bufferAmount);
1329
+ break;
1330
+ }
1331
+ accumulatedHeight += height;
1332
+ }
1333
+ // Find end index
1334
+ let endIndex = startIndex;
1335
+ accumulatedHeight = 0;
1336
+ for (let i = startIndex; i < nodes.length; i++) {
1337
+ const height = typeof nodeHeight === 'number'
1338
+ ? nodeHeight
1339
+ : nodeHeight(nodes[i]);
1340
+ accumulatedHeight += height;
1341
+ if (accumulatedHeight > viewportHeight + (bufferAmount * (typeof nodeHeight === 'number' ? nodeHeight : 22))) {
1342
+ endIndex = i;
1343
+ break;
1344
+ }
1345
+ }
1346
+ if (endIndex === startIndex) {
1347
+ endIndex = nodes.length;
1348
+ }
1349
+ return { startIndex, endIndex };
1350
+ });
1351
+ visibleNodes = computed(() => {
1352
+ const range = this.visibleRange();
1353
+ const nodes = this.flattenedNodes();
1354
+ return nodes.slice(range.startIndex, range.endIndex);
1355
+ });
1356
+ paddingTop = computed(() => {
1357
+ const range = this.visibleRange();
1358
+ const nodes = this.flattenedNodes();
1359
+ const nodeHeight = this.options.nodeHeight || 22;
1360
+ let height = 0;
1361
+ for (let i = 0; i < range.startIndex; i++) {
1362
+ height += typeof nodeHeight === 'number'
1363
+ ? nodeHeight
1364
+ : nodeHeight(nodes[i]);
1365
+ }
1366
+ return height;
1367
+ });
1368
+ paddingBottom = computed(() => {
1369
+ const range = this.visibleRange();
1370
+ const nodes = this.flattenedNodes();
1371
+ const nodeHeight = this.options.nodeHeight || 22;
1372
+ const totalHeight = this.totalHeight();
1373
+ let visibleHeight = this.paddingTop();
1374
+ for (let i = range.startIndex; i < range.endIndex; i++) {
1375
+ visibleHeight += typeof nodeHeight === 'number'
1376
+ ? nodeHeight
1377
+ : nodeHeight(nodes[i]);
1378
+ }
1379
+ return Math.max(0, totalHeight - visibleHeight);
1380
+ });
1381
+ constructor() {
1382
+ // Update viewport when tree changes
1383
+ effect(() => {
1384
+ this.flattenedNodes(); // Subscribe to changes
1385
+ this.updateViewport();
1386
+ });
1387
+ }
1388
+ ngOnInit() {
1389
+ this.setupScrollListener();
1390
+ this.updateViewportHeight();
1391
+ }
1392
+ ngOnDestroy() {
1393
+ this.destroy$.next();
1394
+ this.destroy$.complete();
1395
+ }
1396
+ setupScrollListener() {
1397
+ fromEvent(this.scrollContainer.nativeElement, 'scroll')
1398
+ .pipe(debounceTime(10), takeUntil(this.destroy$))
1399
+ .subscribe(() => {
1400
+ this.onScroll();
1401
+ });
1402
+ }
1403
+ updateViewportHeight() {
1404
+ const height = this.scrollContainer.nativeElement.clientHeight;
1405
+ this.viewportHeight.set(height);
1406
+ }
1407
+ onScroll() {
1408
+ const scrollTop = this.scrollContainer.nativeElement.scrollTop;
1409
+ this.scrollTop.set(scrollTop);
1410
+ }
1411
+ updateViewport() {
1412
+ // Force recalculation
1413
+ this.scrollTop.set(this.scrollContainer.nativeElement.scrollTop);
1414
+ }
1415
+ scrollToNode(node) {
1416
+ const nodes = this.flattenedNodes();
1417
+ const index = nodes.findIndex(n => n.id === node.id);
1418
+ if (index === -1)
1419
+ return;
1420
+ const nodeHeight = this.options.nodeHeight || 22;
1421
+ let targetScrollTop = 0;
1422
+ for (let i = 0; i < index; i++) {
1423
+ targetScrollTop += typeof nodeHeight === 'number'
1424
+ ? nodeHeight
1425
+ : nodeHeight(nodes[i]);
1426
+ }
1427
+ this.scrollContainer.nativeElement.scrollTop = targetScrollTop;
1428
+ }
1429
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeViewportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1430
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: TreeViewportComponent, isStandalone: true, selector: "tree-viewport", inputs: { treeModel: "treeModel", options: "options", nodeTemplate: "nodeTemplate" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, static: true }], ngImport: i0, template: `
1431
+ <div
1432
+ #scrollContainer
1433
+ class="tree-viewport-container"
1434
+ [style.height.px]="options.scrollContainerHeight || 400"
1435
+ (scroll)="onScroll()">
1436
+
1437
+ <div class="tree-viewport-content"
1438
+ [style.height.px]="totalHeight()">
1439
+
1440
+ <div class="tree-viewport-padding-top"
1441
+ [style.height.px]="paddingTop()">
1442
+ </div>
1443
+
1444
+ @for (node of visibleNodes(); track node.id) {
1445
+ <tree-node
1446
+ [node]="node"
1447
+ [treeModel]="treeModel"
1448
+ [options]="options"
1449
+ [nodeTemplate]="nodeTemplate">
1450
+ </tree-node>
1451
+ }
1452
+
1453
+ <div class="tree-viewport-padding-bottom"
1454
+ [style.height.px]="paddingBottom()">
1455
+ </div>
1456
+ </div>
1457
+ </div>
1458
+ `, isInline: true, styles: [".tree-viewport-container{overflow-y:auto;overflow-x:hidden;position:relative}.tree-viewport-content{position:relative;width:100%}.tree-viewport-padding-top,.tree-viewport-padding-bottom{width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TreeNodeComponent, selector: "tree-node", inputs: ["node", "treeModel", "options", "nodeTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1459
+ }
1460
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeViewportComponent, decorators: [{
1461
+ type: Component,
1462
+ args: [{ selector: 'tree-viewport', standalone: true, imports: [CommonModule, TreeNodeComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1463
+ <div
1464
+ #scrollContainer
1465
+ class="tree-viewport-container"
1466
+ [style.height.px]="options.scrollContainerHeight || 400"
1467
+ (scroll)="onScroll()">
1468
+
1469
+ <div class="tree-viewport-content"
1470
+ [style.height.px]="totalHeight()">
1471
+
1472
+ <div class="tree-viewport-padding-top"
1473
+ [style.height.px]="paddingTop()">
1474
+ </div>
1475
+
1476
+ @for (node of visibleNodes(); track node.id) {
1477
+ <tree-node
1478
+ [node]="node"
1479
+ [treeModel]="treeModel"
1480
+ [options]="options"
1481
+ [nodeTemplate]="nodeTemplate">
1482
+ </tree-node>
1483
+ }
1484
+
1485
+ <div class="tree-viewport-padding-bottom"
1486
+ [style.height.px]="paddingBottom()">
1487
+ </div>
1488
+ </div>
1489
+ </div>
1490
+ `, styles: [".tree-viewport-container{overflow-y:auto;overflow-x:hidden;position:relative}.tree-viewport-content{position:relative;width:100%}.tree-viewport-padding-top,.tree-viewport-padding-bottom{width:100%}\n"] }]
1491
+ }], ctorParameters: () => [], propDecorators: { treeModel: [{
1492
+ type: Input,
1493
+ args: [{ required: true }]
1494
+ }], options: [{
1495
+ type: Input,
1496
+ args: [{ required: true }]
1497
+ }], nodeTemplate: [{
1498
+ type: Input
1499
+ }], scrollContainer: [{
1500
+ type: ViewChild,
1501
+ args: ['scrollContainer', { static: true }]
1502
+ }] } });
1503
+
1504
+ // lib/directives/tree-node-template.directive.ts
1505
+ class TreeNodeTemplateDirective {
1506
+ template;
1507
+ constructor(template) {
1508
+ this.template = template;
1509
+ }
1510
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeNodeTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
1511
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: TreeNodeTemplateDirective, isStandalone: true, selector: "[treeNodeTemplate]", ngImport: i0 });
1512
+ }
1513
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeNodeTemplateDirective, decorators: [{
1514
+ type: Directive,
1515
+ args: [{
1516
+ selector: '[treeNodeTemplate]',
1517
+ standalone: true,
1518
+ }]
1519
+ }], ctorParameters: () => [{ type: i0.TemplateRef }] });
1520
+
1521
+ // lib/components/tree-root/tree-root.component.ts
1522
+ class TreeRootComponent {
1523
+ nodes = [];
1524
+ options = {};
1525
+ initialized = new EventEmitter();
1526
+ toggleExpanded = new EventEmitter();
1527
+ activate = new EventEmitter();
1528
+ deactivate = new EventEmitter();
1529
+ select = new EventEmitter();
1530
+ deselect = new EventEmitter();
1531
+ focus = new EventEmitter();
1532
+ blur = new EventEmitter();
1533
+ moveNode = new EventEmitter();
1534
+ loadChildren = new EventEmitter();
1535
+ treeEvent = new EventEmitter();
1536
+ // Content queries
1537
+ nodeTemplate = contentChild(TreeNodeTemplateDirective, {
1538
+ read: TemplateRef
1539
+ });
1540
+ treeModel;
1541
+ mergedOptions;
1542
+ isFocused = signal(false);
1543
+ eventsSubscription;
1544
+ ngOnInit() {
1545
+ this.initializeTreeModel();
1546
+ }
1547
+ ngOnChanges(changes) {
1548
+ if (changes['options'] && !changes['options'].firstChange) {
1549
+ this.initializeTreeModel();
1550
+ }
1551
+ if (changes['nodes'] && !changes['nodes'].firstChange) {
1552
+ this.treeModel.setData(this.nodes);
1553
+ }
1554
+ }
1555
+ initializeTreeModel() {
1556
+ // Unsubscribe from previous subscription if exists
1557
+ this.eventsSubscription?.unsubscribe();
1558
+ this.mergedOptions = {
1559
+ ...DEFAULT_TREE_OPTIONS,
1560
+ ...this.options,
1561
+ };
1562
+ this.treeModel = new TreeModel(this.mergedOptions);
1563
+ this.treeModel.setData(this.nodes);
1564
+ // Subscribe to tree events
1565
+ this.eventsSubscription = this.treeModel.events$.subscribe(event => {
1566
+ this.handleTreeEvent(event);
1567
+ });
1568
+ }
1569
+ ngOnDestroy() {
1570
+ // Clean up subscription
1571
+ this.eventsSubscription?.unsubscribe();
1572
+ }
1573
+ handleTreeEvent(event) {
1574
+ this.treeEvent.emit(event);
1575
+ switch (event.type) {
1576
+ case 'initialized':
1577
+ this.initialized.emit(event);
1578
+ break;
1579
+ case 'expand':
1580
+ case 'collapse':
1581
+ this.toggleExpanded.emit(event);
1582
+ break;
1583
+ case 'activate':
1584
+ this.activate.emit(event);
1585
+ break;
1586
+ case 'deactivate':
1587
+ this.deactivate.emit(event);
1588
+ break;
1589
+ case 'select':
1590
+ this.select.emit(event);
1591
+ break;
1592
+ case 'deselect':
1593
+ this.deselect.emit(event);
1594
+ break;
1595
+ case 'focus':
1596
+ this.focus.emit(event);
1597
+ break;
1598
+ case 'blur':
1599
+ this.blur.emit(event);
1600
+ break;
1601
+ case 'moveNode':
1602
+ this.moveNode.emit(event);
1603
+ break;
1604
+ case 'loadChildren':
1605
+ this.loadChildren.emit(event);
1606
+ break;
1607
+ }
1608
+ }
1609
+ // Keyboard navigation
1610
+ onKeydown(event) {
1611
+ if (!this.isFocused())
1612
+ return;
1613
+ const key = event.key;
1614
+ const isRtl = this.options.rtl;
1615
+ switch (key) {
1616
+ case 'ArrowDown':
1617
+ this.treeModel.focusNextNode();
1618
+ event.preventDefault();
1619
+ break;
1620
+ case 'ArrowUp':
1621
+ this.treeModel.focusPreviousNode();
1622
+ event.preventDefault();
1623
+ break;
1624
+ case 'ArrowRight':
1625
+ isRtl ? this.treeModel.focusDrillUp() : this.treeModel.focusDrillDown();
1626
+ event.preventDefault();
1627
+ break;
1628
+ case 'ArrowLeft':
1629
+ isRtl ? this.treeModel.focusDrillDown() : this.treeModel.focusDrillUp();
1630
+ event.preventDefault();
1631
+ break;
1632
+ case 'Enter':
1633
+ case ' ':
1634
+ const focused = this.treeModel.focusedNode();
1635
+ if (focused) {
1636
+ this.treeModel.activateNode(focused);
1637
+ }
1638
+ event.preventDefault();
1639
+ break;
1640
+ }
1641
+ }
1642
+ onFocus() {
1643
+ this.isFocused.set(true);
1644
+ }
1645
+ onBlur() {
1646
+ this.isFocused.set(false);
1647
+ }
1648
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeRootComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1649
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: TreeRootComponent, isStandalone: true, selector: "tree-root", inputs: { nodes: "nodes", options: "options" }, outputs: { initialized: "initialized", toggleExpanded: "toggleExpanded", activate: "activate", deactivate: "deactivate", select: "select", deselect: "deselect", focus: "focus", blur: "blur", moveNode: "moveNode", loadChildren: "loadChildren", treeEvent: "treeEvent" }, host: { listeners: { "keydown": "onKeydown($event)", "focus": "onFocus()", "blur": "onBlur()" }, properties: { "class.tree-focused": "isFocused()" } }, queries: [{ propertyName: "nodeTemplate", first: true, predicate: TreeNodeTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], usesOnChanges: true, ngImport: i0, template: `
1650
+ <div
1651
+ class="tree-container"
1652
+ [class.tree-rtl]="mergedOptions?.rtl"
1653
+ tabindex="0">
1654
+
1655
+ @if (mergedOptions?.useVirtualScroll) {
1656
+ <tree-viewport
1657
+ [treeModel]="treeModel"
1658
+ [options]="mergedOptions"
1659
+ [nodeTemplate]="nodeTemplate()">
1660
+ </tree-viewport>
1661
+ } @else {
1662
+ @for (root of treeModel.roots(); track root.id) {
1663
+ <tree-node
1664
+ [node]="root"
1665
+ [treeModel]="treeModel"
1666
+ [options]="mergedOptions"
1667
+ [nodeTemplate]="nodeTemplate()">
1668
+ </tree-node>
1669
+ }
1670
+ }
1671
+ </div>
1672
+ `, isInline: true, styles: [".tree-container{width:100%;height:100%;outline:none}.tree-rtl{direction:rtl}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TreeNodeComponent, selector: "tree-node", inputs: ["node", "treeModel", "options", "nodeTemplate"] }, { kind: "component", type: TreeViewportComponent, selector: "tree-viewport", inputs: ["treeModel", "options", "nodeTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1673
+ }
1674
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: TreeRootComponent, decorators: [{
1675
+ type: Component,
1676
+ args: [{ selector: 'tree-root', standalone: true, imports: [
1677
+ CommonModule,
1678
+ TreeNodeComponent,
1679
+ TreeViewportComponent,
1680
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1681
+ <div
1682
+ class="tree-container"
1683
+ [class.tree-rtl]="mergedOptions?.rtl"
1684
+ tabindex="0">
1685
+
1686
+ @if (mergedOptions?.useVirtualScroll) {
1687
+ <tree-viewport
1688
+ [treeModel]="treeModel"
1689
+ [options]="mergedOptions"
1690
+ [nodeTemplate]="nodeTemplate()">
1691
+ </tree-viewport>
1692
+ } @else {
1693
+ @for (root of treeModel.roots(); track root.id) {
1694
+ <tree-node
1695
+ [node]="root"
1696
+ [treeModel]="treeModel"
1697
+ [options]="mergedOptions"
1698
+ [nodeTemplate]="nodeTemplate()">
1699
+ </tree-node>
1700
+ }
1701
+ }
1702
+ </div>
1703
+ `, host: {
1704
+ '[class.tree-focused]': 'isFocused()',
1705
+ }, styles: [".tree-container{width:100%;height:100%;outline:none}.tree-rtl{direction:rtl}\n"] }]
1706
+ }], propDecorators: { nodes: [{
1707
+ type: Input
1708
+ }], options: [{
1709
+ type: Input
1710
+ }], initialized: [{
1711
+ type: Output
1712
+ }], toggleExpanded: [{
1713
+ type: Output
1714
+ }], activate: [{
1715
+ type: Output
1716
+ }], deactivate: [{
1717
+ type: Output
1718
+ }], select: [{
1719
+ type: Output
1720
+ }], deselect: [{
1721
+ type: Output
1722
+ }], focus: [{
1723
+ type: Output
1724
+ }], blur: [{
1725
+ type: Output
1726
+ }], moveNode: [{
1727
+ type: Output
1728
+ }], loadChildren: [{
1729
+ type: Output
1730
+ }], treeEvent: [{
1731
+ type: Output
1732
+ }], onKeydown: [{
1733
+ type: HostListener,
1734
+ args: ['keydown', ['$event']]
1735
+ }], onFocus: [{
1736
+ type: HostListener,
1737
+ args: ['focus']
1738
+ }], onBlur: [{
1739
+ type: HostListener,
1740
+ args: ['blur']
1741
+ }] } });
1742
+
1743
+ class ConceptoTreeComponent {
1744
+ nodes = [];
1745
+ options = {};
1746
+ initialized = new EventEmitter();
1747
+ toggleExpanded = new EventEmitter();
1748
+ activate = new EventEmitter();
1749
+ deactivate = new EventEmitter();
1750
+ select = new EventEmitter();
1751
+ deselect = new EventEmitter();
1752
+ focus = new EventEmitter();
1753
+ blur = new EventEmitter();
1754
+ moveNode = new EventEmitter();
1755
+ loadChildren = new EventEmitter();
1756
+ treeEvent = new EventEmitter();
1757
+ treeRoot;
1758
+ isDropdownOpen = signal(false);
1759
+ searchTerm = '';
1760
+ selectedNode = signal(null);
1761
+ // All nodes flattened
1762
+ allNodes = [];
1763
+ filteredNodes = signal([]);
1764
+ get displayField() {
1765
+ return this.options.displayField || 'name';
1766
+ }
1767
+ toggleDropdown() {
1768
+ this.isDropdownOpen.update(v => !v);
1769
+ if (this.isDropdownOpen()) {
1770
+ this.searchTerm = '';
1771
+ this.filterNodes();
1772
+ }
1773
+ }
1774
+ closeDropdown() {
1775
+ this.isDropdownOpen.set(false);
1776
+ }
1777
+ filterNodes() {
1778
+ if (!this.searchTerm) {
1779
+ this.filteredNodes.set(this.allNodes);
1780
+ }
1781
+ else {
1782
+ const term = this.searchTerm.toLowerCase();
1783
+ this.filteredNodes.set(this.allNodes.filter(node => String(node.data[this.displayField]).toLowerCase().includes(term)));
1784
+ }
1785
+ }
1786
+ selectNode(node) {
1787
+ this.selectedNode.set(node);
1788
+ this.isDropdownOpen.set(false);
1789
+ // Activate in tree
1790
+ if (this.treeRoot && this.treeRoot.treeModel) {
1791
+ this.treeRoot.treeModel.activateNode(node);
1792
+ node.ensureVisible();
1793
+ }
1794
+ }
1795
+ onTreeInitialized(event) {
1796
+ this.initialized.emit(event);
1797
+ // Get all nodes for the dropdown
1798
+ if (event.treeModel) {
1799
+ this.allNodes = event.treeModel.getAllNodes();
1800
+ this.filteredNodes.set(this.allNodes);
1801
+ }
1802
+ }
1803
+ onNodeActivated(event) {
1804
+ this.activate.emit(event);
1805
+ if (event.node) {
1806
+ this.selectedNode.set(event.node);
1807
+ }
1808
+ }
1809
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConceptoTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1810
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: ConceptoTreeComponent, isStandalone: true, selector: "lib-concepto-tree", inputs: { nodes: "nodes", options: "options" }, outputs: { initialized: "initialized", toggleExpanded: "toggleExpanded", activate: "activate", deactivate: "deactivate", select: "select", deselect: "deselect", focus: "focus", blur: "blur", moveNode: "moveNode", loadChildren: "loadChildren", treeEvent: "treeEvent" }, viewQueries: [{ propertyName: "treeRoot", first: true, predicate: TreeRootComponent, descendants: true }], ngImport: i0, template: `
1811
+ <div class="concepto-tree-container">
1812
+ <div class="search-bar-container">
1813
+ <div class="custom-select" [class.open]="isDropdownOpen()" (click)="toggleDropdown()">
1814
+ <div class="selected-option">
1815
+ {{ selectedNode() ? selectedNode()?.data?.[displayField] : 'Buscar nodo...' }}
1816
+ </div>
1817
+ <div class="arrow">▼</div>
1818
+ </div>
1819
+
1820
+ <div class="backdrop" *ngIf="isDropdownOpen()" (click)="closeDropdown()"></div>
1821
+
1822
+ <div class="dropdown-menu" *ngIf="isDropdownOpen()">
1823
+ <div class="search-input-wrapper" (click)="$event.stopPropagation()">
1824
+ <input
1825
+ type="text"
1826
+ [(ngModel)]="searchTerm"
1827
+ (ngModelChange)="filterNodes()"
1828
+ placeholder="Buscar..."
1829
+ class="search-input"
1830
+ #searchInput
1831
+ autofocus
1832
+ >
1833
+ </div>
1834
+ <div class="options-list">
1835
+ <div
1836
+ *ngFor="let node of filteredNodes()"
1837
+ class="option-item"
1838
+ (click)="selectNode(node); $event.stopPropagation()"
1839
+ >
1840
+ {{ node.data[displayField] }}
1841
+ </div>
1842
+ <div *ngIf="filteredNodes().length === 0" class="no-results">
1843
+ No se encontraron resultados
1844
+ </div>
1845
+ </div>
1846
+ </div>
1847
+ </div>
1848
+
1849
+ <tree-root
1850
+ [nodes]="nodes"
1851
+ [options]="options"
1852
+ (initialized)="onTreeInitialized($event)"
1853
+ (toggleExpanded)="toggleExpanded.emit($event)"
1854
+ (activate)="onNodeActivated($event)"
1855
+ (deactivate)="deactivate.emit($event)"
1856
+ (select)="select.emit($event)"
1857
+ (deselect)="deselect.emit($event)"
1858
+ (focus)="focus.emit($event)"
1859
+ (blur)="blur.emit($event)"
1860
+ (moveNode)="moveNode.emit($event)"
1861
+ (loadChildren)="loadChildren.emit($event)"
1862
+ (treeEvent)="treeEvent.emit($event)"
1863
+ ></tree-root>
1864
+ </div>
1865
+ `, isInline: true, styles: [".concepto-tree-container{display:flex;flex-direction:column;height:100%;gap:10px}.search-bar-container{position:relative;width:100%;z-index:100}.custom-select{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;background:#fff;-webkit-user-select:none;user-select:none;transition:border-color .2s}.custom-select:hover{border-color:#888}.custom-select.open{border-color:#007bff;border-bottom-left-radius:0;border-bottom-right-radius:0}.backdrop{position:fixed;top:0;left:0;width:100%;height:100%;z-index:101;background:transparent}.dropdown-menu{position:absolute;top:100%;left:0;width:100%;background:#fff;border:1px solid #ccc;border-top:none;border-radius:0 0 4px 4px;box-shadow:0 4px 6px #0000001a;margin-top:0;max-height:300px;overflow-y:auto;z-index:102}.search-input-wrapper{padding:8px;position:sticky;top:0;background:#fff;border-bottom:1px solid #eee;z-index:103}.search-input{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box;outline:none}.search-input:focus{border-color:#007bff}.options-list{max-height:250px;overflow-y:auto}.option-item{padding:8px 12px;cursor:pointer;transition:background-color .1s}.option-item:hover{background-color:#f5f5f5}.no-results{padding:12px;color:#888;font-style:italic;text-align:center}tree-root{flex:1;overflow:hidden}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TreeRootComponent, selector: "tree-root", inputs: ["nodes", "options"], outputs: ["initialized", "toggleExpanded", "activate", "deactivate", "select", "deselect", "focus", "blur", "moveNode", "loadChildren", "treeEvent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1866
+ }
1867
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConceptoTreeComponent, decorators: [{
1868
+ type: Component,
1869
+ args: [{ selector: 'lib-concepto-tree', standalone: true, imports: [CommonModule, FormsModule, TreeRootComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1870
+ <div class="concepto-tree-container">
1871
+ <div class="search-bar-container">
1872
+ <div class="custom-select" [class.open]="isDropdownOpen()" (click)="toggleDropdown()">
1873
+ <div class="selected-option">
1874
+ {{ selectedNode() ? selectedNode()?.data?.[displayField] : 'Buscar nodo...' }}
1875
+ </div>
1876
+ <div class="arrow">▼</div>
1877
+ </div>
1878
+
1879
+ <div class="backdrop" *ngIf="isDropdownOpen()" (click)="closeDropdown()"></div>
1880
+
1881
+ <div class="dropdown-menu" *ngIf="isDropdownOpen()">
1882
+ <div class="search-input-wrapper" (click)="$event.stopPropagation()">
1883
+ <input
1884
+ type="text"
1885
+ [(ngModel)]="searchTerm"
1886
+ (ngModelChange)="filterNodes()"
1887
+ placeholder="Buscar..."
1888
+ class="search-input"
1889
+ #searchInput
1890
+ autofocus
1891
+ >
1892
+ </div>
1893
+ <div class="options-list">
1894
+ <div
1895
+ *ngFor="let node of filteredNodes()"
1896
+ class="option-item"
1897
+ (click)="selectNode(node); $event.stopPropagation()"
1898
+ >
1899
+ {{ node.data[displayField] }}
1900
+ </div>
1901
+ <div *ngIf="filteredNodes().length === 0" class="no-results">
1902
+ No se encontraron resultados
1903
+ </div>
1904
+ </div>
1905
+ </div>
1906
+ </div>
1907
+
1908
+ <tree-root
1909
+ [nodes]="nodes"
1910
+ [options]="options"
1911
+ (initialized)="onTreeInitialized($event)"
1912
+ (toggleExpanded)="toggleExpanded.emit($event)"
1913
+ (activate)="onNodeActivated($event)"
1914
+ (deactivate)="deactivate.emit($event)"
1915
+ (select)="select.emit($event)"
1916
+ (deselect)="deselect.emit($event)"
1917
+ (focus)="focus.emit($event)"
1918
+ (blur)="blur.emit($event)"
1919
+ (moveNode)="moveNode.emit($event)"
1920
+ (loadChildren)="loadChildren.emit($event)"
1921
+ (treeEvent)="treeEvent.emit($event)"
1922
+ ></tree-root>
1923
+ </div>
1924
+ `, styles: [".concepto-tree-container{display:flex;flex-direction:column;height:100%;gap:10px}.search-bar-container{position:relative;width:100%;z-index:100}.custom-select{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border:1px solid #ccc;border-radius:4px;cursor:pointer;background:#fff;-webkit-user-select:none;user-select:none;transition:border-color .2s}.custom-select:hover{border-color:#888}.custom-select.open{border-color:#007bff;border-bottom-left-radius:0;border-bottom-right-radius:0}.backdrop{position:fixed;top:0;left:0;width:100%;height:100%;z-index:101;background:transparent}.dropdown-menu{position:absolute;top:100%;left:0;width:100%;background:#fff;border:1px solid #ccc;border-top:none;border-radius:0 0 4px 4px;box-shadow:0 4px 6px #0000001a;margin-top:0;max-height:300px;overflow-y:auto;z-index:102}.search-input-wrapper{padding:8px;position:sticky;top:0;background:#fff;border-bottom:1px solid #eee;z-index:103}.search-input{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box;outline:none}.search-input:focus{border-color:#007bff}.options-list{max-height:250px;overflow-y:auto}.option-item{padding:8px 12px;cursor:pointer;transition:background-color .1s}.option-item:hover{background-color:#f5f5f5}.no-results{padding:12px;color:#888;font-style:italic;text-align:center}tree-root{flex:1;overflow:hidden}\n"] }]
1925
+ }], propDecorators: { nodes: [{
1926
+ type: Input
1927
+ }], options: [{
1928
+ type: Input
1929
+ }], initialized: [{
1930
+ type: Output
1931
+ }], toggleExpanded: [{
1932
+ type: Output
1933
+ }], activate: [{
1934
+ type: Output
1935
+ }], deactivate: [{
1936
+ type: Output
1937
+ }], select: [{
1938
+ type: Output
1939
+ }], deselect: [{
1940
+ type: Output
1941
+ }], focus: [{
1942
+ type: Output
1943
+ }], blur: [{
1944
+ type: Output
1945
+ }], moveNode: [{
1946
+ type: Output
1947
+ }], loadChildren: [{
1948
+ type: Output
1949
+ }], treeEvent: [{
1950
+ type: Output
1951
+ }], treeRoot: [{
1952
+ type: ViewChild,
1953
+ args: [TreeRootComponent]
1954
+ }] } });
1955
+
1956
+ class EntityComparisonService {
1957
+ parseEntities(xmlString) {
1958
+ const parser = new DOMParser();
1959
+ const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
1960
+ const exportItems = xmlDoc.querySelectorAll('ExportItem[Type="Entity"]');
1961
+ const entities = [];
1962
+ exportItems.forEach((item) => {
1963
+ const entity = item.querySelector('Entity_WithoutRedundancies');
1964
+ if (!entity)
1965
+ return;
1966
+ const structureJson = entity.querySelector('EntityStructureJson')?.textContent;
1967
+ if (!structureJson)
1968
+ return;
1969
+ try {
1970
+ const structure = JSON.parse(structureJson);
1971
+ entities.push({
1972
+ id: entity.querySelector('EntityId')?.textContent || '',
1973
+ name: entity.querySelector('EntityName')?.textContent || '',
1974
+ structure: structure
1975
+ });
1976
+ }
1977
+ catch (e) {
1978
+ console.error('Error parsing EntityStructureJson:', e);
1979
+ }
1980
+ });
1981
+ return entities;
1982
+ }
1983
+ compareStructures(leftStructure, rightStructure) {
1984
+ const leftOnly = [];
1985
+ const rightOnly = [];
1986
+ const modified = [];
1987
+ const unchanged = [];
1988
+ // Create maps for quick lookup
1989
+ const leftMap = this.createNodeMap(leftStructure);
1990
+ const rightMap = this.createNodeMap(rightStructure);
1991
+ // Find nodes only in left or modified
1992
+ for (const [key, leftNode] of leftMap.entries()) {
1993
+ if (!rightMap.has(key)) {
1994
+ leftOnly.push(leftNode);
1995
+ }
1996
+ else {
1997
+ const rightNode = rightMap.get(key);
1998
+ const diff = this.compareNodes(leftNode, rightNode);
1999
+ if (diff.length > 0) {
2000
+ modified.push({ ...leftNode, modifiedProperties: diff });
2001
+ }
2002
+ else {
2003
+ unchanged.push(leftNode);
2004
+ }
2005
+ }
2006
+ }
2007
+ // Find nodes only in right
2008
+ for (const [key, rightNode] of rightMap.entries()) {
2009
+ if (!leftMap.has(key)) {
2010
+ rightOnly.push(rightNode);
2011
+ }
2012
+ }
2013
+ return { leftOnly, rightOnly, modified, unchanged };
2014
+ }
2015
+ createNodeMap(structure, parentPath = '') {
2016
+ const map = new Map();
2017
+ for (const node of structure) {
2018
+ const nodePath = parentPath ? `${parentPath}/${node.Id}-${node.name}` : `${node.Id}-${node.name}`;
2019
+ map.set(nodePath, node);
2020
+ if (node.Children) {
2021
+ try {
2022
+ const children = JSON.parse(node.Children);
2023
+ const childMap = this.createNodeMap(children, nodePath);
2024
+ for (const [key, value] of childMap.entries()) {
2025
+ map.set(key, value);
2026
+ }
2027
+ }
2028
+ catch (e) {
2029
+ // Ignore parsing errors
2030
+ }
2031
+ }
2032
+ }
2033
+ return map;
2034
+ }
2035
+ compareNodes(left, right) {
2036
+ const modifiedProps = [];
2037
+ try {
2038
+ const leftProps = JSON.parse(left.Properties || '{}');
2039
+ const rightProps = JSON.parse(right.Properties || '{}');
2040
+ const allKeys = new Set([
2041
+ ...Object.keys(leftProps),
2042
+ ...Object.keys(rightProps)
2043
+ ]);
2044
+ for (const key of allKeys) {
2045
+ if (leftProps[key] !== rightProps[key]) {
2046
+ modifiedProps.push(key);
2047
+ }
2048
+ }
2049
+ }
2050
+ catch (e) {
2051
+ // Ignore parsing errors
2052
+ }
2053
+ return modifiedProps;
2054
+ }
2055
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: EntityComparisonService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2056
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: EntityComparisonService, providedIn: 'root' });
2057
+ }
2058
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: EntityComparisonService, decorators: [{
2059
+ type: Injectable,
2060
+ args: [{
2061
+ providedIn: 'root'
2062
+ }]
2063
+ }] });
2064
+
2065
+ class EntityComparisonComponent {
2066
+ comparisonService;
2067
+ leftFile = '';
2068
+ rightFile = '';
2069
+ leftEntities = [];
2070
+ rightEntities = [];
2071
+ selectedLeftEntity = null;
2072
+ selectedRightEntity = null;
2073
+ comparisonResult = null;
2074
+ expandedNodes = new Set();
2075
+ isFullScreen = false;
2076
+ currentDifferenceIndex = 0;
2077
+ differences = [];
2078
+ constructor(comparisonService) {
2079
+ this.comparisonService = comparisonService;
2080
+ }
2081
+ onLeftFileSelected(event) {
2082
+ const file = event.target.files[0];
2083
+ if (!file)
2084
+ return;
2085
+ this.leftFile = file.name;
2086
+ const reader = new FileReader();
2087
+ reader.onload = (e) => {
2088
+ const xmlContent = e.target.result;
2089
+ this.leftEntities = this.comparisonService.parseEntities(xmlContent);
2090
+ this.selectedLeftEntity = null;
2091
+ this.comparisonResult = null;
2092
+ };
2093
+ reader.readAsText(file);
2094
+ }
2095
+ onRightFileSelected(event) {
2096
+ const file = event.target.files[0];
2097
+ if (!file)
2098
+ return;
2099
+ this.rightFile = file.name;
2100
+ const reader = new FileReader();
2101
+ reader.onload = (e) => {
2102
+ const xmlContent = e.target.result;
2103
+ this.rightEntities = this.comparisonService.parseEntities(xmlContent);
2104
+ this.selectedRightEntity = null;
2105
+ this.comparisonResult = null;
2106
+ };
2107
+ reader.readAsText(file);
2108
+ }
2109
+ selectLeftEntity(entity) {
2110
+ this.selectedLeftEntity = entity;
2111
+ this.compareIfBothSelected();
2112
+ }
2113
+ selectRightEntity(entity) {
2114
+ this.selectedRightEntity = entity;
2115
+ this.compareIfBothSelected();
2116
+ }
2117
+ compareIfBothSelected() {
2118
+ if (this.selectedLeftEntity && this.selectedRightEntity) {
2119
+ this.comparisonResult = this.comparisonService.compareStructures(this.selectedLeftEntity.structure, this.selectedRightEntity.structure);
2120
+ this.buildDifferencesList();
2121
+ this.currentDifferenceIndex = 0;
2122
+ }
2123
+ }
2124
+ buildDifferencesList() {
2125
+ if (!this.comparisonResult) {
2126
+ this.differences = [];
2127
+ return;
2128
+ }
2129
+ this.differences = [
2130
+ ...this.comparisonResult.leftOnly.map(node => ({ node, type: 'removed' })),
2131
+ ...this.comparisonResult.rightOnly.map(node => ({ node, type: 'added' })),
2132
+ ...this.comparisonResult.modified.map(node => ({ node, type: 'modified' }))
2133
+ ];
2134
+ }
2135
+ navigateToNextDifference() {
2136
+ if (this.differences.length === 0)
2137
+ return;
2138
+ this.currentDifferenceIndex = (this.currentDifferenceIndex + 1) % this.differences.length;
2139
+ this.scrollToDifference();
2140
+ }
2141
+ navigateToPreviousDifference() {
2142
+ if (this.differences.length === 0)
2143
+ return;
2144
+ this.currentDifferenceIndex = this.currentDifferenceIndex === 0
2145
+ ? this.differences.length - 1
2146
+ : this.currentDifferenceIndex - 1;
2147
+ this.scrollToDifference();
2148
+ }
2149
+ scrollToDifference() {
2150
+ if (this.differences.length === 0)
2151
+ return;
2152
+ const currentDiff = this.differences[this.currentDifferenceIndex];
2153
+ const nodeId = `${currentDiff.node.Id}-${currentDiff.node.name}`;
2154
+ // Expand parent nodes FIRST to make the difference visible
2155
+ this.expandPathToNode(currentDiff.node);
2156
+ // Wait for DOM to update after expansion, then scroll
2157
+ setTimeout(() => {
2158
+ const leftElement = document.querySelector(`[data-node-id="left-${nodeId}"]`);
2159
+ const rightElement = document.querySelector(`[data-node-id="right-${nodeId}"]`);
2160
+ if (leftElement) {
2161
+ leftElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
2162
+ }
2163
+ if (rightElement) {
2164
+ rightElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
2165
+ }
2166
+ }, 100);
2167
+ }
2168
+ expandPathToNode(node) {
2169
+ // Find and expand all parent nodes in both structures
2170
+ this.expandParentsInStructure(this.selectedLeftEntity.structure, node, 'left');
2171
+ this.expandParentsInStructure(this.selectedRightEntity.structure, node, 'right');
2172
+ }
2173
+ expandParentsInStructure(structure, targetNode, side) {
2174
+ for (const node of structure) {
2175
+ const nodeId = `${side}-${node.Id}-${node.name}`;
2176
+ const targetId = `${targetNode.Id}-${targetNode.name}`;
2177
+ const currentId = `${node.Id}-${node.name}`;
2178
+ // If this is the target node, we found it
2179
+ if (currentId === targetId) {
2180
+ return true;
2181
+ }
2182
+ // Check children if they exist
2183
+ if (this.hasChildren(node)) {
2184
+ const children = this.getChildren(node);
2185
+ if (this.expandParentsInStructure(children, targetNode, side)) {
2186
+ // If the target is in this branch, expand this node
2187
+ this.expandedNodes.add(nodeId);
2188
+ return true;
2189
+ }
2190
+ }
2191
+ }
2192
+ return false;
2193
+ }
2194
+ get hasDifferences() {
2195
+ return this.differences.length > 0;
2196
+ }
2197
+ get currentDifferenceNumber() {
2198
+ return this.currentDifferenceIndex + 1;
2199
+ }
2200
+ get totalDifferences() {
2201
+ return this.differences.length;
2202
+ }
2203
+ toggleNode(nodeId) {
2204
+ if (this.expandedNodes.has(nodeId)) {
2205
+ this.expandedNodes.delete(nodeId);
2206
+ }
2207
+ else {
2208
+ this.expandedNodes.add(nodeId);
2209
+ }
2210
+ }
2211
+ isExpanded(nodeId) {
2212
+ return this.expandedNodes.has(nodeId);
2213
+ }
2214
+ getNodeId(node, prefix) {
2215
+ return `${prefix}-${node.Id}-${node.name}`;
2216
+ }
2217
+ hasChildren(node) {
2218
+ return node.Children && JSON.parse(node.Children).length > 0;
2219
+ }
2220
+ getChildren(node) {
2221
+ if (!node.Children)
2222
+ return [];
2223
+ try {
2224
+ return JSON.parse(node.Children);
2225
+ }
2226
+ catch {
2227
+ return [];
2228
+ }
2229
+ }
2230
+ getProperties(node) {
2231
+ if (!node.Properties)
2232
+ return {};
2233
+ try {
2234
+ return JSON.parse(node.Properties);
2235
+ }
2236
+ catch {
2237
+ return {};
2238
+ }
2239
+ }
2240
+ getPropertyKeys(node) {
2241
+ const props = this.getProperties(node);
2242
+ return Object.keys(props);
2243
+ }
2244
+ getChangeType(node) {
2245
+ if (!this.comparisonResult)
2246
+ return '';
2247
+ const nodeId = `${node.Id}-${node.name}`;
2248
+ if (this.comparisonResult.leftOnly.some(n => `${n.Id}-${n.name}` === nodeId)) {
2249
+ return 'removed';
2250
+ }
2251
+ if (this.comparisonResult.rightOnly.some(n => `${n.Id}-${n.name}` === nodeId)) {
2252
+ return 'added';
2253
+ }
2254
+ if (this.comparisonResult.modified.some(n => `${n.Id}-${n.name}` === nodeId)) {
2255
+ return 'modified';
2256
+ }
2257
+ return 'unchanged';
2258
+ }
2259
+ getModifiedProperties(node) {
2260
+ if (!this.comparisonResult)
2261
+ return [];
2262
+ const nodeId = `${node.Id}-${node.name}`;
2263
+ const modified = this.comparisonResult.modified.find(n => `${n.Id}-${n.name}` === nodeId);
2264
+ return modified?.modifiedProperties || [];
2265
+ }
2266
+ toggleFullScreen() {
2267
+ this.isFullScreen = !this.isFullScreen;
2268
+ }
2269
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: EntityComparisonComponent, deps: [{ token: EntityComparisonService }], target: i0.ɵɵFactoryTarget.Component });
2270
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: EntityComparisonComponent, isStandalone: true, selector: "app-entity-comparison", providers: [EntityComparisonService], ngImport: i0, template: "<div class=\"comparison-container\" [class.fullscreen]=\"isFullScreen\">\r\n <header class=\"header\">\r\n <h1>Entity Structure Comparison</h1>\r\n <p class=\"subtitle\">Compare EntityStructureJson between two entity exports</p>\r\n </header>\r\n\r\n <div class=\"file-selection\">\r\n <div class=\"file-input-group\">\r\n <label class=\"file-label left\">\r\n <input type=\"file\" accept=\".xml\" (change)=\"onLeftFileSelected($event)\" hidden>\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\r\n <polyline points=\"17 8 12 3 7 8\" />\r\n <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\" />\r\n </svg>\r\n Load Left Entity\r\n </label>\r\n <span *ngIf=\"leftFile\" class=\"file-name\">{{ leftFile }}</span>\r\n </div>\r\n\r\n <div class=\"vs-separator\">VS</div>\r\n\r\n <div class=\"file-input-group\">\r\n <label class=\"file-label right\">\r\n <input type=\"file\" accept=\".xml\" (change)=\"onRightFileSelected($event)\" hidden>\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\r\n <polyline points=\"17 8 12 3 7 8\" />\r\n <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\" />\r\n </svg>\r\n Load Right Entity\r\n </label>\r\n <span *ngIf=\"rightFile\" class=\"file-name\">{{ rightFile }}</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"entity-selection\" *ngIf=\"leftEntities.length > 0 || rightEntities.length > 0\">\r\n <div class=\"entity-list\">\r\n <h3>Left Entities</h3>\r\n <div class=\"entities\">\r\n <div *ngFor=\"let entity of leftEntities\" class=\"entity-card\"\r\n [class.selected]=\"selectedLeftEntity === entity\" (click)=\"selectLeftEntity(entity)\">\r\n <div class=\"entity-icon\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\r\n stroke-width=\"2\">\r\n <path d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z\" />\r\n </svg>\r\n </div>\r\n <div class=\"entity-info\">\r\n <div class=\"entity-name\">{{ entity.name }}</div>\r\n <div class=\"entity-id\">{{ entity.id }}</div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"entity-list\">\r\n <h3>Right Entities</h3>\r\n <div class=\"entities\">\r\n <div *ngFor=\"let entity of rightEntities\" class=\"entity-card\"\r\n [class.selected]=\"selectedRightEntity === entity\" (click)=\"selectRightEntity(entity)\">\r\n <div class=\"entity-icon\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\r\n stroke-width=\"2\">\r\n <path d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z\" />\r\n </svg>\r\n </div>\r\n <div class=\"entity-info\">\r\n <div class=\"entity-name\">{{ entity.name }}</div>\r\n <div class=\"entity-id\">{{ entity.id }}</div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"summary-section\" *ngIf=\"comparisonResult\">\r\n <div class=\"summary\">\r\n <div class=\"summary-item added\">\r\n <span class=\"count\">{{ comparisonResult.rightOnly.length }}</span>\r\n <span class=\"label\">Added</span>\r\n </div>\r\n <div class=\"summary-item removed\">\r\n <span class=\"count\">{{ comparisonResult.leftOnly.length }}</span>\r\n <span class=\"label\">Removed</span>\r\n </div>\r\n <div class=\"summary-item modified\">\r\n <span class=\"count\">{{ comparisonResult.modified.length }}</span>\r\n <span class=\"label\">Modified</span>\r\n </div>\r\n <div class=\"summary-item unchanged\">\r\n <span class=\"count\">{{ comparisonResult.unchanged.length }}</span>\r\n <span class=\"label\">Unchanged</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"comparison-results\" *ngIf=\"comparisonResult\">\r\n <div class=\"results-header\">\r\n <div class=\"header-title\">\r\n <h2>Comparison Results</h2>\r\n <div class=\"header-actions\">\r\n <div class=\"diff-navigation\" *ngIf=\"hasDifferences\">\r\n <button class=\"nav-btn\" (click)=\"navigateToPreviousDifference()\" title=\"Previous Difference\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <polyline points=\"18 15 12 9 6 15\" />\r\n </svg>\r\n </button>\r\n <span class=\"diff-counter\">{{ currentDifferenceNumber }} / {{ totalDifferences }}</span>\r\n <button class=\"nav-btn\" (click)=\"navigateToNextDifference()\" title=\"Next Difference\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <polyline points=\"6 9 12 15 18 9\" />\r\n </svg>\r\n </button>\r\n </div>\r\n <button class=\"fullscreen-btn\" (click)=\"toggleFullScreen()\" [title]=\"isFullScreen ? 'Exit Full Screen' : 'Enter Full Screen'\">\r\n <svg *ngIf=\"!isFullScreen\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3\" />\r\n </svg>\r\n <svg *ngIf=\"isFullScreen\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3\" />\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"structure-comparison\">\r\n <div class=\"comparison-section\">\r\n <h3>\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M9 11l3 3L22 4\" />\r\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\" />\r\n </svg>\r\n {{ selectedLeftEntity.name }}\r\n </h3>\r\n <div class=\"structure-tree\">\r\n <ng-container *ngFor=\"let node of selectedLeftEntity.structure\">\r\n <ng-container\r\n *ngTemplateOutlet=\"nodeTemplate; context: { $implicit: node, side: 'left' }\"></ng-container>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <div class=\"comparison-section\">\r\n <h3>\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M9 11l3 3L22 4\" />\r\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\" />\r\n </svg>\r\n {{ selectedRightEntity.name }}\r\n </h3>\r\n <div class=\"structure-tree\">\r\n <ng-container *ngFor=\"let node of selectedRightEntity.structure\">\r\n <ng-container\r\n *ngTemplateOutlet=\"nodeTemplate; context: { $implicit: node, side: 'right' }\"></ng-container>\r\n </ng-container>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<ng-template #nodeTemplate let-node let-side=\"side\">\r\n <div class=\"tree-node\" [class]=\"getChangeType(node)\" [attr.data-node-id]=\"getNodeId(node, side)\">\r\n <div class=\"node-header\">\r\n <button *ngIf=\"hasChildren(node)\" class=\"expand-btn\" (click)=\"toggleNode(getNodeId(node, side))\">\r\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <polyline *ngIf=\"!isExpanded(getNodeId(node, side))\" points=\"9 18 15 12 9 6\" />\r\n <polyline *ngIf=\"isExpanded(getNodeId(node, side))\" points=\"6 9 12 15 18 9\" />\r\n </svg>\r\n </button>\r\n <div *ngIf=\"!hasChildren(node)\" class=\"spacer\"></div>\r\n\r\n <div class=\"node-icon\">\r\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path *ngIf=\"node.nodeType === 'Group'\"\r\n d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z\" />\r\n <circle *ngIf=\"node.nodeType === 'Attribute'\" cx=\"12\" cy=\"12\" r=\"10\" />\r\n </svg>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <span class=\"node-name\">{{ node.name }}</span>\r\n <span class=\"node-type\">{{ node.nodeType }}</span>\r\n <span class=\"change-badge\" *ngIf=\"getChangeType(node) !== 'unchanged'\">\r\n {{ getChangeType(node) }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-properties\" *ngIf=\"getPropertyKeys(node).length > 0\">\r\n <div *ngFor=\"let key of getPropertyKeys(node)\" class=\"property\"\r\n [class.modified]=\"getModifiedProperties(node).includes(key)\">\r\n <span class=\"property-key\">{{ key }}:</span>\r\n <span class=\"property-value\">{{ getProperties(node)[key] }}</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-children\" *ngIf=\"hasChildren(node) && isExpanded(getNodeId(node, side))\">\r\n <ng-container *ngFor=\"let child of getChildren(node)\">\r\n <ng-container\r\n *ngTemplateOutlet=\"nodeTemplate; context: { $implicit: child, side: side }\"></ng-container>\r\n </ng-container>\r\n </div>\r\n </div>\r\n</ng-template>", styles: [".comparison-container{display:flex;flex-direction:column;height:100%;width:100%;background:#f9fafb}.header{background:#fff;padding:1.5rem;border-bottom:1px solid #e5e7eb;box-shadow:0 1px 3px #0000001a}.header h1{margin:0;font-size:1.5rem;font-weight:700;color:#1f2937}.subtitle{margin:.5rem 0 0;font-size:.875rem;color:#6b7280}.file-selection{display:flex;align-items:center;gap:2rem;padding:1.5rem;background:#fff;border-bottom:1px solid #e5e7eb}.file-input-group{flex:1;display:flex;flex-direction:column;gap:.5rem}.file-label{display:inline-flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;border:2px solid #2563eb;border-radius:.5rem;cursor:pointer;font-weight:500;transition:all .2s;justify-content:center}.file-label.left{background:#fff;color:#2563eb}.file-label.left:hover{background:#eff6ff}.file-label.right{background:#fff;color:#7c3aed;border-color:#7c3aed}.file-label.right:hover{background:#f5f3ff}.file-name{font-size:.875rem;color:#6b7280;text-align:center}.vs-separator{padding:.75rem 1.5rem;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;font-weight:700;border-radius:2rem;font-size:1.25rem;box-shadow:0 4px 6px #0000001a}.entity-selection{display:flex;gap:1rem;padding:1rem;background:#fff;border-bottom:1px solid #e5e7eb}.entity-list{flex:1}.entity-list h3{margin:0 0 .75rem;font-size:.875rem;font-weight:600;color:#6b7280;text-transform:uppercase}.entities{display:flex;flex-direction:column;gap:.5rem}.entity-card{display:flex;align-items:center;gap:.75rem;padding:.75rem;border:1px solid #e5e7eb;border-radius:.5rem;cursor:pointer;transition:all .2s}.entity-card:hover{background:#f9fafb;border-color:#d1d5db}.entity-card.selected{background:#eff6ff;border-color:#2563eb}.entity-icon{color:#6b7280}.entity-card.selected .entity-icon{color:#2563eb}.entity-info{flex:1}.entity-name{font-size:.875rem;font-weight:500;color:#1f2937}.entity-id{font-size:.75rem;color:#6b7280}.summary-section{padding:1.5rem 1.5rem 0;background:#f9fafb}.comparison-results{flex:1;overflow:auto;padding:1.5rem}.results-header{margin-bottom:1.5rem;position:sticky;top:0;background:#f9fafb;z-index:100;padding-top:1rem;padding-bottom:1rem;margin-top:-1rem}.header-title{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem}.results-header h2{margin:0;font-size:1.25rem;font-weight:700;color:#1f2937}.header-actions{display:flex;align-items:center;gap:1rem}.diff-navigation{display:flex;align-items:center;gap:.5rem;background:#fff;border:1px solid #e5e7eb;border-radius:.5rem;padding:.25rem}.nav-btn{background:#fff;border:1px solid #e5e7eb;border-radius:.375rem;padding:.5rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;color:#6b7280}.nav-btn:hover{background:#f9fafb;border-color:#2563eb;color:#2563eb}.diff-counter{font-size:.875rem;font-weight:600;color:#1f2937;padding:0 .5rem;white-space:nowrap;min-width:60px;text-align:center}.fullscreen-btn{background:#fff;border:1px solid #e5e7eb;border-radius:.5rem;padding:.5rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;color:#6b7280}.fullscreen-btn:hover{background:#f9fafb;border-color:#2563eb;color:#2563eb}.summary{display:flex;gap:1rem}.summary-item{flex:1;display:flex;flex-direction:column;align-items:center;padding:1rem;border-radius:.5rem;background:#fff;border:2px solid}.summary-item.added{border-color:#10b981;background:#ecfdf5}.summary-item.removed{border-color:#ef4444;background:#fef2f2}.summary-item.modified{border-color:#f59e0b;background:#fffbeb}.summary-item.unchanged{border-color:#6b7280;background:#f9fafb}.summary-item .count{font-size:1.5rem;font-weight:700}.summary-item.added .count{color:#10b981}.summary-item.removed .count{color:#ef4444}.summary-item.modified .count{color:#f59e0b}.summary-item.unchanged .count{color:#6b7280}.summary-item .label{font-size:.75rem;font-weight:600;text-transform:uppercase;margin-top:.25rem}.structure-comparison{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;height:100%}.comparison-section{background:#fff;border-radius:.5rem;padding:1rem;border:1px solid #e5e7eb;min-width:0;width:100%;overflow:auto}.comparison-section h3{display:flex;align-items:center;gap:.5rem;margin:0 0 1rem;font-size:1rem;font-weight:600;color:#1f2937;padding-bottom:.75rem;border-bottom:1px solid #e5e7eb}.structure-tree{display:flex;flex-direction:column;gap:.5rem}.tree-node{border-left:3px solid transparent;padding-left:.5rem;transition:all .2s;max-width:100%;overflow:hidden}.tree-node.added{background:#ecfdf5;border-left-color:#10b981}.tree-node.removed{background:#fef2f2;border-left-color:#ef4444}.tree-node.modified{background:#fffbeb;border-left-color:#f59e0b}.node-header{display:flex;align-items:center;gap:.5rem;padding:.5rem;border-radius:.375rem}.expand-btn{background:none;border:none;cursor:pointer;padding:.25rem;display:flex;align-items:center;justify-content:center;border-radius:.25rem;transition:background .2s}.expand-btn:hover{background:#f3f4f6}.spacer{width:24px}.node-icon{color:#6b7280}.node-content{display:flex;align-items:center;gap:.5rem;flex:1}.node-name{font-size:.875rem;font-weight:500;color:#1f2937}.node-type{font-size:.75rem;color:#6b7280;background:#f3f4f6;padding:.125rem .5rem;border-radius:.25rem}.change-badge{font-size:.625rem;font-weight:600;text-transform:uppercase;padding:.125rem .5rem;border-radius:.25rem;margin-left:auto}.tree-node.added .change-badge{background:#10b981;color:#fff}.tree-node.removed .change-badge{background:#ef4444;color:#fff}.tree-node.modified .change-badge{background:#f59e0b;color:#fff}.node-properties{margin-left:2.5rem;margin-top:.5rem;padding:.5rem;background:#f9fafb;border-radius:.375rem;font-size:.75rem;max-width:100%;overflow:hidden}.property{display:flex;flex-wrap:wrap;gap:.5rem;padding:.25rem 0;max-width:100%;overflow:hidden}.property.modified{background:#fef3c7;padding:.25rem .5rem;border-radius:.25rem;margin:.125rem 0}.property-key{font-weight:600;color:#374151;flex-shrink:0}.property-value{color:#6b7280;word-break:break-word;overflow:auto;overflow-wrap:break-word;flex:1;min-width:0}.node-children{margin-left:1.5rem;margin-top:.5rem;display:flex;flex-direction:column;gap:.5rem}.comparison-container.fullscreen{position:fixed;inset:0;z-index:9999;background:#f9fafb}.comparison-container.fullscreen .header,.comparison-container.fullscreen .file-selection,.comparison-container.fullscreen .entity-selection,.comparison-container.fullscreen .summary-section{display:none}.comparison-container.fullscreen .comparison-results{height:100vh;padding:2rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
2271
+ }
2272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: EntityComparisonComponent, decorators: [{
2273
+ type: Component,
2274
+ args: [{ selector: 'app-entity-comparison', standalone: true, imports: [CommonModule], providers: [EntityComparisonService], template: "<div class=\"comparison-container\" [class.fullscreen]=\"isFullScreen\">\r\n <header class=\"header\">\r\n <h1>Entity Structure Comparison</h1>\r\n <p class=\"subtitle\">Compare EntityStructureJson between two entity exports</p>\r\n </header>\r\n\r\n <div class=\"file-selection\">\r\n <div class=\"file-input-group\">\r\n <label class=\"file-label left\">\r\n <input type=\"file\" accept=\".xml\" (change)=\"onLeftFileSelected($event)\" hidden>\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\r\n <polyline points=\"17 8 12 3 7 8\" />\r\n <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\" />\r\n </svg>\r\n Load Left Entity\r\n </label>\r\n <span *ngIf=\"leftFile\" class=\"file-name\">{{ leftFile }}</span>\r\n </div>\r\n\r\n <div class=\"vs-separator\">VS</div>\r\n\r\n <div class=\"file-input-group\">\r\n <label class=\"file-label right\">\r\n <input type=\"file\" accept=\".xml\" (change)=\"onRightFileSelected($event)\" hidden>\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\r\n <polyline points=\"17 8 12 3 7 8\" />\r\n <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\" />\r\n </svg>\r\n Load Right Entity\r\n </label>\r\n <span *ngIf=\"rightFile\" class=\"file-name\">{{ rightFile }}</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"entity-selection\" *ngIf=\"leftEntities.length > 0 || rightEntities.length > 0\">\r\n <div class=\"entity-list\">\r\n <h3>Left Entities</h3>\r\n <div class=\"entities\">\r\n <div *ngFor=\"let entity of leftEntities\" class=\"entity-card\"\r\n [class.selected]=\"selectedLeftEntity === entity\" (click)=\"selectLeftEntity(entity)\">\r\n <div class=\"entity-icon\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\r\n stroke-width=\"2\">\r\n <path d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z\" />\r\n </svg>\r\n </div>\r\n <div class=\"entity-info\">\r\n <div class=\"entity-name\">{{ entity.name }}</div>\r\n <div class=\"entity-id\">{{ entity.id }}</div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"entity-list\">\r\n <h3>Right Entities</h3>\r\n <div class=\"entities\">\r\n <div *ngFor=\"let entity of rightEntities\" class=\"entity-card\"\r\n [class.selected]=\"selectedRightEntity === entity\" (click)=\"selectRightEntity(entity)\">\r\n <div class=\"entity-icon\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\r\n stroke-width=\"2\">\r\n <path d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z\" />\r\n </svg>\r\n </div>\r\n <div class=\"entity-info\">\r\n <div class=\"entity-name\">{{ entity.name }}</div>\r\n <div class=\"entity-id\">{{ entity.id }}</div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"summary-section\" *ngIf=\"comparisonResult\">\r\n <div class=\"summary\">\r\n <div class=\"summary-item added\">\r\n <span class=\"count\">{{ comparisonResult.rightOnly.length }}</span>\r\n <span class=\"label\">Added</span>\r\n </div>\r\n <div class=\"summary-item removed\">\r\n <span class=\"count\">{{ comparisonResult.leftOnly.length }}</span>\r\n <span class=\"label\">Removed</span>\r\n </div>\r\n <div class=\"summary-item modified\">\r\n <span class=\"count\">{{ comparisonResult.modified.length }}</span>\r\n <span class=\"label\">Modified</span>\r\n </div>\r\n <div class=\"summary-item unchanged\">\r\n <span class=\"count\">{{ comparisonResult.unchanged.length }}</span>\r\n <span class=\"label\">Unchanged</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"comparison-results\" *ngIf=\"comparisonResult\">\r\n <div class=\"results-header\">\r\n <div class=\"header-title\">\r\n <h2>Comparison Results</h2>\r\n <div class=\"header-actions\">\r\n <div class=\"diff-navigation\" *ngIf=\"hasDifferences\">\r\n <button class=\"nav-btn\" (click)=\"navigateToPreviousDifference()\" title=\"Previous Difference\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <polyline points=\"18 15 12 9 6 15\" />\r\n </svg>\r\n </button>\r\n <span class=\"diff-counter\">{{ currentDifferenceNumber }} / {{ totalDifferences }}</span>\r\n <button class=\"nav-btn\" (click)=\"navigateToNextDifference()\" title=\"Next Difference\">\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <polyline points=\"6 9 12 15 18 9\" />\r\n </svg>\r\n </button>\r\n </div>\r\n <button class=\"fullscreen-btn\" (click)=\"toggleFullScreen()\" [title]=\"isFullScreen ? 'Exit Full Screen' : 'Enter Full Screen'\">\r\n <svg *ngIf=\"!isFullScreen\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3\" />\r\n </svg>\r\n <svg *ngIf=\"isFullScreen\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3\" />\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"structure-comparison\">\r\n <div class=\"comparison-section\">\r\n <h3>\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M9 11l3 3L22 4\" />\r\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\" />\r\n </svg>\r\n {{ selectedLeftEntity.name }}\r\n </h3>\r\n <div class=\"structure-tree\">\r\n <ng-container *ngFor=\"let node of selectedLeftEntity.structure\">\r\n <ng-container\r\n *ngTemplateOutlet=\"nodeTemplate; context: { $implicit: node, side: 'left' }\"></ng-container>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <div class=\"comparison-section\">\r\n <h3>\r\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path d=\"M9 11l3 3L22 4\" />\r\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\" />\r\n </svg>\r\n {{ selectedRightEntity.name }}\r\n </h3>\r\n <div class=\"structure-tree\">\r\n <ng-container *ngFor=\"let node of selectedRightEntity.structure\">\r\n <ng-container\r\n *ngTemplateOutlet=\"nodeTemplate; context: { $implicit: node, side: 'right' }\"></ng-container>\r\n </ng-container>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<ng-template #nodeTemplate let-node let-side=\"side\">\r\n <div class=\"tree-node\" [class]=\"getChangeType(node)\" [attr.data-node-id]=\"getNodeId(node, side)\">\r\n <div class=\"node-header\">\r\n <button *ngIf=\"hasChildren(node)\" class=\"expand-btn\" (click)=\"toggleNode(getNodeId(node, side))\">\r\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <polyline *ngIf=\"!isExpanded(getNodeId(node, side))\" points=\"9 18 15 12 9 6\" />\r\n <polyline *ngIf=\"isExpanded(getNodeId(node, side))\" points=\"6 9 12 15 18 9\" />\r\n </svg>\r\n </button>\r\n <div *ngIf=\"!hasChildren(node)\" class=\"spacer\"></div>\r\n\r\n <div class=\"node-icon\">\r\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path *ngIf=\"node.nodeType === 'Group'\"\r\n d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z\" />\r\n <circle *ngIf=\"node.nodeType === 'Attribute'\" cx=\"12\" cy=\"12\" r=\"10\" />\r\n </svg>\r\n </div>\r\n\r\n <div class=\"node-content\">\r\n <span class=\"node-name\">{{ node.name }}</span>\r\n <span class=\"node-type\">{{ node.nodeType }}</span>\r\n <span class=\"change-badge\" *ngIf=\"getChangeType(node) !== 'unchanged'\">\r\n {{ getChangeType(node) }}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-properties\" *ngIf=\"getPropertyKeys(node).length > 0\">\r\n <div *ngFor=\"let key of getPropertyKeys(node)\" class=\"property\"\r\n [class.modified]=\"getModifiedProperties(node).includes(key)\">\r\n <span class=\"property-key\">{{ key }}:</span>\r\n <span class=\"property-value\">{{ getProperties(node)[key] }}</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"node-children\" *ngIf=\"hasChildren(node) && isExpanded(getNodeId(node, side))\">\r\n <ng-container *ngFor=\"let child of getChildren(node)\">\r\n <ng-container\r\n *ngTemplateOutlet=\"nodeTemplate; context: { $implicit: child, side: side }\"></ng-container>\r\n </ng-container>\r\n </div>\r\n </div>\r\n</ng-template>", styles: [".comparison-container{display:flex;flex-direction:column;height:100%;width:100%;background:#f9fafb}.header{background:#fff;padding:1.5rem;border-bottom:1px solid #e5e7eb;box-shadow:0 1px 3px #0000001a}.header h1{margin:0;font-size:1.5rem;font-weight:700;color:#1f2937}.subtitle{margin:.5rem 0 0;font-size:.875rem;color:#6b7280}.file-selection{display:flex;align-items:center;gap:2rem;padding:1.5rem;background:#fff;border-bottom:1px solid #e5e7eb}.file-input-group{flex:1;display:flex;flex-direction:column;gap:.5rem}.file-label{display:inline-flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;border:2px solid #2563eb;border-radius:.5rem;cursor:pointer;font-weight:500;transition:all .2s;justify-content:center}.file-label.left{background:#fff;color:#2563eb}.file-label.left:hover{background:#eff6ff}.file-label.right{background:#fff;color:#7c3aed;border-color:#7c3aed}.file-label.right:hover{background:#f5f3ff}.file-name{font-size:.875rem;color:#6b7280;text-align:center}.vs-separator{padding:.75rem 1.5rem;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;font-weight:700;border-radius:2rem;font-size:1.25rem;box-shadow:0 4px 6px #0000001a}.entity-selection{display:flex;gap:1rem;padding:1rem;background:#fff;border-bottom:1px solid #e5e7eb}.entity-list{flex:1}.entity-list h3{margin:0 0 .75rem;font-size:.875rem;font-weight:600;color:#6b7280;text-transform:uppercase}.entities{display:flex;flex-direction:column;gap:.5rem}.entity-card{display:flex;align-items:center;gap:.75rem;padding:.75rem;border:1px solid #e5e7eb;border-radius:.5rem;cursor:pointer;transition:all .2s}.entity-card:hover{background:#f9fafb;border-color:#d1d5db}.entity-card.selected{background:#eff6ff;border-color:#2563eb}.entity-icon{color:#6b7280}.entity-card.selected .entity-icon{color:#2563eb}.entity-info{flex:1}.entity-name{font-size:.875rem;font-weight:500;color:#1f2937}.entity-id{font-size:.75rem;color:#6b7280}.summary-section{padding:1.5rem 1.5rem 0;background:#f9fafb}.comparison-results{flex:1;overflow:auto;padding:1.5rem}.results-header{margin-bottom:1.5rem;position:sticky;top:0;background:#f9fafb;z-index:100;padding-top:1rem;padding-bottom:1rem;margin-top:-1rem}.header-title{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem}.results-header h2{margin:0;font-size:1.25rem;font-weight:700;color:#1f2937}.header-actions{display:flex;align-items:center;gap:1rem}.diff-navigation{display:flex;align-items:center;gap:.5rem;background:#fff;border:1px solid #e5e7eb;border-radius:.5rem;padding:.25rem}.nav-btn{background:#fff;border:1px solid #e5e7eb;border-radius:.375rem;padding:.5rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;color:#6b7280}.nav-btn:hover{background:#f9fafb;border-color:#2563eb;color:#2563eb}.diff-counter{font-size:.875rem;font-weight:600;color:#1f2937;padding:0 .5rem;white-space:nowrap;min-width:60px;text-align:center}.fullscreen-btn{background:#fff;border:1px solid #e5e7eb;border-radius:.5rem;padding:.5rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;color:#6b7280}.fullscreen-btn:hover{background:#f9fafb;border-color:#2563eb;color:#2563eb}.summary{display:flex;gap:1rem}.summary-item{flex:1;display:flex;flex-direction:column;align-items:center;padding:1rem;border-radius:.5rem;background:#fff;border:2px solid}.summary-item.added{border-color:#10b981;background:#ecfdf5}.summary-item.removed{border-color:#ef4444;background:#fef2f2}.summary-item.modified{border-color:#f59e0b;background:#fffbeb}.summary-item.unchanged{border-color:#6b7280;background:#f9fafb}.summary-item .count{font-size:1.5rem;font-weight:700}.summary-item.added .count{color:#10b981}.summary-item.removed .count{color:#ef4444}.summary-item.modified .count{color:#f59e0b}.summary-item.unchanged .count{color:#6b7280}.summary-item .label{font-size:.75rem;font-weight:600;text-transform:uppercase;margin-top:.25rem}.structure-comparison{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;height:100%}.comparison-section{background:#fff;border-radius:.5rem;padding:1rem;border:1px solid #e5e7eb;min-width:0;width:100%;overflow:auto}.comparison-section h3{display:flex;align-items:center;gap:.5rem;margin:0 0 1rem;font-size:1rem;font-weight:600;color:#1f2937;padding-bottom:.75rem;border-bottom:1px solid #e5e7eb}.structure-tree{display:flex;flex-direction:column;gap:.5rem}.tree-node{border-left:3px solid transparent;padding-left:.5rem;transition:all .2s;max-width:100%;overflow:hidden}.tree-node.added{background:#ecfdf5;border-left-color:#10b981}.tree-node.removed{background:#fef2f2;border-left-color:#ef4444}.tree-node.modified{background:#fffbeb;border-left-color:#f59e0b}.node-header{display:flex;align-items:center;gap:.5rem;padding:.5rem;border-radius:.375rem}.expand-btn{background:none;border:none;cursor:pointer;padding:.25rem;display:flex;align-items:center;justify-content:center;border-radius:.25rem;transition:background .2s}.expand-btn:hover{background:#f3f4f6}.spacer{width:24px}.node-icon{color:#6b7280}.node-content{display:flex;align-items:center;gap:.5rem;flex:1}.node-name{font-size:.875rem;font-weight:500;color:#1f2937}.node-type{font-size:.75rem;color:#6b7280;background:#f3f4f6;padding:.125rem .5rem;border-radius:.25rem}.change-badge{font-size:.625rem;font-weight:600;text-transform:uppercase;padding:.125rem .5rem;border-radius:.25rem;margin-left:auto}.tree-node.added .change-badge{background:#10b981;color:#fff}.tree-node.removed .change-badge{background:#ef4444;color:#fff}.tree-node.modified .change-badge{background:#f59e0b;color:#fff}.node-properties{margin-left:2.5rem;margin-top:.5rem;padding:.5rem;background:#f9fafb;border-radius:.375rem;font-size:.75rem;max-width:100%;overflow:hidden}.property{display:flex;flex-wrap:wrap;gap:.5rem;padding:.25rem 0;max-width:100%;overflow:hidden}.property.modified{background:#fef3c7;padding:.25rem .5rem;border-radius:.25rem;margin:.125rem 0}.property-key{font-weight:600;color:#374151;flex-shrink:0}.property-value{color:#6b7280;word-break:break-word;overflow:auto;overflow-wrap:break-word;flex:1;min-width:0}.node-children{margin-left:1.5rem;margin-top:.5rem;display:flex;flex-direction:column;gap:.5rem}.comparison-container.fullscreen{position:fixed;inset:0;z-index:9999;background:#f9fafb}.comparison-container.fullscreen .header,.comparison-container.fullscreen .file-selection,.comparison-container.fullscreen .entity-selection,.comparison-container.fullscreen .summary-section{display:none}.comparison-container.fullscreen .comparison-results{height:100vh;padding:2rem}\n"] }]
2275
+ }], ctorParameters: () => [{ type: EntityComparisonService }] });
2276
+
137
2277
  /*
138
2278
  * Public API Surface of concepto-message
139
2279
  */
@@ -142,5 +2282,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
142
2282
  * Generated bundle index. Do not edit.
143
2283
  */
144
2284
 
145
- export { ConceptoContextMenuComponent, ConceptoMessageComponent, ConceptoUserControls, ConceptoUserControlsService };
2285
+ export { ConceptoContextMenuComponent, ConceptoMessageComponent, ConceptoTreeComponent, ConceptoUserControls, ConceptoUserControlsService, EntityComparisonComponent, EntityComparisonService };
146
2286
  //# sourceMappingURL=concepto-user-controls.mjs.map