Refactor #5612 - Dropdown / Select

pull/5677/head
tugcekucukoglu 2024-04-18 17:17:41 +03:00
parent def5d060c0
commit 55ae9908d1
16 changed files with 1915 additions and 1778 deletions

View File

@ -2,735 +2,76 @@
*
* Dropdown also known as Select, is used to choose an item from a collection of options.
*
* [Live Demo](https://www.primevue.org/dropdown/)
* [Live Demo](https://www.primevue.org/select/)
*
* @module dropdown
*
*/
import { TransitionProps, VNode } from 'vue';
import { ComponentHooks } from '../basecomponent';
import { PassThroughOptions } from '../passthrough';
import { ClassComponent, DesignToken, GlobalComponentConstructor, HintedString, PassThrough } from '../ts-helpers';
import { VirtualScrollerItemOptions, VirtualScrollerPassThroughOptionType, VirtualScrollerProps } from '../virtualscroller';
export declare type DropdownPassThroughOptionType<T = any> = DropdownPassThroughAttributes | ((options: DropdownPassThroughMethodOptions<T>) => DropdownPassThroughAttributes | string) | string | null | undefined;
export declare type DropdownPassThroughTransitionType<T = any> = TransitionProps | ((options: DropdownPassThroughMethodOptions<T>) => TransitionProps) | undefined;
import 'vue';
import * as Select from '../select';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
/**
* Custom passthrough(pt) option method.
*/
export interface DropdownPassThroughMethodOptions<T> {
/**
* Defines instance.
*/
instance: any;
/**
* Defines valid properties.
*/
props: DropdownProps;
/**
* Defines current inline state.
*/
state: DropdownState;
/**
* Defines parent instance.
*/
parent: T | any;
/**
* Defines current options.
*/
context: DropdownContext;
/**
* Defines passthrough(pt) options in global config.
*/
global: object | undefined;
}
export interface DropdownPassThroughMethodOptions<T> extends Select.SelectPassThroughMethodOptions<T> {}
/**
* Custom change event.
* @see {@link DropdownEmits.change}
*/
export interface DropdownChangeEvent {
/**
* Browser event.
*/
originalEvent: Event;
/**
* Selected option value
*/
value: any;
}
export interface DropdownChangeEvent extends Select.SelectChangeEvent {}
/**
* Custom filter event.
* @see {@link DropdownEmits.filter}
*/
export interface DropdownFilterEvent {
/**
* Browser event.
*/
originalEvent: Event;
/**
* Filter value
*/
value: any;
}
export interface DropdownFilterEvent extends Select.SelectFilterEvent {}
/**
* Custom passthrough(pt) options.
* @see {@link DropdownProps.pt}
*/
export interface DropdownPassThroughOptions<T = any> {
/**
* Used to pass attributes to the root's DOM element.
*/
root?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the input's DOM element.
*/
input?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the clear icon's DOM element.
*/
clearIcon?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the trigger' DOM element.
*/
trigger?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the loading icon's DOM element.
*/
loadingIcon?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the panel's DOM element.
*/
panel?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the header's DOM element.
*/
header?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the filter container's DOM element.
*/
filterContainer?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the filter input's DOM element.
*/
filterInput?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the filter icon's DOM element.
*/
filterIcon?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the wrapper's DOM element.
*/
wrapper?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the VirtualScroller component.
* @see {@link VirtualScrollerPassThroughOptionType}
*/
virtualScroller?: VirtualScrollerPassThroughOptionType;
/**
* Used to pass attributes to the list's DOM element.
*/
list?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the item group's DOM element.
*/
itemGroup?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the item group label's DOM element.
*/
itemGroupLabel?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the item's DOM element.
*/
item?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the item label's DOM element.
*/
itemLabel?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the check icon's DOM element.
*/
checkIcon?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the bank icon's DOM element.
*/
blankIcon?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the empty message's DOM element.
*/
emptyMessage?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the hidden first focusable element's DOM element.
*/
hiddenFirstFocusableEl?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the hidden filter result's DOM element.
*/
hiddenFilterResult?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the hidden empty message's DOM element.
*/
hiddenEmptyMessage?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the hidden selected message's DOM element.
*/
hiddenSelectedMessage?: DropdownPassThroughOptionType<T>;
/**
* Used to pass attributes to the hidden last focusable element's DOM element.
*/
hiddenLastFocusableEl?: DropdownPassThroughOptionType<T>;
/**
* Used to manage all lifecycle hooks.
* @see {@link BaseComponent.ComponentHooks}
*/
hooks?: ComponentHooks;
/**
* Used to control Vue Transition API.
*/
transition?: DropdownPassThroughTransitionType;
}
export interface DropdownPassThroughOptions<T = any> extends Select.SelectPassThroughOptions<T> {}
/**
* Custom passthrough attributes for each DOM elements
*/
export interface DropdownPassThroughAttributes {
[key: string]: any;
}
export interface DropdownPassThroughAttributes extends Select.SelectPassThroughAttributes {}
/**
* Defines current inline state in Dropdown component.
*/
export interface DropdownState {
/**
* Current id state as a string.
*/
id: string;
/**
* Current focused state as a boolean.
* @defaultValue false
*/
focused: boolean;
/**
* Current focused item index as a number.
* @defaultValue -1
*/
focusedOptionIndex: number;
/**
* Current filter value state as a string.
*/
filterValue: string;
/**
* Current overlay visible state as a boolean.
* @defaultValue false
*/
overlayVisible: boolean;
}
export interface DropdownState extends Select.SelectState {}
/**
* Defines current options in Dropdown component.
*/
export interface DropdownContext {
/**
* Current item option.
*/
option: any;
/**
* Current item index.
*/
index: number;
/**
* Current selection state of the item as a boolean.
* @defaultValue false
*/
selected: boolean;
/**
* Current focus state of the item as a boolean.
* @defaultValue false
*/
focused: boolean;
/**
* Current disabled state of the item as a boolean.
* @defaultValue false
*/
disabled: boolean;
}
export interface DropdownContext extends Select.SelectContext {}
/**
* Defines valid properties in Dropdown component.
*/
export interface DropdownProps {
/**
* Value of the component.
*/
modelValue?: any;
/**
* An array of select items to display as the available options.
*/
options?: any[];
/**
* Property name or getter function to use as the label of an option.
*/
optionLabel?: string | ((data: any) => string) | undefined;
/**
* Property name or getter function to use as the value of an option, defaults to the option itself when not defined.
*/
optionValue?: string | ((data: any) => any) | undefined;
/**
* Property name or getter function to use as the disabled flag of an option, defaults to false when not defined.
*/
optionDisabled?: string | ((data: any) => boolean) | undefined;
/**
* Property name or getter function to use as the label of an option group.
*/
optionGroupLabel?: string | ((data: any) => string) | undefined;
/**
* Property name or getter function that refers to the children options of option group.
*/
optionGroupChildren?: string | ((data: any) => any[]) | undefined;
/**
* Height of the viewport, a scrollbar is defined if height of list exceeds this value.
* @defaultValue 14rem
*/
scrollHeight?: string | undefined;
/**
* When specified, displays a filter input at header.
* @defaultValue false
*/
filter?: boolean | undefined;
/**
* Placeholder text to show when filter input is empty.
*/
filterPlaceholder?: string | undefined;
/**
* Locale to use in filtering. The default locale is the host environment's current locale.
*/
filterLocale?: string | undefined;
/**
* Defines the filtering algorithm to use when searching the options.
* @defaultValue contains
*/
filterMatchMode?: HintedString<'contains' | 'startsWith' | 'endsWith'> | undefined;
/**
* Fields used when filtering the options, defaults to optionLabel.
*/
filterFields?: string[] | undefined;
/**
* When present, custom value instead of predefined options can be entered using the editable input field.
* @defaultValue false
*/
editable?: boolean | undefined;
/**
* Default text to display when no option is selected.
*/
placeholder?: string | undefined;
/**
* When present, it specifies that the component should have invalid state style.
* @defaultValue false
*/
invalid?: boolean | undefined;
/**
* When present, it specifies that the component should be disabled.
* @defaultValue false
*/
disabled?: boolean | undefined;
/**
* Specifies the input variant of the component.
* @defaultValue outlined
*/
variant?: 'outlined' | 'filled' | undefined;
/**
* A property to uniquely identify an option.
*/
dataKey?: string | undefined;
/**
* When enabled, a clear icon is displayed to clear the value.
* @defaultValue false
*/
showClear?: boolean | undefined;
/**
* Identifier of the underlying input element.
*/
inputId?: string | undefined;
/**
* Inline style of the input field.
*/
inputStyle?: object | undefined;
/**
* Style class of the input field.
*/
inputClass?: string | object | undefined;
/**
* Inline style of the overlay panel.
*/
panelStyle?: object | undefined;
/**
* Style class of the overlay panel.
*/
panelClass?: string | object | undefined;
/**
* A valid query selector or an HTMLElement to specify where the overlay gets attached.
* @defaultValue body
*/
appendTo?: HintedString<'body' | 'self'> | undefined | HTMLElement;
/**
* Whether the dropdown is in loading state.
* @defaultValue false
*/
loading?: boolean | undefined;
/**
* Icon to display in clear button.
* @deprecated since v3.27.0. Use 'clearicon' slot.
*/
clearIcon?: string | undefined;
/**
* Icon to display in the dropdown.
* @deprecated since v3.27.0. Use 'dropdownicon' slot.
*/
dropdownIcon?: string | undefined;
/**
* Icon to display in filter input.
* @deprecated since v3.27.0. Use 'filtericon' slot.
*/
filterIcon?: string | undefined;
/**
* Icon to display in loading state.
* @deprecated since v3.27.0. Use 'loadingicon' slot.
*/
loadingIcon?: string | undefined;
/**
* Clears the filter value when hiding the dropdown.
* @defaultValue false
*/
resetFilterOnHide?: boolean;
/**
* Clears the filter value when clicking on the clear icon.
* @defaultValue false
*/
resetFilterOnClear?: boolean;
/**
* Whether to use the virtualScroller feature. The properties of VirtualScroller component can be used like an object in it.
*/
virtualScrollerOptions?: VirtualScrollerProps;
/**
* Whether to focus on the first visible or selected element when the overlay panel is shown.
* @defaultValue false
*/
autoOptionFocus?: boolean | undefined;
/**
* Whether to focus on the filter element when the overlay panel is shown.
* @defaultValue false
*/
autoFilterFocus?: boolean | undefined;
/**
* When enabled, the focused option is selected.
* @defaultValue false
*/
selectOnFocus?: boolean | undefined;
/**
* When enabled, the focus is placed on the hovered option.
* @defaultValue true
*/
focusOnHover?: boolean | undefined;
/**
* Whether the selected option will be add highlight class.
* @defaultValue true
*/
highlightOnSelect?: boolean | undefined;
/**
* Whether the selected option will be shown with a check mark.
* @defaultValue false
*/
checkmark?: boolean | undefined;
/**
* Text to be displayed in hidden accessible field when filtering returns any results. Defaults to value from PrimeVue locale configuration.
* @defaultValue '{0} results are available'
*/
filterMessage?: string | undefined;
/**
* Text to be displayed in hidden accessible field when options are selected. Defaults to value from PrimeVue locale configuration.
* @defaultValue '{0} items selected'
*/
selectionMessage?: string | undefined;
/**
* Text to be displayed in hidden accessible field when any option is not selected. Defaults to value from PrimeVue locale configuration.
* @defaultValue No selected item
*/
emptySelectionMessage?: string | undefined;
/**
* Text to display when filtering does not return any results. Defaults to value from PrimeVue locale configuration.
* @defaultValue No results found
*/
emptyFilterMessage?: string | undefined;
/**
* Text to display when there are no options available. Defaults to value from PrimeVue locale configuration.
* @defaultValue No results found
*/
emptyMessage?: string | undefined;
/**
* Index of the element in tabbing order.
*/
tabindex?: number | string | undefined;
/**
* Defines a string value that labels an interactive element.
*/
ariaLabel?: string | undefined;
/**
* Identifier of the underlying input element.
*/
ariaLabelledby?: string | undefined;
/**
* It generates scoped CSS variables using design tokens for the component.
*/
dt?: DesignToken<any>;
/**
* Used to pass attributes to DOM elements inside the component.
* @type {DropdownPassThroughOptions}
*/
pt?: PassThrough<DropdownPassThroughOptions>;
/**
* Used to configure passthrough(pt) options of the component.
* @type {PassThroughOptions}
*/
ptOptions?: PassThroughOptions;
/**
* When enabled, it removes component related styles in the core.
* @defaultValue false
*/
unstyled?: boolean;
}
export interface DropdownProps extends Select.SelectProps {}
/**
* Defines valid slots in Dropdown component.
*/
export interface DropdownSlots {
/**
* Custom value template.
* @param {Object} scope - value slot's params.
*/
value(scope: {
/**
* Value of the component
*/
value: any;
/**
* Placeholder prop value
*/
placeholder: string;
}): VNode[];
/**
* Custom indicator template.
* @deprecated since v3.27.0. Use 'dropdownicon or loadingicon' slots.
*/
indicator(): VNode[];
/**
* Custom header template of panel.
* @param {Object} scope - header slot's params.
*/
header(scope: {
/**
* Value of the component
*/
value: any;
/**
* Displayed options
*/
options: any[];
}): VNode[];
/**
* Custom footer template of panel.
* @param {Object} scope - footer slot's params.
*/
footer(scope: {
/**
* Value of the component
*/
value: any;
/**
* Displayed options
*/
options: any[];
}): VNode[];
/**
* Custom option template.
* @param {Object} scope - option slot's params.
*/
option(scope: {
/**
* Option instance
*/
option: any;
/**
* Index of the option
*/
index: number;
}): VNode[];
/**
* Custom option group template.
* @param {Object} scope - option group slot's params.
*/
optiongroup(scope: {
/**
* Option instance
*/
option: any;
/**
* Index of the option
*/
index: number;
}): VNode[];
/**
* Custom empty filter template.
*/
emptyfilter(): VNode[];
/**
* Custom empty template.
*/
empty(): VNode[];
/**
* Custom content template.
* @param {Object} scope - content slot's params.
*/
content(scope: {
/**
* An array of objects to display for virtualscroller
*/
items: any;
/**
* Style class of the component
*/
styleClass: string;
/**
* Referance of the content
* @param {HTMLElement} el - Element of 'ref' property
*/
contentRef: (el: any) => void;
/**
* Options of the items
* @param {number} index - Rendered index
* @return {@link VirtualScrollerItemOptions}
*/
getItemOptions: (index: number) => VirtualScrollerItemOptions;
}): VNode[];
/**
* Custom loader template.
* @param {Object} scope - loader slot's params.
*/
loader(scope: {
/**
* Options of the loader items for virtualscroller
*/
options: any[];
}): VNode[];
/**
* Custom clear icon template.
* @param {Object} scope - clear icon slot's params.
*/
clearicon(scope: {
/**
* Style class of the clear icon
*/
class: any;
/**
* Clear icon click function.
* @param {Event} event - Browser event
* @deprecated since v3.39.0. Use 'clearCallback' property instead.
*/
onClick: (event: Event) => void;
/**
* Clear icon click function.
* @param {Event} event - Browser event
*/
clearCallback: (event: Event) => void;
}): VNode[];
/**
* Custom dropdown icon template.
* @param {Object} scope - dropdown icon slot's params.
*/
dropdownicon(scope: {
/**
* Style class of the dropdown icon
*/
class: any;
}): VNode[];
/**
* Custom loading icon template.
* @param {Object} scope - loading icon slot's params.
*/
loadingicon(scope: {
/**
* Style class of the loading icon
*/
class: any;
}): VNode[];
/**
* Custom filter icon template.
* @param {Object} scope - filter icon slot's params.
*/
filtericon(scope: {
/**
* Style class of the filter icon
*/
class: any;
}): VNode[];
}
export interface DropdownSlots extends Select.SelectSlots {}
/**
* Defines valid emits in Dropdown component.
*/
export interface DropdownEmits {
/**
* Emitted when the value changes.
* @param {*} value - New value.
*/
'update:modelValue'(value: any): void;
/**
* Callback to invoke on value change.
* @param {DropdownChangeEvent} event - Custom change event.
*/
change(event: DropdownChangeEvent): void;
/**
* Callback to invoke when the component receives focus.
* @param {Event} event - Browser event.
*/
focus(event: Event): void;
/**
* Callback to invoke when the component loses focus.
* @param {Event} event - Browser event.
*/
blur(event: Event): void;
/**
* Callback to invoke before the overlay is shown.
*/
'before-show'(): void;
/**
* Callback to invoke before the overlay is hidden.
*/
'before-hide'(): void;
/**
* Callback to invoke when the overlay is shown.
*/
show(): void;
/**
* Callback to invoke when the overlay is hidden.
*/
hide(): void;
/**
* Callback to invoke on filter input.
* @param {DropdownFilterEvent} event - Custom filter event.
*/
filter(event: DropdownFilterEvent): void;
}
export interface DropdownEmits extends Select.SelectEmits {}
/**
* @deprecated Deprecated since v4. Use Select component instead.
*
* **PrimeVue - Dropdown**
*
* _Dropdown also known as Select, is used to choose an item from a collection of options._
*
* [Live Demo](https://www.primevue.org/dropdown/)
* [Live Demo](https://www.primevue.org/select/)
* --- ---
* ![PrimeVue](https://primefaces.org/cdn/primevue/images/logo-100.png)
*

View File

@ -1,991 +1,11 @@
<template>
<div ref="container" :id="id" :class="cx('root')" @click="onContainerClick" v-bind="ptmi('root')">
<input
v-if="editable"
ref="focusInput"
:id="inputId"
type="text"
:class="[cx('input'), inputClass]"
:style="inputStyle"
:value="editableInputValue"
:placeholder="placeholder"
:tabindex="!disabled ? tabindex : -1"
:disabled="disabled"
autocomplete="off"
role="combobox"
:aria-label="ariaLabel"
:aria-labelledby="ariaLabelledby"
aria-haspopup="listbox"
:aria-expanded="overlayVisible"
:aria-controls="id + '_list'"
:aria-activedescendant="focused ? focusedOptionId : undefined"
:aria-invalid="invalid || undefined"
@focus="onFocus"
@blur="onBlur"
@keydown="onKeyDown"
@input="onEditableInput"
v-bind="ptm('input')"
/>
<span
v-else
ref="focusInput"
:id="inputId"
:class="[cx('input'), inputClass]"
:style="inputStyle"
:tabindex="!disabled ? tabindex : -1"
role="combobox"
:aria-label="ariaLabel || (label === 'p-emptylabel' ? undefined : label)"
:aria-labelledby="ariaLabelledby"
aria-haspopup="listbox"
:aria-expanded="overlayVisible"
:aria-controls="id + '_list'"
:aria-activedescendant="focused ? focusedOptionId : undefined"
:aria-disabled="disabled"
@focus="onFocus"
@blur="onBlur"
@keydown="onKeyDown"
v-bind="ptm('input')"
>
<slot name="value" :value="modelValue" :placeholder="placeholder">{{ label === 'p-emptylabel' ? '&nbsp;' : label || 'empty' }}</slot>
</span>
<slot v-if="showClear && modelValue != null" name="clearicon" :class="cx('clearIcon')" :onClick="onClearClick" :clearCallback="onClearClick">
<component :is="clearIcon ? 'i' : 'TimesIcon'" ref="clearIcon" :class="[cx('clearIcon'), clearIcon]" @click="onClearClick" v-bind="ptm('clearIcon')" data-pc-section="clearicon" />
</slot>
<div :class="cx('trigger')" v-bind="ptm('trigger')">
<slot v-if="loading" name="loadingicon" :class="cx('loadingIcon')">
<span v-if="loadingIcon" :class="[cx('loadingIcon'), 'pi-spin', loadingIcon]" aria-hidden="true" v-bind="ptm('loadingIcon')" />
<SpinnerIcon v-else :class="cx('loadingIcon')" spin aria-hidden="true" v-bind="ptm('loadingIcon')" />
</slot>
<slot v-else name="dropdownicon" :class="cx('dropdownIcon')">
<component :is="dropdownIcon ? 'span' : 'ChevronDownIcon'" :class="[cx('dropdownIcon'), dropdownIcon]" aria-hidden="true" v-bind="ptm('dropdownIcon')" />
</slot>
</div>
<Portal :appendTo="appendTo">
<transition name="p-connected-overlay" @enter="onOverlayEnter" @after-enter="onOverlayAfterEnter" @leave="onOverlayLeave" @after-leave="onOverlayAfterLeave" v-bind="ptm('transition')">
<div v-if="overlayVisible" :ref="overlayRef" :class="[cx('panel'), panelClass]" :style="panelStyle" @click="onOverlayClick" @keydown="onOverlayKeyDown" v-bind="ptm('panel')">
<span
ref="firstHiddenFocusableElementOnOverlay"
role="presentation"
aria-hidden="true"
class="p-hidden-accessible p-hidden-focusable"
:tabindex="0"
@focus="onFirstHiddenFocus"
v-bind="ptm('hiddenFirstFocusableEl')"
:data-p-hidden-accessible="true"
:data-p-hidden-focusable="true"
></span>
<slot name="header" :value="modelValue" :options="visibleOptions"></slot>
<div v-if="filter" :class="cx('header')" v-bind="ptm('header')">
<div :class="cx('filterContainer')" v-bind="ptm('filterContainer')">
<input
ref="filterInput"
type="text"
:value="filterValue"
@vue:mounted="onFilterUpdated"
@vue:updated="onFilterUpdated"
:class="cx('filterInput')"
:placeholder="filterPlaceholder"
role="searchbox"
autocomplete="off"
:aria-owns="id + '_list'"
:aria-activedescendant="focusedOptionId"
@keydown="onFilterKeyDown"
@blur="onFilterBlur"
@input="onFilterChange"
v-bind="ptm('filterInput')"
/>
<slot name="filtericon" :class="cx('filterIcon')">
<component :is="filterIcon ? 'span' : 'SearchIcon'" :class="[cx('filterIcon'), filterIcon]" v-bind="ptm('filterIcon')" />
</slot>
</div>
<span role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('hiddenFilterResult')" :data-p-hidden-accessible="true">
{{ filterResultMessageText }}
</span>
</div>
<div :class="cx('wrapper')" :style="{ 'max-height': virtualScrollerDisabled ? scrollHeight : '' }" v-bind="ptm('wrapper')">
<VirtualScroller :ref="virtualScrollerRef" v-bind="virtualScrollerOptions" :items="visibleOptions" :style="{ height: scrollHeight }" :tabindex="-1" :disabled="virtualScrollerDisabled" :pt="ptm('virtualScroller')">
<template v-slot:content="{ styleClass, contentRef, items, getItemOptions, contentStyle, itemSize }">
<ul :ref="(el) => listRef(el, contentRef)" :id="id + '_list'" :class="[cx('list'), styleClass]" :style="contentStyle" role="listbox" v-bind="ptm('list')">
<template v-for="(option, i) of items" :key="getOptionRenderKey(option, getOptionIndex(i, getItemOptions))">
<li v-if="isOptionGroup(option)" :id="id + '_' + getOptionIndex(i, getItemOptions)" :style="{ height: itemSize ? itemSize + 'px' : undefined }" :class="cx('itemGroup')" role="option" v-bind="ptm('itemGroup')">
<slot name="optiongroup" :option="option.optionGroup" :index="getOptionIndex(i, getItemOptions)">
<span :class="cx('itemGroupLabel')" v-bind="ptm('itemGroupLabel')">{{ getOptionGroupLabel(option.optionGroup) }}</span>
</slot>
</li>
<li
v-else
:id="id + '_' + getOptionIndex(i, getItemOptions)"
v-ripple
:class="cx('item', { option, focusedOption: getOptionIndex(i, getItemOptions) })"
:style="{ height: itemSize ? itemSize + 'px' : undefined }"
role="option"
:aria-label="getOptionLabel(option)"
:aria-selected="isSelected(option)"
:aria-disabled="isOptionDisabled(option)"
:aria-setsize="ariaSetSize"
:aria-posinset="getAriaPosInset(getOptionIndex(i, getItemOptions))"
@click="onOptionSelect($event, option)"
@mousemove="onOptionMouseMove($event, getOptionIndex(i, getItemOptions))"
:data-p-highlight="isSelected(option)"
:data-p-focused="focusedOptionIndex === getOptionIndex(i, getItemOptions)"
:data-p-disabled="isOptionDisabled(option)"
v-bind="getPTItemOptions(option, getItemOptions, i, 'item')"
>
<template v-if="checkmark">
<CheckIcon v-if="isSelected(option)" :class="cx('checkIcon')" v-bind="ptm('checkIcon')" />
<BlankIcon v-else :class="cx('blankIcon')" v-bind="ptm('blankIcon')" />
</template>
<slot name="option" :option="option" :index="getOptionIndex(i, getItemOptions)">
<span :class="cx('itemLabel')" v-bind="ptm('itemLabel')">{{ getOptionLabel(option) }}</span>
</slot>
</li>
</template>
<li v-if="filterValue && (!items || (items && items.length === 0))" :class="cx('emptyMessage')" role="option" v-bind="ptm('emptyMessage')" :data-p-hidden-accessible="true">
<slot name="emptyfilter">{{ emptyFilterMessageText }}</slot>
</li>
<li v-else-if="!options || (options && options.length === 0)" :class="cx('emptyMessage')" role="option" v-bind="ptm('emptyMessage')" :data-p-hidden-accessible="true">
<slot name="empty">{{ emptyMessageText }}</slot>
</li>
</ul>
</template>
<template v-if="$slots.loader" v-slot:loader="{ options }">
<slot name="loader" :options="options"></slot>
</template>
</VirtualScroller>
</div>
<slot name="footer" :value="modelValue" :options="visibleOptions"></slot>
<span v-if="!options || (options && options.length === 0)" role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('hiddenEmptyMessage')" :data-p-hidden-accessible="true">
{{ emptyMessageText }}
</span>
<span role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('hiddenSelectedMessage')" :data-p-hidden-accessible="true">
{{ selectedMessageText }}
</span>
<span
ref="lastHiddenFocusableElementOnOverlay"
role="presentation"
aria-hidden="true"
class="p-hidden-accessible p-hidden-focusable"
:tabindex="0"
@focus="onLastHiddenFocus"
v-bind="ptm('hiddenLastFocusableEl')"
:data-p-hidden-accessible="true"
:data-p-hidden-focusable="true"
></span>
</div>
</transition>
</Portal>
</div>
</template>
<script>
import { FilterService } from 'primevue/api';
import BlankIcon from 'primevue/icons/blank';
import CheckIcon from 'primevue/icons/check';
import ChevronDownIcon from 'primevue/icons/chevrondown';
import SearchIcon from 'primevue/icons/search';
import SpinnerIcon from 'primevue/icons/spinner';
import TimesIcon from 'primevue/icons/times';
import OverlayEventBus from 'primevue/overlayeventbus';
import Portal from 'primevue/portal';
import Ripple from 'primevue/ripple';
import { ConnectedOverlayScrollHandler, DomHandler, ObjectUtils, UniqueComponentId, ZIndexUtils } from 'primevue/utils';
import VirtualScroller from 'primevue/virtualscroller';
import BaseDropdown from './BaseDropdown.vue';
import Select from 'primevue/select';
export default {
name: 'Dropdown',
extends: BaseDropdown,
inheritAttrs: false,
emits: ['update:modelValue', 'change', 'focus', 'blur', 'before-show', 'before-hide', 'show', 'hide', 'filter'],
outsideClickListener: null,
scrollHandler: null,
resizeListener: null,
labelClickListener: null,
overlay: null,
list: null,
virtualScroller: null,
searchTimeout: null,
searchValue: null,
isModelValueChanged: false,
data() {
return {
id: this.$attrs.id,
clicked: false,
focused: false,
focusedOptionIndex: -1,
filterValue: null,
overlayVisible: false
};
},
watch: {
'$attrs.id': function (newValue) {
this.id = newValue || UniqueComponentId();
},
modelValue() {
this.isModelValueChanged = true;
},
options() {
this.autoUpdateModel();
}
},
extends: Select,
mounted() {
this.id = this.id || UniqueComponentId();
this.autoUpdateModel();
this.bindLabelClickListener();
},
updated() {
if (this.overlayVisible && this.isModelValueChanged) {
this.scrollInView(this.findSelectedOptionIndex());
}
this.isModelValueChanged = false;
},
beforeUnmount() {
this.unbindOutsideClickListener();
this.unbindResizeListener();
this.unbindLabelClickListener();
if (this.scrollHandler) {
this.scrollHandler.destroy();
this.scrollHandler = null;
}
if (this.overlay) {
ZIndexUtils.clear(this.overlay);
this.overlay = null;
}
},
methods: {
getOptionIndex(index, fn) {
return this.virtualScrollerDisabled ? index : fn && fn(index)['index'];
},
getOptionLabel(option) {
return this.optionLabel ? ObjectUtils.resolveFieldData(option, this.optionLabel) : option;
},
getOptionValue(option) {
return this.optionValue ? ObjectUtils.resolveFieldData(option, this.optionValue) : option;
},
getOptionRenderKey(option, index) {
return (this.dataKey ? ObjectUtils.resolveFieldData(option, this.dataKey) : this.getOptionLabel(option)) + '_' + index;
},
getPTItemOptions(option, itemOptions, index, key) {
return this.ptm(key, {
context: {
option,
index,
selected: this.isSelected(option),
focused: this.focusedOptionIndex === this.getOptionIndex(index, itemOptions),
disabled: this.isOptionDisabled(option)
}
});
},
isOptionDisabled(option) {
return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false;
},
isOptionGroup(option) {
return this.optionGroupLabel && option.optionGroup && option.group;
},
getOptionGroupLabel(optionGroup) {
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupLabel);
},
getOptionGroupChildren(optionGroup) {
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupChildren);
},
getAriaPosInset(index) {
return (this.optionGroupLabel ? index - this.visibleOptions.slice(0, index).filter((option) => this.isOptionGroup(option)).length : index) + 1;
},
show(isFocus) {
this.$emit('before-show');
this.overlayVisible = true;
this.focusedOptionIndex = this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : this.editable ? -1 : this.findSelectedOptionIndex();
isFocus && DomHandler.focus(this.$refs.focusInput);
},
hide(isFocus) {
const _hide = () => {
this.$emit('before-hide');
this.overlayVisible = false;
this.clicked = false;
this.focusedOptionIndex = -1;
this.searchValue = '';
this.resetFilterOnHide && (this.filterValue = null);
isFocus && DomHandler.focus(this.$refs.focusInput);
};
setTimeout(() => {
_hide();
}, 0); // For ScreenReaders
},
onFocus(event) {
if (this.disabled) {
// For ScreenReaders
return;
}
this.focused = true;
if (this.overlayVisible) {
this.focusedOptionIndex = this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : this.editable ? -1 : this.findSelectedOptionIndex();
this.scrollInView(this.focusedOptionIndex);
}
this.$emit('focus', event);
},
onBlur(event) {
this.focused = false;
this.focusedOptionIndex = -1;
this.searchValue = '';
this.$emit('blur', event);
},
onKeyDown(event) {
if (this.disabled || DomHandler.isAndroid()) {
event.preventDefault();
return;
}
const metaKey = event.metaKey || event.ctrlKey;
switch (event.code) {
case 'ArrowDown':
this.onArrowDownKey(event);
break;
case 'ArrowUp':
this.onArrowUpKey(event, this.editable);
break;
case 'ArrowLeft':
case 'ArrowRight':
this.onArrowLeftKey(event, this.editable);
break;
case 'Delete':
this.onDeleteKey(event);
case 'Home':
this.onHomeKey(event, this.editable);
break;
case 'End':
this.onEndKey(event, this.editable);
break;
case 'PageDown':
this.onPageDownKey(event);
break;
case 'PageUp':
this.onPageUpKey(event);
break;
case 'Space':
this.onSpaceKey(event, this.editable);
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, this.editable);
break;
case 'ShiftLeft':
case 'ShiftRight':
//NOOP
break;
default:
if (!metaKey && ObjectUtils.isPrintableCharacter(event.key)) {
!this.overlayVisible && this.show();
!this.editable && this.searchOptions(event, event.key);
}
break;
}
this.clicked = false;
},
onEditableInput(event) {
const value = event.target.value;
this.searchValue = '';
const matched = this.searchOptions(event, value);
!matched && (this.focusedOptionIndex = -1);
this.updateModel(event, value);
!this.overlayVisible && ObjectUtils.isNotEmpty(value) && this.show();
},
onContainerClick(event) {
if (this.disabled || this.loading) {
return;
}
if (event.target.tagName === 'INPUT' || event.target.getAttribute('data-pc-section') === 'clearicon' || event.target.closest('[data-pc-section="clearicon"]')) {
return;
} else if (!this.overlay || !this.overlay.contains(event.target)) {
this.overlayVisible ? this.hide(true) : this.show(true);
}
this.clicked = true;
},
onClearClick(event) {
this.updateModel(event, null);
this.resetFilterOnClear && (this.filterValue = null);
},
onFirstHiddenFocus(event) {
const focusableEl = event.relatedTarget === this.$refs.focusInput ? DomHandler.getFirstFocusableElement(this.overlay, ':not([data-p-hidden-focusable="true"])') : this.$refs.focusInput;
DomHandler.focus(focusableEl);
},
onLastHiddenFocus(event) {
const focusableEl = event.relatedTarget === this.$refs.focusInput ? DomHandler.getLastFocusableElement(this.overlay, ':not([data-p-hidden-focusable="true"])') : this.$refs.focusInput;
DomHandler.focus(focusableEl);
},
onOptionSelect(event, option, isHide = true) {
const value = this.getOptionValue(option);
this.updateModel(event, value);
isHide && this.hide(true);
},
onOptionMouseMove(event, index) {
if (this.focusOnHover) {
this.changeFocusedOptionIndex(event, index);
}
},
onFilterChange(event) {
const value = event.target.value;
this.filterValue = value;
this.focusedOptionIndex = -1;
this.$emit('filter', { originalEvent: event, value });
!this.virtualScrollerDisabled && this.virtualScroller.scrollToIndex(0);
},
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;
}
},
onFilterBlur() {
this.focusedOptionIndex = -1;
},
onFilterUpdated() {
if (this.overlayVisible) {
this.alignOverlay();
}
},
onOverlayClick(event) {
OverlayEventBus.emit('overlay-click', {
originalEvent: event,
target: this.$el
});
},
onOverlayKeyDown(event) {
switch (event.code) {
case 'Escape':
this.onEscapeKey(event);
break;
default:
break;
}
},
onDeleteKey(event) {
if (this.showClear) {
this.updateModel(event, null);
event.preventDefault();
}
},
onArrowDownKey(event) {
if (!this.overlayVisible) {
this.show();
this.editable && this.changeFocusedOptionIndex(event, this.findSelectedOptionIndex());
} else {
const optionIndex = this.focusedOptionIndex !== -1 ? this.findNextOptionIndex(this.focusedOptionIndex) : this.clicked ? this.findFirstOptionIndex() : this.findFirstFocusedOptionIndex();
this.changeFocusedOptionIndex(event, optionIndex);
}
event.preventDefault();
},
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.clicked ? this.findLastOptionIndex() : this.findLastFocusedOptionIndex();
this.changeFocusedOptionIndex(event, optionIndex);
!this.overlayVisible && this.show();
event.preventDefault();
}
},
onArrowLeftKey(event, pressedInInputText = false) {
pressedInInputText && (this.focusedOptionIndex = -1);
},
onHomeKey(event, pressedInInputText = false) {
if (pressedInInputText) {
event.currentTarget.setSelectionRange(0, 0);
this.focusedOptionIndex = -1;
} else {
this.changeFocusedOptionIndex(event, this.findFirstOptionIndex());
!this.overlayVisible && this.show();
}
event.preventDefault();
},
onEndKey(event, pressedInInputText = false) {
if (pressedInInputText) {
const target = event.currentTarget;
const len = target.value.length;
target.setSelectionRange(len, len);
this.focusedOptionIndex = -1;
} else {
this.changeFocusedOptionIndex(event, this.findLastOptionIndex());
!this.overlayVisible && this.show();
}
event.preventDefault();
},
onPageUpKey(event) {
this.scrollInView(0);
event.preventDefault();
},
onPageDownKey(event) {
this.scrollInView(this.visibleOptions.length - 1);
event.preventDefault();
},
onEnterKey(event) {
if (!this.overlayVisible) {
this.focusedOptionIndex = -1; // reset
this.onArrowDownKey(event);
} else {
if (this.focusedOptionIndex !== -1) {
this.onOptionSelect(event, this.visibleOptions[this.focusedOptionIndex]);
}
this.hide();
}
event.preventDefault();
},
onSpaceKey(event, pressedInInputText = false) {
!pressedInInputText && this.onEnterKey(event);
},
onEscapeKey(event) {
this.overlayVisible && this.hide(true);
event.preventDefault();
event.stopPropagation(); //@todo will be changed next versionss
},
onTabKey(event, pressedInInputText = false) {
if (!pressedInInputText) {
if (this.overlayVisible && this.hasFocusableElements()) {
DomHandler.focus(this.$refs.firstHiddenFocusableElementOnOverlay);
event.preventDefault();
} else {
if (this.focusedOptionIndex !== -1) {
this.onOptionSelect(event, this.visibleOptions[this.focusedOptionIndex]);
}
this.overlayVisible && this.hide(this.filter);
}
}
},
onBackspaceKey(event, pressedInInputText = false) {
if (pressedInInputText) {
!this.overlayVisible && this.show();
}
},
onOverlayEnter(el) {
ZIndexUtils.set('overlay', el, this.$primevue.config.zIndex.overlay);
DomHandler.addStyles(el, { position: 'absolute', top: '0', left: '0' });
this.alignOverlay();
this.scrollInView();
this.autoFilterFocus && DomHandler.focus(this.$refs.filterInput);
},
onOverlayAfterEnter() {
this.bindOutsideClickListener();
this.bindScrollListener();
this.bindResizeListener();
this.$emit('show');
},
onOverlayLeave() {
this.unbindOutsideClickListener();
this.unbindScrollListener();
this.unbindResizeListener();
this.$emit('hide');
this.overlay = null;
},
onOverlayAfterLeave(el) {
ZIndexUtils.clear(el);
},
alignOverlay() {
if (this.appendTo === 'self') {
DomHandler.relativePosition(this.overlay, this.$el);
} else {
this.overlay.style.minWidth = DomHandler.getOuterWidth(this.$el) + 'px';
DomHandler.absolutePosition(this.overlay, this.$el);
}
},
bindOutsideClickListener() {
if (!this.outsideClickListener) {
this.outsideClickListener = (event) => {
if (this.overlayVisible && this.overlay && !this.$el.contains(event.target) && !this.overlay.contains(event.target)) {
this.hide();
}
};
document.addEventListener('click', this.outsideClickListener);
}
},
unbindOutsideClickListener() {
if (this.outsideClickListener) {
document.removeEventListener('click', this.outsideClickListener);
this.outsideClickListener = null;
}
},
bindScrollListener() {
if (!this.scrollHandler) {
this.scrollHandler = new ConnectedOverlayScrollHandler(this.$refs.container, () => {
if (this.overlayVisible) {
this.hide();
}
});
}
this.scrollHandler.bindScrollListener();
},
unbindScrollListener() {
if (this.scrollHandler) {
this.scrollHandler.unbindScrollListener();
}
},
bindResizeListener() {
if (!this.resizeListener) {
this.resizeListener = () => {
if (this.overlayVisible && !DomHandler.isTouchDevice()) {
this.hide();
}
};
window.addEventListener('resize', this.resizeListener);
}
},
unbindResizeListener() {
if (this.resizeListener) {
window.removeEventListener('resize', this.resizeListener);
this.resizeListener = null;
}
},
bindLabelClickListener() {
if (!this.editable && !this.labelClickListener) {
const label = document.querySelector(`label[for="${this.inputId}"]`);
if (label && DomHandler.isVisible(label)) {
this.labelClickListener = () => {
DomHandler.focus(this.$refs.focusInput);
};
label.addEventListener('click', this.labelClickListener);
}
}
},
unbindLabelClickListener() {
if (this.labelClickListener) {
const label = document.querySelector(`label[for="${this.inputId}"]`);
if (label && DomHandler.isVisible(label)) {
label.removeEventListener('click', this.labelClickListener);
}
}
},
hasFocusableElements() {
return DomHandler.getFocusableElements(this.overlay, ':not([data-p-hidden-focusable="true"])').length > 0;
},
isOptionMatched(option) {
return this.isValidOption(option) && typeof this.getOptionLabel(option) === 'string' && this.getOptionLabel(option)?.toLocaleLowerCase(this.filterLocale).startsWith(this.searchValue.toLocaleLowerCase(this.filterLocale));
},
isValidOption(option) {
return ObjectUtils.isNotEmpty(option) && !(this.isOptionDisabled(option) || this.isOptionGroup(option));
},
isValidSelectedOption(option) {
return this.isValidOption(option) && this.isSelected(option);
},
isSelected(option) {
return this.isValidOption(option) && ObjectUtils.equals(this.modelValue, this.getOptionValue(option), this.equalityKey);
},
findFirstOptionIndex() {
return this.visibleOptions.findIndex((option) => this.isValidOption(option));
},
findLastOptionIndex() {
return ObjectUtils.findLastIndex(this.visibleOptions, (option) => this.isValidOption(option));
},
findNextOptionIndex(index) {
const matchedOptionIndex = index < this.visibleOptions.length - 1 ? this.visibleOptions.slice(index + 1).findIndex((option) => this.isValidOption(option)) : -1;
return matchedOptionIndex > -1 ? matchedOptionIndex + index + 1 : index;
},
findPrevOptionIndex(index) {
const matchedOptionIndex = index > 0 ? ObjectUtils.findLastIndex(this.visibleOptions.slice(0, index), (option) => this.isValidOption(option)) : -1;
return matchedOptionIndex > -1 ? matchedOptionIndex : index;
},
findSelectedOptionIndex() {
return this.hasSelectedOption ? this.visibleOptions.findIndex((option) => this.isValidSelectedOption(option)) : -1;
},
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;
if (ObjectUtils.isNotEmpty(this.searchValue)) {
if (this.focusedOptionIndex !== -1) {
optionIndex = this.visibleOptions.slice(this.focusedOptionIndex).findIndex((option) => this.isOptionMatched(option));
optionIndex = optionIndex === -1 ? this.visibleOptions.slice(0, this.focusedOptionIndex).findIndex((option) => this.isOptionMatched(option)) : optionIndex + this.focusedOptionIndex;
} else {
optionIndex = this.visibleOptions.findIndex((option) => this.isOptionMatched(option));
}
if (optionIndex !== -1) {
matched = true;
}
if (optionIndex === -1 && this.focusedOptionIndex === -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;
},
changeFocusedOptionIndex(event, index) {
if (this.focusedOptionIndex !== index) {
this.focusedOptionIndex = index;
this.scrollInView();
if (this.selectOnFocus) {
this.onOptionSelect(event, this.visibleOptions[index], false);
}
}
},
scrollInView(index = -1) {
this.$nextTick(() => {
const id = index !== -1 ? `${this.id}_${index}` : this.focusedOptionId;
const element = DomHandler.findSingle(this.list, `li[id="${id}"]`);
if (element) {
element.scrollIntoView && element.scrollIntoView({ block: 'nearest', inline: 'start' });
} else if (!this.virtualScrollerDisabled) {
this.virtualScroller && this.virtualScroller.scrollToIndex(index !== -1 ? index : this.focusedOptionIndex);
}
});
},
autoUpdateModel() {
if (this.selectOnFocus && this.autoOptionFocus && !this.hasSelectedOption) {
this.focusedOptionIndex = this.findFirstFocusedOptionIndex();
this.onOptionSelect(null, this.visibleOptions[this.focusedOptionIndex], false);
}
},
updateModel(event, value) {
this.$emit('update:modelValue', value);
this.$emit('change', { originalEvent: event, value });
},
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;
}, []);
},
overlayRef(el) {
this.overlay = el;
},
listRef(el, contentRef) {
this.list = el;
contentRef && contentRef(el); // For VirtualScroller
},
virtualScrollerRef(el) {
this.virtualScroller = el;
}
},
computed: {
visibleOptions() {
const options = this.optionGroupLabel ? this.flatOptions(this.options) : this.options || [];
if (this.filterValue) {
const filteredOptions = FilterService.filter(options, this.searchFields, this.filterValue, this.filterMatchMode, this.filterLocale);
if (this.optionGroupLabel) {
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;
},
hasSelectedOption() {
return ObjectUtils.isNotEmpty(this.modelValue);
},
label() {
const selectedOptionIndex = this.findSelectedOptionIndex();
return selectedOptionIndex !== -1 ? this.getOptionLabel(this.visibleOptions[selectedOptionIndex]) : this.placeholder || 'p-emptylabel';
},
editableInputValue() {
const selectedOptionIndex = this.findSelectedOptionIndex();
return selectedOptionIndex !== -1 ? this.getOptionLabel(this.visibleOptions[selectedOptionIndex]) : this.modelValue || '';
},
equalityKey() {
return this.optionValue ? null : this.dataKey;
},
searchFields() {
return this.filterFields || [this.optionLabel];
},
filterResultMessageText() {
return ObjectUtils.isNotEmpty(this.visibleOptions) ? this.filterMessageText.replaceAll('{0}', this.visibleOptions.length) : this.emptyFilterMessageText;
},
filterMessageText() {
return this.filterMessage || this.$primevue.config.locale.searchMessage || '';
},
emptyFilterMessageText() {
return this.emptyFilterMessage || this.$primevue.config.locale.emptySearchMessage || this.$primevue.config.locale.emptyFilterMessage || '';
},
emptyMessageText() {
return this.emptyMessage || this.$primevue.config.locale.emptyMessage || '';
},
selectionMessageText() {
return this.selectionMessage || this.$primevue.config.locale.selectionMessage || '';
},
emptySelectionMessageText() {
return this.emptySelectionMessage || this.$primevue.config.locale.emptySelectionMessage || '';
},
selectedMessageText() {
return this.hasSelectedOption ? this.selectionMessageText.replaceAll('{0}', '1') : this.emptySelectionMessageText;
},
focusedOptionId() {
return this.focusedOptionIndex !== -1 ? `${this.id}_${this.focusedOptionIndex}` : null;
},
ariaSetSize() {
return this.visibleOptions.filter((option) => !this.isOptionGroup(option)).length;
},
virtualScrollerDisabled() {
return !this.virtualScrollerOptions;
}
},
directives: {
ripple: Ripple
},
components: {
VirtualScroller,
Portal,
TimesIcon,
ChevronDownIcon,
SpinnerIcon,
SearchIcon,
CheckIcon,
BlankIcon
console.warn('Deprecated since v4. Use Select component instead.');
}
};
</script>

View File

@ -1,3 +1,3 @@
import { BaseStyle } from '../../base/style';
import { SelectStyle } from '../../select/style/SelectStyle';
export interface DropdownStyle extends BaseStyle {}
export interface DropdownStyle extends SelectStyle {}

View File

@ -1,58 +1,5 @@
import BaseStyle from 'primevue/base/style';
const classes = {
root: ({ instance, props, state }) => [
'p-select p-component p-inputwrapper',
{
'p-disabled': props.disabled,
'p-invalid': props.invalid,
'p-variant-filled': props.variant ? props.variant === 'filled' : instance.$primevue.config.inputStyle === 'filled',
'p-focus': state.focused,
'p-inputwrapper-filled': instance.hasSelectedOption,
'p-inputwrapper-focus': state.focused || state.overlayVisible,
'p-select-open': state.overlayVisible
}
],
input: ({ instance, props }) => [
'p-select-label',
{
'p-placeholder': !props.editable && instance.label === props.placeholder,
'p-select-label-empty': !props.editable && !instance.$slots['value'] && (instance.label === 'p-emptylabel' || instance.label.length === 0)
}
],
clearIcon: 'p-select-clear-icon',
trigger: 'p-select-dropdown',
loadingicon: 'p-select-loading-icon',
dropdownIcon: 'p-select-dropdown-icon',
panel: ({ instance }) => [
'p-select-overlay p-component',
{
'p-ripple-disabled': instance.$primevue.config.ripple === false
}
],
header: 'p-select-header',
filterContainer: 'p-select-filter-container',
filterInput: 'p-select-filter',
filterIcon: 'p-select-filter-icon',
wrapper: 'p-select-list-container',
list: 'p-select-list',
itemGroup: 'p-select-option-group',
itemGroupLabel: 'p-select-option-group-label',
item: ({ instance, props, state, option, focusedOption }) => [
'p-select-option',
{
'p-select-option-selected': instance.isSelected(option) && props.highlightOnSelect,
'p-focus': state.focusedOptionIndex === focusedOption,
'p-disabled': instance.isOptionDisabled(option)
}
],
itemLabel: 'p-select-option-label',
checkIcon: 'p-select-option-check-icon',
blankIcon: 'p-select-option-blank-icon',
emptyMessage: 'p-select-empty-message'
};
export default BaseStyle.extend({
name: 'dropdown',
classes
name: 'dropdown'
});

View File

@ -1,9 +1,9 @@
<script>
import BaseComponent from 'primevue/basecomponent';
import DropdownStyle from 'primevue/dropdown/style';
import SelectStyle from 'primevue/select/style';
export default {
name: 'BaseDropdown',
name: 'BaseSelect',
extends: BaseComponent,
props: {
modelValue: null,
@ -163,7 +163,7 @@ export default {
default: null
}
},
style: DropdownStyle,
style: SelectStyle,
provide() {
return {
$parentInstance: this

762
components/lib/select/Select.d.ts vendored Executable file
View File

@ -0,0 +1,762 @@
/**
*
* Select also known as Select, is used to choose an item from a collection of options.
*
* [Live Demo](https://www.primevue.org/select/)
*
* @module select
*
*/
import { TransitionProps, VNode } from 'vue';
import { ComponentHooks } from '../basecomponent';
import { PassThroughOptions } from '../passthrough';
import { ClassComponent, DesignToken, GlobalComponentConstructor, HintedString, PassThrough } from '../ts-helpers';
import { VirtualScrollerItemOptions, VirtualScrollerPassThroughOptionType, VirtualScrollerProps } from '../virtualscroller';
export declare type SelectPassThroughOptionType<T = any> = SelectPassThroughAttributes | ((options: SelectPassThroughMethodOptions<T>) => SelectPassThroughAttributes | string) | string | null | undefined;
export declare type SelectPassThroughTransitionType<T = any> = TransitionProps | ((options: SelectPassThroughMethodOptions<T>) => TransitionProps) | undefined;
/**
* Custom passthrough(pt) option method.
*/
export interface SelectPassThroughMethodOptions<T> {
/**
* Defines instance.
*/
instance: any;
/**
* Defines valid properties.
*/
props: SelectProps;
/**
* Defines current inline state.
*/
state: SelectState;
/**
* Defines parent instance.
*/
parent: T | any;
/**
* Defines current options.
*/
context: SelectContext;
/**
* Defines passthrough(pt) options in global config.
*/
global: object | undefined;
}
/**
* Custom change event.
* @see {@link SelectEmits.change}
*/
export interface SelectChangeEvent {
/**
* Browser event.
*/
originalEvent: Event;
/**
* Selected option value
*/
value: any;
}
/**
* Custom filter event.
* @see {@link SelectEmits.filter}
*/
export interface SelectFilterEvent {
/**
* Browser event.
*/
originalEvent: Event;
/**
* Filter value
*/
value: any;
}
/**
* Custom passthrough(pt) options.
* @see {@link SelectProps.pt}
*/
export interface SelectPassThroughOptions<T = any> {
/**
* Used to pass attributes to the root's DOM element.
*/
root?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the input's DOM element.
*/
input?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the clear icon's DOM element.
*/
clearIcon?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the trigger' DOM element.
*/
trigger?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the loading icon's DOM element.
*/
loadingIcon?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the panel's DOM element.
*/
panel?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the header's DOM element.
*/
header?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the filter container's DOM element.
*/
filterContainer?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the filter input's DOM element.
*/
filterInput?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the filter icon's DOM element.
*/
filterIcon?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the wrapper's DOM element.
*/
wrapper?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the VirtualScroller component.
* @see {@link VirtualScrollerPassThroughOptionType}
*/
virtualScroller?: VirtualScrollerPassThroughOptionType;
/**
* Used to pass attributes to the list's DOM element.
*/
list?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the item group's DOM element.
*/
itemGroup?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the item group label's DOM element.
*/
itemGroupLabel?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the item's DOM element.
*/
item?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the item label's DOM element.
*/
itemLabel?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the check icon's DOM element.
*/
checkIcon?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the bank icon's DOM element.
*/
blankIcon?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the empty message's DOM element.
*/
emptyMessage?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the hidden first focusable element's DOM element.
*/
hiddenFirstFocusableEl?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the hidden filter result's DOM element.
*/
hiddenFilterResult?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the hidden empty message's DOM element.
*/
hiddenEmptyMessage?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the hidden selected message's DOM element.
*/
hiddenSelectedMessage?: SelectPassThroughOptionType<T>;
/**
* Used to pass attributes to the hidden last focusable element's DOM element.
*/
hiddenLastFocusableEl?: SelectPassThroughOptionType<T>;
/**
* Used to manage all lifecycle hooks.
* @see {@link BaseComponent.ComponentHooks}
*/
hooks?: ComponentHooks;
/**
* Used to control Vue Transition API.
*/
transition?: SelectPassThroughTransitionType;
}
/**
* Custom passthrough attributes for each DOM elements
*/
export interface SelectPassThroughAttributes {
[key: string]: any;
}
/**
* Defines current inline state in Select component.
*/
export interface SelectState {
/**
* Current id state as a string.
*/
id: string;
/**
* Current focused state as a boolean.
* @defaultValue false
*/
focused: boolean;
/**
* Current focused item index as a number.
* @defaultValue -1
*/
focusedOptionIndex: number;
/**
* Current filter value state as a string.
*/
filterValue: string;
/**
* Current overlay visible state as a boolean.
* @defaultValue false
*/
overlayVisible: boolean;
}
/**
* Defines current options in Select component.
*/
export interface SelectContext {
/**
* Current item option.
*/
option: any;
/**
* Current item index.
*/
index: number;
/**
* Current selection state of the item as a boolean.
* @defaultValue false
*/
selected: boolean;
/**
* Current focus state of the item as a boolean.
* @defaultValue false
*/
focused: boolean;
/**
* Current disabled state of the item as a boolean.
* @defaultValue false
*/
disabled: boolean;
}
/**
* Defines valid properties in Select component.
*/
export interface SelectProps {
/**
* Value of the component.
*/
modelValue?: any;
/**
* An array of select items to display as the available options.
*/
options?: any[];
/**
* Property name or getter function to use as the label of an option.
*/
optionLabel?: string | ((data: any) => string) | undefined;
/**
* Property name or getter function to use as the value of an option, defaults to the option itself when not defined.
*/
optionValue?: string | ((data: any) => any) | undefined;
/**
* Property name or getter function to use as the disabled flag of an option, defaults to false when not defined.
*/
optionDisabled?: string | ((data: any) => boolean) | undefined;
/**
* Property name or getter function to use as the label of an option group.
*/
optionGroupLabel?: string | ((data: any) => string) | undefined;
/**
* Property name or getter function that refers to the children options of option group.
*/
optionGroupChildren?: string | ((data: any) => any[]) | undefined;
/**
* Height of the viewport, a scrollbar is defined if height of list exceeds this value.
* @defaultValue 14rem
*/
scrollHeight?: string | undefined;
/**
* When specified, displays a filter input at header.
* @defaultValue false
*/
filter?: boolean | undefined;
/**
* Placeholder text to show when filter input is empty.
*/
filterPlaceholder?: string | undefined;
/**
* Locale to use in filtering. The default locale is the host environment's current locale.
*/
filterLocale?: string | undefined;
/**
* Defines the filtering algorithm to use when searching the options.
* @defaultValue contains
*/
filterMatchMode?: HintedString<'contains' | 'startsWith' | 'endsWith'> | undefined;
/**
* Fields used when filtering the options, defaults to optionLabel.
*/
filterFields?: string[] | undefined;
/**
* When present, custom value instead of predefined options can be entered using the editable input field.
* @defaultValue false
*/
editable?: boolean | undefined;
/**
* Default text to display when no option is selected.
*/
placeholder?: string | undefined;
/**
* When present, it specifies that the component should have invalid state style.
* @defaultValue false
*/
invalid?: boolean | undefined;
/**
* When present, it specifies that the component should be disabled.
* @defaultValue false
*/
disabled?: boolean | undefined;
/**
* Specifies the input variant of the component.
* @defaultValue outlined
*/
variant?: 'outlined' | 'filled' | undefined;
/**
* A property to uniquely identify an option.
*/
dataKey?: string | undefined;
/**
* When enabled, a clear icon is displayed to clear the value.
* @defaultValue false
*/
showClear?: boolean | undefined;
/**
* Identifier of the underlying input element.
*/
inputId?: string | undefined;
/**
* Inline style of the input field.
*/
inputStyle?: object | undefined;
/**
* Style class of the input field.
*/
inputClass?: string | object | undefined;
/**
* Inline style of the overlay panel.
*/
panelStyle?: object | undefined;
/**
* Style class of the overlay panel.
*/
panelClass?: string | object | undefined;
/**
* A valid query selector or an HTMLElement to specify where the overlay gets attached.
* @defaultValue body
*/
appendTo?: HintedString<'body' | 'self'> | undefined | HTMLElement;
/**
* Whether the select is in loading state.
* @defaultValue false
*/
loading?: boolean | undefined;
/**
* Icon to display in clear button.
* @deprecated since v3.27.0. Use 'clearicon' slot.
*/
clearIcon?: string | undefined;
/**
* Icon to display in the select.
* @deprecated since v3.27.0. Use 'dropdownicon' slot.
*/
dropdownIcon?: string | undefined;
/**
* Icon to display in filter input.
* @deprecated since v3.27.0. Use 'filtericon' slot.
*/
filterIcon?: string | undefined;
/**
* Icon to display in loading state.
* @deprecated since v3.27.0. Use 'loadingicon' slot.
*/
loadingIcon?: string | undefined;
/**
* Clears the filter value when hiding the select.
* @defaultValue false
*/
resetFilterOnHide?: boolean;
/**
* Clears the filter value when clicking on the clear icon.
* @defaultValue false
*/
resetFilterOnClear?: boolean;
/**
* Whether to use the virtualScroller feature. The properties of VirtualScroller component can be used like an object in it.
*/
virtualScrollerOptions?: VirtualScrollerProps;
/**
* Whether to focus on the first visible or selected element when the overlay panel is shown.
* @defaultValue false
*/
autoOptionFocus?: boolean | undefined;
/**
* Whether to focus on the filter element when the overlay panel is shown.
* @defaultValue false
*/
autoFilterFocus?: boolean | undefined;
/**
* When enabled, the focused option is selected.
* @defaultValue false
*/
selectOnFocus?: boolean | undefined;
/**
* When enabled, the focus is placed on the hovered option.
* @defaultValue true
*/
focusOnHover?: boolean | undefined;
/**
* Whether the selected option will be add highlight class.
* @defaultValue true
*/
highlightOnSelect?: boolean | undefined;
/**
* Whether the selected option will be shown with a check mark.
* @defaultValue false
*/
checkmark?: boolean | undefined;
/**
* Text to be displayed in hidden accessible field when filtering returns any results. Defaults to value from PrimeVue locale configuration.
* @defaultValue '{0} results are available'
*/
filterMessage?: string | undefined;
/**
* Text to be displayed in hidden accessible field when options are selected. Defaults to value from PrimeVue locale configuration.
* @defaultValue '{0} items selected'
*/
selectionMessage?: string | undefined;
/**
* Text to be displayed in hidden accessible field when any option is not selected. Defaults to value from PrimeVue locale configuration.
* @defaultValue No selected item
*/
emptySelectionMessage?: string | undefined;
/**
* Text to display when filtering does not return any results. Defaults to value from PrimeVue locale configuration.
* @defaultValue No results found
*/
emptyFilterMessage?: string | undefined;
/**
* Text to display when there are no options available. Defaults to value from PrimeVue locale configuration.
* @defaultValue No results found
*/
emptyMessage?: string | undefined;
/**
* Index of the element in tabbing order.
*/
tabindex?: number | string | undefined;
/**
* Defines a string value that labels an interactive element.
*/
ariaLabel?: string | undefined;
/**
* Identifier of the underlying input element.
*/
ariaLabelledby?: string | undefined;
/**
* It generates scoped CSS variables using design tokens for the component.
*/
dt?: DesignToken<any>;
/**
* Used to pass attributes to DOM elements inside the component.
* @type {SelectPassThroughOptions}
*/
pt?: PassThrough<SelectPassThroughOptions>;
/**
* Used to configure passthrough(pt) options of the component.
* @type {PassThroughOptions}
*/
ptOptions?: PassThroughOptions;
/**
* When enabled, it removes component related styles in the core.
* @defaultValue false
*/
unstyled?: boolean;
}
/**
* Defines valid slots in Select component.
*/
export interface SelectSlots {
/**
* Custom value template.
* @param {Object} scope - value slot's params.
*/
value(scope: {
/**
* Value of the component
*/
value: any;
/**
* Placeholder prop value
*/
placeholder: string;
}): VNode[];
/**
* Custom indicator template.
* @deprecated since v3.27.0. Use 'dropdownicon or loadingicon' slots.
*/
indicator(): VNode[];
/**
* Custom header template of panel.
* @param {Object} scope - header slot's params.
*/
header(scope: {
/**
* Value of the component
*/
value: any;
/**
* Displayed options
*/
options: any[];
}): VNode[];
/**
* Custom footer template of panel.
* @param {Object} scope - footer slot's params.
*/
footer(scope: {
/**
* Value of the component
*/
value: any;
/**
* Displayed options
*/
options: any[];
}): VNode[];
/**
* Custom option template.
* @param {Object} scope - option slot's params.
*/
option(scope: {
/**
* Option instance
*/
option: any;
/**
* Index of the option
*/
index: number;
}): VNode[];
/**
* Custom option group template.
* @param {Object} scope - option group slot's params.
*/
optiongroup(scope: {
/**
* Option instance
*/
option: any;
/**
* Index of the option
*/
index: number;
}): VNode[];
/**
* Custom empty filter template.
*/
emptyfilter(): VNode[];
/**
* Custom empty template.
*/
empty(): VNode[];
/**
* Custom content template.
* @param {Object} scope - content slot's params.
*/
content(scope: {
/**
* An array of objects to display for virtualscroller
*/
items: any;
/**
* Style class of the component
*/
styleClass: string;
/**
* Referance of the content
* @param {HTMLElement} el - Element of 'ref' property
*/
contentRef: (el: any) => void;
/**
* Options of the items
* @param {number} index - Rendered index
* @return {@link VirtualScrollerItemOptions}
*/
getItemOptions: (index: number) => VirtualScrollerItemOptions;
}): VNode[];
/**
* Custom loader template.
* @param {Object} scope - loader slot's params.
*/
loader(scope: {
/**
* Options of the loader items for virtualscroller
*/
options: any[];
}): VNode[];
/**
* Custom clear icon template.
* @param {Object} scope - clear icon slot's params.
*/
clearicon(scope: {
/**
* Style class of the clear icon
*/
class: any;
/**
* Clear icon click function.
* @param {Event} event - Browser event
* @deprecated since v3.39.0. Use 'clearCallback' property instead.
*/
onClick: (event: Event) => void;
/**
* Clear icon click function.
* @param {Event} event - Browser event
*/
clearCallback: (event: Event) => void;
}): VNode[];
/**
* Custom select icon template.
* @param {Object} scope - select icon slot's params.
*/
dropdownicon(scope: {
/**
* Style class of the select icon
*/
class: any;
}): VNode[];
/**
* Custom loading icon template.
* @param {Object} scope - loading icon slot's params.
*/
loadingicon(scope: {
/**
* Style class of the loading icon
*/
class: any;
}): VNode[];
/**
* Custom filter icon template.
* @param {Object} scope - filter icon slot's params.
*/
filtericon(scope: {
/**
* Style class of the filter icon
*/
class: any;
}): VNode[];
}
/**
* Defines valid emits in Select component.
*/
export interface SelectEmits {
/**
* Emitted when the value changes.
* @param {*} value - New value.
*/
'update:modelValue'(value: any): void;
/**
* Callback to invoke on value change.
* @param {SelectChangeEvent} event - Custom change event.
*/
change(event: SelectChangeEvent): void;
/**
* Callback to invoke when the component receives focus.
* @param {Event} event - Browser event.
*/
focus(event: Event): void;
/**
* Callback to invoke when the component loses focus.
* @param {Event} event - Browser event.
*/
blur(event: Event): void;
/**
* Callback to invoke before the overlay is shown.
*/
'before-show'(): void;
/**
* Callback to invoke before the overlay is hidden.
*/
'before-hide'(): void;
/**
* Callback to invoke when the overlay is shown.
*/
show(): void;
/**
* Callback to invoke when the overlay is hidden.
*/
hide(): void;
/**
* Callback to invoke on filter input.
* @param {SelectFilterEvent} event - Custom filter event.
*/
filter(event: SelectFilterEvent): void;
}
/**
* **PrimeVue - Select**
*
* _Select is used to choose an item from a collection of options._
*
* [Live Demo](https://www.primevue.org/select/)
* --- ---
* ![PrimeVue](https://primefaces.org/cdn/primevue/images/logo-100.png)
*
* @group Component
*/
declare class Select extends ClassComponent<SelectProps, SelectSlots, SelectEmits> {
/**
* Shows the overlay.
* @param {boolean} [isFocus] - Decides whether to focus on the component. @defaultValue false.
*
* @memberof Select
*/
show: (isFocus?: boolean) => void;
/**
* Hides the overlay.
* @param {boolean} [isFocus] - Decides whether to focus on the component. @defaultValue false.
*
* @memberof Select
*/
hide: (isFocus?: boolean) => void;
}
declare module 'vue' {
export interface GlobalComponents {
Select: GlobalComponentConstructor<Select>;
}
}
export default Select;

View File

@ -1,13 +1,13 @@
import { mount } from '@vue/test-utils';
import PrimeVue from 'primevue/config';
import { h } from 'vue';
import Dropdown from './Dropdown.vue';
import Select from './Select.vue';
describe('Dropdown.vue', () => {
describe('Select.vue', () => {
let wrapper;
beforeEach(async () => {
wrapper = mount(Dropdown, {
wrapper = mount(Select, {
global: {
plugins: [PrimeVue],
stubs: {
@ -19,10 +19,10 @@ describe('Dropdown.vue', () => {
await wrapper.trigger('click');
});
it('should Dropdown exist', () => {
expect(wrapper.find('.p-dropdown.p-component').exists()).toBe(true);
expect(wrapper.find('.p-dropdown-panel').exists()).toBe(true);
expect(wrapper.find('.p-dropdown-empty-message').exists()).toBe(true);
it('should Select exist', () => {
expect(wrapper.find('.p-select.p-component').exists()).toBe(true);
expect(wrapper.find('.p-select-panel').exists()).toBe(true);
expect(wrapper.find('.p-select-empty-message').exists()).toBe(true);
expect(wrapper.find('.p-inputwrapper-filled').exists()).toBe(false);
expect(wrapper.find('.p-inputwrapper-focus').exists()).toBe(true);
});
@ -32,7 +32,7 @@ describe('option checks', () => {
let wrapper;
beforeEach(async () => {
wrapper = mount(Dropdown, {
wrapper = mount(Select, {
global: {
plugins: [PrimeVue],
stubs: {
@ -57,11 +57,11 @@ describe('option checks', () => {
});
it('should show the options', () => {
expect(wrapper.find('.p-dropdown-label.p-placeholder').text()).toBe('Select a City');
expect(wrapper.find('.p-dropdown-items-wrapper > .p-dropdown-items').exists()).toBe(true);
expect(wrapper.find('.p-dropdown-item').exists()).toBe(true);
expect(wrapper.findAll('.p-dropdown-item').length).toBe(5);
expect(wrapper.findAll('.p-dropdown-item')[0].text()).toBe('New York');
expect(wrapper.find('.p-select-label.p-placeholder').text()).toBe('Select a City');
expect(wrapper.find('.p-select-items-wrapper > .p-select-items').exists()).toBe(true);
expect(wrapper.find('.p-select-item').exists()).toBe(true);
expect(wrapper.findAll('.p-select-item').length).toBe(5);
expect(wrapper.findAll('.p-select-item')[0].text()).toBe('New York');
});
});
@ -69,7 +69,7 @@ describe('clear checks', () => {
let wrapper;
beforeEach(async () => {
wrapper = mount(Dropdown, {
wrapper = mount(Select, {
global: {
plugins: [PrimeVue],
stubs: {
@ -87,13 +87,13 @@ describe('clear checks', () => {
});
it('should have correct icon', () => {
expect(wrapper.find('.p-dropdown-clear-icon').classes()).toContain('pi-discord');
expect(wrapper.find('.p-select-clear-icon').classes()).toContain('pi-discord');
});
it('should clear with delete key', async () => {
const updateModelSpy = vi.spyOn(wrapper.vm, 'updateModel');
await wrapper.find('.p-dropdown-label.p-inputtext').trigger('keydown', { code: 'Delete' });
await wrapper.find('.p-select-label.p-inputtext').trigger('keydown', { code: 'Delete' });
expect(updateModelSpy).toHaveBeenCalledOnce();
expect(updateModelSpy).toHaveBeenCalledWith(expect.any(KeyboardEvent), null);
});
@ -103,7 +103,7 @@ describe('editable checks', () => {
let wrapper;
beforeEach(async () => {
wrapper = mount(Dropdown, {
wrapper = mount(Select, {
global: {
plugins: [PrimeVue],
stubs: {
@ -129,8 +129,8 @@ describe('editable checks', () => {
});
it('should show the options', () => {
expect(wrapper.find('.p-dropdown-label.p-placeholder').exists()).toBe(false);
expect(wrapper.find('.p-dropdown-label.p-inputtext').exists()).toBe(true);
expect(wrapper.find('.p-select-label.p-placeholder').exists()).toBe(false);
expect(wrapper.find('.p-select-label.p-inputtext').exists()).toBe(true);
});
});
@ -138,7 +138,7 @@ describe('option groups checks', () => {
let wrapper;
beforeEach(async () => {
wrapper = mount(Dropdown, {
wrapper = mount(Select, {
global: {
plugins: [PrimeVue],
stubs: {
@ -188,8 +188,8 @@ describe('option groups checks', () => {
});
it('should show the option groups', () => {
expect(wrapper.findAll('.p-dropdown-item-group').length).toBe(3);
expect(wrapper.findAll('.p-dropdown-item-group')[0].text()).toBe('Germany');
expect(wrapper.findAll('.p-select-item-group').length).toBe(3);
expect(wrapper.findAll('.p-select-item-group')[0].text()).toBe('Germany');
});
});
@ -197,7 +197,7 @@ describe('templating checks', () => {
let wrapper;
beforeEach(async () => {
wrapper = mount(Dropdown, {
wrapper = mount(Select, {
global: {
plugins: [PrimeVue],
stubs: {
@ -249,7 +249,7 @@ describe('empty templating checks', () => {
let wrapper;
beforeEach(async () => {
wrapper = mount(Dropdown, {
wrapper = mount(Select, {
global: {
plugins: [PrimeVue],
stubs: {
@ -270,8 +270,8 @@ describe('empty templating checks', () => {
});
it('should see empty slots', () => {
expect(wrapper.find('.p-dropdown-empty-message').exists()).toBe(true);
expect(wrapper.find('.p-dropdown-empty-message').text()).toBe('Need options prop');
expect(wrapper.find('.p-select-empty-message').exists()).toBe(true);
expect(wrapper.find('.p-select-empty-message').text()).toBe('Need options prop');
});
});
@ -279,7 +279,7 @@ describe('loader checks', () => {
let wrapper;
beforeEach(async () => {
wrapper = mount(Dropdown, {
wrapper = mount(Select, {
global: {
plugins: [PrimeVue],
stubs: {
@ -306,11 +306,11 @@ describe('loader checks', () => {
});
it('should show the loader', async () => {
expect(wrapper.find('.p-dropdown-trigger-icon').classes()).toContain('pi-discord');
expect(wrapper.find('.p-select-trigger-icon').classes()).toContain('pi-discord');
await wrapper.setProps({ loading: false });
expect(wrapper.find('.p-dropdown-trigger-icon').classes()).not.toContain('pi-discord');
expect(wrapper.find('.p-select-trigger-icon').classes()).not.toContain('pi-discord');
});
});
@ -318,7 +318,7 @@ describe('filter checks', () => {
let wrapper;
beforeEach(async () => {
wrapper = mount(Dropdown, {
wrapper = mount(Select, {
global: {
plugins: [PrimeVue],
stubs: {
@ -348,8 +348,8 @@ describe('filter checks', () => {
});
it('should make filtering', async () => {
const filterInput = wrapper.find('.p-dropdown-filter');
const filterIcon = wrapper.find('.p-dropdown-filter-icon');
const filterInput = wrapper.find('.p-select-filter');
const filterIcon = wrapper.find('.p-select-filter-icon');
expect(filterInput.exists()).toBe(true);
expect(filterIcon.classes()).toContain('pi-discord');
@ -364,6 +364,6 @@ describe('filter checks', () => {
await wrapper.setData({ filterValue: 'c' });
expect(wrapper.findAll('.p-dropdown-item').length).toBe(2);
expect(wrapper.findAll('.p-select-item').length).toBe(2);
});
});

991
components/lib/select/Select.vue Executable file
View File

@ -0,0 +1,991 @@
<template>
<div ref="container" :id="id" :class="cx('root')" @click="onContainerClick" v-bind="ptmi('root')">
<input
v-if="editable"
ref="focusInput"
:id="inputId"
type="text"
:class="[cx('input'), inputClass]"
:style="inputStyle"
:value="editableInputValue"
:placeholder="placeholder"
:tabindex="!disabled ? tabindex : -1"
:disabled="disabled"
autocomplete="off"
role="combobox"
:aria-label="ariaLabel"
:aria-labelledby="ariaLabelledby"
aria-haspopup="listbox"
:aria-expanded="overlayVisible"
:aria-controls="id + '_list'"
:aria-activedescendant="focused ? focusedOptionId : undefined"
:aria-invalid="invalid || undefined"
@focus="onFocus"
@blur="onBlur"
@keydown="onKeyDown"
@input="onEditableInput"
v-bind="ptm('input')"
/>
<span
v-else
ref="focusInput"
:id="inputId"
:class="[cx('input'), inputClass]"
:style="inputStyle"
:tabindex="!disabled ? tabindex : -1"
role="combobox"
:aria-label="ariaLabel || (label === 'p-emptylabel' ? undefined : label)"
:aria-labelledby="ariaLabelledby"
aria-haspopup="listbox"
:aria-expanded="overlayVisible"
:aria-controls="id + '_list'"
:aria-activedescendant="focused ? focusedOptionId : undefined"
:aria-disabled="disabled"
@focus="onFocus"
@blur="onBlur"
@keydown="onKeyDown"
v-bind="ptm('input')"
>
<slot name="value" :value="modelValue" :placeholder="placeholder">{{ label === 'p-emptylabel' ? '&nbsp;' : label || 'empty' }}</slot>
</span>
<slot v-if="showClear && modelValue != null" name="clearicon" :class="cx('clearIcon')" :onClick="onClearClick" :clearCallback="onClearClick">
<component :is="clearIcon ? 'i' : 'TimesIcon'" ref="clearIcon" :class="[cx('clearIcon'), clearIcon]" @click="onClearClick" v-bind="ptm('clearIcon')" data-pc-section="clearicon" />
</slot>
<div :class="cx('trigger')" v-bind="ptm('trigger')">
<slot v-if="loading" name="loadingicon" :class="cx('loadingIcon')">
<span v-if="loadingIcon" :class="[cx('loadingIcon'), 'pi-spin', loadingIcon]" aria-hidden="true" v-bind="ptm('loadingIcon')" />
<SpinnerIcon v-else :class="cx('loadingIcon')" spin aria-hidden="true" v-bind="ptm('loadingIcon')" />
</slot>
<slot v-else name="dropdownicon" :class="cx('dropdownIcon')">
<component :is="dropdownIcon ? 'span' : 'ChevronDownIcon'" :class="[cx('dropdownIcon'), dropdownIcon]" aria-hidden="true" v-bind="ptm('dropdownIcon')" />
</slot>
</div>
<Portal :appendTo="appendTo">
<transition name="p-connected-overlay" @enter="onOverlayEnter" @after-enter="onOverlayAfterEnter" @leave="onOverlayLeave" @after-leave="onOverlayAfterLeave" v-bind="ptm('transition')">
<div v-if="overlayVisible" :ref="overlayRef" :class="[cx('panel'), panelClass]" :style="panelStyle" @click="onOverlayClick" @keydown="onOverlayKeyDown" v-bind="ptm('panel')">
<span
ref="firstHiddenFocusableElementOnOverlay"
role="presentation"
aria-hidden="true"
class="p-hidden-accessible p-hidden-focusable"
:tabindex="0"
@focus="onFirstHiddenFocus"
v-bind="ptm('hiddenFirstFocusableEl')"
:data-p-hidden-accessible="true"
:data-p-hidden-focusable="true"
></span>
<slot name="header" :value="modelValue" :options="visibleOptions"></slot>
<div v-if="filter" :class="cx('header')" v-bind="ptm('header')">
<div :class="cx('filterContainer')" v-bind="ptm('filterContainer')">
<input
ref="filterInput"
type="text"
:value="filterValue"
@vue:mounted="onFilterUpdated"
@vue:updated="onFilterUpdated"
:class="cx('filterInput')"
:placeholder="filterPlaceholder"
role="searchbox"
autocomplete="off"
:aria-owns="id + '_list'"
:aria-activedescendant="focusedOptionId"
@keydown="onFilterKeyDown"
@blur="onFilterBlur"
@input="onFilterChange"
v-bind="ptm('filterInput')"
/>
<slot name="filtericon" :class="cx('filterIcon')">
<component :is="filterIcon ? 'span' : 'SearchIcon'" :class="[cx('filterIcon'), filterIcon]" v-bind="ptm('filterIcon')" />
</slot>
</div>
<span role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('hiddenFilterResult')" :data-p-hidden-accessible="true">
{{ filterResultMessageText }}
</span>
</div>
<div :class="cx('wrapper')" :style="{ 'max-height': virtualScrollerDisabled ? scrollHeight : '' }" v-bind="ptm('wrapper')">
<VirtualScroller :ref="virtualScrollerRef" v-bind="virtualScrollerOptions" :items="visibleOptions" :style="{ height: scrollHeight }" :tabindex="-1" :disabled="virtualScrollerDisabled" :pt="ptm('virtualScroller')">
<template v-slot:content="{ styleClass, contentRef, items, getItemOptions, contentStyle, itemSize }">
<ul :ref="(el) => listRef(el, contentRef)" :id="id + '_list'" :class="[cx('list'), styleClass]" :style="contentStyle" role="listbox" v-bind="ptm('list')">
<template v-for="(option, i) of items" :key="getOptionRenderKey(option, getOptionIndex(i, getItemOptions))">
<li v-if="isOptionGroup(option)" :id="id + '_' + getOptionIndex(i, getItemOptions)" :style="{ height: itemSize ? itemSize + 'px' : undefined }" :class="cx('itemGroup')" role="option" v-bind="ptm('itemGroup')">
<slot name="optiongroup" :option="option.optionGroup" :index="getOptionIndex(i, getItemOptions)">
<span :class="cx('itemGroupLabel')" v-bind="ptm('itemGroupLabel')">{{ getOptionGroupLabel(option.optionGroup) }}</span>
</slot>
</li>
<li
v-else
:id="id + '_' + getOptionIndex(i, getItemOptions)"
v-ripple
:class="cx('item', { option, focusedOption: getOptionIndex(i, getItemOptions) })"
:style="{ height: itemSize ? itemSize + 'px' : undefined }"
role="option"
:aria-label="getOptionLabel(option)"
:aria-selected="isSelected(option)"
:aria-disabled="isOptionDisabled(option)"
:aria-setsize="ariaSetSize"
:aria-posinset="getAriaPosInset(getOptionIndex(i, getItemOptions))"
@click="onOptionSelect($event, option)"
@mousemove="onOptionMouseMove($event, getOptionIndex(i, getItemOptions))"
:data-p-highlight="isSelected(option)"
:data-p-focused="focusedOptionIndex === getOptionIndex(i, getItemOptions)"
:data-p-disabled="isOptionDisabled(option)"
v-bind="getPTItemOptions(option, getItemOptions, i, 'item')"
>
<template v-if="checkmark">
<CheckIcon v-if="isSelected(option)" :class="cx('checkIcon')" v-bind="ptm('checkIcon')" />
<BlankIcon v-else :class="cx('blankIcon')" v-bind="ptm('blankIcon')" />
</template>
<slot name="option" :option="option" :index="getOptionIndex(i, getItemOptions)">
<span :class="cx('itemLabel')" v-bind="ptm('itemLabel')">{{ getOptionLabel(option) }}</span>
</slot>
</li>
</template>
<li v-if="filterValue && (!items || (items && items.length === 0))" :class="cx('emptyMessage')" role="option" v-bind="ptm('emptyMessage')" :data-p-hidden-accessible="true">
<slot name="emptyfilter">{{ emptyFilterMessageText }}</slot>
</li>
<li v-else-if="!options || (options && options.length === 0)" :class="cx('emptyMessage')" role="option" v-bind="ptm('emptyMessage')" :data-p-hidden-accessible="true">
<slot name="empty">{{ emptyMessageText }}</slot>
</li>
</ul>
</template>
<template v-if="$slots.loader" v-slot:loader="{ options }">
<slot name="loader" :options="options"></slot>
</template>
</VirtualScroller>
</div>
<slot name="footer" :value="modelValue" :options="visibleOptions"></slot>
<span v-if="!options || (options && options.length === 0)" role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('hiddenEmptyMessage')" :data-p-hidden-accessible="true">
{{ emptyMessageText }}
</span>
<span role="status" aria-live="polite" class="p-hidden-accessible" v-bind="ptm('hiddenSelectedMessage')" :data-p-hidden-accessible="true">
{{ selectedMessageText }}
</span>
<span
ref="lastHiddenFocusableElementOnOverlay"
role="presentation"
aria-hidden="true"
class="p-hidden-accessible p-hidden-focusable"
:tabindex="0"
@focus="onLastHiddenFocus"
v-bind="ptm('hiddenLastFocusableEl')"
:data-p-hidden-accessible="true"
:data-p-hidden-focusable="true"
></span>
</div>
</transition>
</Portal>
</div>
</template>
<script>
import { FilterService } from 'primevue/api';
import BlankIcon from 'primevue/icons/blank';
import CheckIcon from 'primevue/icons/check';
import ChevronDownIcon from 'primevue/icons/chevrondown';
import SearchIcon from 'primevue/icons/search';
import SpinnerIcon from 'primevue/icons/spinner';
import TimesIcon from 'primevue/icons/times';
import OverlayEventBus from 'primevue/overlayeventbus';
import Portal from 'primevue/portal';
import Ripple from 'primevue/ripple';
import { ConnectedOverlayScrollHandler, DomHandler, ObjectUtils, UniqueComponentId, ZIndexUtils } from 'primevue/utils';
import VirtualScroller from 'primevue/virtualscroller';
import BaseSelect from './BaseSelect.vue';
export default {
name: 'Select',
extends: BaseSelect,
inheritAttrs: false,
emits: ['update:modelValue', 'change', 'focus', 'blur', 'before-show', 'before-hide', 'show', 'hide', 'filter'],
outsideClickListener: null,
scrollHandler: null,
resizeListener: null,
labelClickListener: null,
overlay: null,
list: null,
virtualScroller: null,
searchTimeout: null,
searchValue: null,
isModelValueChanged: false,
data() {
return {
id: this.$attrs.id,
clicked: false,
focused: false,
focusedOptionIndex: -1,
filterValue: null,
overlayVisible: false
};
},
watch: {
'$attrs.id': function (newValue) {
this.id = newValue || UniqueComponentId();
},
modelValue() {
this.isModelValueChanged = true;
},
options() {
this.autoUpdateModel();
}
},
mounted() {
this.id = this.id || UniqueComponentId();
this.autoUpdateModel();
this.bindLabelClickListener();
},
updated() {
if (this.overlayVisible && this.isModelValueChanged) {
this.scrollInView(this.findSelectedOptionIndex());
}
this.isModelValueChanged = false;
},
beforeUnmount() {
this.unbindOutsideClickListener();
this.unbindResizeListener();
this.unbindLabelClickListener();
if (this.scrollHandler) {
this.scrollHandler.destroy();
this.scrollHandler = null;
}
if (this.overlay) {
ZIndexUtils.clear(this.overlay);
this.overlay = null;
}
},
methods: {
getOptionIndex(index, fn) {
return this.virtualScrollerDisabled ? index : fn && fn(index)['index'];
},
getOptionLabel(option) {
return this.optionLabel ? ObjectUtils.resolveFieldData(option, this.optionLabel) : option;
},
getOptionValue(option) {
return this.optionValue ? ObjectUtils.resolveFieldData(option, this.optionValue) : option;
},
getOptionRenderKey(option, index) {
return (this.dataKey ? ObjectUtils.resolveFieldData(option, this.dataKey) : this.getOptionLabel(option)) + '_' + index;
},
getPTItemOptions(option, itemOptions, index, key) {
return this.ptm(key, {
context: {
option,
index,
selected: this.isSelected(option),
focused: this.focusedOptionIndex === this.getOptionIndex(index, itemOptions),
disabled: this.isOptionDisabled(option)
}
});
},
isOptionDisabled(option) {
return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false;
},
isOptionGroup(option) {
return this.optionGroupLabel && option.optionGroup && option.group;
},
getOptionGroupLabel(optionGroup) {
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupLabel);
},
getOptionGroupChildren(optionGroup) {
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupChildren);
},
getAriaPosInset(index) {
return (this.optionGroupLabel ? index - this.visibleOptions.slice(0, index).filter((option) => this.isOptionGroup(option)).length : index) + 1;
},
show(isFocus) {
this.$emit('before-show');
this.overlayVisible = true;
this.focusedOptionIndex = this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : this.editable ? -1 : this.findSelectedOptionIndex();
isFocus && DomHandler.focus(this.$refs.focusInput);
},
hide(isFocus) {
const _hide = () => {
this.$emit('before-hide');
this.overlayVisible = false;
this.clicked = false;
this.focusedOptionIndex = -1;
this.searchValue = '';
this.resetFilterOnHide && (this.filterValue = null);
isFocus && DomHandler.focus(this.$refs.focusInput);
};
setTimeout(() => {
_hide();
}, 0); // For ScreenReaders
},
onFocus(event) {
if (this.disabled) {
// For ScreenReaders
return;
}
this.focused = true;
if (this.overlayVisible) {
this.focusedOptionIndex = this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : this.editable ? -1 : this.findSelectedOptionIndex();
this.scrollInView(this.focusedOptionIndex);
}
this.$emit('focus', event);
},
onBlur(event) {
this.focused = false;
this.focusedOptionIndex = -1;
this.searchValue = '';
this.$emit('blur', event);
},
onKeyDown(event) {
if (this.disabled || DomHandler.isAndroid()) {
event.preventDefault();
return;
}
const metaKey = event.metaKey || event.ctrlKey;
switch (event.code) {
case 'ArrowDown':
this.onArrowDownKey(event);
break;
case 'ArrowUp':
this.onArrowUpKey(event, this.editable);
break;
case 'ArrowLeft':
case 'ArrowRight':
this.onArrowLeftKey(event, this.editable);
break;
case 'Delete':
this.onDeleteKey(event);
case 'Home':
this.onHomeKey(event, this.editable);
break;
case 'End':
this.onEndKey(event, this.editable);
break;
case 'PageDown':
this.onPageDownKey(event);
break;
case 'PageUp':
this.onPageUpKey(event);
break;
case 'Space':
this.onSpaceKey(event, this.editable);
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, this.editable);
break;
case 'ShiftLeft':
case 'ShiftRight':
//NOOP
break;
default:
if (!metaKey && ObjectUtils.isPrintableCharacter(event.key)) {
!this.overlayVisible && this.show();
!this.editable && this.searchOptions(event, event.key);
}
break;
}
this.clicked = false;
},
onEditableInput(event) {
const value = event.target.value;
this.searchValue = '';
const matched = this.searchOptions(event, value);
!matched && (this.focusedOptionIndex = -1);
this.updateModel(event, value);
!this.overlayVisible && ObjectUtils.isNotEmpty(value) && this.show();
},
onContainerClick(event) {
if (this.disabled || this.loading) {
return;
}
if (event.target.tagName === 'INPUT' || event.target.getAttribute('data-pc-section') === 'clearicon' || event.target.closest('[data-pc-section="clearicon"]')) {
return;
} else if (!this.overlay || !this.overlay.contains(event.target)) {
this.overlayVisible ? this.hide(true) : this.show(true);
}
this.clicked = true;
},
onClearClick(event) {
this.updateModel(event, null);
this.resetFilterOnClear && (this.filterValue = null);
},
onFirstHiddenFocus(event) {
const focusableEl = event.relatedTarget === this.$refs.focusInput ? DomHandler.getFirstFocusableElement(this.overlay, ':not([data-p-hidden-focusable="true"])') : this.$refs.focusInput;
DomHandler.focus(focusableEl);
},
onLastHiddenFocus(event) {
const focusableEl = event.relatedTarget === this.$refs.focusInput ? DomHandler.getLastFocusableElement(this.overlay, ':not([data-p-hidden-focusable="true"])') : this.$refs.focusInput;
DomHandler.focus(focusableEl);
},
onOptionSelect(event, option, isHide = true) {
const value = this.getOptionValue(option);
this.updateModel(event, value);
isHide && this.hide(true);
},
onOptionMouseMove(event, index) {
if (this.focusOnHover) {
this.changeFocusedOptionIndex(event, index);
}
},
onFilterChange(event) {
const value = event.target.value;
this.filterValue = value;
this.focusedOptionIndex = -1;
this.$emit('filter', { originalEvent: event, value });
!this.virtualScrollerDisabled && this.virtualScroller.scrollToIndex(0);
},
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;
}
},
onFilterBlur() {
this.focusedOptionIndex = -1;
},
onFilterUpdated() {
if (this.overlayVisible) {
this.alignOverlay();
}
},
onOverlayClick(event) {
OverlayEventBus.emit('overlay-click', {
originalEvent: event,
target: this.$el
});
},
onOverlayKeyDown(event) {
switch (event.code) {
case 'Escape':
this.onEscapeKey(event);
break;
default:
break;
}
},
onDeleteKey(event) {
if (this.showClear) {
this.updateModel(event, null);
event.preventDefault();
}
},
onArrowDownKey(event) {
if (!this.overlayVisible) {
this.show();
this.editable && this.changeFocusedOptionIndex(event, this.findSelectedOptionIndex());
} else {
const optionIndex = this.focusedOptionIndex !== -1 ? this.findNextOptionIndex(this.focusedOptionIndex) : this.clicked ? this.findFirstOptionIndex() : this.findFirstFocusedOptionIndex();
this.changeFocusedOptionIndex(event, optionIndex);
}
event.preventDefault();
},
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.clicked ? this.findLastOptionIndex() : this.findLastFocusedOptionIndex();
this.changeFocusedOptionIndex(event, optionIndex);
!this.overlayVisible && this.show();
event.preventDefault();
}
},
onArrowLeftKey(event, pressedInInputText = false) {
pressedInInputText && (this.focusedOptionIndex = -1);
},
onHomeKey(event, pressedInInputText = false) {
if (pressedInInputText) {
event.currentTarget.setSelectionRange(0, 0);
this.focusedOptionIndex = -1;
} else {
this.changeFocusedOptionIndex(event, this.findFirstOptionIndex());
!this.overlayVisible && this.show();
}
event.preventDefault();
},
onEndKey(event, pressedInInputText = false) {
if (pressedInInputText) {
const target = event.currentTarget;
const len = target.value.length;
target.setSelectionRange(len, len);
this.focusedOptionIndex = -1;
} else {
this.changeFocusedOptionIndex(event, this.findLastOptionIndex());
!this.overlayVisible && this.show();
}
event.preventDefault();
},
onPageUpKey(event) {
this.scrollInView(0);
event.preventDefault();
},
onPageDownKey(event) {
this.scrollInView(this.visibleOptions.length - 1);
event.preventDefault();
},
onEnterKey(event) {
if (!this.overlayVisible) {
this.focusedOptionIndex = -1; // reset
this.onArrowDownKey(event);
} else {
if (this.focusedOptionIndex !== -1) {
this.onOptionSelect(event, this.visibleOptions[this.focusedOptionIndex]);
}
this.hide();
}
event.preventDefault();
},
onSpaceKey(event, pressedInInputText = false) {
!pressedInInputText && this.onEnterKey(event);
},
onEscapeKey(event) {
this.overlayVisible && this.hide(true);
event.preventDefault();
event.stopPropagation(); //@todo will be changed next versionss
},
onTabKey(event, pressedInInputText = false) {
if (!pressedInInputText) {
if (this.overlayVisible && this.hasFocusableElements()) {
DomHandler.focus(this.$refs.firstHiddenFocusableElementOnOverlay);
event.preventDefault();
} else {
if (this.focusedOptionIndex !== -1) {
this.onOptionSelect(event, this.visibleOptions[this.focusedOptionIndex]);
}
this.overlayVisible && this.hide(this.filter);
}
}
},
onBackspaceKey(event, pressedInInputText = false) {
if (pressedInInputText) {
!this.overlayVisible && this.show();
}
},
onOverlayEnter(el) {
ZIndexUtils.set('overlay', el, this.$primevue.config.zIndex.overlay);
DomHandler.addStyles(el, { position: 'absolute', top: '0', left: '0' });
this.alignOverlay();
this.scrollInView();
this.autoFilterFocus && DomHandler.focus(this.$refs.filterInput);
},
onOverlayAfterEnter() {
this.bindOutsideClickListener();
this.bindScrollListener();
this.bindResizeListener();
this.$emit('show');
},
onOverlayLeave() {
this.unbindOutsideClickListener();
this.unbindScrollListener();
this.unbindResizeListener();
this.$emit('hide');
this.overlay = null;
},
onOverlayAfterLeave(el) {
ZIndexUtils.clear(el);
},
alignOverlay() {
if (this.appendTo === 'self') {
DomHandler.relativePosition(this.overlay, this.$el);
} else {
this.overlay.style.minWidth = DomHandler.getOuterWidth(this.$el) + 'px';
DomHandler.absolutePosition(this.overlay, this.$el);
}
},
bindOutsideClickListener() {
if (!this.outsideClickListener) {
this.outsideClickListener = (event) => {
if (this.overlayVisible && this.overlay && !this.$el.contains(event.target) && !this.overlay.contains(event.target)) {
this.hide();
}
};
document.addEventListener('click', this.outsideClickListener);
}
},
unbindOutsideClickListener() {
if (this.outsideClickListener) {
document.removeEventListener('click', this.outsideClickListener);
this.outsideClickListener = null;
}
},
bindScrollListener() {
if (!this.scrollHandler) {
this.scrollHandler = new ConnectedOverlayScrollHandler(this.$refs.container, () => {
if (this.overlayVisible) {
this.hide();
}
});
}
this.scrollHandler.bindScrollListener();
},
unbindScrollListener() {
if (this.scrollHandler) {
this.scrollHandler.unbindScrollListener();
}
},
bindResizeListener() {
if (!this.resizeListener) {
this.resizeListener = () => {
if (this.overlayVisible && !DomHandler.isTouchDevice()) {
this.hide();
}
};
window.addEventListener('resize', this.resizeListener);
}
},
unbindResizeListener() {
if (this.resizeListener) {
window.removeEventListener('resize', this.resizeListener);
this.resizeListener = null;
}
},
bindLabelClickListener() {
if (!this.editable && !this.labelClickListener) {
const label = document.querySelector(`label[for="${this.inputId}"]`);
if (label && DomHandler.isVisible(label)) {
this.labelClickListener = () => {
DomHandler.focus(this.$refs.focusInput);
};
label.addEventListener('click', this.labelClickListener);
}
}
},
unbindLabelClickListener() {
if (this.labelClickListener) {
const label = document.querySelector(`label[for="${this.inputId}"]`);
if (label && DomHandler.isVisible(label)) {
label.removeEventListener('click', this.labelClickListener);
}
}
},
hasFocusableElements() {
return DomHandler.getFocusableElements(this.overlay, ':not([data-p-hidden-focusable="true"])').length > 0;
},
isOptionMatched(option) {
return this.isValidOption(option) && typeof this.getOptionLabel(option) === 'string' && this.getOptionLabel(option)?.toLocaleLowerCase(this.filterLocale).startsWith(this.searchValue.toLocaleLowerCase(this.filterLocale));
},
isValidOption(option) {
return ObjectUtils.isNotEmpty(option) && !(this.isOptionDisabled(option) || this.isOptionGroup(option));
},
isValidSelectedOption(option) {
return this.isValidOption(option) && this.isSelected(option);
},
isSelected(option) {
return this.isValidOption(option) && ObjectUtils.equals(this.modelValue, this.getOptionValue(option), this.equalityKey);
},
findFirstOptionIndex() {
return this.visibleOptions.findIndex((option) => this.isValidOption(option));
},
findLastOptionIndex() {
return ObjectUtils.findLastIndex(this.visibleOptions, (option) => this.isValidOption(option));
},
findNextOptionIndex(index) {
const matchedOptionIndex = index < this.visibleOptions.length - 1 ? this.visibleOptions.slice(index + 1).findIndex((option) => this.isValidOption(option)) : -1;
return matchedOptionIndex > -1 ? matchedOptionIndex + index + 1 : index;
},
findPrevOptionIndex(index) {
const matchedOptionIndex = index > 0 ? ObjectUtils.findLastIndex(this.visibleOptions.slice(0, index), (option) => this.isValidOption(option)) : -1;
return matchedOptionIndex > -1 ? matchedOptionIndex : index;
},
findSelectedOptionIndex() {
return this.hasSelectedOption ? this.visibleOptions.findIndex((option) => this.isValidSelectedOption(option)) : -1;
},
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;
if (ObjectUtils.isNotEmpty(this.searchValue)) {
if (this.focusedOptionIndex !== -1) {
optionIndex = this.visibleOptions.slice(this.focusedOptionIndex).findIndex((option) => this.isOptionMatched(option));
optionIndex = optionIndex === -1 ? this.visibleOptions.slice(0, this.focusedOptionIndex).findIndex((option) => this.isOptionMatched(option)) : optionIndex + this.focusedOptionIndex;
} else {
optionIndex = this.visibleOptions.findIndex((option) => this.isOptionMatched(option));
}
if (optionIndex !== -1) {
matched = true;
}
if (optionIndex === -1 && this.focusedOptionIndex === -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;
},
changeFocusedOptionIndex(event, index) {
if (this.focusedOptionIndex !== index) {
this.focusedOptionIndex = index;
this.scrollInView();
if (this.selectOnFocus) {
this.onOptionSelect(event, this.visibleOptions[index], false);
}
}
},
scrollInView(index = -1) {
this.$nextTick(() => {
const id = index !== -1 ? `${this.id}_${index}` : this.focusedOptionId;
const element = DomHandler.findSingle(this.list, `li[id="${id}"]`);
if (element) {
element.scrollIntoView && element.scrollIntoView({ block: 'nearest', inline: 'start' });
} else if (!this.virtualScrollerDisabled) {
this.virtualScroller && this.virtualScroller.scrollToIndex(index !== -1 ? index : this.focusedOptionIndex);
}
});
},
autoUpdateModel() {
if (this.selectOnFocus && this.autoOptionFocus && !this.hasSelectedOption) {
this.focusedOptionIndex = this.findFirstFocusedOptionIndex();
this.onOptionSelect(null, this.visibleOptions[this.focusedOptionIndex], false);
}
},
updateModel(event, value) {
this.$emit('update:modelValue', value);
this.$emit('change', { originalEvent: event, value });
},
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;
}, []);
},
overlayRef(el) {
this.overlay = el;
},
listRef(el, contentRef) {
this.list = el;
contentRef && contentRef(el); // For VirtualScroller
},
virtualScrollerRef(el) {
this.virtualScroller = el;
}
},
computed: {
visibleOptions() {
const options = this.optionGroupLabel ? this.flatOptions(this.options) : this.options || [];
if (this.filterValue) {
const filteredOptions = FilterService.filter(options, this.searchFields, this.filterValue, this.filterMatchMode, this.filterLocale);
if (this.optionGroupLabel) {
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;
},
hasSelectedOption() {
return ObjectUtils.isNotEmpty(this.modelValue);
},
label() {
const selectedOptionIndex = this.findSelectedOptionIndex();
return selectedOptionIndex !== -1 ? this.getOptionLabel(this.visibleOptions[selectedOptionIndex]) : this.placeholder || 'p-emptylabel';
},
editableInputValue() {
const selectedOptionIndex = this.findSelectedOptionIndex();
return selectedOptionIndex !== -1 ? this.getOptionLabel(this.visibleOptions[selectedOptionIndex]) : this.modelValue || '';
},
equalityKey() {
return this.optionValue ? null : this.dataKey;
},
searchFields() {
return this.filterFields || [this.optionLabel];
},
filterResultMessageText() {
return ObjectUtils.isNotEmpty(this.visibleOptions) ? this.filterMessageText.replaceAll('{0}', this.visibleOptions.length) : this.emptyFilterMessageText;
},
filterMessageText() {
return this.filterMessage || this.$primevue.config.locale.searchMessage || '';
},
emptyFilterMessageText() {
return this.emptyFilterMessage || this.$primevue.config.locale.emptySearchMessage || this.$primevue.config.locale.emptyFilterMessage || '';
},
emptyMessageText() {
return this.emptyMessage || this.$primevue.config.locale.emptyMessage || '';
},
selectionMessageText() {
return this.selectionMessage || this.$primevue.config.locale.selectionMessage || '';
},
emptySelectionMessageText() {
return this.emptySelectionMessage || this.$primevue.config.locale.emptySelectionMessage || '';
},
selectedMessageText() {
return this.hasSelectedOption ? this.selectionMessageText.replaceAll('{0}', '1') : this.emptySelectionMessageText;
},
focusedOptionId() {
return this.focusedOptionIndex !== -1 ? `${this.id}_${this.focusedOptionIndex}` : null;
},
ariaSetSize() {
return this.visibleOptions.filter((option) => !this.isOptionGroup(option)).length;
},
virtualScrollerDisabled() {
return !this.virtualScrollerOptions;
}
},
directives: {
ripple: Ripple
},
components: {
VirtualScroller,
Portal,
TimesIcon,
ChevronDownIcon,
SpinnerIcon,
SearchIcon,
CheckIcon,
BlankIcon
}
};
</script>

View File

@ -0,0 +1,9 @@
{
"main": "./select.cjs.js",
"module": "./select.esm.js",
"unpkg": "./select.min.js",
"types": "./Select.d.ts",
"browser": {
"./sfc": "./Select.vue"
}
}

View File

@ -0,0 +1,3 @@
import { BaseStyle } from '../../base/style';
export interface SelectStyle extends BaseStyle {}

View File

@ -0,0 +1,58 @@
import BaseStyle from 'primevue/base/style';
const classes = {
root: ({ instance, props, state }) => [
'p-select p-component p-inputwrapper',
{
'p-disabled': props.disabled,
'p-invalid': props.invalid,
'p-variant-filled': props.variant ? props.variant === 'filled' : instance.$primevue.config.inputStyle === 'filled',
'p-focus': state.focused,
'p-inputwrapper-filled': instance.hasSelectedOption,
'p-inputwrapper-focus': state.focused || state.overlayVisible,
'p-select-open': state.overlayVisible
}
],
input: ({ instance, props }) => [
'p-select-label',
{
'p-placeholder': !props.editable && instance.label === props.placeholder,
'p-select-label-empty': !props.editable && !instance.$slots['value'] && (instance.label === 'p-emptylabel' || instance.label.length === 0)
}
],
clearIcon: 'p-select-clear-icon',
trigger: 'p-select-dropdown',
loadingicon: 'p-select-loading-icon',
dropdownIcon: 'p-select-dropdown-icon',
panel: ({ instance }) => [
'p-select-overlay p-component',
{
'p-ripple-disabled': instance.$primevue.config.ripple === false
}
],
header: 'p-select-header',
filterContainer: 'p-select-filter-container',
filterInput: 'p-select-filter',
filterIcon: 'p-select-filter-icon',
wrapper: 'p-select-list-container',
list: 'p-select-list',
itemGroup: 'p-select-option-group',
itemGroupLabel: 'p-select-option-group-label',
item: ({ instance, props, state, option, focusedOption }) => [
'p-select-option',
{
'p-select-option-selected': instance.isSelected(option) && props.highlightOnSelect,
'p-focus': state.focusedOptionIndex === focusedOption,
'p-disabled': instance.isOptionDisabled(option)
}
],
itemLabel: 'p-select-option-label',
checkIcon: 'p-select-option-check-icon',
blankIcon: 'p-select-option-blank-icon',
emptyMessage: 'p-select-empty-message'
};
export default BaseStyle.extend({
name: 'select',
classes
});

View File

@ -0,0 +1,6 @@
{
"main": "./selectstyle.cjs.js",
"module": "./selectstyle.esm.js",
"unpkg": "./selectstyle.min.js",
"types": "./SelectStyle.d.ts"
}

View File

@ -5,38 +5,38 @@ export default {
cursor: pointer;
position: relative;
user-select: none;
background: ${dt('dropdown.background')};
border: 1px solid ${dt('dropdown.border.color')};
background: ${dt('select.background')};
border: 1px solid ${dt('select.border.color')};
transition: background-color ${dt('transition.duration')}, color ${dt('transition.duration')}, border-color ${dt('transition.duration')}, outline-color ${dt('transition.duration')};
border-radius: ${dt('rounded.base')};
outline-color: transparent;
box-shadow: ${dt('dropdown.box.shadow')};
box-shadow: ${dt('select.box.shadow')};
}
.p-select:not(.p-disabled):hover {
border-color: ${dt('dropdown.hover.border.color')};
border-color: ${dt('select.hover.border.color')};
}
.p-select:not(.p-disabled).p-focus {
border-color:${dt('dropdown.focus.border.color')};
border-color:${dt('select.focus.border.color')};
outline: 0 none;
}
.p-select.p-variant-filled {
background: ${dt('dropdown.filled.background')};
background: ${dt('select.filled.background')};
}
.p-select.p-variant-filled.p-focus {
background: ${dt('dropdown.filled.focus.background')};
background: ${dt('select.filled.focus.background')};
}
.p-select.p-invalid {
border-color: ${dt('dropdown.invalid.border.color')};
border-color: ${dt('select.invalid.border.color')};
}
.p-select.p-disabled {
opacity: 1;
background: ${dt('dropdown.disabled.background')};
background: ${dt('select.disabled.background')};
}
.p-select-clear-icon {
@ -53,7 +53,7 @@ export default {
justify-content: center;
flex-shrink: 0;
background: transparent;
color: ${dt('dropdown.toggle.color')};
color: ${dt('select.toggle.color')};
width: 2.5rem;
border-top-right-radius: ${dt('rounded.base')};
border-bottom-right-radius: ${dt('rounded.base')};
@ -68,14 +68,14 @@ export default {
padding: 0.5rem 0.75rem;
text-overflow: ellipsis;
cursor: pointer;
color: ${dt('dropdown.color')};
color: ${dt('select.color')};
background: transparent;
border: 0 none;
outline: 0 none;
}
.p-select-label.p-placeholder {
color: ${dt('dropdown.placeholder.color')};
color: ${dt('select.placeholder.color')};
}
.p-select:has(.p-select-clear-icon) .p-select-label {
@ -83,7 +83,7 @@ export default {
}
.p-select.p-disabled .p-select-label {
color: ${dt('dropdown.disabled.color')};
color: ${dt('select.disabled.color')};
}
.p-select-label-empty {
@ -103,9 +103,9 @@ input.p-select-label {
position: absolute;
top: 0;
left: 0;
background: ${dt('dropdown.overlay.background')};
color: ${dt('dropdown.overlay.color')};
border: 1px solid ${dt('dropdown.overlay.border.color')};
background: ${dt('select.overlay.background')};
color: ${dt('select.overlay.color')};
border: 1px solid ${dt('select.overlay.border.color')};
border-radius: ${dt('rounded.base')};
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
@ -129,7 +129,7 @@ input.p-select-label {
top: 50%;
margin-top: -0.5rem;
right: 0.75rem;
color: ${dt('dropdown.filter.icon.color')};
color: ${dt('select.filter.icon.color')};
}
.p-select-list-container {
@ -140,8 +140,8 @@ input.p-select-label {
cursor: auto;
margin: 0;
padding: 0.5rem 0.75rem;
background: ${dt('dropdown.item.group.background')};
color: ${dt('dropdown.item.group.color')};
background: ${dt('select.item.group.background')};
color: ${dt('select.item.group.color')};
font-weight: 600;
}
@ -163,7 +163,7 @@ input.p-select-label {
margin: 2px 0;
padding: 0.5rem 0.75rem;
border: 0 none;
color: ${dt('dropdown.item.color')};
color: ${dt('select.item.color')};
background: transparent;
transition: background-color ${dt('transition.duration')}, color ${dt('transition.duration')}, border-color ${dt('transition.duration')}, box-shadow ${dt('transition.duration')}, outline-color ${dt('transition.duration')};
border-radius: ${dt('rounded.sm')};
@ -178,25 +178,25 @@ input.p-select-label {
}
.p-select-option:not(.p-select-option-selected):not(.p-disabled).p-focus {
background: ${dt('dropdown.item.focus.background')};
color: ${dt('dropdown.item.focus.color')};
background: ${dt('select.item.focus.background')};
color: ${dt('select.item.focus.color')};
}
.p-select-option.p-select-option-selected {
background: ${dt('dropdown.item.selected.background')};
color: ${dt('dropdown.item.selected.color')};
background: ${dt('select.item.selected.background')};
color: ${dt('select.item.selected.color')};
}
.p-select-option.p-select-option-selected.p-focus {
background: ${dt('dropdown.item.selected.focus.background')};
color: ${dt('dropdown.item.selected.focus.color')};
background: ${dt('select.item.selected.focus.background')};
color: ${dt('select.item.selected.focus.color')};
}
.p-select-option-check-icon {
position: relative;
margin-left: -0.375rem;
margin-right: 0.375rem;
color: ${dt('dropdown.checkmark.color')};
color: ${dt('select.checkmark.color')};
}
.p-select-empty-message {