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