From a99384e887ddac42596852a656cb4207dfb6bdaf Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Tue, 22 Jul 2025 15:39:52 -0700 Subject: [PATCH] time input: refactor to allow setting value reactively --- src/lib/TimeInput.svelte | 147 +++++++++++++++++++-------------------- src/routes/+page.svelte | 9 ++- 2 files changed, 79 insertions(+), 77 deletions(-) diff --git a/src/lib/TimeInput.svelte b/src/lib/TimeInput.svelte index b6ba775..3503bb4 100644 --- a/src/lib/TimeInput.svelte +++ b/src/lib/TimeInput.svelte @@ -20,7 +20,7 @@ import StyledRawInput from './StyledRawInput.svelte'; import moment from 'moment'; import type { ClassValue } from 'svelte/elements'; - import { generateIdentifier, targetMust } from './util'; + import { generateIdentifier, prefixZero, targetMust } from './util'; import { FocusManager } from './focus'; import { Time } from '@internationalized/date'; @@ -54,9 +54,22 @@ type componentKey = 'hour' | 'minute'; const id = $derived(generateIdentifier('time-input', name)); - const values: Record = $state({ - hour: '', - minute: '' + const values: Record = $derived.by(() => { + let hour = ''; + if (value) { + if (value.hour > 12) { + hour = (value.hour - 12).toString(); + } else if (value.hour === 0) { + hour = '12'; // 12 AM case + } else { + hour = value.hour.toString(); + } + } + + return { + hour, + minute: !value ? '' : prefixZero(value.minute.toString()) + }; }); let ampm: ampmKey = $state('AM'); let valid: boolean = $state(true); @@ -87,28 +100,22 @@ * @returns true if the value was incremented, false if it looped back to 0 */ const incrementValue = (input: HTMLInputElement, max: number, start: number): boolean => { - const f = () => { - if (input.value.length === 0) { - input.value = start.toString(); - return true; - } + if (input.value.length === 0) { + input.value = start.toString(); + return true; + } - const value = parseInt(input.value); - if (value === max) { - input.value = start.toString(); - return false; - } else if (value > max) { - input.value = (value - max).toString(); - return false; - } else { - input.value = (value + 1).toString(); - return true; - } - }; - - const res = f(); - updateValue(); - return res; + const value = parseInt(input.value); + if (value === max) { + input.value = start.toString(); + return false; + } else if (value > max) { + input.value = (value - max).toString(); + return false; + } else { + input.value = (value + 1).toString(); + return true; + } }; /** @@ -119,36 +126,30 @@ * @returns true if the value was decremented, false if it looped back to max */ const decrementValue = (input: HTMLInputElement, max: number, start: number): boolean => { - const f = () => { - if (input.value.length === 0) { - input.value = max.toString(); - return true; - } + if (input.value.length === 0) { + input.value = max.toString(); + return true; + } - const value = parseInt(input.value); - if (value <= start) { - input.value = max.toString(); - return false; - } else { - input.value = (value - 1).toString(); - return true; - } - }; - - const res = f(); - updateValue(); - return res; + const value = parseInt(input.value); + if (value <= start) { + input.value = max.toString(); + return false; + } else { + input.value = (value - 1).toString(); + return true; + } }; /** * updateValue updates `value` with the current time in 24-hour format. * If any component is invalid or blank, it sets `value` to an empty string. */ - const updateValue = () => { - let ampmLocal = ampm; - let hourValue = parseInt(values.hour); - let minuteValue = parseInt(values.minute); + const updateValue = (override?: Partial>) => { + let hourValue = parseInt(override?.hour || values.hour); + let minuteValue = parseInt(override?.minute || values.minute); + // if no hour, clear value if (isNaN(hourValue)) { value = null; formattedValue = ''; @@ -156,16 +157,21 @@ return; } - // internally convert to 24-hour format + // if hour is greater than 12, set to 12-hour format if (hourValue > 12) { - hourValue = hourValue - 12; - ampmLocal = 'PM'; + hourValue -= 12; + ampm = 'PM'; } + + // convert to 24-hour format + if (ampm === 'PM' && hourValue < 12) hourValue += 12; + else if (ampm === 'AM' && hourValue >= 12) hourValue -= 12; + // default minutes to zero if empty if (isNaN(minuteValue)) { minuteValue = 0; } - value = new Time(hourValue + (ampmLocal === 'PM' ? 12 : 0), minuteValue, 0); + value = new Time(hourValue, minuteValue); updateHiddenInput(); // update formatted value @@ -187,9 +193,9 @@ { max: number; pattern: RegExp; - onkeydown: (e: KeyboardEvent) => void; - oninput: (e: Event) => void; - onblur: (e: FocusEvent) => void; + onkeydown?: (e: KeyboardEvent) => void; + oninput?: (e: Event) => void; + onblur?: (e: FocusEvent) => void; divider?: string; placeholder?: string; } @@ -207,37 +213,27 @@ } if (e.key === 'ArrowUp') { - if (!incrementValue(target, 12, 1)) { - toggleAMPM(); - } + incrementValue(target, 12, 1); + if (target.value === '12') toggleAMPM(); } else if (e.key === 'ArrowDown') { - if (!decrementValue(target, 12, 1)) { - toggleAMPM(); - } + decrementValue(target, 12, 1); + if (target.value === '11') toggleAMPM(); } else { return; } + updateValue({ hour: target.value }); e.preventDefault(); }, oninput: (e) => { const target = targetMust(e); - updateValue(); - if (target.value.length === 2) { focusList.focusNext(); } }, - onblur: (e: FocusEvent) => { + onblur: (e) => { const target = targetMust(e); - const hourValue = parseInt(target.value); - - if (hourValue > 12) { - target.value = (hourValue - 12).toString(); - ampm = 'PM'; - } - - updateValue(); + updateValue({ hour: target.value }); }, divider: ':' }, @@ -256,13 +252,12 @@ return; } + updateValue({ minute: target.value }); e.preventDefault(); }, - oninput: () => { - updateValue(); - }, onblur: (e: FocusEvent) => { const target = targetMust(e); + updateValue({ minute: target.value }); if (target.value.length === 1) { target.value = '0' + target.value; } @@ -295,7 +290,7 @@ 'text-center text-xl focus:placeholder:text-transparent', compact ? 'h-9 w-16!' : 'h-16 w-24!' ]} - bind:value={values[key]} + value={values[key]} inputmode="numeric" pattern="[0-9]*" placeholder={opts.placeholder ?? '00'} @@ -306,7 +301,7 @@ onkeydown={opts.onkeydown} oninput={opts.oninput} onblur={opts.onblur} - {@attach focusList.input()} + {@attach focusList.input({ selectAll: true })} /> {#if opts.divider} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d01ff6b..1a9d5ce 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -266,7 +266,14 @@ -

Selected time is {formatTime(timeValue, 'undefined')}

+

Selected time is {formatTime(timeValue, 'undefined')} ({timeValue?.toString()})

+