@edugouvfr/ngx-dsfr-ext
Version:
NgxDsfrExt est une extension au package @edugouvfr/ngx-dsfr (portage Angular des éléments DSFR)
483 lines • 102 kB
JavaScript
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0b2NvbXBsZXRlLmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL25neC1kc2ZyLWV4dC9zcmMvbGliL2F1dG9jb21wbGV0ZS9hdXRvY29tcGxldGUuY29tcG9uZW50LnRzIiwiLi4vLi4vLi4vLi4vLi4vcHJvamVjdHMvbmd4LWRzZnItZXh0L3NyYy9saWIvYXV0b2NvbXBsZXRlL2F1dG9jb21wbGV0ZS5jb21wb25lbnQuaHRtbCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsd0JBQXdCLEVBQUUsZUFBZSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDbkYsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQy9DLE9BQU8sRUFFTCx1QkFBdUIsRUFDdkIsU0FBUyxFQUNULFFBQVEsRUFDUixZQUFZLEVBQ1osVUFBVSxFQUNWLFlBQVksRUFDWixVQUFVLEVBQ1YsTUFBTSxFQUNOLEtBQUssRUFHTCxNQUFNLEVBRU4sTUFBTSxFQUVOLFNBQVMsRUFDVCxpQkFBaUIsR0FDbEIsTUFBTSxlQUFlLENBQUM7QUFDdkIsT0FBTyxFQUFFLFdBQVcsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ2hFLE9BQU8sRUFDTCx1QkFBdUIsRUFDdkIsZ0JBQWdCLEVBR2hCLG1CQUFtQixFQUNuQixZQUFZLEVBQ1osZUFBZSxFQUVmLGFBQWEsRUFDYixRQUFRLEVBQ1IsV0FBVyxHQUNaLE1BQU0scUJBQXFCLENBQUM7QUFDN0IsT0FBTyxFQUFFLFlBQVksRUFBRSxvQkFBb0IsRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBQzlFLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLHNFQUFzRSxDQUFDO0FBQ2xILE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxNQUFNLGlEQUFpRCxDQUFDO0FBQzlGLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLFlBQVksQ0FBQztBQUVsRCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDakQsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLG9CQUFvQixDQUFDOzs7Ozs7QUFFakQsTUFBTSxxQkFBcUIsR0FBRyxVQUFVLENBQUM7QUFDekMsTUFBTSx3QkFBd0IsR0FBRyxHQUFHLEdBQUcscUJBQXFCLENBQUM7QUF3QjdEOzs7R0FHRztBQUNILE1BQU0sT0FBTyx5QkFDWCxTQUFRLHVCQUE0QjtJQXlKcEMsWUFBNkIsU0FBb0I7UUFDL0MsS0FBSyxFQUFFLENBQUM7UUFEbUIsY0FBUyxHQUFULFNBQVMsQ0FBVztRQXhJakQsaUlBQWlJO1FBQ3ZILGlCQUFZLEdBQUcsSUFBSSxZQUFZLEVBQXFCLENBQUM7UUFFL0Qsb0RBQW9EO1FBQzFDLHFCQUFnQixHQUFHLElBQUksWUFBWSxFQUFPLENBQUM7UUFFckQsMkNBQTJDO1FBQ2pDLGNBQVMsR0FBRyxJQUFJLFlBQVksRUFBUSxDQUFDO1FBUS9DLHdFQUF3RTtRQUMvRCxlQUFVLEdBQUcsS0FBSyxDQUFDO1FBRTVCLG1HQUFtRztRQUMxRixxQkFBZ0IsR0FBRyxLQUFLLENBQUM7UUFFbEMseUNBQXlDO1FBQ2hDLGFBQVEsR0FBRyxLQUFLLENBQUM7UUFLMUIsZ0NBQWdDO1FBQ3ZCLGtCQUFhLEdBQUcsS0FBSyxDQUFDO1FBRS9CLG9IQUFvSDtRQUMzRyxpQkFBWSxHQUFXLE9BQU8sQ0FBQztRQUV4QywrRkFBK0Y7UUFDdEYsNEJBQXVCLEdBQVcsRUFBRSxDQUFDO1FBSzlDLGtFQUFrRTtRQUN6RCxZQUFPLEdBQUcsS0FBSyxDQUFDO1FBRXpCOztXQUVHO1FBQ00sZ0JBQVcsR0FBRyxLQUFLLENBQUM7UUFXN0I7OztXQUdHO1FBQ00sVUFBSyxHQUFXLENBQUMsQ0FBQztRQUUzQix1RkFBdUY7UUFDOUUsY0FBUyxHQUFXLENBQUMsQ0FBQztRQUUvQixtR0FBbUc7UUFDbkcsMEVBQTBFO1FBQ2pFLFNBQUksR0FBc0IsTUFBTSxDQUFDO1FBQzFDLDhEQUE4RDtRQUNyRCxhQUFRLEdBQUcsS0FBSyxDQUFDO1FBZ0IxQiwyRkFBMkY7UUFDakYsaUJBQVksR0FBRyxJQUFJLFlBQVksRUFBUyxDQUFDO1FBRW5ELHVIQUF1SDtRQUV2SCxpRkFBaUY7UUFDeEUsbUJBQWMsR0FBRyxLQUFLLENBQUM7UUFTaEMsMERBQTBEO1FBQ2pELGtCQUFhLEdBQXNCLFNBQVMsQ0FBQztRQUd0RCw4RUFBOEU7UUFDckUsa0JBQWEsR0FBdUIsT0FBTyxDQUFDO1FBRXJELDhEQUE4RDtRQUNwRCxzQkFBaUIsR0FBc0QsSUFBSSxZQUFZLEVBQUUsQ0FBQztRQUsxRixlQUFVLEdBQUcsS0FBSyxDQUFDO1FBRW5CLDBCQUFxQixHQUFHLEVBQUUsQ0FBQztRQUVyQyw4RUFBOEU7UUFDcEUsa0JBQWEsR0FBRyxRQUFRLENBQUMsR0FBRyxFQUFFO1lBQ3RDLE9BQU8sSUFBSSxDQUFDLG1CQUFtQixFQUFFLEVBQUUsTUFBTSxDQUFDO1FBQzVDLENBQUMsQ0FBQyxDQUFDO1FBRUgsK0VBQStFO1FBQ3JFLHdCQUFtQixHQUFHLE1BQU0sQ0FBUSxFQUFFLENBQUMsQ0FBQztRQUUxQyxTQUFJLEdBQUcsTUFBTSxDQUFDLGVBQWUsQ0FBQyxDQUFDO1FBSy9CLGlCQUFZLEdBQUcsSUFBSSxPQUFPLEVBQVMsQ0FBQyxDQUFDLGdEQUFnRDtRQUNyRixvQkFBZSxHQUFHLElBQUksT0FBTyxFQUFRLENBQUM7UUFFOUMsNERBQTREO1FBQ3BELGlCQUFZLEdBQUcsS0FBSyxDQUFDO1FBYzdCLCtGQUErRjtRQUN0RixtQkFBYyxHQUF3QyxDQUFDLGtCQUFrQixFQUFFLEVBQUUsQ0FDcEYsSUFBSSxDQUFDLGtCQUFrQixDQUFDLENBQUMsQ0FBQyxrQkFBa0IsQ0FBQyxJQUFJLENBQUMsa0JBQWtCLENBQUMsQ0FBQyxDQUFDLENBQUMsa0JBQWtCLENBQUM7UUFaM0YsSUFBSSxDQUFDLElBQUksQ0FBQyxtQkFBbUIsQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFLFdBQVcsQ0FBQyxDQUFDO1FBQ3hELElBQUksQ0FBQyxJQUFJLENBQUMsbUJBQW1CLENBQUMsUUFBUSxDQUFDLEVBQUUsRUFBRSxXQUFXLENBQUMsQ0FBQztJQUMxRCxDQUFDO0lBRUQsNEJBQTRCO0lBQzVCLElBQWEsV0FBVyxDQUFDLFdBQWtCO1FBQ3pDLElBQUksQ0FBQyxtQkFBbUIsR0FBRyxXQUFXLENBQUM7UUFDdkMsSUFBSSxDQUFDLG1CQUFtQixDQUFDLEdBQUcsQ0FBQyxXQUFXLENBQUMsQ0FBQztJQUM1QyxDQUFDO0lBTUQsZ0JBQWdCO0lBQ1AsUUFBUTtRQUNmLEtBQUssQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUVqQixvREFBb0Q7UUFDcEQsSUFBSSxDQUFDLFlBQVk7YUFDZCxJQUFJLENBQUMsWUFBWSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsRUFBRSxvQkFBb0IsRUFBRSxFQUFFLFNBQVMsQ0FBQyxJQUFJLENBQUMsZUFBZSxDQUFDLENBQUM7YUFDdkYsU0FBUyxDQUFDLENBQUMsS0FBWSxFQUFFLEVBQUU7WUFDMUI