@mozaic-ds/angular 2.0.33 → 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.
@@ -11433,7 +11433,8 @@ class TreeStateService {
11433
11433
  this.toggleExpanded(node.id);
11434
11434
  if (!wasExpanded) {
11435
11435
  const loadFn = this.loadChildrenFn();
11436
- if (loadFn && node.children === undefined) {
11436
+ const resolved = this.findNode(node.id) ?? node;
11437
+ if (loadFn && resolved.children === undefined) {
11437
11438
  this.addLoading(node.id);
11438
11439
  return loadFn(node).pipe(take(1), tap((children) => {
11439
11440
  this.patchChildren(node.id, children);
@@ -11495,6 +11496,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
11495
11496
  }] });
11496
11497
 
11497
11498
  class TreeSelectionService {
11499
+ stateService = inject(TreeStateService);
11498
11500
  selectedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "selectedIds" }] : /* istanbul ignore next */ []));
11499
11501
  selectionMode = signal('none', ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
11500
11502
  rootNodes = signal([], ...(ngDevMode ? [{ debugName: "rootNodes" }] : /* istanbul ignore next */ []));
@@ -11515,40 +11517,62 @@ class TreeSelectionService {
11515
11517
  return true;
11516
11518
  return ancestors.some((a) => a.disabled);
11517
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
+ */
11518
11524
  isIndeterminate(node) {
11519
- if (node.children === undefined)
11525
+ const resolved = this._resolveNode(node);
11526
+ const leaves = this._collectLeafIds(resolved);
11527
+ if (leaves.length === 0)
11520
11528
  return false;
11521
- if (node.children.length === 0)
11522
- return false;
11523
- const enabledIds = this._collectEnabledDescendantIds(node);
11524
- if (enabledIds.length === 0)
11525
- return false;
11526
- const selectedCount = enabledIds.filter((id) => this.selectedIds().has(id)).length;
11527
- return selectedCount > 0 && selectedCount < enabledIds.length;
11529
+ const selectedCount = leaves.filter((id) => this.selectedIds().has(id)).length;
11530
+ return selectedCount > 0 && selectedCount < leaves.length;
11528
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
+ */
11529
11537
  isCheckedComputed(node) {
11530
11538
  if (this.selectionMode() === 'radio') {
11531
11539
  return this.selectedIds().has(node.id);
11532
11540
  }
11533
- 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) {
11534
11545
  return this.selectedIds().has(node.id);
11535
11546
  }
11536
- const enabledIds = this._collectEnabledDescendantIds(node);
11537
- if (enabledIds.length === 0)
11538
- return this.selectedIds().has(node.id);
11539
- return enabledIds.every((id) => this.selectedIds().has(id));
11547
+ return leaves.every((id) => this.selectedIds().has(id));
11540
11548
  }
11549
+ /**
11550
+ * Select a node: add all leaf descendant IDs (or the node's own ID if leaf).
11551
+ */
11541
11552
  selectNode(node) {
11542
11553
  if (this.selectionMode() === 'radio') {
11543
11554
  this.selectedIds.set(new Set([node.id]));
11544
11555
  return;
11545
11556
  }
11557
+ const resolved = this._resolveNode(node);
11546
11558
  this.selectedIds.update((current) => {
11547
11559
  const next = new Set(current);
11548
- 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
+ }
11549
11569
  return next;
11550
11570
  });
11551
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
+ */
11552
11576
  deselectNode(node) {
11553
11577
  if (this.selectionMode() === 'radio') {
11554
11578
  this.selectedIds.update((current) => {
@@ -11558,9 +11582,13 @@ class TreeSelectionService {
11558
11582
  });
11559
11583
  return;
11560
11584
  }
11585
+ const resolved = this._resolveNode(node);
11561
11586
  this.selectedIds.update((current) => {
11562
11587
  const next = new Set(current);
11563
- 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));
11564
11592
  return next;
11565
11593
  });
11566
11594
  }
@@ -11572,30 +11600,83 @@ class TreeSelectionService {
11572
11600
  this.selectNode(node);
11573
11601
  }
11574
11602
  }
11575
- _collectEnabledIds(node) {
11576
- 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) {
11577
11643
  return [];
11578
- const ids = [node.id];
11579
- if (node.children) {
11580
- for (const child of node.children) {
11581
- 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);
11582
11657
  }
11583
11658
  }
11584
11659
  return ids;
11585
11660
  }
11586
- _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);
11587
11666
  const ids = [];
11588
- if (!node.children)
11667
+ if (!resolved.children)
11589
11668
  return ids;
11590
- for (const child of node.children) {
11591
- if (!child.disabled) {
11592
- ids.push(child.id);
11593
- ids.push(...this._collectEnabledDescendantIds(child));
11594
- }
11669
+ for (const child of resolved.children) {
11670
+ const resolvedChild = this._resolveNode(child);
11671
+ ids.push(resolvedChild.id);
11672
+ ids.push(...this._collectAllDescendantIds(resolvedChild));
11595
11673
  }
11596
11674
  return ids;
11597
11675
  }
11598
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
+ }
11599
11680
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TreeSelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
11600
11681
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: TreeSelectionService });
11601
11682
  }
@@ -11826,7 +11907,7 @@ class MozTreeNodeComponent {
11826
11907
  return null;
11827
11908
  }
11828
11909
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: MozTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11829
- 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 });
11830
11911
  }
11831
11912
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: MozTreeNodeComponent, decorators: [{
11832
11913
  type: Component,
@@ -11838,7 +11919,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
11838
11919
  MozLoaderComponent,
11839
11920
  ChevronDown20,
11840
11921
  ChevronRight20,
11841
- ], 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"] }]
11842
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"] }] } });
11843
11924
 
11844
11925
  class MozTreeNodeTemplateDirective {
@@ -11907,9 +11988,44 @@ class MozTreeComponent {
11907
11988
  effect(() => {
11908
11989
  this.stateService.loadChildrenFn.set(this.loadChildren());
11909
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
+ });
11910
12021
  }
11911
12022
  onTreeKeydown(event) {
12023
+ const prevSelection = this.selectionService.allSelectedIds();
11912
12024
  this.keyboardService.handleKeydown(event);
12025
+ const newSelection = this.selectionService.allSelectedIds();
12026
+ if (prevSelection !== newSelection) {
12027
+ this.selectionChange.emit(newSelection);
12028
+ }
11913
12029
  }
11914
12030
  onTreeFocus() {
11915
12031
  this.keyboardService.initFocus();