created date input
This commit is contained in:
@@ -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>
|
||||
|
||||
102
ui/src/components/form/DateInput.vue
Normal file
102
ui/src/components/form/DateInput.vue
Normal 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>
|
||||
Reference in New Issue
Block a user