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,484 lines (1,447 loc) 116 kB
import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { Injectable, forwardRef, EventEmitter, booleanAttribute, numberAttribute, Output, Input, Component, inject, signal, computed, effect, ContentChildren, ContentChild, HostBinding, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, NgModule } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { deepEquals, isNotEmpty, isEmpty, uuid, findSingle, scrollInView, equals, resolveFieldData, focus, unblockBodyScroll, 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 { 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 { BaseStyle } from 'primeng/base'; const theme = ({ dt }) => ` .p-select { display: inline-flex; cursor: pointer; position: relative; user-select: none; background: ${dt('select.background')}; border: 1px solid ${dt('select.border.color')}; transition: background ${dt('select.transition.duration')}, color ${dt('select.transition.duration')}, border-color ${dt('select.transition.duration')}, outline-color ${dt('select.transition.duration')}, box-shadow ${dt('select.transition.duration')}; border-radius: ${dt('select.border.radius')}; outline-color: transparent; box-shadow: ${dt('select.shadow')}; } .p-select.ng-invalid.ng-dirty { border-color: ${dt('select.invalid.border.color')}; } .p-select:not(.p-disabled):hover { border-color: ${dt('select.hover.border.color')}; } .p-select:not(.p-disabled).p-focus { border-color: ${dt('select.focus.border.color')}; box-shadow: ${dt('select.focus.ring.shadow')}; outline: ${dt('select.focus.ring.width')} ${dt('select.focus.ring.style')} ${dt('select.focus.ring.color')}; outline-offset: ${dt('select.focus.ring.offset')}; } .p-select.p-variant-filled { background: ${dt('select.filled.background')}; } .p-select.p-variant-filled:not(.p-disabled):hover { background: ${dt('select.filled.hover.background')}; } .p-select.p-variant-filled.p-focus { background: ${dt('select.filled.focus.background')}; } .p-select.p-disabled { opacity: 1; background: ${dt('select.disabled.background')}; } .p-select-clear-icon { position: absolute; top: 50%; margin-top: -0.5rem; color: ${dt('select.clear.icon.color')}; inset-inline-end: ${dt('select.dropdown.width')}; } .p-select-dropdown { display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: transparent; color: ${dt('select.dropdown.color')}; width: ${dt('select.dropdown.width')}; border-start-end-radius: ${dt('select.border.radius')}; border-end-end-radius: ${dt('select.border.radius')}; } .p-select-label { display: block; white-space: nowrap; overflow: hidden; flex: 1 1 auto; width: 1%; padding: ${dt('select.padding.y')} ${dt('select.padding.x')}; text-overflow: ellipsis; cursor: pointer; color: ${dt('select.color')}; background: transparent; border: 0 none; outline: 0 none; } .p-select-label.p-placeholder { color: ${dt('select.placeholder.color')}; } .p-select.ng-invalid.ng-dirty .p-select-label.p-placeholder { color: ${dt('select.invalid.placeholder.color')}; } .p-select:has(.p-select-clear-icon) .p-select-label { padding-inline-end: calc(1rem + ${dt('select.padding.x')}); } .p-select.p-disabled .p-select-label { color: ${dt('select.disabled.color')}; } .p-select-label-empty { overflow: hidden; opacity: 0; } input.p-select-label { cursor: default; } .p-select .p-select-overlay { min-width: 100%; } .p-select-overlay { cursor: default; background: ${dt('select.overlay.background')}; color: ${dt('select.overlay.color')}; border: 1px solid ${dt('select.overlay.border.color')}; border-radius: ${dt('select.overlay.border.radius')}; box-shadow: ${dt('select.overlay.shadow')}; } .p-select-header { padding: ${dt('select.list.header.padding')}; } .p-select-filter { width: 100%; } .p-select-list-container { overflow: auto; } .p-select-option-group { cursor: auto; margin: 0; padding: ${dt('select.option.group.padding')}; background: ${dt('select.option.group.background')}; color: ${dt('select.option.group.color')}; font-weight: ${dt('select.option.group.font.weight')}; } .p-select-list { margin: 0; padding: 0; list-style-type: none; padding: ${dt('select.list.padding')}; gap: ${dt('select.list.gap')}; display: flex; flex-direction: column; } .p-select-option { cursor: pointer; font-weight: normal; white-space: nowrap; position: relative; overflow: hidden; display: flex; align-items: center; padding: ${dt('select.option.padding')}; border: 0 none; color: ${dt('select.option.color')}; background: transparent; transition: background ${dt('select.transition.duration')}, color ${dt('select.transition.duration')}, border-color ${dt('select.transition.duration')}, box-shadow ${dt('select.transition.duration')}, outline-color ${dt('select.transition.duration')}; border-radius: ${dt('select.option.border.radius')}; } .p-select-option:not(.p-select-option-selected):not(.p-disabled).p-focus { background: ${dt('select.option.focus.background')}; color: ${dt('select.option.focus.color')}; } .p-select-option.p-select-option-selected { background: ${dt('select.option.selected.background')}; color: ${dt('select.option.selected.color')}; } .p-select-option.p-select-option-selected.p-focus { background: ${dt('select.option.selected.focus.background')}; color: ${dt('select.option.selected.focus.color')}; } .p-select-option-check-icon { position: relative; margin-inline-start: ${dt('select.checkmark.gutter.start')}; margin-inline-end: ${dt('select.checkmark.gutter.end')}; color: ${dt('select.checkmark.color')}; } .p-select-empty-message { padding: ${dt('select.empty.message.padding')}; } .p-select-fluid { display: flex; } .p-select-sm .p-select-label { font-size: ${dt('select.sm.font.size')}; padding-block: ${dt('select.sm.padding.y')}; padding-inline: ${dt('select.sm.padding.x')}; } .p-select-sm .p-select-dropdown .p-icon { font-size: ${dt('select.sm.font.size')}; width: ${dt('select.sm.font.size')}; height: ${dt('select.sm.font.size')}; } .p-select-lg .p-select-label { font-size: ${dt('select.lg.font.size')}; padding-block: ${dt('select.lg.padding.y')}; padding-inline: ${dt('select.lg.padding.x')}; } .p-select-lg .p-select-dropdown .p-icon { font-size: ${dt('select.lg.font.size')}; width: ${dt('select.lg.font.size')}; height: ${dt('select.lg.font.size')}; } `; const classes = { root: ({ instance }) => [ 'p-select p-component p-inputwrapper', { 'p-disabled': instance.disabled, 'p-variant-filled': instance.variant === 'filled' || instance.config.inputVariant() === 'filled' || instance.config.inputStyle() === 'filled', 'p-focus': instance.focused, 'p-inputwrapper-filled': instance.modelValue() !== undefined && instance.modelValue() !== null, '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, props }) => [ 'p-select-label', { 'p-placeholder': !props.editable && instance.label === props.placeholder, 'p-select-label-empty': !props.editable && !instance.$slots['value'] && (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', 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, props, state, option, focusedOption }) => [ 'p-select-option', { 'p-select-option-selected': instance.isSelected(option) && props.highlightOnSelect, 'p-focus': state.focusedOptionIndex === focusedOption, 'p-disabled': instance.isOptionDisabled(option) } ], 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: "19.2.5", ngImport: i0, type: SelectStyle, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: SelectStyle }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.5", 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(); onOptionClick(event) { this.onClick.emit(event); } onOptionMouseEnter(event) { this.onMouseEnter.emit(event); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: SelectItem, deps: null, target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "19.2.5", 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" }, 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' }" [ngClass]="{ 'p-select-option': true, 'p-select-option-selected': selected && !checkmark, 'p-disabled': disabled, 'p-focus': focused }" > <ng-container *ngIf="checkmark"> <CheckIcon *ngIf="selected" styleClass="p-select-option-check-icon" /> <BlankIcon *ngIf="!selected" styleClass="p-select-option-blank-icon" /> </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.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: "CheckIcon" }, { kind: "component", type: BlankIcon, selector: "BlankIcon" }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.5", 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' }" [ngClass]="{ 'p-select-option': true, 'p-select-option-selected': selected && !checkmark, 'p-disabled': disabled, 'p-focus': focused }" > <ng-container *ngIf="checkmark"> <CheckIcon *ngIf="selected" styleClass="p-select-option-check-icon" /> <BlankIcon *ngIf="!selected" styleClass="p-select-option-blank-icon" /> </ng-container> <span *ngIf="!template">{{ label ?? 'empty' }}</span> <ng-container *ngTemplateOutlet="template; context: { $implicit: option }"></ng-container> </li> ` }] }], 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 BaseComponent { 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; /** * Name of the input element. * @group Props */ name; /** * Inline style of the element. * @group Props */ style; /** * Inline style of the overlay panel element. * @group Props */ panelStyle; /** * Style class of the element. * @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, it specifies that an input field must be filled out before submitting the form. * @group Props */ required; /** * When present, custom value instead of predefined options can be entered using the editable input field. * @group Props */ editable; /** * 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). * @group Props */ appendTo; /** * 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; /** * Specifies the input variant of the component. * @group Props */ variant; /** * 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 the first item as the label if no placeholder is defined and value is null. * @deprecated since v17.3.0, set initial value by model instead. * @group Props */ autoDisplayFirst = true; /** * 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; /** * Defines the size of the component. * @group Props */ size; /** * 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'; /** * Maximum number of character allows in the editable input field. * @group Props */ maxlength; /** * 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; /** * Whether the component should span the full width of its parent. * @group Props */ fluid; /** * When present, it specifies that the component should be disabled. * @group Props */ get disabled() { return this._disabled; } set disabled(_disabled) { if (_disabled) { this.focused = false; if (this.overlayVisible) this.hide(); } this._disabled = _disabled; if (!this.cd.destroyed) { this.cd.detectChanges(); } } /** * Item size of item to be virtual scrolled. * @group Props * @deprecated use virtualScrollItemSize property instead. */ get itemSize() { return this._itemSize; } set itemSize(val) { this._itemSize = val; console.log('The itemSize property is deprecated, use virtualScrollItemSize property instead.'); } _itemSize; /** * Whether to automatically manage layering. * @group Props * @deprecated since v14.2.0, use overlayOptions property instead. */ get autoZIndex() { return this._autoZIndex; } set autoZIndex(val) { this._autoZIndex = val; console.log('The autoZIndex property is deprecated since v14.2.0, use overlayOptions property instead.'); } _autoZIndex; /** * Base zIndex value to use in layering. * @group Props * @deprecated since v14.2.0, use overlayOptions property instead. */ get baseZIndex() { return this._baseZIndex; } set baseZIndex(val) { this._baseZIndex = val; console.log('The baseZIndex property is deprecated since v14.2.0, use overlayOptions property instead.'); } _baseZIndex; /** * Transition options of the show animation. * @group Props * @deprecated since v14.2.0, use overlayOptions property instead. */ get showTransitionOptions() { return this._showTransitionOptions; } set showTransitionOptions(val) { this._showTransitionOptions = val; console.log('The showTransitionOptions property is deprecated since v14.2.0, use overlayOptions property instead.'); } _showTransitionOptions; /** * Transition options of the hide animation. * @group Props * @deprecated since v14.2.0, use overlayOptions property instead. */ get hideTransitionOptions() { return this._hideTransitionOptions; } set hideTransitionOptions(val) { this._hideTransitionOptions = val; console.log('The hideTransitionOptions property is deprecated since v14.2.0, use overlayOptions property instead.'); } _hideTransitionOptions; /** * 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); } } /** * 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; get hasFluid() { const nativeElement = this.el.nativeElement; const fluidComponent = nativeElement.closest('p-fluid'); return this.fluid || !!fluidComponent; } // @todo to be refactored get hostClass() { const classes = this._componentStyle.classes .root({ instance: this }) .map((cls) => { if (typeof cls === 'string') { return cls; } else { return Object.keys(cls) .filter((key) => cls[key]) .join(' '); } }) .join(' '); return classes + ' ' + this.styleClass; } get hostStyle() { return this.style; } _disabled; itemsWrapper; /** * 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); _placeholder = signal(undefined); modelValue = signal(null); value; onModelChange = () => { }; onModelTouched = () => { }; hover; focused; overlayVisible; optionsChanged; panel; dimensionsUpdated; hoveredItem; selectedOptionUpdated; _filterValue = signal(null); searchValue; searchIndex; searchTimeout; previousSearchChar; currentSearchChar; preventModelTouched; focusedOptionIndex = signal(-1); labelId; listId; clicked = signal(false); 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 rootClass() { return this._componentStyle.classes.root({ instance: this }); } get inputClass() { const label = this.label(); return { 'p-select-label': true, 'p-placeholder': this.placeholder() && label === this.placeholder(), 'p-select-label-empty': !this.editable && !this.selectedItemTemplate && (label === undefined || label === null || label === 'p-emptylabel' || label.length === 0) }; } get panelClass() { return { 'p-dropdown-panel p-component': true, 'p-input-filled': this.config.inputStyle() === 'filled' || this.config.inputVariant() === 'filled', 'p-ripple-disabled': this.config.ripple() === false }; } 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; }); 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'; }); filled = computed(() => { if (typeof this.modelValue() === 'string') return !!this.modelValue(); return this.label() !== 'p-emptylabel' && this.modelValue() !== undefined && this.modelValue() !== null; }); selectedOption; editableInputValue = computed(() => this.getOptionLabel(this.selectedOption) || this.modelValue() || ''); 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); } if (this.autoDisplayFirst && (this.modelValue() === null || this.modelValue() === undefined)) { if (!this.placeholder()) { const ind = this.findFirstOptionIndex(); this.onOptionSelect(null, this.visibleOptions()[ind], false, true); } } } 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.modelValue.set(value); this.selectedOptionUpdated = true; } writeValue(value) { if (this.filter) { this.resetFilter(); } this.value = value; this.allowModelChange() && this.onModelChange(value); this.modelValue.set(this.value); this.updateEditableLabel(); this.cd.markForCheck(); } allowModelChange() { return !!this.modelValue() && !this.placeholder() && (this.modelValue() === undefined || this.modelValue() === null) && !this.editable && this.options && this.options.length; } isSelected(option) { return this.isValidOption(option) && this.isOptionValueEqualsModelValue(option); } isOptionValueEqualsModelValue(option) { return 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 = ''; } } registerOnChange(fn) { this.onModelChange = fn; } registerOnTouched(fn) { this.onModelTouched = fn; } setDisabledState(val) { this.disabled = val; this.cd.markForCheck(); } 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);