@edugouvfr/ngx-dsfr-ext
Version:
NgxDsfrExt est une extension au package @edugouvfr/ngx-dsfr (portage Angular des éléments DSFR)
416 lines • 114 kB
JavaScript
import { CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input, ViewChild, ViewEncapsulation, forwardRef, } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { DsfrButtonModule, DsfrI18nPipe, DsfrTagModule } from '@edugouvfr/ngx-dsfr';
import { AbstractSelectComponent, DropdownContainerComponent, EduCheckboxComponent } from '../shared/components';
import { SelectInputLabelComponent } from '../shared/components/select-input-label/select-input-label.component';
import { EduMessageSeverityDirective } from '../shared/directives/message-severity.directive';
import { DsfrSelectionModeConst } from '../shared/models/selection-mode.type';
import { TreeFocusManager } from '../shared/utils/tree/tree-focus-manager';
import { TreeSelectManager } from '../shared/utils/tree/tree-select-manager';
import * as i0 from "@angular/core";
import * as i1 from "@angular/common";
import * as i2 from "@angular/forms";
import * as i3 from "@angular/cdk/scrolling";
import * as i4 from "@edugouvfr/ngx-dsfr";
export class DsfrTreeselectComponent extends AbstractSelectComponent {
constructor() {
super(...arguments);
/** Stratégie de sélection, uniquement en mode single. Par défaut seules les feuilles sont sélectionnables.*/
this.selectionStrategy = 'leaf';
/** @internal*/
this.isAllSelected = false;
this.visibleNodes = []; // clone de liste utilisé en cas de virtualScroll
this.countFilteredOptions = -1;
this.countLeaves = -1;
this.countFilteredLeaves = -1;
this.treeFocusManager = new TreeFocusManager(this);
this.treeSelectManager = new TreeSelectManager(this);
/** Surcharge de compareWith par défaut pour utiliser les id */
this._compareWith = (o1, o2) => o1?.id === o2?.id;
/** Fonction de recherche par défaut */
this._searchFn = function (o, searchText) {
return o.label?.toLowerCase().includes(searchText.toLowerCase());
};
}
/** @internal Equivalent a options */
get nodes() {
return this.optionsFilter;
}
/** @internal Synonyme de value, mais plus explicite. */
get options() {
return this._options;
}
/** @internal */
get selectionMode() {
return this._selectionMode;
}
/** Personnalisation de la fonction de recherche pour filtrer les options. Fonction à 2 arguments : node (DsfrNode) et texte cherché (string) */
set searchFn(fn) {
if (typeof fn !== 'function') {
throw Error('`searchFn` must be a function.');
}
this._searchFn = fn;
// reset filter
this.searchText = '';
this.optionsFilter = this.optionsFilter ?? [];
}
/** Liste des options (DsfrNode[]) disponibles. */
set options(val) {
this._options = val ?? [];
this.optionsFilter = this.options ? [...this.options] : [];
if (!this.treeSelectManager.selectionMode)
this.treeSelectManager.selectionMode = this.selectionMode;
if (this.value !== undefined) {
this.writeValue(this.value);
}
else {
this.optionsFilter = this.treeSelectManager.init(this.optionsFilter);
if (this.virtualScroll)
this.visibleNodes = this.optionsFilter;
}
}
/** Mode de sélection. Checkbox par défaut. */
set selectionMode(mode) {
super.setSelectionMode(mode);
this.treeSelectManager.selectionMode = this.selectionMode;
}
ngOnInit() {
super.ngOnInit();
if (!this._compareWith.name && this.options && this.options.length > 0) {
this.treeSelectManager.noGenericId = true;
}
this.treeSelectManager.selectionStrategy =
this.isSingleMode && this.selectionStrategy ? this.selectionStrategy : 'leaf';
}
/** Positionne un tabindex de 0 sur le premier élément de la liste si aucun n'est sélectionné */
ngAfterViewInit() {
const firstElement = this.listbox.nativeElement?.children[0];
if (this.treeSelectManager.selectedNodes.length === 0 && firstElement) {
firstElement.setAttribute('tabindex', '0');
}
this.countLeaves = this.treeSelectManager.countLeaves;
}
/**
* Mise a jour de la valeur de selectedOptions et value programmatiquement (ex. à l'init reactive form)
* @param value
* @internal
*/
writeValue(value) {
this.treeSelectManager.selectedNodes = [];
if (value) {
if (!Array.isArray(value)) {
value = [value];
}
// Sélection dans l'arbre des valeurs sélectionnées programmatiquement
// ici value est un tableau non-vide
this.setSelectedNodes(this.nodes, value);
}
else {
//si des options etaient sélectionnées, on vide la selection de l'arbre
if (this.selectedOptions.length > 0)
this.treeSelectManager.selectAllNodes(this.optionsFilter, false);
this.indexFirstSelected = null;
}
this.optionsFilter = this.treeSelectManager.init(this.optionsFilter);
if (this.virtualScroll)
this.visibleNodes = this.optionsFilter;
this.selectedOptions = [...this.treeSelectManager.selectedNodes];
this.searchText = '';
super.writeValue(value);
this.cd.markForCheck();
}
/** @internal */
notifySelectEvent(node) {
// nothing
}
/** Mise a jour de l'état des checkbox selon leur id, nécessaire avec le tri en virtualScroll (perte du binding)
* @internal */
updateIndeterminate(node) {
if (this.virtualScroll) {
const checkboxId = `cb-${node.id}`;
const inputElt = this.listbox?.nativeElement?.querySelector('#' + checkboxId);
if (inputElt) {
if (node.selected !== undefined)
inputElt.checked = node.selected;
inputElt.indeterminate = node.selected === undefined;
}
}
}
/**
* Selection/Deselection du noeud
* Mise a jour de la liste des noeuds sélectionnés
* @internal */
doNodeDefaultAction(node, event) {
const nodeElt = event.currentTarget;
this.treeSelectManager.doSelectNode(node, nodeElt);
// Mise a jour de la value
this.selectedOptions = this.treeSelectManager.updateSelectedNodes();
this.value = this.selectedOptions;
this.selectionChange.emit(this.value);
}
/** @internal */
findFocusElement() {
const nativeElt = this.listbox.nativeElement;
return nativeElt.querySelector('.tree-node-content:focus');
}
/** @internal */
findFirstSelectedNode() {
const nodes = this.listbox?.nativeElement?.querySelectorAll('.tree-node-content .selected');
if (nodes[0]) {
return nodes[0];
}
return this.findVisibleNodesElt()[0];
}
/**
* Clic sur une option en mode tag supprimable.
* Déselection option et mise a jour des parents.
* @internal */
clearOption(event) {
this.onOptionClick(event.nativeEvent, event.option);
this.updateValueSelected();
}
/**
* Trouver tous les noeuds <li> visibles
* @internal */
findVisibleNodesElt() {
const nativeElt = this.listbox.nativeElement;
const nodeList = nativeElt.querySelectorAll('.tree-node-content');
const nodesArr = [];
nodeList.forEach((n) => nodesArr.push(n));
return nodesArr;
}
/** @internal */
selectAll() {
this.isAllSelected = !this.isAllSelected;
this.treeSelectManager.selectAllNodes(this.optionsFilter, this.isAllSelected);
this.updateValueSelected();
this.selectionChange.emit(this.value);
}
/** @internal */
clearAllOptions() {
this.isAllSelected = false;
this.treeSelectManager.selectAllNodes(this.optionsFilter, this.isAllSelected);
this.updateValueSelected();
this.selectionChange.emit(this.value);
}
/** @internal */
expandAll() {
this.treeFocusManager.expandAll(this.optionsFilter);
}
/** @internal */
collapseAll() {
this.treeFocusManager.collapseAll(this.optionsFilter);
}
/** @internal */
handleListFocus() {
this.firstNodeElt = this.listbox.nativeElement.querySelector('.tree-node-content');
this.firstNodeElt.focus();
}
/**
* Au clic sur le label ou la checkbox
* Sélection ou déselection des noeuds
* @internal */
onOptionClick(event, node) {
if (node.disabled)
return;
const nodeElt = event.currentTarget;
this.treeSelectManager.doSelectNode(node, nodeElt);
if (this.selectionMode === DsfrSelectionModeConst.SINGLE && (!node.children || this.selectionStrategy === 'all')) {
// Cacher la liste a la selection si selection simple
this.hide();
}
this.selectedOptions = this.treeSelectManager.updateSelectedNodes();
this.value = this.selectedOptions;
this.isAllSelected = this.areAllOptionsSelected();
this.selectionChange.emit(this.value);
}
/**
* Filtrer les options selon la fonction de recherche
* @internal */
onSearch() {
this.countFilteredOptions = 0;
this.countFilteredLeaves = 0;
if (!this.virtualScroll) {
// appliquer l'état hidden pour filtrer
this.optionsFilter.forEach((node) => this.updateNodeVisibility(node));
}
else {
// Dans le cas du virtual scroll on est obligé de filtrer physiquement les noeuds
this.visibleNodes = this.optionsFilter
.map((node) => this.filterNodes(node))
.filter((node) => node !== null);
}
this.isAllSelected = this.areAllOptionsSelected();
}
/**
* Au clic sur les boutons flèches plier/déplier les noeuds
* @internal */
onNodeButton(node) {
this.treeFocusManager.doToggleExpand(node);
}
/**
* Interaction clavier avec un des noeuds
* @internal */
onNodeKeydown(node, event) {
const isParentSelectable = this.isSingleMode && this.selectionStrategy === 'all';
//retour tab
if (event.shiftKey && event.key === 'Tab') {
return;
}
if (event.key === 'Escape' || event.key === 'Tab') {
this.hide();
return;
}
if (event.key === 'Enter') {
this.treeFocusManager.doKeyEvent(node, event, isParentSelectable);
this.hide();
return;
}
this.treeFocusManager.doKeyEvent(node, event, isParentSelectable);
if (!this.virtualScroll) {
this.treeFocusManager.currentElt?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
else {
this.treeFocusManager.scrollForVirtualScroll(this.viewport);
}
}
/** @internal */
trackById(index, item) {
return item?.id;
}
/** @internal */
isCheckboxMode() {
return this.treeSelectManager.isCheckboxMode();
}
/** @internal */
isSelectable() {
return this.treeSelectManager.isSelectable();
}
/** @internal */
isSelected(n) {
if (this.isSingleMode) {
return this._compareWith(this.value, n);
}
if (this.value && !(this.value instanceof Array))
throw Error('`value` must be an array.');
return !!this.value?.find((o) => this._compareWith(o, n));
}
/** Mise a jour selectedOptions puis value */
updateValueSelected() {
this.selectedOptions = [...this.treeSelectManager.selectedNodes];
this.value = this.selectedOptions;
this.cd.markForCheck();
}
/**
* Filtre visuellement les nœuds en appliquant `hidden` en fonction du résultat de la recherche.
* - nœud parent visible : correspond à la recherche ou au moins un de ses enfants est visible.
* - nœud enfant visible : correspond à la recherche ou un de ses ancêtres correspond à la recherche.
* @param node Le nœud à filtrer de manière récursive.
* @returns `true` si le nœud est visible, sinon `false`.
*/
updateNodeVisibility(node, isAncestorVisible) {
const isMatch = this._searchFn(node, this.searchText);
// si un des ancètres est visible on conserve la visibilité
const isNodeVisible = isAncestorVisible || isMatch;
if (node.children) {
// Appliquer hidden récursivement aux enfants
const anyChildVisible = node.children.reduce((acc, child) => {
return this.updateNodeVisibility(child, isNodeVisible) || acc;
}, false);
node.hidden = !(isMatch || anyChildVisible);
if (!node.hidden && this.searchText.length > 1)
node.expanded = true;
}
else {
node.hidden = !isNodeVisible;
}
if (!node.hidden) {
this.countFilteredOptions++;
}
if (!node.hidden && !node.children) {
this.countFilteredLeaves++;
}
return !node.hidden;
}
/**
* Filtrer physiquement les noeuds dans le cadre du virtualScroll
* /!\ Le virtualScroll ne permet pas d'utiliser la propriété hidden
* Identique à updateNodeVisibility mais supprime au lieu d'utiliser la propriété hidden
* @param node Le nœud à filtrer de manière récursive.
* @returns le noeud si match, null sinon
*/
filterNodes(node) {
const isMatch = this._searchFn(node, this.searchText);
if (isMatch) {
this.countFilteredOptions++;
}
if (isMatch && !node.children) {
this.countFilteredLeaves++;
}
if (node.children) {
// Filtrer récursivement les enfants
const filteredChildren = node.children
.map((child) => this.filterNodes(child))
.filter((child) => child !== null);
// match ou un des enfants match
if (isMatch || filteredChildren.length > 0) {
return { ...node, children: isMatch ? node.children : filteredChildren };
}
return null; // suppression du noeud
}
return isMatch ? node : null;
}
/**
* Parcours d'arbre pour mettre a jour la valeur selected des nodes à l'init,
* selon les valeurs sélectionnées programmatiquement
* @param nodes arbre complet
* @param value tableau non-vide des noeuds sélectionnés
*/
setSelectedNodes(nodes, value) {
nodes.forEach((n) => {
// si le noeud courant n'est pas sur une feuille et si on ne peut pas sélectionner les parents, on passe aux enfants
if (n.children && this.selectionStrategy !== 'all') {
this.setSelectedNodes(n.children, value);
return;
}
// recherche du noeud correspondant dans la liste sélectionnée
if (this.isSingleMode) {
n.selected = this._compareWith(n, value[0]);
if (n.selected)
return;
if (n.children)
this.setSelectedNodes(n.children, value);
}
else {
n.selected = value.find((o) => this._compareWith(n, o)) ? true : false;
}
});
}
/**
* Calcule si toutes les options visibles et non-disabled sont sélectionnées
*/
areAllOptionsSelected() {
return this.countFilteredLeaves > 0
? this.selectedOptions.length >= this.countFilteredLeaves
: this.selectedOptions.length === this.countLeaves;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DsfrTreeselectComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: DsfrTreeselectComponent, isStandalone: true, selector: "dsfr-ext-treeselect, dsfrx-treeselect", inputs: { selectionStrategy: "selectionStrategy", searchFn: "searchFn", options: "options", selectionMode: "selectionMode" }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DsfrTreeselectComponent), multi: true }], viewQueries: [{ propertyName: "viewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true }, { propertyName: "listbox", first: true, predicate: ["listbox"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<div\n class=\"fr-select-group dsfrx-select\"\n [class]=\"message ? 'fr-select-group--' + messageSeverity : ''\"\n [ngClass]=\"{\n 'fr-select-group--valid': message && messageSeverity === severityConst.SUCCESS,\n 'fr-select-group--disabled': disabled,\n }\">\n <div class=\"fr-label\" [ngClass]=\"{ 'fr-sr-only': labelSrOnly }\" id=\"{{ selectId }}-label\">\n @if (label) {\n {{ label }}\n } @else {\n <ng-content select=\"[label]\"></ng-content>\n }\n @if (hint) {\n <span class=\"fr-hint-text\">{{ hint }}</span>\n }\n </div>\n\n <edu-dropdown-container\n [displayDropdown]=\"displayDropdown\"\n [appendTo]=\"appendTo\"\n [classToWrap]=\"'dsfrx-multiselect'\"\n [autofocus]=\"true\"\n [hasToolBar]=\"showSearch || showSelectAll\"\n [zIndex]=\"zIndex\"\n [scrollHeight]=\"scrollHeight\"\n [autoWidthDropdown]=\"autoWidthDropdown\"\n [indexFirstSelected]=\"indexFirstSelected\"\n (hideList)=\"hide()\"\n [id]=\"selectId\"\n (focusOnList)=\"handleListFocus()\"\n [virtualScroll]=\"virtualScroll\"\n [hasOptions]=\"optionsFilter.length > 0\">\n <!-- Control input -->\n <div\n ngProjectAs=\"control\"\n ngDefaultControl\n [(ngModel)]=\"value\"\n id=\"{{ selectId }}\"\n [attr.tabindex]=\"disabled ? -1 : 0\"\n role=\"combobox\"\n (click)=\"toggleDropdown($event)\"\n (keydown)=\"onButtonKeyDown($event)\"\n aria-haspopup=\"listbox\"\n [ngClass]=\"{ 'fr-select--disabled': disabled }\"\n [disabled]=\"disabled\"\n [attr.aria-controls]=\"selectId + '-listbox'\"\n [attr.aria-expanded]=\"displayDropdown\"\n [attr.aria-invalid]=\"ariaInvalid || null\"\n [attr.aria-required]=\"ariaRequired || null\"\n [attr.aria-labelledby]=\"selectId + '-label'\"\n [attr.aria-describedby]=\"message ? selectId + '-messages' : null\"\n class=\"fr-select\">\n <edu-select-input-label\n [clearableOptions]=\"clearableOptions\"\n [maxDisplayOptions]=\"maxDisplayOptions\"\n [selectedOptionTemplate]=\"selectedOptionTemplate\"\n [placeHolder]=\"placeHolder\"\n [selectedOptions]=\"selectedOptions\"\n [maxDisplayOptionsTemplate]=\"maxDisplayOptionsTemplate\"\n (clearOptionSelect)=\"clearOption($event)\"\n (clearAllOptionsSelect)=\"clearAllOptions()\">\n </edu-select-input-label>\n </div>\n\n <!-- If toolbar is shown --------------------------------------------------------------------------->\n <ng-container toolbar>\n @if (showSelectAll && !maxSelectedOptions && selectionMode !== 'single') {\n <dsfr-button\n size=\"SM\"\n variant=\"tertiary\"\n customClass=\"fr-btn--select-all\"\n (click)=\"selectAll()\"\n [label]=\"isAllSelected ? ('multiselect.unselectAll' | dsfrI18n) : ('multiselect.selectAll' | dsfrI18n)\">\n </dsfr-button>\n }\n @if (showSearch) {\n <label for=\"{{ selectId }}-searchbox\" class=\"fr-sr-only\">{{ 'multiselect.searchList' | dsfrI18n }}</label>\n <div\n class=\"fr-input-wrap\"\n [class.fr-icon-search-line]=\"!showClearSearch\"\n [class.fr-input-wrap--action]=\"showClearSearch\">\n <input\n #searchBox\n class=\"fr-input\"\n type=\"search\"\n aria-invalid=\"false\"\n id=\"{{ selectId }}-searchbox\"\n [placeholder]=\"searchMessage ?? 'multiselect.search' | dsfrI18n\"\n (input)=\"onSearch()\"\n aria-controls=\"listboxid\"\n aria-expanded=\"true\"\n aria-autocomplete=\"list\"\n aria-haspopup=\"listbox\"\n autocomplete=\"off\"\n [(ngModel)]=\"searchText\" />\n @if (showClearSearch) {\n <button\n type=\"button\"\n class=\"fr-btn fr-icon-close-line fr-btn--tertiary\"\n id=\"{{ selectId }}-reset\"\n (click)=\"resetSearchInput()\"\n [title]=\"'multiselect.resetSearch' | dsfrI18n\">\n {{ 'multiselect.resetSearch' | dsfrI18n }}\n </button>\n }\n </div>\n }\n <div role=\"status\" aria-live=\"polite\" class=\"fr-sr-only\">\n @if (optionsFilter && countFilteredOptions > 0) {\n <p>{{ countFilteredOptions }} {{ 'multiselect.availableResults' | dsfrI18n }}</p>\n }\n </div>\n </ng-container>\n\n @if (virtualScroll) {\n <!-- Template de listbox (options) en virtual scroll ----------------------------------------------------------->\n <div\n #listbox\n id=\"{{ selectId }}-listbox\"\n [ngStyle]=\"{ 'z-index': zIndex }\"\n [style.max-height]=\"scrollHeight\"\n class=\"dsfrx-multiselect__list dsfrx-multiselect__list--has-viewport\">\n <cdk-virtual-scroll-viewport\n #viewport\n minBufferPx=\"200\"\n maxBufferPx=\"600\"\n [itemSize]=\"virtualScrollItemHeight\"\n class=\"viewport\">\n <ul role=\"tree\" [attr.aria-multiselectable]=\"!this.isSingleMode\">\n <!-- add tree node-->\n <li\n role=\"treeitem\"\n [id]=\"selectId + '-item-' + node.id\"\n [class.leaf]=\"!node.children\"\n [attr.aria-expanded]=\"node.expanded ?? (node.children ? false : null)\"\n [class.selected]=\"node.selected\"\n [attr.aria-selected]=\"this.isSingleMode ? (node.selected ?? false) : null\"\n [attr.aria-checked]=\"this.isSingleMode ? null : (node.selected ?? false)\"\n [attr.aria-disabled]=\"node.disabled\"\n [attr.aria-level]=\"node.ariaLevel\"\n [attr.aria-setsize]=\"node.ariaSize\"\n [attr.aria-posinset]=\"i + 1\"\n (keydown)=\"onNodeKeydown(node, $event)\"\n tabindex=\"-1\"\n class=\"tree-node-content tree-node-container\"\n *cdkVirtualFor=\"let node of visibleNodes; let i = index; trackBy: trackById\">\n <ng-container [ngTemplateOutlet]=\"nodeTemplate\" [ngTemplateOutletContext]=\"{ node: node }\"></ng-container>\n </li>\n </ul>\n </cdk-virtual-scroll-viewport>\n </div>\n } @else {\n <!-- Template de listbox (options) SANS virtual scroll ----------------------------------------------------------->\n <ul\n #listbox\n id=\"{{ selectId }}-listbox\"\n [ngStyle]=\"{ 'z-index': zIndex }\"\n role=\"tree\"\n tabindex=\"-1\"\n [style.max-height]=\"scrollHeight\"\n [attr.aria-multiselectable]=\"!this.isSingleMode\"\n class=\"dsfrx-multiselect__list\"\n [ngClass]=\"{ 'select-parent': selectionStrategy === 'all', 'expandable': selectionStrategy === 'leaf' }\">\n @for (node of optionsFilter; track trackById(i, node); let i = $index) {\n @if (!node.hidden) {\n <li\n role=\"treeitem\"\n (keydown)=\"onNodeKeydown(node, $event)\"\n [id]=\"selectId + '-item-' + node.id\"\n class=\"tree-node-content tree-node-container\"\n [class.leaf]=\"!node.children\"\n tabindex=\"-1\"\n [attr.aria-expanded]=\"node.expanded ?? (node.children ? false : null)\"\n [class.selected]=\"node.selected\"\n [attr.aria-level]=\"node.ariaLevel\"\n [attr.aria-setsize]=\"node.ariaSize\"\n [attr.aria-posinset]=\"i + 1\"\n [attr.aria-selected]=\"this.isSingleMode ? (node.selected ?? false) : null\"\n [attr.aria-checked]=\"this.isSingleMode ? null : (node.selected ?? false)\"\n [attr.aria-disabled]=\"node.disabled\">\n <ng-container [ngTemplateOutlet]=\"nodeTemplate\" [ngTemplateOutletContext]=\"{ node: node }\"></ng-container>\n </li>\n }\n }\n </ul>\n }\n\n <!-- Empty list after search filter -->\n <div\n role=\"status\"\n aria-live=\"polite\"\n ngProjectAs=\"empty\"\n class=\"dsfrx-multiselect__list dsfrx-multiselect__list--empty\">\n @if (loading) {\n <p>\n <span class=\"label\"> {{ 'multiselect.loading' | dsfrI18n }}...</span>\n </p>\n } @else if (displayDropdown && (optionsFilter.length === 0 || countFilteredOptions === 0)) {\n <p>\n {{ noResultsMessage ?? ('multiselect.noResult' | dsfrI18n) }}\n </p>\n }\n </div>\n </edu-dropdown-container>\n <div id=\"{{ selectId }}-messages\" class=\"fr-messages-group\" aria-live=\"polite\">\n @if (message) {\n <p class=\"fr-message\" [eduMessageSeverity]=\"messageSeverity\">\n {{ message }}\n </p>\n }\n </div>\n</div>\n\n<!-- Template one node -->\n<ng-template #nodeTemplate let-node=\"node\">\n <div\n class=\"tree-node-content-inside\"\n [class.selectable]=\"isSelectable()\"\n [class.selected]=\"node.selected\"\n [class.disabled]=\"node.disabled\">\n <!-- Bouton collapse / expand -->\n @if (node.children) {\n <span\n (click)=\"onNodeButton(node)\"\n class=\"tree-button\"\n [class]=\"!node.expanded ? 'fr-icon-arrow-right-s-line' : 'fr-icon-arrow-down-s-line'\"></span>\n }\n\n <div\n class=\"tree-node-label\"\n [ngClass]=\"{\n hoverable: (node.children && selectionStrategy === 'all') || showCheckboxes || !node.children,\n }\">\n <!-- Label seul -->\n @if (!showCheckboxes) {\n <label\n (click)=\"node.children && selectionStrategy === 'leaf' ? onNodeButton(node) : onOptionClick($event, node)\"\n class=\"fr-label\"\n [ngClass]=\"{ 'fr-label--disabled': node.disabled }\">\n @if (optionTemplate) {\n <!-- Slot -->\n <span class=\"text-node\"\n ><ng-container *ngTemplateOutlet=\"optionTemplate; context: { $implicit: node }\"></ng-container\n ></span>\n } @else {\n @if (node.icon) {\n <span [class]=\"node.icon + ' fr-icon--sm'\"> </span>\n }\n <span class=\"text-node\">{{ node.label }}</span>\n }\n </label>\n } @else {\n <!-- Checkbox + label -->\n <dsfr-ext-checkbox\n [id]=\"'cb-' + node.id\"\n [disabled]=\"node.disabled\"\n [tabIndex]=\"-1\"\n [icon]=\"node.icon\"\n [label]=\"optionTemplate ? undefined : node.label\"\n (checkboxClick)=\"onOptionClick($event, node)\"\n [selected]=\"node.selected\">\n @if (optionTemplate) {\n <ng-container label\n ><ng-container *ngTemplateOutlet=\"optionTemplate; context: { $implicit: node }\"></ng-container\n ></ng-container>\n }\n </dsfr-ext-checkbox>\n }\n </div>\n </div>\n\n <!-- Children -->\n @if (node.children && node.expanded) {\n <ng-container\n [ngTemplateOutlet]=\"nodesTemplate\"\n [ngTemplateOutletContext]=\"{ nodes: node.children }\"></ng-container>\n }\n</ng-template>\n\n<!-- Liste of child nodes -->\n<ng-template #nodesTemplate let-nodes=\"nodes\">\n <ul role=\"group\" class=\"tree-node\">\n @for (nodeChild of nodes; track trackById(i, nodeChild); let i = $index) {\n @if (!nodeChild.hidden) {\n <li\n [id]=\"selectId + '-item-' + nodeChild.id\"\n [attr.aria-expanded]=\"nodeChild.expanded ?? false\"\n [class.leaf]=\"!nodeChild.children\"\n tabindex=\"-1\"\n [class.leaf]=\"!nodeChild.children\"\n class=\"tree-node-content\"\n (keydown)=\"onNodeKeydown(nodeChild, $event)\"\n [attr.aria-expanded]=\"nodeChild.expanded ?? (nodeChild.children ? false : null)\"\n [attr.aria-selected]=\"this.isSingleMode ? nodeChild.selected : null\"\n [attr.aria-checked]=\"this.isSingleMode ? null : nodeChild.selected\"\n [attr.aria-level]=\"nodeChild.ariaLevel\"\n [attr.aria-setsize]=\"nodeChild.ariaSize\"\n [attr.aria-posinset]=\"i + 1\"\n role=\"treeitem\">\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate\"\n [ngTemplateOutletContext]=\"{ node: nodeChild, index: i }\"></ng-container>\n </li>\n }\n }\n </ul>\n</ng-template>\n", styles: [".dsfrx-multiselect{display:block;position:relative}.dsfrx-multiselect .fr-select{min-height:2.5rem;text-align:left;cursor:pointer;font-weight:400}.dsfrx-multiselect .fr-select:hover{background-color:var(--background-contrast-grey)}.dsfrx-multiselect .fr-select--disabled{pointer-events:none;color:var(--text-disabled-grey);box-shadow:inset 0 -2px 0 0 var(--border-disabled-grey);--data-uri-svg: url(\"data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23929292' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>\");background-image:var(--data-uri-svg)}.dsfrx-multiselect__toolbar{display:block;padding:.5rem .75rem;border-bottom:1px solid var(--background-contrast-grey)}.dsfrx-multiselect__toolbar input[type=search]::-webkit-search-cancel-button{display:none}.dsfrx-multiselect__toolbar .fr-input-wrap{flex-grow:1}.dsfrx-multiselect__toolbar .fr-btn--select-all{text-align:center;display:block;margin:0;min-width:100%;margin-bottom:.75rem}.dsfrx-multiselect__list{overflow:auto;list-style:none;background-color:var(--background-overlap-grey);margin:0;line-height:1.5rem;color:var(--text-title-grey);text-align:left;padding:0;max-height:224px}.dsfrx-multiselect__list--has-viewport{overflow:hidden}.dsfrx-multiselect__list .viewport{height:224px}.dsfrx-multiselect__list--empty p{padding:.5rem 1rem 0rem;background-color:var(--background-default-grey);margin:0}.dsfrx-multiselect--placeholder{color:var(--text-default-grey)}.dsfrx-select>.fr-label{margin-bottom:.5rem}.fr-label+.dsfrx-multiselect{margin-top:.5rem}.dsfrx-multiselect .fr-tags-group{margin:0;gap:.5rem 0}.dsfrx-multiselect .fr-tags-group li{line-height:1rem}.dsfrx-multiselect .fr-tags-group .fr-tag{margin-bottom:0}dsfr-ext-multiselect .fr-select-group--error label span,dsfr-ext-multiselect .fr-select-group--valid label span,dsfrx-treeselect .fr-select-group--error label span,dsfrx-treeselect .fr-select-group--valid label span{color:var(--text-label-grey)}@container (min-width: 500px){.dsfrx-multiselect .dsfrx-multiselect__toolbar{display:flex;padding:.5rem;justify-content:space-between;align-items:center}.dsfrx-multiselect .dsfrx-multiselect__toolbar dsfr-button{width:initial}.dsfrx-multiselect .dsfrx-multiselect__toolbar .fr-btn--select-all{margin:0;min-width:10rem;margin-right:1rem}}.dsfrx-multiselect__list--has-viewport ul{list-style-type:none}.dsfrx-multiselect__list--has-viewport ul[role=tree]{padding:0}.dsfrx-multiselect__list ul.tree-node{list-style:none;margin-top:.25rem;margin-left:1.5rem;margin-bottom:-.25rem}.dsfrx-multiselect__list[role=tree]{padding-bottom:.25rem;padding-top:.25rem}.dsfrx-multiselect__list .tree-node-content{margin-left:4px;margin-right:4px;width:calc(100% - 8px)}.dsfrx-multiselect__list .tree-node-content:focus-visible{outline-offset:1px}.dsfrx-multiselect__list .tree-node-content-inside{--hover-tint: var(--hover);display:flex;padding-left:.25rem;margin-left:-.25rem;position:relative}.dsfrx-multiselect__list .tree-node-content-inside .tree-node-label{display:inline-block;width:100%;padding-left:.25rem}.dsfrx-multiselect__list .tree-node-content-inside .tree-node-label .fr-label{cursor:pointer;padding:.25rem 0}.dsfrx-multiselect__list .tree-node-content-inside .tree-node-label .fr-checkbox-group--sm input[type=checkbox]+label:before{margin-top:.5rem}.dsfrx-multiselect__list .tree-node-content-inside .tree-button{margin-left:-.25rem;padding:.25rem;cursor:pointer;margin-top:0}.dsfrx-multiselect__list .tree-node-content-inside.selectable.selected:not(:has(dsfr-ext-checkbox)):after{mask-image:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width=\"24\" height=\"24\"><path d=\"m10 15.172 9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414 4.95 4.95Z\"/></svg>');content:\"\";--icon-size: 1rem;height:var(--icon-size);mask-size:100% 100%;width:var(--icon-size);background-color:var(--text-action-high-blue-france);right:1rem;position:absolute;top:25%}.dsfrx-multiselect__list .tree-node-content-inside.selectable.selected:not(:has(dsfr-ext-checkbox)) label:not([for]){color:var(--text-active-blue-france)}.dsfrx-multiselect__list .tree-node-content-inside.selectable.selected:not(:has(dsfr-ext-checkbox)) .tree-node-label{outline:1px solid var(--border-action-high-blue-france);background-color:var(--background-open-blue-france)}.dsfrx-multiselect__list .tree-node-content-inside.selectable.selected:not(:has(dsfr-ext-checkbox)) .tree-node-label:hover{background-color:var(--background-open-blue-france-hover)}.dsfrx-multiselect__list .tree-node-content-inside.selectable.selectable .tree-node-label.hoverable:hover,.dsfrx-multiselect__list .tree-node-content-inside.selectable.selectable .tree-button:hover{background:var(--hover-tint)}.dsfrx-multiselect__list .tree-node-content-inside:focus-visible{outline-offset:1px;outline:#0a76f6 solid 2px;width:calc(100% - 3px)}.dsfrx-multiselect__list.expandable .selectable:hover{background:var(--hover-tint)}.dsfrx-select.fr-select-group--disabled{cursor:not-allowed}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: i3.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i3.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i3.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: DropdownContainerComponent, selector: "edu-dropdown-container", inputs: ["virtualScroll", "virtualScrollItemHeight", "appendTo", "hasOptions", "zIndex", "scrollHeight", "classToWrap", "hasToolBar", "indexFirstSelected", "autofocus", "autoWidthDropdown", "displayDropdown"], outputs: ["hideList", "focusOnList", "dropdownAttached"] }, { kind: "ngmodule", type: DsfrTagModule }, { kind: "ngmodule", type: DsfrButtonModule }, { kind: "component", type: i4.DsfrButtonComponent, selector: "dsfr-button", inputs: ["label", "type", "tooltipMessage", "variant", "buttonSize", "icon", "iconPosition", "disabled", "uppercase", "loader", "ariaLabel", "invertedOutlineContrast", "id", "buttonId", "ariaControls", "ariaPressed", "ariaHasPopup", "ariaExpanded", "tabIndex", "customClass", "buttonRole", "labelSrOnly", "size"] }, { kind: "component", type: EduCheckboxComponent, selector: "dsfr-ext-checkbox, dsfrx-checkbox", inputs: ["id", "label", "selected", "tabIndex", "disabled", "icon"], outputs: ["checkboxClick"] }, { kind: "pipe", type: DsfrI18nPipe, name: "dsfrI18n" }, { kind: "directive", type: EduMessageSeverityDirective, selector: "[eduMessageSeverity]", inputs: ["eduMessageSeverity"] }, { kind: "component", type: SelectInputLabelComponent, selector: "edu-select-input-label", inputs: ["placeHolder", "maxDisplayOptions", "maxDisplayOptionsTemplate", "selectedOptionTemplate", "clearableOptions", "selectedOptions"], outputs: ["clearAllOptionsSelect", "clearOptionSelect"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DsfrTreeselectComponent, decorators: [{
type: Component,
args: [{ selector: 'dsfr-ext-treeselect, dsfrx-treeselect', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
CommonModule,
FormsModule,
ScrollingModule,
ReactiveFormsModule,
DropdownContainerComponent,
DsfrTagModule,
DsfrButtonModule,
EduCheckboxComponent,
DsfrI18nPipe,
EduMessageSeverityDirective,
SelectInputLabelComponent,
], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DsfrTreeselectComponent), multi: true }], template: "<div\n class=\"fr-select-group dsfrx-select\"\n [class]=\"message ? 'fr-select-group--' + messageSeverity : ''\"\n [ngClass]=\"{\n 'fr-select-group--valid': message && messageSeverity === severityConst.SUCCESS,\n 'fr-select-group--disabled': disabled,\n }\">\n <div class=\"fr-label\" [ngClass]=\"{ 'fr-sr-only': labelSrOnly }\" id=\"{{ selectId }}-label\">\n @if (label) {\n {{ label }}\n } @else {\n <ng-content select=\"[label]\"></ng-content>\n }\n @if (hint) {\n <span class=\"fr-hint-text\">{{ hint }}</span>\n }\n </div>\n\n <edu-dropdown-container\n [displayDropdown]=\"displayDropdown\"\n [appendTo]=\"appendTo\"\n [classToWrap]=\"'dsfrx-multiselect'\"\n [autofocus]=\"true\"\n [hasToolBar]=\"showSearch || showSelectAll\"\n [zIndex]=\"zIndex\"\n [scrollHeight]=\"scrollHeight\"\n [autoWidthDropdown]=\"autoWidthDropdown\"\n [indexFirstSelected]=\"indexFirstSelected\"\n (hideList)=\"hide()\"\n [id]=\"selectId\"\n (focusOnList)=\"handleListFocus()\"\n [virtualScroll]=\"virtualScroll\"\n [hasOptions]=\"optionsFilter.length > 0\">\n <!-- Control input -->\n <div\n ngProjectAs=\"control\"\n ngDefaultControl\n [(ngModel)]=\"value\"\n id=\"{{ selectId }}\"\n [attr.tabindex]=\"disabled ? -1 : 0\"\n role=\"combobox\"\n (click)=\"toggleDropdown($event)\"\n (keydown)=\"onButtonKeyDown($event)\"\n aria-haspopup=\"listbox\"\n [ngClass]=\"{ 'fr-select--disabled': disabled }\"\n [disabled]=\"disabled\"\n [attr.aria-controls]=\"selectId + '-listbox'\"\n [attr.aria-expanded]=\"displayDropdown\"\n [attr.aria-invalid]=\"ariaInvalid || null\"\n [attr.aria-required]=\"ariaRequired || null\"\n [attr.aria-labelledby]=\"selectId + '-label'\"\n [attr.aria-describedby]=\"message ? selectId + '-messages' : null\"\n class=\"fr-select\">\n <edu-select-input-label\n [clearableOptions]=\"clearableOptions\"\n [maxDisplayOptions]=\"maxDisplayOptions\"\n [selectedOptionTemplate]=\"selectedOptionTemplate\"\n [placeHolder]=\"placeHolder\"\n [selectedOptions]=\"selectedOptions\"\n [maxDisplayOptionsTemplate]=\"maxDisplayOptionsTemplate\"\n (clearOptionSelect)=\"clearOption($event)\"\n (clearAllOptionsSelect)=\"clearAllOptions()\">\n </edu-select-input-label>\n </div>\n\n <!-- If toolbar is shown --------------------------------------------------------------------------->\n <ng-container toolbar>\n @if (showSelectAll && !maxSelectedOptions && selectionMode !== 'single') {\n <dsfr-button\n size=\"SM\"\n variant=\"tertiary\"\n customClass=\"fr-btn--select-all\"\n (click)=\"selectAll()\"\n [label]=\"isAllSelected ? ('multiselect.unselectAll' | dsfrI18n) : ('multiselect.selectAll' | dsfrI18n)\">\n </dsfr-button>\n }\n @if (showSearch) {\n <label for=\"{{ selectId }}-searchbox\" class=\"fr-sr-only\">{{ 'multiselect.searchList' | dsfrI18n }}</label>\n <div\n class=\"fr-input-wrap\"\n [class.fr-icon-search-line]=\"!showClearSearch\"\n [class.fr-input-wrap--action]=\"showClearSearch\">\n <input\n #searchBox\n class=\"fr-input\"\n type=\"search\"\n aria-invalid=\"false\"\n id=\"{{ selectId }}-searchbox\"\n [placeholder]=\"searchMessage ?? 'multiselect.search' | dsfrI18n\"\n (input)=\"onSearch()\"\n aria-controls=\"listboxid\"\n aria-expanded=\"true\"\n aria-autocomplete=\"list\"\n aria-haspopup=\"listbox\"\n autocomplete=\"off\"\n [(ngModel)]=\"searchText\" />\n @if (showClearSearch) {\n <button\n type=\"button\"\n class=\"fr-btn fr-icon-close-line fr-btn--tertiary\"\n id=\"{{ selectId }}-reset\"\n (click)=\"resetSearchInput()\"\n [title]=\"'multiselect.resetSearch' | dsfrI18n\">\n {{ 'multiselect.resetSearch' | dsfrI18n }}\n </button>\n }\n </div>\n }\n <div role=\"status\" aria-live=\"polite\" class=\"fr-sr-only\">\n @if (optionsFilter && countFilteredOptions > 0) {\n <p>{{ countFilteredOptions }} {{ 'multiselect.availableResults' | dsfrI18n }}</p>\n }\n </div>\n </ng-container>\n\n @if (virtualScroll) {\n <!-- Template de listbox (options) en virtual scroll ----------------------------------------------------------->\n <div\n #listbox\n id=\"{{ selectId }}-listbox\"\n [ngStyle]=\"{ 'z-index': zIndex }\"\n [style.max-height]=\"scrollHeight\"\n class=\"dsfrx-multiselect__list dsfrx-multiselect__list--has-viewport\">\n <cdk-virtual-scroll-viewport\n #viewport\n minBufferPx=\"200\"\n maxBufferPx=\"600\"\n [itemSize]=\"virtualScrollItemHeight\"\n class=\"viewport\">\n <ul role=\"tree\" [attr.aria-multiselectable]=\"!this.isSingleMode\">\n <!-- add tree node-->\n <li\n role=\"treeitem\"\n [id]=\"selectId + '-item-' + node.id\"\n [class.leaf]=\"!node.children\"\n [attr.aria-expanded]=\"node.expanded ?? (node.children ? false : null)\"\n [class.selected]=\"node.selected\"\n [attr.aria-selected]=\"this.isSingleMode ? (node.selected ?? false) : null\"\n [attr.aria-checked]=\"this.isSingleMode ? null : (node.selected ?? false)\"\n [attr.aria-disabled]=\"node.disabled\"\n [attr.aria-level]=\"node.ariaLevel\"\n [attr.aria-setsize]=\"node.ariaSize\"\n [attr.aria-posinset]=\"i + 1\"\n (keydown)=\"onNodeKeydown(node, $event)\"\n tabindex=\"-1\"\n class=\"tree-node-content tree-node-container\"\n *cdkVirtualFor=\"let node of visibleNodes; let i = index; trackBy: trackById\">\n <ng-container [ngTemplateOutlet]=\"nodeTemplate\" [ngTemplateOutletContext]=\"{ node: node }\"></ng-container>\n </li>\n </ul>\n </cdk-virtual-scroll-viewport>\n </div>\n } @else {\n <!-- Template de listbox (options) SANS virtual scroll ----------------------------------------------------------->\n <ul\n #listbox\n id=\"{{ selectId }}-listbox\"\n [ngStyle]=\"{ 'z-index': zIndex }\"\n role=\"tree\"\n tabindex=\"-1\"\n [style.max-height]=\"scrollHeight\"\n [attr.aria-multiselectable]=\"!this.isSingleMode\"\n class=\"dsfrx-multiselect__list\"\n [ngClass]=\"{ 'select-parent': selectionStrategy === 'all', 'expandable': selectionStrategy === 'leaf' }\">\n @for (node of optionsFilter; track trackById(i, node); let i = $index) {\n @if (!node.hidden) {\n <li\n role=\"treeitem\"\n (keydown)=\"onNodeKeydown(node, $event)\"\n [id]=\"selectId + '-item-' + node.id\"\n class=\"tree-node-content tree-node-container\"\n [class.leaf]=\"!node.children\"\n tabindex=\"-1\"\n [attr.aria-expanded]=\"node.expanded ?? (node.children ? false : null)\"\n [class.selected]=\"node.selected\"\n [attr.aria-level]=\"node.ariaLevel\"\n [attr.aria-setsize]=\"node.ariaSize\"\n [attr.aria-posinset]=\"i + 1\"\n [attr.aria-selected]=\"this.isSingleMode ? (node.selected ?? false) : null\"\n [attr.aria-checked]=\"this.isSingleMode ? null : (node.selected ?? false)\"\n [attr.aria-disabled]=\"node.disabled\">\n <ng-container [ngTemplateOutlet]=\"nodeTemplate\" [ngTemplateOutletContext]=\"{ node: node }\"></ng-container>\n </li>\n }\n }\n </ul>\n }\n\n <!-- Empty list after search filter -->\n <div\n role=\"status\"\n aria-live=\"polite\"\n ngProjectAs=\"empty\"\n class=\"dsfrx-multiselect__list dsfrx-multiselect__list--empty\">\n @if (loading) {\n <p>\n <span class=\"label\"> {{ 'multiselect.loading' | dsfrI18n }}...</span>\n </p>\n } @else if (displayDropdown && (optionsFilter.length === 0 || countFilteredOptions === 0)) {\n <p>\n {{ noResultsMessage ?? ('multiselect.noResult' | dsfrI18n) }}\n </p>\n }\n </div>\n </edu-dropdown-container>\n <div id=\"{{ selectId }}-messages\" class=\"fr-messages-group\" aria-live=\"polite\">\n @if (message) {\n <p class=\"fr-message\" [eduMessageSeverity]=\"messageSeverity\">\n {{ message }}\n </p>\n }\n </div>\n</div>\n\n<!-- Template one node -->\n<ng-template #nodeTemplate let-node=\"node\">\n <div\n class=\"tree-node-content-inside\"\n [class.selectable]=\"isSelectable()\"\n [class.selected]=\"node.selected\"\n [class.disabled]=\"node.disabled\">\n <!-- Bouton collapse / expand -->\n @if (node.children) {\n <span\n (click)=\"onNodeButton(node)\"\n class=\"tree-button\"\n [class]=\"!node.expanded ? 'fr-icon-arrow-right-s-line' : 'fr-icon-arrow-down-s-line'\"></span>\n }\n\n <div\n class=\"tree-node-label\"\n [ngClass]=\"{\n hoverable: (node.children && selectionStrategy === 'all') || showCheckboxes || !node.children,\n }\">\n <!-- Label seul -->\n @if (!showCheckboxes) {\n <label\n (click)=\"node.children && selectionStrategy === 'leaf' ? onNodeButton(node) : onOptionClick($event, node)\"\n class=\"fr-label\"\n [ngClass]=\"{ 'fr-label--disabled': node.disabled }\">\n @if (optionTemplate) {\n <!-- Slot -->\n <span class=\"text-node\"\n ><ng-container *ngTemplateOutlet=\"optionTemplate; context: { $implicit: node }\"></ng-container\n ></span>\n } @else {\n @if (node.icon) {\n <span [class]=\"node.icon + ' fr-icon--sm'\"> </span>\n }\n <span class=\"text-node\">{{ node.label }}</span>\n }\n </label>\n } @else {\n <!-- Checkbox + label -->\n <dsfr-ext-checkbox\n [id]=\"'cb-' + node.id\"\n [disabled]=\"node.disabled\"\n [tabIndex]=\"-1\"\n [icon]=\"node.icon\"\n [label]=\"optionTemplate ? undefined : node.label\"\n (checkboxClick)=\"onOptionClick($event, node)\"\n [selected]=\"node.selected\">\n @if (optionTemplate) {\n <ng-container label\n ><ng-container *ngTemplateOutlet=\"optionTemplate; context: { $implicit: node }\"></ng-container\n ></ng-container>\n }\n </dsfr-ext-checkbox>\n }\n </div>\n </div>\n\n <!-- Children -->\n @if (node.children && node.expanded) {\n <ng-container\n [ngTemplateOutlet]=\"nodesTemplate\"\n [ngTemplateOutletContext]=\"{ nodes: node.children }\"></ng-container>\n }\n</ng-template>\n\n<!-- Liste of child nodes -->\n<ng-template #nodesTemplate let-node