fix: table page break
This commit is contained in:
1 parent
44dc72c890
commit
27e5eb6e33
2 files changed
+121
-78
No files matched your search
@@ -224,7 +224,7 @@
|
||||
overflow: hidden;
|
||||
font-family: var(--font-serif);
|
||||
font-size: 12pt;
|
||||
color: var(--color-text);
|
||||
color: var(--color-page-text);
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
@@ -235,11 +235,6 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Inactive zones render slightly muted, like Word's greyed header/footer. */
|
||||
.hf-zone:not(.hf-active) {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* Empty zone: invisible until hovered, then show a faint label hint. */
|
||||
.hf-empty::before {
|
||||
content: attr(data-hf-label);
|
||||
@@ -259,7 +254,6 @@
|
||||
.hf-active {
|
||||
outline: 1px dashed var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Strip the page margins from the rendered header/footer paragraph. */
|
||||
|
||||
+120
-71
@@ -388,17 +388,29 @@ export const PageBreaks = Extension.create({
|
||||
}
|
||||
}
|
||||
allRects.sort((a, b) => a.top - b.top);
|
||||
// Estimate the line advance (top-to-top distance) from the gaps between
|
||||
// rects, ignoring near-zero gaps (multiple runs on one line).
|
||||
const gaps: number[] = [];
|
||||
for (let i = 1; i < allRects.length; i++) {
|
||||
const g = allRects[i].top - allRects[i - 1].top;
|
||||
if (g > 2) gaps.push(g);
|
||||
}
|
||||
gaps.sort((a, b) => a - b);
|
||||
const advance = gaps.length ? gaps[gaps.length >> 1] : 0;
|
||||
// Group rects into lines by top proximity, not vertical overlap: decorative
|
||||
// fonts (e.g. Trattatello) render ink boxes taller than the line advance, so
|
||||
// an overlap test chains whole paragraphs into one phantom line. Only the top
|
||||
// is used downstream; same-line runs sit within a fraction of the advance.
|
||||
const tol = advance > 0 ? advance * 0.6 : 3;
|
||||
const lines: { top: number; bottom: number }[] = [];
|
||||
let lineTop = -Infinity;
|
||||
for (const r of allRects) {
|
||||
const last = lines[lines.length - 1];
|
||||
// Rects are sorted by top. Merge into the current line when they overlap
|
||||
// vertically, so a smaller-font inline run shares its neighbours' line
|
||||
// instead of forming a phantom line. The −1px tolerance separates touching rects.
|
||||
if (last && r.top < last.bottom - 1) {
|
||||
last.top = Math.min(last.top, r.top);
|
||||
last.bottom = Math.max(last.bottom, r.bottom);
|
||||
} else {
|
||||
if (r.top - lineTop > tol) {
|
||||
lines.push({ top: r.top, bottom: r.bottom });
|
||||
lineTop = r.top;
|
||||
} else {
|
||||
const last = lines[lines.length - 1];
|
||||
last.bottom = Math.max(last.bottom, r.bottom);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
@@ -715,61 +727,93 @@ export const PageBreaks = Extension.create({
|
||||
const contentStart = pageContentStart(page, vm.top, CYCLE_PX);
|
||||
const contentEnd = pageContentEnd(page, vm.top, CONTENT_HEIGHT, CYCLE_PX);
|
||||
|
||||
let spacerHeight = 0;
|
||||
let spacerDocPos: number | null = null;
|
||||
let spacerRow: { columns: number } | null = null;
|
||||
let bandOpenY: number | null = null;
|
||||
let reason = 'fits';
|
||||
// A leaf may need several spacers: a splittable block taller than one
|
||||
// page crosses multiple boundaries, one break each.
|
||||
const breaks: {
|
||||
height: number;
|
||||
docPos: number | null;
|
||||
row: { columns: number } | null;
|
||||
bandOpenY: number | null;
|
||||
reason: string;
|
||||
}[] = [];
|
||||
// Set when a leaf overflows but no spacer can bridge it (block > one page).
|
||||
let noPushReason: string | null = null;
|
||||
|
||||
if (effectiveTop < contentStart && i > 0) {
|
||||
spacerHeight = contentStart - effectiveTop;
|
||||
({ docPos: spacerDocPos, row: spacerRow } = leafSpacer(leaf));
|
||||
bandOpenY = contentStart;
|
||||
reason = 'pre-leaf-push-to-content-start';
|
||||
const { docPos, row } = leafSpacer(leaf);
|
||||
breaks.push({
|
||||
height: contentStart - effectiveTop,
|
||||
docPos,
|
||||
row,
|
||||
bandOpenY: contentStart,
|
||||
reason: 'pre-leaf-push-to-content-start',
|
||||
});
|
||||
} else if (effectiveTop >= contentEnd) {
|
||||
spacerHeight = pageContentStart(page + 1, vm.top, CYCLE_PX) - effectiveTop;
|
||||
({ docPos: spacerDocPos, row: spacerRow } = leafSpacer(leaf));
|
||||
bandOpenY = pageContentStart(page + 1, vm.top, CYCLE_PX);
|
||||
reason = 'leaf-jump-to-next-page';
|
||||
const target = pageContentStart(page + 1, vm.top, CYCLE_PX);
|
||||
const { docPos, row } = leafSpacer(leaf);
|
||||
breaks.push({
|
||||
height: target - effectiveTop,
|
||||
docPos,
|
||||
row,
|
||||
bandOpenY: target,
|
||||
reason: 'leaf-jump-to-next-page',
|
||||
});
|
||||
} else if (effectiveBottom > contentEnd) {
|
||||
if (leaf.kind === 'atomic') {
|
||||
if (leaf.naturalHeight <= CONTENT_HEIGHT) {
|
||||
spacerHeight = pageContentStart(page + 1, vm.top, CYCLE_PX) - effectiveTop;
|
||||
({ docPos: spacerDocPos, row: spacerRow } = leafSpacer(leaf));
|
||||
bandOpenY = pageContentStart(page + 1, vm.top, CYCLE_PX);
|
||||
reason = 'atomic-push-to-next-page';
|
||||
const target = pageContentStart(page + 1, vm.top, CYCLE_PX);
|
||||
const { docPos, row } = leafSpacer(leaf);
|
||||
breaks.push({
|
||||
height: target - effectiveTop,
|
||||
docPos,
|
||||
row,
|
||||
bandOpenY: target,
|
||||
reason: 'atomic-push-to-next-page',
|
||||
});
|
||||
} else {
|
||||
reason = 'atomic-too-tall-no-push';
|
||||
noPushReason = 'atomic-too-tall-no-push';
|
||||
}
|
||||
} else {
|
||||
const split = findLineSplit(leaf.el, contentEnd - effectiveTop, scale);
|
||||
if (split === null) {
|
||||
// A splittable leaf taller than one page crosses several boundaries:
|
||||
// keep splitting until the rest fits. Boundaries are in the leaf's
|
||||
// spacer-free natural coords, so successive splits stay correct.
|
||||
let boundaryNatural = contentEnd - effectiveTop;
|
||||
let targetPage = page + 1;
|
||||
let extraShift = 0;
|
||||
while (boundaryNatural < leaf.naturalHeight) {
|
||||
const split = findLineSplit(leaf.el, boundaryNatural, scale);
|
||||
if (split === null) break;
|
||||
const target = pageContentStart(targetPage, vm.top, CYCLE_PX);
|
||||
const h = target - (effectiveTop + extraShift + split.naturalLineTop);
|
||||
if (h <= 0) break;
|
||||
breaks.push({
|
||||
height: h,
|
||||
docPos: split.docPos,
|
||||
row: null,
|
||||
bandOpenY: target,
|
||||
reason: 'line-split',
|
||||
});
|
||||
extraShift += h;
|
||||
boundaryNatural = split.naturalLineTop + CONTENT_HEIGHT;
|
||||
targetPage++;
|
||||
}
|
||||
if (breaks.length === 0) {
|
||||
if (leaf.naturalHeight <= CONTENT_HEIGHT) {
|
||||
spacerHeight = pageContentStart(page + 1, vm.top, CYCLE_PX) - effectiveTop;
|
||||
spacerDocPos = preLeafDocPos(leaf.el);
|
||||
bandOpenY = pageContentStart(page + 1, vm.top, CYCLE_PX);
|
||||
reason = 'split-fallback-push-whole-leaf';
|
||||
const target = pageContentStart(page + 1, vm.top, CYCLE_PX);
|
||||
breaks.push({
|
||||
height: target - effectiveTop,
|
||||
docPos: preLeafDocPos(leaf.el),
|
||||
row: null,
|
||||
bandOpenY: target,
|
||||
reason: 'split-fallback-push-whole-leaf',
|
||||
});
|
||||
} else {
|
||||
reason = 'splittable-too-tall-no-push';
|
||||
noPushReason = 'splittable-too-tall-no-push';
|
||||
}
|
||||
} else {
|
||||
spacerHeight = pageContentStart(page + 1, vm.top, CYCLE_PX) - (effectiveTop + split.naturalLineTop);
|
||||
spacerDocPos = split.docPos;
|
||||
bandOpenY = pageContentStart(page + 1, vm.top, CYCLE_PX);
|
||||
reason = 'line-split';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A break needs a close/open band when it sits inside the continuous table
|
||||
// box, where the outer borders bleed through the gap: inTableCell, or a
|
||||
// between-rows push (spacerRow). First-row pushes move the whole table → no band.
|
||||
const inBand = !!leaf.inTableCell || spacerRow !== null;
|
||||
if (inBand && bandOpenY !== null && spacerHeight > 0) {
|
||||
const key = Math.round(bandOpenY);
|
||||
if (!tableBands.has(key)) tableBands.set(key, bandOpenY);
|
||||
}
|
||||
|
||||
leavesDebug.push({
|
||||
index: i,
|
||||
tag: leaf.el.tagName,
|
||||
@@ -793,33 +837,38 @@ export const PageBreaks = Extension.create({
|
||||
overflowsPageEnd: effectiveBottom > contentEnd,
|
||||
});
|
||||
|
||||
if (spacerHeight > 0 && spacerDocPos !== null) {
|
||||
// Round to integer px: the browser renders the spacer at integer
|
||||
// offsetHeight anyway, so an unrounded value would make the model disagree
|
||||
// with the DOM next pass → 1px shake on every keystroke at non-100% zoom.
|
||||
const h = Math.round(spacerHeight);
|
||||
if (h > 0) {
|
||||
placements.push({
|
||||
docPos: spacerDocPos,
|
||||
height: h,
|
||||
row: spacerRow,
|
||||
});
|
||||
placementsDebug.push({
|
||||
leafIndex: i,
|
||||
docPos: spacerDocPos,
|
||||
height: h,
|
||||
reason,
|
||||
spacerKind: spacerRow ? 'table-row' : 'block',
|
||||
columns: spacerRow ? spacerRow.columns : null,
|
||||
});
|
||||
cumulativeShift += h;
|
||||
// Round each spacer to integer px (the browser renders spacers at integer
|
||||
// height, so an unrounded model shakes 1px per keystroke at non-100% zoom).
|
||||
let placedAny = false;
|
||||
for (const br of breaks) {
|
||||
if (br.docPos === null) continue;
|
||||
const h = Math.round(br.height);
|
||||
if (h <= 0) continue;
|
||||
// In-cell or between-rows breaks sit inside the continuous table box,
|
||||
// whose borders bleed through the gap, so they need a close/open band.
|
||||
const inBand = !!leaf.inTableCell || br.row !== null;
|
||||
if (inBand && br.bandOpenY !== null) {
|
||||
const key = Math.round(br.bandOpenY);
|
||||
if (!tableBands.has(key)) tableBands.set(key, br.bandOpenY);
|
||||
}
|
||||
} else if (reason !== 'fits') {
|
||||
placements.push({ docPos: br.docPos, height: h, row: br.row });
|
||||
placementsDebug.push({
|
||||
leafIndex: i,
|
||||
docPos: spacerDocPos,
|
||||
height: spacerHeight,
|
||||
reason,
|
||||
docPos: br.docPos,
|
||||
height: h,
|
||||
reason: br.reason,
|
||||
spacerKind: br.row ? 'table-row' : 'block',
|
||||
columns: br.row ? br.row.columns : null,
|
||||
});
|
||||
cumulativeShift += h;
|
||||
placedAny = true;
|
||||
}
|
||||
if (!placedAny && noPushReason !== null) {
|
||||
placementsDebug.push({
|
||||
leafIndex: i,
|
||||
docPos: null,
|
||||
height: 0,
|
||||
reason: noPushReason,
|
||||
spacerKind: 'none',
|
||||
columns: null,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user