concepto-user-controls 0.0.8 → 0.0.10

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 (37) hide show
  1. package/esm2022/lib/concepto-input/concepto-input.component.mjs +20 -0
  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 +214 -13
  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/public-api.mjs +2 -1
  18. package/fesm2022/concepto-user-controls.mjs +1837 -13
  19. package/fesm2022/concepto-user-controls.mjs.map +1 -1
  20. package/lib/concepto-input/concepto-input.component.d.ts +8 -0
  21. package/lib/concepto-tree/components/tree-node/tree-node.component.d.ts +19 -0
  22. package/lib/concepto-tree/components/tree-node-checkbox/tree-node-checkbox.component.d.ts +17 -0
  23. package/lib/concepto-tree/components/tree-node-content/tree-node-content.component.d.ts +18 -0
  24. package/lib/concepto-tree/components/tree-node-expander/tree-node-expander.component.d.ts +12 -0
  25. package/lib/concepto-tree/components/tree-root/tree-root.component.d.ts +35 -0
  26. package/lib/concepto-tree/components/tree-viewport/tree-viewport.component.d.ts +33 -0
  27. package/lib/concepto-tree/concepto-tree.component.d.ts +32 -1
  28. package/lib/concepto-tree/core/models/tree-events.model.d.ts +13 -0
  29. package/lib/concepto-tree/core/models/tree-node.model.d.ts +39 -0
  30. package/lib/concepto-tree/core/models/tree-options.model.d.ts +28 -0
  31. package/lib/concepto-tree/core/models/tree.model.d.ts +54 -0
  32. package/lib/concepto-tree/core/services/tree-drag-drop.service.d.ts +11 -0
  33. package/lib/concepto-tree/directives/tree-drag.directive.d.ts +16 -0
  34. package/lib/concepto-tree/directives/tree-drop.directive.d.ts +25 -0
  35. package/lib/concepto-tree/directives/tree-node-template.directive.d.ts +8 -0
  36. package/package.json +1 -1
  37. package/public-api.d.ts +1 -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() { }
@@ -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) {
@@ -468,6 +2274,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
468
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"] }]
469
2275
  }], ctorParameters: () => [{ type: EntityComparisonService }] });
470
2276
 
2277
+ class ConceptoInputComponent {
2278
+ labelText = 'Date';
2279
+ name = 'date-input';
2280
+ type = 'text';
2281
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConceptoInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2282
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: ConceptoInputComponent, isStandalone: true, selector: "concepto-input", inputs: { labelText: "labelText", name: "name", type: "type" }, ngImport: i0, template: "<div class=\"field\">\r\n @if(labelText !== '') {\r\n <label [for]=\"name\" class=\"label\">{{labelText}}</label>\r\n }\r\n @if(type === 'text') {\r\n <input [name]=\"name\" [id]=\"name\" type=\"text\" class=\"text-input\" />\r\n } @else if(type === 'date') {\r\n <input [name]=\"name\" [id]=\"name\" type=\"date\" class=\"date-input\" />\r\n } @else if(type === 'dropdown') {\r\n <select [name]=\"name\" [id]=\"name\" class=\"dropdown-input\">\r\n <option value=\"\" disabled selected>Select an option</option>\r\n </select>\r\n }\r\n</div>", styles: [".field{display:flex;flex-direction:column;align-items:flex-start;gap:10px;width:100%;margin:20px 0}.label{font-family:var(--font);font-style:normal;font-weight:400;font-size:12px;line-height:15px;color:var(--primary-text-color)}.date-input,.text-input,.dropdown-input{box-sizing:border-box;width:100%;height:35px;padding:10px;border:1px solid var(--secondary-color);border-radius:5px;font-family:var(--font);font-style:normal;font-weight:400;font-size:12px;line-height:15px;color:var(--primary-text-color);background-color:var( --primary-background-color )}.dropdown-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;background-image:url('data:image/svg+xml;utf8,<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M6 9L12 15L18 9\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke=\"currentColor\"/></svg>');background-repeat:no-repeat;background-position:right 10px center;background-size:18px}.dropdown-input:open{background-image:url('data:image/svg+xml;utf8,<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M6 15L12 9L18 15\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke=\"currentColor\"/></svg>')}.date-input:focus,.text-input:focus,.dropdown-input:focus{outline:none;border-color:#0e0e12}.date-input::-webkit-calendar-picker-indicator{cursor:pointer}\n"] });
2283
+ }
2284
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ConceptoInputComponent, decorators: [{
2285
+ type: Component,
2286
+ args: [{ selector: 'concepto-input', standalone: true, imports: [], template: "<div class=\"field\">\r\n @if(labelText !== '') {\r\n <label [for]=\"name\" class=\"label\">{{labelText}}</label>\r\n }\r\n @if(type === 'text') {\r\n <input [name]=\"name\" [id]=\"name\" type=\"text\" class=\"text-input\" />\r\n } @else if(type === 'date') {\r\n <input [name]=\"name\" [id]=\"name\" type=\"date\" class=\"date-input\" />\r\n } @else if(type === 'dropdown') {\r\n <select [name]=\"name\" [id]=\"name\" class=\"dropdown-input\">\r\n <option value=\"\" disabled selected>Select an option</option>\r\n </select>\r\n }\r\n</div>", styles: [".field{display:flex;flex-direction:column;align-items:flex-start;gap:10px;width:100%;margin:20px 0}.label{font-family:var(--font);font-style:normal;font-weight:400;font-size:12px;line-height:15px;color:var(--primary-text-color)}.date-input,.text-input,.dropdown-input{box-sizing:border-box;width:100%;height:35px;padding:10px;border:1px solid var(--secondary-color);border-radius:5px;font-family:var(--font);font-style:normal;font-weight:400;font-size:12px;line-height:15px;color:var(--primary-text-color);background-color:var( --primary-background-color )}.dropdown-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;background-image:url('data:image/svg+xml;utf8,<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M6 9L12 15L18 9\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke=\"currentColor\"/></svg>');background-repeat:no-repeat;background-position:right 10px center;background-size:18px}.dropdown-input:open{background-image:url('data:image/svg+xml;utf8,<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M6 15L12 9L18 15\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke=\"currentColor\"/></svg>')}.date-input:focus,.text-input:focus,.dropdown-input:focus{outline:none;border-color:#0e0e12}.date-input::-webkit-calendar-picker-indicator{cursor:pointer}\n"] }]
2287
+ }], propDecorators: { labelText: [{
2288
+ type: Input
2289
+ }], name: [{
2290
+ type: Input
2291
+ }], type: [{
2292
+ type: Input
2293
+ }] } });
2294
+
471
2295
  /*
472
2296
  * Public API Surface of concepto-message
473
2297
  */
@@ -476,5 +2300,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
476
2300
  * Generated bundle index. Do not edit.
477
2301
  */
478
2302
 
479
- export { ConceptoContextMenuComponent, ConceptoMessageComponent, ConceptoTreeComponent, ConceptoUserControls, ConceptoUserControlsService, EntityComparisonComponent, EntityComparisonService };
2303
+ export { ConceptoContextMenuComponent, ConceptoInputComponent, ConceptoMessageComponent, ConceptoTreeComponent, ConceptoUserControls, ConceptoUserControlsService, EntityComparisonComponent, EntityComparisonService };
480
2304
  //# sourceMappingURL=concepto-user-controls.mjs.map