created date input
This commit is contained in:
@@ -15,9 +15,15 @@ import { toTypedSchema } from '@vee-validate/zod';
|
|||||||
import { Form } from 'vee-validate';
|
import { Form } from 'vee-validate';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import * as z from 'zod';
|
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({
|
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(),
|
name: z.string(),
|
||||||
playtime: z.coerce.number({ invalid_type_error: "Must be a number", }).min(0, "Cannot be less than 0"),
|
playtime: z.coerce.number({ invalid_type_error: "Must be a number", }).min(0, "Cannot be less than 0"),
|
||||||
hobbies: z.string(),
|
hobbies: z.string(),
|
||||||
@@ -62,6 +68,8 @@ onMounted(() => {
|
|||||||
initialValues.value = { ...fallbackInitials }
|
initialValues.value = { ...fallbackInitials }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -70,9 +78,10 @@ onMounted(() => {
|
|||||||
<!-- Age -->
|
<!-- Age -->
|
||||||
<FormField name="age" v-slot="{ value, handleChange }">
|
<FormField name="age" v-slot="{ value, handleChange }">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>What is your age?</FormLabel>
|
<FormLabel>What is your date of birth?</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
|
<DateInput :model-value="(value as string) ?? ''" :disabled="readOnly"
|
||||||
|
@update:model-value="handleChange" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</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