rudimentary support for multiple selections

This commit is contained in:
Brent Schroeter 2025-11-06 00:41:31 +00:00
parent d934d4ad90
commit 52c014e53e
2 changed files with 168 additions and 67 deletions

View file

@ -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;
}

View file

@ -101,6 +101,13 @@
}
}
function selections_eq(
a: Omit<Selection, "original_value">,
b: Omit<Selection, "original_value">,
): 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<Selection, "original_value"> | 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([
{
if (cursor.region === "main") {
if (cursor.coords[0] < lazy_data.rows.length - 1) {
new_cursor = {
region: "main",
coords: [last_selection.coords[0] + 1, last_selection.coords[1]],
},
]);
coords: [cursor.coords[0] + 1, cursor.coords[1]],
};
} else {
// At bottom of main table.
set_selections([
{
new_cursor = {
region: "inserter",
coords: [0, last_selection.coords[1]],
},
]);
coords: [0, cursor.coords[1]],
};
}
} else if (last_selection.region === "inserter") {
if (last_selection.coords[0] < inserter_rows.length - 1) {
set_selections([
{
} else if (cursor.region === "inserter") {
if (cursor.coords[0] < inserter_rows.length - 1) {
new_cursor = {
region: "inserter",
coords: [last_selection.coords[0] + 1, last_selection.coords[1]],
},
]);
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([
{
if (cursor.region === "main") {
if (cursor.coords[0] > 0) {
new_cursor = {
region: "main",
coords: [last_selection.coords[0] - 1, last_selection.coords[1]],
},
]);
coords: [cursor.coords[0] - 1, cursor.coords[1]],
};
}
} else if (last_selection.region === "inserter") {
if (last_selection.coords[0] > 0) {
set_selections([
{
} else if (cursor.region === "inserter") {
if (cursor.coords[0] > 0) {
new_cursor = {
region: "inserter",
coords: [last_selection.coords[0] - 1, last_selection.coords[1]],
},
]);
coords: [cursor.coords[0] - 1, cursor.coords[1]],
};
} else {
// At top of inserter table.
set_selections([
{
new_cursor = {
region: "main",
coords: [lazy_data.rows.length - 1, last_selection.coords[1]],
},
]);
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<Selection, "original_value">[] = [];
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}
<div class="lens-grid">
<div
class="lens-grid"
onpaste={(ev) => {
console.log("paste");
console.log(ev.clipboardData?.getData("text"));
}}
>
{#if lazy_data}
<div class="lens-table" role="grid">
<div class={["lens-table__headers"]}>