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,211 lines (1,207 loc) 85.2 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, ChangeDetectionStrategy, ViewEncapsulation, Component, input, signal, computed, effect, ContentChildren, HostListener, ContentChild, ViewChild, NgModule } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { resolveFieldData, isNotEmpty, equals, getOffset, getViewport, getHiddenElementOuterWidth, getOuterWidth, calculateScrollbarWidth, isPrintableCharacter, isEmpty, findSingle, findLastIndex, focus, uuid } from '@primeuix/utils'; import * as i2 from 'primeng/api'; import { TranslationKeys, SharedModule, PrimeTemplate } from 'primeng/api'; import { AutoFocus } from 'primeng/autofocus'; import { BaseComponent } from 'primeng/basecomponent'; import { BaseEditableHolder } from 'primeng/baseeditableholder'; import { Fluid } from 'primeng/fluid'; import { AngleRightIcon, ChevronDownIcon, TimesIcon } from 'primeng/icons'; import { Overlay } from 'primeng/overlay'; import { Ripple } from 'primeng/ripple'; import { style } from '@primeuix/styles/cascadeselect'; import { BaseStyle } from 'primeng/base'; const theme = /*css*/ ` ${style} /* For PrimeNG */ .p-cascadeselect.ng-invalid.ng-dirty:not(.ng-untouched):not(.ng-pristine) { border-color: dt('cascadeselect.invalid.border.color'); } .p-cascadeselect.ng-invalid.ng-dirty:not(.ng-untouched):not(.ng-pristine) .p-cascadeselect-label.p-placeholder { color: dt('cascadeselect.invalid.placeholder.color'); } `; const inlineStyles = { root: ({ instance }) => ({ position: instance.$appendTo() === 'self' ? 'relative' : undefined }) }; const classes = { root: ({ instance }) => [ 'p-cascadeselect p-component p-inputwrapper', { 'p-cascadeselect p-component p-inputwrapper': true, 'p-cascadeselect-clearable': instance.showClear && !instance.$disabled(), 'p-cascadeselect-mobile': instance.queryMatches(), 'p-disabled': instance.$disabled(), 'p-invalid': instance.invalid(), 'p-focus': instance.focused, 'p-inputwrapper-filled': instance.modelValue(), 'p-variant-filled': instance.$variant() === 'filled', 'p-inputwrapper-focus': instance.focused || instance.overlayVisible, 'p-cascadeselect-open': instance.overlayVisible, 'p-cascadeselect-fluid': instance.hasFluid, 'p-cascadeselect-sm p-inputfield-sm': instance.size() === 'small', 'p-cascadeselect-lg p-inputfield-lg': instance.size() === 'large' } ], label: ({ instance }) => [ 'p-cascadeselect-label', { 'p-placeholder': instance.label() === instance.placeholder, 'p-cascadeselect-label-empty': !instance.value && (instance.label() === 'p-emptylabel' || instance.label().length === 0) } ], clearIcon: 'p-cascadeselect-clear-icon', dropdown: 'p-cascadeselect-dropdown', loadingIcon: 'p-cascadeselect-loading-icon', dropdownIcon: 'p-cascadeselect-dropdown-icon', overlay: ({ instance }) => [ 'p-cascadeselect-overlay p-component-overlay p-component', { 'p-cascadeselect-mobile-active': instance.queryMatches() } ], listContainer: 'p-cascadeselect-list-container', list: 'p-cascadeselect-list', option: ({ instance, processedOption }) => [ 'p-cascadeselect-option', { 'p-cascadeselect-option-group': instance.isOptionGroup(processedOption), 'p-cascadeselect-option-active': instance.isOptionActive(processedOption), 'p-cascadeselect-option-selected': instance.isOptionSelected(processedOption), 'p-focus': instance.isOptionFocused(processedOption), 'p-disabled': instance.isOptionDisabled(processedOption) } ], optionContent: 'p-cascadeselect-option-content', optionText: 'p-cascadeselect-option-text', groupIcon: 'p-cascadeselect-group-icon', optionList: 'p-cascadeselect-list p-cascadeselect-overlay p-cascadeselect-option-list' }; class CascadeSelectStyle extends BaseStyle { name = 'cascadeselect'; theme = theme; classes = classes; inlineStyles = inlineStyles; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CascadeSelectStyle, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CascadeSelectStyle }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CascadeSelectStyle, decorators: [{ type: Injectable }] }); /** * * CascadeSelect is a form component to select a value from a nested structure of options. * * [Live Demo](https://www.primeng.org/cascadeselect/) * * @module cascadeselectstyle * */ var CascadeSelectClasses; (function (CascadeSelectClasses) { /** * Class name of the root element */ CascadeSelectClasses["root"] = "p-cascadeselect"; /** * Class name of the label element */ CascadeSelectClasses["label"] = "p-cascadeselect-label"; /** * Class name of the dropdown element */ CascadeSelectClasses["dropdown"] = "p-cascadeselect-dropdown"; /** * Class name of the loading icon element */ CascadeSelectClasses["loadingIcon"] = "p-cascadeselect-loading-icon"; /** * Class name of the dropdown icon element */ CascadeSelectClasses["clearIcon"] = "p-cascadeselect-clear-icon"; /** * Class name of the dropdown icon element */ CascadeSelectClasses["dropdownIcon"] = "p-cascadeselect-dropdown-icon"; /** * Class name of the overlay element */ CascadeSelectClasses["overlay"] = "p-cascadeselect-overlay"; /** * Class name of the list container element */ CascadeSelectClasses["listContainer"] = "p-cascadeselect-list-container"; /** * Class name of the list element */ CascadeSelectClasses["list"] = "p-cascadeselect-list"; /** * Class name of the item element */ CascadeSelectClasses["item"] = "p-cascadeselect-item"; /** * Class name of the item content element */ CascadeSelectClasses["itemContent"] = "p-cascadeselect-item-content"; /** * Class name of the item text element */ CascadeSelectClasses["itemText"] = "p-cascadeselect-item-text"; /** * Class name of the group icon element */ CascadeSelectClasses["groupIcon"] = "p-cascadeselect-group-icon"; /** * Class name of the item list element */ CascadeSelectClasses["itemList"] = "p-cascadeselect-item-list"; })(CascadeSelectClasses || (CascadeSelectClasses = {})); const CASCADESELECT_VALUE_ACCESSOR = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CascadeSelect), multi: true }; class CascadeSelectSub extends BaseComponent { cascadeselect; role; selectId; activeOptionPath; optionDisabled; focusedOptionId; options; optionGroupChildren; optionTemplate; groupicon; level = 0; optionLabel; optionValue; optionGroupLabel; dirty; root; onChange = new EventEmitter(); onFocusChange = new EventEmitter(); onFocusEnterChange = new EventEmitter(); _componentStyle = inject(CascadeSelectStyle); get listLabel() { return this.config.getTranslation(TranslationKeys.ARIA)['listLabel']; } constructor(cascadeselect) { super(); this.cascadeselect = cascadeselect; } ngOnInit() { super.ngOnInit(); if (!this.root) { this.position(); } } onOptionClick(event, processedOption) { this.onChange.emit({ originalEvent: event, processedOption, isFocus: true }); } onOptionMouseEnter(event, processedOption) { this.onFocusEnterChange.emit({ originalEvent: event, processedOption }); } onOptionMouseMove(event, processedOption) { this.onFocusChange.emit({ originalEvent: event, processedOption }); } getOptionId(processedOption) { return `${this.selectId}_${processedOption.key}`; } getOptionLabel(processedOption) { return this.optionLabel ? resolveFieldData(processedOption.option, this.optionLabel) : processedOption.option; } getOptionValue(processedOption) { return this.optionValue ? resolveFieldData(processedOption.option, this.optionValue) : processedOption.option; } getOptionLabelToRender(processedOption) { return this.isOptionGroup(processedOption) ? this.getOptionGroupLabel(processedOption) : this.getOptionLabel(processedOption); } isOptionDisabled(processedOption) { return this.optionDisabled ? resolveFieldData(processedOption.option, this.optionDisabled) : false; } getOptionGroupLabel(processedOption) { return this.optionGroupLabel ? resolveFieldData(processedOption.option, this.optionGroupLabel) : null; } getOptionGroupChildren(processedOption) { return processedOption.children; } isOptionGroup(processedOption) { return isNotEmpty(processedOption.children); } isOptionSelected(processedOption) { return equals(this.cascadeselect?.modelValue(), processedOption?.option); } isOptionActive(processedOption) { return this.activeOptionPath.some((path) => path.key === processedOption.key); } isOptionFocused(processedOption) { return this.focusedOptionId === this.getOptionId(processedOption); } position() { const parentItem = this.el.nativeElement.parentElement; const containerOffset = getOffset(parentItem); const viewport = getViewport(); const sublistWidth = this.el.nativeElement.children[0].offsetParent ? this.el.nativeElement.children[0].offsetWidth : getHiddenElementOuterWidth(this.el.nativeElement.children[0]); const itemOuterWidth = getOuterWidth(parentItem.children[0]); if (parseInt(containerOffset.left, 10) + itemOuterWidth + sublistWidth > viewport.width - calculateScrollbarWidth()) { this.el.nativeElement.children[0].style.left = '-200%'; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CascadeSelectSub, deps: [{ token: CascadeSelect }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "20.1.3", type: CascadeSelectSub, isStandalone: true, selector: "p-cascadeSelectSub, p-cascadeselect-sub", inputs: { role: "role", selectId: "selectId", activeOptionPath: "activeOptionPath", optionDisabled: "optionDisabled", focusedOptionId: "focusedOptionId", options: "options", optionGroupChildren: "optionGroupChildren", optionTemplate: "optionTemplate", groupicon: "groupicon", level: ["level", "level", numberAttribute], optionLabel: "optionLabel", optionValue: "optionValue", optionGroupLabel: "optionGroupLabel", dirty: ["dirty", "dirty", booleanAttribute], root: ["root", "root", booleanAttribute] }, outputs: { onChange: "onChange", onFocusChange: "onFocusChange", onFocusEnterChange: "onFocusEnterChange" }, providers: [CascadeSelectStyle], usesInheritance: true, ngImport: i0, template: ` <ul [class]="cx('list')" [attr.role]="role" aria-orientation="horizontal" [attr.data-pc-section]="level === 0 ? 'list' : 'sublist'" [attr.aria-label]="listLabel"> <ng-template ngFor let-processedOption [ngForOf]="options" let-i="index"> <li [class]="cx('option', { processedOption })" role="treeitem" [attr.aria-level]="level + 1" [attr.aria-setsize]="options.length" [attr.data-pc-section]="'item'" [id]="getOptionId(processedOption)" [attr.aria-label]="getOptionLabelToRender(processedOption)" [attr.aria-selected]="isOptionGroup(processedOption) ? undefined : isOptionSelected(processedOption)" [attr.aria-posinset]="i + 1" > <div [class]="cx('optionContent')" (click)="onOptionClick($event, processedOption)" (mouseenter)="onOptionMouseEnter($event, processedOption)" (mousemove)="onOptionMouseMove($event, processedOption)" pRipple [attr.data-pc-section]="'content'" > <ng-container *ngIf="optionTemplate; else defaultOptionTemplate"> <ng-container *ngTemplateOutlet="optionTemplate; context: { $implicit: processedOption?.option }"></ng-container> </ng-container> <ng-template #defaultOptionTemplate> <span [class]="cx('optionText')" [attr.data-pc-section]="'text'">{{ getOptionLabelToRender(processedOption) }}</span> </ng-template> <span [class]="cx('groupIcon')" *ngIf="isOptionGroup(processedOption)" [attr.data-pc-section]="'groupIcon'"> <svg data-p-icon="angle-right" *ngIf="!groupicon" /> <ng-template *ngTemplateOutlet="groupicon"></ng-template> </span> </div> <p-cascadeselect-sub *ngIf="isOptionGroup(processedOption) && isOptionActive(processedOption)" [role]="'group'" [class]="cx('optionList')" [selectId]="selectId" [focusedOptionId]="focusedOptionId" [activeOptionPath]="activeOptionPath" [options]="getOptionGroupChildren(processedOption)" [optionLabel]="optionLabel" [optionValue]="optionValue" [level]="level + 1" (onChange)="onChange.emit($event)" (onFocusChange)="onFocusChange.emit($event)" (onFocusEnterChange)="onFocusEnterChange.emit($event)" [optionGroupLabel]="optionGroupLabel" [optionGroupChildren]="optionGroupChildren" [dirty]="dirty" [optionTemplate]="optionTemplate" > </p-cascadeselect-sub> </li> </ng-template> </ul> `, isInline: true, dependencies: [{ kind: "component", type: CascadeSelectSub, selector: "p-cascadeSelectSub, p-cascadeselect-sub", inputs: ["role", "selectId", "activeOptionPath", "optionDisabled", "focusedOptionId", "options", "optionGroupChildren", "optionTemplate", "groupicon", "level", "optionLabel", "optionValue", "optionGroupLabel", "dirty", "root"], outputs: ["onChange", "onFocusChange", "onFocusEnterChange"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { 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: Ripple, selector: "[pRipple]" }, { kind: "component", type: AngleRightIcon, selector: "[data-p-icon=\"angle-right\"]" }, { kind: "ngmodule", type: SharedModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CascadeSelectSub, decorators: [{ type: Component, args: [{ selector: 'p-cascadeSelectSub, p-cascadeselect-sub', standalone: true, imports: [CommonModule, Ripple, AngleRightIcon, SharedModule], template: ` <ul [class]="cx('list')" [attr.role]="role" aria-orientation="horizontal" [attr.data-pc-section]="level === 0 ? 'list' : 'sublist'" [attr.aria-label]="listLabel"> <ng-template ngFor let-processedOption [ngForOf]="options" let-i="index"> <li [class]="cx('option', { processedOption })" role="treeitem" [attr.aria-level]="level + 1" [attr.aria-setsize]="options.length" [attr.data-pc-section]="'item'" [id]="getOptionId(processedOption)" [attr.aria-label]="getOptionLabelToRender(processedOption)" [attr.aria-selected]="isOptionGroup(processedOption) ? undefined : isOptionSelected(processedOption)" [attr.aria-posinset]="i + 1" > <div [class]="cx('optionContent')" (click)="onOptionClick($event, processedOption)" (mouseenter)="onOptionMouseEnter($event, processedOption)" (mousemove)="onOptionMouseMove($event, processedOption)" pRipple [attr.data-pc-section]="'content'" > <ng-container *ngIf="optionTemplate; else defaultOptionTemplate"> <ng-container *ngTemplateOutlet="optionTemplate; context: { $implicit: processedOption?.option }"></ng-container> </ng-container> <ng-template #defaultOptionTemplate> <span [class]="cx('optionText')" [attr.data-pc-section]="'text'">{{ getOptionLabelToRender(processedOption) }}</span> </ng-template> <span [class]="cx('groupIcon')" *ngIf="isOptionGroup(processedOption)" [attr.data-pc-section]="'groupIcon'"> <svg data-p-icon="angle-right" *ngIf="!groupicon" /> <ng-template *ngTemplateOutlet="groupicon"></ng-template> </span> </div> <p-cascadeselect-sub *ngIf="isOptionGroup(processedOption) && isOptionActive(processedOption)" [role]="'group'" [class]="cx('optionList')" [selectId]="selectId" [focusedOptionId]="focusedOptionId" [activeOptionPath]="activeOptionPath" [options]="getOptionGroupChildren(processedOption)" [optionLabel]="optionLabel" [optionValue]="optionValue" [level]="level + 1" (onChange)="onChange.emit($event)" (onFocusChange)="onFocusChange.emit($event)" (onFocusEnterChange)="onFocusEnterChange.emit($event)" [optionGroupLabel]="optionGroupLabel" [optionGroupChildren]="optionGroupChildren" [dirty]="dirty" [optionTemplate]="optionTemplate" > </p-cascadeselect-sub> </li> </ng-template> </ul> `, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [CascadeSelectStyle] }] }], ctorParameters: () => [{ type: CascadeSelect }], propDecorators: { role: [{ type: Input }], selectId: [{ type: Input }], activeOptionPath: [{ type: Input }], optionDisabled: [{ type: Input }], focusedOptionId: [{ type: Input }], options: [{ type: Input }], optionGroupChildren: [{ type: Input }], optionTemplate: [{ type: Input }], groupicon: [{ type: Input }], level: [{ type: Input, args: [{ transform: numberAttribute }] }], optionLabel: [{ type: Input }], optionValue: [{ type: Input }], optionGroupLabel: [{ type: Input }], dirty: [{ type: Input, args: [{ transform: booleanAttribute }] }], root: [{ type: Input, args: [{ transform: booleanAttribute }] }], onChange: [{ type: Output }], onFocusChange: [{ type: Output }], onFocusEnterChange: [{ type: Output }] } }); /** * CascadeSelect is a form component to select a value from a nested structure of options. * @group Components */ class CascadeSelect extends BaseEditableHolder { overlayService; /** * Unique identifier of the component * @group Props */ id; /** * Text to display when the search is active. Defaults to global value in i18n translation configuration. * @group Props * @defaultValue '{0} results are available' */ searchMessage; /** * Text to display when there is no data. Defaults to global value in i18n translation configuration. * @group Props */ emptyMessage; /** * Text to be displayed in hidden accessible field when options are selected. Defaults to global value in i18n translation configuration. * @group Props * @defaultValue '{0} items selected' */ selectionMessage; /** * Text to display when filtering does not return any results. Defaults to value from PrimeNG locale configuration. * @group Props * @defaultValue 'No available options' */ emptySearchMessage; /** * Text to display when filtering does not return any results. Defaults to global value in i18n translation configuration. * @group Props * @defaultValue 'No selected item' */ emptySelectionMessage; /** * Locale to use in searching. The default locale is the host environment's current locale. * @group Props */ searchLocale; /** * Name of the disabled field of an option. * @group Props */ optionDisabled; /** * 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; /** * Style class of the component. * @deprecated since v20.0.0, use `class` instead. * @group Props */ styleClass; /** * An array of selectitems to display as the available options. * @group Props */ options; /** * Property name or getter function to use as the label of an option. * @group Props */ optionLabel; /** * Property name or getter function to use as the value of an option, defaults to the option itself when not defined. * @group Props */ optionValue; /** * Property name or getter function to use as the label of an option group. * @group Props */ optionGroupLabel; /** * Property name or getter function to retrieve the items of a group. * @group Props */ optionGroupChildren; /** * Default text to display when no option is selected. * @group Props */ placeholder; /** * Selected value of the component. * @group Props */ value; /** * A property to uniquely identify an option. * @group Props */ dataKey; /** * Identifier of the underlying input element. * @group Props */ inputId; /** * Index of the element in tabbing order. * @group Props */ tabindex = 0; /** * Establishes relationships between the component and label(s) where its value should be one or more element IDs. * @group Props */ ariaLabelledBy; /** * Label of the input for accessibility. * @group Props */ inputLabel; /** * Defines a string that labels the input for accessibility. * @group Props */ ariaLabel; /** * When enabled, a clear icon is displayed to clear the value. * @group Props */ showClear = false; /** * Style class of the overlay panel. * @group Props */ panelStyleClass; /** * Inline style of the overlay panel. * @group Props */ panelStyle; /** * Whether to use overlay API feature. The properties of overlay API can be used like an object in it. * @group Props */ overlayOptions; /** * When present, it specifies that the component should automatically get focus on load. * @group Props */ autofocus; /** * Whether the dropdown is in loading state. * @group Props */ loading = false; /** * Icon to display in loading state. * @group Props */ loadingIcon; /** * The breakpoint to define the maximum width boundary. * @group Props */ breakpoint = '960px'; /** * 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 on value change. * @param {CascadeSelectChangeEvent} event - Custom change event. * @group Emits */ onChange = new EventEmitter(); /** * Callback to invoke when a group changes. * @param {Event} event - Browser event. * @group Emits */ onGroupChange = new EventEmitter(); /** * Callback to invoke when the overlay is shown. * @param {CascadeSelectShowEvent} event - Custom overlay show event. * @group Emits */ onShow = new EventEmitter(); /** * Callback to invoke when the overlay is hidden. * @param {CascadeSelectHideEvent} event - Custom overlay hide event. * @group Emits */ onHide = new EventEmitter(); /** * Callback to invoke when the clear token is clicked. * @group Emits */ onClear = new EventEmitter(); /** * Callback to invoke before overlay is shown. * @param {CascadeSelectBeforeShowEvent} event - Custom overlay show event. * @group Emits */ onBeforeShow = new EventEmitter(); /** * Callback to invoke before overlay is hidden. * @param {CascadeSelectBeforeHideEvent} event - Custom overlay hide event. * @group Emits */ onBeforeHide = new EventEmitter(); /** * Callback to invoke when input receives focus. * @param {FocusEvent} event - Focus event. * @group Emits */ onFocus = new EventEmitter(); /** * Callback to invoke when input loses focus. * @param {FocusEvent} event - Focus event. * @group Emits */ onBlur = new EventEmitter(); focusInputViewChild; panelViewChild; overlayViewChild; /** * Content template for displaying the selected value. * @group Templates */ valueTemplate; /** * Content template for customizing the option display. * @group Templates */ optionTemplate; /** * Content template for customizing the header. * @group Templates */ headerTemplate; /** * Content template for customizing the footer. * @group Templates */ footerTemplate; /** * Content template for customizing the trigger icon. * @group Templates */ triggerIconTemplate; /** * Content template for customizing the loading icon. * @group Templates */ loadingIconTemplate; /** * Content template for customizing the group icon. * @group Templates */ groupIconTemplate; /** * Content template for customizing the clear icon. * @group Templates */ clearIconTemplate; _valueTemplate; _optionTemplate; _headerTemplate; _footerTemplate; _triggerIconTemplate; _loadingIconTemplate; _groupIconTemplate; _clearIconTemplate; selectionPath = null; focused = false; overlayVisible = false; clicked = false; dirty = false; searchValue; searchTimeout; focusedOptionInfo = signal({ index: -1, level: 0, parentKey: '' }, ...(ngDevMode ? [{ debugName: "focusedOptionInfo" }] : [])); activeOptionPath = signal([], ...(ngDevMode ? [{ debugName: "activeOptionPath" }] : [])); processedOptions = []; _componentStyle = inject(CascadeSelectStyle); initialized = false; $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; } onHostClick(event) { this.onContainerClick(event); } get focusedOptionId() { return this.focusedOptionInfo().index !== -1 ? `${this.id}${isNotEmpty(this.focusedOptionInfo().parentKey) ? '_' + this.focusedOptionInfo().parentKey : ''}_${this.focusedOptionInfo().index}` : null; } get searchResultMessageText() { return isNotEmpty(this.visibleOptions()) ? this.searchMessageText.replaceAll('{0}', this.visibleOptions().length) : this.emptySearchMessageText; } get searchMessageText() { return this.searchMessage || this.config.translation.searchMessage || ''; } get emptySearchMessageText() { return this.emptySearchMessage || this.config.translation.emptySearchMessage || ''; } get emptyMessageText() { return this.emptyMessage || this.config.translation.emptyMessage || ''; } get selectionMessageText() { return this.selectionMessage || this.config.translation.selectionMessage || ''; } get emptySelectionMessageText() { return this.emptySelectionMessage || this.config.translation.emptySelectionMessage || ''; } get selectedMessageText() { return this.hasSelectedOption ? this.selectionMessageText.replaceAll('{0}', '1') : this.emptySelectionMessageText; } visibleOptions = computed(() => { const processedOption = this.activeOptionPath().find((p) => p.key === this.focusedOptionInfo().parentKey); return processedOption ? processedOption.children : this.processedOptions; }, ...(ngDevMode ? [{ debugName: "visibleOptions" }] : [])); label = computed(() => { const label = this.placeholder || 'p-emptylabel'; if (this.hasSelectedOption()) { const activeOptionPath = this.findOptionPathByValue(this.modelValue(), null); const processedOption = isNotEmpty(activeOptionPath) ? activeOptionPath[activeOptionPath.length - 1] : null; return processedOption ? this.getOptionLabel(processedOption.option) : label; } return label; }, ...(ngDevMode ? [{ debugName: "label" }] : [])); get _label() { const label = this.placeholder || 'p-emptylabel'; if (this.hasSelectedOption()) { const activeOptionPath = this.findOptionPathByValue(this.modelValue(), null); const processedOption = isNotEmpty(activeOptionPath) ? activeOptionPath[activeOptionPath.length - 1] : null; return processedOption ? this.getOptionLabel(processedOption.option) : label; } return label; } templates; ngAfterContentInit() { this.templates.forEach((item) => { switch (item.getType()) { case 'value': this._valueTemplate = item.template; break; case 'option': this._optionTemplate = item.template; break; case 'triggericon': this._triggerIconTemplate = item.template; break; case 'loadingicon': this._loadingIconTemplate = item.template; break; case 'clearicon': this._clearIconTemplate = item.template; break; case 'optiongroupicon': this._groupIconTemplate = item.template; break; } }); } ngOnChanges(changes) { super.ngOnChanges(changes); if (changes.options) { this.processedOptions = this.createProcessedOptions(changes.options.currentValue || []); this.updateModel(null); } } hasSelectedOption() { return isNotEmpty(this.modelValue()); } createProcessedOptions(options, level = 0, parent = {}, parentKey = '') { const processedOptions = []; options && options.forEach((option, index) => { const key = (parentKey !== '' ? parentKey + '_' : '') + index; const newOption = { option, index, level, key, parent, parentKey }; newOption['children'] = this.createProcessedOptions(this.getOptionGroupChildren(option, level), level + 1, newOption, key); processedOptions.push(newOption); }); return processedOptions; } onInputFocus(event) { if (this.$disabled()) { // For screenreaders return; } this.focused = true; this.onFocus.emit(event); } onInputBlur(event) { this.focused = false; this.focusedOptionInfo.set({ indeX: -1, level: 0, parentKey: '' }); this.searchValue = ''; this.onModelTouched(); this.onBlur.emit(event); } onInputKeyDown(event) { if (this.$disabled() || this.loading) { 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 'ArrowLeft': this.onArrowLeftKey(event); break; case 'ArrowRight': this.onArrowRightKey(event); break; case 'Home': this.onHomeKey(event); break; case 'End': this.onEndKey(event); break; case 'Space': this.onSpaceKey(event); break; case 'Enter': case 'NumpadEnter': this.onEnterKey(event); break; case 'Escape': this.onEscapeKey(event); break; case 'Tab': this.onTabKey(event); break; case 'Backspace': this.onBackspaceKey(event); break; case 'PageDown': case 'PageUp': case 'ShiftLeft': case 'ShiftRight': //NOOP break; default: if (!metaKey && isPrintableCharacter(event.key)) { !this.overlayVisible && this.show(); this.searchOptions(event, event.key); } break; } this.clicked = false; } onArrowDownKey(event) { if (!this.overlayVisible) { this.show(); } else { const optionIndex = this.focusedOptionInfo().index !== -1 ? this.findNextOptionIndex(this.focusedOptionInfo().index) : this.clicked ? this.findFirstOptionIndex() : this.findFirstFocusedOptionIndex(); this.changeFocusedOptionIndex(event, optionIndex, true); } event.preventDefault(); } onArrowUpKey(event) { if (event.altKey) { if (this.focusedOptionInfo().index !== -1) { const processedOption = this.visibleOptions[this.focusedOptionInfo().index]; const grouped = this.isProccessedOptionGroup(processedOption); !grouped && this.onOptionChange({ originalEvent: event, processedOption }); } this.overlayVisible && this.hide(); event.preventDefault(); } else { const optionIndex = this.focusedOptionInfo().index !== -1 ? this.findPrevOptionIndex(this.focusedOptionInfo().index) : this.clicked ? this.findLastOptionIndex() : this.findLastFocusedOptionIndex(); this.changeFocusedOptionIndex(event, optionIndex, true); !this.overlayVisible && this.show(); event.preventDefault(); } } onArrowLeftKey(event) { if (this.overlayVisible) { const processedOption = this.visibleOptions()[this.focusedOptionInfo().index]; const parentOption = this.activeOptionPath().find((p) => p.key === processedOption.parentKey); const matched = this.focusedOptionInfo().parentKey === '' || (parentOption && parentOption.key === this.focusedOptionInfo().parentKey); const root = isEmpty(processedOption.parent); if (matched) { const activeOptionPath = this.activeOptionPath().filter((p) => p.parentKey !== this.focusedOptionInfo().parentKey); this.activeOptionPath.set(activeOptionPath); } if (!root) { this.focusedOptionInfo.set({ index: -1, parentKey: parentOption ? parentOption.parentKey : '' }); this.searchValue = ''; this.onArrowDownKey(event); } event.preventDefault(); } } onArrowRightKey(event) { if (this.overlayVisible) { const processedOption = this.visibleOptions()[this.focusedOptionInfo().index]; const grouped = this.isProccessedOptionGroup(processedOption); if (grouped) { const matched = this.activeOptionPath().some((p) => processedOption.key === p.key); if (matched) { this.focusedOptionInfo.set({ index: -1, parentKey: processedOption.key }); this.searchValue = ''; this.onArrowDownKey(event); } else { this.onOptionChange({ originalEvent: event, processedOption }); } } event.preventDefault(); } } onHomeKey(event) { this.changeFocusedOptionIndex(event, this.findFirstOptionIndex()); !this.overlayVisible && this.show(); event.preventDefault(); } onEndKey(event) { this.changeFocusedOptionIndex(event, this.findLastOptionIndex()); !this.overlayVisible && this.show(); event.preventDefault(); } onEnterKey(event) { if (!this.overlayVisible) { this.focusedOptionInfo.set({ ...this.focusedOptionInfo(), index: -1 }); // reset this.onArrowDownKey(event); } else { if (this.focusedOptionInfo().index !== -1) { const processedOption = this.visibleOptions()[this.focusedOptionInfo().index]; const grouped = this.isProccessedOptionGroup(processedOption); this.onOptionClick({ originalEvent: event, processedOption }); !grouped && this.hide(); } } event.preventDefault(); } onSpaceKey(event) { this.onEnterKey(event); } onEscapeKey(event) { this.overlayVisible && this.hide(event, true); event.preventDefault(); } onTabKey(event) { if (this.focusedOptionInfo().index !== -1) { const processedOption = this.visibleOptions()[this.focusedOptionInfo().index]; const grouped = this.isProccessedOptionGroup(processedOption); !grouped && this.onOptionChange({ originalEvent: event, processedOption }); } this.overlayVisible && this.hide(); } onBackspaceKey(event) { if (isNotEmpty(this.modelValue()) && this.showClear) { this.clear(); } event.stopPropagation(); } equalityKey() { return this.optionValue ? null : this.dataKey; } updateModel(value, event) { this.value = value; this.onModelChange(value); this.writeModelValue(value); if (this.initialized) { this.onChange.emit({ originalEvent: event, value: value }); } } autoUpdateModel() { if (this.selectOnFocus && this.autoOptionFocus && !this.hasSelectedOption()) { this.focusedOptionInfo().index = this.findFirstFocusedOptionIndex(); this.onOptionChange({ originalEvent: null, processedOption: this.visibleOptions()[this.focusedOptionInfo().index], isHide: false }); !this.overlayVisible && this.focusedOptionInfo.set({ index: -1, level: 0, parentKey: '' }); } } scrollInView(index = -1) { const id = index !== -1 ? `${this.id}_${index}` : this.focusedOptionId; const element = findSingle(this.panelViewChild?.nativeElement, `li[id="${id}"]`); if (element) { element.scrollIntoView && element.scrollIntoView({ block: 'nearest', inline: 'start' }); } } changeFocusedOptionIndex(event, index, preventSelection) { const focusedOptionInfo = this.focusedOptionInfo(); if (focusedOptionInfo.index !== index) { this.focusedOptionInfo.set({ ...focusedOptionInfo, index }); this.scrollInView(); if (this.focusOnHover) { this.onOptionClick({ originalEvent: event, processedOption: this.visibleOptions()[index], isHide: false, preventSelection }); } if (this.selectOnFocus) { this.onOptionChange({ originalEvent: event, processedOption: this.visibleOptions()[index], isHide: false }); } } } matchMediaListener; onOptionSelect(event) { const { originalEvent, value, isHide } = event; const newValue = this.getOptionValue(value); const activeOptionPath = this.activeOptionPath(); activeOptionPath.forEach((p) => (p.selected = true)); this.activeOptionPath.set(activeOptionPath); this.updateModel(newValue, originalEvent); isHide && this.hide(event, true); } onOptionGroupSelect(event) { this.dirty = true; this.onGroupChange.emit(event); } onContainerClick(event) { if (this.$disabled() || this.loading) { return; } if (!this.overlayViewChild?.el?.nativeElement?.contains(event.target)) { if (this.overlayVisible) { this.hide(); } else { this.show(); } this.focusInputViewChild?.nativeElement.focus(); } this.clicked = true; } isOptionMatched(processedOption) { return this.isValidOption(processedOption) && this.getProccessedOptionLabel(processedOption).toLocaleLowerCase(this.searchLocale).startsWith(this.searchValue.toLocaleLowerCase(this.searchLocale)); } isOptionDisabled(option) { return this.optionDisabled ? resolveFieldData(option, this.optionDisabled) : false; } isValidOption(processedOption) { return !!processedOption && !this.isOptionDisabled(processedOption.option); } isValidSelectedOption(processedOption) { return this.isValidOption(processedOption) && this.isSelected(processedOption); } isSelected(processedOption) { return this.activeOptionPath().some((p) => p.key === processedOption.key); } findOptionPathByValue(value, processedOptions, level = 0) { processedOptions = processedOptions || (level === 0 && this.processedOptions); if (!processedOptions) return null; if (isEmpty(value)) return []; for (let i = 0; i < processedOptions.length; i++) { const processedOption = processedOptions[i]; if (equals(value, this.getOptionValue(processedOption.option), this.equalityKey())) { return [processedOption]; } const matchedOptions = this.findOptionPathByValue(value, processedOption.children, level + 1); if (matchedOptions) { matchedOptions.unshift(processedOption); return matchedOptions; } } } findFirstOptionIndex() { return this.visibleOptions().findIndex((processedOption) => this.isValidOption(processedOption)); } findLastOptionIndex() { return findLastIndex(this.visibleOptions(), (processedOption) => this.isValidOption(processedOption)); } findNextOptionIndex(index) { const matchedOptionIndex = index < this.visibleOptions().length - 1 ? this.visibleOptions() .slice(index + 1) .findIndex((processedOption) => this.isValidOption(processedOption)) : -1; return matchedOptionIndex > -1 ? matchedOptionIndex + index + 1 : index; } findPrevOptionIndex(index) { const matchedOptionIndex = index > 0 ? findLastIndex(this.visibleOptions().slice(0, index), (processedOption) => this.isValidOption(processedOption)) : -1; return matchedOptionIndex > -1 ? matchedOptionIndex : index; } findSelectedOptionIndex() { return this.visibleOptions().findIndex((processedOption) => this.isValidSelectedOption(processedOption)); } findFirstFocusedOptionIndex() { const selectedIndex = this.findSelectedOptionIndex(); return selectedIndex < 0 ? this.findFirstOptionIndex() : selectedIndex; } findLastFocusedOptionIndex() { const selectedIndex = this.findSelectedOptionIndex(); return selectedIndex < 0 ? this.findLastOptionIndex() : selectedIndex; } searchOptions(event, char) { this.searchValue = (this.searchValue || '') + char; let optionIndex = -1; let matched = false; const focusedOptionInfo = this.focusedOptionInfo(); if (focusedOptionInfo.index !== -1) { optionIndex = this.visibleOptions() .slice(focusedOptionInfo.index) .findIndex((processedOption) => this.isOptionMatched(processedOption)); optionIndex = optionIndex === -1 ? this.visibleOptions() .slice(0, focusedOptionInfo.index) .findIndex((processedOption) => this.isOptionMatched(processedOption)) : optionIndex + focusedOptionInfo.index; } else { optionIndex = this.visibleOptions().findIndex((processedOption) => this.isOptionMatched(processedOption)); } if (optionIndex !== -1) { matched = true; } if (optionIndex === -1 && focusedOptionInfo.index === -1) { optionIndex = this.findFirstFocusedOptionIndex(); } if (optionIndex !== -1) { this.changeFocusedOptionIndex(event, optionIndex); } if (this.searchTimeout) { clearTimeout(this.searchTimeout); } this.searchTimeout = setTimeout(() => { this.searchValue = ''; this.searchTimeout = null; }, 500); return matched; } hide(event, isFocus = false) { const _hide = () => { this.overlayVisible = false; this.clicked = false; this.activeOptionPath.set([]); this.focusedOptionInfo.set({ index: -1, level: 0, parentKey: '' }); isFocus && f