diff --git a/svelte/src/coords.svelte.ts b/svelte/src/coords.svelte.ts new file mode 100644 index 0000000..1821bfe --- /dev/null +++ b/svelte/src/coords.svelte.ts @@ -0,0 +1,136 @@ +export type TableViewerShape = { + n_fields: number; + n_rows_main: number; + n_rows_inserter: number; +}; + +/** + * Describes the location of a table cell in the table UI. + */ +export type Coords = { + region: "main" | "inserter"; + row_idx: number; + field_idx: number; +}; + +/** + * Compare two table coordinates for equality. + */ +export function coords_eq(a: Coords, b: Coords): boolean { + return a.region === b.region && a.row_idx === b.row_idx && + a.field_idx === b.field_idx; +} + +/** + * Returns the cell coordinates offset relative to a starting position. Clamps + * the result to available positions. Assumes that each table region has at + * least 1 row. + */ +export function offset_coords( + start: Coords, + row_offset: number, + field_offset: number, + boundaries: TableViewerShape, +): Coords { + const naive_field_idx = start.field_idx + field_offset; + const bounded_field_idx = Math.min( + boundaries.n_fields - 1, + Math.max(0, naive_field_idx), + ); + const naive_row_idx = start.row_idx + row_offset; + if (start.region === "inserter" && naive_row_idx < 0) { + return { + region: "main", + row_idx: Math.max(0, boundaries.n_rows_main + naive_row_idx), + field_idx: bounded_field_idx, + }; + } + if (start.region === "main" && naive_row_idx >= boundaries.n_rows_main) { + return { + region: "inserter", + row_idx: Math.min( + boundaries.n_rows_inserter - 1, + naive_row_idx - boundaries.n_rows_main, + ), + field_idx: bounded_field_idx, + }; + } + const n_rows = start.region === "main" + ? boundaries.n_rows_main + : boundaries.n_rows_inserter; + return { + region: start.region, + row_idx: Math.min(n_rows, Math.max(0, naive_row_idx)), + field_idx: bounded_field_idx, + }; +} + +/** + * Returns the list of coordinates comprising the rectangular box defined by the + * coordinates of any pair of opposite corners. No guarantees are made + * concerning the ordering of the returned array. + */ +export function get_box( + corner_a: Coords, + corner_b: Coords, + boundaries: TableViewerShape, +): Coords[] { + const box: Coords[] = []; + const left_idx = Math.min(corner_a.field_idx, corner_b.field_idx); + const right_idx = Math.max(corner_a.field_idx, corner_b.field_idx); + if (corner_a.region === corner_b.region) { + for ( + let row_idx = Math.min(corner_a.row_idx, corner_b.row_idx); + row_idx <= Math.max(corner_a.row_idx, corner_b.row_idx); + row_idx += 1 + ) { + for (let field_idx = left_idx; field_idx <= right_idx; field_idx += 1) { + box.push({ + region: corner_b.region, + row_idx, + field_idx, + }); + } + } + } else { + const corner_main = corner_b.region === "main" ? corner_b : corner_a; + const corner_inserter = corner_b.region === "inserter" + ? corner_b + : corner_a; + for ( + let row_idx = corner_main.row_idx; + row_idx < boundaries.n_rows_main; + row_idx += 1 + ) { + for ( + let field_idx = left_idx; + field_idx <= right_idx; + field_idx += 1 + ) { + box.push({ + region: "main", + row_idx, + field_idx, + }); + } + } + for ( + let row_idx = 0; + row_idx <= corner_inserter.row_idx; + row_idx += 1 + ) { + for ( + let field_idx = left_idx; + field_idx <= right_idx; + field_idx += 1 + ) { + box.push({ + region: "inserter", + row_idx, + field_idx, + }); + } + } + } + return box; +} diff --git a/svelte/src/datum.svelte.ts b/svelte/src/datum.svelte.ts index 213d7e7..760f5c6 100644 --- a/svelte/src/datum.svelte.ts +++ b/svelte/src/datum.svelte.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { get_empty_datum_for, Presentation } from "./presentation.svelte.ts"; type Assert<_T extends true> = void; @@ -35,3 +36,31 @@ export const datum_schema = z.union([ ]); export type Datum = z.infer; + +export function parse_clipboard_value( + presentation: Presentation, + input: string, +): Datum { + if (presentation.t === "Dropdown") { + if ( + presentation.c.allow_custom || + presentation.c.options.some(({ value }) => value === input) + ) { + return { t: "Text", c: input }; + } else { + return get_empty_datum_for(presentation); + } + } else if (presentation.t === "Text") { + return { t: "Text", c: input }; + } else if (presentation.t === "Timestamp") { + // TODO: implement + console.warn("parsing timestamps from clipboard is not yet supported"); + return get_empty_datum_for(presentation); + } else if (presentation.t === "Uuid") { + // TODO: implement + console.warn("parsing uuids from clipboard is not yet supported"); + return get_empty_datum_for(presentation); + } + type _ = Assert; + throw new Error("should be unreachable"); +} diff --git a/svelte/src/field.svelte.ts b/svelte/src/field.svelte.ts index 47e124e..4bb236a 100644 --- a/svelte/src/field.svelte.ts +++ b/svelte/src/field.svelte.ts @@ -25,13 +25,7 @@ export type FieldInfo = z.infer; // -------- Table Utils -------- // // TODO move this to its own module -export type Coords = [number, number]; - export type Row = { key: string | number; data: Datum[]; }; - -export function coords_eq(a: Coords, b: Coords): boolean { - return a[0] === b[0] && a[1] === b[1]; -} diff --git a/svelte/src/table-cell.svelte b/svelte/src/table-cell.svelte index 419aea7..be4b8ad 100644 --- a/svelte/src/table-cell.svelte +++ b/svelte/src/table-cell.svelte @@ -1,16 +1,19 @@
onmousedown?.(ev, coords)} + {oncopy} ondblclick={(ev) => ondblclick?.(ev, coords)} onfocus={(ev) => onfocus?.(ev, coords)} onkeydown={(ev) => onkeydown?.(ev, coords)} + onmousedown={(ev) => onmousedown?.(ev, coords)} + {onpaste} role="gridcell" style:width={`${field.field.table_width_px}px`} tabindex={selected ? 0 : -1} diff --git a/svelte/src/table-viewer.webc.svelte b/svelte/src/table-viewer.webc.svelte index 78a1ae4..decbeb2 100644 --- a/svelte/src/table-viewer.webc.svelte +++ b/svelte/src/table-viewer.webc.svelte @@ -11,15 +11,19 @@ {#snippet table_region({ - region_name, + region, rows, on_cell_click, }: { - region_name: "main" | "inserter"; + region: "main" | "inserter"; rows: Row[]; on_cell_click(ev: MouseEvent, coords: Coords): void; })} {#if lazy_data} - {#each rows as row, row_index} + {#each rows as row, row_idx}
- {#each lazy_data.fields as field, field_index} + {#each lazy_data.fields as field, field_idx} { focus_cursor = focus; @@ -580,13 +582,14 @@ ondblclick={() => datum_editor?.focus()} onkeydown={(ev) => handle_cell_keydown(ev)} onmousedown={on_cell_click} + onpaste={handle_cell_paste} selected={selections.some( (sel) => - sel.region === region_name && - coords_eq(sel.coords, [row_index, field_index]), + sel.coords.region === region && + coords_eq(sel.coords, { region, row_idx, field_idx }), )} - table_region={region_name} - value={row.data[field_index]} + table_region={region} + value={row.data[field_idx]} /> {/each}
@@ -594,13 +597,7 @@ {/if} {/snippet} -
{ - console.log("paste"); - console.log(ev.clipboardData?.getData("text")); - }} -> +
{#if lazy_data}
@@ -632,15 +629,20 @@
{@render table_region({ - region_name: "main", + region: "main", rows: lazy_data.rows, on_cell_click: (ev: MouseEvent, coords: Coords) => { if (ev.metaKey || ev.ctrlKey) { - // TODO - // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords]; - // editor_input_value = ""; + set_selections([ + coords, + ...selections + .filter((sel) => !coords_eq(sel.coords, coords)) + .map((sel) => sel.coords), + ]); + } else if (ev.shiftKey) { + move_cursor(coords, { additive: true }); } else { - set_selections([{ region: "main", coords }]); + move_cursor(coords); } }, })} @@ -653,15 +655,20 @@
{@render table_region({ - region_name: "inserter", + region: "inserter", rows: inserter_rows, on_cell_click: (ev: MouseEvent, coords: Coords) => { if (ev.metaKey || ev.ctrlKey) { - // TODO - // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords]; - // editor_input_value = ""; + set_selections([ + coords, + ...selections + .filter((sel) => !coords_eq(sel.coords, coords)) + .map((sel) => sel.coords), + ]); + } else if (ev.shiftKey) { + move_cursor(coords, { additive: true }); } else { - set_selections([{ region: "inserter", coords }]); + move_cursor(coords); } }, })} @@ -693,11 +700,11 @@
- {#if selections.length === 1} + {#if selections.length !== 0 && selections.every(({ coords: { field_idx } }) => field_idx === selections[0]?.coords.field_idx)} try_queue_delta()} on_cancel_edit={cancel_edit} on_change={() => {