Größenänderungs-Verhalten der Tabellenmodi vertauscht
Der SE-Griff-Effekt war genau falsch herum zugeordnet: "Automatisch"- Tabellen (Größe folgt immer dem Inhalt) sollen beim Ziehen die Zeilen-/ Spaltenzahl ändern, "Feste Größe"-Tabellen (expliziter, unabhängig wählbarer Scroll-Viewport) sollen unverändert nur den Pixel-Viewport ändern. - "Automatisch": Ziehen zeigt ein gestricheltes Vorschau-Rechteck (die eigentliche Zeilen-/Spaltenänderung ist teuer -- voller Workbook-Rebuild -- und committet daher erst beim Loslassen, nicht live). Wachsen fügt leere Zeilen/Spalten hinzu, Schrumpfen entfernt sie nie über echten Zellinhalt hinweg (gleicher Inhaltsschutz wie zuvor, nur am richtigen Modus). - "Feste Größe": zurück auf reines Pixel-Viewport-Resize, unabhängig von der Zeilen-/Spaltenzahl -- das ursprüngliche Verhalten. - Die dadurch tot gewordene alte "Automatisch"-Funktion (proportionale Spaltenbreiten-Skalierung beim Ziehen) entfernt (scaleTableColumns), keine andere Stelle rief sie noch auf. vue-tsc/eslint/build sauber, Playwright-live-verifiziert (Automatisch: Wachsen 12×8→19×11, hartes Schrumpfen klemmt auf 6×5 mit erhaltenem Inhalt; Feste Größe: Zeilen-/Spaltenzahl bleibt bei Viewport-Resize unverändert). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,16 @@
|
||||
* Draggable / resizable wrapper for a table or chart object on the sheet canvas.
|
||||
*
|
||||
* - the header strip and the frame border move the object;
|
||||
* - the SE handle resizes a chart (free w/h) or proportionally scales a table's
|
||||
* columns (Numbers-style);
|
||||
* - the SE handle resizes a chart/text freely; for a table, what it does
|
||||
* depends on the view mode (see `resizeKind` below):
|
||||
* - "Automatisch" (`mode: 'auto'`, size always == content): dragging
|
||||
* drives the row/col *count* directly -- growing adds empty rows/
|
||||
* cols, shrinking removes them, never past real content;
|
||||
* - "Feste Größe" (`mode: 'fixed'`, an explicit scrollable viewport):
|
||||
* dragging only resizes that pixel viewport, independent of row/col
|
||||
* count -- the original behaviour, since a fixed-size table's whole
|
||||
* point is a box smaller than its (possibly much larger) content,
|
||||
* scrolling internally.
|
||||
* - name is editable via double-click, delete via the × button.
|
||||
*
|
||||
* All pointer deltas are divided by `zoom` so dragging tracks the cursor at any
|
||||
@@ -62,12 +70,16 @@ function onMoveUp() {
|
||||
|
||||
// ---- SE resize --------------------------------------------------
|
||||
let rs: { px: number; py: number; w: number; h: number } | null = null;
|
||||
const contentEl = ref<HTMLElement | null>(null);
|
||||
/** Ghost outline shown *only* while dragging an "Automatisch" table's
|
||||
* handle (see onResizeMove) -- the real row/col-count change is expensive
|
||||
* (a full syncWorkbook() per call) so it commits once, on release; this is
|
||||
* purely a lightweight visual preview in the meantime, touching neither
|
||||
* the store nor the table's actual DOM. */
|
||||
const previewSize = ref<{ w: number; h: number } | 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).
|
||||
// An "Automatisch" table's resize handle drags the row/col *count* directly
|
||||
// -- these mirror TableGrid.vue's own pixel model (HEAD_W/DEFAULT_W are
|
||||
// duplicated there too).
|
||||
const HEAD_W = 44;
|
||||
const DEFAULT_W = 104;
|
||||
|
||||
@@ -95,10 +107,12 @@ function rowsForHeight(t: TableObject, targetPx: number): number {
|
||||
}
|
||||
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). */
|
||||
/** Pixel width/height that exactly fits `t`'s *current* rows/cols -- an
|
||||
* "Automatisch" table has no stored viewport size of its own (it always
|
||||
* renders at its natural content size), so this is the reference point a
|
||||
* resize drag starts from; no explicit "resync" is needed afterwards
|
||||
* either, since the DOM already re-renders at the new natural size the
|
||||
* moment `setTableRows`/`setTableCols` changes `rows`/`cols`. */
|
||||
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;
|
||||
@@ -121,11 +135,12 @@ function lastNonEmptyCol(t: TableObject): number {
|
||||
return max;
|
||||
}
|
||||
|
||||
/** How the SE handle behaves for this object. */
|
||||
const resizeKind = computed<'free' | 'table-viewport' | 'table-columns'>(() => {
|
||||
/** How the SE handle behaves for this object -- see the module doc for the
|
||||
* "Automatisch" vs. "Feste Größe" split. */
|
||||
const resizeKind = computed<'free' | 'viewport' | 'grid-size'>(() => {
|
||||
const o = props.object;
|
||||
if (o.kind === 'chart' || o.kind === 'text') return 'free';
|
||||
return o.view.mode === 'fixed' ? 'table-viewport' : 'table-columns';
|
||||
return o.view.mode === 'fixed' ? 'viewport' : 'grid-size';
|
||||
});
|
||||
|
||||
function onResizeDown(e: MouseEvent) {
|
||||
@@ -133,7 +148,6 @@ function onResizeDown(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
emit('select');
|
||||
const o = props.object;
|
||||
const rect = contentEl.value?.getBoundingClientRect();
|
||||
let w: number;
|
||||
let h: number;
|
||||
if (o.kind === 'chart' || o.kind === 'text') {
|
||||
@@ -143,8 +157,9 @@ function onResizeDown(e: MouseEvent) {
|
||||
w = o.view.width;
|
||||
h = o.view.height;
|
||||
} else {
|
||||
w = (rect?.width ?? 300) / props.zoom;
|
||||
h = (rect?.height ?? 200) / props.zoom;
|
||||
const size = fittingSize(o);
|
||||
w = size.width;
|
||||
h = size.height;
|
||||
}
|
||||
rs = { px: e.clientX, py: e.clientY, w, h };
|
||||
document.body.style.userSelect = 'none';
|
||||
@@ -159,31 +174,31 @@ function onResizeMove(e: MouseEvent) {
|
||||
if (resizeKind.value === 'free') {
|
||||
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.
|
||||
} else if (resizeKind.value === 'viewport') {
|
||||
// "Feste Größe": pure pixel-viewport resize, independent of row/col
|
||||
// count -- cheap (setTableView never rebuilds the workbook), so a live
|
||||
// update on every mousemove is fine, same as it always was.
|
||||
store.setTableView(o.id, { width: rs.w + dx, height: rs.h + dy });
|
||||
} else {
|
||||
const factor = Math.max(0.4, (rs.w + dx) / rs.w);
|
||||
store.scaleTableColumns(o.id, factor, 104);
|
||||
rs = { ...rs, px: e.clientX, w: rs.w + dx };
|
||||
// "Automatisch": dragging drives row/col *count* -- but that's a full
|
||||
// syncWorkbook() per call, too expensive for every mousemove, so only a
|
||||
// lightweight ghost-box preview updates live here (see the template);
|
||||
// the real change commits once, on release, in onResizeUp.
|
||||
previewSize.value = { w: Math.max(80, rs.w + dx), h: Math.max(52, rs.h + dy) };
|
||||
}
|
||||
}
|
||||
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);
|
||||
if (rs && resizeKind.value === 'grid-size' && o.kind === 'table' && previewSize.value) {
|
||||
// Never past non-empty content, so a hard shrink can't silently delete
|
||||
// data -- it just settles at the smallest size that still shows
|
||||
// everything, however far past that the drag actually went.
|
||||
const targetCols = colsForWidth(o, previewSize.value.w);
|
||||
const targetRows = rowsForHeight(o, previewSize.value.h);
|
||||
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));
|
||||
}
|
||||
previewSize.value = null;
|
||||
rs = null;
|
||||
document.body.style.userSelect = '';
|
||||
window.removeEventListener('mousemove', onResizeMove);
|
||||
@@ -236,12 +251,17 @@ const style = computed(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ref="contentEl" class="content">
|
||||
<div class="content">
|
||||
<TableGrid v-if="object.kind === 'table'" :table-id="object.id" />
|
||||
<ChartView v-else-if="object.kind === 'chart'" :obj-id="object.id" />
|
||||
<TextBox v-else :obj-id="object.id" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="previewSize"
|
||||
class="resize-ghost"
|
||||
:style="{ width: previewSize.w + 'px', height: previewSize.h + 'px' }"
|
||||
/>
|
||||
<div v-if="selected" class="rs-handle" @mousedown="onResizeDown" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -314,6 +334,16 @@ const style = computed(() => {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.resize-ghost {
|
||||
position: absolute;
|
||||
top: 27px;
|
||||
left: 0;
|
||||
border: 2px dashed var(--accent);
|
||||
border-radius: 0 0 8px 8px;
|
||||
background: var(--accent-weak);
|
||||
pointer-events: none;
|
||||
z-index: 7;
|
||||
}
|
||||
.rs-handle {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
|
||||
@@ -1381,15 +1381,6 @@ function createStore() {
|
||||
const t = tableById(tableId);
|
||||
if (t) t.colWidths[col] = Math.max(MIN_COL_W, Math.round(width));
|
||||
}
|
||||
/** Numbers-style proportional resize: scale every column width by `factor`. */
|
||||
function scaleTableColumns(tableId: string, factor: number, base: number) {
|
||||
const t = tableById(tableId);
|
||||
if (!t) return;
|
||||
for (let c = 0; c < t.cols; c++) {
|
||||
const cur = t.colWidths[c] ?? base;
|
||||
t.colWidths[c] = Math.max(MIN_COL_W, Math.round(cur * factor));
|
||||
}
|
||||
}
|
||||
/** Height of one row (per-row override, else the table default). */
|
||||
function rowHeightOf(tableId: string, row: number): number {
|
||||
const t = tableById(tableId);
|
||||
@@ -2241,7 +2232,6 @@ function createStore() {
|
||||
removeLastRow,
|
||||
removeLastColumn,
|
||||
setColWidth,
|
||||
scaleTableColumns,
|
||||
setRowHeight,
|
||||
setTableRowHeight,
|
||||
rowHeightOf,
|
||||
|
||||
Reference in New Issue
Block a user