diff --git a/README.md b/README.md index 30f2903..713f42f 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), 127 Tests +npm run test # Vitest-Suite (Engine + Store + IO), 132 Tests npm run build # Typecheck + Produktionsbuild npm run lint # ESLint (flat config) ``` @@ -97,12 +97,21 @@ 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) | -| **`F4`** (im Editor/in der Formelleiste) | Bezug unter dem Cursor sperren/zyklisch umschalten: `A1` → `$A$1` → `A$1` → `$A1` → `A1` | +| **`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`** (im Editor) | Zeilenumbruch in der Zelle einfügen, statt zu committen | Bei **festen (scrollbaren) Tabellen** wird die aktive Zelle bei jeder Navigation automatisch in den sichtbaren Bereich gescrollt (unter Beachtung fixierter Zeilen/Spalten). +**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` +eingefügten Zeilenumbrüchen, bis zu einem Maximum, danach intern scrollbar. +Ein Zeilenumbruch bleibt auch nach dem Committen erhalten und wird in der +Zelle als solcher dargestellt (umbrechend statt einzeiliger Ellipse), auf die +Zeilenhöhe der Tabelle geklemmt. + ### Zell-Formatierung Auswahl (Zelle, Bereich, Mehrfachauswahl, ganze Zeile/Spalte über den Kopf) → im @@ -315,14 +324,17 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, ## Tests -`npm run test` — Vitest, 127 Tests: +`npm run test` — Vitest, 132 Tests: - **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^` rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`, Bereiche, `;`-Argumente, Parse-Fehler. - **refs (`$`-Sperrachsen, `F4`)** — `parseA1`/`toA1` runden relativ/absolut/ - gemischt; `cycleReferenceLock` durchläuft die vier Kombinationen, trifft - bei Cursor auf einem Bereich die richtige Seite, liefert `null` ohne Treffer. + gemischt; `cycleReferenceLocks` durchläuft die vier Kombinationen, trifft + bei Cursor auf einem Bereich die richtige Seite, liefert `null` ohne + Treffer; bei echter Textauswahl schaltet es **alle** überlappten Bezüge auf + einmal um (jeden vom eigenen Zustand aus) und lässt Tabellen-Qualifier, die + wie eine Zelle aussehen (`AB12!A1`), unangetastet. - **evaluator** — Arithmetik, leere Zelle = 0, alle Funktionen inkl. Alias, Cross-Table, Case-/`::`-Toleranz, jeder Fehlerfall, Fehler-Propagierung. - **graph** — `affected`, topologische Ordnung, Selbst-/2er-/längerer Zyklus. diff --git a/src/components/FormulaBar.vue b/src/components/FormulaBar.vue index 101092e..7b09b97 100644 --- a/src/components/FormulaBar.vue +++ b/src/components/FormulaBar.vue @@ -1,7 +1,7 @@ diff --git a/src/components/TableGrid.vue b/src/components/TableGrid.vue index f1ef62a..e7254d0 100644 --- a/src/components/TableGrid.vue +++ b/src/components/TableGrid.vue @@ -11,7 +11,7 @@ * in-cell editing and per-column resize. */ import { computed, nextTick, onBeforeUnmount, ref } from 'vue'; -import { numberToCol, cycleReferenceLock } from '../engine'; +import { numberToCol, cycleReferenceLocks } from '../engine'; import { useDocument } from '../store/document'; import { DEFAULT_ROW_H } from '../store/types'; import ContextMenu from './ContextMenu.vue'; @@ -23,6 +23,11 @@ const table = computed(() => store.tableById(props.tableId)); const HEAD_W = 44; const DEFAULT_W = 104; +/** How far the in-cell editor may grow beyond the cell before it wraps/scrolls + * -- an approximation of "the visible display width/height" rather than + * exact viewport math (the canvas can be panned/zoomed independently). */ +const EDIT_MAX_W = 560; +const EDIT_MAX_H = 320; const colWidth = (c: number) => table.value?.colWidths[c] ?? DEFAULT_W; const rowH = (r: number) => store.rowHeightOf(props.tableId, r); const headerH = computed(() => table.value?.rowHeight ?? DEFAULT_ROW_H); @@ -58,6 +63,10 @@ function formatStyle(c: number, r: number): Record { return s; } +/** A cell holding an explicit line break (Alt+Enter while editing) wraps and + * shows it instead of collapsing to a single ellipsised line. */ +const isMultiline = (c: number, r: number) => store.cellInput(props.tableId, c, r).includes('\n'); + const root = ref(null); // ---- fixed viewport + frozen panes ------------------------------- @@ -99,6 +108,17 @@ function cellStyle(c: number, r: number): Record { if (fr || fc) s.zIndex = fr && fc ? '5' : '3'; return s; } +/** Caps a cell's rendered text to the row's own height. A `` is + * only a *minimum* -- a ``'s own `max-height` doesn't clip it either + * (browsers don't cap table-row growth that way); a plain block-level child + * with `max-height` + `overflow: hidden` does, so `.cell-content` (not the + * `` itself) carries this. Without it a wrapped multi-line cell (see + * `.cell.multiline`) would grow its row past rowH(r), desyncing every pixel + * position computed from rowH()/rowTop() -- the clip/fill overlays, sticky + * frozen panes, the row-resize handle, ... */ +function cellContentStyle(r: number): Record { + return { maxHeight: rowH(r) + 'px' }; +} function rowHeadStyle(r: number): Record { if (!isFixed.value || r >= frozenRows.value) return {}; return { top: rowTop(r) + 'px', zIndex: '5' }; @@ -557,14 +577,53 @@ function selectAll() { } // ---- editing --------------------------------------------------- +// The editor overlays the cell (positioned via colLeft/rowTop, like the +// clip/fill overlays) rather than living inside the , so it's free to +// grow past the cell's own width/height -- clipped only by EDIT_MAX_W/H, not +// by the grid's per-cell `overflow: hidden`. const editing = ref<{ col: number; row: number } | null>(null); const editValue = ref(''); -const editInput = ref(null); +const editInput = ref(null); +const editMirrorEl = ref(null); const setEditRef = (el: unknown) => { - editInput.value = (el as HTMLInputElement) ?? null; + editInput.value = (el as HTMLTextAreaElement) ?? null; }; let committing = false; +const editStyle = computed(() => { + const s = editing.value; + if (!s) return {}; + return { + left: colLeft(s.col) + 'px', + top: rowTop(s.row) + 'px', + minWidth: colWidth(s.col) + 'px', + minHeight: rowH(s.row) + 'px', + }; +}); +const editMirrorStyle = computed(() => { + const s = editing.value; + if (!s) return {}; + return { + minWidth: colWidth(s.col) + 'px', + minHeight: rowH(s.row) + 'px', + maxWidth: EDIT_MAX_W + 'px', + }; +}); + +/** Grow the editor to fit its content (width first, then wrap and grow + * height), via a hidden mirror element -- a plain `