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,282 lines (1,241 loc) 91.3 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, ChangeDetectionStrategy, ViewEncapsulation, Component, signal, inject, computed, effect, ContentChildren, 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 { AngleRightIcon, ChevronDownIcon, TimesIcon } from 'primeng/icons'; import { Overlay } from 'primeng/overlay'; import { Ripple } from 'primeng/ripple'; import { BaseStyle } from 'primeng/base'; const theme = ({ dt }) => ` .p-cascadeselect { display: inline-flex; cursor: pointer; position: relative; user-select: none; background: ${dt('cascadeselect.background')}; border: 1px solid ${dt('cascadeselect.border.color')}; transition: background ${dt('cascadeselect.transition.duration')}, color ${dt('cascadeselect.transition.duration')}, border-color ${dt('cascadeselect.transition.duration')}, outline-color ${dt('cascadeselect.transition.duration')}, box-shadow ${dt('cascadeselect.transition.duration')}; border-radius: ${dt('cascadeselect.border.radius')}; outline-color: transparent; box-shadow: ${dt('cascadeselect.shadow')}; } p-cascadeSelect.ng-invalid.ng-dirty .p-cascadeselect, p-cascade-select.ng-invalid.ng-dirty .p-cascadeselect, p-cascadeselect.ng-invalid.ng-dirty .p-cascadeselect { border-color: ${dt('cascadeselect.invalid.border.color')}; } p-cascadeSelect.ng-invalid.ng-dirty .p-cascadeselect.p-focus, p-cascade-select.ng-invalid.ng-dirty .p-cascadeselect.p-focus, p-cascadeselect.ng-invalid.ng-dirty .p-cascadeselect.p-focus { border-color: ${dt('cascadeselect.focus.border.color')}; } .p-cascadeselect:not(.p-disabled):hover { border-color: ${dt('cascadeselect.hover.border.color')}; } .p-cascadeselect:not(.p-disabled).p-focus { border-color: ${dt('cascadeselect.focus.border.color')}; box-shadow: ${dt('cascadeselect.focus.ring.shadow')}; outline: ${dt('cascadeselect.focus.ring.width')} ${dt('cascadeselect.focus.ring.style')} ${dt('cascadeselect.focus.ring.color')}; outline-offset: ${dt('multiscascadeselectelect.focus.ring.offset')}; } .p-cascadeselect.p-variant-filled { background: ${dt('cascadeselect.filled.background')}; } .p-cascadeselect.p-variant-filled:not(.p-disabled):hover { background: ${dt('cascadeselect.filled.hover.background')}; } .p-cascadeselect.p-variant-filled.p-focus { background: ${dt('cascadeselect.filled.focus.background')}; } .p-cascadeselect.p-disabled { opacity: 1; background: ${dt('cascadeselect.disabled.background')}; } .p-cascadeselect-dropdown { display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: transparent; color: ${dt('cascadeselect.dropdown.color')}; width: ${dt('cascadeselect.dropdown.width')}; border-start-end-radius: ${dt('border.radius.md')}; border-end-end-radius: ${dt('border.radius.md')}; } .p-cascadeselect-label { display: block; white-space: nowrap; overflow: hidden; flex: 1 1 auto; width: 1%; text-overflow: ellipsis; cursor: pointer; padding: ${dt('cascadeselect.padding.y')} ${dt('cascadeselect.padding.x')}; background: transparent; border: 0 none; outline: 0 none; } .p-cascadeselect-label.p-placeholder { color: ${dt('cascadeselect.placeholder.color')}; } p-cascadeselect.ng-invalid.ng-dirty .p-cascadeselect-label.p-placeholder { color: ${dt('cascadeselect.invalid.placeholder.color')}; } .p-cascadeselect.p-disabled .p-cascadeselect-label { color: ${dt('cascadeselect.disabled.color')}; } .p-cascadeselect-label-empty { overflow: hidden; visibility: hidden; } .p-cascadeselect-fluid { display: flex; } .p-cascadeselect-fluid .p-cascadeselect-label { width: 1%; } .p-cascadeselect-overlay { background: ${dt('cascadeselect.overlay.background')}; color: ${dt('cascadeselect.overlay.color')}; border: 1px solid ${dt('cascadeselect.overlay.border.color')}; border-radius: ${dt('cascadeselect.overlay.border.radius')}; box-shadow: ${dt('cascadeselect.overlay.shadow')}; } .p-cascadeselect .p-cascadeselect-overlay { min-width: 100%; } .p-cascadeselect-option-list { display: none; min-width: 100%; position: absolute; z-index: 1; } .p-cascadeselect-list { min-width: 100%; margin: 0; padding: 0; list-style-type: none; padding: ${dt('cascadeselect.list.padding')}; display: flex; flex-direction: column; gap: ${dt('cascadeselect.list.gap')} } .p-cascadeselect-option { cursor: pointer; font-weight: normal; white-space: nowrap; border: 0 none; color: ${dt('cascadeselect.option.color')}; background: transparent; transition: background ${dt('cascadeselect.transition.duration')}, color ${dt('cascadeselect.transition.duration')}, border-color ${dt('cascadeselect.transition.duration')}, box-shadow ${dt('cascadeselect.transition.duration')}, outline-color ${dt('cascadeselect.transition.duration')}; border-radius: ${dt('cascadeselect.option.border.radius')}; } .p-cascadeselect-option-active { overflow: visible; } .p-cascadeselect-option-active > .p-cascadeselect-option-content { background: ${dt('cascadeselect.option.focus.background')}; color: ${dt('cascadeselect.option.focus.color')}; } .p-cascadeselect-option:not(.p-cascadeselect-option-selected):not(.p-disabled).p-focus > .p-cascadeselect-option-content { background: ${dt('cascadeselect.option.focus.background')}; color: ${dt('cascadeselect.option.focus.color')}; } .p-cascadeselect-option:not(.p-cascadeselect-option-selected):not(.p-disabled).p-focus > .p-cascadeselect-option-content > .p-cascadeselect-group-icon-container > .p-cascadeselect-group-icon { color: ${dt('cascadeselect.option.icon.focus.color')}; } .p-cascadeselect-option-selected > .p-cascadeselect-option-content { background: ${dt('cascadeselect.option.selected.background')}; color: ${dt('cascadeselect.option.selected.color')}; } .p-cascadeselect-option-selected.p-focus > .p-cascadeselect-option-content { background: ${dt('cascadeselect.option.selected.focus.background')}; color: ${dt('cascadeselect.option.selected.focus.color')}; } .p-cascadeselect-option-active > .p-cascadeselect-option-list { inset-inline-start: 100%; top: 0; } .p-cascadeselect-option-content { display: flex; align-items: center; justify-content: space-between; overflow: hidden; position: relative; padding: ${dt('cascadeselect.option.padding')}; border-radius: ${dt('cascadeselect.option.border.radius')}; transition: background ${dt('cascadeselect.transition.duration')}, color ${dt('cascadeselect.transition.duration')}, border-color ${dt('cascadeselect.transition.duration')}, box-shadow ${dt('cascadeselect.transition.duration')}, outline-color ${dt('cascadeselect.transition.duration')}; } .p-cascadeselect-group-icon { font-size: ${dt('cascadeselect.option.icon.size')}; width: ${dt('cascadeselect.option.icon.size')}; height: ${dt('cascadeselect.option.icon.size')}; color: ${dt('cascadeselect.option.icon.color')}; } .p-cascadeselect-group-icon:dir(rtl) { transform: rotate(180deg); } .p-cascadeselect-mobile-active .p-cascadeselect-option-list { position: static; box-shadow: none; border: 0 none; padding-inline-start: 1rem; padding-inline-end: 0; } .p-cascadeselect-mobile-active .p-cascadeselect-group-icon { transition: transform 0.2s; transform: rotate(90deg); } .p-cascadeselect-mobile-active .p-cascadeselect-option-active > .p-cascadeselect-option-content .p-cascadeselect-group-icon { transform: rotate(-90deg); } .p-cascadeselect-sm .p-cascadeselect-label { font-size: ${dt('cascadeselect.sm.font.size')}; padding-block: ${dt('cascadeselect.sm.padding.y')}; padding-inline: ${dt('cascadeselect.sm.padding.x')}; } .p-cascadeselect-sm .p-cascadeselect-dropdown .p-icon { font-size: ${dt('cascadeselect.sm.font.size')}; width: ${dt('cascadeselect.sm.font.size')}; height: ${dt('cascadeselect.sm.font.size')}; } .p-cascadeselect-lg .p-cascadeselect-label { font-size: ${dt('cascadeselect.lg.font.size')}; padding-block: ${dt('cascadeselect.lg.padding.y')}; padding-inline: ${dt('cascadeselect.lg.padding.x')}; } .p-cascadeselect-lg .p-cascadeselect-dropdown .p-icon { font-size: ${dt('cascadeselect.lg.font.size')}; width: ${dt('cascadeselect.lg.font.size')}; height: ${dt('cascadeselect.lg.font.size')}; } /* For PrimeNG */ .p-cascadeselect-clear-icon { cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: transparent; color: ${dt('cascadeselect.clear.icon.color')}; }`; const inlineStyles = { root: ({ props }) => ({ position: props.appendTo === 'self' ? 'relative' : undefined }) }; const classes = { root: ({ instance, props }) => [ 'p-cascadeselect p-component p-inputwrapper', { 'p-cascadeselect-mobile': instance.queryMatches(), 'p-disabled': props.disabled, 'p-invalid': props.invalid, 'p-variant-filled': props.variant ? props.variant === 'filled' : instance.config.inputStyle === 'filled' || instance.config.inputVariant === 'filled', 'p-focus': instance.focused, 'p-inputwrapper-filled': props.modelValue, 'p-inputwrapper-focus': instance.focused || instance.overlayVisible, 'p-cascadeselect-open': instance.overlayVisible, 'p-cascadeselect-fluid': props.fluid, 'p-cascadeselect-sm p-inputfield-sm': props.size === 'small', 'p-cascadeselect-lg p-inputfield-lg': props.size === 'large' } ], label: ({ instance, props }) => [ 'p-cascadeselect-label', { 'p-placeholder': instance.label === props.placeholder, 'p-cascadeselect-label-empty': !instance.$slots['value'] && (instance.label === 'p-emptylabel' || instance.label.length === 0) } ], dropdown: 'p-cascadeselect-dropdown', loadingIcon: 'p-cascadeselect-loading-icon', dropdownIcon: 'p-cascadeselect-dropdown-icon', overlay: ({ instance }) => [ 'p-cascadeselect-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-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-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: "19.2.5", ngImport: i0, type: CascadeSelectStyle, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: CascadeSelectStyle }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.5", 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["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(); 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); } getItemClass(option) { return { 'p-cascadeselect-option': true, 'p-cascadeselect-option-group': this.isOptionGroup(option), 'p-cascadeselect-option-active': this.isOptionActive(option), 'p-cascadeselect-option-selected': this.isOptionSelected(option), 'p-focus': this.isOptionFocused(option), 'p-disabled': this.isOptionDisabled(option) }; } 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: "19.2.5", ngImport: i0, type: CascadeSelectSub, deps: [{ token: CascadeSelect }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "19.2.5", 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" }, usesInheritance: true, ngImport: i0, template: ` <ul class="p-cascadeselect-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 [ngClass]="getItemClass(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="p-cascadeselect-option-content" (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="p-cascadeselect-option-text" [attr.data-pc-section]="'text'">{{ getOptionLabelToRender(processedOption) }}</span> </ng-template> <span class="p-cascadeselect-group-icon" *ngIf="isOptionGroup(processedOption)" [attr.data-pc-section]="'groupIcon'"> <AngleRightIcon *ngIf="!groupicon" /> <ng-template *ngTemplateOutlet="groupicon"></ng-template> </span> </div> <p-cascadeselect-sub *ngIf="isOptionGroup(processedOption) && isOptionActive(processedOption)" [role]="'group'" class="p-cascadeselect-list p-cascadeselect-overlay p-cascadeselect-option-list" [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.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: "AngleRightIcon" }, { kind: "ngmodule", type: SharedModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: CascadeSelectSub, decorators: [{ type: Component, args: [{ selector: 'p-cascadeSelectSub, p-cascadeselect-sub', standalone: true, imports: [CommonModule, Ripple, AngleRightIcon, SharedModule], template: ` <ul class="p-cascadeselect-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 [ngClass]="getItemClass(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="p-cascadeselect-option-content" (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="p-cascadeselect-option-text" [attr.data-pc-section]="'text'">{{ getOptionLabelToRender(processedOption) }}</span> </ng-template> <span class="p-cascadeselect-group-icon" *ngIf="isOptionGroup(processedOption)" [attr.data-pc-section]="'groupIcon'"> <AngleRightIcon *ngIf="!groupicon" /> <ng-template *ngTemplateOutlet="groupicon"></ng-template> </span> </div> <p-cascadeselect-sub *ngIf="isOptionGroup(processedOption) && isOptionActive(processedOption)" [role]="'group'" class="p-cascadeselect-list p-cascadeselect-overlay p-cascadeselect-option-list" [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 }] }], 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 BaseComponent { 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. * @group Props */ styleClass; /** * Inline style of the component. * @group Props */ style; /** * 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; /** * Defines the size of the component. * @group Props */ size; /** * 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; /** * Id of the element or "body" for document where the overlay should be appended to. * @group Props */ appendTo; /** * When present, it specifies that the component should be disabled. * @group Props */ disabled; /** * 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; /** * Transition options of the show animation. * @group Props * @deprecated 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.'); } /** * Specifies the input variant of the component. * @group Props */ variant; /** * Whether the dropdown is in loading state. * @group Props */ loading = false; /** * Icon to display in loading state. * @group Props */ loadingIcon; /** * Transition options of the hide animation. * @group Props * @deprecated 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.'); } /** * Spans 100% width of the container when enabled. * @group Props */ fluid = false; /** * The breakpoint to define the maximum width boundary. * @group Props */ breakpoint = '960px'; /** * 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; containerViewChild; 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; _showTransitionOptions = ''; _hideTransitionOptions = ''; selectionPath = null; focused = false; overlayVisible = false; clicked = false; dirty = false; searchValue; searchTimeout; onModelChange = () => { }; onModelTouched = () => { }; focusedOptionInfo = signal({ index: -1, level: 0, parentKey: '' }); activeOptionPath = signal([]); modelValue = signal(null); processedOptions = []; _componentStyle = inject(CascadeSelectStyle); get containerClass() { return { 'p-cascadeselect p-component p-inputwrapper': true, 'p-cascadeselect-clearable': this.showClear && !this.disabled, 'p-cascadeselect-mobile': this.queryMatches(), 'p-disabled': this.disabled, 'p-focus': this.focused, 'p-inputwrapper-filled': this.modelValue(), 'p-variant-filled': this.variant === 'filled' || this.config.inputStyle() === 'filled' || this.config.inputVariant() === 'filled', 'p-inputwrapper-focus': this.focused || this.overlayVisible, 'p-cascadeselect-open': this.overlayVisible, 'p-cascadeselect-fluid': this.hasFluid, 'p-cascadeselect-sm p-inputfield-sm': this.size === 'small', 'p-cascadeselect-lg p-inputfield-lg': this.size === 'large' }; } get labelClass() { return { 'p-cascadeselect-label': true, 'p-placeholder': this.label() === this.placeholder, 'p-cascadeselect-label-empty': !this.value && (this.label() === 'p-emptylabel' || this.label().length === 0) }; } get hasFluid() { const nativeElement = this.el.nativeElement; const fluidComponent = nativeElement.closest('p-fluid'); return this.fluid || !!fluidComponent; } get focusedOptionId() { return this.focusedOptionInfo().index !== -1 ? `${this.id}${isNotEmpty(this.focusedOptionInfo().parentKey) ? '_' + this.focusedOptionInfo().parentKey : ''}_${this.focusedOptionInfo().index}` : null; } get filled() { if (typeof this.modelValue() === 'string') return !!this.modelValue(); return this.modelValue() || this.modelValue() != null || this.modelValue() != undefined; } 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; }); 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; }); 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(ev