333 lines
8.9 KiB
JavaScript
333 lines
8.9 KiB
JavaScript
|
// Styles
|
||
|
import "../../../src/components/VAutocomplete/VAutocomplete.sass"; // Extensions
|
||
|
|
||
|
import VSelect, { defaultMenuProps as VSelectMenuProps } from '../VSelect/VSelect';
|
||
|
import VTextField from '../VTextField/VTextField'; // Utilities
|
||
|
|
||
|
import { keyCodes } from '../../util/helpers';
|
||
|
const defaultMenuProps = { ...VSelectMenuProps,
|
||
|
offsetY: true,
|
||
|
offsetOverflow: true,
|
||
|
transition: false
|
||
|
};
|
||
|
/* @vue/component */
|
||
|
|
||
|
export default VSelect.extend({
|
||
|
name: 'v-autocomplete',
|
||
|
props: {
|
||
|
allowOverflow: {
|
||
|
type: Boolean,
|
||
|
default: true
|
||
|
},
|
||
|
autoSelectFirst: {
|
||
|
type: Boolean,
|
||
|
default: false
|
||
|
},
|
||
|
filter: {
|
||
|
type: Function,
|
||
|
default: (item, queryText, itemText) => {
|
||
|
return itemText.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1;
|
||
|
}
|
||
|
},
|
||
|
hideNoData: Boolean,
|
||
|
menuProps: {
|
||
|
type: VSelect.options.props.menuProps.type,
|
||
|
default: () => defaultMenuProps
|
||
|
},
|
||
|
noFilter: Boolean,
|
||
|
searchInput: {
|
||
|
type: String,
|
||
|
default: undefined
|
||
|
}
|
||
|
},
|
||
|
|
||
|
data() {
|
||
|
return {
|
||
|
lazySearch: this.searchInput
|
||
|
};
|
||
|
},
|
||
|
|
||
|
computed: {
|
||
|
classes() {
|
||
|
return { ...VSelect.options.computed.classes.call(this),
|
||
|
'v-autocomplete': true,
|
||
|
'v-autocomplete--is-selecting-index': this.selectedIndex > -1
|
||
|
};
|
||
|
},
|
||
|
|
||
|
computedItems() {
|
||
|
return this.filteredItems;
|
||
|
},
|
||
|
|
||
|
selectedValues() {
|
||
|
return this.selectedItems.map(item => this.getValue(item));
|
||
|
},
|
||
|
|
||
|
hasDisplayedItems() {
|
||
|
return this.hideSelected ? this.filteredItems.some(item => !this.hasItem(item)) : this.filteredItems.length > 0;
|
||
|
},
|
||
|
|
||
|
currentRange() {
|
||
|
if (this.selectedItem == null) return 0;
|
||
|
return String(this.getText(this.selectedItem)).length;
|
||
|
},
|
||
|
|
||
|
filteredItems() {
|
||
|
if (!this.isSearching || this.noFilter || this.internalSearch == null) return this.allItems;
|
||
|
return this.allItems.filter(item => this.filter(item, String(this.internalSearch), String(this.getText(item))));
|
||
|
},
|
||
|
|
||
|
internalSearch: {
|
||
|
get() {
|
||
|
return this.lazySearch;
|
||
|
},
|
||
|
|
||
|
set(val) {
|
||
|
this.lazySearch = val;
|
||
|
this.$emit('update:search-input', val);
|
||
|
}
|
||
|
|
||
|
},
|
||
|
|
||
|
isAnyValueAllowed() {
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
isDirty() {
|
||
|
return this.searchIsDirty || this.selectedItems.length > 0;
|
||
|
},
|
||
|
|
||
|
isSearching() {
|
||
|
return this.multiple && this.searchIsDirty || this.searchIsDirty && this.internalSearch !== this.getText(this.selectedItem);
|
||
|
},
|
||
|
|
||
|
menuCanShow() {
|
||
|
if (!this.isFocused) return false;
|
||
|
return this.hasDisplayedItems || !this.hideNoData;
|
||
|
},
|
||
|
|
||
|
$_menuProps() {
|
||
|
const props = VSelect.options.computed.$_menuProps.call(this);
|
||
|
props.contentClass = `v-autocomplete__content ${props.contentClass || ''}`.trim();
|
||
|
return { ...defaultMenuProps,
|
||
|
...props
|
||
|
};
|
||
|
},
|
||
|
|
||
|
searchIsDirty() {
|
||
|
return this.internalSearch != null && this.internalSearch !== '';
|
||
|
},
|
||
|
|
||
|
selectedItem() {
|
||
|
if (this.multiple) return null;
|
||
|
return this.selectedItems.find(i => {
|
||
|
return this.valueComparator(this.getValue(i), this.getValue(this.internalValue));
|
||
|
});
|
||
|
},
|
||
|
|
||
|
listData() {
|
||
|
const data = VSelect.options.computed.listData.call(this);
|
||
|
data.props = { ...data.props,
|
||
|
items: this.virtualizedItems,
|
||
|
noFilter: this.noFilter || !this.isSearching || !this.filteredItems.length,
|
||
|
searchInput: this.internalSearch
|
||
|
};
|
||
|
return data;
|
||
|
}
|
||
|
|
||
|
},
|
||
|
watch: {
|
||
|
filteredItems: 'onFilteredItemsChanged',
|
||
|
internalValue: 'setSearch',
|
||
|
|
||
|
isFocused(val) {
|
||
|
if (val) {
|
||
|
this.$refs.input && this.$refs.input.select();
|
||
|
} else {
|
||
|
this.updateSelf();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
isMenuActive(val) {
|
||
|
if (val || !this.hasSlot) return;
|
||
|
this.lazySearch = undefined;
|
||
|
},
|
||
|
|
||
|
items(val, oldVal) {
|
||
|
// If we are focused, the menu
|
||
|
// is not active, hide no data is enabled,
|
||
|
// and items change
|
||
|
// User is probably async loading
|
||
|
// items, try to activate the menu
|
||
|
if (!(oldVal && oldVal.length) && this.hideNoData && this.isFocused && !this.isMenuActive && val.length) this.activateMenu();
|
||
|
},
|
||
|
|
||
|
searchInput(val) {
|
||
|
this.lazySearch = val;
|
||
|
},
|
||
|
|
||
|
internalSearch: 'onInternalSearchChanged',
|
||
|
itemText: 'updateSelf'
|
||
|
},
|
||
|
|
||
|
created() {
|
||
|
this.setSearch();
|
||
|
},
|
||
|
|
||
|
methods: {
|
||
|
onFilteredItemsChanged(val, oldVal) {
|
||
|
// TODO: How is the watcher triggered
|
||
|
// for duplicate items? no idea
|
||
|
if (val === oldVal) return;
|
||
|
this.setMenuIndex(-1);
|
||
|
this.$nextTick(() => {
|
||
|
if (!this.internalSearch || val.length !== 1 && !this.autoSelectFirst) return;
|
||
|
this.$refs.menu.getTiles();
|
||
|
this.setMenuIndex(0);
|
||
|
});
|
||
|
},
|
||
|
|
||
|
onInternalSearchChanged() {
|
||
|
this.updateMenuDimensions();
|
||
|
},
|
||
|
|
||
|
updateMenuDimensions() {
|
||
|
// Type from menuable is not making it through
|
||
|
this.isMenuActive && this.$refs.menu && this.$refs.menu.updateDimensions();
|
||
|
},
|
||
|
|
||
|
changeSelectedIndex(keyCode) {
|
||
|
// Do not allow changing of selectedIndex
|
||
|
// when search is dirty
|
||
|
if (this.searchIsDirty) return;
|
||
|
if (![keyCodes.backspace, keyCodes.left, keyCodes.right, keyCodes.delete].includes(keyCode)) return;
|
||
|
const index = this.selectedItems.length - 1;
|
||
|
|
||
|
if (keyCode === keyCodes.left) {
|
||
|
if (this.selectedIndex === -1) {
|
||
|
this.selectedIndex = index;
|
||
|
} else {
|
||
|
this.selectedIndex--;
|
||
|
}
|
||
|
} else if (keyCode === keyCodes.right) {
|
||
|
if (this.selectedIndex >= index) {
|
||
|
this.selectedIndex = -1;
|
||
|
} else {
|
||
|
this.selectedIndex++;
|
||
|
}
|
||
|
} else if (this.selectedIndex === -1) {
|
||
|
this.selectedIndex = index;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const currentItem = this.selectedItems[this.selectedIndex];
|
||
|
|
||
|
if ([keyCodes.backspace, keyCodes.delete].includes(keyCode) && !this.getDisabled(currentItem)) {
|
||
|
const newIndex = this.selectedIndex === index ? this.selectedIndex - 1 : this.selectedItems[this.selectedIndex + 1] ? this.selectedIndex : -1;
|
||
|
|
||
|
if (newIndex === -1) {
|
||
|
this.setValue(this.multiple ? [] : undefined);
|
||
|
} else {
|
||
|
this.selectItem(currentItem);
|
||
|
}
|
||
|
|
||
|
this.selectedIndex = newIndex;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
clearableCallback() {
|
||
|
this.internalSearch = undefined;
|
||
|
VSelect.options.methods.clearableCallback.call(this);
|
||
|
},
|
||
|
|
||
|
genInput() {
|
||
|
const input = VTextField.options.methods.genInput.call(this);
|
||
|
input.data = input.data || {};
|
||
|
input.data.attrs = input.data.attrs || {};
|
||
|
input.data.domProps = input.data.domProps || {};
|
||
|
input.data.domProps.value = this.internalSearch;
|
||
|
return input;
|
||
|
},
|
||
|
|
||
|
genInputSlot() {
|
||
|
const slot = VSelect.options.methods.genInputSlot.call(this);
|
||
|
slot.data.attrs.role = 'combobox';
|
||
|
return slot;
|
||
|
},
|
||
|
|
||
|
genSelections() {
|
||
|
return this.hasSlot || this.multiple ? VSelect.options.methods.genSelections.call(this) : [];
|
||
|
},
|
||
|
|
||
|
onClick() {
|
||
|
if (this.isDisabled) return;
|
||
|
this.selectedIndex > -1 ? this.selectedIndex = -1 : this.onFocus();
|
||
|
this.activateMenu();
|
||
|
},
|
||
|
|
||
|
onInput(e) {
|
||
|
if (this.selectedIndex > -1 || !e.target) return;
|
||
|
const target = e.target;
|
||
|
const value = target.value; // If typing and menu is not currently active
|
||
|
|
||
|
if (target.value) this.activateMenu();
|
||
|
this.internalSearch = value;
|
||
|
this.badInput = target.validity && target.validity.badInput;
|
||
|
},
|
||
|
|
||
|
onKeyDown(e) {
|
||
|
const keyCode = e.keyCode;
|
||
|
VSelect.options.methods.onKeyDown.call(this, e); // The ordering is important here
|
||
|
// allows new value to be updated
|
||
|
// and then moves the index to the
|
||
|
// proper location
|
||
|
|
||
|
this.changeSelectedIndex(keyCode);
|
||
|
},
|
||
|
|
||
|
onSpaceDown(e) {},
|
||
|
|
||
|
onTabDown(e) {
|
||
|
VSelect.options.methods.onTabDown.call(this, e);
|
||
|
this.updateSelf();
|
||
|
},
|
||
|
|
||
|
onUpDown() {
|
||
|
// For autocomplete / combobox, cycling
|
||
|
// interfers with native up/down behavior
|
||
|
// instead activate the menu
|
||
|
this.activateMenu();
|
||
|
},
|
||
|
|
||
|
setSelectedItems() {
|
||
|
VSelect.options.methods.setSelectedItems.call(this); // #4273 Don't replace if searching
|
||
|
// #4403 Don't replace if focused
|
||
|
|
||
|
if (!this.isFocused) this.setSearch();
|
||
|
},
|
||
|
|
||
|
setSearch() {
|
||
|
// Wait for nextTick so selectedItem
|
||
|
// has had time to update
|
||
|
this.$nextTick(() => {
|
||
|
if (!this.multiple || !this.internalSearch || !this.isMenuActive) {
|
||
|
this.internalSearch = !this.selectedItems.length || this.multiple || this.hasSlot ? null : this.getText(this.selectedItem);
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
|
||
|
updateSelf() {
|
||
|
if (!this.searchIsDirty && !this.internalValue) return;
|
||
|
|
||
|
if (!this.valueComparator(this.internalSearch, this.getValue(this.internalValue))) {
|
||
|
this.setSearch();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
hasItem(item) {
|
||
|
return this.selectedValues.indexOf(this.getValue(item)) > -1;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
});
|
||
|
//# sourceMappingURL=VAutocomplete.js.map
|