UNPKG

@edugouvfr/ngx-dsfr-ext

Version:

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

1 lines 805 kB
{"version":3,"file":"edugouvfr-ngx-dsfr-ext.mjs","sources":["../../../projects/ngx-dsfr-ext/src/lib/shared/directives/click-outside.directive.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/list-toolbar/list-toolbar.utils.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/dropdown-container/dropdown.utils.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/dropdown-container/dropdown-position.directive.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/dropdown-container/dropdown-container.component.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/dropdown-container/dropdown-container.component.html","../../../projects/ngx-dsfr-ext/src/lib/shared/directives/message-severity.directive.ts","../../../projects/ngx-dsfr-ext/src/lib/spinner/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/spinner/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/spinner/spinner.component.ts","../../../projects/ngx-dsfr-ext/src/lib/spinner/spinner.component.html","../../../projects/ngx-dsfr-ext/src/lib/autocomplete/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/autocomplete/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/autocomplete/autocomplete.component.ts","../../../projects/ngx-dsfr-ext/src/lib/autocomplete/autocomplete.component.html","../../../projects/ngx-dsfr-ext/src/lib/shared/utils/date/date-utils.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/utils/range/range-utils.ts","../../../projects/ngx-dsfr-ext/src/lib/calendar/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/calendar/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/calendar/calendar.component.ts","../../../projects/ngx-dsfr-ext/src/lib/calendar/calendar.component.html","../../../projects/ngx-dsfr-ext/src/lib/calendar/calendar-modal/calendar-modal.component.ts","../../../projects/ngx-dsfr-ext/src/lib/calendar/calendar-modal/calendar-modal.component.html","../../../projects/ngx-dsfr-ext/src/lib/shared/models/point.ts","../../../projects/ngx-dsfr-ext/src/lib/carousel/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/carousel/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/carousel/carousel.component.ts","../../../projects/ngx-dsfr-ext/src/lib/carousel/carousel.component.html","../../../projects/ngx-dsfr-ext/src/lib/shared/models/selection-mode.type.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/abstract-select/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/abstract-select/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/abstract-select/abstract-select.component.ts","../../../projects/ngx-dsfr-ext/src/lib/datepicker/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/datepicker/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/datepicker/mask/date-format.directive.ts","../../../projects/ngx-dsfr-ext/src/lib/datepicker/mask/date-mask.directive.ts","../../../projects/ngx-dsfr-ext/src/lib/datepicker/datepicker.component.ts","../../../projects/ngx-dsfr-ext/src/lib/datepicker/datepicker.component.html","../../../projects/ngx-dsfr-ext/src/lib/dropdownmenu/component/dropdownmenu-group.component.ts","../../../projects/ngx-dsfr-ext/src/lib/dropdownmenu/component/dropdownmenu-item.component.ts","../../../projects/ngx-dsfr-ext/src/lib/dropdownmenu/dropdownmenu.component.ts","../../../projects/ngx-dsfr-ext/src/lib/dropdownmenu/dropdownmenu.component.html","../../../projects/ngx-dsfr-ext/src/lib/multiselect/select-focus.utils.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/list-options/list-options.component.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/list-options/list-options.component.html","../../../projects/ngx-dsfr-ext/src/lib/shared/components/list-toolbar/list-toolbar.component.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/list-toolbar/list-toolbar.component.html","../../../projects/ngx-dsfr-ext/src/lib/shared/components/select-input-label/select-input-label.component.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/components/select-input-label/select-input-label.component.html","../../../projects/ngx-dsfr-ext/src/lib/shared/utils/option/option-utils.ts","../../../projects/ngx-dsfr-ext/src/lib/multiselect/multiselect.component.ts","../../../projects/ngx-dsfr-ext/src/lib/multiselect/multiselect.component.html","../../../projects/ngx-dsfr-ext/src/lib/panel/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/panel/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/panel/panel.component.ts","../../../projects/ngx-dsfr-ext/src/lib/panel/panel.component.html","../../../projects/ngx-dsfr-ext/src/lib/picklist/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/picklist/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/picklist/picklist.model.ts","../../../projects/ngx-dsfr-ext/src/lib/picklist/unit/picklist-unit.component.ts","../../../projects/ngx-dsfr-ext/src/lib/picklist/unit/picklist-unit.component.html","../../../projects/ngx-dsfr-ext/src/lib/picklist/picklist.component.ts","../../../projects/ngx-dsfr-ext/src/lib/picklist/picklist.component.html","../../../projects/ngx-dsfr-ext/src/lib/progressbar/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/progressbar/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/progressbar/progressbar.component.ts","../../../projects/ngx-dsfr-ext/src/lib/progressbar/progressbar.component.html","../../../projects/ngx-dsfr-ext/src/lib/timeline/timeline.component.ts","../../../projects/ngx-dsfr-ext/src/lib/timeline/timeline.component.html","../../../projects/ngx-dsfr-ext/src/lib/toast/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/toast/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/toast/item/toast-item.component.ts","../../../projects/ngx-dsfr-ext/src/lib/toast/item/toast-item.component.html","../../../projects/ngx-dsfr-ext/src/lib/toast/toast.model.ts","../../../projects/ngx-dsfr-ext/src/lib/toast/toast.service.ts","../../../projects/ngx-dsfr-ext/src/lib/toast/toast.component.ts","../../../projects/ngx-dsfr-ext/src/lib/toast/toast.component.html","../../../projects/ngx-dsfr-ext/src/lib/shared/utils/tree/tree.model.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/utils/tree/tree-focus-manager.ts","../../../projects/ngx-dsfr-ext/src/lib/shared/utils/tree/tree-select-manager.ts","../../../projects/ngx-dsfr-ext/src/lib/tree/tree.component.ts","../../../projects/ngx-dsfr-ext/src/lib/tree/tree.component.html","../../../projects/ngx-dsfr-ext/src/lib/treeselect/treeselect.component.ts","../../../projects/ngx-dsfr-ext/src/lib/treeselect/treeselect.component.html","../../../projects/ngx-dsfr-ext/src/lib/treetable/component/expand-arrow-node.component.ts","../../../projects/ngx-dsfr-ext/src/lib/treetable/treetable.context.ts","../../../projects/ngx-dsfr-ext/src/lib/treetable/directive/row-node.directive.ts","../../../projects/ngx-dsfr-ext/src/lib/treetable/directive/row-node-select.directive.ts","../../../projects/ngx-dsfr-ext/src/lib/treetable/base-treetable.component.ts","../../../projects/ngx-dsfr-ext/src/lib/treetable/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/treetable/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/treetable/treetable.component.ts","../../../projects/ngx-dsfr-ext/src/lib/treetable/treetable.component.html","../../../projects/ngx-dsfr-ext/src/lib/upload-dnd/directives/file-actions.directive.ts","../../../projects/ngx-dsfr-ext/src/lib/upload-dnd/directives/file-delete-button.directive.ts","../../../projects/ngx-dsfr-ext/src/lib/upload-dnd/file-size-unit.model.ts","../../../projects/ngx-dsfr-ext/src/lib/upload-dnd/file-details.pipe.ts","../../../projects/ngx-dsfr-ext/src/lib/upload-dnd/i18n/messages.en.ts","../../../projects/ngx-dsfr-ext/src/lib/upload-dnd/i18n/messages.fr.ts","../../../projects/ngx-dsfr-ext/src/lib/upload-dnd/upload-dnd.component.ts","../../../projects/ngx-dsfr-ext/src/lib/upload-dnd/upload-dnd.component.html","../../../projects/ngx-dsfr-ext/src/lib/profilemenu/component/toolslinks.component.ts","../../../projects/ngx-dsfr-ext/src/lib/profilemenu/component/toolslinks.component.html","../../../projects/ngx-dsfr-ext/src/public-api.ts","../../../projects/ngx-dsfr-ext/src/edugouvfr-ngx-dsfr-ext.ts"],"sourcesContent":["import {\n Directive,\n ElementRef,\n EventEmitter,\n NgZone,\n OnDestroy,\n OnInit,\n Output,\n Renderer2,\n inject,\n} from '@angular/core';\n\n/**\n * Directive pour détecter un click en dehors du composant (ex. pour fermer une liste déroulante)\n * Nécessite l'ajout d'un attribut data-expanded sur le composant possedant clickOutside\n */\n@Directive({\n selector: '[clickOutside]',\n standalone: true,\n})\nexport class EduClickOutsideDirective implements OnInit, OnDestroy {\n @Output() clickOutsideEvent = new EventEmitter<void>();\n\n private _unlisten: { (): void }[] = [];\n private readonly _elementRef = inject(ElementRef);\n private readonly ngZone = inject(NgZone);\n private readonly renderer2 = inject(Renderer2);\n\n ngOnInit() {\n this.ngZone.runOutsideAngular(() => {\n this._unlisten.push(\n this.renderer2.listen('window', 'mouseup', (e) => {\n this.onDocumentClick(e);\n }),\n this.renderer2.listen('window', 'resize', (e) => {\n this.onDocumentResize(e);\n }),\n );\n });\n }\n\n ngOnDestroy() {\n this._unlisten.forEach((unlistenFunc) => unlistenFunc());\n }\n\n /**\n * En cas de redimensionnement ou de zoom de la fenetre\n * Si on est en mode appendTo, on force la fermeture de la dropdown pour eviter son mauvais positionnement\n * @internal */\n private onDocumentResize(e: Event): void {\n if (\n this._elementRef.nativeElement.hasAttribute('data-expanded') &&\n !this._elementRef.nativeElement.querySelector('ul.dsfrx-treeselect__list') &&\n !this._elementRef.nativeElement.querySelector('ul.dsfrx-select__list')\n ) {\n this.onNeedToCloseDropdown(e);\n }\n }\n\n /**\n * Emet l'evenement click outside en cas de clic en dehors du composant\n * @internal */\n private onDocumentClick(e: MouseEvent): void {\n const elem = e?.target as HTMLElement;\n // positionnement d'un attribut data-expanded qui indique l'affiche de la liste sur le composant\n if (\n elem &&\n this._elementRef.nativeElement.hasAttribute('data-expanded') &&\n !this._elementRef.nativeElement.contains(elem) &&\n !elem.closest('.dsfrx-dropdown')\n ) {\n this.onNeedToCloseDropdown(e);\n }\n }\n\n /** Emet l'evenement clickOutsideEvent pour forcer la fermeture de la dropdown */\n private onNeedToCloseDropdown(e: Event) {\n this.ngZone.run(() => {\n e.preventDefault();\n this.clickOutsideEvent.emit();\n });\n }\n}\n","export class ListToolbarUtils {\n private constructor() {}\n\n /**\n * cas TAB : L'élément est le dernier élément focusable de la toolbar :\n * - l'élément est la croix pour vider la recherche\n * - l'élément est la barre de recherche et la croix n'est pas présente\n * - la barre de recherche n'est pas présente (un seul élément focusable : tout sélectionner)\n */\n static isLastFocusableOnToolBar(element: HTMLElement, toolBarWrapper: HTMLElement): boolean {\n return (\n element.className.includes('fr-icon-close-line') ||\n (!toolBarWrapper.querySelector('.fr-icon-close-line') && element.className.includes('fr-input')) ||\n !toolBarWrapper.querySelector('.fr-input')\n );\n }\n}\n","export class DropdownUtils {\n private constructor() {}\n\n /**\n * Si appendTo est défini, trouver le container auquel le rattacher (element ou classe)\n * Si le container parent ne correspond ni au body ni à une modale, le placer en position relative\n * */\n static setParentContainer(appendTo: HTMLElement | string, comboBox: HTMLElement): HTMLElement | undefined {\n let containerToAttach: HTMLElement | undefined = undefined;\n\n if (appendTo === 'body') {\n containerToAttach = document.querySelector('body')!;\n } else {\n if (typeof appendTo === 'string') {\n const elem: HTMLElement | null = comboBox.closest(`.${appendTo}`);\n containerToAttach = elem ?? undefined;\n } else {\n containerToAttach = appendTo;\n }\n if (!containerToAttach) {\n console.warn('`appendTo` must be equal to `body`, an element ref or a class of an ancestor container');\n }\n if (containerToAttach && !DropdownUtils.isBodyOrFixedElement(containerToAttach)) {\n // surcharge du style de l'élément parent avec position relative si appendTo !== body\n containerToAttach.style.position = 'relative';\n }\n }\n\n return containerToAttach;\n }\n\n /**\n * Get viewport for overlay position\n * From primeNG */\n static getViewport(): { width: number; height: number } {\n const win = window,\n d = document,\n e = d.documentElement,\n g = d.getElementsByTagName('body')[0],\n w = win.innerWidth || e.clientWidth || g.clientWidth,\n h = win.innerHeight || e.clientHeight || g.clientHeight;\n\n return { width: w, height: h };\n }\n\n /**\n * Get scroll from top for overlay position\n * From primeNG */\n static getWindowScrollTop(): number {\n const doc = document.documentElement;\n\n return (window.scrollY || doc.scrollTop) - (doc.clientTop || 0);\n }\n\n static isBodyOrFixedElement(container: HTMLElement): boolean {\n return container.tagName === 'BODY' || container.tagName === 'DIALOG' || container.style.position === 'fixed';\n }\n}\n","import {\n afterNextRender,\n Directive,\n ElementRef,\n EventEmitter,\n inject,\n Input,\n input,\n InputSignal,\n OnDestroy,\n Output,\n Renderer2,\n} from '@angular/core';\nimport { DropdownUtils } from './dropdown.utils';\n\n@Directive({\n selector: '[dsfrxDropdown]',\n standalone: true,\n})\nexport class DsfrDropdownPositionDirective implements OnDestroy {\n @Input() autoWidth: boolean = false;\n\n /** Element auquel le dropdown est programmatiquement attaché en cas de appendTo ('body' ou container relatif) */\n @Input() parentContainer: HTMLElement | undefined;\n /** Emission après attachement de la dropdown au parentContainer en appendTo */\n @Output() appendDropdown: EventEmitter<void> = new EventEmitter();\n\n comboBox: InputSignal<HTMLElement> = input.required();\n\n private readonly dropdownRef = inject(ElementRef<HTMLElement>);\n private readonly _renderer = inject(Renderer2);\n private readonly _unlistenFn!: Function;\n\n /**\n * Ajoute l'élément dropdown et le repositionne\n * /!\\ ne pas utiliser un effect() : le cycle de rendu est différent entre angular 17 et angular 19,\n * le composant ne sera pas encore ajouté dans le DOM en angular 19.\n */\n constructor() {\n afterNextRender(() => {\n if (this.dropdownRef) {\n if (this.parentContainer) {\n this.appendElement(this.dropdownRef.nativeElement);\n }\n\n this.setPosition(this.dropdownRef.nativeElement);\n this.appendDropdown.emit();\n }\n });\n }\n\n /** A la destruction du composant, supprimer l'élément listbox si il a été ajouté au parent ou body (appendTo) */\n public ngOnDestroy(): void {\n if (this.parentContainer && this.dropdownRef) {\n this.destroyOverlayDropdown();\n }\n }\n\n private setPosition(dropdownElement: HTMLElement) {\n const viewport: { width: number; height: number } = DropdownUtils.getViewport();\n let comboBox: HTMLElement = this.comboBox(); //box container to attach to\n\n if (dropdownElement) {\n const top: string = this.calcPositionY(dropdownElement, comboBox, viewport.height);\n this._renderer.setStyle(dropdownElement, 'top', top);\n\n // Ajuster la hauteur (marginTop) en cas d'éléments messages présents à l'interieur de la comboBox\n const marginTopOffset = this.adjustMarginTop(comboBox);\n\n this._renderer.setStyle(dropdownElement, 'margin-top', `${marginTopOffset}rem`);\n\n // Calculer le positionnement en valeur absolue\n if (this.autoWidth || this.parentContainer) {\n const left: string = this.calcPositionX(dropdownElement, comboBox, viewport.width);\n this._renderer.setStyle(dropdownElement, 'left', left);\n\n if (this.parentContainer && marginTopOffset) {\n this._renderer.setStyle(dropdownElement, 'margin-top', `${marginTopOffset}rem`);\n }\n }\n }\n }\n\n /**\n * Calcul de la position verticale de la dropdown\n * - selon la place disponible (en dessous / au dessus)\n * - en valeur absolue selon le container parent (si appendTo)\n * @param dropdown dropdown a positionner\n * @param comboBox container auquel rattacher la dropdown visuellement\n * @param viewportHeight\n * @returns nouvelle position en string (px/auto)\n */\n private calcPositionY(dropdown: HTMLElement, comboBox: HTMLElement, viewportHeight: number): string {\n let top: string = 'auto';\n\n const dropdownHeight = dropdown.offsetHeight; // hauteur de mon dropdownElement // ELEMENTOUTERHEIGHT\n const comboBoxOffset = comboBox.getBoundingClientRect(); // position de ma box container (ex. select)\n const windowScrollTop = DropdownUtils.getWindowScrollTop();\n\n // si position de ma box container + hauteur de ma box container + hauteur de mon dropdownElement inférieur a hauteur disponible\n const canDisplayBelow: boolean = comboBoxOffset.top + comboBox.offsetHeight + dropdownHeight < viewportHeight;\n let calcPos: number = 0;\n\n if (this.parentContainer) {\n // calcul de la position si appendTo en fonction du container passé en parametre\n calcPos = this.computeTopPositionRelative(comboBox, this.parentContainer);\n top = `${calcPos}px`;\n }\n\n if (!canDisplayBelow) {\n // si il n'y a pas assez de place pour afficher au-dessus, on laisse en dessous par défaut\n const candisplayAbove = comboBoxOffset.top + windowScrollTop - dropdownHeight > 0;\n\n // TODO FIX BETTER -- fix pour la hauteur de l'input qui n'est pas la bonne a cause de l'ensemble combobox\n let diff = 0;\n let input = comboBox.querySelector('.fr-input');\n if (input) {\n diff = comboBox.offsetHeight - input.getBoundingClientRect().height;\n }\n // ----------------------\n\n if (candisplayAbove) {\n // calcul de la position en fonction de appendTo (container to attach)\n calcPos = this.parentContainer\n ? calcPos + diff - dropdownHeight - comboBox.offsetHeight\n : -dropdownHeight + diff;\n top = `${calcPos}px`; // afficher au dessus du combobox\n }\n }\n\n return top;\n }\n\n /**\n * Calcul de la position horizontale de la dropdown\n * - selon la place disponible (décalage à gauche éventuel)\n * - en valeur absolue selon le container parent (si appendTo)\n * @param dropdown dropdown a positionner\n * @param comboBox container auquel rattacher la dropdown visuellement\n * @param viewportWidth\n * @returns nouvelle position en string (px/auto)\n */\n private calcPositionX(dropdown: HTMLElement, comboBox: HTMLElement, viewportWidth: number): string {\n const elementOuterWidth = dropdown.offsetWidth;\n const comboBoxOffset = comboBox.getBoundingClientRect(); // position de ma box container (ex. select)\n\n if (this.autoWidth) {\n if (comboBoxOffset.left + elementOuterWidth > viewportWidth) {\n return this.parentContainer\n ? Math.max(0, comboBoxOffset.left + comboBox.offsetWidth - elementOuterWidth) + 'px'\n : comboBox.offsetWidth - elementOuterWidth + 'px';\n }\n }\n\n if (this.parentContainer && DropdownUtils.isBodyOrFixedElement(this.parentContainer)) {\n // decalage a droite en cas de body\n return comboBox.getBoundingClientRect().left + 'px';\n } else if (this.parentContainer && comboBox.offsetLeft > 0) {\n return comboBox.offsetLeft + 'px';\n }\n\n return 'auto';\n }\n\n /**\n * Compute top position relative to parent element (top position, margin/padding, scroll inside parent)\n * @param element element to move relatively\n * @param parentElement parent container\n * @returns new top position\n */\n private computeTopPositionRelative(element: HTMLElement, parentElement: HTMLElement): number {\n if (DropdownUtils.isBodyOrFixedElement(parentElement)) {\n return (\n element.getBoundingClientRect().top +\n element.offsetHeight +\n (parentElement.tagName === 'BODY' ? window.scrollY : 0)\n );\n }\n\n return (\n element.getBoundingClientRect().top +\n element.offsetHeight +\n parentElement.scrollTop -\n parentElement.getBoundingClientRect().top\n );\n }\n\n /**\n * Ajouter l'element dropdown directement au body ou container spécifié dans le cas de appendTo\n * Permet d'afficher la dropdown \"en dehors\" d'un container visuel type modale ou accordéon (pas de scroll)\n * @param element dropdown\n */\n private appendElement(element: HTMLElement): void {\n const box = this.comboBox() as HTMLElement;\n\n if (!this.autoWidth) {\n this._renderer.setStyle(element, 'width', `${box.offsetWidth}px`);\n }\n this._renderer.setStyle(element, 'top', `0px`); // obligatoire pour positionner correctement l'element ensuite\n\n // Attach element to new parent\n this._renderer.appendChild(this.parentContainer, element);\n }\n\n /** Suppression de la liste rattachée au container appendTo */\n private destroyOverlayDropdown(): void {\n if (this.dropdownRef.nativeElement?.parentElement === this.parentContainer) {\n this._renderer.removeChild(this.parentContainer, this.dropdownRef.nativeElement);\n }\n\n if (this._unlistenFn) this._unlistenFn();\n }\n\n /**\n * Réajuster la marge supérieure de la dropdown en cas de fieldset ou de messages groupe\n */\n private adjustMarginTop(comboBox: HTMLElement): number {\n const hasInputGroupWithMessage = !!comboBox.querySelector('.fr-input-group .fr-message');\n const hasFieldsetWithMessage = !!comboBox.querySelector('.fr-fieldset .fr-message');\n\n if (!hasInputGroupWithMessage && !hasFieldsetWithMessage) return 0;\n\n const messageElemList = comboBox.querySelectorAll('.fr-messages-group');\n const messageElem = messageElemList[messageElemList.length - 1]; // on prend le dernier pour supporter le cas du fieldset\n const heightMessagePx = messageElem ? messageElem.getBoundingClientRect().height : 0;\n let heightMessageRem = this.pxToRem(heightMessagePx);\n\n if (hasInputGroupWithMessage) {\n // si on se trouve dans le cas d'un input-group on doit ajouter la valeur de la marge haute soit 1rem\n heightMessageRem = 1 + heightMessageRem;\n }\n\n // et on bascule en négatif\n return heightMessageRem * -1;\n }\n\n private pxToRem(pixelNumber: number, base: number = 16): number {\n return (1 / base) * pixelNumber;\n }\n}\n","import { ScrollingModule } from '@angular/cdk/scrolling';\nimport { CommonModule } from '@angular/common';\nimport {\n afterNextRender,\n booleanAttribute,\n Component,\n ContentChild,\n ElementRef,\n EventEmitter,\n inject,\n Input,\n Output,\n Renderer2,\n ViewChild,\n ViewEncapsulation,\n} from '@angular/core';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\nimport { EduClickOutsideDirective } from '../../directives';\nimport { ListOptionsComponent } from '../list-options';\nimport { ListToolbarUtils } from '../list-toolbar/list-toolbar.utils';\nimport { DsfrDropdownPositionDirective } from './dropdown-position.directive';\nimport { DropdownUtils } from './dropdown.utils';\n\n@Component({\n selector: 'edu-dropdown-container',\n templateUrl: './dropdown-container.component.html',\n encapsulation: ViewEncapsulation.None,\n styleUrls: ['./dropdown-container.component.scss'],\n imports: [\n CommonModule,\n FormsModule,\n ReactiveFormsModule,\n ScrollingModule,\n EduClickOutsideDirective,\n DsfrDropdownPositionDirective,\n ],\n})\nexport class DropdownContainerComponent {\n @ViewChild('combobox') comboBox?: ElementRef;\n @ViewChild('toolbarWrapper') toolbarWrapper?: ElementRef;\n\n @ContentChild('listbox') listbox?: ElementRef;\n @ContentChild('listOptionsComponent') listOptionsComponent?: ListOptionsComponent;\n\n /** Activer le virtual scroll */\n @Input({ transform: booleanAttribute }) virtualScroll: boolean = false;\n\n /** Surcharger la hauteur d'un item dans le cas du virtual scroll. Par défaut 24px. */\n @Input() virtualScrollItemHeight: number = 24;\n\n /** Attacher l'élément de liste déroulante au 'body' ou a un container (réf. vers un élément #div ou nom de la classe du parent) */\n @Input() appendTo?: 'body' | HTMLElement | string;\n\n @Input({ transform: booleanAttribute }) hasOptions: boolean = false;\n\n /** Propriété CSS z-index de la liste, par défaut supérieur aux composants 'lifted' DSFR */\n @Input() zIndex?: number;\n\n /** Hauteur maximale avant le scroll sur la liste déroulante. */\n @Input() scrollHeight: string = '224px';\n\n @Input() classToWrap?: string;\n\n @Input({ transform: booleanAttribute }) hasToolBar: boolean = false;\n\n @Input() indexFirstSelected: number | null = null;\n\n /** Set focus on list on opening */\n @Input({ transform: booleanAttribute }) autofocus: boolean = false;\n\n /** La dropdown prend la taille des éléments qu'il contient (par défaut : taille du select) */\n @Input({ transform: booleanAttribute }) autoWidthDropdown: boolean = false;\n\n /**\n * Élément optionnel, si positionné il remplacera comboBox.nativeElement pour le calcul de positionnement.\n */\n @Input() customControlElement?: HTMLElement;\n\n @Output() hideList: EventEmitter<void> = new EventEmitter();\n @Output() focusOnList: EventEmitter<HTMLElement> = new EventEmitter();\n @Output() dropdownAttached: EventEmitter<void> = new EventEmitter(); // la box est append et positionnée\n\n /** Element auquel la liste est attachée programmatiquement si appendTo défini */\n protected containerToAttach: HTMLElement | undefined;\n\n private _toolBarFirstElement: HTMLElement | undefined; // first focusable element on toolbar\n private _displayDropdown: boolean = false;\n\n private readonly _renderer = inject(Renderer2);\n\n constructor() {\n // Recherche du container parent apres initialisation complete du DOM (= afterViewInit non suffisant)\n afterNextRender(() => {\n if (!this.listbox && this.listOptionsComponent?.listbox) {\n this.listbox = this.listOptionsComponent.listbox;\n }\n\n if (this.appendTo && this.controlElement) {\n this.containerToAttach = DropdownUtils.setParentContainer(this.appendTo, this.controlElement);\n }\n });\n }\n\n get displayDropdown() {\n return this._displayDropdown;\n }\n\n /* Element auquel attacher visuellement le container de la dropdown (ex. la combobox contenant le fr-select) */\n get controlElement(): HTMLElement {\n return this.customControlElement ? this.customControlElement : this.comboBox?.nativeElement;\n }\n\n @Input()\n set displayDropdown(value: boolean) {\n this._displayDropdown = value;\n if (!this.displayDropdown && this.comboBox && this.autofocus) {\n this.comboBox.nativeElement?.querySelector('.fr-select')?.focus();\n }\n }\n\n /**\n * Fermer la liste déroulante en cas de clic en dehors du composant et detruire le composant si il a été ajouté en appendTo\n * @internal */\n public onClickOutside(): void {\n this.hideList.emit();\n }\n\n /** La dropdown est visible et positionnée */\n public onDropdownAttached(): void {\n this.dropdownAttached.emit();\n this.setFocus();\n }\n\n /**\n * Positionne le focus sur la toolbar si elle existe ou un élément de la liste à l'ouverture de la dropdown\n * @internal */\n public setFocus() {\n if (!this.autofocus) return;\n\n if (this.hasToolBar && !this._toolBarFirstElement && this.toolbarWrapper) {\n const elem = this.toolbarWrapper.nativeElement.querySelector('.fr-btn--select-all');\n this._toolBarFirstElement = elem ? elem : this.toolbarWrapper.nativeElement.querySelector('.fr-input');\n }\n\n if (this.listbox && !this.hasToolBar) {\n this.focusOnListElement();\n } else if (this._toolBarFirstElement) {\n this._toolBarFirstElement.focus();\n }\n }\n\n /**\n * Gestion des événements clavier sur la toolbar\n * - \"flèche bas\" : le focus est positionné sur la liste.\n * - \"TAB\" : si dernier élément focusable de la toolbar, fermer la dropdown et aller sur l'element suivant\n * - \"SHIFT TAB\": si premier élément focusable de la toolbar, fermer la dropdown et aller sur l'element suivant\n * - \"ECHAP\" : fermer la dropdown et conserver le focus sur le select\n */\n protected onKeyDownToolbar(event: KeyboardEvent): void {\n switch (event.key) {\n case 'ArrowDown':\n if (this.listbox) {\n event.preventDefault();\n this.focusOnListElement();\n }\n break;\n\n case 'Tab':\n if (\n (event.shiftKey && event.target == this._toolBarFirstElement) ||\n (!event.shiftKey &&\n ListToolbarUtils.isLastFocusableOnToolBar(event.target as HTMLElement, this.toolbarWrapper?.nativeElement))\n ) {\n this.hideList.emit();\n }\n break;\n\n case 'Escape':\n event.preventDefault();\n (this.controlElement.querySelector('.fr-select') as HTMLElement)?.focus();\n this.hideList.emit();\n break;\n }\n }\n\n /**\n * Positionne le focus sur un élément de la liste lors de l'ouverture ou après \"flèche bas\" depuis le champ de recherche.\n * - Si un élément est déjà sélectionné, le focus est positionné sur celui-ci.\n * - Sinon, le focus est placé sur le premier élément de la liste.\n */\n private focusOnListElement() {\n if (!this.listbox) return;\n\n // en virtualScroll le premier container est cdk-virtualscroll\n const listboxNode = this.virtualScroll\n ? this.listbox.nativeElement?.querySelector('ul')\n : this.listbox.nativeElement;\n\n let elementToFocus: HTMLElement = listboxNode?.firstElementChild;\n\n if (this.indexFirstSelected && this.indexFirstSelected !== -1) {\n elementToFocus = listboxNode?.children[this.indexFirstSelected] ?? listboxNode?.firstElementChild;\n }\n\n if (elementToFocus) {\n this.setAriaActiveDescendant(listboxNode, elementToFocus.getAttribute('id'));\n elementToFocus.focus();\n this.focusOnList.emit(elementToFocus);\n }\n }\n\n /**\n * Attribut aria-activedescendant sur l'input égal à l'option qui a le focus\n * @internal */\n private setAriaActiveDescendant(listboxNode: HTMLElement, optionId: string | null) {\n if (optionId && listboxNode.getAttribute('role')) {\n this._renderer.setAttribute(listboxNode, 'aria-activedescendant', optionId);\n }\n }\n}\n","<div\n #combobox\n [attr.data-expanded]=\"displayDropdown ? displayDropdown : null\"\n clickOutside\n class=\"{{ classToWrap }}\"\n (clickOutsideEvent)=\"onClickOutside()\">\n <ng-content select=\"control\"></ng-content>\n\n @if (displayDropdown) {\n <div\n #dropdown\n dsfrxDropdown\n (appendDropdown)=\"onDropdownAttached()\"\n [comboBox]=\"controlElement\"\n [parentContainer]=\"containerToAttach\"\n [autoWidth]=\"autoWidthDropdown\"\n [ngClass]=\"{ 'dsfrx-dropdown--auto-width': autoWidthDropdown }\"\n class=\"dsfrx-dropdown\"\n [ngStyle]=\"{ 'z-index': zIndex }\">\n @if (hasToolBar) {\n <div #toolbarWrapper (keydown)=\"onKeyDownToolbar($event)\" class=\"{{ classToWrap }}__toolbar\">\n <ng-content select=\"[toolbar]\"></ng-content>\n </div>\n }\n <ng-content select=\"empty\"> </ng-content>\n <ng-content></ng-content>\n </div>\n }\n</div>\n","import { Directive, HostBinding, Input } from '@angular/core';\nimport { DsfrSeverity, DsfrSeverityConst } from '@edugouvfr/ngx-dsfr';\n\n/**\n */\n@Directive({ selector: '[eduMessageSeverity]', standalone: true })\nexport class EduMessageSeverityDirective {\n private _severity: DsfrSeverity | null = null;\n\n @HostBinding('class')\n get className(): string {\n switch (this._severity) {\n case DsfrSeverityConst.ERROR:\n return 'fr-message fr-message--error';\n case DsfrSeverityConst.SUCCESS:\n case DsfrSeverityConst.VALID:\n return 'fr-message fr-message--valid';\n case DsfrSeverityConst.INFO:\n return 'fr-message fr-message--info';\n case DsfrSeverityConst.WARNING:\n return 'fr-message fr-message--warning';\n default:\n return 'fr-message ';\n }\n }\n\n @Input('eduMessageSeverity')\n set severity(value: DsfrSeverity | undefined) {\n if (value) this._severity = value;\n }\n}\n","// Ordre alphabétique requis\nexport const MESSAGES_EN = {\n spinner: {\n messageContext: 'Loading',\n },\n};\n","// Ordre alphabétique requis\nexport const MESSAGES_FR = {\n spinner: {\n messageContext: 'Chargement en cours',\n },\n};\n","import { CommonModule } from '@angular/common';\nimport { Component, inject, Input, ViewEncapsulation } from '@angular/core';\nimport { DsfrI18nPipe, DsfrI18nService, DsfrSize, DsfrSizeConst, Language } from '@edugouvfr/ngx-dsfr';\nimport { MESSAGES_EN } from './i18n/messages.en';\nimport { MESSAGES_FR } from './i18n/messages.fr';\n\n@Component({\n selector: 'dsfr-ext-spinner, dsfrx-spinner',\n templateUrl: './spinner.component.html',\n styleUrls: ['./spinner.scss'],\n encapsulation: ViewEncapsulation.None,\n imports: [CommonModule, DsfrI18nPipe],\n})\nexport class DsfrSpinnerComponent {\n /**\n * Type de loader.\n */\n @Input() typeLoader: 'sun' | 'quarter' | 'line' = 'sun';\n\n /**\n * Message permettant de donner plus de contexte aux usagers visualisant le spinner de temporisation.\n */\n @Input() label?: string;\n\n /**\n * Permet de masquer visuellement le libellé fourni par la propriété `label`.\n *\n * ⚠️ Changement depuis 1.6.0, cette propriété ne sert plus à fournir le libellé accessible : les types `string` et\n * `undefined` sont dépréciés au profit d'un type `boolean` permettant de masquer le libellé (par défaut à `true`).\n */\n @Input() labelSrOnly?: string | boolean = true;\n\n /**\n * Ajoute un overlay fullscreen.\n */\n @Input() overlay = false;\n\n /**\n * Permet le changement de taille du spinner.\n */\n @Input() spinnerSize: DsfrSize = `SM`;\n\n private i18n = inject(DsfrI18nService);\n\n constructor() {\n this.i18n.extendsLabelsBundle(Language.EN, MESSAGES_EN);\n this.i18n.extendsLabelsBundle(Language.FR, MESSAGES_FR);\n }\n\n protected getSpinnerClasses(): {} {\n return {\n 'dsfrx-spinner': true,\n 'dsfrx-spinner--sun': this.typeLoader === 'sun',\n 'dsfrx-spinner--quarter': this.typeLoader === 'quarter',\n 'dsfrx-spinner--line': this.typeLoader === 'line',\n 'dsfrx-spinner--sm': this.spinnerSize === DsfrSizeConst.SM,\n 'dsfrx-spinner--md': this.spinnerSize === DsfrSizeConst.MD,\n 'dsfrx-spinner--lg': this.spinnerSize === DsfrSizeConst.LG,\n };\n }\n\n protected isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n }\n}\n","@if (overlay) {\n <div class=\"dsfrx-overlay\" aria-busy=\"true\">\n <div class=\"content\">\n <ng-container *ngTemplateOutlet=\"spinner\"></ng-container>\n </div>\n </div>\n} @else {\n <ng-container *ngTemplateOutlet=\"spinner\"></ng-container>\n}\n\n<ng-template #spinner>\n <div [ngClass]=\"getSpinnerClasses()\">\n @switch (typeLoader) {\n @default {\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\n <path\n d=\"M11.9995 2C12.5518 2 12.9995 2.44772 12.9995 3V6C12.9995 6.55228 12.5518 7 11.9995 7C11.4472 7 10.9995 6.55228 10.9995 6V3C10.9995 2.44772 11.4472 2 11.9995 2ZM11.9995 17C12.5518 17 12.9995 17.4477 12.9995 18V21C12.9995 21.5523 12.5518 22 11.9995 22C11.4472 22 10.9995 21.5523 10.9995 21V18C10.9995 17.4477 11.4472 17 11.9995 17ZM20.6597 7C20.9359 7.47829 20.772 8.08988 20.2937 8.36602L17.6956 9.86602C17.2173 10.1422 16.6057 9.97829 16.3296 9.5C16.0535 9.02171 16.2173 8.41012 16.6956 8.13398L19.2937 6.63397C19.772 6.35783 20.3836 6.52171 20.6597 7ZM7.66935 14.5C7.94549 14.9783 7.78161 15.5899 7.30332 15.866L4.70525 17.366C4.22695 17.6422 3.61536 17.4783 3.33922 17C3.06308 16.5217 3.22695 15.9101 3.70525 15.634L6.30332 14.134C6.78161 13.8578 7.3932 14.0217 7.66935 14.5ZM20.6597 17C20.3836 17.4783 19.772 17.6422 19.2937 17.366L16.6956 15.866C16.2173 15.5899 16.0535 14.9783 16.3296 14.5C16.6057 14.0217 17.2173 13.8578 17.6956 14.134L20.2937 15.634C20.772 15.9101 20.9359 16.5217 20.6597 17ZM7.66935 9.5C7.3932 9.97829 6.78161 10.1422 6.30332 9.86602L3.70525 8.36602C3.22695 8.08988 3.06308 7.47829 3.33922 7C3.61536 6.52171 4.22695 6.35783 4.70525 6.63397L7.30332 8.13398C7.78161 8.41012 7.94549 9.02171 7.66935 9.5Z\" />\n </svg>\n }\n @case ('quarter') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\n <path d=\"M12 3C16.9706 3 21 7.02944 21 12H19C19 8.13401 15.866 5 12 5V3Z\"></path>\n </svg>\n }\n @case ('line') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\n <path\n d=\"M18.364 5.63604L16.9497 7.05025C15.683 5.7835 13.933 5 12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12H21C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C14.4853 3 16.7353 4.00736 18.364 5.63604Z\"></path>\n </svg>\n }\n }\n </div>\n @if (label) {\n <span [ngClass]=\"{ 'fr-sr-only': labelSrOnly === true }\">{{ label }}</span>\n } @else {\n <span class=\"fr-sr-only\">{{\n isNonEmptyString(labelSrOnly) ? labelSrOnly : ('spinner.messageContext' | dsfrI18n)\n }}</span>\n }\n</ng-template>\n","export const MESSAGES_EN = {\n autocomplete: {\n loading: 'Loading',\n deleteTag: 'Unselect option',\n availableResults: 'options available',\n noResults: 'No option available',\n noSelect: 'No option selected',\n alreadyExist: 'This value already exists in the list of selected values.',\n maxSelection: 'The maximum number of selectable values has been reached.',\n },\n};\n","// Ordre alphabétique requis\nexport const MESSAGES_FR = {\n autocomplete: {\n loading: 'Chargement',\n deleteTag: \"Déselectionner l'option\",\n availableResults: 'suggestions disponibles',\n noResults: 'Aucune suggestion disponible',\n noSelect: 'Aucune suggestion sélectionnée',\n alreadyExist: 'Cette valeur existe déjà dans la liste des valeurs sélectionnées.',\n maxSelection: 'Le nombre maximum de valeurs sélectionnables est atteint.',\n },\n};\n","import { CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling';\nimport { CommonModule } from '@angular/common';\nimport {\n AfterViewInit,\n booleanAttribute,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n computed,\n ContentChild,\n effect,\n ElementRef,\n EventEmitter,\n forwardRef,\n inject,\n input,\n Input,\n numberAttribute,\n OnDestroy,\n OnInit,\n Output,\n Renderer2,\n signal,\n TemplateRef,\n ViewChild,\n ViewEncapsulation,\n} from '@angular/core';\nimport { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport {\n DefaultControlComponent,\n DsfrButtonModule,\n DsfrButtonType,\n DsfrButtonVariant,\n DsfrFormInputModule,\n DsfrI18nPipe,\n DsfrI18nService,\n DsfrTagModule,\n Language,\n newUniqueId,\n} from '@edugouvfr/ngx-dsfr';\nimport { debounceTime, distinctUntilChanged, Subject, takeUntil } from 'rxjs';\nimport { DropdownContainerComponent } from '../shared/components/dropdown-container/dropdown-container.component';\nimport { EduMessageSeverityDirective } from '../shared/directives/message-severity.directive';\nimport { DsfrSpinnerComponent } from '../spinner';\nimport { DsfrCompleteEvent, DsfrSuggestionContext } from './autocomplete.model';\nimport { MESSAGES_EN } from './i18n/messages.en';\nimport { MESSAGES_FR } from './i18n/messages.fr';\n\n@Component({\n selector: 'dsfr-ext-autocomplete, dsfrx-autocomplete',\n encapsulation: ViewEncapsulation.None,\n templateUrl: './autocomplete.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n CommonModule,\n FormsModule,\n ScrollingModule,\n DsfrFormInputModule,\n DsfrButtonModule,\n DropdownContainerComponent,\n DsfrSpinnerComponent,\n DsfrI18nPipe,\n DsfrTagModule,\n EduMessageSeverityDirective,\n ],\n providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DsfrAutocompleteComponent), multi: true }],\n styleUrls: ['./autocomplete.component.scss'],\n})\n\n/**\n * Plus d'héritage de DsfrFormInputComponent afin de pouvoir utiliser le type de value (générique)\n * fixme : éviter la copie des différents inputs de DsfrFormInputComponent\n */\nexport class DsfrAutocompleteComponent\n extends DefaultControlComponent<any>\n implements OnInit, AfterViewInit, OnDestroy\n{\n /** Template des suggestions, option en paramètre */\n @ContentChild('suggestionTemplate', { static: true }) suggestionTemplate?: TemplateRef<DsfrSuggestionContext>;\n\n /** @internal viewport pour le virtualScroll */\n @ViewChild(CdkVirtualScrollViewport) viewport?: CdkVirtualScrollViewport;\n\n /** @internal le composant de saisie (<input>) */\n @ViewChild('inputComponent', { read: ElementRef }) inputComponent?: ElementRef;\n\n // Attributs c/c pour le dsfr-form-input -----------------------------------------------------------------------------\n\n /** (form-input) type de l'input, 'text' ou 'search'. 'text' par défaut */\n @Input() type: 'text' | 'search' = 'text';\n /** (form-input) champ obligatoire ou non, faux par défaut. */\n @Input() required = false;\n /** (form-input) placeholder de l'input. */\n @Input() placeholder?: string;\n /*** (form-input) Attribut maxLength de l'input. */\n @Input({ transform: numberAttribute }) maxLength?: number;\n /** (form-input) ajoute un icone à droite dans le champ de saisie. */\n @Input() icon?: string;\n /** (form-input) pattern de l'input.*/\n @Input() pattern?: string;\n /** (form-input) ajout d'un style spécifique, permet par exemple de la limiter la largeur d'un input. */\n @Input() customClass?: string;\n\n /** Emission de l'événement si le type du bouton est différent de `submit`. */\n @Output() buttonSelect = new EventEmitter<Event>();\n\n // Attributs pour le combo du dsfr-form-input\n\n /*** (form-input) permet de désactiver le bouton d'action. 'false' par défaut. */\n @Input() buttonDisabled = false;\n /*** (form-input) crée un combo champ + bouton si buttonIcon est renseigné */\n @Input() buttonIcon?: string;\n /** (form-input) crée un combo champ + bouton si buttonLabel est renseigné */\n @Input() buttonLabel?: string;\n /*** (form-input) tooltip message sur le bouton s'il y a lieu. */\n @Input() buttonTooltipMessage?: string;\n /*** (form-input) type du button,'button' par défaut. */\n @Input() buttonType: DsfrButtonType = 'button';\n /** (form-input) style du bouton, 'primary' par défaut. */\n @Input() buttonVariant: DsfrButtonVariant = 'primary';\n /** (form-input) [accessibilité] Spécifie le libellé qui sera retranscrit par les narrateurs d'écran. */\n @Input() buttonAriaLabel?: string;\n /** (form-input) Change la mise en page d'un input accompagné d'un bouton. */\n @Input() inputWrapMode: 'addon' | 'action' = 'addon';\n /**\n * [accessibilité] Permet de masquer visuellement le libellé du bouton en le laissant accessible aux dispositifs d'assistance.\n *\n * A utiliser pour le bouton si icône seule.\n */\n @Input({ transform: booleanAttribute }) buttonLabelSrOnly: boolean = false; // TODO\n\n // -------------------------------------------------------------------------------------------------------------------\n\n /** L'utilisateur doit traiter cet événement pour déclencher une méthode de recherche qui met à jour la propriété suggestions. */\n @Output() filterChange = new EventEmitter<DsfrCompleteEvent>();\n\n /** Emet la suggestion à la sélection de celle-ci */\n @Output() suggestionSelect = new EventEmitter<any>();\n\n /** Fermeture de la liste de suggestions */\n @Output() listClose = new EventEmitter<void>();\n\n /** Mode multiple : à la suppression d'une suggestion (tag) */\n @Output() clearOptionSelect: EventEmitter<{ nativeEvent: Event; option: any }> = new EventEmitter();\n\n /** Attacher l'élément de liste déroulante au 'body' ou a un container (réf. vers un élément #div ou nom de la classe du parent) */\n appendTo = input<'body' | HTMLElement | string>();\n\n /** Propriété CSS z-index de la liste, par défaut supérieur aux composants 'lifted' DSFR */\n zIndex = input<number>();\n\n /** Activer le filtre automatique sur l'autocomplete. Faux par défaut */\n autoFilter = input(false); // FIXME v2 : valeur true par défaut\n\n /** Supprime la valeur de l'input si elle ne provient pas directement de la liste de suggestion. */\n requireSelection = input(false);\n\n /** Mode sélection multiple de valeurs */\n multiple = input(false);\n\n /** Mode multiple: Nombre max. d'options sélectionnables */\n maxSelectedOptions = input<number>();\n\n /** Activer le virtual scroll */\n virtualScroll = input(false);\n\n /** Hauteur maximale avec unité avant le scroll sur la liste déroulante (correspond à la propriété css max-height) */\n scrollHeight = input('224px');\n\n /** Surcharger la hauteur en px d'un item dans le cas du virtual scroll. Par défaut 24 (px). */\n virtualScrollItemHeight = input(24);\n\n /** Nom de la propriété correspondant à la valeur (string) affichée dans l'objet suggestion */\n suggestionValueKey = input<string>();\n\n /** Optionnel : nom de la propriété correspond à la valeur de sélection dans l'objet suggestion.\n * Sans définition la sélection concerne tout l'objet suggestion */\n selectedValueKey = input<string>();\n\n /** Afficher le spinner indiquant le chargement des suggestions */\n loading = input(false);\n\n /** Cache le label visuellement en le laissant disponible aux lecteurs d'écran. */\n labelSrOnly = input(false);\n\n /** 👓 Correspond à l'attribut `aria-label` */\n ariaLabel = input<string>();\n\n /** 👓 Correspond à l'attribut `aria-invalid` */\n ariaInvalid = input<string>();\n\n /** 👓 Correspond à l'attribut `aria-describedby` */\n ariaDescribedby = input<string>();\n\n /**\n * Le délai (en millisecondes) pour déclencher l'événement de changement de saisie et de filtrage.\n * Par défaut la valeur est 0\n */\n delay = input(0);\n\n /** Minimum de caractères nécessaires au déclenchement des suggestions. 1 par défaut */\n minLength = input(1);\n\n /** Liste des suggestions */\n suggestions = input<any[]>([]);\n\n /** @internal value interne du dsfr-form-input */\n valueInput = signal<string>('');\n\n /** @internal */\n override readonly combinedAriaDescribedBy = computed<string | null>(() => {\n const ariaDescribedBy = this.ariaDescribedBy();\n const multiple = this.multiple();\n const ids: string[] = [];\n\n if (ariaDescribedBy) ids.push(ariaDescribedBy);\n ids.push(this.messagesGroupId); // toujours présent meme si aucun message\n if (multiple) ids.push(this.inputId + '-list');\n\n return ids.length ? ids.join(' ') : null;\n });\n\n protected isExpanded = signal(false); // Etat de la liste dropdown\n protected currentOptionIndex = signal<number>(-1); // Index courant de l'option ayant le focus\n protected statusMessageMultiple = '';\n protected listSelectedOptions: any[] = [];\n\n /** Nombre de résultats après une recherche (pas de valeur initiale = a11y) */\n protected nbSuggestions = computed(() => {\n return this.filteredSuggestions()?.length;\n });\n\n /** Les suggestions filtrées en cas de autoFilter, égales aux suggestions sinon */\n protected filteredSuggestions = signal<any[]>([]);\n\n private inputChange$ = new Subject<Event>(); // Subject pour capturer les changements d'input\n private notifyOnDestroy = new Subject<void>();\n\n /** Container HTML <ul> de la liste des suggestions */\n private containerListElement: HTMLElement | null = null;\n\n private readonly _cd = inject(ChangeDetectorRef);\n private readonly _i18n = inject(DsfrI18nService);\n private readonly _renderer = inject(Renderer2);\n\n constructor() {\n super();\n this._i18n.extendsLabelsBundle(Language.EN, MESSAGES_EN);\n this._i18n.extendsLabelsBundle(Language.FR, MESSAGES_FR);\n\n // Initialisation de filteredSuggestions selon suggestions\n effect(() => {\n const suggestions = this.suggestions();\n this.filteredSuggestions.set(suggestions ?? []);\n });\n }\n\n /** Fonction de formatage de la valeur affichée dans l'input à la sélection d'une suggestion */\n @Input() formatOnSelect: (selectedSuggestion: any) => string = (selectedSuggestion) => {\n const suggestionKey = this.suggestionValueKey();\n return suggestionKey ? selectedSuggestion[suggestionKey] : selectedSuggestion;\n };\n\n public override ngOnInit() {\n super.ngOnInit();\n this.inputId ??= newUniqueId();\n\n // Initialise le debounce lié à l'event input change\n this.inputChange$\n .pipe(debounceTime(this.delay()), distinctUntilChanged(), takeUntil(this.notifyOnDestroy))\n .subscribe((event: Event) => {\n this.handleInputChange(event);\n });\n }\