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,334 lines (1,330 loc) 130 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, ViewEncapsulation, Component, input, signal, computed, effect, ContentChildren, ContentChild, ViewChild, ChangeDetectionStrategy, NgModule } from '@angular/core'; import * as i2 from '@angular/forms'; import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'; import { deepEquals, isNotEmpty, isArray, uuid, equals, focus, findLastIndex, resolveFieldData, isPrintableCharacter, getFirstFocusableElement, getLastFocusableElement, findSingle, getFocusableElements } from '@primeuix/utils'; import * as i3 from 'primeng/api'; import { SharedModule, TranslationKeys, Footer, Header, PrimeTemplate } from 'primeng/api'; import { AutoFocus } from 'primeng/autofocus'; import { BaseComponent } from 'primeng/basecomponent'; import { BaseEditableHolder } from 'primeng/baseeditableholder'; import { Checkbox } from 'primeng/checkbox'; import { Chip } from 'primeng/chip'; import { DomHandler, unblockBodyScroll } from 'primeng/dom'; import { Fluid } from 'primeng/fluid'; import { IconField } from 'primeng/iconfield'; import { CheckIcon, SearchIcon, TimesIcon, ChevronDownIcon } 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 { ObjectUtils } from 'primeng/utils'; import { style } from '@primeuix/styles/multiselect'; import { BaseStyle } from 'primeng/base'; const theme = /*css*/ ` ${style} /* For PrimeNG */ .p-multiselect.ng-invalid.ng-dirty { border-color: dt('multiselect.invalid.border.color'); } p-multiSelect.ng-invalid.ng-dirty .p-multiselect-label.p-placeholder, p-multi-select.ng-invalid.ng-dirty .p-multiselect-label.p-placeholder, p-multiselect.ng-invalid.ng-dirty .p-multiselect-label.p-placeholder { color: dt('multiselect.invalid.placeholder.color'); } `; const inlineStyles = { root: ({ instance }) => ({ position: instance.$appendTo() === 'self' ? 'relative' : undefined }) }; const classes = { root: ({ instance }) => [ 'p-multiselect p-component p-inputwrapper', { 'p-multiselect p-component p-inputwrapper': true, 'p-multiselect-display-chip': instance.display === 'chip', 'p-disabled': instance.$disabled(), 'p-invalid': instance.invalid(), 'p-variant-filled': instance.$variant(), 'p-focus': instance.focused, 'p-inputwrapper-filled': instance.$filled(), 'p-inputwrapper-focus': instance.focused || instance.overlayVisible, 'p-multiselect-open': instance.overlayVisible, 'p-multiselect-fluid': instance.hasFluid, 'p-multiselect-sm p-inputfield-sm': instance.size() === 'small', 'p-multiselect-lg p-inputfield-lg': instance.size() === 'large' } ], labelContainer: 'p-multiselect-label-container', label: ({ instance }) => ({ 'p-multiselect-label': true, 'p-placeholder': instance.label() === instance.placeholder(), 'p-multiselect-label-empty': !instance.placeholder() && !instance.defaultLabel && (!instance.modelValue() || instance.modelValue().length === 0) }), chipItem: 'p-multiselect-chip-item', pcChip: 'p-multiselect-chip', chipIcon: 'p-multiselect-chip-icon', dropdown: 'p-multiselect-dropdown', loadingIcon: 'p-multiselect-loading-icon', dropdownIcon: 'p-multiselect-dropdown-icon', overlay: 'p-multiselect-overlay p-component-overlay p-component', header: 'p-multiselect-header', pcFilterContainer: 'p-multiselect-filter-container', pcFilter: 'p-multiselect-filter', listContainer: 'p-multiselect-list-container', list: 'p-multiselect-list', optionGroup: 'p-multiselect-option-group', option: ({ instance }) => ({ 'p-multiselect-option': true, 'p-multiselect-option-selected': instance.selected && instance.highlightOnSelect, 'p-disabled': instance.disabled, 'p-focus': instance.focused }), emptyMessage: 'p-multiselect-empty-message', clearIcon: 'p-multiselect-clear-icon' }; class MultiSelectStyle extends BaseStyle { name = 'multiselect'; theme = theme; classes = classes; inlineStyles = inlineStyles; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: MultiSelectStyle, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: MultiSelectStyle }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: MultiSelectStyle, decorators: [{ type: Injectable }] }); /** * * MultiSelect is used to select multiple items from a collection. * * [Live Demo](https://www.primeng.org/multiselect/) * * @module multiselectstyle * */ var MultiSelectClasses; (function (MultiSelectClasses) { /** * Class name of the root element */ MultiSelectClasses["root"] = "p-multiselect"; /** * Class name of the label container element */ MultiSelectClasses["labelContainer"] = "p-multiselect-label-container"; /** * Class name of the label element */ MultiSelectClasses["label"] = "p-multiselect-label"; /** * Class name of the chip item element */ MultiSelectClasses["chipItem"] = "p-multiselect-chip-item"; /** * Class name of the chip element */ MultiSelectClasses["pcChip"] = "p-multiselect-chip"; /** * Class name of the chip icon element */ MultiSelectClasses["chipIcon"] = "p-multiselect-chip-icon"; /** * Class name of the dropdown element */ MultiSelectClasses["dropdown"] = "p-multiselect-dropdown"; /** * Class name of the loading icon element */ MultiSelectClasses["loadingIcon"] = "p-multiselect-loading-icon"; /** * Class name of the dropdown icon element */ MultiSelectClasses["dropdownIcon"] = "p-multiselect-dropdown-icon"; /** * Class name of the overlay element */ MultiSelectClasses["overlay"] = "p-multiselect-overlay"; /** * Class name of the header element */ MultiSelectClasses["header"] = "p-multiselect-header"; /** * Class name of the filter container element */ MultiSelectClasses["pcFilterContainer"] = "p-multiselect-filter-container"; /** * Class name of the filter element */ MultiSelectClasses["pcFilter"] = "p-multiselect-filter"; /** * Class name of the list container element */ MultiSelectClasses["listContainer"] = "p-multiselect-list-container"; /** * Class name of the list element */ MultiSelectClasses["list"] = "p-multiselect-list"; /** * Class name of the option group element */ MultiSelectClasses["optionGroup"] = "p-multiselect-option-group"; /** * Class name of the option element */ MultiSelectClasses["option"] = "p-multiselect-option"; /** * Class name of the empty message element */ MultiSelectClasses["emptyMessage"] = "p-multiselect-empty-message"; /** * Class name of the clear icon */ MultiSelectClasses["clearIcon"] = "p-autocomplete-clear-icon"; })(MultiSelectClasses || (MultiSelectClasses = {})); const MULTISELECT_VALUE_ACCESSOR = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MultiSelect), multi: true }; class MultiSelectItem extends BaseComponent { id; option; selected; label; disabled; itemSize; focused; ariaPosInset; ariaSetSize; variant; template; checkIconTemplate; itemCheckboxIconTemplate; highlightOnSelect; onClick = new EventEmitter(); onMouseEnter = new EventEmitter(); _componentStyle = inject(MultiSelectStyle); onOptionClick(event) { this.onClick.emit({ originalEvent: event, option: this.option, selected: this.selected }); event.stopPropagation(); event.preventDefault(); } onOptionMouseEnter(event) { this.onMouseEnter.emit({ originalEvent: event, option: this.option, selected: this.selected }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: MultiSelectItem, deps: null, target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "20.1.3", type: MultiSelectItem, isStandalone: true, selector: "p-multiSelectItem, p-multiselect-item", inputs: { id: "id", option: "option", selected: ["selected", "selected", booleanAttribute], label: "label", disabled: ["disabled", "disabled", booleanAttribute], itemSize: ["itemSize", "itemSize", numberAttribute], focused: ["focused", "focused", booleanAttribute], ariaPosInset: "ariaPosInset", ariaSetSize: "ariaSetSize", variant: "variant", template: "template", checkIconTemplate: "checkIconTemplate", itemCheckboxIconTemplate: "itemCheckboxIconTemplate", highlightOnSelect: ["highlightOnSelect", "highlightOnSelect", booleanAttribute] }, outputs: { onClick: "onClick", onMouseEnter: "onMouseEnter" }, providers: [MultiSelectStyle], usesInheritance: true, ngImport: i0, template: ` <li pRipple [class]="cx('option')" role="option" [ngStyle]="{ height: itemSize + 'px' }" [id]="id" [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" [attr.aria-checked]="selected" (click)="onOptionClick($event)" (mouseenter)="onOptionMouseEnter($event)" > <p-checkbox [ngModel]="selected" [binary]="true" [tabindex]="-1" [variant]="variant" [ariaLabel]="label"> <ng-container *ngIf="itemCheckboxIconTemplate"> <ng-template #icon let-klass="class"> <ng-template *ngTemplateOutlet="itemCheckboxIconTemplate; context: { checked: selected, class: klass }"></ng-template> </ng-template> </ng-container> </p-checkbox> <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: "component", type: Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: Ripple, selector: "[pRipple]" }, { kind: "ngmodule", type: SharedModule }], encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: MultiSelectItem, decorators: [{ type: Component, args: [{ selector: 'p-multiSelectItem, p-multiselect-item', standalone: true, imports: [CommonModule, Checkbox, FormsModule, Ripple, SharedModule], template: ` <li pRipple [class]="cx('option')" role="option" [ngStyle]="{ height: itemSize + 'px' }" [id]="id" [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" [attr.aria-checked]="selected" (click)="onOptionClick($event)" (mouseenter)="onOptionMouseEnter($event)" > <p-checkbox [ngModel]="selected" [binary]="true" [tabindex]="-1" [variant]="variant" [ariaLabel]="label"> <ng-container *ngIf="itemCheckboxIconTemplate"> <ng-template #icon let-klass="class"> <ng-template *ngTemplateOutlet="itemCheckboxIconTemplate; context: { checked: selected, class: klass }"></ng-template> </ng-template> </ng-container> </p-checkbox> <span *ngIf="!template">{{ label ?? 'empty' }}</span> <ng-container *ngTemplateOutlet="template; context: { $implicit: option }"></ng-container> </li> `, encapsulation: ViewEncapsulation.None, providers: [MultiSelectStyle] }] }], propDecorators: { id: [{ type: Input }], option: [{ type: Input }], selected: [{ type: Input, args: [{ transform: booleanAttribute }] }], label: [{ type: Input }], disabled: [{ type: Input, args: [{ transform: booleanAttribute }] }], itemSize: [{ type: Input, args: [{ transform: numberAttribute }] }], focused: [{ type: Input, args: [{ transform: booleanAttribute }] }], ariaPosInset: [{ type: Input }], ariaSetSize: [{ type: Input }], variant: [{ type: Input }], template: [{ type: Input }], checkIconTemplate: [{ type: Input }], itemCheckboxIconTemplate: [{ type: Input }], highlightOnSelect: [{ type: Input, args: [{ transform: booleanAttribute }] }], onClick: [{ type: Output }], onMouseEnter: [{ type: Output }] } }); /** * MultiSelect is used to select multiple items from a collection. * @group Components */ class MultiSelect extends BaseEditableHolder { zone; filterService; overlayService; /** * Unique identifier of the component * @group Props */ id; /** * Defines a string that labels the input for accessibility. * @group Props */ ariaLabel; /** * Style class of the element. * @deprecated since v20.0.0, use `class` instead. * @group Props */ styleClass; /** * Inline style of the overlay panel. * @group Props */ panelStyle; /** * Style class of the overlay panel element. * @group Props */ panelStyleClass; /** * Identifier of the focus input to match a label defined for the component. * @group Props */ inputId; /** * When present, it specifies that the component cannot be edited. * @group Props */ readonly; /** * Whether to display options as grouped when nested options are provided. * @group Props */ group; /** * When specified, displays an input field to filter the items on keyup. * @group Props */ filter = true; /** * Defines placeholder of the filter input. * @group Props */ filterPlaceHolder; /** * Locale to use in filtering. The default locale is the host environment's current locale. * @group Props */ filterLocale; /** * Specifies the visibility of the options panel. * @group Props */ overlayVisible; /** * Index of the element in tabbing order. * @group Props */ tabindex = 0; /** * A property to uniquely identify a value in options. * @group Props */ dataKey; /** * Establishes relationships between the component and label(s) where its value should be one or more element IDs. * @group Props */ ariaLabelledBy; /** * Whether to show labels of selected item labels or use default label. * @group Props * @defaultValue true */ set displaySelectedLabel(val) { this._displaySelectedLabel = val; } get displaySelectedLabel() { return this._displaySelectedLabel; } /** * Decides how many selected item labels to show at most. * @group Props * @defaultValue 3 */ set maxSelectedLabels(val) { this._maxSelectedLabels = val; } get maxSelectedLabels() { return this._maxSelectedLabels; } /** * Maximum number of selectable items. * @group Props */ selectionLimit; /** * Label to display after exceeding max selected labels e.g. ({0} items selected), defaults "ellipsis" keyword to indicate a text-overflow. * @group Props */ selectedItemsLabel; /** * Whether to show the checkbox at header to toggle all items at once. * @group Props */ showToggleAll = true; /** * Text to display when filtering does not return any results. * @group Props */ emptyFilterMessage = ''; /** * Text to display when there is no data. Defaults to global value in i18n translation configuration. * @group Props */ emptyMessage = ''; /** * Clears the filter value when hiding the dropdown. * @group Props */ resetFilterOnHide = false; /** * Icon class of the dropdown icon. * @group Props */ dropdownIcon; /** * Icon class of the chip icon. * @group Props */ chipIcon; /** * 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 show the header. * @group Props */ showHeader = true; /** * When filtering is enabled, filterBy decides which field or fields (comma separated) to search against. * @group Props */ filterBy; /** * Height of the viewport in pixels, a scrollbar is defined if height of list exceeds this value. * @group Props */ scrollHeight = '200px'; /** * 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; /** * Whether the multiselect is in loading state. * @group Props */ loading = false; /** * Height of an item in the list for VirtualScrolling. * @group Props */ virtualScrollItemSize; /** * Icon to display in loading state. * @group Props */ loadingIcon; /** * 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; /** * 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; /** * Applies focus to the filter element when the overlay is shown. * @group Props */ autofocusFilter = false; /** * Defines how the selected items are displayed. * @group Props */ display = 'comma'; /** * Defines the autocomplete is active. * @group Props */ autocomplete = 'off'; /** * When enabled, a clear icon is displayed to clear the value. * @group Props */ showClear = false; /** * When present, it specifies that the component should automatically get focus on load. * @group Props */ autofocus; /** * Label to display when there are no selections. * @group Props */ set placeholder(val) { this._placeholder.set(val); } get placeholder() { return this._placeholder.asReadonly(); } /** * An array of objects to display as the available options. * @group Props */ get options() { return this._options(); } set options(val) { if (!deepEquals(this._options(), val)) { this._options.set(val); } } /** * When specified, filter displays with this value. * @group Props */ get filterValue() { return this._filterValue(); } set filterValue(val) { this._filterValue.set(val); } /** * Whether all data is selected. * @group Props */ get selectAll() { return this._selectAll; } set selectAll(value) { this._selectAll = value; } /** * Indicates whether to focus on options when hovering over them, defaults to optionLabel. * @group Props */ focusOnHover = true; /** * Fields used when filtering the options, defaults to optionLabel. * @group Props */ filterFields; /** * 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; /** * Whether the selected option will be add highlight class. * @group Props */ highlightOnSelect = true; /** * Specifies the size of the component. * @defaultValue undefined * @group Props */ size = input(...(ngDevMode ? [undefined, { debugName: "size" }] : [])); /** * Specifies the input variant of the component. * @defaultValue undefined * @group Props */ variant = input(...(ngDevMode ? [undefined, { debugName: "variant" }] : [])); /** * Spans 100% width of the container when enabled. * @defaultValue undefined * @group Props */ fluid = input(undefined, ...(ngDevMode ? [{ debugName: "fluid", transform: booleanAttribute }] : [{ transform: booleanAttribute }])); /** * 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 changes. * @param {MultiSelectChangeEvent} event - Custom change event. * @group Emits */ onChange = new EventEmitter(); /** * Callback to invoke when data is filtered. * @param {MultiSelectFilterEvent} event - Custom filter event. * @group Emits */ onFilter = new EventEmitter(); /** * Callback to invoke when multiselect receives focus. * @param {MultiSelectFocusEvent} event - Custom focus event. * @group Emits */ onFocus = new EventEmitter(); /** * Callback to invoke when multiselect loses focus. * @param {MultiSelectBlurEvent} event - Custom blur event. * @group Emits */ onBlur = new EventEmitter(); /** * Callback to invoke when component is clicked. * @param {Event} event - Browser event. * @group Emits */ onClick = new EventEmitter(); /** * Callback to invoke when input field is cleared. * @group Emits */ onClear = new EventEmitter(); /** * Callback to invoke when overlay panel becomes visible. * @param {AnimationEvent} event - Animation event. * @group Emits */ onPanelShow = new EventEmitter(); /** * Callback to invoke when overlay panel becomes hidden. * @param {AnimationEvent} event - Animation event. * @group Emits */ onPanelHide = new EventEmitter(); /** * Callback to invoke in lazy mode to load new data. * @param {MultiSelectLazyLoadEvent} event - Lazy load event. * @group Emits */ onLazyLoad = new EventEmitter(); /** * Callback to invoke in lazy mode to load new data. * @param {MultiSelectRemoveEvent} event - Remove event. * @group Emits */ onRemove = new EventEmitter(); /** * Callback to invoke when all data is selected. * @param {MultiSelectSelectAllChangeEvent} event - Custom select event. * @group Emits */ onSelectAllChange = new EventEmitter(); overlayViewChild; filterInputChild; focusInputViewChild; itemsViewChild; scroller; lastHiddenFocusableElementOnOverlay; firstHiddenFocusableElementOnOverlay; headerCheckboxViewChild; footerFacet; headerFacet; _componentStyle = inject(MultiSelectStyle); searchValue; searchTimeout; _selectAll = null; _placeholder = signal(undefined, ...(ngDevMode ? [{ debugName: "_placeholder" }] : [])); _disableTooltip = false; value; _filteredOptions; focus; filtered; itemTemplate; groupTemplate; loaderTemplate; headerTemplate; filterTemplate; footerTemplate; emptyFilterTemplate; emptyTemplate; selectedItemsTemplate; loadingIconTemplate; filterIconTemplate; removeTokenIconTemplate; chipIconTemplate; clearIconTemplate; dropdownIconTemplate; itemCheckboxIconTemplate; headerCheckboxIconTemplate; templates; _itemTemplate; _groupTemplate; _loaderTemplate; _headerTemplate; _filterTemplate; _footerTemplate; _emptyFilterTemplate; _emptyTemplate; _selectedItemsTemplate; _loadingIconTemplate; _filterIconTemplate; _removeTokenIconTemplate; _chipIconTemplate; _clearIconTemplate; _dropdownIconTemplate; _itemCheckboxIconTemplate; _headerCheckboxIconTemplate; $variant = computed(() => this.variant() || this.config.inputStyle() || this.config.inputVariant(), ...(ngDevMode ? [{ debugName: "$variant" }] : [])); $appendTo = computed(() => this.appendTo() || this.config.overlayAppendTo(), ...(ngDevMode ? [{ debugName: "$appendTo" }] : [])); pcFluid = inject(Fluid, { optional: true, host: true, skipSelf: true }); get hasFluid() { return this.fluid() ?? !!this.pcFluid; } ngAfterContentInit() { this.templates.forEach((item) => { switch (item.getType()) { case 'item': this._itemTemplate = item.template; break; case 'group': this._groupTemplate = item.template; break; case 'selectedItems': case 'selecteditems': this._selectedItemsTemplate = item.template; break; case 'header': this._headerTemplate = item.template; break; case 'filter': this._filterTemplate = item.template; break; case 'emptyfilter': this._emptyFilterTemplate = item.template; break; case 'empty': this._emptyTemplate = item.template; break; case 'footer': this._footerTemplate = item.template; break; case 'loader': this._loaderTemplate = item.template; break; case 'headercheckboxicon': this._headerCheckboxIconTemplate = item.template; break; case 'loadingicon': this._loadingIconTemplate = item.template; break; case 'filtericon': this._filterIconTemplate = item.template; break; case 'removetokenicon': this._removeTokenIconTemplate = item.template; break; case 'clearicon': this._clearIconTemplate = item.template; break; case 'dropdownicon': this._dropdownIconTemplate = item.template; break; case 'itemcheckboxicon': this._itemCheckboxIconTemplate = item.template; break; case 'chipicon': this._chipIconTemplate = item.template; break; default: this._itemTemplate = item.template; break; } }); } headerCheckboxFocus; filterOptions; preventModelTouched; focused = false; itemsWrapper; _displaySelectedLabel = true; _maxSelectedLabels = 3; modelValue = signal(null, ...(ngDevMode ? [{ debugName: "modelValue" }] : [])); _filterValue = signal(null, ...(ngDevMode ? [{ debugName: "_filterValue" }] : [])); _options = signal(null, ...(ngDevMode ? [{ debugName: "_options" }] : [])); startRangeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "startRangeIndex" }] : [])); focusedOptionIndex = signal(-1, ...(ngDevMode ? [{ debugName: "focusedOptionIndex" }] : [])); selectedOptions; clickInProgress = 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.modelValue() !== '' && isNotEmpty(this.modelValue()) && this.showClear && !this.$disabled() && !this.readonly && this.$filled(); } get toggleAllAriaLabel() { return this.config.translation.aria ? this.config.translation.aria[this.allSelected() ? 'selectAll' : 'unselectAll'] : undefined; } get listLabel() { return this.config.getTranslation(TranslationKeys.ARIA)['listLabel']; } getAllVisibleAndNonVisibleOptions() { return this.group ? this.flatOptions(this.options) : this.options || []; } visibleOptions = computed(() => { const options = this.getAllVisibleAndNonVisibleOptions(); const isArrayOfObjects = isArray(options) && ObjectUtils.isObject(options[0]); if (this._filterValue()) { let filteredOptions; if (isArrayOfObjects) { filteredOptions = this.filterService.filter(options, this.searchFields(), this._filterValue(), this.filterMatchMode, this.filterLocale); } else { filteredOptions = options.filter((option) => option.toString().toLocaleLowerCase().includes(this._filterValue().toLocaleLowerCase())); } 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(() => { let label; const modelValue = this.modelValue(); if (modelValue && modelValue.length && this.displaySelectedLabel) { if (isNotEmpty(this.maxSelectedLabels) && modelValue.length > this.maxSelectedLabels) { return this.getSelectedItemsLabel(); } else { label = ''; for (let i = 0; i < modelValue.length; i++) { if (i !== 0) { label += ', '; } label += this.getLabelByValue(modelValue[i]); } } } else { label = this.placeholder() || ''; } return label; }, ...(ngDevMode ? [{ debugName: "label" }] : [])); chipSelectedItems = computed(() => { return isNotEmpty(this.maxSelectedLabels) && this.modelValue() && this.modelValue().length > this.maxSelectedLabels ? this.modelValue().slice(0, this.maxSelectedLabels) : this.modelValue(); }, ...(ngDevMode ? [{ debugName: "chipSelectedItems" }] : [])); constructor(zone, filterService, overlayService) { super(); this.zone = zone; this.filterService = filterService; this.overlayService = overlayService; effect(() => { const modelValue = this.modelValue(); const allVisibleAndNonVisibleOptions = this.getAllVisibleAndNonVisibleOptions(); if (allVisibleAndNonVisibleOptions && isNotEmpty(allVisibleAndNonVisibleOptions)) { if (this.optionValue && this.optionLabel && modelValue) { this.selectedOptions = allVisibleAndNonVisibleOptions.filter((option) => modelValue.includes(option[this.optionLabel]) || modelValue.includes(option[this.optionValue])); } else { this.selectedOptions = modelValue; } this.cd.markForCheck(); } }); } 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() }; } } maxSelectionLimitReached() { return this.selectionLimit && this.modelValue() && this.modelValue().length === this.selectionLimit; } ngAfterViewInit() { super.ngAfterViewInit(); if (this.overlayVisible) { this.show(); } } ngAfterViewChecked() { if (this.filtered) { this.zone.runOutsideAngular(() => { setTimeout(() => { this.overlayViewChild?.alignOverlay(); }, 1); }); this.filtered = 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()); const value = this.getOptionValue(this.visibleOptions()[this.focusedOptionIndex()]); this.onOptionSelect({ originalEvent: null, option: [value] }); } } /** * Updates the model value. * @group Method */ updateModel(value, event) { this.value = value; this.onModelChange(value); this.writeValue(value); } onInputClick(event) { event.stopPropagation(); event.preventDefault(); this.focusedOptionIndex.set(-1); } onOptionSelect(event, isFocus = false, index = -1) { const { originalEvent, option } = event; if (this.$disabled() || this.isOptionDisabled(option)) { return; } let selected = this.isSelected(option); let value = null; if (selected) { value = this.modelValue().filter((val) => !equals(val, this.getOptionValue(option), this.equalityKey())); } else { value = [...(this.modelValue() || []), this.getOptionValue(option)]; } this.updateModel(value, originalEvent); index !== -1 && this.focusedOptionIndex.set(index); isFocus && focus(this.focusInputViewChild?.nativeElement); this.onChange.emit({ originalEvent: event, value: value, itemValue: option }); } findSelectedOptionIndex() { return this.hasSelectedOption() ? this.visibleOptions().findIndex((option) => this.isValidSelectedOption(option)) : -1; } onOptionSelectRange(event, start = -1, end = -1) { start === -1 && (start = this.findNearestSelectedOptionIndex(end, true)); end === -1 && (end = this.findNearestSelectedOptionIndex(start)); if (start !== -1 && end !== -1) { const rangeStart = Math.min(start, end); const rangeEnd = Math.max(start, end); const value = this.visibleOptions() .slice(rangeStart, rangeEnd + 1) .filter((option) => this.isValidOption(option)) .map((option) => this.getOptionValue(option)); this.updateModel(value, event); } } searchFields() { return (this.filterBy || this.optionLabel || 'label').split(','); } findNearestSelectedOptionIndex(index, firstCheckUp = false) { let matchedOptionIndex = -1; if (this.hasSelectedOption()) { if (firstCheckUp) { matchedOptionIndex = this.findPrevSelectedOptionIndex(index); matchedOptionIndex = matchedOptionIndex === -1 ? this.findNextSelectedOptionIndex(index) : matchedOptionIndex; } else { matchedOptionIndex = this.findNextSelectedOptionIndex(index); matchedOptionIndex = matchedOptionIndex === -1 ? this.findPrevSelectedOptionIndex(index) : matchedOptionIndex; } } return matchedOptionIndex > -1 ? matchedOptionIndex : index; } findPrevSelectedOptionIndex(index) { const matchedOptionIndex = this.hasSelectedOption() && index > 0 ? findLastIndex(this.visibleOptions().slice(0, index), (option) => this.isValidSelectedOption(option)) : -1; return matchedOptionIndex > -1 ? matchedOptionIndex : -1; } findFirstFocusedOptionIndex() { const selectedIndex = this.findFirstSelectedOptionIndex(); return selectedIndex < 0 ? this.findFirstOptionIndex() : selectedIndex; } findFirstOptionIndex() { return this.visibleOptions().findIndex((option) => this.isValidOption(option)); } findFirstSelectedOptionIndex() { return this.hasSelectedOption() ? this.visibleOptions().findIndex((option) => this.isValidSelectedOption(option)) : -1; } findNextSelectedOptionIndex(index) { const matchedOptionIndex = this.hasSelectedOption() && index < this.visibleOptions().length - 1 ? this.visibleOptions() .slice(index + 1) .findIndex((option) => this.isValidSelectedOption(option)) : -1; return matchedOptionIndex > -1 ? matchedOptionIndex + index + 1 : -1; } equalityKey() { return this.optionValue ? null : this.dataKey; } hasSelectedOption() { return isNotEmpty(this.modelValue()); } isValidSelectedOption(option) { return this.isValidOption(option) && this.isSelected(option); } isOptionGroup(option) { return (this.group || this.optionGroupLabel) && option.optionGroup && option.group; } isValidOption(option) { return option && !(this.isOptionDisabled(option) || this.isOptionGroup(option)); } isOptionDisabled(option) { if (this.maxSelectionLimitReached() && !this.isSelected(option)) { return true; } return this.optionDisabled ? resolveFieldData(option, this.optionDisabled) : option && option.disabled !== undefined ? option.disabled : false; } isSelected(option) { const optionValue = this.getOptionValue(option); return (this.modelValue() || []).some((value) => equals(value, optionValue, this.equalityKey())); } isOptionMatched(option) { return this.isValidOption(option) && this.getOptionLabel(option).toString().toLocaleLowerCase(this.filterLocale).startsWith(this.searchValue.toLocaleLowerCase(this.filterLocale)); } isEmpty() { return !this._options() || (this.visibleOptions() && this.visibleOptions().length === 0); } getOptionIndex(index, scrollerOptions) { return this.virtualScrollerDisabled ? index : scrollerOptions && scrollerOptions.getItemOptions(index)['index']; } 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; } getLabelByValue(value) { const options = this.group ? this.flatOptions(this._options()) : this._options() || []; const matchedOption = options.find((option) => !this.isOptionGroup(option) && equals(this.getOptionValue(option), value, this.equalityKey())); return matchedOption ? this.getOptionLabel(matchedOption) : null; } getSelectedItemsLabel() { let pattern = /{(.*?)}/; let message = this.selectedItemsLabel ? this.selectedItemsLabel : this.config.getTranslation(TranslationKeys.SELECTION_MESSAGE); if (pattern.test(message)) { return message.replace(message.match(pattern)[0], this.modelValue().length + ''); } return message; } getOptionLabel(option) { return this.optionLabel ? resolveFieldData(option, this.optionLabel) : option && option.label != undefined ? option.label : option; } getOptionValue(option) { return this.optionValue ? resolveFieldData(option, this.optionValue) : !this.optionLabel && option && option.value !== undefined ? option.value : option; } getOptionGroupLabel(optionGroup) { return this.optionGroupLabel ? resolveFieldData(optionGroup, this.optionGroupLabel) : optionGroup && optionGroup.label != undefined ? optionGroup.label : optionGroup; } getOptionGroupChildren(optionGroup) { return this.optionGroupChildren ? resolveFieldData(optionGroup, this.optionGroupChildren) : optionGroup.items; } onKeyDown(event) { if (this.$disabled()) { event.preventDefault(); return; } const metaKey = event.metaKey || event.ctrlKey; switch (event.code) { case 'ArrowDown': this.onArrowDownKey(event); break; case 'ArrowUp': this.onArrowUpKey(event); break; case 'Home': this.onHomeKey(event); break; case 'End': this.onEndKey(event); break; case 'PageDown': this.onPageDownKey(event); break; case 'PageUp': this.onPageUpKey(event); break; case 'Enter': case 'Space': this.onEnterKey(event); break; case 'Escape': this.onEscapeKey(event); break; case 'Tab': this.onTabKey(event); break; case 'ShiftLeft': case 'ShiftRight': this.onShiftKey(); break; default: if (event.code === 'KeyA' && metaKey) { const value = this.visibleOptions() .filter((option) => this.isValidOption(option)) .map((option) => this.getOptionValue(option)); this.updateModel(value, event); event.preventDefault(); break; } if (!metaKey && isPrintableCharacter(event.key)) { !this.overlayVisible && this.show(); this.searchOptions(event, event.key); event.preventDefault(); } break; } } 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); break; case 'Escape': this.onEscapeKey(event); break; case 'Tab': this.onTabKey(event, true); break; default: break; } } onArrowLeftKey(event, pressedInInputText = false) { pressedInInputText && this.focusedOptionIndex.set(-1); } onArrowDownKey(event) { const optionIndex = this.focusedOptionIndex() !== -1 ? this.findNextOptionIndex(this.focusedOptionIndex()) : this.findFirstFocusedOptionIndex(); if (event.shiftKey) { this.onOptionSelectRange(event, this.startRangeIndex(), optionIndex); } this.changeFocusedOptionIndex(event, optionIndex); !this.overlayVisible && this.show(); event.preventDefault(); event.stopPropagation(); } onArrowUpKey(event, pressedInInputText = false) { if (event.altKey && !pressedInInputText) { if (this.focusedOptionIndex() !== -1) { this.onOptionSelect(event, this.visibleOptions()[this.focusedOptionIndex()]); } this.overlayVisible && this.hide(); event.preventDefault(); } else { const optionIndex = this.focusedOptionIndex() !== -1 ? this.findPrevOptionIndex(this.focusedOptionIndex()) : this.findLastFocusedOptionIndex(); if (event.shiftKey) { this.onOptionSelectRange(event, optionIndex, this.startRangeIndex()); } this.changeFocusedOptionIndex(event, optionIndex); !this.overlayVisible && this.show(); event.preventDefault(); } event.stopPropagation(); } onHomeKey(event, pressedInInputText = false) { const { currentTarget } = event; if (pressedInInputText) { const len = currentTarget.value.length; currentTarget.setSelectionRange(0, event.shiftKey ? len : 0); this.focusedOptionIndex.set(-1); } else { let metaKey = event.metaKey || event.ctrlKey; let optionIndex = this.findFirstOptionIndex(); if (event.shiftKey && metaKey) { this.onOptionSelectRange(event, optionIndex, this.startRangeIndex());