135 lines
3.2 KiB
Svelte
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>
|