UNPKG

@edugouvfr/ngx-dsfr-ext

Version:

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

483 lines 102 kB
import { CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling'; import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, ContentChild, ElementRef, EventEmitter, forwardRef, inject, Input, Output, signal, ViewChild, ViewEncapsulation, } from '@angular/core'; import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import { DefaultControlComponent, DsfrButtonModule, DsfrFormInputModule, DsfrI18nPipe, DsfrI18nService, DsfrTagModule, Language, newUniqueId, } from '@edugouvfr/ngx-dsfr'; import { debounceTime, distinctUntilChanged, Subject, takeUntil } from 'rxjs'; import { DropdownContainerComponent } from '../shared/components/dropdown-container/dropdown-container.component'; import { EduMessageSeverityDirective } from '../shared/directives/message-severity.directive'; import { DsfrSpinnerComponent } from '../spinner'; import { MESSAGES_EN } from './i18n/messages.en'; import { MESSAGES_FR } from './i18n/messages.fr'; 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"; const OPTION_SELECTED_CLASS = 'selected'; const OPTION_SELECTED_SELECTOR = '.' + OPTION_SELECTED_CLASS; /** * Plus d'héritage de DsfrFormInputComponent afin de pouvoir utiliser le type de value (générique) * fixme : éviter la copie des différents inputs de DsfrFormInputComponent */ export class DsfrAutocompleteComponent extends DefaultControlComponent { constructor(_renderer) { super(); this._renderer = _renderer; /** L'utilisateur doit traiter cet événement pour déclencher une méthode de recherche qui met à jour la propriété suggestions. */ this.filterChange = new EventEmitter(); /** Emet la suggestion à la sélection de celle-ci */ this.suggestionSelect = new EventEmitter(); /** Fermeture de la liste de suggestions */ this.listClose = new EventEmitter(); /** Activer le filtre automatique sur l'autocomplete. Faux par défaut */ this.autoFilter = false; /** Supprime la valeur de l'input si elle ne provient pas directement de la liste de suggestion. */ this.requireSelection = false; /** Mode sélection multiple de valeurs */ this.multiple = false; /** Activer le virtual scroll */ this.virtualScroll = false; /** Hauteur maximale avec unité avant le scroll sur la liste déroulante (correspond à la propriété css max-height)*/ this.scrollHeight = '224px'; /** Surcharger la hauteur en px d'un item dans le cas du virtual scroll. Par défaut 24 (px). */ this.virtualScrollItemHeight = 24; /** Afficher le spinner indiquant le chargement des suggestions */ this.loading = false; /** * Cache le label visuellement en le laissant disponible aux lecteurs d'écran. */ this.labelSrOnly = false; /** * Le délai (en millisecondes) pour déclencher l'événement de changement de saisie et de filtrage. * Par défaut la valeur est 0 */ this.delay = 0; /** minimum de caractères nécessaires au déclenchement des suggestions. 1 par défaut */ this.minLength = 1; // Attributs pour le dsfr-form-input ----------------------------------------------------------- */ /** (form-input) type de l'input, 'text' ou 'search'. 'text' par défaut */ this.type = 'text'; /** (form-input) champ obligatoire ou non, faux par défaut. */ this.required = false; /** (form-input) Emission de l'événement si le type du bouton est différent de `submit`. */ this.buttonSelect = new EventEmitter(); // Attributs pour le combo du dsfr-form-input ---------------------------------------------------------------------- */ /*** (form-input) permet de désactiver le bouton d'action. 'false' par défaut. */ this.buttonDisabled = false; /** (form-input) style du bouton, 'primary' par défaut. */ this.buttonVariant = 'primary'; /** (form-input) Change la mise en page d'un input accompagné d'un bouton. */ this.inputWrapMode = 'addon'; /** Mode multiple : à la suppression d'une suggestion (tag) */ this.clearOptionSelect = new EventEmitter(); this.isExpanded = false; this.statusMessageMultiple = ''; /** Nombre de résultats après une recherche (pas de valeur initiale = a11y) */ this.nbSuggestions = computed(() => { return this.filteredSuggestions()?.length; }); /** Les suggestions manipulées par la fonction de filtrage de l'utilisateur. */ this.filteredSuggestions = signal([]); this.i18n = inject(DsfrI18nService); this.inputChange$ = new Subject(); // Subject pour capturer les changements d'input this.notifyOnDestroy = new Subject(); /** Indique si l'utilisateur a tapé au moins un caractère */ this.hasUserTyped = false; /** Fonction de formatage de la valeur affichée dans l'input à la sélection d'une suggestion */ this.formatOnSelect = (selectedSuggestion) => this.suggestionValueKey ? selectedSuggestion[this.suggestionValueKey] : selectedSuggestion; this.i18n.extendsLabelsBundle(Language.EN, MESSAGES_EN); this.i18n.extendsLabelsBundle(Language.FR, MESSAGES_FR); } /** Liste des suggestions */ set suggestions(suggestions) { this.originalSuggestions = suggestions; this.filteredSuggestions.set(suggestions); } /** @internal */ ngOnInit() { super.ngOnInit(); // Initialise le debounce lié à l'event input change this.inputChange$ .pipe(debounceTime(this.delay), distinctUntilChanged(), takeUntil(this.notifyOnDestroy)) .subscribe((event) => { this.handleInputChange(event); }); this.minLength ??= 1; this.inputId ??= newUniqueId(); this.currentOptionIndex = 0; if (this.multiple && !this.value) this.value = []; } ngAfterViewInit() { const inputElement = this.inputComponent.nativeElement; if (inputElement) { this._renderer.setAttribute(inputElement, 'aria-controls', `listbox-${this.inputId}`); } } ngOnDestroy() { this.notifyOnDestroy.next(); this.notifyOnDestroy.complete(); } /** @internal */ onToogleDisplayList(display) { this.isExpanded = display; } /** @internal */ onInput(event) { if (!this.hasUserTyped) this.hasUserTyped = true; this.inputChange$.next(event); } /** @internal Ouverture de la liste des suggestions au focus si le minLength est de 0 */ onFocusInput() { if (this.minLength === 0) { this.isExpanded = true; } } /** Gestion des touches de navigation. */ /** @internal */ doKeyEvent(event, options) { switch (event.key) { case 'ArrowDown': const nextIndex = Math.min(this.currentOptionIndex + 1, options?.length); // si on est focus sur la dernire option, on va sur la premiere de la liste this.currentOptionIndex = nextIndex !== this.currentOptionIndex ? this.activateOption(nextIndex) : this.activateOption(1); event.preventDefault(); break; case 'ArrowUp': const prevIndex = Math.max(1, this.currentOptionIndex - 1); // si on est focus sur la premiere option, on va sur la derniere de la liste this.currentOptionIndex = prevIndex !== this.currentOptionIndex ? this.activateOption(prevIndex) : this.activateOption(options?.length); event.preventDefault(); break; case 'Enter': case 'Tab': case 'NumpadEnter': /* If the ENTER key is pressed, prevent the form from being submitted, */ if (event.key !== 'Tab') event.preventDefault(); if (this.currentOptionIndex > 0) { this.onOptionSelect(this.filteredSuggestions()[this.currentOptionIndex - 1], this.currentOptionIndex, false); } else { this.updateValueAndCloseList(); } break; case 'Escape': this.updateValueAndCloseList(); event.preventDefault(); break; default: } } /** * On ferme la liste des suggestions quand on sort du champ. * Mise a jour de la valeur dans le cas de requireSelection ou multiple * */ /** @internal */ updateValueAndCloseList(hasSelectOption) { this.isExpanded = false; this.resetAriaActiveDescendant(); if (this.requireSelection) { // Vérifie si la valeur saisie correspond à une suggestion existante const selectedSuggestion = this.filteredSuggestions()?.find((o) => this.formatOnSelect(o) === this.valueInput); if (selectedSuggestion) { this.multiple ? this.addSuggestionMultiple(selectedSuggestion) : (this.value = selectedSuggestion); } else if (this.hasUserTyped) { this.resetValueAndIndex(); } } else if (this.multiple && this.valueInput && !hasSelectOption) { // En mode multiple si sélection non requise on ajoute la valeur saisie en tant que tag (suggestion ou valeur libre) // Seulement si l'utilisateur n'a pas cliqué sur une option const selectedSuggestion = this.filteredSuggestions()?.find((o) => this.formatOnSelect(o) === this.valueInput); this.addSuggestionMultiple(selectedSuggestion ?? this.valueInput); } this.listClose.emit(); } /** Sélection d'une suggestion */ /** @internal */ onOptionSelect(suggestion, i, hasClickOnOption) { if (suggestion) { this.activateOption(i); if (this.multiple) { this.addSuggestionMultiple(suggestion); } else { this.value = suggestion; this.valueInput = this.formatOnSelect(this.value); } /* insert the value for the autocomplete text field: */ this.currentOptionIndex = i; } else { this.currentOptionIndex = 0; } this.suggestionSelect.emit(suggestion); /* close the list of autocompleted values */ this.updateValueAndCloseList(hasClickOnOption); } /** * Ajout de l'attribut aria-activedescendant correspond à l'option qui a le focus * @internal */ onOptionFocus(optionId) { this._renderer.setAttribute(this.inputComponent.nativeElement, 'aria-activedescendant', optionId); } /** Override writeValue pour setter la valeur de l'input programmatiquement à l'initialisation * @internal */ writeValue(value) { super.writeValue(value); if (this.multiple) { if (!this.value) this.value = []; return; } if (this.value) { this.valueInput = this.formatOnSelect(this.value); } else { this.valueInput = this.value; } } /** @internal */ isOptionSelected(i, option) { if (this.multiple && this.value?.length) { return this.value.some((o) => o === option); } return this.currentOptionIndex === i; } /** Emission de buttonSelect au niveau de form-input, seulement si le type du bouton n'est pas `submit` */ onInputButtonSelect(e) { this.buttonSelect.emit(e); } /** Suppression d'une suggestion sélectionnée en mode multiple */ removeSuggestion(e, opt) { const i = this.value.findIndex((o) => o === opt); if (i !== -1) { this.value.splice(i, 1); } this.clearOptionSelect.emit({ nativeEvent: e, option: opt }); } /** Filtrage apres input change+debounce */ handleInputChange(event) { if (!this.multiple) this.value = this.valueInput; this.isExpanded = this.valueInput !== null && this.valueInput !== undefined && this.valueInput.length >= this.minLength; if (!this.isExpanded) { this.updateValueAndCloseList(); return; } if (this.isExpanded) { if (!this.autoFilter) { const completeEvent = { originalEvent: event, query: this.valueInput }; this.filterChange.emit(completeEvent); } else { this.doAutoFilter(); } this.resetAriaActiveDescendant(); } } /** * Filtre la liste avec la valeur * * La recherche se fait toujours à partir de : originalSuggestions * On valorise directement la propriété privée _suggestions sans passer par le setter (réservé à l'utilisateur) * afin de garder intact originalSuggestions */ doAutoFilter() { let filteredSuggestions = []; // Ici, on est sûr que 'this.value' soit valorisée if (this.originalSuggestions?.length > 0) { const searchValue = this.valueInput.toLowerCase(); if (this.suggestionValueKey) { // originalSuggestions liste d'objets et suggestionValueKey définie filteredSuggestions = this.originalSuggestions.filter((item) => { return item[this.suggestionValueKey].toLowerCase().includes(searchValue); }); } else if (typeof this.originalSuggestions[0] === 'string') { // originalSuggestions liste de string filteredSuggestions = this.originalSuggestions.filter((item) => item.toLowerCase().includes(searchValue)); } } this.filteredSuggestions.set(filteredSuggestions); } activateOption(index) { const nativeElt = this.listBox?.nativeElement; let options = nativeElt?.querySelector('ul'); // Selection de l'element de liste si ajouté au body if (this.appendTo) { options = document.getElementById(`listbox-${this.inputId}`); } // Dé-sélection de l'option active s'il y a lieu const previousOption = options?.querySelector(OPTION_SELECTED_SELECTOR); previousOption?.classList.remove(OPTION_SELECTED_CLASS); // Sélection de l'indice courant const activeOption = options?.querySelector(`li:nth-child(${index})`); if (activeOption) { this.onOptionFocus(activeOption.id); activeOption.classList.add(OPTION_SELECTED_CLASS); // hover sur l'élément // comme on ne déplace pas le focus, scroll manuel sur l'élément if (!this.virtualScroll) activeOption.scrollIntoView({ behavior: 'smooth', block: 'end' }); else this.manageScrollForVirtualScroll(index); } return index; } /** * En mode virtualScroll le scroll doit se faire par la methode scrollToIndex de CDK virtualscroll * @param index index de l'option dans la liste gérée par cdkVirtualFor */ manageScrollForVirtualScroll(index) { const indexToScroll = index > this.nbSuggestions() ? 0 : index - 2; // decalage de 2 pour ne pas avoir le viewport collé à l'input this.viewport.scrollToIndex(indexToScroll, 'smooth'); } // Effacement de aria-activedescendant lorsque la liste est cachée ou une lettre tapée resetAriaActiveDescendant() { this._renderer.setAttribute(this.inputComponent.nativeElement, 'aria-activedescendant', ''); } /** * Ajoute la suggestion dans la liste si elle n'est pas déja présente et si le nombre d'options sélectionnées est inférieur à maxSelectedOptions * @param suggestion */ addSuggestionMultiple(suggestion) { this.statusMessageMultiple = ''; const matchOnValue = this.value.find((s) => s === suggestion); if (!matchOnValue && (!this.maxSelectedOptions || this.value.length < this.maxSelectedOptions)) { this.value.push(suggestion); this.valueInput = ''; } else if (matchOnValue) { this.statusMessageMultiple = 'autocomplete.alreadyExist'; } else if (this.maxSelectedOptions && this.value.length >= this.maxSelectedOptions) { this.statusMessageMultiple = 'autocomplete.maxSelection'; } } resetValueAndIndex() { this.valueInput = ''; if (!this.multiple) this.value = undefined; this.currentOptionIndex = 0; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DsfrAutocompleteComponent, deps: [{ token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: DsfrAutocompleteComponent, isStandalone: true, selector: "dsfr-ext-autocomplete, dsfrx-autocomplete", inputs: { appendTo: "appendTo", zIndex: "zIndex", autoFilter: "autoFilter", requireSelection: "requireSelection", multiple: "multiple", maxSelectedOptions: "maxSelectedOptions", virtualScroll: "virtualScroll", scrollHeight: "scrollHeight", virtualScrollItemHeight: "virtualScrollItemHeight", suggestionValueKey: "suggestionValueKey", loading: "loading", labelSrOnly: "labelSrOnly", ariaLabel: "ariaLabel", ariaInvalid: "ariaInvalid", ariaDescribedby: "ariaDescribedby", delay: "delay", minLength: "minLength", type: "type", required: "required", placeholder: "placeholder", maxLength: "maxLength", icon: "icon", pattern: "pattern", customClass: "customClass", message: "message", messageSeverity: "messageSeverity", buttonDisabled: "buttonDisabled", buttonIcon: "buttonIcon", buttonLabel: "buttonLabel", buttonTooltipMessage: "buttonTooltipMessage", buttonType: "buttonType", buttonVariant: "buttonVariant", buttonAriaLabel: "buttonAriaLabel", inputWrapMode: "inputWrapMode", suggestions: "suggestions", formatOnSelect: "formatOnSelect" }, outputs: { filterChange: "filterChange", suggestionSelect: "suggestionSelect", listClose: "listClose", buttonSelect: "buttonSelect", clearOptionSelect: "clearOptionSelect" }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DsfrAutocompleteComponent), multi: true }], queries: [{ propertyName: "suggestionTemplate", first: true, predicate: ["suggestionTemplate"], descendants: true, static: true }], viewQueries: [{ propertyName: "viewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true }, { propertyName: "inputComponent", first: true, predicate: ["inputComponent"], descendants: true, read: ElementRef }, { propertyName: "listBox", first: true, predicate: DropdownContainerComponent, descendants: true, read: ElementRef }], usesInheritance: true, ngImport: i0, template: "<div\n class=\"fr-input-group dsfrx-autocomplete\"\n [class]=\"message ? 'fr-input-group--' + messageSeverity : ''\"\n [ngClass]=\"{\n 'fr-input-group--disabled': disabled,\n 'fr-input-group--valid': message && messageSeverity === 'success',\n }\">\n <!-- Label -->\n <label [for]=\"inputId\" class=\"fr-label\" [ngClass]=\"{ 'fr-sr-only': labelSrOnly }\">\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 </label>\n\n <!-- Liste -->\n @if (multiple) {\n <div class=\"dsfrx-autocomplete__multiple-group\">\n @if (value && value.length) {\n <ul [id]=\"inputId + '-list'\" class=\"fr-tags-group\">\n @for (opt of value; track opt) {\n <li>\n <dsfr-tag\n [mode]=\"'deletable'\"\n [label]=\"suggestionValueKey && opt[suggestionValueKey] ? opt[suggestionValueKey] : opt\"\n [small]=\"true\"\n [ariaLabel]=\"\n ('autocomplete.deleteTag' | dsfrI18n) +\n ' ' +\n (suggestionValueKey && opt[suggestionValueKey] ? opt[suggestionValueKey] : opt)\n \"\n (click)=\"removeSuggestion($event, opt)\"></dsfr-tag>\n </li>\n }\n </ul>\n }\n <div role=\"status\" aria-live=\"polite\" class=\"fr-sr-only\">\n @if (statusMessageMultiple) {\n <p>{{ statusMessageMultiple | dsfrI18n }}</p>\n }\n </div>\n </div>\n }\n\n <edu-dropdown-container\n #comboboxBox\n [displayDropdown]=\"isExpanded\"\n [classToWrap]=\"'dsfrx-autocomplete'\"\n [hasToolBar]=\"false\"\n [appendTo]=\"appendTo\"\n [virtualScroll]=\"virtualScroll\"\n [zIndex]=\"zIndex\"\n (hideList)=\"updateValueAndCloseList()\"\n [hasOptions]=\"true\">\n <!-- Control -->\n <!-- todo mode action ou icon-->\n <div\n ngProjectAs=\"control\"\n [class.fr-input-wrap--addon]=\"inputWrapMode === 'addon' && (buttonIcon || buttonLabel)\"\n [class.fr-input-wrap--action]=\"inputWrapMode === 'action' && (buttonIcon || buttonLabel)\">\n @if (buttonIcon || buttonLabel) {\n <ng-container [ngTemplateOutlet]=\"inputTemplate\"></ng-container>\n <dsfr-button\n [ariaLabel]=\"buttonAriaLabel\"\n [disabled]=\"buttonDisabled\"\n [icon]=\"buttonIcon\"\n [label]=\"buttonLabel\"\n [labelSrOnly]=\"true\"\n [tooltipMessage]=\"buttonTooltipMessage\"\n [type]=\"buttonType\"\n [variant]=\"buttonVariant\"\n (click)=\"onInputButtonSelect($event)\"></dsfr-button>\n } @else {\n <ng-container [ngTemplateOutlet]=\"inputTemplate\"></ng-container>\n }\n </div>\n\n @if (virtualScroll) {\n <!-- Listebox avec virtualScroll-->\n <div\n #listbox\n [ngStyle]=\"{ 'z-index': zIndex }\"\n [style.max-height]=\"scrollHeight\"\n class=\"dsfrx-autocomplete__list dsfrx-autocomplete__list--has-viewport\">\n @if (filteredSuggestions() && nbSuggestions() > 0) {\n <cdk-virtual-scroll-viewport\n #viewport\n minBufferPx=\"200\"\n maxBufferPx=\"600\"\n [itemSize]=\"virtualScrollItemHeight\"\n class=\"viewport\">\n <ul role=\"listbox\" id=\"listbox-{{ inputId }}\" [attr.aria-labelledby]=\"inputId\">\n <li\n *cdkVirtualFor=\"let suggestion of filteredSuggestions(); let i = index\"\n role=\"option\"\n [id]=\"inputId + '-option-' + i\"\n [attr.aria-selected]=\"isOptionSelected(i, suggestion)\"\n [attr.data-index]=\"i\"\n [attr.data-dsfrx-option]=\"true\"\n class=\"option\"\n (click)=\"onOptionSelect(suggestion, i, true)\">\n <!-- Affichage suggestion ou template suggestion -->\n <ng-container\n [ngTemplateOutlet]=\"sugestionTemplateDefault\"\n [ngTemplateOutletContext]=\"{ suggestion: suggestion, index: i }\"></ng-container>\n </li>\n </ul>\n </cdk-virtual-scroll-viewport>\n }\n <ng-container [ngTemplateOutlet]=\"loadingTemplate\"></ng-container>\n </div>\n } @else {\n <!-- Listebox sans virtualScroll-->\n <div #listbox [ngStyle]=\"{ 'z-index': zIndex }\" class=\"dsfrx-autocomplete__list\">\n @if (filteredSuggestions() && nbSuggestions()) {\n <ul\n id=\"listbox-{{ inputId }}\"\n [ngStyle]=\"{ 'z-index': zIndex }\"\n [attr.aria-labelledby]=\"inputId\"\n role=\"listbox\">\n @for (suggestion of filteredSuggestions(); track suggestion; let i = $index) {\n <li\n role=\"option\"\n [id]=\"inputId + '-option-' + i\"\n [attr.aria-selected]=\"isOptionSelected(i, suggestion)\"\n [attr.data-index]=\"i\"\n [attr.data-dsfrx-option]=\"true\"\n class=\"option\"\n (click)=\"onOptionSelect(suggestion, i, true)\">\n <!-- Affichage suggestion ou template suggestion -->\n <ng-container\n [ngTemplateOutlet]=\"sugestionTemplateDefault\"\n [ngTemplateOutletContext]=\"{ suggestion: suggestion, index: i }\"></ng-container>\n </li>\n }\n </ul>\n }\n <ng-container [ngTemplateOutlet]=\"loadingTemplate\"></ng-container>\n </div>\n }\n </edu-dropdown-container>\n\n <div class=\"fr-messages-group\" [id]=\"inputId + 'messages'\" aria-live=\"polite\">\n @if (message) {\n <p class=\"fr-message\" [eduMessageSeverity]=\"messageSeverity\">{{ message }}</p>\n }\n </div>\n <span class=\"fr-sr-only\" role=\"status\" aria-live=\"polite\">\n @if (nbSuggestions() === 0) {\n {{ 'autocomplete.noResults' | dsfrI18n }}\n } @else if (nbSuggestions() > 0) {\n {{ nbSuggestions() }} {{ 'autocomplete.availableResults' | dsfrI18n }}\n }\n </span>\n</div>\n\n<ng-template #sugestionTemplateDefault let-suggestion=\"suggestion\" let-index=\"index\">\n @if (suggestionTemplate) {\n <!-- Slot -->\n <ng-container\n *ngTemplateOutlet=\"suggestionTemplate; context: { $implicit: suggestion, index: index }\"></ng-container>\n } @else {\n {{ suggestion && suggestionValueKey ? suggestion[suggestionValueKey] : suggestion }}\n }\n</ng-template>\n\n<ng-template #loadingTemplate>\n <div role=\"status\" aria-live=\"polite\" class=\"dsfrx-autocomplete--loading\">\n @if (loading) {\n <p>\n <dsfr-ext-spinner typeLoader=\"quarter\"></dsfr-ext-spinner>\n <span class=\"label\"> {{ 'autocomplete.loading' | dsfrI18n }}...</span>\n </p>\n }\n </div>\n</ng-template>\n\n<ng-template #inputTemplate>\n <input\n #inputComponent\n class=\"fr-input\"\n [ngClass]=\"customClass || null\"\n [attr.autocomplete]=\"'list'\"\n [attr.autocomplete]=\"'off'\"\n [attr.aria-describedby]=\"\n (ariaDescribedby + ' ' || '') + (multiple ? inputId + '-list ' : '') + (message ? inputId + 'messages' : '')\n \"\n [attr.aria-disabled]=\"disabled || null\"\n [attr.aria-expanded]=\"isExpanded\"\n [attr.aria-label]=\"ariaLabel || null\"\n [attr.aria-invalid]=\"ariaInvalid || null\"\n [attr.aria-required]=\"required || null\"\n [attr.role]=\"'combobox'\"\n [(ngModel)]=\"valueInput\"\n [id]=\"inputId\"\n [attr.name]=\"name || null\"\n [attr.type]=\"type\"\n [disabled]=\"disabled\"\n [required]=\"required\"\n [attr.pattern]=\"pattern || null\"\n [attr.placeholder]=\"placeholder || null\"\n [attr.maxLength]=\"maxLength || null\"\n [attr.minLength]=\"minLength || null\"\n (buttonSelect)=\"onInputButtonSelect($event)\"\n (focusin)=\"onFocusInput()\"\n (keydown)=\"doKeyEvent($event, filteredSuggestions())\"\n (input)=\"onInput($event)\" />\n</ng-template>\n", styles: [".dsfrx-autocomplete{position:relative;margin-top:.5rem}.dsfrx-autocomplete--loading p{display:flex;padding:.25rem}.dsfrx-autocomplete--loading p .label{padding-left:.5rem}.dsfrx-autocomplete[data-expanded=true] .fr-messages-group p{margin:0;height:0}.dsfrx-autocomplete__multiple-group ul.fr-tags-group li{line-height:1}.dsfrx-autocomplete__list{list-style-type:none;margin-top:0;border:1px solid var(--border-default-grey);background-color:var(--background-default-grey);padding:0;overflow-y:auto;scrollbar-width:thin;max-height:18rem}.dsfrx-autocomplete__list ul{list-style-type:none;padding:0;margin:0}.dsfrx-autocomplete__list li.option{cursor:pointer;padding-bottom:0;padding:0 1rem}.dsfrx-autocomplete__list li.option.focus{background:var(--background-alt-blue-france-hover)}.dsfrx-autocomplete__list li.option.selected,.dsfrx-autocomplete__list li.option:hover{background:var(--background-alt-blue-france-active)}.dsfrx-autocomplete__list li.option.selected{outline-style:solid;outline-offset:0;outline-width:2px;outline-color:#0a76f6}.dsfrx-autocomplete__list--has-viewport{overflow:hidden}.dsfrx-autocomplete__list .viewport{min-height:100%;max-height:224px}.dsfrx-autocomplete__list .viewport.cdk-virtual-scrollable{contain:content}@media (forced-colors: active){input{border:1px solid}}\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.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { 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: DsfrFormInputModule }, { 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: DropdownContainerComponent, selector: "edu-dropdown-container", inputs: ["virtualScroll", "virtualScrollItemHeight", "appendTo", "hasOptions", "zIndex", "scrollHeight", "classToWrap", "hasToolBar", "indexFirstSelected", "autofocus", "autoWidthDropdown", "displayDropdown"], outputs: ["hideList", "focusOnList", "dropdownAttached"] }, { kind: "component", type: DsfrSpinnerComponent, selector: "dsfr-ext-spinner", inputs: ["typeLoader", "overlay", "labelSrOnly"] }, { kind: "pipe", type: DsfrI18nPipe, name: "dsfrI18n" }, { kind: "ngmodule", type: DsfrTagModule }, { kind: "component", type: i4.DsfrTagComponent, selector: "dsfr-tag", inputs: ["tagId", "customClass", "disabled", "icon", "label", "link", "linkTarget", "route", "routePath", "routerLinkActive", "routerLinkExtras", "selected", "small", "tooltipMessage", "ariaLabel", "mode", "id", "routerLink"], outputs: ["tagSelect"] }, { kind: "directive", type: EduMessageSeverityDirective, selector: "[eduMessageSeverity]", inputs: ["eduMessageSeverity"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DsfrAutocompleteComponent, decorators: [{ type: Component, args: [{ selector: 'dsfr-ext-autocomplete, dsfrx-autocomplete', encapsulation: ViewEncapsulation.None, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ CommonModule, FormsModule, ScrollingModule, DsfrFormInputModule, DsfrButtonModule, DropdownContainerComponent, DsfrSpinnerComponent, DsfrI18nPipe, DsfrTagModule, EduMessageSeverityDirective, ], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DsfrAutocompleteComponent), multi: true }], template: "<div\n class=\"fr-input-group dsfrx-autocomplete\"\n [class]=\"message ? 'fr-input-group--' + messageSeverity : ''\"\n [ngClass]=\"{\n 'fr-input-group--disabled': disabled,\n 'fr-input-group--valid': message && messageSeverity === 'success',\n }\">\n <!-- Label -->\n <label [for]=\"inputId\" class=\"fr-label\" [ngClass]=\"{ 'fr-sr-only': labelSrOnly }\">\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 </label>\n\n <!-- Liste -->\n @if (multiple) {\n <div class=\"dsfrx-autocomplete__multiple-group\">\n @if (value && value.length) {\n <ul [id]=\"inputId + '-list'\" class=\"fr-tags-group\">\n @for (opt of value; track opt) {\n <li>\n <dsfr-tag\n [mode]=\"'deletable'\"\n [label]=\"suggestionValueKey && opt[suggestionValueKey] ? opt[suggestionValueKey] : opt\"\n [small]=\"true\"\n [ariaLabel]=\"\n ('autocomplete.deleteTag' | dsfrI18n) +\n ' ' +\n (suggestionValueKey && opt[suggestionValueKey] ? opt[suggestionValueKey] : opt)\n \"\n (click)=\"removeSuggestion($event, opt)\"></dsfr-tag>\n </li>\n }\n </ul>\n }\n <div role=\"status\" aria-live=\"polite\" class=\"fr-sr-only\">\n @if (statusMessageMultiple) {\n <p>{{ statusMessageMultiple | dsfrI18n }}</p>\n }\n </div>\n </div>\n }\n\n <edu-dropdown-container\n #comboboxBox\n [displayDropdown]=\"isExpanded\"\n [classToWrap]=\"'dsfrx-autocomplete'\"\n [hasToolBar]=\"false\"\n [appendTo]=\"appendTo\"\n [virtualScroll]=\"virtualScroll\"\n [zIndex]=\"zIndex\"\n (hideList)=\"updateValueAndCloseList()\"\n [hasOptions]=\"true\">\n <!-- Control -->\n <!-- todo mode action ou icon-->\n <div\n ngProjectAs=\"control\"\n [class.fr-input-wrap--addon]=\"inputWrapMode === 'addon' && (buttonIcon || buttonLabel)\"\n [class.fr-input-wrap--action]=\"inputWrapMode === 'action' && (buttonIcon || buttonLabel)\">\n @if (buttonIcon || buttonLabel) {\n <ng-container [ngTemplateOutlet]=\"inputTemplate\"></ng-container>\n <dsfr-button\n [ariaLabel]=\"buttonAriaLabel\"\n [disabled]=\"buttonDisabled\"\n [icon]=\"buttonIcon\"\n [label]=\"buttonLabel\"\n [labelSrOnly]=\"true\"\n [tooltipMessage]=\"buttonTooltipMessage\"\n [type]=\"buttonType\"\n [variant]=\"buttonVariant\"\n (click)=\"onInputButtonSelect($event)\"></dsfr-button>\n } @else {\n <ng-container [ngTemplateOutlet]=\"inputTemplate\"></ng-container>\n }\n </div>\n\n @if (virtualScroll) {\n <!-- Listebox avec virtualScroll-->\n <div\n #listbox\n [ngStyle]=\"{ 'z-index': zIndex }\"\n [style.max-height]=\"scrollHeight\"\n class=\"dsfrx-autocomplete__list dsfrx-autocomplete__list--has-viewport\">\n @if (filteredSuggestions() && nbSuggestions() > 0) {\n <cdk-virtual-scroll-viewport\n #viewport\n minBufferPx=\"200\"\n maxBufferPx=\"600\"\n [itemSize]=\"virtualScrollItemHeight\"\n class=\"viewport\">\n <ul role=\"listbox\" id=\"listbox-{{ inputId }}\" [attr.aria-labelledby]=\"inputId\">\n <li\n *cdkVirtualFor=\"let suggestion of filteredSuggestions(); let i = index\"\n role=\"option\"\n [id]=\"inputId + '-option-' + i\"\n [attr.aria-selected]=\"isOptionSelected(i, suggestion)\"\n [attr.data-index]=\"i\"\n [attr.data-dsfrx-option]=\"true\"\n class=\"option\"\n (click)=\"onOptionSelect(suggestion, i, true)\">\n <!-- Affichage suggestion ou template suggestion -->\n <ng-container\n [ngTemplateOutlet]=\"sugestionTemplateDefault\"\n [ngTemplateOutletContext]=\"{ suggestion: suggestion, index: i }\"></ng-container>\n </li>\n </ul>\n </cdk-virtual-scroll-viewport>\n }\n <ng-container [ngTemplateOutlet]=\"loadingTemplate\"></ng-container>\n </div>\n } @else {\n <!-- Listebox sans virtualScroll-->\n <div #listbox [ngStyle]=\"{ 'z-index': zIndex }\" class=\"dsfrx-autocomplete__list\">\n @if (filteredSuggestions() && nbSuggestions()) {\n <ul\n id=\"listbox-{{ inputId }}\"\n [ngStyle]=\"{ 'z-index': zIndex }\"\n [attr.aria-labelledby]=\"inputId\"\n role=\"listbox\">\n @for (suggestion of filteredSuggestions(); track suggestion; let i = $index) {\n <li\n role=\"option\"\n [id]=\"inputId + '-option-' + i\"\n [attr.aria-selected]=\"isOptionSelected(i, suggestion)\"\n [attr.data-index]=\"i\"\n [attr.data-dsfrx-option]=\"true\"\n class=\"option\"\n (click)=\"onOptionSelect(suggestion, i, true)\">\n <!-- Affichage suggestion ou template suggestion -->\n <ng-container\n [ngTemplateOutlet]=\"sugestionTemplateDefault\"\n [ngTemplateOutletContext]=\"{ suggestion: suggestion, index: i }\"></ng-container>\n </li>\n }\n </ul>\n }\n <ng-container [ngTemplateOutlet]=\"loadingTemplate\"></ng-container>\n </div>\n }\n </edu-dropdown-container>\n\n <div class=\"fr-messages-group\" [id]=\"inputId + 'messages'\" aria-live=\"polite\">\n @if (message) {\n <p class=\"fr-message\" [eduMessageSeverity]=\"messageSeverity\">{{ message }}</p>\n }\n </div>\n <span class=\"fr-sr-only\" role=\"status\" aria-live=\"polite\">\n @if (nbSuggestions() === 0) {\n {{ 'autocomplete.noResults' | dsfrI18n }}\n } @else if (nbSuggestions() > 0) {\n {{ nbSuggestions() }} {{ 'autocomplete.availableResults' | dsfrI18n }}\n }\n </span>\n</div>\n\n<ng-template #sugestionTemplateDefault let-suggestion=\"suggestion\" let-index=\"index\">\n @if (suggestionTemplate) {\n <!-- Slot -->\n <ng-container\n *ngTemplateOutlet=\"suggestionTemplate; context: { $implicit: suggestion, index: index }\"></ng-container>\n } @else {\n {{ suggestion && suggestionValueKey ? suggestion[suggestionValueKey] : suggestion }}\n }\n</ng-template>\n\n<ng-template #loadingTemplate>\n <div role=\"status\" aria-live=\"polite\" class=\"dsfrx-autocomplete--loading\">\n @if (loading) {\n <p>\n <dsfr-ext-spinner typeLoader=\"quarter\"></dsfr-ext-spinner>\n <span class=\"label\"> {{ 'autocomplete.loading' | dsfrI18n }}...</span>\n </p>\n }\n </div>\n</ng-template>\n\n<ng-template #inputTemplate>\n <input\n #inputComponent\n class=\"fr-input\"\n [ngClass]=\"customClass || null\"\n [attr.autocomplete]=\"'list'\"\n [attr.autocomplete]=\"'off'\"\n [attr.aria-describedby]=\"\n (ariaDescribedby + ' ' || '') + (multiple ? inputId + '-list ' : '') + (message ? inputId + 'messages' : '')\n \"\n [attr.aria-disabled]=\"disabled || null\"\n [attr.aria-expanded]=\"isExpanded\"\n [attr.aria-label]=\"ariaLabel || null\"\n [attr.aria-invalid]=\"ariaInvalid || null\"\n [attr.aria-required]=\"required || null\"\n [attr.role]=\"'combobox'\"\n [(ngModel)]=\"valueInput\"\n [id]=\"inputId\"\n [attr.name]=\"name || null\"\n [attr.type]=\"type\"\n [disabled]=\"disabled\"\n [required]=\"required\"\n [attr.pattern]=\"pattern || null\"\n [attr.placeholder]=\"placeholder || null\"\n [attr.maxLength]=\"maxLength || null\"\n [attr.minLength]=\"minLength || null\"\n (buttonSelect)=\"onInputButtonSelect($event)\"\n (focusin)=\"onFocusInput()\"\n (keydown)=\"doKeyEvent($event, filteredSuggestions())\"\n (input)=\"onInput($event)\" />\n</ng-template>\n", styles: [".dsfrx-autocomplete{position:relative;margin-top:.5rem}.dsfrx-autocomplete--loading p{display:flex;padding:.25rem}.dsfrx-autocomplete--loading p .label{padding-left:.5rem}.dsfrx-autocomplete[data-expanded=true] .fr-messages-group p{margin:0;height:0}.dsfrx-autocomplete__multiple-group ul.fr-tags-group li{line-height:1}.dsfrx-autocomplete__list{list-style-type:none;margin-top:0;border:1px solid var(--border-default-grey);background-color:var(--background-default-grey);padding:0;overflow-y:auto;scrollbar-width:thin;max-height:18rem}.dsfrx-autocomplete__list ul{list-style-type:none;padding:0;margin:0}.dsfrx-autocomplete__list li.option{cursor:pointer;padding-bottom:0;padding:0 1rem}.dsfrx-autocomplete__list li.option.focus{background:var(--background-alt-blue-france-hover)}.dsfrx-autocomplete__list li.option.selected,.dsfrx-autocomplete__list li.option:hover{background:var(--background-alt-blue-france-active)}.dsfrx-autocomplete__list li.option.selected{outline-style:solid;outline-offset:0;outline-width:2px;outline-color:#0a76f6}.dsfrx-autocomplete__list--has-viewport{overflow:hidden}.dsfrx-autocomplete__list .viewport{min-height:100%;max-height:224px}.dsfrx-autocomplete__list .viewport.cdk-virtual-scrollable{contain:content}@media (forced-colors: active){input{border:1px solid}}\n"] }] }], ctorParameters: () => [{ type: i0.Renderer2 }], propDecorators: { suggestionTemplate: [{ type: ContentChild, args: ['suggestionTemplate', { static: true }] }], viewport: [{ type: ViewChild, args: [CdkVirtualScrollViewport] }], inputComponent: [{ type: ViewChild, args: ['inputComponent', { read: ElementRef }] }], listBox: [{ type: ViewChild, args: [DropdownContainerComponent, { read: ElementRef }] }], filterChange: [{ type: Output }], suggestionSelect: [{ type: Output }], listClose: [{ type: Output }], appendTo: [{ type: Input }], zIndex: [{ type: Input }], autoFilter: [{ type: Input }], requireSelection: [{ type: Input }], multiple: [{ type: Input }], maxSelectedOptions: [{ type: Input }], virtualScroll: [{ type: Input }], scrollHeight: [{ type: Input }], virtualScrollItemHeight: [{ type: Input }], suggestionValueKey: [{ type: Input }], loading: [{ type: Input }], labelSrOnly: [{ type: Input }], ariaLabel: [{ type: Input }], ariaInvalid: [{ type: Input }], ariaDescribedby: [{ type: Input }], delay: [{ type: Input }], minLength: [{ type: Input }], type: [{ type: Input }], required: [{ type: Input }], placeholder: [{ type: Input }], maxLength: [{ type: Input }], icon: [{ type: Input }], pattern: [{ type: Input }], customClass: [{ type: Input }], message: [{ type: Input }], messageSeverity: [{ type: Input }], buttonSelect: [{ type: Output }], buttonDisabled: [{ type: Input }], buttonIcon: [{ type: Input }], buttonLabel: [{ type: Input }], buttonTooltipMessage: [{ type: Input }], buttonType: [{ type: Input }], buttonVariant: [{ type: Input }], buttonAriaLabel: [{ type: Input }], inputWrapMode: [{ type: Input }], clearOptionSelect: [{ type: Output }], suggestions: [{ type: Input }], formatOnSelect: [{ type: Input }] } }); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0b2NvbXBsZXRlLmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL25neC1kc2ZyLWV4dC9zcmMvbGliL2F1dG9jb21wbGV0ZS9hdXRvY29tcGxldGUuY29tcG9uZW50LnRzIiwiLi4vLi4vLi4vLi4vLi4vcHJvamVjdHMvbmd4LWRzZnItZXh0L3NyYy9saWIvYXV0b2NvbXBsZXRlL2F1dG9jb21wbGV0ZS5jb21wb25lbnQuaHRtbCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsd0JBQXdCLEVBQUUsZUFBZSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDbkYsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQy9DLE9BQU8sRUFFTCx1QkFBdUIsRUFDdkIsU0FBUyxFQUNULFFBQVEsRUFDUixZQUFZLEVBQ1osVUFBVSxFQUNWLFlBQVksRUFDWixVQUFVLEVBQ1YsTUFBTSxFQUNOLEtBQUssRUFHTCxNQUFNLEVBRU4sTUFBTSxFQUVOLFNBQVMsRUFDVCxpQkFBaUIsR0FDbEIsTUFBTSxlQUFlLENBQUM7QUFDdkIsT0FBTyxFQUFFLFdBQVcsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ2hFLE9BQU8sRUFDTCx1QkFBdUIsRUFDdkIsZ0JBQWdCLEVBR2hCLG1CQUFtQixFQUNuQixZQUFZLEVBQ1osZUFBZSxFQUVmLGFBQWEsRUFDYixRQUFRLEVBQ1IsV0FBVyxHQUNaLE1BQU0scUJBQXFCLENBQUM7QUFDN0IsT0FBTyxFQUFFLFlBQVksRUFBRSxvQkFBb0IsRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBQzlFLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLHNFQUFzRSxDQUFDO0FBQ2xILE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxNQUFNLGlEQUFpRCxDQUFDO0FBQzlGLE9BQU8sRUFBRSxvQkFBb0Is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