Skip to content

Instantly share code, notes, and snippets.

@tleilax
Last active April 8, 2021 12:46
Show Gist options
  • Save tleilax/6d7f19d4fb3a9064c2e59072d1175026 to your computer and use it in GitHub Desktop.
Save tleilax/6d7f19d4fb3a9064c2e59072d1175026 to your computer and use it in GitHub Desktop.
Stud.IP: AutoComplete component for vue.js
<!--
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>
@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