From fb5915d5be55604e5400175dcf63ed1c3ee795e1 Mon Sep 17 00:00:00 2001 From: Wild Karl-Heinz Date: Wed, 2 Sep 2026 14:41:27 +0200 Subject: [PATCH] Add formula "point mode": click/drag a cell to insert its reference Excel/Numbers-style reference picking: while editing a formula (buffer starts with "="), a click on any grid cell -- in the same table or a completely different one -- inserts its reference at the caret instead of changing the selection; a drag inserts/grows a range reference live as you drag. A further click/drag before typing anything replaces the just-inserted reference; typing anything (";" to separate SUM() arguments included, but not special-cased -- any character ends the window, same as Excel) makes the next click append a fresh one instead. - document.ts: whichever editor is open (in-cell editor or formula bar) registers a small handle (home tableId + read/replace the buffer); pointClick()/pointDrag() insert or replace a reference into it, qualifying with `Name!` (quoted if needed) when the clicked table differs from the editor's own. formulaPointModeActive() gates it on the buffer being a formula; noteFormulaTyped() (wired to real typing, not the programmatic inserts) ends the "replace" window. - TableGrid.vue: onCellMouseDown checks point mode before its normal selection/edit handling, tracking a drag the same way normal selection does; dblclick during point mode no longer hijacks into editing that cell. - FormulaBar.vue: registers on focus, same click/drag targeting works from there without disturbing the grid's own selection. Verified interactively against the dev server with Playwright, including a real bug the store-level tests (a mocked editor handle, no real DOM) missed entirely: preventDefault() alone kept focus on the editor, but the mousedown still bubbled to CanvasObject.vue's own handler, which selects/brings the clicked table to front -- closing the very editor being pointed into. Fixed with stopPropagation() alongside it. After that fix: same-table and cross-table click/drag both insert correctly-qualified references, a second click replaces before typing and appends after, and the live selection/ active object are untouched by a point-mode click. Co-Authored-By: Claude Sonnet 5 --- README.md | 20 ++++- src/components/FormulaBar.vue | 41 +++++++++- src/components/TableGrid.vue | 89 ++++++++++++++++++++- src/store/__tests__/document.spec.ts | 111 +++++++++++++++++++++++++++ src/store/document.ts | 84 ++++++++++++++++++++ 5 files changed, 339 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5c7b79c..42bec38 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), 138 Tests +npm run test # Vitest-Suite (Engine + Store + IO), 146 Tests npm run build # Typecheck + Produktionsbuild npm run lint # ESLint (flat config) ``` @@ -136,6 +136,17 @@ an der unteren Kante lässt sie sich per **Maus** (natives `resize: vertical`) auf beliebig viele Zeilen aufziehen, um mehr auf einmal zu sehen. `Alt`+`Enter` bzw. `Cmd`+`Enter`, `F4` und `Esc` verhalten sich wie im In-Zellen-Editor. +**Bezüge per Maus einfügen ("Point-Mode")**: sobald der Editor (Zelle oder +Formelleiste) eine Formel (`=…`) enthält, fügt ein **Klick** auf eine Zelle — +in derselben Tabelle **oder einer ganz anderen**, mit `Name!`-Qualifier bei +Bedarf `'in Quotes'` — deren Bezug an der Cursorposition ein; **Ziehen** +fügt stattdessen einen Bereichsbezug (`A1:C3`) ein, live wachsend während +der Zug. Ein weiterer Klick/Zug **ersetzt** den zuletzt eingefügten Bezug, +solange nichts dazwischen getippt wurde — **jede** Eingabe (ein `;` als +Argumenttrennzeichen ebenso wie jedes andere Zeichen) beendet dieses Fenster, +der nächste Klick fügt dann einen neuen Bezug an. Ein Klick während des +Point-Mode ändert nie die Tabellen-/Objektauswahl im Hintergrund. + ### Zell-Formatierung Auswahl (Zelle, Bereich, Mehrfachauswahl, ganze Zeile/Spalte über den Kopf) → im @@ -348,7 +359,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, ## Tests -`npm run test` — Vitest, 138 Tests: +`npm run test` — Vitest, 146 Tests: - **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^` rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`, @@ -405,6 +416,11 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, 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. +- **Formel-Point-Mode** — Klick fügt einen lokalen Bezug an der Cursorposition + ein; ein zweiter Klick ohne Zwischen-Tippen ersetzt ihn, mit Tippen dazwischen + hängt er einen neuen an; Ziehen fügt/erweitert einen Bereichsbezug; ein Klick + in eine andere Tabelle qualifiziert mit deren Namen (Quoting bei Leerzeichen); + ohne Formel im Puffer greift nichts. - **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/components/FormulaBar.vue b/src/components/FormulaBar.vue index 2fb1fce..83ef19b 100644 --- a/src/components/FormulaBar.vue +++ b/src/components/FormulaBar.vue @@ -6,6 +6,7 @@ import { cycleReferenceLocks } from '../engine'; const store = useDocument(); const draft = ref(''); const focused = ref(false); +const fieldEl = ref(null); // keep the field in sync with the active cell unless the user is editing it watch( @@ -20,6 +21,31 @@ function commit() { const sel = store.doc.selection; if (sel) store.setCellInput(sel.tableId, sel.anchor.col, sel.anchor.row, draft.value); } +/** Lets the store insert a reference into this field when the user clicks a + * grid cell mid-formula ("point mode") -- see store.pointClick/pointDrag. */ +function registerAsFormulaEditor() { + const sel = store.doc.selection; + if (!sel) return; + store.registerFormulaEditor({ + tableId: sel.tableId, + getState: () => ({ + text: draft.value, + start: fieldEl.value?.selectionStart ?? draft.value.length, + end: fieldEl.value?.selectionEnd ?? draft.value.length, + }), + replace: (start, end, value) => { + draft.value = draft.value.slice(0, start) + value + draft.value.slice(end); + nextTick(() => { + fieldEl.value?.focus(); + fieldEl.value?.setSelectionRange(start + value.length, start + value.length); + }); + }, + }); +} +function onFocus() { + focused.value = true; + registerAsFormulaEditor(); +} function onEnter() { commit(); (document.activeElement as HTMLElement)?.blur(); @@ -27,11 +53,21 @@ function onEnter() { function onBlur() { focused.value = false; commit(); + store.registerFormulaEditor(null); } function onEscape() { draft.value = store.activeCellInput.value; (document.activeElement as HTMLElement)?.blur(); } +/** Actual typing (not a point-mode click inserting a reference programmatically) + * ends the "next click replaces the last reference" window. */ +function onInput() { + store.noteFormulaTyped(); +} +/** Clicking inside the field to reposition the caret should also end that window. */ +function onMousedown() { + store.noteFormulaTyped(); +} /** F4: cycle relative -> absolute -> row-locked -> col-locked (Excel/Numbers * "Bezug sperren") -- for the reference at the caret, or every reference a * real text selection overlaps at once. */ @@ -77,15 +113,18 @@ function onKeydown(e: KeyboardEvent) { fx