From 52c014e53ee4f9b850c54111f251718c355eee6e Mon Sep 17 00:00:00 2001 From: Brent Schroeter Date: Thu, 6 Nov 2025 00:41:31 +0000 Subject: [PATCH] rudimentary support for multiple selections --- sass/viewer.scss | 4 +- svelte/src/table-viewer.webc.svelte | 231 ++++++++++++++++++++-------- 2 files changed, 168 insertions(+), 67 deletions(-) diff --git a/sass/viewer.scss b/sass/viewer.scss index 028a35e..f0b6bac 100644 --- a/sass/viewer.scss +++ b/sass/viewer.scss @@ -69,11 +69,11 @@ $table-border-color: #ccc; width: 100%; &--selected { - outline: 1px solid #37f; - outline-offset: -1px; + background: #07f3; } &--cursor { + background: transparent; outline: 3px solid #37f; outline-offset: -2px; } diff --git a/svelte/src/table-viewer.webc.svelte b/svelte/src/table-viewer.webc.svelte index d1d4920..78a1ae4 100644 --- a/svelte/src/table-viewer.webc.svelte +++ b/svelte/src/table-viewer.webc.svelte @@ -101,6 +101,13 @@ } } + function selections_eq( + a: Omit, + b: Omit, + ): boolean { + return a.region === b.region && coords_eq(a.coords, b.coords); + } + function update_field_ordinality({ field_index, beyond_index, @@ -188,87 +195,166 @@ } } - function move_selection(direction: "Down" | "Left" | "Right" | "Up") { + function move_cursor( + direction: "Down" | "Left" | "Right" | "Up", + { additive }: { additive?: boolean } = {}, + ) { if (!lazy_data || selections.length === 0) { console.warn("move_selection() preconditions not met"); return; } - const last_selection = selections[selections.length - 1]; + const cursor = selections[0]; + let new_cursor: Omit | undefined; if ( direction === "Right" && - last_selection.coords[1] < lazy_data.fields.length - 1 + cursor.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], - }, - ]); + new_cursor = { + region: cursor.region, + coords: [cursor.coords[0], cursor.coords[1] + 1], + }; + } else if (direction === "Left" && cursor.coords[1] > 0) { + new_cursor = { + region: cursor.region, + coords: [cursor.coords[0], cursor.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]], - }, - ]); + if (cursor.region === "main") { + if (cursor.coords[0] < lazy_data.rows.length - 1) { + new_cursor = { + region: "main", + coords: [cursor.coords[0] + 1, cursor.coords[1]], + }; } else { // At bottom of main table. - set_selections([ - { - region: "inserter", - coords: [0, last_selection.coords[1]], - }, - ]); + new_cursor = { + region: "inserter", + coords: [0, cursor.coords[1]], + }; } - } 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 (cursor.region === "inserter") { + if (cursor.coords[0] < inserter_rows.length - 1) { + new_cursor = { + region: "inserter", + coords: [cursor.coords[0] + 1, cursor.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]], - }, - ]); + if (cursor.region === "main") { + if (cursor.coords[0] > 0) { + new_cursor = { + region: "main", + coords: [cursor.coords[0] - 1, cursor.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 if (cursor.region === "inserter") { + if (cursor.coords[0] > 0) { + new_cursor = { + region: "inserter", + coords: [cursor.coords[0] - 1, cursor.coords[1]], + }; } else { // At top of inserter table. - set_selections([ - { - region: "main", - coords: [lazy_data.rows.length - 1, last_selection.coords[1]], - }, - ]); + new_cursor = { + region: "main", + coords: [lazy_data.rows.length - 1, cursor.coords[1]], + }; } } } + if (new_cursor !== undefined) { + const first_selection = selections[selections.length - 1]; + if ( + additive && + first_selection !== undefined && + !selections_eq(new_cursor, first_selection) + ) { + // By convention, we keep the first selected cell at the end of the + // selections array, and the current cursor at the beginning. Everything + // in the bounded box should be populated in between. + const all_selections: Omit[] = []; + const left_idx = Math.min( + new_cursor.coords[1], + first_selection.coords[1], + ); + const right_idx = Math.max( + new_cursor.coords[1], + first_selection.coords[1], + ); + if (new_cursor.region === first_selection.region) { + for ( + let row_idx = Math.min( + new_cursor.coords[0], + first_selection.coords[0], + ); + row_idx <= + Math.max(new_cursor.coords[0], first_selection.coords[0]); + row_idx += 1 + ) { + for ( + let field_idx = left_idx; + field_idx <= right_idx; + field_idx += 1 + ) { + all_selections.push({ + region: new_cursor.region, + coords: [row_idx, field_idx], + }); + } + } + } else { + const main_sel = + new_cursor.region === "main" ? new_cursor : first_selection; + const inserter_sel = + new_cursor.region === "inserter" ? new_cursor : first_selection; + for ( + let row_idx = main_sel.coords[0]; + row_idx < lazy_data.rows.length; + row_idx += 1 + ) { + for ( + let field_idx = left_idx; + field_idx <= right_idx; + field_idx += 1 + ) { + all_selections.push({ + region: "main", + coords: [row_idx, field_idx], + }); + } + } + for ( + let row_idx = 0; + row_idx <= inserter_sel.coords[0]; + row_idx += 1 + ) { + for ( + let field_idx = left_idx; + field_idx <= right_idx; + field_idx += 1 + ) { + all_selections.push({ + region: "inserter", + coords: [row_idx, field_idx], + }); + } + } + } + set_selections([ + new_cursor, + ...all_selections.filter( + (sel) => + !selections_eq(sel, new_cursor) && + !selections_eq(sel, first_selection), + ), + first_selection, + ]); + } else { + set_selections([new_cursor]); + } + } } function try_sync_edit_to_cells() { @@ -375,16 +461,25 @@ // -------- Event Handlers -------- // - function handle_table_keydown(ev: KeyboardEvent) { + function handle_cell_keydown(ev: KeyboardEvent) { 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)) { + if (ev.shiftKey) { + move_cursor(arrow_direction, { additive: true }); + } else { + move_cursor(arrow_direction); + } + } else if ( + !ev.altKey && + !ev.ctrlKey && + !ev.metaKey && + /^[a-zA-Z0-9`~!@#$%^&*()_=+[\]{}\\|;:'",<.>/?-]$/.test(ev.key) + ) { const sel = selections[0]; if (sel) { editor_value = get_empty_datum_for( @@ -483,7 +578,7 @@ focus_cursor = focus; }} ondblclick={() => datum_editor?.focus()} - onkeydown={(ev) => handle_table_keydown(ev)} + onkeydown={(ev) => handle_cell_keydown(ev)} onmousedown={on_cell_click} selected={selections.some( (sel) => @@ -499,7 +594,13 @@ {/if} {/snippet} -
+
{ + console.log("paste"); + console.log(ev.clipboardData?.getData("text")); + }} +> {#if lazy_data}