All checks were successful
Deploy production / deploy (push) Successful in 2m33s
156 lines
4.3 KiB
Svelte
156 lines
4.3 KiB
Svelte
<script lang="ts">
|
|
import type { Col } from './data-format.js';
|
|
|
|
let {
|
|
column,
|
|
value = $bindable(null),
|
|
lookupUrl,
|
|
autofocus = false,
|
|
compact = false,
|
|
onsave,
|
|
oncancel
|
|
}: {
|
|
column: Col;
|
|
value: string | null;
|
|
lookupUrl?: string;
|
|
autofocus?: boolean;
|
|
compact?: boolean;
|
|
onsave?: (move?: 'next' | 'prev') => void;
|
|
oncancel?: () => void;
|
|
} = $props();
|
|
|
|
const kind = $derived(column.kind);
|
|
const long = $derived(
|
|
kind === 'json' ||
|
|
column.uiType === 'longtext' ||
|
|
(kind === 'text' && (value?.length ?? 0) > 80)
|
|
);
|
|
const listId = `fk-${Math.random().toString(36).slice(2)}`;
|
|
let options = $state.raw<{ value: string; label: string }[]>([]);
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
async function lookup(q: string) {
|
|
if (!column.fk || !lookupUrl) return;
|
|
const url = `${lookupUrl}?column=${encodeURIComponent(column.name)}&q=${encodeURIComponent(q)}`;
|
|
const res = await fetch(url).catch(() => null);
|
|
if (res?.ok) options = (await res.json()).options;
|
|
}
|
|
function queueLookup(q: string) {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => lookup(q), 200);
|
|
}
|
|
|
|
function focusOnMount(node: HTMLElement) {
|
|
if (!autofocus) return;
|
|
node.focus();
|
|
if (node instanceof HTMLInputElement && node.type === 'text') node.select();
|
|
if (column.fk) lookup('');
|
|
}
|
|
|
|
function keydown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
oncancel?.();
|
|
} else if (e.key === 'Enter' && (!long || e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onsave?.();
|
|
} else if (e.key === 'Tab' && onsave) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onsave(e.shiftKey ? 'prev' : 'next');
|
|
}
|
|
}
|
|
|
|
const base =
|
|
'w-full rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-2 py-1 text-sm focus:outline-2 focus:outline-[var(--site-text)]';
|
|
const placeholder = $derived(
|
|
value === null
|
|
? column.hasDefault
|
|
? `default${column.defaultExpr ? `: ${column.defaultExpr}` : ''}`
|
|
: 'null'
|
|
: ''
|
|
);
|
|
</script>
|
|
|
|
<div class={['flex items-start gap-1', compact ? '' : 'w-full']}>
|
|
{#if kind === 'bool'}
|
|
<select
|
|
class={base}
|
|
value={value ?? ''}
|
|
onchange={(e) => (value = e.currentTarget.value === '' ? null : e.currentTarget.value)}
|
|
onkeydown={keydown}
|
|
{@attach focusOnMount}
|
|
aria-label={column.label}
|
|
>
|
|
{#if column.nullable || column.hasDefault}<option value=""
|
|
>{column.hasDefault ? '(default)' : 'null'}</option
|
|
>{/if}
|
|
<option value="true">true</option>
|
|
<option value="false">false</option>
|
|
</select>
|
|
{:else if kind === 'enum' || (column.uiType === 'select' && column.enumValues.length)}
|
|
<select
|
|
class={base}
|
|
value={value ?? ''}
|
|
onchange={(e) => (value = e.currentTarget.value === '' ? null : e.currentTarget.value)}
|
|
onkeydown={keydown}
|
|
{@attach focusOnMount}
|
|
aria-label={column.label}
|
|
>
|
|
{#if column.nullable || column.hasDefault}<option value=""
|
|
>{column.hasDefault ? '(default)' : 'null'}</option
|
|
>{/if}
|
|
{#each column.enumValues as v (v)}<option value={v}>{v}</option>{/each}
|
|
</select>
|
|
{:else if long}
|
|
<textarea
|
|
class={[base, 'min-h-24 font-mono']}
|
|
rows={kind === 'json' ? 6 : 3}
|
|
value={value ?? ''}
|
|
{placeholder}
|
|
oninput={(e) => (value = e.currentTarget.value)}
|
|
onkeydown={keydown}
|
|
{@attach focusOnMount}
|
|
aria-label={column.label}></textarea>
|
|
{:else}
|
|
<input
|
|
class={base}
|
|
type={kind === 'date'
|
|
? 'date'
|
|
: kind === 'datetime'
|
|
? 'datetime-local'
|
|
: kind === 'time'
|
|
? 'time'
|
|
: kind === 'number'
|
|
? 'number'
|
|
: 'text'}
|
|
step={kind === 'number' ? 'any' : kind === 'datetime' || kind === 'time' ? 1 : undefined}
|
|
list={column.fk ? listId : undefined}
|
|
value={value ?? ''}
|
|
{placeholder}
|
|
oninput={(e) => {
|
|
value = e.currentTarget.value;
|
|
if (column.fk) queueLookup(e.currentTarget.value);
|
|
}}
|
|
onkeydown={keydown}
|
|
{@attach focusOnMount}
|
|
aria-label={column.label}
|
|
/>
|
|
{#if column.fk}
|
|
<datalist id={listId}>
|
|
{#each options as o (o.value)}<option value={o.value}>{o.label}</option>{/each}
|
|
</datalist>
|
|
{/if}
|
|
{/if}
|
|
{#if column.nullable && value !== null}
|
|
<button
|
|
type="button"
|
|
class="shrink-0 rounded px-1.5 py-1 text-xs text-[var(--site-muted)] hover:bg-[var(--site-hover)]"
|
|
title="Set to NULL"
|
|
onclick={() => (value = null)}>null</button
|
|
>
|
|
{/if}
|
|
</div>
|