@mozaic-ds/angular 2.0.32 → 2.0.34

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.
@@ -7,7 +7,7 @@ import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/fo
7
7
  import { WarningCircle32, Uploading32, CheckCircle32, CrossCircleFilled20, Refresh32, Refresh20, Eye20, Upload24, Cross24, ChevronLeft24, ChevronRight24, ChevronLeft20, ChevronRight20, CrossCircleFilled24, More24, Less24, InfoCircle32, CrossCircle32, Cross20, CrossCircle24, ImageAlt32, ChevronDown24, CheckCircleFilled32, WarningCircleFilled32, CrossCircleFilled32, InfoCircleFilled32, SidebarExpand24, ChevronDown20, InfoCircleFilled24, WarningCircleFilled24, CheckCircleFilled24, ArrowBottomRight24, ArrowTopRight24, StarHalf32, StarFilled32, Star32, StarHalf24, StarFilled24, Star24, StarHalf20, StarFilled20, Star20, Check20, Check24, ArrowBack24, ArrowNext24, HelpCircle24, Menu24, Notification24, Search24, PauseCircle24, PlayCircle24, ChevronUp20, Settings20, Drag20, ListAdd20, ViewGridX420, Filter20, FullscreenEnter20, FullscreenExit20, Download20, CheckCircle24 } from '@mozaic-ds/icons-angular';
8
8
  import { Overlay, OverlayConfig, OverlayPositionBuilder, CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
9
9
  import { CdkPortalOutlet, ComponentPortal } from '@angular/cdk/portal';
10
- import { Subject, take } from 'rxjs';
10
+ import { Subject, take, tap, of, firstValueFrom } from 'rxjs';
11
11
  import parsePhoneNumberFromString, { getCountries, getExampleNumber, isValidPhoneNumber, getCountryCallingCode } from 'libphonenumber-js';
12
12
  import examples from 'libphonenumber-js/mobile/examples';
13
13
  import * as i1$1 from '@angular/cdk/scrolling';
@@ -11392,6 +11392,7 @@ class TreeStateService {
11392
11392
  expandedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedIds" }] : /* istanbul ignore next */ []));
11393
11393
  loadingIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "loadingIds" }] : /* istanbul ignore next */ []));
11394
11394
  internalNodes = signal([], ...(ngDevMode ? [{ debugName: "internalNodes" }] : /* istanbul ignore next */ []));
11395
+ loadChildrenFn = signal(null, ...(ngDevMode ? [{ debugName: "loadChildrenFn" }] : /* istanbul ignore next */ []));
11395
11396
  flatVisibleNodes = computed(() => {
11396
11397
  const result = [];
11397
11398
  this._flatten(this.internalNodes(), result, 0, null);
@@ -11427,6 +11428,22 @@ class TreeStateService {
11427
11428
  return next;
11428
11429
  });
11429
11430
  }
11431
+ expandAndLoad(node) {
11432
+ const wasExpanded = this.expandedIds().has(node.id);
11433
+ this.toggleExpanded(node.id);
11434
+ if (!wasExpanded) {
11435
+ const loadFn = this.loadChildrenFn();
11436
+ const resolved = this.findNode(node.id) ?? node;
11437
+ if (loadFn && resolved.children === undefined) {
11438
+ this.addLoading(node.id);
11439
+ return loadFn(node).pipe(take(1), tap((children) => {
11440
+ this.patchChildren(node.id, children);
11441
+ this.removeLoading(node.id);
11442
+ }), tap(() => void 0));
11443
+ }
11444
+ }
11445
+ return of(undefined);
11446
+ }
11430
11447
  addLoading(id) {
11431
11448
  this.loadingIds.update((s) => new Set([...s, id]));
11432
11449
  }
@@ -11451,6 +11468,21 @@ class TreeStateService {
11451
11468
  return n;
11452
11469
  });
11453
11470
  }
11471
+ findNode(nodeId) {
11472
+ return this._findNodeRecursive(this.internalNodes(), nodeId);
11473
+ }
11474
+ _findNodeRecursive(nodes, id) {
11475
+ for (const node of nodes) {
11476
+ if (node.id === id)
11477
+ return node;
11478
+ if (node.children) {
11479
+ const found = this._findNodeRecursive(node.children, id);
11480
+ if (found)
11481
+ return found;
11482
+ }
11483
+ }
11484
+ return null;
11485
+ }
11454
11486
  findParentId(nodeId) {
11455
11487
  const flat = this.flatVisibleNodes();
11456
11488
  const entry = flat.find((f) => f.node.id === nodeId);
@@ -11464,6 +11496,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
11464
11496
  }] });
11465
11497
 
11466
11498
  class TreeSelectionService {
11499
+ stateService = inject(TreeStateService);
11467
11500
  selectedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "selectedIds" }] : /* istanbul ignore next */ []));
