123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- import { createTextVNode as _createTextVNode, mergeProps as _mergeProps, createVNode as _createVNode, Fragment as _Fragment } from "vue";
- // Styles
- import "./VSelect.css";
- // Components
- import { VDialogTransition } from "../transitions/index.mjs";
- import { VAvatar } from "../VAvatar/index.mjs";
- import { VCheckboxBtn } from "../VCheckbox/index.mjs";
- import { VChip } from "../VChip/index.mjs";
- import { VDefaultsProvider } from "../VDefaultsProvider/index.mjs";
- import { VIcon } from "../VIcon/index.mjs";
- import { VList, VListItem } from "../VList/index.mjs";
- import { VMenu } from "../VMenu/index.mjs";
- import { makeVTextFieldProps, VTextField } from "../VTextField/VTextField.mjs";
- import { VVirtualScroll } from "../VVirtualScroll/index.mjs"; // Composables
- import { useScrolling } from "./useScrolling.mjs";
- import { useForm } from "../../composables/form.mjs";
- import { forwardRefs } from "../../composables/forwardRefs.mjs";
- import { IconValue } from "../../composables/icons.mjs";
- import { makeItemsProps, useItems } from "../../composables/list-items.mjs";
- import { useLocale } from "../../composables/locale.mjs";
- import { useProxiedModel } from "../../composables/proxiedModel.mjs";
- import { makeTransitionProps } from "../../composables/transition.mjs"; // Utilities
- import { computed, mergeProps, nextTick, ref, shallowRef, watch } from 'vue';
- import { checkPrintable, ensureValidVNode, genericComponent, IN_BROWSER, matchesSelector, omit, propsFactory, useRender, wrapInArray } from "../../util/index.mjs"; // Types
- export const makeSelectProps = propsFactory({
- chips: Boolean,
- closableChips: Boolean,
- closeText: {
- type: String,
- default: '$vuetify.close'
- },
- openText: {
- type: String,
- default: '$vuetify.open'
- },
- eager: Boolean,
- hideNoData: Boolean,
- hideSelected: Boolean,
- listProps: {
- type: Object
- },
- menu: Boolean,
- menuIcon: {
- type: IconValue,
- default: '$dropdown'
- },
- menuProps: {
- type: Object
- },
- multiple: Boolean,
- noDataText: {
- type: String,
- default: '$vuetify.noDataText'
- },
- openOnClear: Boolean,
- itemColor: String,
- ...makeItemsProps({
- itemChildren: false
- })
- }, 'Select');
- export const makeVSelectProps = propsFactory({
- ...makeSelectProps(),
- ...omit(makeVTextFieldProps({
- modelValue: null,
- role: 'combobox'
- }), ['validationValue', 'dirty', 'appendInnerIcon']),
- ...makeTransitionProps({
- transition: {
- component: VDialogTransition
- }
- })
- }, 'VSelect');
- export const VSelect = genericComponent()({
- name: 'VSelect',
- props: makeVSelectProps(),
- emits: {
- 'update:focused': focused => true,
- 'update:modelValue': value => true,
- 'update:menu': ue => true
- },
- setup(props, _ref) {
- let {
- slots
- } = _ref;
- const {
- t
- } = useLocale();
- const vTextFieldRef = ref();
- const vMenuRef = ref();
- const vVirtualScrollRef = ref();
- const _menu = useProxiedModel(props, 'menu');
- const menu = computed({
- get: () => _menu.value,
- set: v => {
- if (_menu.value && !v && vMenuRef.value?.ΨopenChildren.size) return;
- _menu.value = v;
- }
- });
- const {
- items,
- transformIn,
- transformOut
- } = useItems(props);
- const model = useProxiedModel(props, 'modelValue', [], v => transformIn(v === null ? [null] : wrapInArray(v)), v => {
- const transformed = transformOut(v);
- return props.multiple ? transformed : transformed[0] ?? null;
- });
- const counterValue = computed(() => {
- return typeof props.counterValue === 'function' ? props.counterValue(model.value) : typeof props.counterValue === 'number' ? props.counterValue : model.value.length;
- });
- const form = useForm(props);
- const selectedValues = computed(() => model.value.map(selection => selection.value));
- const isFocused = shallowRef(false);
- const label = computed(() => menu.value ? props.closeText : props.openText);
- let keyboardLookupPrefix = '';
- let keyboardLookupLastTime;
- const displayItems = computed(() => {
- if (props.hideSelected) {
- return items.value.filter(item => !model.value.some(s => props.valueComparator(s, item)));
- }
- return items.value;
- });
- const menuDisabled = computed(() => props.hideNoData && !displayItems.value.length || form.isReadonly.value || form.isDisabled.value);
- const computedMenuProps = computed(() => {
- return {
- ...props.menuProps,
- activatorProps: {
- ...(props.menuProps?.activatorProps || {}),
- 'aria-haspopup': 'listbox' // Set aria-haspopup to 'listbox'
- }
- };
- });
- const listRef = ref();
- const listEvents = useScrolling(listRef, vTextFieldRef);
- function onClear(e) {
- if (props.openOnClear) {
- menu.value = true;
- }
- }
- function onMousedownControl() {
- if (menuDisabled.value) return;
- menu.value = !menu.value;
- }
- function onListKeydown(e) {
- if (checkPrintable(e)) {
- onKeydown(e);
- }
- }
- function onKeydown(e) {
- if (!e.key || form.isReadonly.value) return;
- if (['Enter', ' ', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) {
- e.preventDefault();
- }
- if (['Enter', 'ArrowDown', ' '].includes(e.key)) {
- menu.value = true;
- }
- if (['Escape', 'Tab'].includes(e.key)) {
- menu.value = false;
- }
- if (e.key === 'Home') {
- listRef.value?.focus('first');
- } else if (e.key === 'End') {
- listRef.value?.focus('last');
- }
- // html select hotkeys
- const KEYBOARD_LOOKUP_THRESHOLD = 1000; // milliseconds
- if (props.multiple || !checkPrintable(e)) return;
- const now = performance.now();
- if (now - keyboardLookupLastTime > KEYBOARD_LOOKUP_THRESHOLD) {
- keyboardLookupPrefix = '';
- }
- keyboardLookupPrefix += e.key.toLowerCase();
- keyboardLookupLastTime = now;
- const item = items.value.find(item => item.title.toLowerCase().startsWith(keyboardLookupPrefix));
- if (item !== undefined) {
- model.value = [item];
- const index = displayItems.value.indexOf(item);
- IN_BROWSER && window.requestAnimationFrame(() => {
- index >= 0 && vVirtualScrollRef.value?.scrollToIndex(index);
- });
- }
- }
- /** @param set - null means toggle */
- function select(item) {
- let set = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
- if (item.props.disabled) return;
- if (props.multiple) {
- const index = model.value.findIndex(selection => props.valueComparator(selection.value, item.value));
- const add = set == null ? !~index : set;
- if (~index) {
- const value = add ? [...model.value, item] : [...model.value];
- value.splice(index, 1);
- model.value = value;
- } else if (add) {
- model.value = [...model.value, item];
- }
- } else {
- const add = set !== false;
- model.value = add ? [item] : [];
- nextTick(() => {
- menu.value = false;
- });
- }
- }
- function onBlur(e) {
- if (!listRef.value?.$el.contains(e.relatedTarget)) {
- menu.value = false;
- }
- }
- function onAfterEnter() {
- if (props.eager) {
- vVirtualScrollRef.value?.calculateVisibleItems();
- }
- }
- function onAfterLeave() {
- if (isFocused.value) {
- vTextFieldRef.value?.focus();
- }
- }
- function onFocusin(e) {
- isFocused.value = true;
- }
- function onModelUpdate(v) {
- if (v == null) model.value = [];else if (matchesSelector(vTextFieldRef.value, ':autofill') || matchesSelector(vTextFieldRef.value, ':-webkit-autofill')) {
- const item = items.value.find(item => item.title === v);
- if (item) {
- select(item);
- }
- } else if (vTextFieldRef.value) {
- vTextFieldRef.value.value = '';
- }
- }
- watch(menu, () => {
- if (!props.hideSelected && menu.value && model.value.length) {
- const index = displayItems.value.findIndex(item => model.value.some(s => props.valueComparator(s.value, item.value)));
- IN_BROWSER && window.requestAnimationFrame(() => {
- index >= 0 && vVirtualScrollRef.value?.scrollToIndex(index);
- });
- }
- });
- watch(() => props.items, (newVal, oldVal) => {
- if (menu.value) return;
- if (isFocused.value && !oldVal.length && newVal.length) {
- menu.value = true;
- }
- });
- useRender(() => {
- const hasChips = !!(props.chips || slots.chip);
- const hasList = !!(!props.hideNoData || displayItems.value.length || slots['prepend-item'] || slots['append-item'] || slots['no-data']);
- const isDirty = model.value.length > 0;
- const textFieldProps = VTextField.filterProps(props);
- const placeholder = isDirty || !isFocused.value && props.label && !props.persistentPlaceholder ? undefined : props.placeholder;
- return _createVNode(VTextField, _mergeProps({
- "ref": vTextFieldRef
- }, textFieldProps, {
- "modelValue": model.value.map(v => v.props.value).join(', '),
- "onUpdate:modelValue": onModelUpdate,
- "focused": isFocused.value,
- "onUpdate:focused": $event => isFocused.value = $event,
- "validationValue": model.externalValue,
- "counterValue": counterValue.value,
- "dirty": isDirty,
- "class": ['v-select', {
- 'v-select--active-menu': menu.value,
- 'v-select--chips': !!props.chips,
- [`v-select--${props.multiple ? 'multiple' : 'single'}`]: true,
- 'v-select--selected': model.value.length,
- 'v-select--selection-slot': !!slots.selection
- }, props.class],
- "style": props.style,
- "inputmode": "none",
- "placeholder": placeholder,
- "onClick:clear": onClear,
- "onMousedown:control": onMousedownControl,
- "onBlur": onBlur,
- "onKeydown": onKeydown,
- "aria-label": t(label.value),
- "title": t(label.value)
- }), {
- ...slots,
- default: () => _createVNode(_Fragment, null, [_createVNode(VMenu, _mergeProps({
- "ref": vMenuRef,
- "modelValue": menu.value,
- "onUpdate:modelValue": $event => menu.value = $event,
- "activator": "parent",
- "contentClass": "v-select__content",
- "disabled": menuDisabled.value,
- "eager": props.eager,
- "maxHeight": 310,
- "openOnClick": false,
- "closeOnContentClick": false,
- "transition": props.transition,
- "onAfterEnter": onAfterEnter,
- "onAfterLeave": onAfterLeave
- }, computedMenuProps.value), {
- default: () => [hasList && _createVNode(VList, _mergeProps({
- "ref": listRef,
- "selected": selectedValues.value,
- "selectStrategy": props.multiple ? 'independent' : 'single-independent',
- "onMousedown": e => e.preventDefault(),
- "onKeydown": onListKeydown,
- "onFocusin": onFocusin,
- "tabindex": "-1",
- "aria-live": "polite",
- "color": props.itemColor ?? props.color
- }, listEvents, props.listProps), {
- default: () => [slots['prepend-item']?.(), !displayItems.value.length && !props.hideNoData && (slots['no-data']?.() ?? _createVNode(VListItem, {
- "key": "no-data",
- "title": t(props.noDataText)
- }, null)), _createVNode(VVirtualScroll, {
- "ref": vVirtualScrollRef,
- "renderless": true,
- "items": displayItems.value
- }, {
- default: _ref2 => {
- let {
- item,
- index,
- itemRef
- } = _ref2;
- const itemProps = mergeProps(item.props, {
- ref: itemRef,
- key: item.value,
- onClick: () => select(item, null)
- });
- return slots.item?.({
- item,
- index,
- props: itemProps
- }) ?? _createVNode(VListItem, _mergeProps(itemProps, {
- "role": "option"
- }), {
- prepend: _ref3 => {
- let {
- isSelected
- } = _ref3;
- return _createVNode(_Fragment, null, [props.multiple && !props.hideSelected ? _createVNode(VCheckboxBtn, {
- "key": item.value,
- "modelValue": isSelected,
- "ripple": false,
- "tabindex": "-1"
- }, null) : undefined, item.props.prependAvatar && _createVNode(VAvatar, {
- "image": item.props.prependAvatar
- }, null), item.props.prependIcon && _createVNode(VIcon, {
- "icon": item.props.prependIcon
- }, null)]);
- }
- });
- }
- }), slots['append-item']?.()]
- })]
- }), model.value.map((item, index) => {
- function onChipClose(e) {
- e.stopPropagation();
- e.preventDefault();
- select(item, false);
- }
- const slotProps = {
- 'onClick:close': onChipClose,
- onKeydown(e) {
- if (e.key !== 'Enter' && e.key !== ' ') return;
- e.preventDefault();
- e.stopPropagation();
- onChipClose(e);
- },
- onMousedown(e) {
- e.preventDefault();
- e.stopPropagation();
- },
- modelValue: true,
- 'onUpdate:modelValue': undefined
- };
- const hasSlot = hasChips ? !!slots.chip : !!slots.selection;
- const slotContent = hasSlot ? ensureValidVNode(hasChips ? slots.chip({
- item,
- index,
- props: slotProps
- }) : slots.selection({
- item,
- index
- })) : undefined;
- if (hasSlot && !slotContent) return undefined;
- return _createVNode("div", {
- "key": item.value,
- "class": "v-select__selection"
- }, [hasChips ? !slots.chip ? _createVNode(VChip, _mergeProps({
- "key": "chip",
- "closable": props.closableChips,
- "size": "small",
- "text": item.title,
- "disabled": item.props.disabled
- }, slotProps), null) : _createVNode(VDefaultsProvider, {
- "key": "chip-defaults",
- "defaults": {
- VChip: {
- closable: props.closableChips,
- size: 'small',
- text: item.title
- }
- }
- }, {
- default: () => [slotContent]
- }) : slotContent ?? _createVNode("span", {
- "class": "v-select__selection-text"
- }, [item.title, props.multiple && index < model.value.length - 1 && _createVNode("span", {
- "class": "v-select__selection-comma"
- }, [_createTextVNode(",")])])]);
- })]),
- 'append-inner': function () {
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
- args[_key] = arguments[_key];
- }
- return _createVNode(_Fragment, null, [slots['append-inner']?.(...args), props.menuIcon ? _createVNode(VIcon, {
- "class": "v-select__menu-icon",
- "icon": props.menuIcon
- }, null) : undefined]);
- }
- });
- });
- return forwardRefs({
- isFocused,
- menu,
- select
- }, vTextFieldRef);
- }
- });
- //# sourceMappingURL=VSelect.mjs.map
|