From 5a06d08a6ad1ba1875b70d7897df515854deb10f Mon Sep 17 00:00:00 2001 From: Wild Karl-Heinz Date: Wed, 2 Sep 2026 14:19:43 +0200 Subject: [PATCH] Implement document-wide undo/redo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Snapshot-based, like the existing JSON export/import round-trip (snapshot()/replaceDocument()) rather than a patch/command log -- simple and provably correct for a document this size. - document.ts: a deep watch on `doc` tracks a "content fingerprint" (sheets + locale + csvDelimiter, deliberately excluding selection/activeObjectId, so moving the cursor is never an undo step) and debounces commits by 150ms -- long enough to coalesce a resize-drag's per-mousemove calls into one step, short enough that two genuinely separate quick actions don't merge. A new edit after an undo immediately drops the redo branch (not debounced, so a bound button is never briefly wrong). undo()/redo() flush any pending change first, then pop/apply a snapshot via a new applyDocumentState() core that replaceDocument() also uses -- replaceDocument() (JSON import "replace", "reset to seed") additionally resets history, since loading a different document shouldn't leave the old one undoable into it; undo/redo themselves must not (that's the very stack they're unwinding). - TableGrid.vue: Strg/Cmd+Z undoes, Strg/Cmd+Y or +Shift+Z redoes, deferring to a cell editor's native undo while one is open (the existing `if (editing.value) return` guard already covers this). - App.vue: a window-level fallback for when no table has focus, skipped whenever focus sits in any text field so their own native undo keeps working. - AppToolbar.vue: ↶/↷ buttons, disabled via store.canUndo/canRedo. Verified interactively against the dev server with Playwright: the toolbar button enables the instant a change happens (not after the debounce); separately-timed edits each undo as their own step; a multi-tick resize drag undoes in one step back to the original width; Ctrl+Z, Ctrl+Y and the toolbar buttons all work. Co-Authored-By: Claude Sonnet 5 --- README.md | 27 ++++- src/App.vue | 26 ++++- src/components/AppToolbar.vue | 23 +++++ src/components/TableGrid.vue | 10 ++ src/store/__tests__/document.spec.ts | 125 ++++++++++++++++++++++- src/store/document.ts | 145 ++++++++++++++++++++++++++- 6 files changed, 351 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index da7b1db..5c7b79c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Vorbild von **Apple Numbers**. Zwei Dinge stehen im Mittelpunkt: ```bash npm install npm run dev # Vite-Devserver -npm run test # Vitest-Suite (Engine + Store + IO), 132 Tests +npm run test # Vitest-Suite (Engine + Store + IO), 138 Tests npm run build # Typecheck + Produktionsbuild npm run lint # ESLint (flat config) ``` @@ -97,6 +97,7 @@ umbenennen, löschen, per Drag sortieren). Zoom 40–200 % pro Blatt | `Pos1` / `Strg`+`Pos1` / `Ende` / `Strg`+`Ende` | Zeilenanfang / A1 / Zeilenende / letzte Zelle | | `Bild↑` / `Bild↓` | seitenweise (bei festen Tabellen nach Viewport-Höhe) | | **`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 ↶ ↷) | | **`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 | | **`Alt`+`Enter`** bzw. **`Cmd`+`Enter`** (im Editor) | Zeilenumbruch in der Zelle einfügen, statt zu committen — `Cmd`+`Enter` als Mac-Fallback, da `⌥` (Option) dort eine Compose-Taste ist und nicht überall zuverlässig als reines Modifier ankommt | @@ -104,6 +105,23 @@ Bei **festen (scrollbaren) Tabellen** wird die aktive Zelle bei jeder Navigation automatisch in den sichtbaren Bereich gescrollt (unter Beachtung fixierter Zeilen/Spalten). +**Rückgängig/Wiederholen**: dokumentweiter Undo-Stack (Snapshot-basiert, wie +der bereits vorhandene JSON-Export/Import-Round-Trip — einfach und +nachweisbar korrekt für ein Dokument dieser Größe, statt eines Patch-/ +Kommando-Logs). Reine Auswahl-/Cursor-Bewegungen zählen nie als Schritt; eine +schnelle Folge von Mutationen (ein Spalten-/Zeilen-Resize-Drag feuert bei +jeder Mausbewegung) wird zu **einem** Schritt zusammengefasst, sofern sie +innerhalb von 150 ms aufeinanderfolgen — knapp unter normalem Tipp-/Klick- +Tempo, damit zwei tatsächlich getrennte schnelle Aktionen nicht verschmelzen, +aber weit über dem Abstand aufeinanderfolgender Drag-Ereignisse (~16–30 ms). +Ein neuer Bearbeitungsschritt verwirft sofort einen etwaigen Wiederholen-Ast. +„Auf Beispiel zurücksetzen" und JSON-Import „Ersetzen" starten eine **neue** +Historie (das vorherige Dokument ist danach nicht mehr zurückholbar), Undo/ +Redo selbst tun das nicht. Symbolleiste zeigt zwei Buttons (↶ ↷, deaktiviert +wenn nichts zu tun ist); greift auch ohne Tabellenfokus, außer während eine +Texteingabe (Zell-Editor, Formelleiste, Blatt-Umbenennen, …) den nativen +Undo dieses Feldes benutzen soll. + **In-Zellen-Editor**: das Eingabefeld liegt als Overlay über der Zelle (nicht in ihr) und wächst mit dem Inhalt — in der Breite bis zu einer angenäherten „sichtbaren Anzeigebreite", danach umbrechend; in der Höhe mit `Alt`+`Enter` @@ -330,7 +348,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, ## Tests -`npm run test` — Vitest, 132 Tests: +`npm run test` — Vitest, 138 Tests: - **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^` rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`, @@ -382,6 +400,11 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, ein mehrzeiliger/-spaltiger Quellbereich wiederholt sich zyklisch, auch bei einer unvollständigen letzten Wiederholung; füllt ebenso hoch/links; Formatierung wird mitkopiert. +- **Rückgängig/Wiederholen** — Einzel-Edit rückgäng-/wiederholbar; mehrere + Edits schrittweise in der richtigen Reihenfolge zurück; ein neuer Edit nach + einem Undo verwirft den Wiederholen-Ast; reine Auswahländerungen sind nie + ein Schritt; deckt Kopieren/Einfügen, Ausfüllen und Formatierung ab (nicht + nur `setCellInput`); „Auf Beispiel zurücksetzen" beginnt eine neue Historie. - **IO** — JSON verlustfreier Round-Trip (identisch), defekte/leere Datei → Fehler ohne Nebenwirkung, unbekannter Tabellenname → `#REF`, v1→v2 beim Import; CSV Übersicht-Export→Re-Import gleiche Zahlen, RFC-4180-Quoting, diff --git a/src/App.vue b/src/App.vue index 3828b66..4e92fdb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,5 @@