platypush/platypush/backend/http/webapp/src/components/elements/Autocomplete.vue

268 lines
5.4 KiB
Vue

<template>
<div class="autocomplete">
<label :text="label">
<input
type="text"
class="input"
ref="input"
:placeholder="placeholder"
:disabled="disabled"
:value="value"
@focus="onFocus"
@input="onInput"
@blur="onBlur"
@keydown="onInputKeyDown"
@keyup="onInputKeyUp"
>
</label>
<div class="items" v-if="showItems">
<div
class="item"
:class="{ active: i === curIndex }"
:key="item"
:data-item="item"
v-for="(item, i) in visibleItems"
@click="onItemSelect(item)">
<span class="matching" v-if="value?.length">{{ item.substr(0, value.length) }}</span>
<span class="normal">{{ item.substr(value?.length || 0) }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Autocomplete",
emits: ["input"],
props: {
items: {
type: Array,
required: true,
},
value: {
type: String,
default: "",
},
disabled: {
type: Boolean,
default: false,
},
autofocus: {
type: Boolean,
default: false,
},
label: {
type: String,
},
placeholder: {
type: String,
},
showResultsWhenBlank: {
type: Boolean,
default: false,
},
},
data() {
return {
visible: false,
curIndex: -1,
}
},
computed: {
visibleItems() {
if (!this.value?.length)
return this.items
const val = this.value.toUpperCase()
if (!val?.length)
return this.showResultsWhenBlank ? this.items : []
return this.items.filter(
(item) => item.substr(0, val.length).toUpperCase() === val
)
},
showItems() {
return this.visible && this.items?.length
},
},
methods: {
selectNextItem() {
this.curIndex++
this.normalizeIndex()
},
selectPrevItem() {
this.curIndex--
this.normalizeIndex()
},
normalizeIndex() {
// Go to the beginning after reaching the end
if (this.curIndex >= this.visibleItems.length)
this.curIndex = 0
// Go to the end after moving back from the start
if (this.curIndex < 0)
this.curIndex = this.visibleItems.length - 1
// Scroll to the element
const el = this.$el.querySelector("[data-item='" + this.visibleItems[this.curIndex] + "']")
if (el)
el.scrollIntoView({
block: "start",
inline: "nearest",
behavior: "smooth",
})
},
valueIsInItems() {
if (!this.value)
return false
return this.items.indexOf(this.value) >= 0
},
onFocus() {
if (this.showResultsWhenBlank || this.value?.length)
this.visible = true
},
onInput(e) {
let val = e.target.value
if (this.valueIsInItems())
this.visible = false
e.stopPropagation()
this.$emit("input", val)
this.curIndex = -1
this.visible = true
},
onBlur(e) {
this.onInput(e)
this.$nextTick(() => {
if (this.valueIsInItems())
this.visible = false
})
},
onItemSelect(item) {
this.$emit("input", item)
this.$nextTick(() => {
if (this.valueIsInItems()) {
this.visible = false
}
})
},
onInputKeyUp(e) {
if (["ArrowUp", "ArrowDown", "Tab", "Enter", "Escape"].indexOf(e.key) >= 0)
e.stopPropagation()
if (e.key === "Enter" && this.valueIsInItems()) {
this.$refs.input.blur()
this.visible = false
}
},
onInputKeyDown(e) {
if (
e.key === 'ArrowDown' ||
(e.key === 'Tab' && !e.shiftKey) ||
(e.key === 'j' && e.ctrlKey)
) {
this.selectNextItem()
e.preventDefault()
} else if (
e.key === 'ArrowUp' ||
(e.key === 'Tab' && e.shiftKey) ||
(e.key === 'k' && e.ctrlKey)
) {
this.selectPrevItem()
e.preventDefault()
} else if (e.key === 'Enter') {
if (this.curIndex > -1 && this.visible) {
e.preventDefault()
this.onItemSelect(this.visibleItems[this.curIndex])
this.$refs.input.focus()
}
} else if (e.key === 'Escape') {
this.visible = false
}
},
onDocumentClick(e) {
if (this.$el.contains(e.target) || e.target.classList.contains("item"))
return
this.visible = false
},
},
mounted() {
document.addEventListener("click", this.onDocumentClick)
if (this.autofocus)
this.$refs.input.focus()
},
}
</script>
<style lang="scss" scoped>
.autocomplete {
width: 100%;
position: relative;
display: inline-block;
.input {
width: 100%;
box-shadow: $search-bar-shadow;
}
.items {
width: 100%;
max-height: 50vh;
position: absolute;
overflow: auto;
border: $default-border-2;
border-bottom: none;
border-top: none;
border-radius: 1em;
box-shadow: $search-bar-shadow;
z-index: 99;
top: 100%;
left: 0;
right: 0;
.item {
padding: 1em;
cursor: pointer;
border-bottom: $default-border-2;
background-color: $background-color;
&:hover {
background-color: $hover-bg-2;
}
.matching {
font-weight: bold;
}
}
}
.active {
background-color: $hover-bg-2 !important;
}
}
</style>