Skip to content

Instantly share code, notes, and snippets.

@ardianferdianto
Created August 15, 2020 10:14
Show Gist options
  • Save ardianferdianto/bbf14dbc06b7c84780a91a5d43533873 to your computer and use it in GitHub Desktop.
Save ardianferdianto/bbf14dbc06b7c84780a91a5d43533873 to your computer and use it in GitHub Desktop.
<template>
<div class="autocomplete">
<b-form-textarea v-if="textarea" :id="computedId" :rows="rows" :cols="cols"
class="autocomplete-input form-control"
:placeholder="placeholder" @focusout="focusout" @focus="focus" @keydown.13="chooseItem"
@keydown.tab="chooseItem" @keydown.40="moveDown" @keydown.38="moveUp"
v-model="inputValue"
v-on:focus="$emit('change', $event.target.focus())"
v-on:blur="$emit('blur', $event.target.blur())"
type="text"
:state="state"
:max-rows="maxRows"
></b-form-textarea>
<b-form-input v-else :id="computedId" class="autocomplete-input form-control" :placeholder="placeholder"
@focusout="focusout" @focus="focus"
@keydown.13="chooseItem" @keydown.tab="chooseItem" @keydown.40="moveDown" @keydown.38="moveUp"
v-model="inputValue" type="text"
:state="state"
/>
<ul :class="{'autocomplete-list': true, [id+'-list']: true }" v-if="showList">
<li
:class="{active: selectedIndex === index}"
v-for="(result, index) in searchMatch"
@click="selectItem(index)"
v-html="highlightWord(result)"
>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "autocomplete",
props: {
id: {},
items: {
default: []
},
placeholder: {
default: ''
},
textarea: {
default: false
},
rows: {
default: 4
},
cols: {},
value: {},
queryEndPoint: {
default: null,
type: String
},
state: {},
maxRows: {},
highlightColor: {
default: '#CA8E2F'
},
highlightTextColor: {
default: 'white'
},
triggerChar: {
default: '@'
}
},
data() {
return {
inputValue: "",
searchMatch: [],
selectedIndex: 0,
clickedChooseItem: false,
wordIndex: 0,
listItems: [],
lookupCache: {}
};
},
mounted: function () {
this.inputValue = this.value;
},
computed: {
listToSearch() {
if (typeof this.listItems !== "undefined" && this.listItems.length > 0) {
return this.listItems;
} else {
return [];
}
},
currentWord() {
let word = (this.inputValue.toLowerCase().replace(/(\r\n|\n|\r)/gm, ' ').split(' ')[this.wordIndex]) + '';
if (this.triggerChar.length > 0 && word.length > 0 && !word.startsWith(this.triggerChar)) {
return '';
}
return word ? word : '';
},
inputSplitted() {
return this.inputValue.toLowerCase().replace(/(\r\n|\n|\r)/gm, ' ').split(" ");
},
showList() {
return (this.searchMatch.length > 0 && this.currentWord.length > 1);
},
computedId: function () {
return this.id ? this.id : 'input-' + parseInt(Math.random() * 1000);
}
},
watch: {
inputValue() {
this.focus();
this.selectedIndex = 0;
this.wordIndex = this.inputSplitted.length - 1;
this.$emit('input', this.inputValue);
},
value() {
this.inputValue = this.value;
}
},
methods: {
highlightWord(word) {
const regex = new RegExp("(" + this.currentWord + ")", "g");
return word.replace(regex, `<span style="background-color: ${this.highlightColor}; color: ${this.highlightTextColor}">$1</span>`);
},
setWord(word) {
let currentWords = this.inputValue.replace(/(\r\n|\n|\r)/gm, '__br__ ').split(' ');
let currentWordsLower = this.inputValue.toLowerCase().replace(/(\r\n|\n|\r)/gm, '__br__ ').split(' ');
currentWords[this.wordIndex] = currentWordsLower[this.wordIndex].replace(this.currentWord, word + ' ');
this.wordIndex += 1;
this.inputValue = currentWords.join(' ').replace(/__br__\s/g, '\n');
},
moveDown() {
if (this.selectedIndex < this.searchMatch.length - 1) {
this.selectedIndex++;
}
},
moveUp() {
if (this.selectedIndex !== -1) {
this.selectedIndex--;
}
},
selectItem(index) {
this.selectedIndex = index;
this.chooseItem();
},
chooseItem(e) {
this.clickedChooseItem = true;
if (this.selectedIndex !== -1 && this.searchMatch.length > 0) {
if (e) {
e.preventDefault();
}
this.setWord(this.searchMatch[this.selectedIndex]);
this.selectedIndex = -1;
}
},
focusout(e) {
setTimeout(() => {
if (!this.clickedChooseItem) {
this.searchMatch = [];
this.selectedIndex = -1;
}
this.clickedChooseItem = false;
}, 100);
},
focus() {
if (this.queryEndPoint && this.currentWord.length > 1) {
const self = this;
const q = this.currentWord.replace('@', '');
if (q in this.lookupCache) {
this.listItems = this.lookupCache[q];
this.searchMatch = this.listToSearch.filter(
el => el.toLowerCase().indexOf(this.currentWord) >= 0
);
this.focusFinally();
} else {
axios.get(this.queryEndPoint, {
params: {
q
}
}).then(function (response) {
self.listItems = response.data;
self.lookupCache[q] = response.data;
self.searchMatch = self.listToSearch.filter(
el => el.toLowerCase().indexOf(self.currentWord) >= 0
);
}).catch(function (e) {
console.error(e.message);
}).finally(function () {
self.focusFinally();
});
}
} else {
this.searchMatch = [];
if (this.currentWord !== "") {
this.searchMatch = this.listToSearch.filter(
el => el.toLowerCase().indexOf(this.currentWord) >= 0
);
}
this.focusFinally();
}
},
focusFinally: function () {
if (
this.searchMatch.length === 1 &&
this.currentWord === this.searchMatch[0]
) {
this.searchMatch = [];
}
}
}
};
</script>
<style scoped>
.autocomplete {
position: relative;
}
.autocomplete-input {
padding: 7px 10px;
width: 100%;
border: 1px solid #ddd;
border-radius: 4px;
outline: none;
}
.autocomplete-list {
position: absolute;
z-index: 2;
overflow: auto;
min-width: 250px;
max-height: 150px;
margin: 0;
margin-top: 5px;
padding: 0;
border: 1px solid #eee;
list-style: none;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.05);
}
.autocomplete-list li {
margin: 0;
padding: 8px 15px;
border-bottom: 1px solid #f5f5f5;
}
.autocomplete-list li:last-child {
border-bottom: 0;
}
.autocomplete-list li:hover, .autocomplete-list li.active {
background-color: #f5f5f5;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment