Ctrl/Cmd+A: zweistufige Auswahl (zusammenhängender Bereich, dann alles Befüllte)
Erster Druck markiert den zusammenhängenden Bereich um die aktive Zelle (Excel-Verhalten, iterative Rechteck-Expansion bis zur ersten leeren Zeile/ Spalte oder dem Tabellenrand); ein zweiter, unmittelbar folgender Druck erweitert auf alle befüllten Zellen der Tabelle. Der Zyklus erkennt sich selbst über einen Abgleich der aktuellen Auswahl mit der zuletzt gesetzten und setzt sich bei jeder anderen Aktion dazwischen zurück. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
|
||||
import { numberToCol, cycleReferenceLocks } from '../engine';
|
||||
import { useDocument } from '../store/document';
|
||||
import { DEFAULT_ROW_H } from '../store/types';
|
||||
import { DEFAULT_ROW_H, type SelectionState } from '../store/types';
|
||||
import { useFormulaAutocomplete } from '../composables/useFormulaAutocomplete';
|
||||
import ContextMenu from './ContextMenu.vue';
|
||||
import FormulaSuggest from './FormulaSuggest.vue';
|
||||
@@ -620,6 +620,93 @@ function selectAll() {
|
||||
focusRoot();
|
||||
}
|
||||
|
||||
// ---- Ctrl/Cmd+A: two-stage "select current region, then everything" ------
|
||||
// Same idea as Excel: the first press selects the contiguous block of
|
||||
// filled cells around the active cell (bounded by a fully-empty row/column
|
||||
// or the table edge); a *second, immediately-following* press escalates to
|
||||
// every filled cell in the whole table. Detected by comparing the current
|
||||
// selection to whatever the previous Ctrl+A press actually set -- if
|
||||
// anything else changed the selection in between (a click, an arrow key, an
|
||||
// edit, ...), the next press starts over at stage one, exactly like Excel
|
||||
// forgets the cycle the moment you do something else.
|
||||
function isCellEmpty(t: { cells: Record<string, string> }, c: number, r: number): boolean {
|
||||
return !(`${c},${r}` in t.cells);
|
||||
}
|
||||
function isRowEmpty(t: { cells: Record<string, string> }, r: number, c0: number, c1: number): boolean {
|
||||
for (let c = c0; c <= c1; c++) if (!isCellEmpty(t, c, r)) return false;
|
||||
return true;
|
||||
}
|
||||
function isColEmpty(t: { cells: Record<string, string> }, c: number, r0: number, r1: number): boolean {
|
||||
for (let r = r0; r <= r1; r++) if (!isCellEmpty(t, c, r)) return false;
|
||||
return true;
|
||||
}
|
||||
/** Excel's "current region": the maximal rectangle around (`ac`,`ar`) such
|
||||
* that expanding it any further in any direction would immediately cross
|
||||
* a row/column that is *completely* empty (or the table's own edge). */
|
||||
function currentRegion(t: { cells: Record<string, string>; rows: number; cols: number }, ac: number, ar: number) {
|
||||
let c0 = ac, c1 = ac, r0 = ar, r1 = ar;
|
||||
let grew = true;
|
||||
while (grew) {
|
||||
grew = false;
|
||||
if (r0 > 0 && !isRowEmpty(t, r0 - 1, c0, c1)) { r0--; grew = true; }
|
||||
if (r1 < t.rows - 1 && !isRowEmpty(t, r1 + 1, c0, c1)) { r1++; grew = true; }
|
||||
if (c0 > 0 && !isColEmpty(t, c0 - 1, r0, r1)) { c0--; grew = true; }
|
||||
if (c1 < t.cols - 1 && !isColEmpty(t, c1 + 1, r0, r1)) { c1++; grew = true; }
|
||||
}
|
||||
return { c0, r0, c1, r1 };
|
||||
}
|
||||
/** Bounding box of every filled cell in the whole table, or `null` if it's
|
||||
* entirely empty. */
|
||||
function filledBounds(t: { cells: Record<string, string> }) {
|
||||
let c0 = Infinity, c1 = -Infinity, r0 = Infinity, r1 = -Infinity;
|
||||
for (const key of Object.keys(t.cells)) {
|
||||
const [c, r] = key.split(',').map(Number);
|
||||
if (c < c0) c0 = c;
|
||||
if (c > c1) c1 = c;
|
||||
if (r < r0) r0 = r;
|
||||
if (r > r1) r1 = r;
|
||||
}
|
||||
return r1 === -Infinity ? null : { c0, r0, c1, r1 };
|
||||
}
|
||||
type Rect4 = { c0: number; r0: number; c1: number; r1: number };
|
||||
/** The rect the *previous* Ctrl/Cmd+A press set (so the next press can tell
|
||||
* whether it's a genuine second press, selection unchanged since) and
|
||||
* which stage it represents. */
|
||||
let ctrlARect: Rect4 | null = null;
|
||||
let ctrlAStage: 1 | 2 = 1;
|
||||
function rectMatchesSelection(rect: Rect4, sel: SelectionState | null): boolean {
|
||||
if (!sel || sel.ranges?.length) return false; // a multi-range selection never came from this cycle
|
||||
const c0 = Math.min(sel.anchor.col, sel.focus.col);
|
||||
const c1 = Math.max(sel.anchor.col, sel.focus.col);
|
||||
const r0 = Math.min(sel.anchor.row, sel.focus.row);
|
||||
const r1 = Math.max(sel.anchor.row, sel.focus.row);
|
||||
return c0 === rect.c0 && c1 === rect.c1 && r0 === rect.r0 && r1 === rect.r1;
|
||||
}
|
||||
function selectAllCycle() {
|
||||
const t = table.value;
|
||||
if (!t) return;
|
||||
clearFlow();
|
||||
const stillAtLastPress = !!ctrlARect && rectMatchesSelection(ctrlARect, store.doc.selection);
|
||||
let target: Rect4;
|
||||
if (stillAtLastPress && ctrlAStage === 1) {
|
||||
target = filledBounds(t) ?? { c0: 0, r0: 0, c1: t.cols - 1, r1: t.rows - 1 };
|
||||
ctrlAStage = 2;
|
||||
} else if (stillAtLastPress && ctrlAStage === 2) {
|
||||
target = ctrlARect!; // already at "everything" -- stays there, like Excel
|
||||
} else {
|
||||
const anchor = mine.value?.focus ?? { col: 0, row: 0 };
|
||||
target = currentRegion(t, anchor.col, anchor.row);
|
||||
ctrlAStage = 1;
|
||||
}
|
||||
store.setSelection({
|
||||
tableId: props.tableId,
|
||||
anchor: { col: target.c0, row: target.r0 },
|
||||
focus: { col: target.c1, row: target.r1 },
|
||||
});
|
||||
ctrlARect = target;
|
||||
focusRoot();
|
||||
}
|
||||
|
||||
// ---- editing ---------------------------------------------------
|
||||
// The editor overlays the cell (positioned via colLeft/rowTop, like the
|
||||
// clip/fill overlays) rather than living inside the <td>, so it's free to
|
||||
@@ -856,6 +943,11 @@ function onKeydown(e: KeyboardEvent) {
|
||||
|
||||
if (mod) {
|
||||
const k = e.key.toLowerCase();
|
||||
if (k === 'a') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return selectAllCycle();
|
||||
}
|
||||
if (k === 'c') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
Reference in New Issue
Block a user