Last active
April 8, 2021 12:46
-
-
Save tleilax/6d7f19d4fb3a9064c2e59072d1175026 to your computer and use it in GitHub Desktop.
Stud.IP: AutoComplete component for vue.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
Autocomplete component with keyboard selection | |
Usage: | |
<autocomplete :results="results" | |
@select="selectedAnswer"> | |
</autocomplete> | |
Slots are optional but if you want to adjust the result display you should | |
at least provide a default slot: | |
<template v-slot:default="slotProps"> | |
<img :src="slotProps.result.img"> | |
{{ slotProps.result.label }} | |
</template> | |
--> | |
<template> | |
<div class="infoservice-autocomplete" @mouseover="entered = focussed" @mouseleave="entered = false"> | |
<input type="text" | |
class="infoservice-autocomplete__input" | |
v-model.trim="needle" | |
@input="search" | |
minlength="3" | |
:disabled="searching" | |
@focus="focussed = true" | |
@blur="focussed = false" | |
@keydown.up="selectUp" | |
@keydown.down="selectDown" | |
@keydown.enter.prevent="selectByKey" | |
@keydown.escape="reset" | |
ref="input" /> | |
<ul class="infoservice-autocomplete__results" v-if="isVisible"> | |
<li class="infoservice-autocomplete__search__indicator" v-if="searching"> | |
Suchen... | |
</li> | |
<li class="infoservice-autocomplete__empty__results" | |
v-if="!searching && results.length === 0"> | |
<slot name="empty"> | |
Nichts gefunden | |
</slot> | |
</li> | |
<li class="infoservice-autocomplete__result" | |
v-for="(result, index) in results" | |
:key="index" | |
:class="{'infoservice-autocomplete__result--selected': index === selected}" | |
@click="select(result)"> | |
<slot v-bind:result="result"> | |
{{ result }} | |
</slot> | |
</li> | |
</ul> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'autocomplete', | |
props: { | |
results: { | |
type: Array, | |
required: true | |
} | |
}, | |
data () { | |
return { | |
needle: '', | |
searching: false, | |
debounceTimeout: null, | |
focussed: false, | |
entered: false, | |
selected: null | |
}; | |
}, | |
methods: { | |
search () { | |
clearTimeout(this.debounceTimeout); | |
this.debounceTimeout = setTimeout(() => { | |
if (this.needle.length > 2) { | |
this.searching = true; | |
this.$emit('search', this.needle); | |
} | |
}, 500); | |
}, | |
select (value) { | |
this.$emit('select', value); | |
}, | |
selectUp () { | |
if (this.$refs.input.selectionStart !== 0) { | |
return; | |
} | |
if (this.selected > 0) { | |
this.selected -= 1; | |
} else if (this.selected === null) { | |
this.selected = this.results.length - 1; | |
} else { | |
this.selected = null; | |
} | |
}, | |
selectDown () { | |
if (this.$refs.input.selectionStart !== this.$refs.input.value.length) { | |
return; | |
} | |
if (this.selected === null) { | |
this.selected = 0; | |
} else if (this.selected < this.results.length - 1) { | |
this.selected += 1; | |
} else { | |
this.selected = null; | |
} | |
}, | |
selectByKey () { | |
if (this.selected !== null) { | |
this.select(this.results[this.selected]); | |
} | |
return false; | |
}, | |
reset (trigger = true) { | |
this.needle = ''; | |
this.selected = null; | |
if (trigger) { | |
this.$emit('update:results', []); | |
} | |
} | |
}, | |
computed: { | |
isVisible() { | |
return (this.focussed || this.entered) | |
&& (this.searching || this.results.length > 0); | |
} | |
}, | |
watch: { | |
results (current, previous) { | |
this.searching = false; | |
if (current.length === 0) { | |
this.reset(false); | |
} | |
} | |
} | |
} | |
</script> | |
<style lang="less"> | |
@import "../webpack.prefix.less"; | |
.infoservice-autocomplete { | |
position: relative; | |
} | |
.infoservice-autocomplete__input { | |
.background-icon('search', 'clickable'); | |
background-position: right 4px center; | |
background-repeat: no-repeat; | |
max-width: unset !important; | |
width: 100%; | |
} | |
.infoservice-autocomplete__results { | |
list-style: none; | |
margin: 0; | |
padding: 0; | |
position: absolute; | |
background: @white; | |
border: 1px solid @content-color-40; | |
width: calc(100% - 2px); | |
z-index: 2; | |
} | |
.infoservice-autocomplete__search__indicator { | |
background-image: url("@{image-path}/ajax-indicator-black.svg"); | |
background-position: left 0.5em center; | |
background-repeat: no-repeat; | |
background-size: 2em; | |
padding: 1em; | |
padding-left: 3em; | |
~ .infoservice-autocomplete__result { | |
display: none; | |
} | |
} | |
.infoservice-autocomplete__result { | |
padding: 0.5em 1em; | |
&:not(:last-child) { | |
border-bottom: 1px solid @content-color-20; | |
} | |
&:hover { | |
background-color: @activity-color-20; | |
cursor: pointer; | |
} | |
&--selected { | |
background-color: @activity-color-60; | |
} | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@image-path: "../../../../assets/images"; | |
@icon-path: "@{image-path}/icons"; | |
@import (optional, reference) "../../../../../resources/assets/stylesheets/mixins/colors.less"; | |
@import (optional, reference) "../../../../../resources/assets/stylesheets/mixins/twitter-mixins.less"; | |
@import (optional, reference) "../../../../../resources/assets/stylesheets/mixins/studip.less"; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment