Größenänderung einer "Feste Größe"-Tabelle steuert Zeilen-/Spaltenzahl
Der SE-Griff einer Tabelle im "Feste Größe"-Modus ändert nicht mehr nur den scrollbaren Pixel-Viewport, sondern direkt die Zeilen-/Spaltenzahl: Vergrößern fügt leere Zeilen/Spalten hinzu, Verkleinern entfernt sie — aber nie über echten Zellinhalt hinweg (klemmt auf die kleinste Größe, die den vorhandenen Inhalt noch vollständig zeigt). Live-Vorschau während des Ziehens bleibt der günstige reine Viewport-Resize (kein Workbook- Rebuild pro Mausbewegung); die eigentliche Zeilen-/Spaltenänderung committet erst beim Loslassen. Reine UI-Änderung (CanvasObject.vue), nutzt die bestehenden setTableRows/setTableCols weiter. Playwright-live-verifiziert, kein Komponenten-Test-Setup vorhanden (README dokumentiert die Lücke). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,9 +34,18 @@ scroll- und zoombare Fläche, die **Objekte** trägt:
|
||||
Kopfleiste). Zwei Darstellungen:
|
||||
- **Automatisch** — so groß wie die Zellen, alle Zeilen sichtbar; der SE-Griff
|
||||
skaliert die Spaltenbreiten proportional.
|
||||
- **Feste Größe** — fester Viewport, **intern scrollbar**, mit frei wählbarer
|
||||
Zahl **fixierter Kopfzeilen und fixierter Spalten** (`position: sticky`
|
||||
gegen den Scroll-Container). Der SE-Griff zieht dann den Viewport auf.
|
||||
- **Feste Größe** — mit frei wählbarer Zahl **fixierter Kopfzeilen und
|
||||
fixierter Spalten** (`position: sticky` gegen den Scroll-Container, falls
|
||||
der Inhalt doch mal über die Boxgröße hinauswächst — etwa durch Tippen
|
||||
über den Rand hinaus). Der SE-Griff zieht hier **direkt an Zeilen-/
|
||||
Spaltenzahl**: Vergrößern fügt leere Zeilen/Spalten hinzu, Verkleinern
|
||||
entfernt sie — aber **nie** über echten Zellinhalt hinweg. Beim Loslassen
|
||||
wird auf die kleinste Größe geklemmt, die den vorhandenen Inhalt noch
|
||||
vollständig zeigt; die Box passt sich exakt an (kein Leerraum, kein
|
||||
Scrollen nötig, solange nicht direkt über die Boxgröße hinaus getippt
|
||||
wird). Die Breiten-/Höhenfelder im Einstellungen-Panel (⚙, s. u.) bleiben
|
||||
davon unabhängig — reine Pixelmaße, ohne Rückwirkung auf Zeilen-/
|
||||
Spaltenzahl.
|
||||
Spaltenbreite per Drag am Spaltenkopf (ab **24 px**), **Zeilenhöhe** per Drag
|
||||
an der Unterkante des Zeilenkopfs (Standard + je Zeile).
|
||||
- **Diagramm-Objekt** — Chart.js (Balken/Linie), frei positionier- **und**
|
||||
@@ -1028,6 +1037,12 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
`OFFSET`/`INDEX`-Äquivalent — ein `#REF`, das aus einer gelöschten Zeile
|
||||
entsteht, muss von Hand korrigiert werden, es gibt keine „nächstbeste
|
||||
Zelle"-Heuristik.
|
||||
- **Größenänderung einer „Feste Größe"-Tabelle** (SE-Griff → Zeilen-/
|
||||
Spaltenzahl, s. o. „Das Canvas-Modell") ist rein UI-seitig (`CanvasObject.vue`)
|
||||
implementiert, ohne eigene Unit-Tests — das Projekt hat (noch) kein
|
||||
Komponenten-Test-Setup (z. B. Vue Test Utils), nur Engine-/Store-Tests.
|
||||
Nur live per Playwright verifiziert (Wachsen, Schrumpfen mit
|
||||
Inhaltsschutz, deutliches Überschreiten der bestehenden Größe).
|
||||
- **Objekt-Bezüge beim Anhängen.** Beim JSON-*Anhängen* zweier Dokumente werden
|
||||
nur Blatt-/Objekt-IDs neu vergeben, nicht Tabellennamen. Kollidiert ein
|
||||
angehängter Tabellenname mit einem bestehenden, gewinnt bei der Auflösung der
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
* zoom level.
|
||||
*/
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import type { SheetObject } from '../store/types';
|
||||
import type { SheetObject, TableObject } from '../store/types';
|
||||
import { DEFAULT_ROW_H } from '../store/types';
|
||||
import { useDocument } from '../store/document';
|
||||
import TableGrid from './TableGrid.vue';
|
||||
import ChartView from './ChartView.vue';
|
||||
@@ -63,6 +64,63 @@ function onMoveUp() {
|
||||
let rs: { px: number; py: number; w: number; h: number } | null = null;
|
||||
const contentEl = ref<HTMLElement | null>(null);
|
||||
|
||||
// A "fixed" table's resize handle drags the row/col *count* directly (not
|
||||
// an independent scrollable viewport, see the module doc) -- these mirror
|
||||
// TableGrid.vue's own pixel model (HEAD_W/DEFAULT_W are duplicated there
|
||||
// too, same as the existing `104` passed to scaleTableColumns below).
|
||||
const HEAD_W = 44;
|
||||
const DEFAULT_W = 104;
|
||||
|
||||
/** How many columns fit in `targetPx`, at least 1. */
|
||||
function colsForWidth(t: TableObject, targetPx: number): number {
|
||||
let w = HEAD_W;
|
||||
let c = 0;
|
||||
while (true) {
|
||||
const next = w + (t.colWidths[c] ?? DEFAULT_W);
|
||||
if (next > targetPx && c > 0) break;
|
||||
w = next;
|
||||
c++;
|
||||
}
|
||||
return Math.max(1, c);
|
||||
}
|
||||
/** How many rows fit in `targetPx`, at least 1. */
|
||||
function rowsForHeight(t: TableObject, targetPx: number): number {
|
||||
let h = t.rowHeight ?? DEFAULT_ROW_H;
|
||||
let r = 0;
|
||||
while (true) {
|
||||
const next = h + store.rowHeightOf(t.id, r);
|
||||
if (next > targetPx && r > 0) break;
|
||||
h = next;
|
||||
r++;
|
||||
}
|
||||
return Math.max(1, r);
|
||||
}
|
||||
/** Pixel width/height that exactly fits `t`'s *current* rows/cols -- used
|
||||
* to re-sync the viewport box after a row/col-count change, since the
|
||||
* actual result can differ from what was dragged to (a shrink that would
|
||||
* destroy non-empty content is clamped, see lastNonEmptyRow/Col below). */
|
||||
function fittingSize(t: TableObject): { width: number; height: number } {
|
||||
let width = HEAD_W;
|
||||
for (let c = 0; c < t.cols; c++) width += t.colWidths[c] ?? DEFAULT_W;
|
||||
let height = t.rowHeight ?? DEFAULT_ROW_H;
|
||||
for (let r = 0; r < t.rows; r++) height += store.rowHeightOf(t.id, r);
|
||||
return { width, height };
|
||||
}
|
||||
/** The last row/col index holding a real (non-empty) cell, or -1 -- a
|
||||
* row/col-count shrink must never go below this, or it would silently
|
||||
* delete data. `t.cells` never stores an empty-string entry (an emptied
|
||||
* cell removes its key, see setCellInput), so every present key is real. */
|
||||
function lastNonEmptyRow(t: TableObject): number {
|
||||
let max = -1;
|
||||
for (const k of Object.keys(t.cells)) max = Math.max(max, Number(k.split(',')[1]));
|
||||
return max;
|
||||
}
|
||||
function lastNonEmptyCol(t: TableObject): number {
|
||||
let max = -1;
|
||||
for (const k of Object.keys(t.cells)) max = Math.max(max, Number(k.split(',')[0]));
|
||||
return max;
|
||||
}
|
||||
|
||||
/** How the SE handle behaves for this object. */
|
||||
const resizeKind = computed<'free' | 'table-viewport' | 'table-columns'>(() => {
|
||||
const o = props.object;
|
||||
@@ -102,6 +160,9 @@ function onResizeMove(e: MouseEvent) {
|
||||
if (o.kind === 'chart') store.resizeChart(o.id, rs.w + dx, rs.h + dy);
|
||||
else if (o.kind === 'text') store.resizeText(o.id, rs.w + dx, rs.h + dy);
|
||||
} else if (resizeKind.value === 'table-viewport') {
|
||||
// Cheap live preview only (setTableView never rebuilds the workbook) --
|
||||
// the actual row/col-count change (setTableRows/setTableCols, each a
|
||||
// full syncWorkbook()) is committed once, on release, in onResizeUp.
|
||||
store.setTableView(o.id, { width: rs.w + dx, height: rs.h + dy });
|
||||
} else {
|
||||
const factor = Math.max(0.4, (rs.w + dx) / rs.w);
|
||||
@@ -110,6 +171,19 @@ function onResizeMove(e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
function onResizeUp() {
|
||||
const o = props.object;
|
||||
if (rs && resizeKind.value === 'table-viewport' && o.kind === 'table') {
|
||||
// Resizing a "fixed" table now drives its row/col *count* directly
|
||||
// (Numbers-style: the box IS the grid, no independent scroll viewport)
|
||||
// instead of only the pixel box -- but never past non-empty content,
|
||||
// so a shrink can't silently delete data.
|
||||
const targetCols = colsForWidth(o, o.view.width);
|
||||
const targetRows = rowsForHeight(o, o.view.height);
|
||||
store.setTableCols(o.id, Math.max(targetCols, lastNonEmptyCol(o) + 1));
|
||||
store.setTableRows(o.id, Math.max(targetRows, lastNonEmptyRow(o) + 1));
|
||||
const fresh = store.tableById(o.id);
|
||||
if (fresh) store.setTableView(o.id, fittingSize(fresh));
|
||||
}
|
||||
rs = null;
|
||||
document.body.style.userSelect = '';
|
||||
window.removeEventListener('mousemove', onResizeMove);
|
||||
|
||||
Reference in New Issue
Block a user