improved performance of vue modules
This commit is contained in:
@@ -1,211 +1,181 @@
|
||||
//TODO: if autocomplete is focus and the input has not a single character still show "Bitte mindestens 3 Zeichen eingeben"
|
||||
//TODO: fix the fetchSuggestions function to not show a loading spinner when the input gets cleared
|
||||
//TODO: fix so we can use arrow keys to navigate the suggestions
|
||||
// TODO: Implement giving the option without the need of an API || need to use computed property to filter the items
|
||||
// TODO: Fix the weirdness with timeout and selecting the suggestion
|
||||
|
||||
Vue.component('tt-autocomplete', {
|
||||
template: `
|
||||
<div class="form-group" :class="{'row': row}"
|
||||
:data-api-url="apiUrl"
|
||||
<div class="form-group" :class="{'row': row}">
|
||||
<slot name="prepend"></slot>
|
||||
<label v-if="label" :for="label" :class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}">{{ label }}</label>
|
||||
<div class="autocomplete position-relative" :class="{'col-sm-8 p-0': row}">
|
||||
<input
|
||||
:id="label"
|
||||
v-model="displayValue"
|
||||
:placeholder="placeholder"
|
||||
:class="{'form-control-sm': sm}"
|
||||
:style="{'padding-right': $slots.append ? '30px' : '0'}"
|
||||
type="text"
|
||||
class="form-control tt-autocomplete"
|
||||
autocomplete="off"
|
||||
@input="onInput"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@keydown.esc="showSuggestions = false"
|
||||
@keydown.down.prevent="navigate(1)"
|
||||
@keydown.up.prevent="navigate(-1)"
|
||||
@keydown.enter.prevent="selectOnEnter"
|
||||
/>
|
||||
<slot name="append"></slot>
|
||||
<button v-show="displayValue.length > 0" @click="clear" tabindex="-1" type="button" class="btn btn-link position-absolute" style="right: -5px; top: 50%; transform: translateY(-50%);">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
||||
<ul v-if="showSuggestions" class="dropdown-menu show dropdown-shadow">
|
||||
<li v-if="isLoading" class="dropdown-item">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"><span class="sr-only">Loading...</span></div>
|
||||
Einträge werden geladen...
|
||||
</li>
|
||||
<template v-if="!isLoading">
|
||||
<li v-if="displayValue.length < 3" class="dropdown-item disabled">
|
||||
Bitte mindestens 3 Zeichen eingeben
|
||||
</li>
|
||||
<li v-if="displayValue.length >= 3 && !displayingItems.length" class="dropdown-item disabled">
|
||||
Keine Suchergebnisse vorhanden.
|
||||
</li>
|
||||
<li
|
||||
v-for="(item, index) in displayingItems.slice(0, 10)"
|
||||
:key="item.value"
|
||||
:class="{'active': cursor === index}"
|
||||
class="dropdown-item"
|
||||
style="cursor: pointer;"
|
||||
@mousedown.prevent="selectSuggestion(item)"
|
||||
>
|
||||
<slot name="prepend"></slot>
|
||||
<label :class="{'col-form-label': row, 'col-sm-4': row, 'col-form-label-sm': sm && row}"
|
||||
v-if="label" :for="label">{{ label }}</label>
|
||||
<div class="autocomplete position-relative" :class="{'col-sm-8 p-0': row}">
|
||||
<input
|
||||
type="text"
|
||||
:id="label"
|
||||
class="form-control tt-autocomplete"
|
||||
:class="{'form-control-sm': sm}"
|
||||
v-model="displayValue"
|
||||
:placeholder="placeholder"
|
||||
@input="onInput"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
autocomplete="off"
|
||||
:style="{'padding-right': $slots.append ? '30px' : '0'}"
|
||||
/>
|
||||
<slot name="append"></slot>
|
||||
<button v-show="displayValue.length > 0" @click="displayValue = ''; $emit('input', '');" tabindex="-1" type="button"
|
||||
class="btn btn-link position-absolute"
|
||||
style="right: -5px; top: 50%; transform: translateY(-50%);">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
||||
<ul v-show="showSuggestions && displayValue.length > 0 || isLoading"
|
||||
class="dropdown-menu show dropdown-shadow">
|
||||
|
||||
<li class="dropdown-item" v-show="isLoading">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
Einträge werden geladen...
|
||||
</li>
|
||||
|
||||
<template v-show="showSuggestions && displayingItems.length && isLoading !== true">
|
||||
<li
|
||||
v-for="(item) in displayingItems.slice(0, 10)"
|
||||
:key="item.value"
|
||||
:class="{'active': value === item.value}"
|
||||
class="dropdown-item"
|
||||
@click.prevent="selectSuggestion(item)"
|
||||
@mousedown.prevent="selectSuggestion(item)"
|
||||
style="cursor: pointer;"
|
||||
>
|
||||
{{ item.text }}
|
||||
</li>
|
||||
<!-- display more search results available define it more precisely in German -->
|
||||
<li v-show="displayingItems.length > 10" class="dropdown-item disabled">
|
||||
Mehr Suchergebnisse vorhanden. Bitte genauer eingeben
|
||||
</li>
|
||||
</template>
|
||||
<li v-show="displayingItems.length === 0 && isLoading === false && displayValue.length >= 3"
|
||||
class="dropdown-item disabled">
|
||||
Keine Suchergebnisse vorhanden.
|
||||
</li>
|
||||
<li v-show="displayValue.length < 3" class="dropdown-item disabled">
|
||||
Bitte mindestens 3 Zeichen eingeben
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
{{ item.text }}
|
||||
</li>
|
||||
<li v-if="displayingItems.length > 10" class="dropdown-item disabled">
|
||||
Mehr Suchergebnisse vorhanden. Bitte genauer eingeben
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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},
|
||||
emitDisplayValue: {type: Boolean, default: false},
|
||||
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() {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
window,
|
||||
displayingItems: [],
|
||||
oldDisplayValue: '',
|
||||
displayValue: '',
|
||||
isLoading: false,
|
||||
showSuggestions: false,
|
||||
cursor: -1,
|
||||
displayValue: '',
|
||||
displayingItems: [],
|
||||
isLoading: false,
|
||||
showSuggestions: false,
|
||||
fetchSuggestionsDebounceTimer: null,
|
||||
disableIDFetch: false
|
||||
disableValueWatcher: false,
|
||||
cursor: -1,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.updateDisplayValue().then();
|
||||
watch: {
|
||||
value: {
|
||||
handler(newValue) {
|
||||
if (this.disableValueWatcher) return;
|
||||
this.updateDisplayValue(newValue);
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
apiUrl: {
|
||||
handler: 'fetchSuggestions',
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setOldDisplayValue(newValue, oldValue) {
|
||||
if (this.emitDisplayValue && newValue && typeof this.value !== 'number') {
|
||||
this.$emit('displayValue', newValue);
|
||||
}
|
||||
this.oldDisplayValue = oldValue;
|
||||
},
|
||||
async updateDisplayValue(newValue, oldValue) {
|
||||
if (this.disableIDFetch) {
|
||||
this.disableIDFetch = false;
|
||||
async updateDisplayValue(id) {
|
||||
if (!id) {
|
||||
this.displayValue = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue) {
|
||||
this.$emit('input', newValue);
|
||||
this.value = newValue;
|
||||
}
|
||||
if (oldValue && !newValue) {
|
||||
this.$emit('input', '');
|
||||
this.displayValue = '';
|
||||
if (this.returnText && isNaN(id)) {
|
||||
this.displayValue = id;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (this.value && this.apiUrl) {
|
||||
if (this.returnText && isNaN(this.value)) {
|
||||
this.displayValue = this.value;
|
||||
return;
|
||||
}
|
||||
const response = await axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&searchedID=${this.value}`);
|
||||
this.displayValue = response.data[0].text;
|
||||
} else if (this.value) {
|
||||
const selectedItem = this.caseInsensitive ?
|
||||
this.items.find(item => item.value.toLowerCase() === this.value.toLowerCase()) : this.items.find(item => item.value === this.value)
|
||||
;
|
||||
this.displayValue = selectedItem ? selectedItem.text : this.displayValue;
|
||||
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 {
|
||||
if (this.returnText === false && !(typeof this.value === 'undefined' || this.value === '')) this.$emit('input', '');
|
||||
this.displayValue = this.displayValue.replace(this.oldDisplayValue, '');
|
||||
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(event) {
|
||||
this.displayValue = event.target.value;
|
||||
onInput() {
|
||||
if (this.returnText) this.$emit('input', this.displayValue);
|
||||
this.fetchSuggestions();
|
||||
}, onFocus() {
|
||||
},
|
||||
onFocus() {
|
||||
this.showSuggestions = true;
|
||||
}, onBlur() {
|
||||
setTimeout(() => {
|
||||
this.showSuggestions = false;
|
||||
}, 200);
|
||||
}, fetchSuggestions() {
|
||||
this.$set(this, 'displayingItems', []);
|
||||
if (this.displayValue.length < 3) return;
|
||||
|
||||
if (!this.apiUrl) {
|
||||
|
||||
const isNegated = this.displayValue.startsWith('!');
|
||||
const substrings = (isNegated ? this.displayValue.slice(1) : this.displayValue).split(' ').map(s => s.toLowerCase());
|
||||
this.displayingItems = this.items.filter(item => {
|
||||
let substringMatch = true;
|
||||
for (var k = 0, klen = substrings.length; k < klen; ++k) {
|
||||
if (!item.text.toLowerCase().includes(substrings[k])) {
|
||||
substringMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (isNegated && !substringMatch) || (!isNegated && substringMatch);
|
||||
})
|
||||
|
||||
return
|
||||
this.fetchSuggestions();
|
||||
},
|
||||
onBlur() {
|
||||
setTimeout(() => this.showSuggestions = false, 200);
|
||||
},
|
||||
fetchSuggestions() {
|
||||
clearTimeout(this.fetchSuggestionsDebounceTimer);
|
||||
if (this.displayValue.length < 3) {
|
||||
this.displayingItems = [];
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
clearTimeout(this.fetchSuggestionsDebounceTimer);
|
||||
|
||||
this.fetchSuggestionsDebounceTimer = setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
if (this.displayValue.length < 3) {
|
||||
this.displayingItems = [];
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.cursor = -1;
|
||||
|
||||
const response = await axios.get(`${this.apiUrl}${this.apiUrl.includes('?') ? '&' : '?'}autocomplete=1&q=${encodeURIComponent(this.displayValue)}`);
|
||||
if (response.data?.status === 'error') {
|
||||
this.displayingItems = [];
|
||||
} else {
|
||||
this.displayingItems = response.data
|
||||
if (response.data.length === 1 && response.data[0].text === this.displayValue) this.selectSuggestion(response.data[0]);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
this.fetchSuggestionsDebounceTimer = null;
|
||||
}, 100);
|
||||
}, 300); // Adjust the 300ms debounce time as needed
|
||||
}, selectSuggestion(item) {
|
||||
this.disableIDFetch = true;
|
||||
this.$emit('input', item.value);
|
||||
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;
|
||||
}, clear() {
|
||||
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 = [];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
displayValue: {handler: 'setOldDisplayValue', immediate: true},
|
||||
value: {handler: 'updateDisplayValue', immediate: true},
|
||||
apiUrl: {handler: 'fetchSuggestions', immediate: true},
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user