11468
11501
  selectionMode = signal('none', ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
11469
11502
  rootNodes = signal([], ...(ngDevMode ? [{ debugName: "rootNodes" }] : /* istanbul ignore next */ []));
@@ -11484,40 +11517,62 @@ class TreeSelectionService {
11484
11517
  return true;
11485
11518
  return ancestors.some((a) => a.disabled);
11486
11519
  }
11520
+ /**
11521
+ * A node is indeterminate when it has loaded children AND
11522
+ * some (but not all) of its leaf-level descendants are selected.
11523
+ */
11487
11524
  isIndeterminate(node) {
11488
- if (node.children === undefined)
11489
- return false;
11490
- if (node.children.length === 0)
11525
+ const resolved = this._resolveNode(node);
11526
+ const leaves = this._collectLeafIds(resolved);
11527
+ if (leaves.length === 0)
11491
11528
  return false;
11492
- const enabledIds = this._collectEnabledDescendantIds(node);
11493
- if (enabledIds.length === 0)
11494
- return false;
11495
- const selectedCount = enabledIds.filter((id) => this.selectedIds().has(id)).length;
11496
- return selectedCount > 0 && selectedCount < enabledIds.length;
11529
+ const selectedCount = leaves.filter((id) => this.selectedIds().has(id)).length;
11530
+ return selectedCount > 0 && selectedCount < leaves.length;
11497
11531
  }
11532
+ /**
11533
+ * A node is checked when:
11534
+ * - Leaf node (no children or children not loaded): its ID is in selectedIds
11535
+ * - Parent with loaded children: ALL leaf descendants are selected
11536
+ */
11498
11537
  isCheckedComputed(node) {
11499
11538
  if (this.selectionMode() === 'radio') {
11500
11539
  return this.selectedIds().has(node.id);
11501
11540
  }
11502
- if (node.children === undefined || node.children.length === 0) {
11541
+ const resolved = this._resolveNode(node);
11542
+ const leaves = this._collectLeafIds(resolved);
11543
+ // Node is a leaf or has no loaded children
11544
+ if (leaves.length === 0) {
11503
11545
  return this.selectedIds().has(node.id);
11504
11546
  }
11505
- const enabledIds = this._collectEnabledDescendantIds(node);
11506
- if (enabledIds.length === 0)
11507
- return this.selectedIds().has(node.id);
11508
- return enabledIds.every((id) => this.selectedIds().has(id));
11547
+ return leaves.every((id) => this.selectedIds().has(id));
11509
11548
  }
11549
+ /**
11550
+ * Select a node: add all leaf descendant IDs (or the node's own ID if leaf).
11551
+ */
11510
11552
  selectNode(node) {
11511
11553
  if (this.selectionMode() === 'radio') {
11512
11554
  this.selectedIds.set(new Set([node.id]));
11513
11555
  return;
11514
11556
  }
11557
+ const resolved = this._resolveNode(node);
11515
11558
  this.selectedIds.update((current) => {
11516
11559
  const next = new Set(current);
11517
- this._collectEnabledIds(node).forEach((id) => next.add(id));
11560
+ const leaves = this._collectLeafIds(resolved);
11561
+ if (leaves.length === 0) {
11562
+ // Node is a leaf
11563
+ if (!node.disabled)
11564
+ next.add(node.id);
11565
+ }
11566
+ else {
11567
+ leaves.forEach((id) => next.add(id));
11568
+ }
11518
11569
  return next;
11519
11570
  });
11520
11571
  }
11572
+ /**
11573
+ * Deselect a node: remove all leaf descendant IDs (or the node's own ID if leaf).
11574
+ * Also remove the node's own ID in case it's in the set from external sources.
11575
+ */
11521
11576
  deselectNode(node) {
11522
11577
  if (this.selectionMode() === 'radio') {
11523
11578
  this.selectedIds.update((current) => {
@@ -11527,9 +11582,13 @@ class TreeSelectionService {
11527
11582
  });
11528
11583
  return;
11529
11584
  }
11585
+ const resolved = this._resolveNode(node);
11530
11586
  this.selectedIds.update((current) => {
11531
11587
  const next = new Set(current);
11532
- this._collectEnabledIds(node).forEach((id) => next.delete(id));
11588
+ // Always remove the node itself
11589
+ next.delete(node.id);
11590
+ // Remove all known descendants (leaf + parents)
11591
+ this._collectAllDescendantIds(resolved).forEach((id) => next.delete(id));
11533
11592
  return next;
11534
11593
  });
11535
11594
  }
@@ -11541,30 +11600,83 @@ class TreeSelectionService {
11541
11600
  this.selectNode(node);
11542
11601
  }
11543
11602
  }
11544
- _collectEnabledIds(node) {
11545
- if (node.disabled)
11603
+ /**
11604
+ * Called when children are loaded for a node.
11605
+ * - If parent ID was in selectedIds: replace it with children's leaf IDs (propagate down).
11606
+ * - If parent ID was NOT in selectedIds: remove any orphan children IDs that
11607
+ * may have been left from a prior bulk operation (e.g. Select All → deselect parent).
11608
+ */
11609
+ propagateOnChildrenLoaded(parentId) {
11610
+ const parent = this._resolveNode({ id: parentId });
11611
+ if (!parent || !parent.children || parent.children.length === 0)
11612
+ return;
11613
+ const selected = this.selectedIds();
11614
+ const next = new Set(selected);
11615
+ if (selected.has(parentId)) {
11616
+ // Parent was selected → replace with leaf children
11617
+ next.delete(parentId);
11618
+ for (const child of parent.children) {
11619
+ if (!child.disabled) {
11620
+ const childLeaves = this._collectLeafIds(this._resolveNode(child));
11621
+ if (childLeaves.length === 0) {
11622
+ next.add(child.id);
11623
+ }
11624
+ else {
11625
+ childLeaves.forEach((id) => next.add(id));
11626
+ }
11627
+ }
11628
+ }
11629
+ }
11630
+ else {
11631
+ // Parent was NOT selected → clean up any orphan descendant IDs
11632
+ this._collectAllDescendantIds(parent).forEach((id) => next.delete(id));
11633
+ }
11634
+ this.selectedIds.set(next);
11635
+ }
11636
+ /**
11637
+ * Collect leaf-level IDs (terminal nodes that have no loaded children).
11638
+ * Returns empty array if the node itself is a leaf.
11639
+ */
11640
+ _collectLeafIds(node) {
11641
+ const resolved = this._resolveNode(node);
11642
+ if (!resolved.children || resolved.children.length === 0) {
11546
11643
  return [];
11547
- const ids = [node.id];
11548
- if (node.children) {
11549
- for (const child of node.children) {
11550
- ids.push(...this._collectEnabledIds(child));
11644
+ }
11645
+ const ids = [];
11646
+ for (const child of resolved.children) {
11647
+ if (child.disabled)
11648
+ continue;
11649
+ const resolvedChild = this._resolveNode(child);
11650
+ const childLeaves = this._collectLeafIds(resolvedChild);
11651
+ if (childLeaves.length === 0) {
11652
+ // This child is a leaf
11653
+ ids.push(resolvedChild.id);
11654
+ }
11655
+ else {
11656
+ ids.push(...childLeaves);
11551
11657
  }
11552
11658
  }
11553
11659
  return ids;
11554
11660
  }
11555
- _collectEnabledDescendantIds(node) {
11661
+ /**
11662
+ * Collect ALL descendant IDs (both parents and leaves) for thorough cleanup on deselect.
11663
+ */
11664
+ _collectAllDescendantIds(node) {
11665
+ const resolved = this._resolveNode(node);
11556
11666
  const ids = [];
11557
- if (!node.children)
11667
+ if (!resolved.children)
11558
11668
  return ids;
11559
- for (const child of node.children) {
11560
- if (!child.disabled) {
11561
- ids.push(child.id);
11562
- ids.push(...this._collectEnabledDescendantIds(child));
11563
- }
11669
+ for (const child of resolved.children) {
11670
+ const resolvedChild = this._resolveNode(child);
11671
+ ids.push(resolvedChild.id);
11672
+ ids.push(...this._collectAllDescendantIds(resolvedChild));
11564
11673
  }
11565
11674
  return ids;
11566
11675
  }
11567
11676
  allSelectedIds = computed(() => new Set(this.selectedIds()), ...(ngDevMode ? [{ debugName: "allSelectedIds" }] : /* istanbul ignore next */ []));
11677
+ _resolveNode(node) {
11678
+ return this.stateService.findNode(node.id) ?? node;
11679
+ }
11568
11680
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TreeSelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
11569
11681
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TreeSelectionService });
11570
11682
  }
@@ -11614,7 +11726,7 @@ class TreeKeyboardService {
11614
11726
  break;
11615
11727
  const isExpanded = this.state.expandedIds().has(current.node.id);
11616
11728
  if (this._hasChildren(current.node) && !isExpanded && !current.node.disabled) {
11617
- this.state.toggleExpanded(current.node.id);
11729
+ this.state.expandAndLoad(current.node).subscribe();
11618
11730
  }
11619
11731
  else if (isExpanded) {
11620
11732
  const firstChild = flat[currentIndex + 1];
@@ -11642,7 +11754,7 @@ class TreeKeyboardService {
11642
11754
  if (!current)
11643
11755
  break;
11644
11756
  if (this._hasChildren(current.node) && !current.node.disabled) {
11645
- this.state.toggleExpanded(current.node.id);
11757
+ this.state.expandAndLoad(current.node).subscribe();
11646
11758
  }
11647
11759
  break;
11648
11760
  }
@@ -11690,7 +11802,7 @@ class TreeKeyboardService {
11690
11802
  const siblings = flat.filter((f) => f.depth === current.depth && f.parentId === current.parentId);
11691
11803
  for (const sibling of siblings) {
11692
11804
  if (this._hasChildren(sibling.node) && !this.state.expandedIds().has(sibling.node.id)) {
11693
- this.state.toggleExpanded(sibling.node.id);
11805
+ this.state.expandAndLoad(sibling.node).subscribe();
11694
11806
  }
11695
11807
  }
11696
11808
  }
@@ -11760,24 +11872,8 @@ class MozTreeNodeComponent {
11760
11872
  }
11761
11873
  }
11762
11874
  onToggleExpand() {
11763
- const node = this.node();
11764
- if (this.isExpanded()) {
11765
- this.stateService.toggleExpanded(node.id);
11766
- this.expandChange.emit(node.id);
11767
- return;
11768
- }
11769
- this.stateService.toggleExpanded(node.id);
11770
- this.expandChange.emit(node.id);
11771
- const loadFn = this.loadChildren();
11772
- if (loadFn && node.children === undefined) {
11773
- this.stateService.addLoading(node.id);
11774
- loadFn(node)
11775
- .pipe(take(1))
11776
- .subscribe((children) => {
11777
- this.stateService.patchChildren(node.id, children);
11778
- this.stateService.removeLoading(node.id);
11779
- });
11780
- }
11875
+ this.stateService.expandAndLoad(this.node()).subscribe();
11876
+ this.expandChange.emit(this.node().id);
11781
11877
  }
11782
11878
  onCheckboxChange(event) {
11783
11879
  if (this.isDisabled())
@@ -11811,7 +11907,7 @@ class MozTreeNodeComponent {
11811
11907
  return null;
11812
11908
  }
11813
11909
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: MozTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11814
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: MozTreeNodeComponent, isStandalone: true, selector: "moz-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, indentSize: { classPropertyName: "indentSize", publicName: "indentSize", isSignal: true, isRequired: false, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null }, nodeTemplates: { classPropertyName: "nodeTemplates", publicName: "nodeTemplates", isSignal: true, isRequired: false, transformFunction: null }, loadChildren: { classPropertyName: "loadChildren", publicName: "loadChildren", isSignal: true, isRequired: false, transformFunction: null }, ancestors: { classPropertyName: "ancestors", publicName: "ancestors", isSignal: true, isRequired: false, transformFunction: null }, flat: { classPropertyName: "flat", publicName: "flat", isSignal: true, isRequired: false, transformFunction: null }, noResultText: { classPropertyName: "noResultText", publicName: "noResultText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expandChange: "expandChange", selectionChange: "selectionChange" }, host: { styleAttribute: "display: block" }, ngImport: i0, template: "<li\n class=\"tree-node\"\n [class.tree-node--selected]=\"isSelected()\"\n [class.tree-node--disabled]=\"isDisabled()\"\n [class.tree-node--focused]=\"isFocused()\"\n role=\"treeitem\"\n [id]=\"'tree-node-' + node().id\"\n [attr.aria-level]=\"depth() + 1\"\n [attr.aria-expanded]=\"hasChildren() ? isExpanded() : null\"\n [attr.aria-selected]=\"selectionMode() !== 'none' ? isSelected() : null\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n [attr.data-tree-node-id]=\"node().id\"\n [tabindex]=\"isFocused() ? 0 : -1\"\n>\n <div\n class=\"tree-node__header\"\n [class.tree-node__header--expandable]=\"hasChildren()\"\n (click)=\"onHeaderClick()\"\n >\n <div class=\"tree-node__indent\" [style.width.px]=\"indentPx()\"></div>\n\n <span class=\"tree-node__chevron\" [class.tree-node__chevron--leaf]=\"!hasChildren()\">\n @if (hasChildren()) { @if (isExpanded()) {\n <ChevronDown20 />\n } @else {\n <ChevronRight20 />\n } }\n </span>\n\n <div class=\"tree-node__content\">\n @if (resolvedTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"resolvedTemplate()!\"\n [ngTemplateOutletContext]=\"templateContext()\"\n />\n } @else {\n <span class=\"tree-node__label\">{{ node().id }}</span>\n }\n </div>\n\n @if (selectionMode() === 'checkbox') {\n <moz-checkbox\n class=\"tree-node__selection\"\n [id]=\"'tree-checkbox-' + node().id\"\n [indeterminate]=\"isIndeterminate()\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @else if (selectionMode() === 'radio') {\n <moz-radio\n class=\"tree-node__selection\"\n [id]=\"'tree-radio-' + node().id\"\n [name]=\"radioName\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onRadioChange($event)\"\n />\n }\n </div>\n\n @if (isExpanded() && !flat()) {\n <ul class=\"tree-node__children\" role=\"group\">\n @if (isLoading()) {\n <li class=\"tree-node__loading\" role=\"presentation\">\n <moz-loader size=\"s\" [appearance]=\"'accent'\" />\n </li>\n } @else { @if (resolvedChildren().length === 0) {\n <li class=\"tree-node__empty\" role=\"presentation\">\n <span>{{ noResultText() }}</span>\n </li>\n } @for (child of resolvedChildren(); track child.id) {\n <moz-tree-node\n [node]=\"child\"\n [depth]=\"depth() + 1\"\n [selectionMode]=\"selectionMode()\"\n [indentSize]=\"indentSize()\"\n [nodeTemplate]=\"nodeTemplate()\"\n [nodeTemplates]=\"nodeTemplates()\"\n [loadChildren]=\"loadChildren()\"\n [ancestors]=\"ancestorsWithSelf()\"\n (expandChange)=\"expandChange.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n />\n } }\n </ul>\n }\n</li>\n", styles: [".tree-node{list-style:none;margin:0;padding:0;outline:none;display:flex;flex-direction:column;gap:4px}.tree-node--selected>.tree-node__header{background:var(--color-background-accent);border-radius:var(--border-radius-m, 8px)}.tree-node--disabled{pointer-events:none;color:var(--color-text-disabled)}.tree-node--disabled>.tree-node__header{opacity:.5}.tree-node--focused>.tree-node__header,.tree-node:focus-visible>.tree-node__header{outline:2px solid var(--color-background-accent-inverse);outline-offset:-2px;border-radius:var(--border-radius-m, 8px)}.tree-node__header{display:flex;align-items:center;width:100%;min-height:57px;padding:4px 0;border-radius:var(--border-radius-m, 8px);transition:background .15s ease;-webkit-user-select:none;user-select:none}.tree-node__header--expandable{cursor:pointer}.tree-node__header:hover{background:var(--color-background-secondary)}.tree-node--selected>.tree-node__header:hover{background:var(--color-background-accent)}.tree-node__indent{flex-shrink:0}.tree-node__chevron{display:flex;align-items:center;justify-content:center;flex-shrink:0;padding-left:16px;color:var(--color-text-primary)}.tree-node__chevron--leaf{width:28px;visibility:hidden}.tree-node__content{flex:1;min-width:0;display:flex;align-items:center;padding:4px 8px}.tree-node__selection{flex-shrink:0;margin-left:auto;margin-right:8px}.tree-node__label{font-size:var(--font-size-m, 16px);color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-node__children{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:4px}.tree-node__loading{display:flex;align-items:center;justify-content:center;padding:8px 16px}.tree-node__empty{display:flex;align-items:center;padding:8px 16px;font-size:var(--font-size-s, 14px);color:var(--color-text-secondary)}\n"], dependencies: [{ kind: "component", type: MozTreeNodeComponent, selector: "moz-tree-node", inputs: ["node", "depth", "selectionMode", "indentSize", "nodeTemplate", "nodeTemplates", "loadChildren", "ancestors", "flat", "noResultText"], outputs: ["expandChange", "selectionChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MozCheckboxComponent, selector: "moz-checkbox", inputs: ["id", "name", "label", "indeterminate", "isInvalid", "disabled", "indented"] }, { kind: "component", type: MozRadioComponent, selector: "moz-radio", inputs: ["id", "name", "label", "isInvalid", "disabled"] }, { kind: "component", type: MozLoaderComponent, selector: "moz-loader", inputs: ["appearance", "size", "text"] }, { kind: "component", type: ChevronDown20, selector: "ChevronDown20", inputs: ["hostClass"] }, { kind: "component", type: ChevronRight20, selector: "ChevronRight20", inputs: ["hostClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11910
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.6", type: MozTreeNodeComponent, isStandalone: true, selector: "moz-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, indentSize: { classPropertyName: "indentSize", publicName: "indentSize", isSignal: true, isRequired: false, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null }, nodeTemplates: { classPropertyName: "nodeTemplates", publicName: "nodeTemplates", isSignal: true, isRequired: false, transformFunction: null }, loadChildren: { classPropertyName: "loadChildren", publicName: "loadChildren", isSignal: true, isRequired: false, transformFunction: null }, ancestors: { classPropertyName: "ancestors", publicName: "ancestors", isSignal: true, isRequired: false, transformFunction: null }, flat: { classPropertyName: "flat", publicName: "flat", isSignal: true, isRequired: false, transformFunction: null }, noResultText: { classPropertyName: "noResultText", publicName: "noResultText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expandChange: "expandChange", selectionChange: "selectionChange" }, host: { styleAttribute: "display: block" }, ngImport: i0, template: "<li\n class=\"tree-node\"\n [class.tree-node--selected]=\"isSelected()\"\n [class.tree-node--disabled]=\"isDisabled()\"\n [class.tree-node--focused]=\"isFocused()\"\n role=\"treeitem\"\n [id]=\"'tree-node-' + node().id\"\n [attr.aria-level]=\"depth() + 1\"\n [attr.aria-expanded]=\"hasChildren() ? isExpanded() : null\"\n [attr.aria-selected]=\"selectionMode() !== 'none' ? isSelected() : null\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n [attr.data-tree-node-id]=\"node().id\"\n [tabindex]=\"isFocused() ? 0 : -1\"\n>\n <div\n class=\"tree-node__header\"\n [class.tree-node__header--expandable]=\"hasChildren()\"\n (click)=\"onHeaderClick()\"\n >\n <div class=\"tree-node__indent\" [style.width.px]=\"indentPx()\"></div>\n\n <div class=\"tree-node__row\">\n <span class=\"tree-node__chevron\" [class.tree-node__chevron--leaf]=\"!hasChildren()\">\n @if (hasChildren()) { @if (isExpanded()) {\n <ChevronDown20 />\n } @else {\n <ChevronRight20 />\n } }\n </span>\n\n <div class=\"tree-node__content\">\n @if (resolvedTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"resolvedTemplate()!\"\n [ngTemplateOutletContext]=\"templateContext()\"\n />\n } @else {\n <span class=\"tree-node__label\">{{ node().id }}</span>\n }\n </div>\n\n @if (selectionMode() === 'checkbox') {\n <moz-checkbox\n class=\"tree-node__selection\"\n [id]=\"'tree-checkbox-' + node().id\"\n [indeterminate]=\"isIndeterminate()\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @else if (selectionMode() === 'radio') {\n <moz-radio\n class=\"tree-node__selection\"\n [id]=\"'tree-radio-' + node().id\"\n [name]=\"radioName\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onRadioChange($event)\"\n />\n }\n </div>\n </div>\n\n @if (isExpanded() && !flat()) {\n <ul class=\"tree-node__children\" role=\"group\">\n @if (isLoading()) {\n <li class=\"tree-node__loading\" role=\"presentation\">\n <moz-loader size=\"s\" [appearance]=\"'accent'\" />\n </li>\n } @else { @if (resolvedChildren().length === 0) {\n <li class=\"tree-node__empty\" role=\"presentation\">\n <span>{{ noResultText() }}</span>\n </li>\n } @for (child of resolvedChildren(); track child.id) {\n <moz-tree-node\n [node]=\"child\"\n [depth]=\"depth() + 1\"\n [selectionMode]=\"selectionMode()\"\n [indentSize]=\"indentSize()\"\n [nodeTemplate]=\"nodeTemplate()\"\n [nodeTemplates]=\"nodeTemplates()\"\n [loadChildren]=\"loadChildren()\"\n [ancestors]=\"ancestorsWithSelf()\"\n (expandChange)=\"expandChange.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n />\n } }\n </ul>\n }\n</li>\n", styles: [".tree-node{list-style:none;margin:0;padding:0;outline:none;display:flex;flex-direction:column;gap:4px}.tree-node--selected>.tree-node__header>.tree-node__row{background:var(--color-background-accent);border-radius:var(--border-radius-m, 8px)}.tree-node--disabled{pointer-events:none;color:var(--color-text-disabled)}.tree-node--disabled>.tree-node__header{opacity:.5}.tree-node--focused>.tree-node__header>.tree-node__row,.tree-node:focus-visible>.tree-node__header>.tree-node__row{outline:2px solid var(--color-background-accent-inverse);outline-offset:-2px;border-radius:var(--border-radius-m, 8px)}.tree-node__header{display:flex;align-items:center;width:100%;min-height:57px;-webkit-user-select:none;user-select:none}.tree-node__header--expandable{cursor:pointer}.tree-node__row{display:flex;align-items:center;height:57px;flex:1;min-width:0;border-radius:var(--border-radius-m, 8px);transition:background .15s ease}.tree-node__header:hover>.tree-node__row{background:var(--color-background-secondary)}.tree-node--selected>.tree-node__header:hover>.tree-node__row{background:var(--color-background-accent)}.tree-node__indent{flex-shrink:0}.tree-node__chevron{display:flex;align-items:center;justify-content:center;flex-shrink:0;padding-left:16px;color:var(--color-text-primary)}.tree-node__chevron--leaf{width:0;padding-left:0;overflow:hidden}.tree-node__content{flex:1;min-width:0;display:flex;align-items:center;padding:4px 8px}.tree-node__selection{flex-shrink:0;margin-left:auto;margin-right:8px}.tree-node__label{font-size:var(--font-size-m, 16px);color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-node__children{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:4px}.tree-node__loading{display:flex;align-items:center;justify-content:center;padding:8px 16px}.tree-node__empty{display:flex;align-items:center;padding:8px 16px;font-size:var(--font-size-s, 14px);color:var(--color-text-secondary)}\n"], dependencies: [{ kind: "component", type: MozTreeNodeComponent, selector: "moz-tree-node", inputs: ["node", "depth", "selectionMode", "indentSize", "nodeTemplate", "nodeTemplates", "loadChildren", "ancestors", "flat", "noResultText"], outputs: ["expandChange", "selectionChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MozCheckboxComponent, selector: "moz-checkbox", inputs: ["id", "name", "label", "indeterminate", "isInvalid", "disabled", "indented"] }, { kind: "component", type: MozRadioComponent, selector: "moz-radio", inputs: ["id", "name", "label", "isInvalid", "disabled"] }, { kind: "component", type: MozLoaderComponent, selector: "moz-loader", inputs: ["appearance", "size", "text"] }, { kind: "component", type: ChevronDown20, selector: "ChevronDown20", inputs: ["hostClass"] }, { kind: "component", type: ChevronRight20, selector: "ChevronRight20", inputs: ["hostClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11815
11911
  }
11816
11912
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: MozTreeNodeComponent, decorators: [{
11817
11913
  type: Component,
@@ -11823,7 +11919,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
11823
11919
  MozLoaderComponent,
11824
11920
  ChevronDown20,
11825
11921
  ChevronRight20,
11826
- ], template: "<li\n class=\"tree-node\"\n [class.tree-node--selected]=\"isSelected()\"\n [class.tree-node--disabled]=\"isDisabled()\"\n [class.tree-node--focused]=\"isFocused()\"\n role=\"treeitem\"\n [id]=\"'tree-node-' + node().id\"\n [attr.aria-level]=\"depth() + 1\"\n [attr.aria-expanded]=\"hasChildren() ? isExpanded() : null\"\n [attr.aria-selected]=\"selectionMode() !== 'none' ? isSelected() : null\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n [attr.data-tree-node-id]=\"node().id\"\n [tabindex]=\"isFocused() ? 0 : -1\"\n>\n <div\n class=\"tree-node__header\"\n [class.tree-node__header--expandable]=\"hasChildren()\"\n (click)=\"onHeaderClick()\"\n >\n <div class=\"tree-node__indent\" [style.width.px]=\"indentPx()\"></div>\n\n <span class=\"tree-node__chevron\" [class.tree-node__chevron--leaf]=\"!hasChildren()\">\n @if (hasChildren()) { @if (isExpanded()) {\n <ChevronDown20 />\n } @else {\n <ChevronRight20 />\n } }\n </span>\n\n <div class=\"tree-node__content\">\n @if (resolvedTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"resolvedTemplate()!\"\n [ngTemplateOutletContext]=\"templateContext()\"\n />\n } @else {\n <span class=\"tree-node__label\">{{ node().id }}</span>\n }\n </div>\n\n @if (selectionMode() === 'checkbox') {\n <moz-checkbox\n class=\"tree-node__selection\"\n [id]=\"'tree-checkbox-' + node().id\"\n [indeterminate]=\"isIndeterminate()\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @else if (selectionMode() === 'radio') {\n <moz-radio\n class=\"tree-node__selection\"\n [id]=\"'tree-radio-' + node().id\"\n [name]=\"radioName\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onRadioChange($event)\"\n />\n }\n </div>\n\n @if (isExpanded() && !flat()) {\n <ul class=\"tree-node__children\" role=\"group\">\n @if (isLoading()) {\n <li class=\"tree-node__loading\" role=\"presentation\">\n <moz-loader size=\"s\" [appearance]=\"'accent'\" />\n </li>\n } @else { @if (resolvedChildren().length === 0) {\n <li class=\"tree-node__empty\" role=\"presentation\">\n <span>{{ noResultText() }}</span>\n </li>\n } @for (child of resolvedChildren(); track child.id) {\n <moz-tree-node\n [node]=\"child\"\n [depth]=\"depth() + 1\"\n [selectionMode]=\"selectionMode()\"\n [indentSize]=\"indentSize()\"\n [nodeTemplate]=\"nodeTemplate()\"\n [nodeTemplates]=\"nodeTemplates()\"\n [loadChildren]=\"loadChildren()\"\n [ancestors]=\"ancestorsWithSelf()\"\n (expandChange)=\"expandChange.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n />\n } }\n </ul>\n }\n</li>\n", styles: [".tree-node{list-style:none;margin:0;padding:0;outline:none;display:flex;flex-direction:column;gap:4px}.tree-node--selected>.tree-node__header{background:var(--color-background-accent);border-radius:var(--border-radius-m, 8px)}.tree-node--disabled{pointer-events:none;color:var(--color-text-disabled)}.tree-node--disabled>.tree-node__header{opacity:.5}.tree-node--focused>.tree-node__header,.tree-node:focus-visible>.tree-node__header{outline:2px solid var(--color-background-accent-inverse);outline-offset:-2px;border-radius:var(--border-radius-m, 8px)}.tree-node__header{display:flex;align-items:center;width:100%;min-height:57px;padding:4px 0;border-radius:var(--border-radius-m, 8px);transition:background .15s ease;-webkit-user-select:none;user-select:none}.tree-node__header--expandable{cursor:pointer}.tree-node__header:hover{background:var(--color-background-secondary)}.tree-node--selected>.tree-node__header:hover{background:var(--color-background-accent)}.tree-node__indent{flex-shrink:0}.tree-node__chevron{display:flex;align-items:center;justify-content:center;flex-shrink:0;padding-left:16px;color:var(--color-text-primary)}.tree-node__chevron--leaf{width:28px;visibility:hidden}.tree-node__content{flex:1;min-width:0;display:flex;align-items:center;padding:4px 8px}.tree-node__selection{flex-shrink:0;margin-left:auto;margin-right:8px}.tree-node__label{font-size:var(--font-size-m, 16px);color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-node__children{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:4px}.tree-node__loading{display:flex;align-items:center;justify-content:center;padding:8px 16px}.tree-node__empty{display:flex;align-items:center;padding:8px 16px;font-size:var(--font-size-s, 14px);color:var(--color-text-secondary)}\n"] }]
11922
+ ], template: "<li\n class=\"tree-node\"\n [class.tree-node--selected]=\"isSelected()\"\n [class.tree-node--disabled]=\"isDisabled()\"\n [class.tree-node--focused]=\"isFocused()\"\n role=\"treeitem\"\n [id]=\"'tree-node-' + node().id\"\n [attr.aria-level]=\"depth() + 1\"\n [attr.aria-expanded]=\"hasChildren() ? isExpanded() : null\"\n [attr.aria-selected]=\"selectionMode() !== 'none' ? isSelected() : null\"\n [attr.aria-disabled]=\"isDisabled() || null\"\n [attr.data-tree-node-id]=\"node().id\"\n [tabindex]=\"isFocused() ? 0 : -1\"\n>\n <div\n class=\"tree-node__header\"\n [class.tree-node__header--expandable]=\"hasChildren()\"\n (click)=\"onHeaderClick()\"\n >\n <div class=\"tree-node__indent\" [style.width.px]=\"indentPx()\"></div>\n\n <div class=\"tree-node__row\">\n <span class=\"tree-node__chevron\" [class.tree-node__chevron--leaf]=\"!hasChildren()\">\n @if (hasChildren()) { @if (isExpanded()) {\n <ChevronDown20 />\n } @else {\n <ChevronRight20 />\n } }\n </span>\n\n <div class=\"tree-node__content\">\n @if (resolvedTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"resolvedTemplate()!\"\n [ngTemplateOutletContext]=\"templateContext()\"\n />\n } @else {\n <span class=\"tree-node__label\">{{ node().id }}</span>\n }\n </div>\n\n @if (selectionMode() === 'checkbox') {\n <moz-checkbox\n class=\"tree-node__selection\"\n [id]=\"'tree-checkbox-' + node().id\"\n [indeterminate]=\"isIndeterminate()\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @else if (selectionMode() === 'radio') {\n <moz-radio\n class=\"tree-node__selection\"\n [id]=\"'tree-radio-' + node().id\"\n [name]=\"radioName\"\n [disabled]=\"isDisabled()\"\n [ngModel]=\"isSelected()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onRadioChange($event)\"\n />\n }\n </div>\n </div>\n\n @if (isExpanded() && !flat()) {\n <ul class=\"tree-node__children\" role=\"group\">\n @if (isLoading()) {\n <li class=\"tree-node__loading\" role=\"presentation\">\n <moz-loader size=\"s\" [appearance]=\"'accent'\" />\n </li>\n } @else { @if (resolvedChildren().length === 0) {\n <li class=\"tree-node__empty\" role=\"presentation\">\n <span>{{ noResultText() }}</span>\n </li>\n } @for (child of resolvedChildren(); track child.id) {\n <moz-tree-node\n [node]=\"child\"\n [depth]=\"depth() + 1\"\n [selectionMode]=\"selectionMode()\"\n [indentSize]=\"indentSize()\"\n [nodeTemplate]=\"nodeTemplate()\"\n [nodeTemplates]=\"nodeTemplates()\"\n [loadChildren]=\"loadChildren()\"\n [ancestors]=\"ancestorsWithSelf()\"\n (expandChange)=\"expandChange.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n />\n } }\n </ul>\n }\n</li>\n", styles: [".tree-node{list-style:none;margin:0;padding:0;outline:none;display:flex;flex-direction:column;gap:4px}.tree-node--selected>.tree-node__header>.tree-node__row{background:var(--color-background-accent);border-radius:var(--border-radius-m, 8px)}.tree-node--disabled{pointer-events:none;color:var(--color-text-disabled)}.tree-node--disabled>.tree-node__header{opacity:.5}.tree-node--focused>.tree-node__header>.tree-node__row,.tree-node:focus-visible>.tree-node__header>.tree-node__row{outline:2px solid var(--color-background-accent-inverse);outline-offset:-2px;border-radius:var(--border-radius-m, 8px)}.tree-node__header{display:flex;align-items:center;width:100%;min-height:57px;-webkit-user-select:none;user-select:none}.tree-node__header--expandable{cursor:pointer}.tree-node__row{display:flex;align-items:center;height:57px;flex:1;min-width:0;border-radius:var(--border-radius-m, 8px);transition:background .15s ease}.tree-node__header:hover>.tree-node__row{background:var(--color-background-secondary)}.tree-node--selected>.tree-node__header:hover>.tree-node__row{background:var(--color-background-accent)}.tree-node__indent{flex-shrink:0}.tree-node__chevron{display:flex;align-items:center;justify-content:center;flex-shrink:0;padding-left:16px;color:var(--color-text-primary)}.tree-node__chevron--leaf{width:0;padding-left:0;overflow:hidden}.tree-node__content{flex:1;min-width:0;display:flex;align-items:center;padding:4px 8px}.tree-node__selection{flex-shrink:0;margin-left:auto;margin-right:8px}.tree-node__label{font-size:var(--font-size-m, 16px);color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-node__children{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:4px}.tree-node__loading{display:flex;align-items:center;justify-content:center;padding:8px 16px}.tree-node__empty{display:flex;align-items:center;padding:8px 16px;font-size:var(--font-size-s, 14px);color:var(--color-text-secondary)}\n"] }]
11827
11923
  }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], indentSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "indentSize", required: false }] }], nodeTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeTemplate", required: false }] }], nodeTemplates: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeTemplates", required: false }] }], loadChildren: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadChildren", required: false }] }], ancestors: [{ type: i0.Input, args: [{ isSignal: true, alias: "ancestors", required: false }] }], flat: [{ type: i0.Input, args: [{ isSignal: true, alias: "flat", required: false }] }], noResultText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultText", required: false }] }], expandChange: [{ type: i0.Output, args: ["expandChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
11828
11924
 
11829
11925
  class MozTreeNodeTemplateDirective {
@@ -11889,9 +11985,47 @@ class MozTreeComponent {
11889
11985
  effect(() => {
11890
11986
  this.selectionService.setRootNodes(this.nodes());
11891
11987
  });
11988
+ effect(() => {
11989
+ this.stateService.loadChildrenFn.set(this.loadChildren());
11990
+ });
11991
+ // When children load for a node whose ID is in selectedIds,
11992
+ // replace the parent ID with its children's leaf IDs.
11993
+ let prevChildMap = new Map();
11994
+ effect(() => {
11995
+ const nodes = this.stateService.internalNodes();
11996
+ if (this.selectionService.selectionMode() !== 'checkbox')
11997
+ return;
11998
+ const newChildMap = new Map();
11999
+ const newlyLoadedParentIds = [];
12000
+ const visit = (list) => {
12001
+ for (const n of list) {
12002
+ const hadChildren = prevChildMap.get(n.id) ?? false;
12003
+ const hasChildrenNow = !!(n.children && n.children.length > 0);
12004
+ newChildMap.set(n.id, hasChildrenNow);
12005
+ if (!hadChildren && hasChildrenNow) {
12006
+ newlyLoadedParentIds.push(n.id);
12007
+ }
12008
+ if (n.children)
12009
+ visit(n.children);
12010
+ }
12011
+ };
12012
+ visit(nodes);
12013
+ prevChildMap = newChildMap;
12014
+ for (const parentId of newlyLoadedParentIds) {
12015
+ this.selectionService.propagateOnChildrenLoaded(parentId);
12016
+ }
12017
+ if (newlyLoadedParentIds.length > 0) {
12018
+ this.selectionChange.emit(this.selectionService.allSelectedIds());
12019
+ }
12020
+ });
11892
12021
  }
11893
12022
  onTreeKeydown(event) {
12023
+ const prevSelection = this.selectionService.allSelectedIds();
11894
12024
  this.keyboardService.handleKeydown(event);
12025
+ const newSelection = this.selectionService.allSelectedIds();
12026
+ if (prevSelection !== newSelection) {
12027
+ this.selectionChange.emit(newSelection);
12028
+ }
11895
12029
  }
11896
12030
  onTreeFocus() {
11897
12031
  this.keyboardService.initFocus();
@@ -11902,6 +12036,41 @@ class MozTreeComponent {
11902
12036
  onSelectionChange(ids) {
11903
12037
  this.selectionChange.emit(ids);
11904
12038
  }
12039
+ async expandPath(path) {
12040
+ const loadFn = this.stateService.loadChildrenFn();
12041
+ for (let i = 0; i < path.length - 1; i++) {
12042
+ const nodeId = path[i];
12043
+ const node = this.stateService.findNode(nodeId);
12044
+ if (!node)
12045
+ break;
12046
+ // Expand first so the children area is visible
12047
+ if (!this.stateService.expandedIds().has(nodeId)) {
12048
+ this.stateService.toggleExpanded(nodeId);
12049
+ this.expandedIdsChange.emit(new Set(this.stateService.expandedIds()));
12050
+ // Yield to let Angular render the expansion
12051
+ await new Promise((resolve) => setTimeout(resolve));
12052
+ }
12053
+ // Load children if needed
12054
+ if (loadFn && node.children === undefined) {
12055
+ this.stateService.addLoading(nodeId);
12056
+ this.expandedIdsChange.emit(new Set(this.stateService.expandedIds()));
12057
+ // Yield to let Angular render the loader
12058
+ await new Promise((resolve) => setTimeout(resolve));
12059
+ const children = await firstValueFrom(loadFn(node).pipe(take(1)));
12060
+ this.stateService.patchChildren(nodeId, children);
12061
+ this.stateService.removeLoading(nodeId);
12062
+ // Yield to let Angular render the new children
12063
+ await new Promise((resolve) => setTimeout(resolve));
12064
+ }
12065
+ }
12066
+ this.expandedIdsChange.emit(new Set(this.stateService.expandedIds()));
12067
+ }
12068
+ scrollToNode(nodeId) {
12069
+ setTimeout(() => {
12070
+ const el = document.querySelector(`[data-tree-node-id="${nodeId}"]`);
12071
+ el?.scrollIntoView({ block: 'center', behavior: 'smooth' });
12072
+ });
12073
+ }
11905
12074
  expandAll() {
11906
12075
  const flat = this.stateService.flatVisibleNodes();
11907
12076
  const allIds = new Set(flat.map((f) => f.node.id));