@memberjunction/ng-trees 5.23.0 → 5.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/tree/tree.component.d.ts.map +1 -1
- package/dist/lib/tree/tree.component.js +7 -1
- package/dist/lib/tree/tree.component.js.map +1 -1
- package/dist/lib/tree-dropdown/tree-dropdown.component.d.ts +35 -1
- package/dist/lib/tree-dropdown/tree-dropdown.component.d.ts.map +1 -1
- package/dist/lib/tree-dropdown/tree-dropdown.component.js +211 -23
- package/dist/lib/tree-dropdown/tree-dropdown.component.js.map +1 -1
- package/package.json +4 -4
|
@@ -189,7 +189,9 @@ export declare class TreeDropdownComponent implements OnInit, OnDestroy, AfterVi
|
|
|
189
189
|
*/
|
|
190
190
|
onSearchInput(event: Event): void;
|
|
191
191
|
/**
|
|
192
|
-
* Handle search keydown
|
|
192
|
+
* Handle search keydown — provides full keyboard navigation while search input retains focus.
|
|
193
|
+
* The tree's own keyboard handler doesn't fire because the search input has DOM focus,
|
|
194
|
+
* so all navigation is handled here by manipulating the tree's FocusedNode visual state.
|
|
193
195
|
*/
|
|
194
196
|
onSearchKeyDown(event: KeyboardEvent): void;
|
|
195
197
|
/**
|
|
@@ -210,6 +212,38 @@ export declare class TreeDropdownComponent implements OnInit, OnDestroy, AfterVi
|
|
|
210
212
|
*/
|
|
211
213
|
onTreeBeforeNodeSelect(event: BeforeNodeSelectEventArgs): void;
|
|
212
214
|
onTreeAfterNodeSelect(event: AfterNodeSelectEventArgs): void;
|
|
215
|
+
/**
|
|
216
|
+
* Focus the search input after the dropdown opens.
|
|
217
|
+
* Deferred to the next macrotask so that:
|
|
218
|
+
* 1. Angular has fully resolved @ViewChild queries for the @if block
|
|
219
|
+
* 2. The browser's click event (on the trigger) has completed and won't steal focus back
|
|
220
|
+
* Falls back to direct DOM querySelector if the ViewChild isn't resolved.
|
|
221
|
+
*/
|
|
222
|
+
private focusSearchInput;
|
|
223
|
+
/**
|
|
224
|
+
* Navigate tree focus up/down/first/last while search input retains DOM focus
|
|
225
|
+
*/
|
|
226
|
+
private navigateTree;
|
|
227
|
+
/**
|
|
228
|
+
* Select the currently focused tree node (Enter key)
|
|
229
|
+
*/
|
|
230
|
+
private selectFocusedNode;
|
|
231
|
+
/**
|
|
232
|
+
* ArrowRight: expand focused branch or descend to first child
|
|
233
|
+
*/
|
|
234
|
+
private expandOrDescendFocusedNode;
|
|
235
|
+
/**
|
|
236
|
+
* ArrowLeft: collapse focused branch or ascend to parent
|
|
237
|
+
*/
|
|
238
|
+
private collapseOrAscendFocusedNode;
|
|
239
|
+
/**
|
|
240
|
+
* Toggle expand/collapse of a branch node
|
|
241
|
+
*/
|
|
242
|
+
private toggleBranchExpansion;
|
|
243
|
+
/**
|
|
244
|
+
* Scroll the currently focused tree node into view within the dropdown panel
|
|
245
|
+
*/
|
|
246
|
+
private scrollFocusedNodeIntoView;
|
|
213
247
|
/**
|
|
214
248
|
* Create dropdown portal element (attached to body)
|
|
215
249
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree-dropdown.component.d.ts","sourceRoot":"","sources":["../../../src/lib/tree-dropdown/tree-dropdown.component.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAIH,YAAY,EACZ,iBAAiB,EACjB,MAAM,EACN,SAAS,EAET,UAAU,EAAG,6CAA6C;AAC1D,SAAS,EACT,aAAa,EAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EACH,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACH,yBAAyB,EACzB,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,EACpB,2BAA2B,EAC3B,0BAA0B,EAC1B,4BAA4B,EAC5B,2BAA2B,EAC3B,uBAAuB,EACvB,sBAAsB,EACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,EAAY,YAAY,EAAE,MAAM,sBAAsB,CAAC;;
|
|
1
|
+
{"version":3,"file":"tree-dropdown.component.d.ts","sourceRoot":"","sources":["../../../src/lib/tree-dropdown/tree-dropdown.component.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,7 @@ import { BeforeSearchEventArgs, AfterSearchEventArgs, BeforeDropdownOpenEventArg
|
|
|
13
13
|
import { Subject } from 'rxjs';
|
|
14
14
|
import { debounceTime, takeUntil } from 'rxjs/operators';
|
|
15
15
|
import { Metadata, CompositeKey } from '@memberjunction/core';
|
|
16
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
16
17
|
import * as i0 from "@angular/core";
|
|
17
18
|
import * as i1 from "@angular/common";
|
|
18
19
|
import * as i2 from "../tree/tree.component";
|
|
@@ -298,16 +299,13 @@ export class TreeDropdownComponent {
|
|
|
298
299
|
this.IsOpen = true;
|
|
299
300
|
this.calculatePosition();
|
|
300
301
|
this.attachEventListeners();
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
}, 50);
|
|
302
|
+
// Trigger change detection so the @if (IsOpen) block renders the dropdown panel
|
|
303
|
+
this.cdr.detectChanges();
|
|
304
|
+
// Focus search input after rendering
|
|
305
|
+
this.focusSearchInput();
|
|
307
306
|
// Fire after event
|
|
308
307
|
const afterEvent = new AfterDropdownOpenEventArgs(this, this.Position?.renderAbove ? 'above' : 'below');
|
|
309
308
|
this.AfterDropdownOpen.emit(afterEvent);
|
|
310
|
-
this.cdr.detectChanges();
|
|
311
309
|
}
|
|
312
310
|
/**
|
|
313
311
|
* Close the dropdown
|
|
@@ -409,7 +407,9 @@ export class TreeDropdownComponent {
|
|
|
409
407
|
this.searchSubject.next(value);
|
|
410
408
|
}
|
|
411
409
|
/**
|
|
412
|
-
* Handle search keydown
|
|
410
|
+
* Handle search keydown — provides full keyboard navigation while search input retains focus.
|
|
411
|
+
* The tree's own keyboard handler doesn't fire because the search input has DOM focus,
|
|
412
|
+
* so all navigation is handled here by manipulating the tree's FocusedNode visual state.
|
|
413
413
|
*/
|
|
414
414
|
onSearchKeyDown(event) {
|
|
415
415
|
switch (event.key) {
|
|
@@ -420,14 +420,31 @@ export class TreeDropdownComponent {
|
|
|
420
420
|
break;
|
|
421
421
|
case 'ArrowDown':
|
|
422
422
|
event.preventDefault();
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
423
|
+
this.navigateTree('down');
|
|
424
|
+
break;
|
|
425
|
+
case 'ArrowUp':
|
|
426
|
+
event.preventDefault();
|
|
427
|
+
this.navigateTree('up');
|
|
428
|
+
break;
|
|
429
|
+
case 'ArrowRight':
|
|
430
|
+
event.preventDefault();
|
|
431
|
+
this.expandOrDescendFocusedNode();
|
|
432
|
+
break;
|
|
433
|
+
case 'ArrowLeft':
|
|
434
|
+
event.preventDefault();
|
|
435
|
+
this.collapseOrAscendFocusedNode();
|
|
436
|
+
break;
|
|
437
|
+
case 'Enter':
|
|
438
|
+
event.preventDefault();
|
|
439
|
+
this.selectFocusedNode();
|
|
440
|
+
break;
|
|
441
|
+
case 'Home':
|
|
442
|
+
event.preventDefault();
|
|
443
|
+
this.navigateTree('first');
|
|
444
|
+
break;
|
|
445
|
+
case 'End':
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
this.navigateTree('last');
|
|
431
448
|
break;
|
|
432
449
|
}
|
|
433
450
|
}
|
|
@@ -503,6 +520,172 @@ export class TreeDropdownComponent {
|
|
|
503
520
|
this.AfterNodeSelect.emit(event);
|
|
504
521
|
}
|
|
505
522
|
// ========================================
|
|
523
|
+
// Keyboard Navigation Helpers
|
|
524
|
+
// ========================================
|
|
525
|
+
/**
|
|
526
|
+
* Focus the search input after the dropdown opens.
|
|
527
|
+
* Deferred to the next macrotask so that:
|
|
528
|
+
* 1. Angular has fully resolved @ViewChild queries for the @if block
|
|
529
|
+
* 2. The browser's click event (on the trigger) has completed and won't steal focus back
|
|
530
|
+
* Falls back to direct DOM querySelector if the ViewChild isn't resolved.
|
|
531
|
+
*/
|
|
532
|
+
focusSearchInput() {
|
|
533
|
+
if (!this.EnableSearch)
|
|
534
|
+
return;
|
|
535
|
+
const tryFocus = (retriesLeft) => {
|
|
536
|
+
// Try ViewChild first
|
|
537
|
+
if (this.searchInput?.nativeElement) {
|
|
538
|
+
this.searchInput.nativeElement.focus();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
// Fallback: query DOM directly (ViewChild may not resolve for @if blocks)
|
|
542
|
+
const panel = this.dropdownPanel?.nativeElement;
|
|
543
|
+
if (panel) {
|
|
544
|
+
const input = panel.querySelector('.tree-dropdown-search__input');
|
|
545
|
+
if (input) {
|
|
546
|
+
input.focus();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Retry on next frame
|
|
551
|
+
if (retriesLeft > 0) {
|
|
552
|
+
requestAnimationFrame(() => tryFocus(retriesLeft - 1));
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
// Defer to next macrotask so the click event on the trigger completes first
|
|
556
|
+
// and Angular finishes resolving ViewChild queries for the new @if block
|
|
557
|
+
setTimeout(() => tryFocus(5), 0);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Navigate tree focus up/down/first/last while search input retains DOM focus
|
|
561
|
+
*/
|
|
562
|
+
navigateTree(direction) {
|
|
563
|
+
if (!this.treeComponent?.Nodes?.length)
|
|
564
|
+
return;
|
|
565
|
+
const visibleNodes = this.getVisibleNodesInOrder(this.treeComponent.Nodes);
|
|
566
|
+
if (visibleNodes.length === 0)
|
|
567
|
+
return;
|
|
568
|
+
const currentIndex = this.treeComponent.FocusedNode
|
|
569
|
+
? visibleNodes.indexOf(this.treeComponent.FocusedNode)
|
|
570
|
+
: -1;
|
|
571
|
+
let targetNode = null;
|
|
572
|
+
switch (direction) {
|
|
573
|
+
case 'down':
|
|
574
|
+
targetNode = currentIndex === -1
|
|
575
|
+
? visibleNodes[0]
|
|
576
|
+
: currentIndex < visibleNodes.length - 1
|
|
577
|
+
? visibleNodes[currentIndex + 1]
|
|
578
|
+
: null;
|
|
579
|
+
break;
|
|
580
|
+
case 'up':
|
|
581
|
+
targetNode = currentIndex === -1
|
|
582
|
+
? visibleNodes[visibleNodes.length - 1]
|
|
583
|
+
: currentIndex > 0
|
|
584
|
+
? visibleNodes[currentIndex - 1]
|
|
585
|
+
: null;
|
|
586
|
+
break;
|
|
587
|
+
case 'first':
|
|
588
|
+
targetNode = visibleNodes[0];
|
|
589
|
+
break;
|
|
590
|
+
case 'last':
|
|
591
|
+
targetNode = visibleNodes[visibleNodes.length - 1];
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
if (targetNode) {
|
|
595
|
+
this.treeComponent.FocusedNode = targetNode;
|
|
596
|
+
this.cdr.detectChanges();
|
|
597
|
+
this.scrollFocusedNodeIntoView();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Select the currently focused tree node (Enter key)
|
|
602
|
+
*/
|
|
603
|
+
selectFocusedNode() {
|
|
604
|
+
const focusedNode = this.treeComponent?.FocusedNode;
|
|
605
|
+
if (!focusedNode)
|
|
606
|
+
return;
|
|
607
|
+
// Check if the node type is selectable per config
|
|
608
|
+
const isSelectable = this.SelectableTypes === 'both'
|
|
609
|
+
|| (this.SelectableTypes === 'leaf' && focusedNode.Type === 'leaf')
|
|
610
|
+
|| (this.SelectableTypes === 'branch' && focusedNode.Type === 'branch');
|
|
611
|
+
if (isSelectable) {
|
|
612
|
+
this.treeComponent.SelectNodes([focusedNode.ID], true);
|
|
613
|
+
}
|
|
614
|
+
else if (focusedNode.Type === 'branch') {
|
|
615
|
+
// Non-selectable branch: toggle expand/collapse
|
|
616
|
+
this.toggleBranchExpansion(focusedNode);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* ArrowRight: expand focused branch or descend to first child
|
|
621
|
+
*/
|
|
622
|
+
expandOrDescendFocusedNode() {
|
|
623
|
+
const node = this.treeComponent?.FocusedNode;
|
|
624
|
+
if (!node || node.Type !== 'branch')
|
|
625
|
+
return;
|
|
626
|
+
if (!node.Expanded && node.Children?.length) {
|
|
627
|
+
this.treeComponent.ExpandToNode(node.Children[0].ID);
|
|
628
|
+
this.cdr.detectChanges();
|
|
629
|
+
}
|
|
630
|
+
else if (node.Expanded && node.Children?.length) {
|
|
631
|
+
// Already expanded: move focus to first visible child
|
|
632
|
+
const firstVisible = node.Children.find(c => c.Visible);
|
|
633
|
+
if (firstVisible) {
|
|
634
|
+
this.treeComponent.FocusedNode = firstVisible;
|
|
635
|
+
this.cdr.detectChanges();
|
|
636
|
+
this.scrollFocusedNodeIntoView();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* ArrowLeft: collapse focused branch or ascend to parent
|
|
642
|
+
*/
|
|
643
|
+
collapseOrAscendFocusedNode() {
|
|
644
|
+
const node = this.treeComponent?.FocusedNode;
|
|
645
|
+
if (!node)
|
|
646
|
+
return;
|
|
647
|
+
if (node.Type === 'branch' && node.Expanded) {
|
|
648
|
+
// Collapse the branch
|
|
649
|
+
this.toggleBranchExpansion(node);
|
|
650
|
+
}
|
|
651
|
+
else if (node.ParentID) {
|
|
652
|
+
// Move to parent
|
|
653
|
+
const allNodes = this.getVisibleNodesInOrder(this.treeComponent.Nodes);
|
|
654
|
+
const parent = allNodes.find(n => UUIDsEqual(n.ID, node.ParentID));
|
|
655
|
+
if (parent) {
|
|
656
|
+
this.treeComponent.FocusedNode = parent;
|
|
657
|
+
this.cdr.detectChanges();
|
|
658
|
+
this.scrollFocusedNodeIntoView();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Toggle expand/collapse of a branch node
|
|
664
|
+
*/
|
|
665
|
+
toggleBranchExpansion(node) {
|
|
666
|
+
if (node.Expanded) {
|
|
667
|
+
node.Expanded = false;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
this.treeComponent.ExpandToNode(node.ID);
|
|
671
|
+
}
|
|
672
|
+
this.cdr.detectChanges();
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Scroll the currently focused tree node into view within the dropdown panel
|
|
676
|
+
*/
|
|
677
|
+
scrollFocusedNodeIntoView() {
|
|
678
|
+
requestAnimationFrame(() => {
|
|
679
|
+
const panel = this.dropdownPanel?.nativeElement;
|
|
680
|
+
if (!panel)
|
|
681
|
+
return;
|
|
682
|
+
const focused = panel.querySelector('.tree-node-focused, .tree-node--focused, [data-focused="true"]');
|
|
683
|
+
if (focused) {
|
|
684
|
+
focused.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
// ========================================
|
|
506
689
|
// Private Methods
|
|
507
690
|
// ========================================
|
|
508
691
|
/**
|
|
@@ -692,7 +875,8 @@ export class TreeDropdownComponent {
|
|
|
692
875
|
return;
|
|
693
876
|
}
|
|
694
877
|
const searchText = beforeEvent.ModifiedSearchText ?? text;
|
|
695
|
-
|
|
878
|
+
// Guard: tree component may not be ready or may have no nodes loaded
|
|
879
|
+
if (!this.treeComponent || !this.IsLoaded) {
|
|
696
880
|
return;
|
|
697
881
|
}
|
|
698
882
|
const matchedNodes = this.treeComponent.FilterNodes(searchText, {
|
|
@@ -701,14 +885,14 @@ export class TreeDropdownComponent {
|
|
|
701
885
|
searchLeaves: this.SearchConfig.SearchLeaves ?? true,
|
|
702
886
|
searchDescription: this.SearchConfig.SearchDescription
|
|
703
887
|
});
|
|
704
|
-
// Auto-expand to show matches
|
|
705
|
-
if (this.SearchConfig.AutoExpandMatches !== false && searchText.trim()) {
|
|
888
|
+
// Auto-expand to show matches — guard against empty results
|
|
889
|
+
if (matchedNodes.length > 0 && this.SearchConfig.AutoExpandMatches !== false && searchText.trim()) {
|
|
706
890
|
for (const node of matchedNodes) {
|
|
707
891
|
this.treeComponent.ExpandToNode(node.ID);
|
|
708
892
|
}
|
|
709
893
|
}
|
|
710
894
|
// Fire after event
|
|
711
|
-
const afterEvent = new AfterSearchEventArgs(this, searchText, matchedNodes);
|
|
895
|
+
const afterEvent = new AfterSearchEventArgs(this, searchText, matchedNodes ?? []);
|
|
712
896
|
this.AfterSearch.emit(afterEvent);
|
|
713
897
|
this.cdr.detectChanges();
|
|
714
898
|
}
|
|
@@ -716,7 +900,7 @@ export class TreeDropdownComponent {
|
|
|
716
900
|
* Clear search filter
|
|
717
901
|
*/
|
|
718
902
|
clearSearch() {
|
|
719
|
-
if (this.treeComponent) {
|
|
903
|
+
if (this.treeComponent?.Nodes?.length) {
|
|
720
904
|
this.treeComponent.FilterNodes('', {});
|
|
721
905
|
this.cdr.detectChanges();
|
|
722
906
|
}
|
|
@@ -725,15 +909,19 @@ export class TreeDropdownComponent {
|
|
|
725
909
|
* Get all visible nodes in tree order (for keyboard navigation)
|
|
726
910
|
*/
|
|
727
911
|
getVisibleNodesInOrder(nodes) {
|
|
912
|
+
if (!nodes || nodes.length === 0)
|
|
913
|
+
return [];
|
|
728
914
|
const result = [];
|
|
729
915
|
this.collectVisibleNodesRecursive(nodes, result);
|
|
730
916
|
return result;
|
|
731
917
|
}
|
|
732
918
|
collectVisibleNodesRecursive(nodes, result) {
|
|
919
|
+
if (!nodes)
|
|
920
|
+
return;
|
|
733
921
|
for (const node of nodes) {
|
|
734
922
|
if (node.Visible) {
|
|
735
923
|
result.push(node);
|
|
736
|
-
if (node.Expanded && node.Type === 'branch') {
|
|
924
|
+
if (node.Expanded && node.Type === 'branch' && node.Children?.length) {
|
|
737
925
|
this.collectVisibleNodesRecursive(node.Children, result);
|
|
738
926
|
}
|
|
739
927
|
}
|
|
@@ -1005,5 +1193,5 @@ export class TreeDropdownComponent {
|
|
|
1005
1193
|
type: ViewChild,
|
|
1006
1194
|
args: ['treeComponent']
|
|
1007
1195
|
}] }); })();
|
|
1008
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(TreeDropdownComponent, { className: "TreeDropdownComponent", filePath: "src/lib/tree-dropdown/tree-dropdown.component.ts", lineNumber:
|
|
1196
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(TreeDropdownComponent, { className: "TreeDropdownComponent", filePath: "src/lib/tree-dropdown/tree-dropdown.component.ts", lineNumber: 70 }); })();
|
|
1009
1197
|
//# sourceMappingURL=tree-dropdown.component.js.map
|