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:
@@ -71,6 +71,17 @@ umbenennen, löschen, per Drag sortieren). Zoom 40–200 % pro Blatt
|
|||||||
Bewegungen und das Verlassen der Tabelle, scrollt bei festen Tabellen an der
|
Bewegungen und das Verlassen der Tabelle, scrollt bei festen Tabellen an der
|
||||||
Kante automatisch mit und heilt einen verlorenen `mouseup` selbst.
|
Kante automatisch mit und heilt einen verlorenen `mouseup` selbst.
|
||||||
- Kopf-Klick = ganze Spalte / Zeile, Ecke = alles.
|
- Kopf-Klick = ganze Spalte / Zeile, Ecke = alles.
|
||||||
|
- **`Strg`/`Cmd`+`A`** (zweistufig, Excel-Verhalten): der **erste** Druck
|
||||||
|
markiert den **zusammenhängenden Bereich** um die aktive Zelle — iterative
|
||||||
|
Rechteck-Expansion, die an einer vollständig leeren Zeile/Spalte oder am
|
||||||
|
Tabellenrand stoppt (zwei durch eine leere Spalte/Zeile getrennte
|
||||||
|
Dateninseln bleiben getrennt). Ein **zweiter, unmittelbar folgender** Druck
|
||||||
|
erweitert auf **alle befüllten Zellen der Tabelle** — die tatsächlichen
|
||||||
|
Grenzen befüllter Felder, nicht die (oft größeren) Grunddimensionen der
|
||||||
|
Tabelle. Jeder weitere Druck bleibt auf dieser Vollauswahl stehen. Der
|
||||||
|
Zyklus erkennt sich selbst rein über einen Vergleich der aktuellen Auswahl
|
||||||
|
mit der beim letzten Druck gesetzten — jede andere Aktion dazwischen (Klick,
|
||||||
|
Pfeiltaste, Bearbeitung, …) setzt ihn auf Stufe 1 zurück, ganz wie in Excel.
|
||||||
- **`Strg`/`Cmd`+Klick** = weitere Zelle **hinzufügen** (erneut = abwählen),
|
- **`Strg`/`Cmd`+Klick** = weitere Zelle **hinzufügen** (erneut = abwählen),
|
||||||
**`Strg`/`Cmd`+Ziehen** = weiteres Rechteck; auch am Spalten-/Zeilenkopf.
|
**`Strg`/`Cmd`+Ziehen** = weiteres Rechteck; auch am Spalten-/Zeilenkopf.
|
||||||
- Die **diskontinuierliche Mehrfachauswahl** trägt alle folgenden Aktionen:
|
- Die **diskontinuierliche Mehrfachauswahl** trägt alle folgenden Aktionen:
|
||||||
@@ -109,6 +120,7 @@ umbenennen, löschen, per Drag sortieren). Zoom 40–200 % pro Blatt
|
|||||||
| Tippen / `F2` | Zelle editieren; Enter/Tab committen und ziehen weiter (auch im Bereich) |
|
| Tippen / `F2` | Zelle editieren; Enter/Tab committen und ziehen weiter (auch im Bereich) |
|
||||||
| `Pos1` / `Strg`+`Pos1` / `Ende` / `Strg`+`Ende` | Zeilenanfang / A1 / Zeilenende / letzte Zelle |
|
| `Pos1` / `Strg`+`Pos1` / `Ende` / `Strg`+`Ende` | Zeilenanfang / A1 / Zeilenende / letzte Zelle |
|
||||||
| `Bild↑` / `Bild↓` | seitenweise (bei festen Tabellen nach Viewport-Höhe) |
|
| `Bild↑` / `Bild↓` | seitenweise (bei festen Tabellen nach Viewport-Höhe) |
|
||||||
|
| **`Strg`/`Cmd`+`A`** | 1. Druck: zusammenhängender Bereich um die aktive Zelle · 2. Druck: alle befüllten Zellen der Tabelle (s. o. „Zellauswahl") |
|
||||||
| **`Strg`/`Cmd`+`C`/`X`/`V`** | Kopieren / Ausschneiden / Einfügen (Zellen + Formatierung, bezugsangepasst) |
|
| **`Strg`/`Cmd`+`C`/`X`/`V`** | Kopieren / Ausschneiden / Einfügen (Zellen + Formatierung, bezugsangepasst) |
|
||||||
| **`Strg`/`Cmd`+`Z`** · **`Strg`/`Cmd`+`Y`** / **`Strg`/`Cmd`+`Shift`+`Z`** | Rückgängig · Wiederholen (auch als Symbolleisten-Buttons ↶ ↷) |
|
| **`Strg`/`Cmd`+`Z`** · **`Strg`/`Cmd`+`Y`** / **`Strg`/`Cmd`+`Shift`+`Z`** | Rückgängig · Wiederholen (auch als Symbolleisten-Buttons ↶ ↷) |
|
||||||
| **`F4`** (im Editor/in der Formelleiste) | Bezug am Cursor sperren/zyklisch umschalten: `A1` → `$A$1` → `A$1` → `$A1` → `A1`; bei markiertem Text werden **alle** darin gefundenen Bezüge auf einmal umgeschaltet, jeder ausgehend vom eigenen Zustand |
|
| **`F4`** (im Editor/in der Formelleiste) | Bezug am Cursor sperren/zyklisch umschalten: `A1` → `$A$1` → `A$1` → `$A1` → `A1`; bei markiertem Text werden **alle** darin gefundenen Bezüge auf einmal umgeschaltet, jeder ausgehend vom eigenen Zustand |
|
||||||
@@ -1097,6 +1109,12 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
|||||||
vorhanden — die zugrundeliegenden Store-/Engine-Funktionen
|
vorhanden — die zugrundeliegenden Store-/Engine-Funktionen
|
||||||
(`graphPrecedents`/`graphDependents`/`cellGraphNeighbors`/
|
(`graphPrecedents`/`graphDependents`/`cellGraphNeighbors`/
|
||||||
`openDependencyGraph`) sind es aber (s. „Tests" oben).
|
`openDependencyGraph`) sind es aber (s. „Tests" oben).
|
||||||
|
- **`Strg`/`Cmd`+`A`-Zyklus** (s. o. „Zellauswahl") ist rein UI-seitig
|
||||||
|
(`TableGrid.vue`) implementiert, ohne eigene Unit-Tests — wie bei
|
||||||
|
Tabellen-Größenänderung und Abhängigkeiten-Panel oben nur live per
|
||||||
|
Playwright verifiziert (Stufe 1 auf eine von zwei durch eine leere Spalte
|
||||||
|
getrennten Dateninseln, Eskalation auf Stufe 2, Verharren auf Stufe 2 bei
|
||||||
|
weiteren Drücken, Rücksetzen des Zyklus durch einen Klick dazwischen).
|
||||||
- **Objekt-Bezüge beim Anhängen.** Beim JSON-*Anhängen* zweier Dokumente werden
|
- **Objekt-Bezüge beim Anhängen.** Beim JSON-*Anhängen* zweier Dokumente werden
|
||||||
nur Blatt-/Objekt-IDs neu vergeben, nicht Tabellennamen. Kollidiert ein
|
nur Blatt-/Objekt-IDs neu vergeben, nicht Tabellennamen. Kollidiert ein
|
||||||
angehängter Tabellenname mit einem bestehenden, gewinnt bei der Auflösung der
|
angehängter Tabellenname mit einem bestehenden, gewinnt bei der Auflösung der
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
|
||||||
import { numberToCol, cycleReferenceLocks } from '../engine';
|
import { numberToCol, cycleReferenceLocks } from '../engine';
|
||||||
import { useDocument } from '../store/document';
|
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 { useFormulaAutocomplete } from '../composables/useFormulaAutocomplete';
|
||||||
import ContextMenu from './ContextMenu.vue';
|
import ContextMenu from './ContextMenu.vue';
|
||||||
import FormulaSuggest from './FormulaSuggest.vue';
|
import FormulaSuggest from './FormulaSuggest.vue';
|
||||||
@@ -620,6 +620,93 @@ function selectAll() {
|
|||||||
focusRoot();
|
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 ---------------------------------------------------
|
// ---- editing ---------------------------------------------------
|
||||||
// The editor overlays the cell (positioned via colLeft/rowTop, like the
|
// 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
|
// clip/fill overlays) rather than living inside the <td>, so it's free to
|
||||||
@@ -856,6 +943,11 @@ function onKeydown(e: KeyboardEvent) {
|
|||||||
|
|
||||||
if (mod) {
|
if (mod) {
|
||||||
const k = e.key.toLowerCase();
|
const k = e.key.toLowerCase();
|
||||||
|
if (k === 'a') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return selectAllCycle();
|
||||||
|
}
|
||||||
if (k === 'c') {
|
if (k === 'c') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
Reference in New Issue
Block a user