fix: banded rows start on the first body row
The stripe began one row late: regionsAt counted the second body row as the first band. Probed on a five-row fixture with a red band1Horz — LibreOffice shades row 0 where the style paints no header region, and the row under the header where it does. The corpus cannot see this (no fixture uses the editor's own registry), so it stayed at 739; checked by eye and by the unit test's inverted expectations. A document with a table style assigned re-bands on its next open.
This commit is contained in:
1 parent
b6a8db8584
commit
04d1fed5f0
4 files changed
+29
-24
No files matched your search
@@ -39,6 +39,7 @@
|
||||
- Word's Table Style Options as six checkboxes beside the gallery (header row, total row, first/last column, banded rows/columns): an area is painted only when the style defines it *and* the table opts in, and turning the header row off shifts the banding by one row. The toolbar's header row / first column buttons drive the same flags, so both surfaces always agree
|
||||
- Table styles are edited in the same style manager (third tab: area picker for fill and font, outer / row line / column line controls, new / rename / remove), also reachable from the insert-table dropdown
|
||||
- Round-trip: the style name travels as an ODF `style:family="table"` (`style:parent-style-name`) and DOCX `w:tblStyle`, the options as ODF's `table:use-*-styles` and Word's `w:tblLook`. Fills and borders are materialized into the cells and the region's font is baked onto the runs, so the table looks right in Word/LibreOffice even though ODF has no banding concept — the style definitions themselves live in the app, as LibreOffice's AutoFormats do
|
||||
- Banded rows and columns start on the **first** body row/column, as in Word and LibreOffice — the stripe used to begin one row late. A document with a table style assigned re-bands itself when it is next opened
|
||||
- A paragraph style's own rule line and colored field are read from the style, not only from direct formatting, so a Word Title keeps the line under it — with the gap the file asks for (`w:pBdr w:space` / ODF `fo:padding`), on the ruled sides only
|
||||
- A Word table's indent follows the file's compatibility mode: up to Word 2010 `w:tblInd` is measured to the cell's text, so the table hangs its left cell margin into the page margin and the first column's text lines up with the body — from Word 2013 it is the table's edge
|
||||
- A Word file's own table style is read the other way round: its conditional areas (`w:tblStylePr` — header row, banded rows, first/last column) are baked into the cells on import, shading, borders and bold/colour alike, so a document styled with Word's Light Grid or Medium Shading opens looking like it does in Word and LibreOffice
|
||||
|
||||
@@ -31,7 +31,9 @@ We deliberately keep TipTap's `resizable: false` (so its own columnResizing plug
|
||||
`lastColumn` < `firstColumn` < `lastRow` < `headerRow`), with `wholeTable` as the base.
|
||||
`resolveTableCell(style, coords, look)` is the single source of truth for **all four**
|
||||
consumers (apply, CSS, export bake, preview tiles): it layers the matching regions, counts
|
||||
banding over *body* rows (a header row doesn't shift the stripes) and decides each border
|
||||
banding over *body* rows — a header row doesn't shift the stripes, and the **first** body
|
||||
row is the first stripe (probed: LibreOffice shades the row under the header, and row 0
|
||||
where the style paints no header) — and decides each border
|
||||
**per grid line** from both sides, so two facing cells never disagree once collapsed.
|
||||
**Table Style Options** are Word's per-table `w:tblLook`: a `TableLook` (one flag per region)
|
||||
stored space-separated in the table's `tableLook` attr, absent ⇒ `DEFAULT_TABLE_LOOK` (Word's
|
||||
|
||||
@@ -195,7 +195,8 @@ export type ResolvedTableCell = {
|
||||
};
|
||||
|
||||
// The conditional regions a grid position belongs to, in ascending precedence. Banding
|
||||
// counts body rows only, so a header row doesn't shift the stripes (as in Word).
|
||||
// counts body rows only, so a header row doesn't shift the stripes, and the **first**
|
||||
// body row is the first stripe — both probed against LibreOffice.
|
||||
function regionsAt(
|
||||
style: TableStyle, look: TableLook,
|
||||
row: number, col: number, rows: number, cols: number,
|
||||
@@ -206,8 +207,8 @@ function regionsAt(
|
||||
const bodyRow = row - (on('headerRow') ? 1 : 0);
|
||||
const bodyCol = col - (on('firstColumn') ? 1 : 0);
|
||||
const matches: Record<TableRegion, boolean> = {
|
||||
bandedColumn: bodyCol >= 0 && bodyCol % 2 === 1,
|
||||
bandedRow: bodyRow >= 0 && bodyRow % 2 === 1,
|
||||
bandedColumn: bodyCol >= 0 && bodyCol % 2 === 0,
|
||||
bandedRow: bodyRow >= 0 && bodyRow % 2 === 0,
|
||||
lastColumn: col === cols - 1,
|
||||
firstColumn: col === 0,
|
||||
lastRow: row === rows - 1,
|
||||
|
||||
@@ -51,15 +51,15 @@ describe('resolveTableCell', () => {
|
||||
expect(corner.fill).toBe('#EEEEEE');
|
||||
});
|
||||
|
||||
it('counts banding over body rows, so a header row does not shift the stripes', () => {
|
||||
expect(at(banded, 1, 1).fill).toBe(null); // first body row
|
||||
expect(at(banded, 2, 1).fill).toBe('#FAFAFA');
|
||||
expect(at(banded, 3, 1).fill).toBe(null);
|
||||
it('starts the stripes at the first body row, past a header row', () => {
|
||||
expect(at(banded, 1, 1).fill).toBe('#FAFAFA'); // first body row
|
||||
expect(at(banded, 2, 1).fill).toBe(null);
|
||||
expect(at(banded, 3, 1).fill).toBe('#FAFAFA');
|
||||
|
||||
// Without a header region the stripes start one row higher.
|
||||
// Without a header region the body starts at row 0, and so do the stripes.
|
||||
const noHeader: TableStyleDef = { ...banded, regions: { bandedRow: { fill: '#FAFAFA' } } };
|
||||
expect(at(noHeader, 0, 0).fill).toBe(null);
|
||||
expect(at(noHeader, 1, 0).fill).toBe('#FAFAFA');
|
||||
expect(at(noHeader, 0, 0).fill).toBe('#FAFAFA');
|
||||
expect(at(noHeader, 1, 0).fill).toBe(null);
|
||||
});
|
||||
|
||||
it('gives both cells of a grid line the same border', () => {
|
||||
@@ -105,7 +105,8 @@ describe('table style options (Word\'s tblLook)', () => {
|
||||
it('only paints a region the table opts into', () => {
|
||||
const off: TableLook = { ...ALL_ON, headerRow: false };
|
||||
expect(at(banded, 0, 1).fill).toBe('#EEEEEE');
|
||||
expect(at(banded, 0, 1, 4, 2, off).fill).toBe(null);
|
||||
// The header's own fill is gone; row 0 is the body's first stripe now.
|
||||
expect(at(banded, 0, 1, 4, 2, off).fill).toBe('#FAFAFA');
|
||||
// The header's rule goes with it, from both sides of that grid line.
|
||||
expect(at(banded, 0, 0, 4, 2, off).borders.borderBottom).toBe('none');
|
||||
expect(at(banded, 1, 0, 4, 2, off).borders.borderTop).toBe('none');
|
||||
@@ -113,13 +114,13 @@ describe('table style options (Word\'s tblLook)', () => {
|
||||
|
||||
it('shifts the banding when the header row is switched off', () => {
|
||||
const off: TableLook = { ...ALL_ON, headerRow: false };
|
||||
// Header on: row 0 is the header, so the first stripe is row 2.
|
||||
// Header on: row 0 is the header, so the first stripe is row 1.
|
||||
expect([0, 1, 2, 3].map(r => at(banded, r, 1).fill)).toEqual([
|
||||
'#EEEEEE', null, '#FAFAFA', null,
|
||||
'#EEEEEE', '#FAFAFA', null, '#FAFAFA',
|
||||
]);
|
||||
// Header off: the body starts at row 0, so the stripes move up one row.
|
||||
expect([0, 1, 2, 3].map(r => at(banded, r, 1, 4, 2, off).fill)).toEqual([
|
||||
null, '#FAFAFA', null, '#FAFAFA',
|
||||
'#FAFAFA', null, '#FAFAFA', null,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -227,7 +228,7 @@ describe('header toggles and style options are one state', () => {
|
||||
editor.commands.toggleHeaderRowStyle();
|
||||
expect(parseTableLook(table(editor).attrs.tableLook).headerRow).toBe(false);
|
||||
expect(isHeaderStyled(editor.state, 'row')).toBe(false);
|
||||
expect(table(editor).content[0].content[0].attrs.backgroundColor).toBe(null);
|
||||
expect(table(editor).content[0].content[0].attrs.backgroundColor).toBe('#FAFAFA');
|
||||
|
||||
// And the other way round: setting the option updates the button's state.
|
||||
editor.commands.setTableLook('headerRow', true);
|
||||
@@ -260,7 +261,7 @@ describe('setTableStyle', () => {
|
||||
expect(t.content[0].content[1].attrs.region).toBe('headerRow');
|
||||
expect(t.content[0].content[0].attrs.borderBottom).toBe(RULE);
|
||||
expect(t.content[1].content[0].attrs.borderTop).toBe(RULE);
|
||||
expect(fills(editor)).toEqual(['#EEEEEE', null, '#FAFAFA']);
|
||||
expect(fills(editor)).toEqual(['#EEEEEE', '#FAFAFA', null]);
|
||||
|
||||
// Applying again changes nothing — the re-band plugin depends on this.
|
||||
const before = JSON.stringify(editor.getJSON());
|
||||
@@ -273,11 +274,11 @@ describe('setTableStyle', () => {
|
||||
const editor = makeEditor();
|
||||
editor.commands.focus('start');
|
||||
editor.commands.setTableStyle('Test Bands');
|
||||
expect(fills(editor)).toEqual(['#EEEEEE', null, '#FAFAFA']);
|
||||
expect(fills(editor)).toEqual(['#EEEEEE', '#FAFAFA', null]);
|
||||
|
||||
// Cursor is in the header row; a row added after it shifts every stripe below.
|
||||
editor.commands.addRowAfter();
|
||||
expect(fills(editor)).toEqual(['#EEEEEE', null, '#FAFAFA', null]);
|
||||
expect(fills(editor)).toEqual(['#EEEEEE', '#FAFAFA', null, '#FAFAFA']);
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
@@ -341,7 +342,7 @@ describe('setTableStyle', () => {
|
||||
const t = table(editor);
|
||||
expect(t.content[1].content.length).toBe(1);
|
||||
expect(t.content[1].content[0].attrs.colspan).toBe(2);
|
||||
expect(t.content[1].content[0].attrs.region).toBe('firstColumn');
|
||||
expect(t.content[1].content[0].attrs.region).toBe('bandedRow firstColumn');
|
||||
// Its bottom is the boundary to the banded row below, its right the table edge.
|
||||
expect(t.content[1].content[0].attrs.borderRight).toBe(null);
|
||||
editor.destroy();
|
||||
@@ -351,16 +352,16 @@ describe('setTableStyle', () => {
|
||||
const editor = makeEditor(4);
|
||||
editor.commands.focus('start');
|
||||
editor.commands.setTableStyle('Test Bands');
|
||||
expect(fills(editor)).toEqual(['#EEEEEE', null, '#FAFAFA', null]);
|
||||
expect(fills(editor)).toEqual(['#EEEEEE', '#FAFAFA', null, '#FAFAFA']);
|
||||
|
||||
// Header row off: it loses its fill and the stripes move up a row.
|
||||
editor.commands.setTableLook('headerRow', false);
|
||||
expect(fills(editor)).toEqual([null, '#FAFAFA', null, '#FAFAFA']);
|
||||
expect(table(editor).content[0].content[0].attrs.region).toBe('firstColumn');
|
||||
expect(fills(editor)).toEqual(['#FAFAFA', null, '#FAFAFA', null]);
|
||||
expect(table(editor).content[0].content[0].attrs.region).toBe('bandedRow firstColumn');
|
||||
|
||||
// And back again.
|
||||
editor.commands.setTableLook('headerRow', true);
|
||||
expect(fills(editor)).toEqual(['#EEEEEE', null, '#FAFAFA', null]);
|
||||
expect(fills(editor)).toEqual(['#EEEEEE', '#FAFAFA', null, '#FAFAFA']);
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user