concepto-user-controls 0.0.8 → 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 (33) hide show
  1. package/esm2022/lib/concepto-tree/components/tree-node/tree-node.component.mjs +301 -0
  2. package/esm2022/lib/concepto-tree/components/tree-node-checkbox/tree-node-checkbox.component.mjs +90 -0
  3. package/esm2022/lib/concepto-tree/components/tree-node-content/tree-node-content.component.mjs +65 -0
  4. package/esm2022/lib/concepto-tree/components/tree-node-expander/tree-node-expander.component.mjs +74 -0
  5. package/esm2022/lib/concepto-tree/components/tree-root/tree-root.component.mjs +230 -0
  6. package/esm2022/lib/concepto-tree/components/tree-viewport/tree-viewport.component.mjs +216 -0
  7. package/esm2022/lib/concepto-tree/concepto-tree.component.mjs +214 -13
  8. package/esm2022/lib/concepto-tree/core/models/tree-events.model.mjs +2 -0
  9. package/esm2022/lib/concepto-tree/core/models/tree-node.model.mjs +102 -0
  10. package/esm2022/lib/concepto-tree/core/models/tree-options.model.mjs +24 -0
  11. package/esm2022/lib/concepto-tree/core/models/tree.model.mjs +313 -0
  12. package/esm2022/lib/concepto-tree/core/services/tree-drag-drop.service.mjs +27 -0
  13. package/esm2022/lib/concepto-tree/directives/tree-drag.directive.mjs +69 -0
  14. package/esm2022/lib/concepto-tree/directives/tree-drop.directive.mjs +124 -0
  15. package/esm2022/lib/concepto-tree/directives/tree-node-template.directive.mjs +19 -0
  16. package/fesm2022/concepto-user-controls.mjs +1818 -12
  17. package/fesm2022/concepto-user-controls.mjs.map +1 -1
  18. package/lib/concepto-tree/components/tree-node/tree-node.component.d.ts +19 -0
  19. package/lib/concepto-tree/components/tree-node-checkbox/tree-node-checkbox.component.d.ts +17 -0
  20. package/lib/concepto-tree/components/tree-node-content/tree-node-content.component.d.ts +18 -0
  21. package/lib/concepto-tree/components/tree-node-expander/tree-node-expander.component.d.ts +12 -0
  22. package/lib/concepto-tree/components/tree-root/tree-root.component.d.ts +35 -0
  23. package/lib/concepto-tree/components/tree-viewport/tree-viewport.component.d.ts +33 -0
  24. package/lib/concepto-tree/concepto-tree.component.d.ts +32 -1
  25. package/lib/concepto-tree/core/models/tree-events.model.d.ts +13 -0
  26. package/lib/concepto-tree/core/models/tree-node.model.d.ts +39 -0
  27. package/lib/concepto-tree/core/models/tree-options.model.d.ts +28 -0
  28. package/lib/concepto-tree/core/models/tree.model.d.ts +54 -0
  29. package/lib/concepto-tree/core/services/tree-drag-drop.service.d.ts +11 -0
  30. package/lib/concepto-tree/directives/tree-drag.directive.d.ts +16 -0
  31. package/lib/concepto-tree/directives/tree-drop.directive.d.ts +25 -0
  32. package/lib/concepto-tree/directives/tree-node-template.directive.d.ts +8 -0
  33. package/package.json +1 -1
@@ -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() { }
@@ -130,22 +135,1823 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
130
135
  args: ['document:click', ['$event']]
131
136
  }] } });
132
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
+
133
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
+ }
134
1809
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConceptoTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
135
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: ConceptoTreeComponent, isStandalone: true, selector: "lib-concepto-tree", ngImport: i0, template: `
136
- <p>
137
- concepto-tree works!
138
- </p>
139
- `, isInline: true, styles: [""] });
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 });
140
1866
  }
141
1867
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConceptoTreeComponent, decorators: [{
142
1868
  type: Component,
143
- args: [{ selector: 'lib-concepto-tree', standalone: true, imports: [], template: `
144
- <p>
145
- concepto-tree works!
146
- </p>
147
- ` }]
148
- }] });
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
+ }] } });
149
1955
 
150
1956
  class EntityComparisonService {
151
1957
  parseEntities(xmlString) {