Add per-table "hide chrome/headers when unfocused" clean-look option
New Settings checkbox: while a table isn't the selected canvas object, its title bar, column letters and row numbers disappear entirely -- not just visually, their reserved space collapses too, so the table shrinks to just its cell grid. Clicking back into it (e.g. any cell) reselects it and brings everything back. TableObject.hideChromeUnfocused (store/types.ts) + setTableHideChromeUnfocused (document.ts) + io/json.ts normalization, following the existing per-object appearance-setting pattern. CanvasObject.vue gates its title bar on a new showChrome computed and passes its existing `selected` prop down to TableGrid.vue (previously only took tableId) so it can make the same call for its own headers. TableGrid.vue's HEAD_W (a plain constant) becomes a `headW` computed, and the existing `headerH` computed now returns 0 for both whenever hidden -- sufficient on its own since rowTop()/colLeft() already sum those two values as their base offset and are the single source every other position calculation in the file (sticky-frozen offsets, the in-cell editor overlay, clip/fill marquees, scroll-into-view, row virtualization) already goes through. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,13 @@ scroll- und zoombare Fläche, die **Objekte** trägt:
|
||||
Canvas-Objekt, unabhängig vom DOM. Nur Zeilen, nicht Spalten (in der
|
||||
Praxis wird eher die Zeilenzahl groß).
|
||||
Spaltenbreite per Drag am Spaltenkopf (ab **24 px**), **Zeilenhöhe** per Drag
|
||||
an der Unterkante des Zeilenkopfs (Standard + je Zeile).
|
||||
an der Unterkante des Zeilenkopfs (Standard + je Zeile). **„Ohne Rahmen/
|
||||
Kopfzeilen, wenn nicht ausgewählt"** (Einstellungen → Objekt, je Tabelle):
|
||||
solange die Tabelle nicht das ausgewählte Canvas-Objekt ist, verschwinden
|
||||
Titelleiste, Spaltenbuchstaben und Zeilennummern komplett — nicht nur
|
||||
optisch, ihr reservierter Platz fällt mit weg, die Tabelle schrumpft auf
|
||||
reines Zellraster. Ein Klick hinein (z. B. auf eine Zelle) wählt sie
|
||||
wieder aus und blendet alles zurück ein.
|
||||
- **Diagramm-Objekt** — Chart.js (Balken/Linie), frei positionier- **und**
|
||||
skalierbar. Datenquelle = ein Tabellen-Objekt; jede Zelländerung aktualisiert
|
||||
es sofort.
|
||||
@@ -646,7 +652,8 @@ wichtigen Punkte:
|
||||
- **Objekt** (das gewählte):
|
||||
- Tabelle → Name, Zeilen/Spalten (exakt), Darstellung, Viewport-Größe,
|
||||
fixierte Kopfzeilen/Spalten, Position, Zahlenformat-Locale, **Standard-
|
||||
Zeilenhöhe**, **Tabellen-Hintergrund/-Textfarbe**, und – bei aktiver
|
||||
Zeilenhöhe**, **Tabellen-Hintergrund/-Textfarbe**, **„Ohne Rahmen/
|
||||
Kopfzeilen, wenn nicht ausgewählt"** (s. u.), und – bei aktiver
|
||||
Zellauswahl – die **Auswahl**-Formatierung (s. o.).
|
||||
- Diagramm → Name, Ansicht, Quelltabelle, Größe, Position, Zahlenformat-Locale,
|
||||
**Hintergrund**, **Text-/Achsenfarbe**.
|
||||
|
||||
@@ -221,6 +221,16 @@ function onResizeUp() {
|
||||
window.removeEventListener('mouseup', onResizeUp);
|
||||
}
|
||||
|
||||
/** A table with `hideChromeUnfocused` set shows its title bar (and, via the
|
||||
* `selected` prop passed to TableGrid below, its row/column headers) only
|
||||
* while it's the selected object -- a "clean look" presentation option
|
||||
* ("Ohne Rahmen/Kopfzeilen, wenn nicht ausgewählt" in Settings). Every
|
||||
* other object kind is unaffected -- always shown, today's behaviour. */
|
||||
const showChrome = computed(() => {
|
||||
const o = props.object;
|
||||
return props.selected || !(o.kind === 'table' && o.hideChromeUnfocused);
|
||||
});
|
||||
|
||||
const icon = computed(() => {
|
||||
switch (props.object.kind) {
|
||||
case 'table':
|
||||
@@ -253,7 +263,7 @@ const style = computed(() => {
|
||||
:style="style"
|
||||
@mousedown="emit('select')"
|
||||
>
|
||||
<div class="chrome" @mousedown.stop="onMoveDown">
|
||||
<div v-if="showChrome" class="chrome" @mousedown.stop="onMoveDown">
|
||||
<input
|
||||
v-if="renaming"
|
||||
ref="nameInput"
|
||||
@@ -277,8 +287,8 @@ const style = computed(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<TableGrid v-if="object.kind === 'table'" :table-id="object.id" />
|
||||
<div class="content" :class="{ 'full-radius': !showChrome }">
|
||||
<TableGrid v-if="object.kind === 'table'" :table-id="object.id" :selected="selected" />
|
||||
<ChartView v-else-if="object.kind === 'chart'" :obj-id="object.id" />
|
||||
<TextBox v-else-if="object.kind === 'text'" :obj-id="object.id" />
|
||||
<ButtonView v-else-if="object.kind === 'button'" :obj-id="object.id" />
|
||||
@@ -357,6 +367,11 @@ const style = computed(() => {
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
/* the chrome bar is what normally rounds the top corners -- without it
|
||||
(hideChromeUnfocused, currently unfocused), .content fills the whole box */
|
||||
.content.full-radius {
|
||||
border-radius: 10px;
|
||||
}
|
||||
.canvas-object.flex .content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
@@ -161,6 +161,14 @@ function resetDoc() {
|
||||
<em v-if="!table.fg">Design</em>
|
||||
</span>
|
||||
</label>
|
||||
<label class="f row-inline">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="!!table.hideChromeUnfocused"
|
||||
@change="store.setTableHideChromeUnfocused(table.id, chk($event))"
|
||||
/>
|
||||
<span>Ohne Rahmen/Kopfzeilen, wenn nicht ausgewählt</span>
|
||||
</label>
|
||||
|
||||
<!-- ---- current cell selection ---- -->
|
||||
<div v-if="hasCellSel" class="subsection">
|
||||
|
||||
@@ -19,12 +19,11 @@ import { useKeyboardPointMode } from '../composables/useKeyboardPointMode';
|
||||
import ContextMenu from './ContextMenu.vue';
|
||||
import FormulaSuggest from './FormulaSuggest.vue';
|
||||
|
||||
const props = defineProps<{ tableId: string }>();
|
||||
const props = defineProps<{ tableId: string; selected: boolean }>();
|
||||
|
||||
const store = useDocument();
|
||||
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
|
||||
@@ -33,7 +32,18 @@ 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);
|
||||
/** `hideChromeUnfocused` (Settings panel "Ohne Rahmen/Kopfzeilen, wenn nicht
|
||||
* ausgewählt") hides the row/column headers -- not just visually, their
|
||||
* reserved space collapses to 0 too -- whenever this table isn't the
|
||||
* selected canvas object (CanvasObject.vue hides its title bar the same
|
||||
* way, via its own `showChrome`). `headW`/`headerH` below are what
|
||||
* actually implement the collapse: every position calculation in this file
|
||||
* already goes through them (or through `rowTop`/`colLeft`, which do), so
|
||||
* making them 0 when hidden is the entire fix -- cells simply start at
|
||||
* (0,0) instead of past a header that isn't there. */
|
||||
const showHeaders = computed(() => props.selected || table.value?.hideChromeUnfocused !== true);
|
||||
const headW = computed(() => (showHeaders.value ? 44 : 0));
|
||||
const headerH = computed(() => (showHeaders.value ? (table.value?.rowHeight ?? DEFAULT_ROW_H) : 0));
|
||||
|
||||
/** Prefix sums of row heights (`rowCumHeights[i]` = total height of rows
|
||||
* `0..i-1`, excluding the header) -- lets `rowTop()` and the row
|
||||
@@ -127,7 +137,7 @@ const scrollStyle = computed(() =>
|
||||
|
||||
/** px from the table's left edge to the start of column `c` (incl. row header). */
|
||||
function colLeft(c: number): number {
|
||||
return HEAD_W + (colCumWidths.value[c] ?? 0);
|
||||
return headW.value + (colCumWidths.value[c] ?? 0);
|
||||
}
|
||||
|
||||
/** The internal scroll element of a fixed table (null for auto-size tables). */
|
||||
@@ -1284,8 +1294,8 @@ function onRowResizeUp() {
|
||||
|
||||
const totalWidth = computed(() => {
|
||||
const t = table.value;
|
||||
if (!t) return HEAD_W;
|
||||
let w = HEAD_W;
|
||||
if (!t) return headW.value;
|
||||
let w = headW.value;
|
||||
for (let c = 0; c < t.cols; c++) w += colWidth(c);
|
||||
return w;
|
||||
});
|
||||
@@ -1320,10 +1330,10 @@ defineExpose({
|
||||
<div class="tg-scroll" :style="scrollStyle" @scroll.passive="onScroll">
|
||||
<table class="grid" :style="{ width: totalWidth + 'px' }">
|
||||
<colgroup>
|
||||
<col :style="{ width: HEAD_W + 'px' }" />
|
||||
<col v-if="showHeaders" :style="{ width: headW + 'px' }" />
|
||||
<col v-for="c in table.cols" :key="'c' + (c - 1)" :style="{ width: colWidth(c - 1) + 'px' }" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<thead v-if="showHeaders">
|
||||
<tr :style="{ height: headerH + 'px' }">
|
||||
<th class="corner" @click="selectAll" />
|
||||
<th
|
||||
@@ -1344,6 +1354,7 @@ defineExpose({
|
||||
<tr v-for="item in renderedRows" :key="item.key" :style="{ height: (item.kind === 'row' ? rowH(item.r) : item.height) + 'px' }">
|
||||
<template v-if="item.kind === 'row'">
|
||||
<th
|
||||
v-if="showHeaders"
|
||||
class="row-head"
|
||||
:data-rowhead="item.r"
|
||||
:class="{ hot: rowHot(item.r), 'frozen-row': item.r < frozenRows, 'freeze-edge-bottom': item.r === frozenRows - 1 }"
|
||||
@@ -1388,7 +1399,7 @@ defineExpose({
|
||||
<!-- a spacer standing in for the rows currently scrolled out of
|
||||
view -- keeps the scrollbar's size/position correct without
|
||||
those rows' cells actually existing in the DOM -->
|
||||
<td v-else :colspan="table.cols + 1" class="row-spacer" />
|
||||
<td v-else :colspan="showHeaders ? table.cols + 1 : table.cols" class="row-spacer" />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -341,6 +341,7 @@ function normaliseObject(o: SheetObject): SheetObject {
|
||||
locale: localeOrUndef(o.locale),
|
||||
bg: colorOrUndef(o.bg),
|
||||
fg: colorOrUndef(o.fg),
|
||||
hideChromeUnfocused: boolOrUndef(o.hideChromeUnfocused),
|
||||
};
|
||||
}
|
||||
if (o.kind === 'text') {
|
||||
|
||||
@@ -2650,6 +2650,34 @@ describe('per-object background & text colour', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('table "hide chrome when unfocused" (clean-look presentation option)', () => {
|
||||
it('toggles hideChromeUnfocused and clears the field when turned off', () => {
|
||||
const s = fresh();
|
||||
const id = tableId(s, 'Umsatz');
|
||||
expect(s.tableById(id)!.hideChromeUnfocused).toBeUndefined();
|
||||
s.setTableHideChromeUnfocused(id, true);
|
||||
expect(s.tableById(id)!.hideChromeUnfocused).toBe(true);
|
||||
s.setTableHideChromeUnfocused(id, false);
|
||||
expect(s.tableById(id)!.hideChromeUnfocused).toBeUndefined(); // "unset", not stored as false
|
||||
});
|
||||
|
||||
it('survives a JSON round-trip; a non-boolean value on import is dropped', () => {
|
||||
const s = fresh();
|
||||
const id = tableId(s, 'Umsatz');
|
||||
s.setTableHideChromeUnfocused(id, true);
|
||||
const before = s.snapshot();
|
||||
s.replaceDocument(parseDocument(serializeDocument(before)));
|
||||
expect(s.snapshot()).toEqual(before);
|
||||
expect(s.tableById(id)!.hideChromeUnfocused).toBe(true);
|
||||
|
||||
const doc = JSON.parse(serializeDocument(s.snapshot()));
|
||||
const tbl = doc.sheets[0].objects.find((o: { id: string }) => o.id === id);
|
||||
tbl.hideChromeUnfocused = 'yes';
|
||||
s.replaceDocument(parseDocument(JSON.stringify(doc)));
|
||||
expect(s.tableById(id)!.hideChromeUnfocused).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('full-fidelity JSON export — backgrounds, formats, sheet settings', () => {
|
||||
/** Populate *every* persisted setting, then verify the export string carries
|
||||
* it and a round-trip is byte-for-byte identical. */
|
||||
@@ -2678,6 +2706,7 @@ describe('full-fidelity JSON export — backgrounds, formats, sheet settings', (
|
||||
s.setObjectLocale(umsatzId, 'fr-FR');
|
||||
s.setTableView(umsatzId, { mode: 'fixed', width: 520, height: 300, frozenRows: 1, frozenCols: 1 });
|
||||
s.setTableRowHeight(umsatzId, 30);
|
||||
s.setTableHideChromeUnfocused(umsatzId, true);
|
||||
s.setRowHeight(umsatzId, 4, 52);
|
||||
s.setColWidth(umsatzId, 0, 130);
|
||||
s.setRangeFormat(umsatzId, { col: 1, row: 1 }, { col: 3, row: 4 }, {
|
||||
@@ -2751,6 +2780,7 @@ describe('full-fidelity JSON export — backgrounds, formats, sheet settings', (
|
||||
rowHeights: { '4': 52 },
|
||||
colWidths: { '0': 130 },
|
||||
view: { mode: 'fixed', width: 520, height: 300, frozenRows: 1, frozenCols: 1 },
|
||||
hideChromeUnfocused: true,
|
||||
});
|
||||
expect(tbl.formats['1,1']).toEqual({
|
||||
align: 'right',
|
||||
|
||||
@@ -895,6 +895,13 @@ function createStore() {
|
||||
}
|
||||
markDirty();
|
||||
}
|
||||
function setTableHideChromeUnfocused(tableId: string, hide: boolean) {
|
||||
const t = tableById(tableId);
|
||||
if (!t) return;
|
||||
if (hide) t.hideChromeUnfocused = true;
|
||||
else delete t.hideChromeUnfocused;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function setCsvDelimiter(d: ',' | ';') {
|
||||
doc.csvDelimiter = d === ';' ? ';' : ',';
|
||||
@@ -2944,6 +2951,7 @@ function createStore() {
|
||||
selectObject,
|
||||
setObjectBg,
|
||||
setObjectTextColor,
|
||||
setTableHideChromeUnfocused,
|
||||
// table grid
|
||||
tableById,
|
||||
cellInput,
|
||||
|
||||
@@ -86,6 +86,13 @@ export interface TableObject {
|
||||
bg?: string;
|
||||
/** object default text colour override */
|
||||
fg?: string;
|
||||
/** when true, this table's title bar and row/column headers are only
|
||||
* shown while it's the selected canvas object -- otherwise (including
|
||||
* right after the document loads, before anything is selected) they're
|
||||
* hidden entirely, not just visually: the table's footprint shrinks to
|
||||
* just its cell grid. A presentation/"clean look" option; unset/false =
|
||||
* always shown, today's behaviour. */
|
||||
hideChromeUnfocused?: boolean;
|
||||
}
|
||||
|
||||
/** A chart object placed on a sheet canvas. Freely positioned and resized. */
|
||||
|
||||
Reference in New Issue
Block a user