fix: don't cut off the part that's already been typed (#3054)
This commit is contained in:
@@ -1,13 +1,27 @@
|
||||
<template>
|
||||
<div class="autocomplete-wrapper">
|
||||
<input ref="acInput" v-model="text" type="text" autocomplete="off" :placeholder="placeholder" :spellcheck="spellcheck"
|
||||
:autocapitalize="autocapitalize" :class="styles" @input.stop="onInput" @keyup="updateSuggestions"
|
||||
@click="updateSuggestions" @keydown="handleKeystroke" @change="emit('change', $event)" />
|
||||
|
||||
<ul v-if="suggestions.length > 0 && suggestionsVisible" class="suggestions"
|
||||
:style="{ transform: `translate(${suggestionsOffsetLeft}px, 0)` }">
|
||||
<li v-for="(suggestion, index) in suggestions" :key="`suggestion-${index}`"
|
||||
:class="{ active: currentSuggestionIndex === index }" @click.prevent="forceSuggestion(suggestion)">
|
||||
<input
|
||||
ref="acInput"
|
||||
v-model="text"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="placeholder"
|
||||
:spellcheck="spellcheck"
|
||||
:autocapitalize="autocapitalize"
|
||||
:class="styles"
|
||||
@input.stop="onInput"
|
||||
@keyup="updateSuggestions"
|
||||
@click="updateSuggestions"
|
||||
@keydown="handleKeystroke"
|
||||
@change="emit('change', $event)"
|
||||
/>
|
||||
<ul v-if="suggestions.length > 0 && suggestionsVisible" class="suggestions">
|
||||
<li
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:key="`suggestion-${index}`"
|
||||
:class="{ active: currentSuggestionIndex === index }"
|
||||
@click.prevent="forceSuggestion(suggestion)"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</li>
|
||||
</ul>
|
||||
@@ -62,11 +76,9 @@ const emit = defineEmits<{
|
||||
|
||||
const text = ref(props.value)
|
||||
const selectionStart = ref(0)
|
||||
const suggestionsOffsetLeft = ref(0)
|
||||
const currentSuggestionIndex = ref(-1)
|
||||
const suggestionsVisible = ref(false)
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
updateSuggestions({
|
||||
target: acInput,
|
||||
@@ -82,14 +94,11 @@ const suggestions = computed(() => {
|
||||
entry.toLowerCase().startsWith(input.toLowerCase()) &&
|
||||
input.toLowerCase() !== entry.toLowerCase()
|
||||
)
|
||||
// Cut off the part that's already been typed.
|
||||
.map((entry) => entry.substring(selectionStart.value))
|
||||
// We only want the top 10 suggestions.
|
||||
.slice(0, 10)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
function updateSuggestions(event: any) {
|
||||
// Hide suggestions if ESC pressed.
|
||||
if (event.code && event.code === "Escape") {
|
||||
@@ -102,18 +111,16 @@ function updateSuggestions(event: any) {
|
||||
// As suggestions is a reactive property, this implicitly
|
||||
// causes suggestions to update.
|
||||
selectionStart.value = acInput.value?.selectionStart ?? -1
|
||||
suggestionsOffsetLeft.value = 12 * selectionStart.value
|
||||
suggestionsVisible.value = true
|
||||
}
|
||||
|
||||
const onInput = (e: Event) => {
|
||||
emit('input', (e.target as HTMLInputElement).value)
|
||||
emit("input", (e.target as HTMLInputElement).value)
|
||||
updateSuggestions(e)
|
||||
}
|
||||
|
||||
function forceSuggestion(str: string) {
|
||||
const input = text.value.substring(0, selectionStart.value)
|
||||
text.value = input + str
|
||||
function forceSuggestion(suggestion: string) {
|
||||
text.value = suggestion
|
||||
|
||||
selectionStart.value = text.value.length
|
||||
suggestionsVisible.value = true
|
||||
@@ -124,18 +131,9 @@ function forceSuggestion(str: string) {
|
||||
|
||||
function handleKeystroke(event: any) {
|
||||
switch (event.code) {
|
||||
case "Enter":
|
||||
event.preventDefault()
|
||||
if (currentSuggestionIndex.value > -1)
|
||||
forceSuggestion(
|
||||
suggestions.value.find(
|
||||
(_item, index) => index === currentSuggestionIndex.value
|
||||
)!
|
||||
)
|
||||
break
|
||||
|
||||
case "ArrowUp":
|
||||
event.preventDefault()
|
||||
|
||||
currentSuggestionIndex.value =
|
||||
currentSuggestionIndex.value - 1 >= 0
|
||||
? currentSuggestionIndex.value - 1
|
||||
@@ -144,53 +142,63 @@ function handleKeystroke(event: any) {
|
||||
|
||||
case "ArrowDown":
|
||||
event.preventDefault()
|
||||
|
||||
currentSuggestionIndex.value =
|
||||
currentSuggestionIndex.value < suggestions.value.length - 1
|
||||
? currentSuggestionIndex.value + 1
|
||||
: suggestions.value.length - 1
|
||||
break
|
||||
|
||||
case "Enter":
|
||||
event.preventDefault()
|
||||
|
||||
if (currentSuggestionIndex.value > -1)
|
||||
forceSuggestion(
|
||||
suggestions.value.find(
|
||||
(_item, index) => index === currentSuggestionIndex.value
|
||||
)!
|
||||
)
|
||||
break
|
||||
|
||||
case "Tab": {
|
||||
event.preventDefault()
|
||||
|
||||
const activeSuggestion =
|
||||
suggestions.value[
|
||||
currentSuggestionIndex.value >= 0 ? currentSuggestionIndex.value : 0
|
||||
currentSuggestionIndex.value >= 0 ? currentSuggestionIndex.value : 0
|
||||
]
|
||||
|
||||
if (!activeSuggestion) {
|
||||
return
|
||||
}
|
||||
if (!activeSuggestion) return
|
||||
|
||||
event.preventDefault()
|
||||
const input = text.value.substring(0, selectionStart.value)
|
||||
text.value = input + activeSuggestion
|
||||
forceSuggestion(activeSuggestion)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.autocomplete-wrapper {
|
||||
@apply relative;
|
||||
@apply contents;
|
||||
|
||||
input:focus+ul.suggestions,
|
||||
input:focus + ul.suggestions,
|
||||
ul.suggestions:hover {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
ul.suggestions {
|
||||
@apply absolute;
|
||||
@apply hidden;
|
||||
@apply bg-popover;
|
||||
@apply absolute;
|
||||
@apply mx-2;
|
||||
@apply left-0;
|
||||
@apply -left-px;
|
||||
@apply z-50;
|
||||
@apply shadow-lg;
|
||||
@apply max-h-46;
|
||||
@apply overflow-y-auto;
|
||||
top: calc(100% - 4px);
|
||||
@apply border-b border-x border-divider;
|
||||
|
||||
top: calc(100% + 1px);
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
li {
|
||||
@@ -198,15 +206,16 @@ function handleKeystroke(event: any) {
|
||||
@apply block;
|
||||
@apply py-2 px-4;
|
||||
@apply text-secondary;
|
||||
@apply font-semibold;
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
border-radius: 0 0 0 8px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
@apply bg-accentDark;
|
||||
@apply text-accentContrast;
|
||||
@apply bg-primaryDark;
|
||||
@apply text-secondaryDark;
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user