created date input

This commit is contained in:
2025-08-22 01:20:32 -04:00
parent f399129846
commit 289199478e
2 changed files with 114 additions and 3 deletions

View File

@@ -15,9 +15,15 @@ import { toTypedSchema } from '@vee-validate/zod';
import { Form } from 'vee-validate';
import { onMounted, ref } from 'vue';
import * as z from 'zod';
import Popover from '../ui/popover/Popover.vue';
import PopoverTrigger from '../ui/popover/PopoverTrigger.vue';
import { CalculatorIcon } from 'lucide-vue-next';
import PopoverContent from '../ui/popover/PopoverContent.vue';
import Calendar from '../ui/calendar/Calendar.vue';
import DateInput from '../form/DateInput.vue';
const formSchema = toTypedSchema(z.object({
age: z.coerce.number({ invalid_type_error: "Must be a number" }).min(0, "Cannot be less than 0"),
dob: z.string().refine(v => v, { message: "A date of birth is required." }),
name: z.string(),
playtime: z.coerce.number({ invalid_type_error: "Must be a number", }).min(0, "Cannot be less than 0"),
hobbies: z.string(),
@@ -62,6 +68,8 @@ onMounted(() => {
initialValues.value = { ...fallbackInitials }
}
})
</script>
<template>
@@ -70,9 +78,10 @@ onMounted(() => {
<!-- Age -->
<FormField name="age" v-slot="{ value, handleChange }">
<FormItem>
<FormLabel>What is your age?</FormLabel>
<FormLabel>What is your date of birth?</FormLabel>
<FormControl>
<Input :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
<DateInput :model-value="(value as string) ?? ''" :disabled="readOnly"
@update:model-value="handleChange" />
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -0,0 +1,102 @@
<!-- SegmentedDob.vue -->
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
const props = defineProps<{
modelValue: string | null | undefined, // expected "MM/DD/YYYY" or ""
disabled?: boolean
}>();
const emit = defineEmits<{
(e: 'update:modelValue', v: string): void
}>();
const mm = ref(''); const dd = ref(''); const yyyy = ref('');
const mmRef = ref<HTMLInputElement>();
const ddRef = ref<HTMLInputElement>();
const yyyyRef = ref<HTMLInputElement>();
function digitsOnly(s: string) { return s.replace(/\D/g, ''); }
function syncFromModel(v?: string | null) {
const s = v || '';
const m = s.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (m) { mm.value = m[1]; dd.value = m[2]; yyyy.value = m[3]; }
else { mm.value = ''; dd.value = ''; yyyy.value = ''; }
}
function emitCombined() {
const hasAll = mm.value.length === 2 && dd.value.length === 2 && yyyy.value.length === 4;
const partial = [mm.value, dd.value, yyyy.value]
.filter((seg, idx) => seg.length > 0 || idx === 0) // keep first slash if month exists
.join('/');
emit('update:modelValue', hasAll ? `${mm.value}/${dd.value}/${yyyy.value}` : partial);
}
watch(() => props.modelValue, (v) => {
const s = v || '';
const m = s.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (!m) return; // don't overwrite partial typing
if (mm.value !== m[1] || dd.value !== m[2] || yyyy.value !== m[3]) {
mm.value = m[1]; dd.value = m[2]; yyyy.value = m[3];
}
}, { immediate: true });
function onInput(seg: 'mm' | 'dd' | 'yyyy', e: Event) {
const el = e.target as HTMLInputElement;
let val = digitsOnly(el.value);
if (seg !== 'yyyy') val = val.slice(0, 2); else val = val.slice(0, 4);
if (seg === 'mm') mm.value = val;
if (seg === 'dd') dd.value = val;
if (seg === 'yyyy') yyyy.value = val;
emitCombined();
// auto-advance
if (seg === 'mm' && val.length === 2) ddRef.value?.focus();
if (seg === 'dd' && val.length === 2) yyyyRef.value?.focus();
}
function onKeydown(seg: 'mm' | 'dd' | 'yyyy', e: KeyboardEvent) {
const el = e.target as HTMLInputElement;
if (e.key === 'Backspace' && el.selectionStart === 0 && el.selectionEnd === 0) {
if (seg === 'dd') mmRef.value?.focus();
if (seg === 'yyyy') ddRef.value?.focus();
}
}
function onPaste(e: ClipboardEvent) {
const text = e.clipboardData?.getData('text') ?? '';
const num = text.replace(/\D/g, '').slice(0, 8); // MMDDYYYY
if (num.length === 8) {
e.preventDefault();
mm.value = num.slice(0, 2);
dd.value = num.slice(2, 4);
yyyy.value = num.slice(4, 8);
emitCombined();
yyyyRef.value?.focus();
}
}
</script>
<template>
<div class="
inline-flex flex-none w-fit items-center gap-2 *:font-mono *:pl-2 pr-5 *:tabular-nums min-w-0 h-9 rounded-md px-3 py-1
border bg-transparent shadow-xs transition-[color,box-shadow] outline-none
dark:bg-input/30 border-input
focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]
aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive
disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50
">
<input ref="mmRef" :disabled="disabled" inputmode="numeric" autocomplete="bday-month" placeholder="MM"
class="focus:outline-0 w-[3ch] bg-transparent:" :value="mm" @input="onInput('mm', $event)"
@keydown="onKeydown('mm', $event)" maxlength="2" @paste="onPaste" />
<span class="text-muted-foreground">/</span>
<input ref="ddRef" :disabled="disabled" inputmode="numeric" autocomplete="bday-day" placeholder="DD"
class="focus:outline-0 w-[3ch] bg-transparent" :value="dd" @input="onInput('dd', $event)"
@keydown="onKeydown('dd', $event)" maxlength="2" @paste="onPaste" />
<span class="text-muted-foreground">/</span>
<input ref="yyyyRef" :disabled="disabled" inputmode="numeric" autocomplete="bday-year" placeholder="YYYY"
class="focus:outline-0 w-[5ch] bg-transparent" :value="yyyy" @input="onInput('yyyy', $event)" maxlength="4"
@keydown="onKeydown('yyyy', $event)" @paste="onPaste" />
</div>
</template>