mirror of
https://github.com/primefaces/primevue.git
synced 2025-05-09 00:42:36 +00:00
Refactor #5612 - Dropdown / Select
This commit is contained in:
parent
def5d060c0
commit
55ae9908d1
16 changed files with 1915 additions and 1778 deletions
|
@ -1,173 +0,0 @@
|
|||
<script>
|
||||
import BaseComponent from 'primevue/basecomponent';
|
||||
import DropdownStyle from 'primevue/dropdown/style';
|
||||
|
||||
export default {
|
||||
name: 'BaseDropdown',
|
||||
extends: BaseComponent,
|
||||
props: {
|
||||
modelValue: null,
|
||||
options: Array,
|
||||
optionLabel: [String, Function],
|
||||
optionValue: [String, Function],
|
||||
optionDisabled: [String, Function],
|
||||
optionGroupLabel: [String, Function],
|
||||
optionGroupChildren: [String, Function],
|
||||
scrollHeight: {
|
||||
type: String,
|
||||
default: '14rem'
|
||||
},
|
||||
filter: Boolean,
|
||||
filterPlaceholder: String,
|
||||
filterLocale: String,
|
||||
filterMatchMode: {
|
||||
type: String,
|
||||
default: 'contains'
|
||||
},
|
||||
filterFields: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
editable: Boolean,
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
dataKey: null,
|
||||
showClear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inputId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
inputClass: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
},
|
||||
inputStyle: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
panelClass: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
},
|
||||
panelStyle: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
appendTo: {
|
||||
type: [String, Object],
|
||||
default: 'body'
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
clearIcon: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
dropdownIcon: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
filterIcon: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
resetFilterOnHide: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
resetFilterOnClear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
virtualScrollerOptions: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
autoOptionFocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autoFilterFocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectOnFocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
focusOnHover: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
highlightOnSelect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
checkmark: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
filterMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
selectionMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
emptySelectionMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
emptyFilterMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
emptyMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ariaLabelledby: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
style: DropdownStyle,
|
||||
provide() {
|
||||
return {
|
||||
$parentInstance: this
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
693
components/lib/dropdown/Dropdown.d.ts
vendored
693
components/lib/dropdown/Dropdown.d.ts
vendored
|
@ -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/)
|
||||
* --- ---
|
||||
* 
|
||||
*
|
||||
|
|
|
@ -1,369 +0,0 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import { h } from 'vue';
|
||||
import Dropdown from './Dropdown.vue';
|
||||
|
||||
describe('Dropdown.vue', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(Dropdown, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
expect(wrapper.find('.p-inputwrapper-filled').exists()).toBe(false);
|
||||
expect(wrapper.find('.p-inputwrapper-focus').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('option checks', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(Dropdown, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
options: [
|
||||
{ name: 'New York', code: 'NY' },
|
||||
{ name: 'Rome', code: 'RM' },
|
||||
{ name: 'London', code: 'LDN' },
|
||||
{ name: 'Istanbul', code: 'IST' },
|
||||
{ name: 'Paris', code: 'PRS' }
|
||||
],
|
||||
optionLabel: 'name',
|
||||
optionValue: 'code',
|
||||
placeholder: 'Select a City'
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.trigger('click');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear checks', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(Dropdown, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
clearIcon: 'pi pi-discord',
|
||||
modelValue: 'value',
|
||||
showClear: true
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.trigger('click');
|
||||
});
|
||||
|
||||
it('should have correct icon', () => {
|
||||
expect(wrapper.find('.p-dropdown-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' });
|
||||
expect(updateModelSpy).toHaveBeenCalledOnce();
|
||||
expect(updateModelSpy).toHaveBeenCalledWith(expect.any(KeyboardEvent), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editable checks', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(Dropdown, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
options: [
|
||||
{ name: 'New York', code: 'NY' },
|
||||
{ name: 'Rome', code: 'RM' },
|
||||
{ name: 'London', code: 'LDN' },
|
||||
{ name: 'Istanbul', code: 'IST' },
|
||||
{ name: 'Paris', code: 'PRS' }
|
||||
],
|
||||
optionLabel: 'name',
|
||||
optionValue: 'code',
|
||||
placeholder: 'Select a City',
|
||||
editable: true
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.trigger('click');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('option groups checks', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(Dropdown, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
options: [
|
||||
{
|
||||
label: 'Germany',
|
||||
code: 'DE',
|
||||
items: [
|
||||
{ label: 'Berlin', value: 'Berlin' },
|
||||
{ label: 'Frankfurt', value: 'Frankfurt' },
|
||||
{ label: 'Hamburg', value: 'Hamburg' },
|
||||
{ label: 'Munich', value: 'Munich' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'USA',
|
||||
code: 'US',
|
||||
items: [
|
||||
{ label: 'Chicago', value: 'Chicago' },
|
||||
{ label: 'Los Angeles', value: 'Los Angeles' },
|
||||
{ label: 'New York', value: 'New York' },
|
||||
{ label: 'San Francisco', value: 'San Francisco' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Japan',
|
||||
code: 'JP',
|
||||
items: [
|
||||
{ label: 'Kyoto', value: 'Kyoto' },
|
||||
{ label: 'Osaka', value: 'Osaka' },
|
||||
{ label: 'Tokyo', value: 'Tokyo' },
|
||||
{ label: 'Yokohama', value: 'Yokohama' }
|
||||
]
|
||||
}
|
||||
],
|
||||
optionLabel: 'label',
|
||||
optionGroupLabel: 'label',
|
||||
optionGroupChildren: 'items'
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.trigger('click');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('templating checks', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(Dropdown, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
slots: {
|
||||
header: h('span', { class: 'header-slot' }, 'Header slot'),
|
||||
footer: h('span', { class: 'footer-slot' }, 'Footer slot'),
|
||||
option: h('span', { class: 'option-slot' }, 'Option slot'),
|
||||
optiongroup: h('span', { class: 'optiongroup-slot' }, 'OptionGroup slot'),
|
||||
emptyfilter: h('span', { class: 'emptyfilter-slot' }, 'Empty filter slot')
|
||||
},
|
||||
props: {
|
||||
options: [
|
||||
{
|
||||
label: 'Germany',
|
||||
code: 'DE',
|
||||
items: [
|
||||
{ label: 'Berlin', value: 'Berlin' },
|
||||
{ label: 'Frankfurt', value: 'Frankfurt' },
|
||||
{ label: 'Hamburg', value: 'Hamburg' },
|
||||
{ label: 'Munich', value: 'Munich' }
|
||||
]
|
||||
}
|
||||
],
|
||||
optionLabel: 'label',
|
||||
optionGroupLabel: 'label',
|
||||
optionGroupChildren: 'items'
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.trigger('click');
|
||||
});
|
||||
|
||||
it('should see header and footer slots', () => {
|
||||
expect(wrapper.find('.header-slot').exists()).toBe(true);
|
||||
expect(wrapper.find('.header-slot').text()).toBe('Header slot');
|
||||
expect(wrapper.find('.footer-slot').exists()).toBe(true);
|
||||
expect(wrapper.find('.footer-slot').text()).toBe('Footer slot');
|
||||
expect(wrapper.find('.option-slot').exists()).toBe(true);
|
||||
expect(wrapper.find('.option-slot').text()).toBe('Option slot');
|
||||
expect(wrapper.find('.optiongroup-slot').exists()).toBe(true);
|
||||
expect(wrapper.find('.optiongroup-slot').text()).toBe('OptionGroup slot');
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty templating checks', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(Dropdown, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
options: [],
|
||||
optionLabel: 'label',
|
||||
optionGroupLabel: 'label',
|
||||
optionGroupChildren: 'items',
|
||||
emptyMessage: 'Need options prop',
|
||||
filterValue: 'xd'
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.trigger('click');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loader checks', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(Dropdown, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
loading: true,
|
||||
loadingIcon: 'pi pi-discord',
|
||||
options: [
|
||||
{ name: 'New York', code: 'NY' },
|
||||
{ name: 'Rome', code: 'RM' },
|
||||
{ name: 'London', code: 'LDN' },
|
||||
{ name: 'Istanbul', code: 'IST' },
|
||||
{ name: 'Paris', code: 'PRS' }
|
||||
],
|
||||
optionLabel: 'name',
|
||||
optionValue: 'code',
|
||||
placeholder: 'Select a City'
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.trigger('click');
|
||||
});
|
||||
|
||||
it('should show the loader', async () => {
|
||||
expect(wrapper.find('.p-dropdown-trigger-icon').classes()).toContain('pi-discord');
|
||||
|
||||
await wrapper.setProps({ loading: false });
|
||||
|
||||
expect(wrapper.find('.p-dropdown-trigger-icon').classes()).not.toContain('pi-discord');
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter checks', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(Dropdown, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
filter: true,
|
||||
filterIcon: 'pi pi-discord',
|
||||
options: [
|
||||
{ name: 'Australia', code: 'AU' },
|
||||
{ name: 'Brazil', code: 'BR' },
|
||||
{ name: 'China', code: 'CN' },
|
||||
{ name: 'Egypt', code: 'EG' },
|
||||
{ name: 'France', code: 'FR' },
|
||||
{ name: 'Germany', code: 'DE' },
|
||||
{ name: 'India', code: 'IN' },
|
||||
{ name: 'Japan', code: 'JP' },
|
||||
{ name: 'Spain', code: 'ES' },
|
||||
{ name: 'United States', code: 'US' }
|
||||
],
|
||||
optionLabel: 'name'
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.trigger('click');
|
||||
});
|
||||
|
||||
it('should make filtering', async () => {
|
||||
const filterInput = wrapper.find('.p-dropdown-filter');
|
||||
const filterIcon = wrapper.find('.p-dropdown-filter-icon');
|
||||
|
||||
expect(filterInput.exists()).toBe(true);
|
||||
expect(filterIcon.classes()).toContain('pi-discord');
|
||||
|
||||
const event = { target: { value: 'c' } };
|
||||
const onFilterChange = vi.spyOn(wrapper.vm, 'onFilterChange');
|
||||
|
||||
wrapper.vm.onFilterChange(event);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(onFilterChange).toHaveBeenCalled();
|
||||
|
||||
await wrapper.setData({ filterValue: 'c' });
|
||||
|
||||
expect(wrapper.findAll('.p-dropdown-item').length).toBe(2);
|
||||
});
|
||||
});
|
|
@ -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' ? ' ' : 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>
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue