fix(natural-date-input): preserve input on edit, use reliable update event
Two reliability bugs in the natural-language date input: 1. Clicking Edit on a saved-date pill and changing the value immediately re-showed the saved value. clearAndEdit pre-fills the input with the formatted saved date so the admin doesn't start over, but chrono parses that string on the very first keystroke, re-sets parsedDate, and the auto-hide template flips the pill back. Added an isEditing flag that keeps the input visible across re-parses and clears on blur once we have a valid parse. 2. Typing "tomorrow at 2pm" sometimes committed "tomorrow at <current time>". UInput's template spreads \$attrs onto the inner <input> alongside its own @input="onInput", and Vue's listener-array merge intermittently drops the fall-through @input mid-typing — in the reproduction, the final 'm' never reached parseNaturalInput, so chrono's last successful read was "tomorrow at 2p" matching just "tomorrow". Switched to @update:model-value (a declared emit on UInput, so it goes through the reliable component-emit path) and made onBlur always re-parse the final value as a backup.
This commit is contained in:
parent
9e4030ccfd
commit
96470a604a
1 changed files with 24 additions and 6 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-if="!parsedDate || hasError" class="relative">
|
<div v-if="!parsedDate || hasError || isEditing" class="relative">
|
||||||
<UInput
|
<UInput
|
||||||
v-model="naturalInput"
|
:model-value="naturalInput"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:color="
|
:color="
|
||||||
hasError && naturalInput.trim()
|
hasError && naturalInput.trim()
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
? 'success'
|
? 'success'
|
||||||
: undefined
|
: undefined
|
||||||
"
|
"
|
||||||
@input="parseNaturalInput"
|
@update:model-value="onNaturalInputChange"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
>
|
>
|
||||||
<template #trailing>
|
<template #trailing>
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="parsedDate && isValidParse"
|
v-if="parsedDate && isValidParse && !isEditing"
|
||||||
class="text-sm px-3 py-2"
|
class="text-sm px-3 py-2"
|
||||||
style="color: var(--candle); background: color-mix(in srgb, var(--candle) 15%, transparent); border: 1px solid var(--candle)"
|
style="color: var(--candle); background: color-mix(in srgb, var(--candle) 15%, transparent); border: 1px solid var(--candle)"
|
||||||
>
|
>
|
||||||
|
|
@ -108,6 +108,7 @@ const isValidParse = ref(false);
|
||||||
const hasError = ref(false);
|
const hasError = ref(false);
|
||||||
const errorMessage = ref("");
|
const errorMessage = ref("");
|
||||||
const datetimeValue = ref("");
|
const datetimeValue = ref("");
|
||||||
|
const isEditing = ref(false);
|
||||||
|
|
||||||
// Initialize with current value
|
// Initialize with current value
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
@ -132,6 +133,7 @@ watch(
|
||||||
datetimeValue.value = formatForDatetimeLocal(date);
|
datetimeValue.value = formatForDatetimeLocal(date);
|
||||||
isValidParse.value = true;
|
isValidParse.value = true;
|
||||||
naturalInput.value = ""; // Clear natural input when set externally
|
naturalInput.value = ""; // Clear natural input when set externally
|
||||||
|
isEditing.value = false;
|
||||||
}
|
}
|
||||||
} else if (!newValue) {
|
} else if (!newValue) {
|
||||||
reset();
|
reset();
|
||||||
|
|
@ -175,11 +177,23 @@ const parseNaturalInput = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// UInput's fall-through @input listener fires unreliably (the Nuxt UI Input
|
||||||
|
// template merges $attrs with an explicit @input and can drop keystrokes).
|
||||||
|
// update:model-value is a declared emit and fires for every change.
|
||||||
|
const onNaturalInputChange = (value) => {
|
||||||
|
naturalInput.value = value;
|
||||||
|
parseNaturalInput();
|
||||||
|
};
|
||||||
|
|
||||||
const onBlur = () => {
|
const onBlur = () => {
|
||||||
// If we have a valid parse but the input changed, try to parse again
|
// Always re-parse on blur so the final typed value wins, even if an
|
||||||
if (naturalInput.value.trim() && !isValidParse.value) {
|
// intermediate state produced a stale parse.
|
||||||
|
if (naturalInput.value.trim()) {
|
||||||
parseNaturalInput();
|
parseNaturalInput();
|
||||||
}
|
}
|
||||||
|
if (isValidParse.value) {
|
||||||
|
isEditing.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDatetimeChange = () => {
|
const onDatetimeChange = () => {
|
||||||
|
|
@ -190,6 +204,7 @@ const onDatetimeChange = () => {
|
||||||
isValidParse.value = true;
|
isValidParse.value = true;
|
||||||
hasError.value = false;
|
hasError.value = false;
|
||||||
naturalInput.value = ""; // Clear natural input when using traditional picker
|
naturalInput.value = ""; // Clear natural input when using traditional picker
|
||||||
|
isEditing.value = false;
|
||||||
emit("update:modelValue", datetimeValue.value);
|
emit("update:modelValue", datetimeValue.value);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -227,6 +242,9 @@ const clearAndEdit = () => {
|
||||||
parsedDate.value = null;
|
parsedDate.value = null;
|
||||||
isValidParse.value = false;
|
isValidParse.value = false;
|
||||||
hasError.value = false;
|
hasError.value = false;
|
||||||
|
// Prevent auto-hide while editing: chrono re-parses the pre-filled string
|
||||||
|
// on the first keystroke, which would otherwise hide the input.
|
||||||
|
isEditing.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatForDatetimeLocal = (date) => {
|
const formatForDatetimeLocal = (date) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue