Files
lead/frontend/src/lib/components/grid/cell.svelte
2025-09-10 02:17:25 +10:00

135 lines
3.2 KiB
Svelte

<script lang="ts">
import { Input } from '$lib/components/ui/input/index.js';
import clsx from 'clsx';
import { getErrDesc, getErrTitle, getEvalLiteral, isErr, type CellT } from './utils';
import * as HoverCard from '$lib/components/ui/hover-card/index.js';
let {
cla = '',
width = '80px',
height = '30px',
cell = $bindable(undefined),
onmousedown = () => {},
startediting = () => {},
stopediting = () => {},
active = false,
editing = false
}: {
cla?: string;
width?: string;
height?: string;
cell?: CellT;
onmousedown?: (e: MouseEvent) => void;
startediting?: () => void;
stopediting?: () => void;
active?: boolean;
editing?: boolean;
} = $props();
// focus the first focusable descendant (the inner <input>)
function autofocusWithin(node: HTMLElement) {
queueMicrotask(() => {
const el = node.querySelector('input') as HTMLInputElement | null;
if (el !== null) {
el.value = cell?.raw_val ?? '';
el.focus();
}
});
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' || e.key === 'NumpadEnter') {
e.preventDefault(); // avoid form submit/line break
const el = (e.currentTarget as HTMLElement).querySelector('input') as HTMLInputElement | null;
el?.blur(); // triggers on:blur below
} else if (e.key == 'Escape') {
e.preventDefault();
stopediting();
}
}
</script>
{#if editing}
<div use:autofocusWithin onkeydown={handleKeydown}>
<Input
style="width: {width}; height: {height}"
class="relative rounded-none p-1 !transition-none delay-0 duration-0
focus:z-20 focus:shadow-[0_0_0_1px_var(--color-primary)] focus:outline-none"
onblur={(e) => {
cell = {
val: cell?.val,
raw_val: (e.target as HTMLInputElement).value
};
stopediting();
}}
/>
</div>
{:else if cell && isErr(cell.val)}
<HoverCard.Root openDelay={500} closeDelay={500}>
<HoverCard.Trigger>
{@render InnerCell()}
</HoverCard.Trigger>
<HoverCard.Content side="right">
<h2 class="text-md font-semibold tracking-tight transition-colors">
{getErrTitle(cell.val)}
</h2>
{getErrDesc(cell.val)}
</HoverCard.Content>
</HoverCard.Root>
{:else}
{@render InnerCell()}
{/if}
{#snippet InnerCell()}
<div
ondblclick={startediting}
{onmousedown}
style:width
style:height
class={clsx('placeholder bg-background p-1', { active }, cla)}
>
{#if cell && (cell.raw_val !== '' || getEvalLiteral(cell.val) !== '')}
<span class={clsx('pointer-events-none select-none', { err: isErr(cell.val) })}>
{#if cell.val}
{getEvalLiteral(cell.val)}
{:else}
{cell.raw_val}
{/if}
</span>
{/if}
</div>
{/snippet}
<style>
.placeholder {
border: 1px solid var(--input);
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
}
.active {
z-index: 20;
border: 1px solid var(--color-primary);
outline: 1px solid var(--color-primary);
}
.active:has(.err),
.placeholder:has(.err) {
position: relative; /* needed for absolute positioning */
color: red;
}
.active:has(.err)::after,
.placeholder:has(.err)::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 0;
height: 0;
border-top: 12px solid red; /* size & color of the triangle */
border-left: 12px solid transparent;
}
</style>