2018-04-01 21:43:23 -05:00
|
|
|
<template>
|
|
|
|
<div>
|
|
|
|
<input
|
|
|
|
ref="textInput"
|
|
|
|
type="text"
|
|
|
|
autocomplete="off"
|
|
|
|
|
|
|
|
:id="id"
|
|
|
|
:name="name"
|
|
|
|
:placeholder="placeholder"
|
|
|
|
:value="rawValue"
|
|
|
|
:class="finalInputClass"
|
|
|
|
|
2018-04-01 22:32:13 -05:00
|
|
|
@click="clickHandler"
|
2018-04-01 21:43:23 -05:00
|
|
|
@blur="blurHandler"
|
|
|
|
@input="inputHandler"
|
|
|
|
@keydown="keydownHandler"
|
|
|
|
/>
|
|
|
|
<div v-show="isListOpen" class="list">
|
|
|
|
<ul>
|
|
|
|
<li v-for="(opt, idx) in options" :key="optionKey(opt)" :class="optionClass(idx)" @mousemove="optionMousemove(idx)" @click="optionClick(opt)">
|
|
|
|
<b class="opt_value">{{ optionValue(opt) }}</b>
|
|
|
|
<span v-if="optionLabel(opt) !== null" class="opt_label" v-html="optionLabel(opt)"></span>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
|
|
import debounce from 'lodash/debounce';
|
|
|
|
|
|
|
|
export default {
|
|
|
|
props: {
|
|
|
|
value: String,
|
|
|
|
id: String,
|
|
|
|
placeholder: String,
|
|
|
|
name: String,
|
|
|
|
inputClass: {
|
|
|
|
type: [String, Object, Array],
|
|
|
|
required: false,
|
|
|
|
default: null
|
|
|
|
},
|
|
|
|
minLength: {
|
|
|
|
type: Number,
|
|
|
|
default: 0
|
|
|
|
},
|
|
|
|
debounce: {
|
|
|
|
type: Number,
|
|
|
|
required: false,
|
|
|
|
default: 250
|
|
|
|
},
|
|
|
|
|
|
|
|
valueAttribute: String,
|
|
|
|
labelAttribute: String,
|
2018-09-14 19:32:49 -05:00
|
|
|
keyAttribute: String,
|
2018-04-01 21:43:23 -05:00
|
|
|
|
|
|
|
onGetOptions: Function,
|
|
|
|
searchOptions: Array
|
|
|
|
},
|
|
|
|
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
options: [],
|
|
|
|
rawValue: "",
|
|
|
|
isListOpen: false,
|
|
|
|
activeListIndex: 0
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
created() {
|
|
|
|
this.rawValue = this.value;
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
watch: {
|
|
|
|
value(newValue) {
|
|
|
|
this.rawValue = newValue;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
computed: {
|
|
|
|
finalInputClass() {
|
|
|
|
let cls = ['input'];
|
|
|
|
if (this.inputClass === null) {
|
|
|
|
return cls;
|
|
|
|
} else if (Array.isArray(this.inputClass)) {
|
|
|
|
return cls.concat(this.inputClass);
|
|
|
|
} else {
|
|
|
|
cls.push(this.inputClass);
|
|
|
|
return cls;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
debouncedUpdateOptions() {
|
|
|
|
return debounce(this.updateOptions, this.debounce);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
methods: {
|
|
|
|
optionClass(idx) {
|
|
|
|
return this.activeListIndex === idx ? 'option active' : 'option';
|
|
|
|
},
|
|
|
|
|
|
|
|
optionClick(opt) {
|
|
|
|
this.selectOption(opt);
|
|
|
|
},
|
|
|
|
|
|
|
|
optionKey(opt) {
|
2018-09-14 19:32:49 -05:00
|
|
|
if (this.keyAttribute) {
|
|
|
|
return opt[this.keyAttribute]
|
|
|
|
} else if (this.valueAttribute) {
|
2018-04-01 21:43:23 -05:00
|
|
|
return opt[this.valueAttribute];
|
|
|
|
} else {
|
|
|
|
return opt.toString();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
optionValue(opt) {
|
2018-09-14 19:32:49 -05:00
|
|
|
if (this.valueAttribute) {
|
|
|
|
return opt[this.valueAttribute];
|
|
|
|
} else {
|
|
|
|
return opt.toString();
|
|
|
|
}
|
2018-04-01 21:43:23 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
optionLabel(opt) {
|
|
|
|
if (this.labelAttribute) {
|
|
|
|
return opt[this.labelAttribute];
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
optionMousemove(idx) {
|
|
|
|
this.activeListIndex = idx;
|
|
|
|
},
|
|
|
|
|
2018-04-01 22:32:13 -05:00
|
|
|
clickHandler(evt) {
|
|
|
|
this.$emit("inputClick", evt);
|
|
|
|
},
|
|
|
|
|
2018-04-01 21:43:23 -05:00
|
|
|
blurHandler(evt) {
|
|
|
|
// blur fires before click. If the blur was fired because the user clicked a list item, immediately hiding the list here
|
|
|
|
// would prevent the click event from firing
|
|
|
|
setTimeout(() => {
|
|
|
|
this.isListOpen = false;
|
|
|
|
},250);
|
|
|
|
},
|
|
|
|
|
|
|
|
inputHandler(evt) {
|
|
|
|
const newValue = evt.target.value;
|
|
|
|
|
|
|
|
if (this.rawValue !== newValue) {
|
|
|
|
|
|
|
|
this.rawValue = newValue;
|
|
|
|
|
|
|
|
this.$emit("input", newValue);
|
|
|
|
|
|
|
|
if (newValue.length >= Math.max(1, this.minLength)) {
|
|
|
|
this.debouncedUpdateOptions(newValue);
|
|
|
|
} else {
|
|
|
|
this.isListOpen = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
keydownHandler(evt) {
|
|
|
|
if (this.isListOpen === false)
|
|
|
|
return;
|
|
|
|
|
|
|
|
switch (evt.key) {
|
|
|
|
case "ArrowUp":
|
|
|
|
evt.preventDefault();
|
|
|
|
this.activeListIndex = Math.max(0, this.activeListIndex - 1);
|
|
|
|
break;
|
|
|
|
case "ArrowDown":
|
|
|
|
evt.preventDefault();
|
|
|
|
this.activeListIndex = Math.min(this.options.length - 1, this.activeListIndex + 1);
|
|
|
|
break;
|
|
|
|
case "Enter":
|
|
|
|
evt.preventDefault();
|
|
|
|
this.selectOption(this.options[this.activeListIndex]);
|
|
|
|
break;
|
|
|
|
case "Escape":
|
|
|
|
evt.preventDefault();
|
|
|
|
this.isListOpen = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
selectOption(opt) {
|
|
|
|
this.rawValue = this.optionValue(opt);
|
|
|
|
this.$emit("input", this.rawValue);
|
|
|
|
this.$emit("optionSelected", opt);
|
|
|
|
this.isListOpen = false;
|
|
|
|
},
|
|
|
|
|
|
|
|
updateOptions(value) {
|
|
|
|
let p = null;
|
|
|
|
if (this.searchOptions) {
|
|
|
|
const reg = new RegExp("^" + value, "i");
|
|
|
|
const matcher = o => reg.test(this.optionValue(o));
|
|
|
|
p = Promise.resolve(this.searchOptions.filter(matcher));
|
|
|
|
} else {
|
|
|
|
p = this.onGetOptions(value)
|
|
|
|
}
|
|
|
|
|
|
|
|
p.then(opts => {
|
|
|
|
this.options = opts;
|
|
|
|
this.isListOpen = opts.length > 0;
|
|
|
|
this.activeListIndex = 0;
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
|
|
@import "../styles/variables";
|
|
|
|
|
|
|
|
$labelLineHeight: 0.8rem;
|
|
|
|
|
|
|
|
input.input {
|
|
|
|
&::placeholder {
|
|
|
|
color: $grey-darker;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.list {
|
|
|
|
position: relative;
|
|
|
|
z-index: 150;
|
|
|
|
|
|
|
|
ul {
|
|
|
|
background-color: white;
|
|
|
|
position: absolute;
|
|
|
|
width: 100%;
|
|
|
|
border: 1px solid black;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
li.option {
|
|
|
|
|
|
|
|
padding: 4px;
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
|
|
|
//transition: background-color 0.25s;
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
color: white;
|
|
|
|
background-color: $turquoise;
|
|
|
|
}
|
|
|
|
|
|
|
|
.opt_value {
|
|
|
|
}
|
|
|
|
|
|
|
|
.opt_label {
|
|
|
|
display: block;
|
|
|
|
overflow: hidden;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
font-size: 0.8rem;
|
|
|
|
line-height: $labelLineHeight;
|
|
|
|
max-height: $labelLineHeight * 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
</style>
|