2025-09-08 15:56:57 -07:00
|
|
|
<svelte:options
|
|
|
|
|
customElement={{
|
|
|
|
|
props: {
|
|
|
|
|
columns: { type: "Array" },
|
|
|
|
|
},
|
|
|
|
|
shadow: "none",
|
|
|
|
|
tag: "table-viewer",
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2025-08-10 14:32:15 -07:00
|
|
|
|
|
|
|
|
<script lang="ts">
|
2025-08-13 18:52:37 -07:00
|
|
|
import { z } from "zod";
|
2025-08-10 14:32:15 -07:00
|
|
|
|
2025-09-23 13:08:51 -07:00
|
|
|
import { type Datum, datum_schema } from "./datum.svelte";
|
|
|
|
|
import DatumEditor from "./datum-editor.svelte";
|
2025-08-10 14:32:15 -07:00
|
|
|
import {
|
|
|
|
|
type Coords,
|
|
|
|
|
type Row,
|
2025-08-13 18:52:37 -07:00
|
|
|
type FieldInfo,
|
2025-08-10 14:32:15 -07:00
|
|
|
coords_eq,
|
2025-08-13 18:52:37 -07:00
|
|
|
field_info_schema,
|
2025-08-10 14:32:15 -07:00
|
|
|
} from "./field.svelte";
|
2025-10-07 06:23:50 +00:00
|
|
|
import FieldAdder from "./field-adder.svelte";
|
2025-08-13 18:52:37 -07:00
|
|
|
import FieldHeader from "./field-header.svelte";
|
2025-09-23 13:08:51 -07:00
|
|
|
import { get_empty_datum_for } from "./presentation.svelte";
|
2025-11-02 20:26:33 +00:00
|
|
|
import TableCell from "./table-cell.svelte";
|
2025-09-08 15:56:57 -07:00
|
|
|
|
|
|
|
|
type Props = {
|
2025-10-07 06:23:50 +00:00
|
|
|
columns?: {
|
|
|
|
|
name: string;
|
|
|
|
|
regtype: string;
|
|
|
|
|
}[];
|
2025-09-08 15:56:57 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let { columns = [] }: Props = $props();
|
2025-08-10 14:32:15 -07:00
|
|
|
|
2025-10-16 06:40:11 +00:00
|
|
|
type CellDelta = {
|
|
|
|
|
// Assumes that primary keys are immutable and that rows are only added or
|
|
|
|
|
// removed upon a refresh.
|
|
|
|
|
coords: Coords;
|
2025-09-23 13:08:51 -07:00
|
|
|
value_initial: Datum;
|
|
|
|
|
value_updated: Datum;
|
2025-08-10 14:32:15 -07:00
|
|
|
};
|
|
|
|
|
|
2025-10-16 06:40:11 +00:00
|
|
|
type Delta = {
|
|
|
|
|
cells: CellDelta[];
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-10 14:32:15 -07:00
|
|
|
type LazyData = {
|
|
|
|
|
rows: Row[];
|
2025-08-13 18:52:37 -07:00
|
|
|
fields: FieldInfo[];
|
2025-08-10 14:32:15 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type Selection = {
|
|
|
|
|
region: "main" | "inserter";
|
|
|
|
|
coords: Coords;
|
2025-09-23 13:08:51 -07:00
|
|
|
original_value: Datum;
|
2025-08-10 14:32:15 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let selections = $state<Selection[]>([]);
|
2025-10-16 06:40:11 +00:00
|
|
|
// While the datum editor is focused and while updated values are being pushed
|
|
|
|
|
// to the server, other actions such as changing the set of selected cells are
|
|
|
|
|
// restricted.
|
|
|
|
|
let editor_value = $state<Datum | undefined>(undefined);
|
|
|
|
|
let deltas = $state<{
|
|
|
|
|
commit_queued: Delta[];
|
|
|
|
|
commit_pending: Delta[];
|
|
|
|
|
committed: Delta[];
|
|
|
|
|
revert_queued: Delta[];
|
|
|
|
|
revert_pending: Delta[];
|
|
|
|
|
reverted: Delta[];
|
|
|
|
|
}>({
|
|
|
|
|
commit_queued: [],
|
|
|
|
|
commit_pending: [],
|
|
|
|
|
committed: [],
|
|
|
|
|
revert_queued: [],
|
|
|
|
|
revert_pending: [],
|
|
|
|
|
reverted: [],
|
|
|
|
|
});
|
|
|
|
|
let datum_editor = $state<DatumEditor | undefined>();
|
2025-11-02 20:26:33 +00:00
|
|
|
let focus_cursor = $state<(() => unknown) | undefined>();
|
2025-08-10 14:32:15 -07:00
|
|
|
let inserter_rows = $state<Row[]>([]);
|
|
|
|
|
let lazy_data = $state<LazyData | undefined>();
|
|
|
|
|
|
|
|
|
|
// -------- Helper Functions -------- //
|
|
|
|
|
|
|
|
|
|
function arrow_key_direction(
|
|
|
|
|
key: string,
|
|
|
|
|
): "Down" | "Left" | "Right" | "Up" | undefined {
|
|
|
|
|
if (key === "ArrowDown") {
|
|
|
|
|
return "Down";
|
|
|
|
|
} else if (key === "ArrowLeft") {
|
|
|
|
|
return "Left";
|
|
|
|
|
} else if (key === "ArrowRight") {
|
|
|
|
|
return "Right";
|
|
|
|
|
} else if (key === "ArrowUp") {
|
|
|
|
|
return "Up";
|
|
|
|
|
} else {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------- Updates and Effects -------- //
|
|
|
|
|
|
|
|
|
|
function set_selections(arr: Omit<Selection, "original_value">[]) {
|
|
|
|
|
selections = arr.map((sel) => {
|
2025-09-23 13:08:51 -07:00
|
|
|
let cell_data: Datum | undefined;
|
2025-08-10 14:32:15 -07:00
|
|
|
if (sel.region === "main") {
|
|
|
|
|
cell_data = lazy_data?.rows[sel.coords[0]].data[sel.coords[1]];
|
|
|
|
|
} else if (sel.region === "inserter") {
|
|
|
|
|
cell_data = inserter_rows[sel.coords[0]].data[sel.coords[1]];
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error("invalid region");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
...sel,
|
|
|
|
|
original_value: cell_data!,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
if (arr.length === 1) {
|
|
|
|
|
const [sel] = arr;
|
2025-09-23 13:08:51 -07:00
|
|
|
let cell_data: Datum | undefined;
|
2025-08-10 14:32:15 -07:00
|
|
|
if (sel.region === "main") {
|
|
|
|
|
cell_data = lazy_data?.rows[sel.coords[0]].data[sel.coords[1]];
|
|
|
|
|
} else if (sel.region === "inserter") {
|
|
|
|
|
cell_data = inserter_rows[sel.coords[0]].data[sel.coords[1]];
|
|
|
|
|
}
|
2025-10-16 06:40:11 +00:00
|
|
|
editor_value = cell_data;
|
2025-08-10 14:32:15 -07:00
|
|
|
} else {
|
2025-10-16 06:40:11 +00:00
|
|
|
editor_value = undefined;
|
2025-08-10 14:32:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 06:40:11 +00:00
|
|
|
function move_selection(direction: "Down" | "Left" | "Right" | "Up") {
|
|
|
|
|
if (!lazy_data || selections.length === 0) {
|
|
|
|
|
console.warn("move_selection() preconditions not met");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const last_selection = selections[selections.length - 1];
|
|
|
|
|
if (
|
|
|
|
|
direction === "Right" &&
|
|
|
|
|
last_selection.coords[1] < lazy_data.fields.length - 1
|
|
|
|
|
) {
|
|
|
|
|
set_selections([
|
|
|
|
|
{
|
|
|
|
|
region: last_selection.region,
|
|
|
|
|
coords: [last_selection.coords[0], last_selection.coords[1] + 1],
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
} else if (direction === "Left" && last_selection.coords[1] > 0) {
|
|
|
|
|
set_selections([
|
|
|
|
|
{
|
|
|
|
|
region: last_selection.region,
|
|
|
|
|
coords: [last_selection.coords[0], last_selection.coords[1] - 1],
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
} else if (direction === "Down") {
|
|
|
|
|
if (last_selection.region === "main") {
|
|
|
|
|
if (last_selection.coords[0] < lazy_data.rows.length - 1) {
|
|
|
|
|
set_selections([
|
|
|
|
|
{
|
|
|
|
|
region: "main",
|
|
|
|
|
coords: [last_selection.coords[0] + 1, last_selection.coords[1]],
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
} else {
|
|
|
|
|
// At bottom of main table.
|
|
|
|
|
set_selections([
|
|
|
|
|
{
|
|
|
|
|
region: "inserter",
|
|
|
|
|
coords: [0, last_selection.coords[1]],
|
|
|
|
|
},
|
|
|
|
|
]);
|
2025-08-10 14:32:15 -07:00
|
|
|
}
|
2025-10-16 06:40:11 +00:00
|
|
|
} else if (last_selection.region === "inserter") {
|
|
|
|
|
if (last_selection.coords[0] < inserter_rows.length - 1) {
|
|
|
|
|
set_selections([
|
|
|
|
|
{
|
|
|
|
|
region: "inserter",
|
|
|
|
|
coords: [last_selection.coords[0] + 1, last_selection.coords[1]],
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (direction === "Up") {
|
|
|
|
|
if (last_selection.region === "main") {
|
|
|
|
|
if (last_selection.coords[0] > 0) {
|
|
|
|
|
set_selections([
|
|
|
|
|
{
|
|
|
|
|
region: "main",
|
|
|
|
|
coords: [last_selection.coords[0] - 1, last_selection.coords[1]],
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
} else if (last_selection.region === "inserter") {
|
|
|
|
|
if (last_selection.coords[0] > 0) {
|
|
|
|
|
set_selections([
|
|
|
|
|
{
|
|
|
|
|
region: "inserter",
|
|
|
|
|
coords: [last_selection.coords[0] - 1, last_selection.coords[1]],
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
} else {
|
|
|
|
|
// At top of inserter table.
|
|
|
|
|
set_selections([
|
|
|
|
|
{
|
|
|
|
|
region: "main",
|
|
|
|
|
coords: [lazy_data.rows.length - 1, last_selection.coords[1]],
|
|
|
|
|
},
|
|
|
|
|
]);
|
2025-08-10 14:32:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function try_sync_edit_to_cells() {
|
2025-10-21 18:58:09 +00:00
|
|
|
if (!lazy_data || selections.length !== 1) {
|
|
|
|
|
console.warn("preconditions for try_sync_edit_to_cells() not met");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (editor_value === undefined) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const [sel] = selections;
|
|
|
|
|
if (sel.region === "main") {
|
|
|
|
|
lazy_data.rows[sel.coords[0]].data[sel.coords[1]] = editor_value;
|
|
|
|
|
} else if (sel.region === "inserter") {
|
|
|
|
|
inserter_rows[sel.coords[0]].data[sel.coords[1]] = editor_value;
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error("Unknown region");
|
2025-08-10 14:32:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 06:40:11 +00:00
|
|
|
function try_queue_delta() {
|
|
|
|
|
// Copy `editor_value` so that it can be used intuitively within closures.
|
|
|
|
|
const editor_value_scoped = editor_value;
|
|
|
|
|
if (editor_value_scoped === undefined) {
|
2025-10-21 18:58:09 +00:00
|
|
|
console.debug("not a valid cell value");
|
2025-10-16 06:40:11 +00:00
|
|
|
cancel_edit();
|
|
|
|
|
} else {
|
|
|
|
|
if (selections.length > 0) {
|
|
|
|
|
deltas.commit_queued = [
|
|
|
|
|
...deltas.commit_queued,
|
|
|
|
|
{
|
|
|
|
|
cells: selections
|
|
|
|
|
.filter(({ region }) => region === "main")
|
|
|
|
|
.map((sel) => ({
|
|
|
|
|
coords: sel.coords,
|
|
|
|
|
value_initial: sel.original_value,
|
|
|
|
|
value_updated: editor_value_scoped,
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
];
|
2025-10-21 18:58:09 +00:00
|
|
|
console.debug("Commit queue:", deltas.commit_queued);
|
2025-10-16 06:40:11 +00:00
|
|
|
selections = selections.map((sel) => ({
|
|
|
|
|
...sel,
|
|
|
|
|
original_value: editor_value_scoped,
|
|
|
|
|
}));
|
|
|
|
|
}
|
2025-08-10 14:32:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 06:40:11 +00:00
|
|
|
async function commit_delta(delta: Delta) {
|
|
|
|
|
// Copy `lazy_data` so that it can be used intuitively within closures.
|
|
|
|
|
const lazy_data_scoped = lazy_data;
|
|
|
|
|
if (!lazy_data_scoped) {
|
|
|
|
|
console.warn("sync_delta() preconditions not met");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
deltas.commit_pending = [...deltas.commit_pending, delta];
|
|
|
|
|
const resp = await fetch("update-values", {
|
|
|
|
|
method: "post",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
cells: delta.cells.map((cell) => ({
|
|
|
|
|
pkey: JSON.parse(lazy_data_scoped.rows[cell.coords[0]].key as string),
|
|
|
|
|
column: lazy_data_scoped.fields[cell.coords[1]].field.name,
|
|
|
|
|
value: cell.value_updated,
|
|
|
|
|
})),
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
if (resp.status >= 200 && resp.status < 300) {
|
|
|
|
|
deltas.commit_pending = deltas.commit_pending.filter((x) => x !== delta);
|
|
|
|
|
deltas.committed = [...deltas.committed, delta];
|
|
|
|
|
} else {
|
|
|
|
|
// TODO display feedback to user
|
|
|
|
|
console.error(resp);
|
|
|
|
|
console.error(await resp.text());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tick_delta_queue() {
|
|
|
|
|
const front_of_queue: Delta | undefined = deltas.commit_queued[0];
|
|
|
|
|
if (front_of_queue) {
|
|
|
|
|
deltas.commit_queued = deltas.commit_queued.filter(
|
|
|
|
|
(x) => x !== front_of_queue,
|
|
|
|
|
);
|
|
|
|
|
commit_delta(front_of_queue).catch(console.error);
|
|
|
|
|
}
|
2025-08-10 14:32:15 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cancel_edit() {
|
|
|
|
|
selections.forEach(({ coords, original_value, region }) => {
|
|
|
|
|
if (region === "main") {
|
|
|
|
|
if (lazy_data) {
|
|
|
|
|
lazy_data.rows[coords[0]].data[coords[1]] = original_value;
|
|
|
|
|
}
|
|
|
|
|
} else if (region === "inserter") {
|
|
|
|
|
inserter_rows[coords[0]].data[coords[1]] = original_value;
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error("Unknown region");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// Reset editor input value
|
|
|
|
|
set_selections(selections);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 20:26:33 +00:00
|
|
|
// -------- Event Handlers -------- //
|
2025-08-10 14:32:15 -07:00
|
|
|
|
|
|
|
|
function handle_table_keydown(ev: KeyboardEvent) {
|
2025-11-02 20:26:33 +00:00
|
|
|
if (!lazy_data) {
|
|
|
|
|
console.warn("preconditions for handle_table_keydown() not met");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const arrow_direction = arrow_key_direction(ev.key);
|
|
|
|
|
if (arrow_direction) {
|
|
|
|
|
move_selection(arrow_direction);
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
} else if (/^[a-zA-Z0-9`~!@#$%^&*()_=+[\]{}\\|;:'",<.>/?-]$/.test(ev.key)) {
|
|
|
|
|
const sel = selections[0];
|
|
|
|
|
if (sel) {
|
|
|
|
|
editor_value = get_empty_datum_for(
|
|
|
|
|
lazy_data.fields[sel.coords[1]].field.presentation,
|
|
|
|
|
);
|
|
|
|
|
datum_editor?.focus();
|
2025-08-13 18:52:37 -07:00
|
|
|
}
|
2025-11-02 20:26:33 +00:00
|
|
|
} else if (ev.key === "Enter") {
|
|
|
|
|
if (ev.shiftKey) {
|
|
|
|
|
if (selections[0]?.region === "main") {
|
|
|
|
|
set_selections([
|
|
|
|
|
{
|
|
|
|
|
region: "inserter",
|
|
|
|
|
coords: [0, selections[0]?.coords[1] ?? 0],
|
|
|
|
|
},
|
|
|
|
|
]);
|
2025-08-13 18:52:37 -07:00
|
|
|
} else {
|
2025-11-02 20:26:33 +00:00
|
|
|
inserter_rows = [
|
|
|
|
|
...inserter_rows,
|
|
|
|
|
{
|
|
|
|
|
key: inserter_rows.length,
|
|
|
|
|
data: lazy_data.fields.map(({ field: { presentation } }) =>
|
|
|
|
|
get_empty_datum_for(presentation),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
];
|
2025-08-13 18:52:37 -07:00
|
|
|
}
|
2025-11-02 20:26:33 +00:00
|
|
|
} else {
|
|
|
|
|
datum_editor?.focus();
|
2025-08-13 18:52:37 -07:00
|
|
|
}
|
2025-08-10 14:32:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 20:26:33 +00:00
|
|
|
function handle_restore_focus() {
|
|
|
|
|
focus_cursor?.();
|
2025-08-10 14:32:15 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------- Initial API Fetch -------- //
|
|
|
|
|
|
|
|
|
|
(async function () {
|
2025-08-13 18:52:37 -07:00
|
|
|
const get_data_response_schema = z.object({
|
|
|
|
|
rows: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
pkey: z.string(),
|
2025-09-23 13:08:51 -07:00
|
|
|
data: z.array(datum_schema),
|
2025-08-13 18:52:37 -07:00
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
fields: z.array(field_info_schema),
|
|
|
|
|
});
|
2025-08-10 14:32:15 -07:00
|
|
|
const resp = await fetch("get-data");
|
2025-08-13 18:52:37 -07:00
|
|
|
const body = get_data_response_schema.parse(await resp.json());
|
2025-08-10 14:32:15 -07:00
|
|
|
lazy_data = {
|
|
|
|
|
fields: body.fields,
|
2025-08-13 18:52:37 -07:00
|
|
|
rows: body.rows.map(({ data, pkey }) => ({ data, key: pkey })),
|
2025-08-10 14:32:15 -07:00
|
|
|
};
|
|
|
|
|
inserter_rows = [
|
|
|
|
|
{
|
|
|
|
|
key: 0,
|
2025-09-08 15:56:57 -07:00
|
|
|
data: body.fields.map(({ field: { presentation } }) =>
|
2025-09-23 13:08:51 -07:00
|
|
|
get_empty_datum_for(presentation),
|
2025-08-13 18:52:37 -07:00
|
|
|
),
|
2025-08-10 14:32:15 -07:00
|
|
|
},
|
|
|
|
|
];
|
2025-11-02 20:26:33 +00:00
|
|
|
if (lazy_data.rows.length > 0 && lazy_data.fields.length > 0) {
|
|
|
|
|
set_selections([
|
|
|
|
|
{
|
|
|
|
|
region: "main",
|
|
|
|
|
coords: [0, 0],
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
}
|
2025-08-10 14:32:15 -07:00
|
|
|
})().catch(console.error);
|
2025-10-16 06:40:11 +00:00
|
|
|
|
|
|
|
|
setInterval(tick_delta_queue, 500);
|
2025-08-10 14:32:15 -07:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
{#snippet table_region({
|
|
|
|
|
region_name,
|
|
|
|
|
rows,
|
|
|
|
|
on_cell_click,
|
|
|
|
|
}: {
|
2025-08-13 18:52:37 -07:00
|
|
|
region_name: "main" | "inserter";
|
2025-08-10 14:32:15 -07:00
|
|
|
rows: Row[];
|
|
|
|
|
on_cell_click(ev: MouseEvent, coords: Coords): void;
|
|
|
|
|
})}
|
|
|
|
|
{#if lazy_data}
|
|
|
|
|
{#each rows as row, row_index}
|
|
|
|
|
<div class="lens-table__row" role="row">
|
|
|
|
|
{#each lazy_data.fields as field, field_index}
|
2025-11-02 20:26:33 +00:00
|
|
|
<TableCell
|
|
|
|
|
coords={[row_index, field_index]}
|
|
|
|
|
cursor={selections[0]?.region === region_name &&
|
|
|
|
|
coords_eq(selections[0].coords, [row_index, field_index])}
|
|
|
|
|
{field}
|
|
|
|
|
onbecomecursor={(focus) => {
|
|
|
|
|
focus_cursor = focus;
|
2025-10-16 06:40:11 +00:00
|
|
|
}}
|
2025-11-02 20:26:33 +00:00
|
|
|
ondblclick={() => datum_editor?.focus()}
|
|
|
|
|
onkeydown={(ev) => handle_table_keydown(ev)}
|
|
|
|
|
onmousedown={on_cell_click}
|
|
|
|
|
selected={selections.some(
|
|
|
|
|
(sel) =>
|
|
|
|
|
sel.region === region_name &&
|
|
|
|
|
coords_eq(sel.coords, [row_index, field_index]),
|
|
|
|
|
)}
|
|
|
|
|
table_region={region_name}
|
|
|
|
|
value={row.data[field_index]}
|
|
|
|
|
/>
|
2025-08-10 14:32:15 -07:00
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
{/each}
|
|
|
|
|
{/if}
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
<div class="lens-grid">
|
|
|
|
|
{#if lazy_data}
|
2025-11-02 20:26:33 +00:00
|
|
|
<div class="lens-table" role="grid">
|
2025-08-10 14:32:15 -07:00
|
|
|
<div class={["lens-table__headers"]}>
|
2025-09-08 15:56:57 -07:00
|
|
|
{#each lazy_data.fields as _, field_index}
|
|
|
|
|
<FieldHeader
|
|
|
|
|
bind:field={lazy_data.fields[field_index]}
|
|
|
|
|
index={field_index}
|
|
|
|
|
/>
|
2025-08-10 14:32:15 -07:00
|
|
|
{/each}
|
2025-09-08 15:56:57 -07:00
|
|
|
<div class="lens-table__header-actions">
|
2025-10-07 06:23:50 +00:00
|
|
|
<FieldAdder {columns}></FieldAdder>
|
2025-09-08 15:56:57 -07:00
|
|
|
</div>
|
2025-08-10 14:32:15 -07:00
|
|
|
</div>
|
|
|
|
|
<div class="lens-table__main">
|
|
|
|
|
{@render table_region({
|
|
|
|
|
region_name: "main",
|
|
|
|
|
rows: lazy_data.rows,
|
2025-11-02 20:26:33 +00:00
|
|
|
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 = "";
|
|
|
|
|
} else {
|
|
|
|
|
set_selections([{ region: "main", coords }]);
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-08-10 14:32:15 -07:00
|
|
|
})}
|
|
|
|
|
</div>
|
2025-08-13 18:52:37 -07:00
|
|
|
<form method="post" action="insert">
|
2025-11-02 20:26:33 +00:00
|
|
|
<div class="lens-inserter">
|
|
|
|
|
<h3 class="lens-inserter__help">
|
|
|
|
|
Insert rows — press "shift + enter" to jump here or add a row
|
|
|
|
|
</h3>
|
|
|
|
|
<div class="lens-inserter__main">
|
|
|
|
|
<div class="lens-inserter__rows">
|
|
|
|
|
{@render table_region({
|
|
|
|
|
region_name: "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 = "";
|
|
|
|
|
} else {
|
|
|
|
|
set_selections([{ region: "inserter", coords }]);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
aria-label="Insert rows"
|
|
|
|
|
class="lens-inserter__submit"
|
|
|
|
|
onkeydown={(ev) => {
|
|
|
|
|
// Prevent keypress (e.g. pressing Enter on the button to submit
|
|
|
|
|
// it) from triggering a table interaction.
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
}}
|
|
|
|
|
title="Insert rows"
|
|
|
|
|
type="submit"
|
|
|
|
|
>
|
|
|
|
|
<i class="ti ti-upload"></i>
|
|
|
|
|
</button>
|
2025-08-13 18:52:37 -07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{#each inserter_rows as row}
|
|
|
|
|
{#each lazy_data.fields as field, field_index}
|
|
|
|
|
<input
|
|
|
|
|
type="hidden"
|
|
|
|
|
name={field.field.name}
|
|
|
|
|
value={JSON.stringify(row.data[field_index])}
|
|
|
|
|
/>
|
|
|
|
|
{/each}
|
|
|
|
|
{/each}
|
|
|
|
|
</form>
|
2025-08-10 14:32:15 -07:00
|
|
|
</div>
|
2025-10-16 06:40:11 +00:00
|
|
|
<div class="table-viewer__datum-editor">
|
|
|
|
|
{#if selections.length === 1}
|
2025-09-23 13:08:51 -07:00
|
|
|
<DatumEditor
|
2025-10-16 06:40:11 +00:00
|
|
|
bind:this={datum_editor}
|
|
|
|
|
bind:value={editor_value}
|
2025-08-24 23:24:01 -07:00
|
|
|
field_info={lazy_data.fields[selections[0].coords[1]]}
|
2025-11-02 20:26:33 +00:00
|
|
|
on_blur={() => try_queue_delta()}
|
|
|
|
|
on_cancel_edit={cancel_edit}
|
2025-10-16 06:40:11 +00:00
|
|
|
on_change={() => {
|
|
|
|
|
try_sync_edit_to_cells();
|
|
|
|
|
}}
|
2025-11-02 20:26:33 +00:00
|
|
|
on_restore_focus={handle_restore_focus}
|
2025-08-24 23:24:01 -07:00
|
|
|
/>
|
|
|
|
|
{/if}
|
2025-08-10 14:32:15 -07:00
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|