123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- <template>
- <div class="autocomplete-wrapper">
- <input
- ref="acInput"
- v-model="text"
- type="text"
- autocomplete="off"
- :placeholder="placeholder"
- :spellcheck="spellcheck"
- :autocapitalize="autocapitalize"
- :class="styles"
- @input="updateSuggestions"
- @keyup="updateSuggestions"
- @click="updateSuggestions"
- @keydown="handleKeystroke"
- @change="$emit('change', $event)"
- />
- <ul
- v-if="suggestions.length > 0 && suggestionsVisible"
- class="suggestions hide-scrollbar"
- :style="{ transform: `translate(${suggestionsOffsetLeft}px, 0)` }"
- >
- <li
- v-for="(suggestion, index) in suggestions"
- :key="`suggestion-${index}`"
- :class="{ active: currentSuggestionIndex === index }"
- @click.prevent="forceSuggestion(suggestion)"
- >
- {{ suggestion }}
- </li>
- </ul>
- </div>
- </template>
- <script>
- import { defineComponent } from "@nuxtjs/composition-api"
- export default defineComponent({
- props: {
- spellcheck: {
- type: Boolean,
- default: true,
- required: false,
- },
- autocapitalize: {
- type: String,
- default: "off",
- required: false,
- },
- placeholder: {
- type: String,
- default: "",
- required: false,
- },
- source: {
- type: Array,
- required: true,
- },
- value: {
- type: String,
- default: "",
- required: false,
- },
- styles: {
- type: String,
- default: "",
- },
- },
- data() {
- return {
- text: this.value,
- selectionStart: 0,
- suggestionsOffsetLeft: 0,
- currentSuggestionIndex: -1,
- suggestionsVisible: false,
- }
- },
- computed: {
- /**
- * Gets the suggestions list to be displayed under the input box.
- *
- * @returns {default.props.source|{type, required}}
- */
- suggestions() {
- const input = this.text.substring(0, this.selectionStart)
- return (
- this.source
- .filter(
- (entry) =>
- entry.toLowerCase().startsWith(input.toLowerCase()) &&
- input.toLowerCase() !== entry.toLowerCase()
- )
- // Cut off the part that's already been typed.
- .map((entry) => entry.substring(this.selectionStart))
- // We only want the top 10 suggestions.
- .slice(0, 10)
- )
- },
- },
- watch: {
- text() {
- this.$emit("input", this.text)
- },
- value(newValue) {
- this.text = newValue
- },
- },
- mounted() {
- this.updateSuggestions({
- target: this.$refs.acInput,
- })
- },
- methods: {
- updateSuggestions(event) {
- // Hide suggestions if ESC pressed.
- if (event.code && event.code === "Escape") {
- event.preventDefault()
- this.suggestionsVisible = false
- this.currentSuggestionIndex = -1
- return
- }
- // As suggestions is a reactive property, this implicitly
- // causes suggestions to update.
- this.selectionStart = this.$refs.acInput.selectionStart
- this.suggestionsOffsetLeft = 12 * this.selectionStart
- this.suggestionsVisible = true
- },
- forceSuggestion(text) {
- const input = this.text.substring(0, this.selectionStart)
- this.text = input + text
- this.selectionStart = this.text.length
- this.suggestionsVisible = true
- this.currentSuggestionIndex = -1
- },
- handleKeystroke(event) {
- switch (event.code) {
- case "Enter":
- event.preventDefault()
- if (this.currentSuggestionIndex > -1)
- this.forceSuggestion(
- this.suggestions.find(
- (_item, index) => index === this.currentSuggestionIndex
- )
- )
- break
- case "ArrowUp":
- event.preventDefault()
- this.currentSuggestionIndex =
- this.currentSuggestionIndex - 1 >= 0
- ? this.currentSuggestionIndex - 1
- : 0
- break
- case "ArrowDown":
- event.preventDefault()
- this.currentSuggestionIndex =
- this.currentSuggestionIndex < this.suggestions.length - 1
- ? this.currentSuggestionIndex + 1
- : this.suggestions.length - 1
- break
- case "Tab": {
- const activeSuggestion =
- this.suggestions[
- this.currentSuggestionIndex >= 0 ? this.currentSuggestionIndex : 0
- ]
- if (!activeSuggestion) {
- return
- }
- event.preventDefault()
- const input = this.text.substring(0, this.selectionStart)
- this.text = input + activeSuggestion
- break
- }
- }
- },
- },
- })
- </script>
- <style scoped lang="scss">
- .autocomplete-wrapper {
- @apply relative;
- @apply contents;
- input:focus + ul.suggestions,
- ul.suggestions:hover {
- @apply block;
- }
- ul.suggestions {
- @apply hidden;
- @apply bg-popover;
- @apply absolute;
- @apply mx-2;
- @apply left-0;
- @apply z-50;
- @apply shadow-lg;
- @apply max-h-46;
- @apply overflow-y-auto;
- top: calc(100% - 4px);
- border-radius: 0 0 8px 8px;
- li {
- @apply w-full;
- @apply block;
- @apply py-2 px-4;
- @apply text-secondary;
- &:last-child {
- border-radius: 0 0 8px 8px;
- }
- &:hover,
- &.active {
- @apply bg-accentDark;
- @apply text-accentContrast;
- @apply cursor-pointer;
- }
- }
- }
- }
- </style>
|