114 lines
2.8 KiB
Svelte
114 lines
2.8 KiB
Svelte
<script lang="ts">
|
|
import Calendar from "@lucide/svelte/icons/calendar";
|
|
import { untrack } from "svelte";
|
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
import { addDays, today } from "#lib/dates.js";
|
|
import { formatDate } from "#lib/format.js";
|
|
import { parseDate } from "./parse.js";
|
|
import { INPUT } from "./styles.js";
|
|
|
|
let {
|
|
value = $bindable(""),
|
|
ref = $bindable(null),
|
|
invalid = false,
|
|
class: className = "",
|
|
...rest
|
|
}: Omit<HTMLInputAttributes, "value"> & {
|
|
value?: string;
|
|
ref?: HTMLInputElement | null;
|
|
invalid?: boolean;
|
|
} = $props();
|
|
|
|
let text = $state(value);
|
|
let picker = $state<HTMLInputElement | null>(null);
|
|
|
|
const parsed = $derived(parseDate(text, today()));
|
|
const shown = $derived(parsed ?? value);
|
|
const label = $derived(
|
|
shown
|
|
? `${formatDate(shown, "day")}${shown.slice(0, 4) === today().slice(0, 4) ? "" : ` ${shown.slice(0, 4)}`}`
|
|
: ""
|
|
);
|
|
|
|
$effect(() => {
|
|
const next = value;
|
|
untrack(() => {
|
|
if (next && next !== parseDate(text)) {
|
|
text = next;
|
|
}
|
|
});
|
|
});
|
|
|
|
function handleInput(event: Event & { currentTarget: HTMLInputElement }) {
|
|
text = event.currentTarget.value;
|
|
const next = parseDate(text);
|
|
if (next) {
|
|
value = next;
|
|
}
|
|
}
|
|
|
|
function handleBlur() {
|
|
if (parsed) {
|
|
text = parsed;
|
|
value = parsed;
|
|
}
|
|
}
|
|
|
|
function handleKey(event: KeyboardEvent) {
|
|
if (
|
|
(event.key === "ArrowUp" || event.key === "ArrowDown") &&
|
|
!(event.altKey || event.ctrlKey || event.metaKey)
|
|
) {
|
|
event.preventDefault();
|
|
const step =
|
|
(event.key === "ArrowUp" ? 1 : -1) * (event.shiftKey ? 7 : 1);
|
|
value = addDays(parsed ?? value ?? today(), step);
|
|
text = value;
|
|
}
|
|
}
|
|
|
|
function openPicker() {
|
|
picker?.showPicker?.();
|
|
}
|
|
</script>
|
|
|
|
<div class="relative {className}">
|
|
<input
|
|
bind:this={ref}
|
|
{...rest}
|
|
aria-invalid={invalid || (text.trim() !== "" && !parsed) || undefined}
|
|
autocomplete="off"
|
|
class="{INPUT} num pr-28"
|
|
onblur={handleBlur}
|
|
oninput={handleInput}
|
|
onkeydown={handleKey}
|
|
spellcheck="false"
|
|
value={text}
|
|
>
|
|
<span
|
|
class="pointer-events-none absolute inset-y-0 right-9 flex items-center text-[12px] text-cladd-fg-soft"
|
|
>{label}</span
|
|
>
|
|
<button
|
|
aria-label="Pick a date"
|
|
class="absolute inset-y-0 right-0 grid w-8 place-items-center text-cladd-fg-soft hover:text-cladd-fg"
|
|
onclick={openPicker}
|
|
tabindex="-1"
|
|
type="button"
|
|
>
|
|
<Calendar class="size-3.5" />
|
|
</button>
|
|
<input
|
|
aria-hidden="true"
|
|
class="pointer-events-none absolute right-0 bottom-0 h-0 w-0 opacity-0"
|
|
onchange={({ currentTarget }) => {
|
|
({ value } = currentTarget);
|
|
text = value;
|
|
}}
|
|
tabindex="-1"
|
|
type="date"
|
|
{value}
|
|
bind:this={picker}
|
|
>
|
|
</div>
|