vuetify
Version:
Vue Material Component Framework
312 lines (311 loc) • 11.3 kB
JavaScript
import { createTextVNode as _createTextVNode, mergeProps as _mergeProps, createVNode as _createVNode, Fragment as _Fragment } from "vue";
// Styles
import "./VAutocomplete.css";
// Components
import { makeSelectProps } from "../VSelect/VSelect.mjs";
import { VCheckboxBtn } from "../VCheckbox/index.mjs";
import { VChip } from "../VChip/index.mjs";
import { VDefaultsProvider } from "../VDefaultsProvider/index.mjs";
import { VList, VListItem } from "../VList/index.mjs";
import { VMenu } from "../VMenu/index.mjs";
import { VTextField } from "../VTextField/index.mjs"; // Composables
import { forwardRefs } from "../../composables/forwardRefs.mjs";
import { makeFilterProps, useFilter } from "../../composables/filter.mjs";
import { makeTransitionProps } from "../../composables/transition.mjs";
import { useForm } from "../../composables/form.mjs";
import { useItems } from "../../composables/items.mjs";
import { useLocale } from "../../composables/locale.mjs";
import { useProxiedModel } from "../../composables/proxiedModel.mjs"; // Utility
import { computed, mergeProps, nextTick, ref, watch } from 'vue';
import { genericComponent, omit, useRender, wrapInArray } from "../../util/index.mjs";
import { filterVTextFieldProps, makeVTextFieldProps } from "../VTextField/VTextField.mjs"; // Types
function highlightResult(text, matches, length) {
if (matches == null) return text;
if (Array.isArray(matches)) throw new Error('Multiple matches is not implemented');
return typeof matches === 'number' && ~matches ? _createVNode(_Fragment, null, [_createVNode("span", {
"class": "v-autocomplete__unmask"
}, [text.substr(0, matches)]), _createVNode("span", {
"class": "v-autocomplete__mask"
}, [text.substr(matches, length)]), _createVNode("span", {
"class": "v-autocomplete__unmask"
}, [text.substr(matches + length)])]) : text;
}
export const VAutocomplete = genericComponent()({
name: 'VAutocomplete',
props: {
// TODO: implement post keyboard support
// autoSelectFirst: Boolean,
search: String,
...makeFilterProps({
filterKeys: ['title']
}),
...makeSelectProps(),
...omit(makeVTextFieldProps({
modelValue: null
}), ['validationValue', 'dirty', 'appendInnerIcon']),
...makeTransitionProps({
transition: false
})
},
emits: {
'update:search': val => true,
'update:modelValue': val => true,
'update:menu': val => true
},
setup(props, _ref) {
let {
slots
} = _ref;
const {
t
} = useLocale();
const vTextFieldRef = ref();
const isFocused = ref(false);
const isPristine = ref(true);
const vMenuRef = ref();
const _menu = useProxiedModel(props, 'menu');
const menu = computed({
get: () => _menu.value,
set: v => {
if (_menu.value && !v && vMenuRef.value?.ΨopenChildren) return;
_menu.value = v;
}
});
const {
items,
transformIn,
transformOut
} = useItems(props);
const search = useProxiedModel(props, 'search', '');
const model = useProxiedModel(props, 'modelValue', [], v => transformIn(wrapInArray(v)), v => {
const transformed = transformOut(v);
return props.multiple ? transformed : transformed[0] ?? null;
});
const form = useForm();
const {
filteredItems,
getMatches
} = useFilter(props, items, computed(() => isPristine.value ? undefined : search.value));
const selections = computed(() => {
return model.value.map(v => {
return items.value.find(item => props.valueComparator(item.value, v.value)) || v;
});
});
const displayItems = computed(() => {
if (props.hideSelected) {
return filteredItems.value.filter(filteredItem => !selections.value.some(s => s.value === filteredItem.value));
}
return filteredItems.value;
});
const selected = computed(() => selections.value.map(selection => selection.props.value));
const listRef = ref();
function onClear(e) {
if (props.openOnClear) {
menu.value = true;
}
search.value = '';
}
function onMousedownControl() {
if (props.hideNoData && !items.value.length || props.readonly || form?.isReadonly.value) return;
menu.value = true;
}
function onKeydown(e) {
if (props.readonly || form?.isReadonly.value) return;
if (['Enter', 'ArrowDown', 'ArrowUp'].includes(e.key)) {
e.preventDefault();
}
if (['Enter', 'ArrowDown'].includes(e.key)) {
menu.value = true;
}
if (['Escape'].includes(e.key)) {
menu.value = false;
}
if (['Enter', 'Escape', 'Tab'].includes(e.key)) {
isPristine.value = true;
}
if (e.key === 'ArrowDown') {
listRef.value?.focus('next');
} else if (e.key === 'ArrowUp') {
listRef.value?.focus('prev');
}
}
function onInput(e) {
search.value = e.target.value;
}
function onAfterLeave() {
if (isFocused.value) isPristine.value = true;
}
function onFocusin(e) {
isFocused.value = true;
}
function onFocusout(e) {
if (e.relatedTarget == null) {
vTextFieldRef.value?.focus();
}
}
const isSelecting = ref(false);
function select(item) {
if (props.multiple) {
const index = selected.value.findIndex(selection => props.valueComparator(selection, item.value));
if (index === -1) {
model.value = [...model.value, item];
search.value = '';
} else {
const value = [...model.value];
value.splice(index, 1);
model.value = value;
}
} else {
model.value = [item];
isSelecting.value = true;
if (!slots.selection) {
search.value = item.title;
}
menu.value = false;
isPristine.value = true;
nextTick(() => isSelecting.value = false);
}
}
watch(isFocused, val => {
if (val) {
isSelecting.value = true;
search.value = props.multiple || !!slots.selection ? '' : String(selections.value.at(-1)?.props.title ?? '');
isPristine.value = true;
nextTick(() => isSelecting.value = false);
} else {
menu.value = false;
search.value = '';
}
});
watch(search, val => {
if (!isFocused.value || isSelecting.value) return;
if (val) menu.value = true;
isPristine.value = !val;
});
useRender(() => {
const hasChips = !!(props.chips || slots.chip);
const hasList = !!(!props.hideNoData || displayItems.value.length || slots.prepend || slots.append || slots['no-data']);
const [textFieldProps] = filterVTextFieldProps(props);
return _createVNode(VTextField, _mergeProps({
"ref": vTextFieldRef
}, textFieldProps, {
"modelValue": search.value,
"onUpdate:modelValue": v => {
if (v == null) model.value = [];
},
"validationValue": model.externalValue,
"dirty": model.value.length > 0,
"onInput": onInput,
"class": ['v-autocomplete', {
'v-autocomplete--active-menu': menu.value,
'v-autocomplete--chips': !!props.chips,
[`v-autocomplete--${props.multiple ? 'multiple' : 'single'}`]: true,
'v-autocomplete--selection-slot': !!slots.selection
}],
"appendInnerIcon": props.menuIcon,
"readonly": props.readonly,
"onClick:clear": onClear,
"onMousedown:control": onMousedownControl,
"onFocus": () => isFocused.value = true,
"onBlur": () => isFocused.value = false,
"onKeydown": onKeydown
}), {
...slots,
default: () => _createVNode(_Fragment, null, [_createVNode(VMenu, _mergeProps({
"ref": vMenuRef,
"modelValue": menu.value,
"onUpdate:modelValue": $event => menu.value = $event,
"activator": "parent",
"contentClass": "v-autocomplete__content",
"eager": props.eager,
"maxHeight": 310,
"openOnClick": false,
"closeOnContentClick": false,
"transition": props.transition,
"onAfterLeave": onAfterLeave
}, props.menuProps), {
default: () => [hasList && _createVNode(VList, {
"ref": listRef,
"selected": selected.value,
"selectStrategy": props.multiple ? 'independent' : 'single-independent',
"onMousedown": e => e.preventDefault(),
"onFocusin": onFocusin,
"onFocusout": onFocusout
}, {
default: () => [!displayItems.value.length && !props.hideNoData && (slots['no-data']?.() ?? _createVNode(VListItem, {
"title": t(props.noDataText)
}, null)), slots['prepend-item']?.(), displayItems.value.map((item, index) => slots.item?.({
item,
index,
props: mergeProps(item.props, {
onClick: () => select(item)
})
}) ?? _createVNode(VListItem, _mergeProps({
"key": index
}, item.props, {
"onClick": () => select(item)
}), {
prepend: _ref2 => {
let {
isSelected
} = _ref2;
return props.multiple && !props.hideSelected ? _createVNode(VCheckboxBtn, {
"modelValue": isSelected,
"ripple": false
}, null) : undefined;
},
title: () => {
return isPristine.value ? item.title : highlightResult(item.title, getMatches(item)?.title, search.value?.length ?? 0);
}
})), slots['append-item']?.()]
})]
}), selections.value.map((item, index) => {
function onChipClose(e) {
e.stopPropagation();
e.preventDefault();
select(item);
}
const slotProps = {
'onClick:close': onChipClose,
modelValue: true,
'onUpdate:modelValue': undefined
};
return _createVNode("div", {
"key": item.value,
"class": "v-autocomplete__selection"
}, [hasChips ? _createVNode(VDefaultsProvider, {
"defaults": {
VChip: {
closable: props.closableChips,
size: 'small',
text: item.title
}
}
}, {
default: () => [slots.chip ? slots.chip({
item,
index,
props: slotProps
}) : _createVNode(VChip, slotProps, null)]
}) : slots.selection ? slots.selection({
item,
index
}) : _createVNode("span", {
"class": "v-autocomplete__selection-text"
}, [item.title, props.multiple && index < selections.value.length - 1 && _createVNode("span", {
"class": "v-autocomplete__selection-comma"
}, [_createTextVNode(",")])])]);
})])
});
});
return forwardRefs({
isFocused,
isPristine,
menu,
search,
filteredItems,
select
}, vTextFieldRef);
}
});
//# sourceMappingURL=VAutocomplete.mjs.map