UNPKG

primeng

Version:

PrimeNG is an open source UI library for Angular featuring a rich set of 80+ components, a theme designer, various theme alternatives such as Material, Bootstrap, Tailwind, premium templates and professional support. In addition, it integrates with PrimeB

1,374 lines (1,369 loc) 109 kB
import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { Injectable, forwardRef, EventEmitter, inject, booleanAttribute, numberAttribute, Output, Input, Component, input, computed, signal, effect, ContentChildren, ContentChild, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, NgModule } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { deepEquals, isNotEmpty, isEmpty, uuid, findSingle, scrollInView, equals, resolveFieldData, focus, isPrintableCharacter, findLastIndex, getFirstFocusableElement, getLastFocusableElement, getFocusableElements } from '@primeuix/utils'; import * as i2 from 'primeng/api'; import { SharedModule, TranslationKeys, PrimeTemplate } from 'primeng/api'; import { AutoFocus } from 'primeng/autofocus'; import { BaseComponent } from 'primeng/basecomponent'; import { BaseInput } from 'primeng/baseinput'; import { unblockBodyScroll } from 'primeng/dom'; import { IconField } from 'primeng/iconfield'; import { CheckIcon, BlankIcon, TimesIcon, ChevronDownIcon, SearchIcon } from 'primeng/icons'; import { InputIcon } from 'primeng/inputicon'; import { InputText } from 'primeng/inputtext'; import { Overlay } from 'primeng/overlay'; import { Ripple } from 'primeng/ripple'; import { Scroller } from 'primeng/scroller'; import { Tooltip } from 'primeng/tooltip'; import { style } from '@primeuix/styles/select'; import { BaseStyle } from 'primeng/base'; const theme = /*css*/ ` ${style} /* For PrimeNG */ .p-select-label.p-placeholder { color: dt('select.placeholder.color'); } .p-select.ng-invalid.ng-dirty { border-color: dt('select.invalid.border.color'); } .p-dropdown.ng-invalid.ng-dirty .p-dropdown-label.p-placeholder, .p-select.ng-invalid.ng-dirty .p-select-label.p-placeholder { color: dt('select.invalid.placeholder.color'); } `; const classes = { root: ({ instance }) => [ 'p-select p-component p-inputwrapper', { 'p-disabled': instance.$disabled(), 'p-variant-filled': instance.$variant() === 'filled', 'p-focus': instance.focused, 'p-invalid': instance.invalid(), 'p-inputwrapper-filled': instance.$filled(), 'p-inputwrapper-focus': instance.focused || instance.overlayVisible, 'p-select-open': instance.overlayVisible, 'p-select-fluid': instance.hasFluid, 'p-select-sm p-inputfield-sm': instance.size() === 'small', 'p-select-lg p-inputfield-lg': instance.size() === 'large' } ], label: ({ instance }) => [ 'p-select-label', { 'p-placeholder': instance.placeholder() && instance.label() === instance.placeholder(), 'p-select-label-empty': !instance.editable && !instance.selectedItemTemplate && (instance.label() === undefined || instance.label() === null || instance.label() === 'p-emptylabel' || instance.label().length === 0) } ], clearIcon: 'p-select-clear-icon', dropdown: 'p-select-dropdown', loadingIcon: 'p-select-loading-icon', dropdownIcon: 'p-select-dropdown-icon', overlay: 'p-select-overlay p-component-overlay p-component', header: 'p-select-header', pcFilter: 'p-select-filter', listContainer: 'p-select-list-container', list: 'p-select-list', optionGroup: 'p-select-option-group', optionGroupLabel: 'p-select-option-group-label', option: ({ instance }) => [ 'p-select-option', { 'p-select-option-selected': instance.selected && !instance.checkmark, 'p-disabled': instance.disabled, 'p-focus': instance.focused } ], optionLabel: 'p-select-option-label', optionCheckIcon: 'p-select-option-check-icon', optionBlankIcon: 'p-select-option-blank-icon', emptyMessage: 'p-select-empty-message' }; class SelectStyle extends BaseStyle { name = 'select'; theme = theme; classes = classes; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SelectStyle, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SelectStyle }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SelectStyle, decorators: [{ type: Injectable }] }); /** * * Select also known as Select, is used to choose an item from a collection of options. * * [Live Demo](https://www.primeng.org/select/) * * @module selectstyle * */ var SelectClasses; (function (SelectClasses) { /** * Class name of the root element */ SelectClasses["root"] = "p-select"; /** * Class name of the label element */ SelectClasses["label"] = "p-select-label"; /** * Class name of the clear icon element */ SelectClasses["clearIcon"] = "p-select-clear-icon"; /** * Class name of the dropdown element */ SelectClasses["dropdown"] = "p-select-dropdown"; /** * Class name of the loadingicon element */ SelectClasses["loadingIcon"] = "p-select-loading-icon"; /** * Class name of the dropdown icon element */ SelectClasses["dropdownIcon"] = "p-select-dropdown-icon"; /** * Class name of the overlay element */ SelectClasses["overlay"] = "p-select-overlay"; /** * Class name of the header element */ SelectClasses["header"] = "p-select-header"; /** * Class name of the filter element */ SelectClasses["pcFilter"] = "p-select-filter"; /** * Class name of the list container element */ SelectClasses["listContainer"] = "p-select-list-container"; /** * Class name of the list element */ SelectClasses["list"] = "p-select-list"; /** * Class name of the option group element */ SelectClasses["optionGroup"] = "p-select-option-group"; /** * Class name of the option group label element */ SelectClasses["optionGroupLabel"] = "p-select-option-group-label"; /** * Class name of the option element */ SelectClasses["option"] = "p-select-option"; /** * Class name of the option label element */ SelectClasses["optionLabel"] = "p-select-option-label"; /** * Class name of the option check icon element */ SelectClasses["optionCheckIcon"] = "p-select-option-check-icon"; /** * Class name of the option blank icon element */ SelectClasses["optionBlankIcon"] = "p-select-option-blank-icon"; /** * Class name of the empty message element */ SelectClasses["emptyMessage"] = "p-select-empty-message"; })(SelectClasses || (SelectClasses = {})); const SELECT_VALUE_ACCESSOR = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Select), multi: true }; class SelectItem extends BaseComponent { id; option; selected; focused; label; disabled; visible; itemSize; ariaPosInset; ariaSetSize; template; checkmark; onClick = new EventEmitter(); onMouseEnter = new EventEmitter(); _componentStyle = inject(SelectStyle); onOptionClick(event) { this.onClick.emit(event); } onOptionMouseEnter(event) { this.onMouseEnter.emit(event); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SelectItem, deps: null, target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "20.1.3", type: SelectItem, isStandalone: true, selector: "p-selectItem", inputs: { id: "id", option: "option", selected: ["selected", "selected", booleanAttribute], focused: ["focused", "focused", booleanAttribute], label: "label", disabled: ["disabled", "disabled", booleanAttribute], visible: ["visible", "visible", booleanAttribute], itemSize: ["itemSize", "itemSize", numberAttribute], ariaPosInset: "ariaPosInset", ariaSetSize: "ariaSetSize", template: "template", checkmark: ["checkmark", "checkmark", booleanAttribute] }, outputs: { onClick: "onClick", onMouseEnter: "onMouseEnter" }, providers: [SelectStyle], usesInheritance: true, ngImport: i0, template: ` <li [id]="id" (click)="onOptionClick($event)" (mouseenter)="onOptionMouseEnter($event)" role="option" pRipple [attr.aria-label]="label" [attr.aria-setsize]="ariaSetSize" [attr.aria-posinset]="ariaPosInset" [attr.aria-selected]="selected" [attr.data-p-focused]="focused" [attr.data-p-highlight]="selected" [attr.data-p-disabled]="disabled" [ngStyle]="{ height: itemSize + 'px' }" [class]="cx('option')" > <ng-container *ngIf="checkmark"> <svg data-p-icon="check" *ngIf="selected" [class]="cx('optionCheckIcon')" /> <svg data-p-icon="blank" *ngIf="!selected" [class]="cx('optionBlankIcon')" /> </ng-container> <span *ngIf="!template">{{ label ?? 'empty' }}</span> <ng-container *ngTemplateOutlet="template; context: { $implicit: option }"></ng-container> </li> `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: Ripple, selector: "[pRipple]" }, { kind: "component", type: CheckIcon, selector: "[data-p-icon=\"check\"]" }, { kind: "component", type: BlankIcon, selector: "[data-p-icon=\"blank\"]" }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SelectItem, decorators: [{ type: Component, args: [{ selector: 'p-selectItem', standalone: true, imports: [CommonModule, SharedModule, Ripple, CheckIcon, BlankIcon], template: ` <li [id]="id" (click)="onOptionClick($event)" (mouseenter)="onOptionMouseEnter($event)" role="option" pRipple [attr.aria-label]="label" [attr.aria-setsize]="ariaSetSize" [attr.aria-posinset]="ariaPosInset" [attr.aria-selected]="selected" [attr.data-p-focused]="focused" [attr.data-p-highlight]="selected" [attr.data-p-disabled]="disabled" [ngStyle]="{ height: itemSize + 'px' }" [class]="cx('option')" > <ng-container *ngIf="checkmark"> <svg data-p-icon="check" *ngIf="selected" [class]="cx('optionCheckIcon')" /> <svg data-p-icon="blank" *ngIf="!selected" [class]="cx('optionBlankIcon')" /> </ng-container> <span *ngIf="!template">{{ label ?? 'empty' }}</span> <ng-container *ngTemplateOutlet="template; context: { $implicit: option }"></ng-container> </li> `, providers: [SelectStyle] }] }], propDecorators: { id: [{ type: Input }], option: [{ type: Input }], selected: [{ type: Input, args: [{ transform: booleanAttribute }] }], focused: [{ type: Input, args: [{ transform: booleanAttribute }] }], label: [{ type: Input }], disabled: [{ type: Input, args: [{ transform: booleanAttribute }] }], visible: [{ type: Input, args: [{ transform: booleanAttribute }] }], itemSize: [{ type: Input, args: [{ transform: numberAttribute }] }], ariaPosInset: [{ type: Input }], ariaSetSize: [{ type: Input }], template: [{ type: Input }], checkmark: [{ type: Input, args: [{ transform: booleanAttribute }] }], onClick: [{ type: Output }], onMouseEnter: [{ type: Output }] } }); /** * Select is used to choose an item from a collection of options. * @group Components */ class Select extends BaseInput { zone; filterService; /** * Unique identifier of the component * @group Props */ id; /** * Height of the viewport in pixels, a scrollbar is defined if height of list exceeds this value. * @group Props */ scrollHeight = '200px'; /** * When specified, displays an input field to filter the items on keyup. * @group Props */ filter; /** * Inline style of the overlay panel element. * @group Props */ panelStyle; /** * Style class of the element. * @deprecated since v20.0.0, use `class` instead. * @group Props */ styleClass; /** * Style class of the overlay panel element. * @group Props */ panelStyleClass; /** * When present, it specifies that the component cannot be edited. * @group Props */ readonly; /** * When present, custom value instead of predefined options can be entered using the editable input field. * @group Props */ editable; /** * Index of the element in tabbing order. * @group Props */ tabindex = 0; /** * Default text to display when no option is selected. * @group Props */ set placeholder(val) { this._placeholder.set(val); } get placeholder() { return this._placeholder.asReadonly(); } /** * Icon to display in loading state. * @group Props */ loadingIcon; /** * Placeholder text to show when filter input is empty. * @group Props */ filterPlaceholder; /** * Locale to use in filtering. The default locale is the host environment's current locale. * @group Props */ filterLocale; /** * Identifier of the accessible input element. * @group Props */ inputId; /** * A property to uniquely identify a value in options. * @group Props */ dataKey; /** * When filtering is enabled, filterBy decides which field or fields (comma separated) to search against. * @group Props */ filterBy; /** * Fields used when filtering the options, defaults to optionLabel. * @group Props */ filterFields; /** * When present, it specifies that the component should automatically get focus on load. * @group Props */ autofocus; /** * Clears the filter value when hiding the select. * @group Props */ resetFilterOnHide = false; /** * Whether the selected option will be shown with a check mark. * @group Props */ checkmark = false; /** * Icon class of the select icon. * @group Props */ dropdownIcon; /** * Whether the select is in loading state. * @group Props */ loading = false; /** * Name of the label field of an option. * @group Props */ optionLabel; /** * Name of the value field of an option. * @group Props */ optionValue; /** * Name of the disabled field of an option. * @group Props */ optionDisabled; /** * Name of the label field of an option group. * @group Props */ optionGroupLabel = 'label'; /** * Name of the options field of an option group. * @group Props */ optionGroupChildren = 'items'; /** * Whether to display options as grouped when nested options are provided. * @group Props */ group; /** * When enabled, a clear icon is displayed to clear the value. * @group Props */ showClear; /** * Text to display when filtering does not return any results. Defaults to global value in i18n translation configuration. * @group Props */ emptyFilterMessage = ''; /** * Text to display when there is no data. Defaults to global value in i18n translation configuration. * @group Props */ emptyMessage = ''; /** * Defines if data is loaded and interacted with in lazy manner. * @group Props */ lazy = false; /** * Whether the data should be loaded on demand during scroll. * @group Props */ virtualScroll; /** * Height of an item in the list for VirtualScrolling. * @group Props */ virtualScrollItemSize; /** * Whether to use the scroller feature. The properties of scroller component can be used like an object in it. * @group Props */ virtualScrollOptions; /** * Whether to use overlay API feature. The properties of overlay API can be used like an object in it. * @group Props */ overlayOptions; /** * Defines a string that labels the filter input. * @group Props */ ariaFilterLabel; /** * Used to define a aria label attribute the current element. * @group Props */ ariaLabel; /** * Establishes relationships between the component and label(s) where its value should be one or more element IDs. * @group Props */ ariaLabelledBy; /** * Defines how the items are filtered. * @group Props */ filterMatchMode = 'contains'; /** * Advisory information to display in a tooltip on hover. * @group Props */ tooltip = ''; /** * Position of the tooltip. * @group Props */ tooltipPosition = 'right'; /** * Type of CSS position. * @group Props */ tooltipPositionStyle = 'absolute'; /** * Style class of the tooltip. * @group Props */ tooltipStyleClass; /** * Fields used when filtering the options, defaults to optionLabel. * @group Props */ focusOnHover = true; /** * Determines if the option will be selected on focus. * @group Props */ selectOnFocus = false; /** * Whether to focus on the first visible or selected element when the overlay panel is shown. * @group Props */ autoOptionFocus = false; /** * Applies focus to the filter element when the overlay is shown. * @group Props */ autofocusFilter = true; /** * When specified, filter displays with this value. * @group Props */ get filterValue() { return this._filterValue(); } set filterValue(val) { setTimeout(() => { this._filterValue.set(val); }); } /** * An array of objects to display as the available options. * @group Props */ get options() { const options = this._options(); return options; } set options(val) { if (!deepEquals(val, this._options())) { this._options.set(val); } } /** * Target element to attach the overlay, valid values are "body" or a local ng-template variable of another element (note: use binding with brackets for template variables, e.g. [appendTo]="mydiv" for a div element having #mydiv as variable name). * @defaultValue 'self' * @group Props */ appendTo = input(undefined, ...(ngDevMode ? [{ debugName: "appendTo" }] : [])); /** * Callback to invoke when value of select changes. * @param {SelectChangeEvent} event - custom change event. * @group Emits */ onChange = new EventEmitter(); /** * Callback to invoke when data is filtered. * @param {SelectFilterEvent} event - custom filter event. * @group Emits */ onFilter = new EventEmitter(); /** * Callback to invoke when select gets focus. * @param {Event} event - Browser event. * @group Emits */ onFocus = new EventEmitter(); /** * Callback to invoke when select loses focus. * @param {Event} event - Browser event. * @group Emits */ onBlur = new EventEmitter(); /** * Callback to invoke when component is clicked. * @param {MouseEvent} event - Mouse event. * @group Emits */ onClick = new EventEmitter(); /** * Callback to invoke when select overlay gets visible. * @param {AnimationEvent} event - Animation event. * @group Emits */ onShow = new EventEmitter(); /** * Callback to invoke when select overlay gets hidden. * @param {AnimationEvent} event - Animation event. * @group Emits */ onHide = new EventEmitter(); /** * Callback to invoke when select clears the value. * @param {Event} event - Browser event. * @group Emits */ onClear = new EventEmitter(); /** * Callback to invoke in lazy mode to load new data. * @param {SelectLazyLoadEvent} event - Lazy load event. * @group Emits */ onLazyLoad = new EventEmitter(); _componentStyle = inject(SelectStyle); filterViewChild; focusInputViewChild; editableInputViewChild; itemsViewChild; scroller; overlayViewChild; firstHiddenFocusableElementOnOverlay; lastHiddenFocusableElementOnOverlay; itemsWrapper; $appendTo = computed(() => this.appendTo() || this.config.overlayAppendTo(), ...(ngDevMode ? [{ debugName: "$appendTo" }] : [])); /** * Custom item template. * @group Templates */ itemTemplate; /** * Custom group template. * @group Templates */ groupTemplate; /** * Custom loader template. * @group Templates */ loaderTemplate; /** * Custom selected item template. * @group Templates */ selectedItemTemplate; /** * Custom header template. * @group Templates */ headerTemplate; /** * Custom filter template. * @group Templates */ filterTemplate; /** * Custom footer template. * @group Templates */ footerTemplate; /** * Custom empty filter template. * @group Templates */ emptyFilterTemplate; /** * Custom empty template. * @group Templates */ emptyTemplate; /** * Custom dropdown icon template. * @group Templates */ dropdownIconTemplate; /** * Custom loading icon template. * @group Templates */ loadingIconTemplate; /** * Custom clear icon template. * @group Templates */ clearIconTemplate; /** * Custom filter icon template. * @group Templates */ filterIconTemplate; /** * Custom on icon template. * @group Templates */ onIconTemplate; /** * Custom off icon template. * @group Templates */ offIconTemplate; /** * Custom cancel icon template. * @group Templates */ cancelIconTemplate; templates; _itemTemplate; _selectedItemTemplate; _headerTemplate; _filterTemplate; _footerTemplate; _emptyFilterTemplate; _emptyTemplate; _groupTemplate; _loaderTemplate; _dropdownIconTemplate; _loadingIconTemplate; _clearIconTemplate; _filterIconTemplate; _cancelIconTemplate; _onIconTemplate; _offIconTemplate; filterOptions; _options = signal(null, ...(ngDevMode ? [{ debugName: "_options" }] : [])); _placeholder = signal(undefined, ...(ngDevMode ? [{ debugName: "_placeholder" }] : [])); value; hover; focused; overlayVisible; optionsChanged; panel; dimensionsUpdated; hoveredItem; selectedOptionUpdated; _filterValue = signal(null, ...(ngDevMode ? [{ debugName: "_filterValue" }] : [])); searchValue; searchIndex; searchTimeout; previousSearchChar; currentSearchChar; preventModelTouched; focusedOptionIndex = signal(-1, ...(ngDevMode ? [{ debugName: "focusedOptionIndex" }] : [])); labelId; listId; clicked = signal(false, ...(ngDevMode ? [{ debugName: "clicked" }] : [])); get emptyMessageLabel() { return this.emptyMessage || this.config.getTranslation(TranslationKeys.EMPTY_MESSAGE); } get emptyFilterMessageLabel() { return this.emptyFilterMessage || this.config.getTranslation(TranslationKeys.EMPTY_FILTER_MESSAGE); } get isVisibleClearIcon() { return this.modelValue() != null && this.hasSelectedOption() && this.showClear && !this.$disabled(); } get listLabel() { return this.config.getTranslation(TranslationKeys.ARIA)['listLabel']; } get focusedOptionId() { return this.focusedOptionIndex() !== -1 ? `${this.id}_${this.focusedOptionIndex()}` : null; } visibleOptions = computed(() => { const options = this.getAllVisibleAndNonVisibleOptions(); if (this._filterValue()) { const _filterBy = this.filterBy || this.optionLabel; const filteredOptions = !_filterBy && !this.filterFields && !this.optionValue ? this.options.filter((option) => { if (option.label) { return option.label.toString().toLowerCase().indexOf(this._filterValue().toLowerCase().trim()) !== -1; } return option.toString().toLowerCase().indexOf(this._filterValue().toLowerCase().trim()) !== -1; }) : this.filterService.filter(options, this.searchFields(), this._filterValue().trim(), this.filterMatchMode, this.filterLocale); if (this.group) { const optionGroups = this.options || []; const filtered = []; optionGroups.forEach((group) => { const groupChildren = this.getOptionGroupChildren(group); const filteredItems = groupChildren.filter((item) => filteredOptions.includes(item)); if (filteredItems.length > 0) filtered.push({ ...group, [typeof this.optionGroupChildren === 'string' ? this.optionGroupChildren : 'items']: [...filteredItems] }); }); return this.flatOptions(filtered); } return filteredOptions; } return options; }, ...(ngDevMode ? [{ debugName: "visibleOptions" }] : [])); label = computed(() => { // use getAllVisibleAndNonVisibleOptions verses just visible options // this will find the selected option whether or not the user is currently filtering because the filtered (i.e. visible) options, are a subset of all the options const options = this.getAllVisibleAndNonVisibleOptions(); // use isOptionEqualsModelValue for the use case where the dropdown is initalized with a disabled option const selectedOptionIndex = options.findIndex((option) => this.isOptionValueEqualsModelValue(option)); return selectedOptionIndex !== -1 ? this.getOptionLabel(options[selectedOptionIndex]) : this.placeholder() || 'p-emptylabel'; }, ...(ngDevMode ? [{ debugName: "label" }] : [])); selectedOption; constructor(zone, filterService) { super(); this.zone = zone; this.filterService = filterService; effect(() => { const modelValue = this.modelValue(); const visibleOptions = this.visibleOptions(); if (visibleOptions && isNotEmpty(visibleOptions)) { const selectedOptionIndex = this.findSelectedOptionIndex(); if (selectedOptionIndex !== -1 || modelValue === undefined || (typeof modelValue === 'string' && modelValue.length === 0) || this.isModelValueNotSet() || this.editable) { this.selectedOption = visibleOptions[selectedOptionIndex]; } } if (isEmpty(visibleOptions) && (modelValue === undefined || this.isModelValueNotSet()) && isNotEmpty(this.selectedOption)) { this.selectedOption = null; } if (modelValue !== undefined && this.editable) { this.updateEditableLabel(); } this.cd.markForCheck(); }); } isModelValueNotSet() { return this.modelValue() === null && !this.isOptionValueEqualsModelValue(this.selectedOption); } getAllVisibleAndNonVisibleOptions() { return this.group ? this.flatOptions(this.options) : this.options || []; } ngOnInit() { super.ngOnInit(); this.id = this.id || uuid('pn_id_'); this.autoUpdateModel(); if (this.filterBy) { this.filterOptions = { filter: (value) => this.onFilterInputChange(value), reset: () => this.resetFilter() }; } } ngAfterContentInit() { this.templates.forEach((item) => { switch (item.getType()) { case 'item': this._itemTemplate = item.template; break; case 'selectedItem': this._selectedItemTemplate = item.template; break; case 'header': this._headerTemplate = item.template; break; case 'filter': this._filterTemplate = item.template; break; case 'footer': this._footerTemplate = item.template; break; case 'emptyfilter': this._emptyFilterTemplate = item.template; break; case 'empty': this._emptyTemplate = item.template; break; case 'group': this._groupTemplate = item.template; break; case 'loader': this._loaderTemplate = item.template; break; case 'dropdownicon': this._dropdownIconTemplate = item.template; break; case 'loadingicon': this._loadingIconTemplate = item.template; break; case 'clearicon': this._clearIconTemplate = item.template; break; case 'filtericon': this._filterIconTemplate = item.template; break; case 'cancelicon': this._cancelIconTemplate = item.template; break; case 'onicon': this._onIconTemplate = item.template; break; case 'officon': this._offIconTemplate = item.template; break; default: this._itemTemplate = item.template; break; } }); } ngAfterViewChecked() { if (this.optionsChanged && this.overlayVisible) { this.optionsChanged = false; this.zone.runOutsideAngular(() => { setTimeout(() => { if (this.overlayViewChild) { this.overlayViewChild.alignOverlay(); } }, 1); }); } if (this.selectedOptionUpdated && this.itemsWrapper) { let selectedItem = findSingle(this.overlayViewChild?.overlayViewChild?.nativeElement, 'li.p-select-option-selected'); if (selectedItem) { scrollInView(this.itemsWrapper, selectedItem); } this.selectedOptionUpdated = false; } } flatOptions(options) { return (options || []).reduce((result, option, index) => { result.push({ optionGroup: option, group: true, index }); const optionGroupChildren = this.getOptionGroupChildren(option); optionGroupChildren && optionGroupChildren.forEach((o) => result.push(o)); return result; }, []); } autoUpdateModel() { if (this.selectOnFocus && this.autoOptionFocus && !this.hasSelectedOption()) { this.focusedOptionIndex.set(this.findFirstFocusedOptionIndex()); this.onOptionSelect(null, this.visibleOptions()[this.focusedOptionIndex()], false); } } onOptionSelect(event, option, isHide = true, preventChange = false) { if (!this.isSelected(option)) { const value = this.getOptionValue(option); this.updateModel(value, event); this.focusedOptionIndex.set(this.findSelectedOptionIndex()); preventChange === false && this.onChange.emit({ originalEvent: event, value: value }); } if (isHide) { this.hide(true); } } onOptionMouseEnter(event, index) { if (this.focusOnHover) { this.changeFocusedOptionIndex(event, index); } } updateModel(value, event) { this.value = value; this.onModelChange(value); this.writeModelValue(value); this.selectedOptionUpdated = true; } allowModelChange() { return !!this.modelValue() && !this.placeholder() && (this.modelValue() === undefined || this.modelValue() === null) && !this.editable && this.options && this.options.length; } isSelected(option) { return this.isOptionValueEqualsModelValue(option); } isOptionValueEqualsModelValue(option) { return this.isValidOption(option) && equals(this.modelValue(), this.getOptionValue(option), this.equalityKey()); } ngAfterViewInit() { super.ngAfterViewInit(); if (this.editable) { this.updateEditableLabel(); } this.updatePlaceHolderForFloatingLabel(); } updatePlaceHolderForFloatingLabel() { const parentElement = this.el.nativeElement.parentElement; const isInFloatingLabel = parentElement?.classList.contains('p-float-label'); if (parentElement && isInFloatingLabel && !this.selectedOption) { const label = parentElement.querySelector('label'); if (label) { this._placeholder.set(label.textContent); } } } updateEditableLabel() { if (this.editableInputViewChild) { this.editableInputViewChild.nativeElement.value = this.getOptionLabel(this.selectedOption) || this.modelValue() || ''; } } clearEditableLabel() { if (this.editableInputViewChild) { this.editableInputViewChild.nativeElement.value = ''; } } getOptionIndex(index, scrollerOptions) { return this.virtualScrollerDisabled ? index : scrollerOptions && scrollerOptions.getItemOptions(index)['index']; } getOptionLabel(option) { return this.optionLabel !== undefined && this.optionLabel !== null ? resolveFieldData(option, this.optionLabel) : option && option.label !== undefined ? option.label : option; } getOptionValue(option) { return this.optionValue && this.optionValue !== null ? resolveFieldData(option, this.optionValue) : !this.optionLabel && option && option.value !== undefined ? option.value : option; } isSelectedOptionEmpty() { return isEmpty(this.selectedOption); } isOptionDisabled(option) { if (this.getOptionValue(this.modelValue()) === this.getOptionValue(option) || (this.getOptionLabel(this.modelValue() === this.getOptionLabel(option)) && option.disabled === false)) { return false; } else { return this.optionDisabled ? resolveFieldData(option, this.optionDisabled) : option && option.disabled !== undefined ? option.disabled : false; } } getOptionGroupLabel(optionGroup) { return this.optionGroupLabel !== undefined && this.optionGroupLabel !== null ? resolveFieldData(optionGroup, this.optionGroupLabel) : optionGroup && optionGroup.label !== undefined ? optionGroup.label : optionGroup; } getOptionGroupChildren(optionGroup) { return this.optionGroupChildren !== undefined && this.optionGroupChildren !== null ? resolveFieldData(optionGroup, this.optionGroupChildren) : optionGroup.items; } getAriaPosInset(index) { return ((this.optionGroupLabel ? index - this.visibleOptions() .slice(0, index) .filter((option) => this.isOptionGroup(option)).length : index) + 1); } get ariaSetSize() { return this.visibleOptions().filter((option) => !this.isOptionGroup(option)).length; } /** * Callback to invoke on filter reset. * @group Method */ resetFilter() { this._filterValue.set(null); if (this.filterViewChild && this.filterViewChild.nativeElement) { this.filterViewChild.nativeElement.value = ''; } } onContainerClick(event) { if (this.$disabled() || this.readonly || this.loading) { return; } this.focusInputViewChild?.nativeElement.focus({ preventScroll: true }); if (event.target.tagName === 'INPUT' || event.target.getAttribute('data-pc-section') === 'clearicon' || event.target.closest('[data-pc-section="clearicon"]')) { return; } else if (!this.overlayViewChild || !this.overlayViewChild.el.nativeElement.contains(event.target)) { this.overlayVisible ? this.hide(true) : this.show(true); } this.onClick.emit(event); this.clicked.set(true); this.cd.detectChanges(); } isEmpty() { return !this._options() || (this.visibleOptions() && this.visibleOptions().length === 0); } onEditableInput(event) { const value = event.target.value; this.searchValue = ''; const matched = this.searchOptions(event, value); !matched && this.focusedOptionIndex.set(-1); this.onModelChange(value); this.updateModel(value || null, event); setTimeout(() => { this.onChange.emit({ originalEvent: event, value: value }); }, 1); !this.overlayVisible && isNotEmpty(value) && this.show(); } /** * Displays the panel. * @group Method */ show(isFocus) { this.overlayVisible = true; this.focusedOptionIndex.set(this.focusedOptionIndex() !== -1 ? this.focusedOptionIndex() : this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : this.editable ? -1 : this.findSelectedOptionIndex()); if (isFocus) { focus(this.focusInputViewChild?.nativeElement); } this.cd.markForCheck(); } onOverlayAnimationStart(event) { if (event.toState === 'visible') { this.itemsWrapper = findSingle(this.overlayViewChild?.overlayViewChild?.nativeElement, this.virtualScroll ? '.p-scroller' : '.p-select-list-container'); this.virtualScroll && this.scroller?.setContentEl(this.itemsViewChild?.nativeElement); if (this.options && this.options.length) { if (this.virtualScroll) { const selectedIndex = this.modelValue() ? this.focusedOptionIndex() : -1; if (selectedIndex !== -1) { this.scroller?.scrollToIndex(selectedIndex); } } else { let selectedListItem = findSingle(this.itemsWrapper, '.p-select-option.p-select-option-selected'); if (selectedListItem) { selectedListItem.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } } } if (this.filterViewChild && this.filterViewChild.nativeElement) { this.preventModelTouched = true; if (this.autofocusFilter && !this.editable) { this.filterViewChild.nativeElement.focus(); } } this.onShow.emit(event); } if (event.toState === 'void') { this.itemsWrapper = null; this.onModelTouched(); this.onHide.emit(event); } } /** * Hides the panel. * @group Method */ hide(isFocus) { this.overlayVisible = false; this.focusedOptionIndex.set(-1); this.clicked.set(false); this.searchValue = ''; if (this.overlayOptions?.mode === 'modal') { unblockBodyScroll(); } if (this.filter && this.resetFilterOnHide) { this.resetFilter(); } if (isFocus) { if (this.focusInputViewChild) { focus(this.focusInputViewChild?.nativeElement); } if (this.editable && this.editableInputViewChild) { focus(this.editableInputViewChild?.nativeElement); } } this.cd.markForCheck(); } onInputFocus(event) { if (this.$disabled()) { // For ScreenReaders return; } this.focused = true; const focusedOptionIndex = this.focusedOptionIndex() !== -1 ? this.focusedOptionIndex() : this.overlayVisible && this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1; this.focusedOptionIndex.set(focusedOptionIndex); this.overlayVisible && this.scrollInView(this.focusedOptionIndex()); this.onFocus.emit(event); } onInputBlur(event) { this.focused = false; this.onBlur.emit(event); if (!this.preventModelTouched) { this.onModelTouched(); } this.preventModelTouched = false; } onKeyDown(event, search = false) { if (this.$disabled() || this.readonly || this.loading) { return; } switch (event.code) { //down case 'ArrowDown': this.onArrowDownKey(event); break; //up case 'ArrowUp': this.onArrowUpKey(event, this.editable); break; case 'ArrowLeft': case 'ArrowRight': this.onArrowLeftKey(event, this.editable); break; case 'Delete': this.onDeleteKey(event); break; case 'Home': this.onHomeKey(event, this.editable); break; case 'End': this.onEndKey(event, this.editable); break; case 'PageDown': this.onPageDownKey(event); break; case 'PageUp': this.onPageUpKey(event); break; //space case 'Space': this.onSpaceKey(event, search); break; //enter case 'Enter': case 'NumpadEnter': this.onEnterKey(event); break; //escape and tab case 'Escape': this.onEscapeKey(event); break; case 'Tab': this.onTabKey(event); break; case 'Backspace': this.onBackspaceKey(event, this.editable); break; case 'ShiftLeft': case 'ShiftRight': //NOOP break; default: if (!event.metaKey && isPrintableCharacter(event.key)) { !this.overlayVisible && this.show(); !this.editable && this.searchOptions(event, event.key); } break; } this.clicked.set(false); } onFilterKeyDown(event) { switch (event.code) { case 'ArrowDown': this.onArrowDownKey(event); break; case 'ArrowUp': this.onArrowUpKey(event, true); break; case 'ArrowLeft': case 'ArrowRight': this.onArrowLeftKey(event, true); break; case 'Home': this.onHomeKey(event, true); break; case 'End': this.onEndKey(event, true); break; case 'Enter': case 'NumpadEnter': this.onEnterKey(event, true); break; case 'Escape': this.onEscapeKey(event); break; case 'Tab': this.onTabKey(event, true); break; default: break; } } onFilterBlur(event) { this.focusedOptionIndex.set(-1); } onArrowDownKey(event) { if (!this.overlayVisible) { this.show(); this.editable && this.changeFocusedOptionIndex(event, this.findSelectedOptionIndex()); } else { const optionIndex = this.focusedOptionIndex() !== -1 ? this.findNextOptionIndex(this.focusedOptionIndex()) : this.clicked() ? this.findFirstOptionIndex() : this.findFirstFocusedOptionIndex(); this.changeFocusedOptionIndex(event, optionIndex); } // const optionIndex = this.focusedOptionIndex() !== -1 ? this.findNextOptionIndex(this.focusedOptionIndex()) : this.findFirstFocusedOptionIndex(); // this.changeFocusedOptionIndex(event, optionIndex); // !this.overlayVisible && this.show(); event.preventDefault(); event.stopPropagation(); } changeFocusedOptionIndex(event, index) { if (this.focusedOptionIndex() !== index) { this.focusedOptionIndex.set(index); this.scrollInView(); if (this.selectOnFocus) { const option = this.visibleOptions()[index]; this.onOptionSelect(event, option, false); } } } get virtualScrollerDisabled() { return !this.virtualScroll; } scrollInView(index = -1) { const id = index !== -1 ? `${this.id}_${index}` : this.focusedOptionId; if (this.itemsViewChild && this.itemsViewChild.nativeElement) { const element = findSingle(this.itemsViewChild.nativeElement, `li[id="${id}"]`); if (element) { element.scrollIntoView && element.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } else if (!this.virtualScrollerDisabled) { setTimeout(() => { this.virtualScroll && this.scroller?.scrollToIndex(index !== -1 ? index : this.focusedOptionIndex()); }, 0); } } } hasSelectedOption() { return this.modelValue() !== undefined; } isValidSelectedOption(option) { return this.isValidOption(option) && this.isSelected(option); } equalityKey() { return this.optionValue ? null : this.dataKey; } findFirstFocusedOptionIndex() { const selectedIndex = this.findSelectedOptionIndex(); return selectedIndex < 0 ? this.findFirstOptionIndex() : selectedIndex; } findFirstOptionIndex() { return this.visibleOptions().findIndex((option) => this.isValidOption(option)); } findSelectedOptionIndex() { return this.hasSelectedOption() ? this.visibleOptions().findIndex((option) => this.isValidSelectedOption(option)) : -1; } findNextOptionIndex(index) { const matchedOptionIndex = index < this.visibleOptions().length - 1 ? this.visibleOptions() .slice(index + 1) .findIndex((option) => this.isValidOption(option)) : -1; return matchedOptionIndex > -1 ? matchedOptionIndex + index + 1 : index; } findPrevOptionIndex(index) { const matchedOptionIndex = index > 0 ? findLastIndex(this.visibleOptions().slice(0, index), (option) => this.isValidOption(option)) : -1; return matchedOptionIndex > -1 ? matchedOptionIndex : index; } findLastOptionIndex() { return findLastIndex(this.visibleOptions(), (option) => this.isValidOption(option)); } findLastFocusedOptionIndex() { const selectedIndex = this.findSelectedOptionIndex(); return selectedIndex < 0 ? this.findLastOptionIndex() : selectedIndex; } isValidOption(option) { return option !== undefined && option !== null && !(this.isOptionDisabled(option) || this.isOptionGroup(option)); } isOptionGroup(option) { return this.optionGroupLabel !== undefined && this.optionGroupLabel !== null && option.optionGroup !== undefined && option.optionGroup !== null && option.group; } onArrowUpKey(event, pressedInInputText = false) { if (event.altKey && !pressedInInputText) { if (this.focusedOptionIndex() !== -1) { const option = this.visibleOptions()[this.focusedOptionIndex()]; this.onOptionSelect(event, option); } this.overlayVisible && this.hide(); } else { const optionIndex = this.focusedOptionIndex() !== -1 ? this.findPrevOptionIndex(this.focusedOptionIndex()) : this.clicked() ? this.findLastOpt