Vue.component('tt-autocomplete', { template: `
`, props: { value: {type: [String, Number]}, label: {type: String, required: false}, apiUrl: String, placeholder: {type: String, default: ''}, items: {type: Array, default: () => []}, sm: {type: Boolean, default: true}, row: {type: Boolean, default: false}, returnText: {type: Boolean, default: false}, caseInsensitive: {type: Boolean, default: false}, }, data() { return { displayValue: '', displayingItems: [], isLoading: false, showSuggestions: false, fetchSuggestionsDebounceTimer: null, disableValueWatcher: false, cursor: -1, }; }, watch: { value: { handler(newValue) { if (this.disableValueWatcher) return; this.updateDisplayValue(newValue); }, immediate: true }, apiUrl: { handler: 'fetchSuggestions', immediate: true }, }, methods: { async updateDisplayValue(id) { if (!id) { this.displayValue = ''; return; } if (this.returnText && isNaN(id)) { this.displayValue = id; return; } if (this.apiUrl) { const response = await axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&searchedID=${id}`); if (response.data[0]) this.displayValue = response.data[0].text; } else { const selectedItem = this.items.find(item => this.caseInsensitive ? String(item.value).toLowerCase() === String(id).toLowerCase() : item.value === id ); if (selectedItem) this.displayValue = selectedItem.text; } }, onInput() { if (!this.apiUrl && this.items.length > 0) { const lowerCaseInput = this.displayValue.toLowerCase(); const matchingItem = this.items.find(item => item.value.toLowerCase() === lowerCaseInput); if (matchingItem) { this.selectSuggestion(matchingItem); return; } } if (this.returnText) this.$emit('input', this.displayValue); this.fetchSuggestions(); }, onFocus() { this.showSuggestions = true; this.fetchSuggestions(); }, onBlur() { setTimeout(() => this.showSuggestions = false, 200); }, fetchSuggestions() { clearTimeout(this.fetchSuggestionsDebounceTimer); if (this.displayValue.length < 3) { this.displayingItems = []; this.isLoading = false; return; } this.fetchSuggestionsDebounceTimer = setTimeout(() => { this.isLoading = true; this.cursor = -1; if (!this.apiUrl) { const searchTerms = this.displayValue.toLowerCase().split(' ').filter(s => s); this.displayingItems = this.items.filter(item => { const itemText = item.text.toLowerCase(); return searchTerms.every(term => itemText.includes(term)); }); this.isLoading = false; return; } axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&q=${encodeURIComponent(this.displayValue)}`) .then(response => { this.displayingItems = response.data?.status === 'error' ? [] : response.data; if (this.displayingItems.length === 1 && this.displayingItems[0].text === this.displayValue) { this.selectSuggestion(this.displayingItems[0]); } }) .finally(() => this.isLoading = false); }, 300); }, selectSuggestion(item) { this.disableValueWatcher = true; this.displayValue = item.text; this.$emit('input', item.value); this.showSuggestions = false; this.$nextTick(() => this.disableValueWatcher = false); }, selectOnEnter() { if (this.cursor >= 0 && this.displayingItems[this.cursor]) this.selectSuggestion(this.displayingItems[this.cursor]); }, navigate(direction) { const itemCount = this.displayingItems.slice(0, 10).length; if (!itemCount) return; this.cursor = (this.cursor + direction + itemCount) % itemCount; }, clear() { this.displayValue = ''; this.$emit('input', ''); this.displayingItems = []; } } });