feat: a table that may not break between rows moves whole

ODF style:may-break-between-rows="false" — probed: LibreOffice leaves four
lines of a page empty rather than split such a table, and splits the identical
one when the attribute says "true". pageBreaks now emits a single atomic leaf
for the wrapper unless the table is taller than a page, where the rule cannot
be met. Imported, exported and round-tripped; Word has no equivalent.
This commit is contained in:
Steffen Becker committed 2026-08-11 00:02:31 +00:00
1 parent 0d3506a51c
commit db8f4439b1
7 files changed
+88 -19

No files matched your search

+15
View File
@@ -85,6 +85,21 @@ export const PageBreak = Extension.create({
},
},
},
{
types: ['table'],
attributes: {
// ODF style:may-break-between-rows="false": no page break falls between two
// of the table's rows, so one too tall for the space left moves whole. A
// table taller than a page still breaks — the rule is then unsatisfiable.
keepRows: {
default: null,
parseHTML: (element: HTMLElement) =>
element.getAttribute('data-keep-rows') === 'true' ? true : null,
renderHTML: (attributes: Record<string, unknown>) =>
attributes.keepRows === true ? { 'data-keep-rows': 'true' } : {},
},
},
},
];
},
+13 -3
View File
@@ -652,13 +652,23 @@ export const PageBreaks = Extension.create({
? (Array.from(tbody.children) as HTMLElement[])
: [];
const realRows = rowEls.filter(r => r.tagName === 'TR' && !r.dataset?.pageBreakSpacer);
const spacerHeight = rowEls
.filter(r => r.dataset?.pageBreakSpacer)
.reduce((sum, r) => sum + r.offsetHeight, 0);
// No break may fall between the rows (ODF style:may-break-between-rows), so
// the table moves whole — unless it is taller than a page, where the rule
// cannot be met and LibreOffice breaks it anyway.
const keepRows = tableEl?.dataset?.keepRows === 'true'
&& wrapperEl.offsetHeight - spacerHeight <= contentHeight;
// Fallback: no rows found — measure the whole wrapper as one atomic leaf.
if (realRows.length === 0) {
if (realRows.length === 0 || keepRows) {
const top = naturalTopOf(wrapperEl);
cumulativeSpacerHeight += spacerHeight;
leaves.push({
el: wrapperEl,
kind: 'atomic',
naturalTop: naturalTopOf(wrapperEl),
naturalHeight: wrapperEl.offsetHeight,
naturalTop: top,
naturalHeight: wrapperEl.offsetHeight - spacerHeight,
});
return;
}
+3
View File
@@ -62,6 +62,9 @@ function applyTableStyleAttr(node: PMNode, table: HTMLElement): void {
const look = node.attrs.tableLook as string | null;
if (look != null) table.dataset.tableLook = look;
else delete table.dataset.tableLook;
// pageBreaks.ts reads this to keep the table whole (ODF style:may-break-between-rows).
if (node.attrs.keepRows === true) table.dataset.keepRows = 'true';
else delete table.dataset.keepRows;
}
// Also bypassed by the node view: the cell margins editor.css reads off the table.