feat(frontend): keyboard range-select with Shift+Space / Shift+x
Plain Space/x toggles the focused card and drops a range anchor there; Shift+Space / Shift+x now selects everything from that anchor to the focused card, sharing the same anchor (lastSelectedIdx) as Shift+click so mouse and keyboard range-selection are interchangeable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@
|
|||||||
['↑ ↓ ← →', 'Move focus between files'],
|
['↑ ↓ ← →', 'Move focus between files'],
|
||||||
['Enter', 'Open the focused file'],
|
['Enter', 'Open the focused file'],
|
||||||
['Space / x', 'Select / deselect'],
|
['Space / x', 'Select / deselect'],
|
||||||
|
['Shift+Space / Shift+x', 'Select a range from the anchor'],
|
||||||
['e', 'Edit tags (focus the tag filter)'],
|
['e', 'Edit tags (focus the tag filter)'],
|
||||||
['p', 'Add to pool'],
|
['p', 'Add to pool'],
|
||||||
['Del', 'Move to trash'],
|
['Del', 'Move to trash'],
|
||||||
|
|||||||
@@ -86,6 +86,24 @@
|
|||||||
if (f?.id && !$selectionStore.ids.has(f.id)) selectionStore.select(f.id);
|
if (f?.id && !$selectionStore.ids.has(f.id)) selectionStore.select(f.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select via the keyboard: a plain press toggles the focused card and drops the
|
||||||
|
// range anchor there; a Shift press selects everything from the anchor to the
|
||||||
|
// focused card — the same model as Shift+click on the grid.
|
||||||
|
function selectFocused(range: boolean) {
|
||||||
|
const idx = focusedId ? files.findIndex((f) => f.id === focusedId) : -1;
|
||||||
|
if (idx < 0) return;
|
||||||
|
if (range && lastSelectedIdx !== null) {
|
||||||
|
const from = Math.min(lastSelectedIdx, idx);
|
||||||
|
const to = Math.max(lastSelectedIdx, idx);
|
||||||
|
for (let i = from; i <= to; i++) {
|
||||||
|
if (files[i]?.id) selectionStore.select(files[i].id!);
|
||||||
|
}
|
||||||
|
} else if (files[idx]?.id) {
|
||||||
|
selectionStore.toggle(files[idx].id!);
|
||||||
|
}
|
||||||
|
lastSelectedIdx = idx;
|
||||||
|
}
|
||||||
|
|
||||||
function openTagEditor() {
|
function openTagEditor() {
|
||||||
tagEditorOpen = true;
|
tagEditorOpen = true;
|
||||||
void tick().then(() => document.querySelector<HTMLInputElement>('.tag-sheet input')?.focus());
|
void tick().then(() => document.querySelector<HTMLInputElement>('.tag-sheet input')?.focus());
|
||||||
@@ -138,14 +156,10 @@
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case ' ': {
|
case ' ':
|
||||||
const f = focusedFile();
|
|
||||||
if (f?.id) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
selectionStore.toggle(f.id);
|
selectFocused(e.shiftKey);
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
case 'Delete':
|
case 'Delete':
|
||||||
if ($selectionActive || focusedFile()) {
|
if ($selectionActive || focusedFile()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -155,18 +169,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Letter / symbol commands matched by physical position, so they fire the
|
// Select by position (x), Shift = range — handled before the unshifted-only
|
||||||
// same on a non-Latin layout.
|
// guard below because Shift+x is a valid range-select.
|
||||||
|
if (e.code === 'KeyX') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectFocused(e.shiftKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The remaining letter / symbol commands are unshifted-only, matched by
|
||||||
|
// physical position so they fire the same on a non-Latin layout.
|
||||||
if (e.shiftKey) return;
|
if (e.shiftKey) return;
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case 'KeyX': {
|
|
||||||
const f = focusedFile();
|
|
||||||
if (f?.id) {
|
|
||||||
e.preventDefault();
|
|
||||||
selectionStore.toggle(f.id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'KeyE':
|
case 'KeyE':
|
||||||
if ($selectionActive || focusedFile()) {
|
if ($selectionActive || focusedFile()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
Reference in New Issue
Block a user