UNPKG

@edugouvfr/ngx-dsfr-ext

Version:

NgxDsfrExt est une extension au package @edugouvfr/ngx-dsfr (portage Angular des éléments DSFR)

416 lines 114 kB
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 }}&nbsp;{{ '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'\">&nbsp;</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 }}&nbsp;{{ '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'\">&nbsp;</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