fix: remove grey table lines spanning the page break
This commit is contained in:
1 parent
c641dda4e0
commit
6931054b02
2 files changed
+146
-67
No files matched your search
@@ -121,8 +121,10 @@
|
||||
}
|
||||
|
||||
// --- Table page-break overlay ---
|
||||
// pageBreaks.ts reports (via pm-pagecount) the in-cell table breaks in document px
|
||||
// relative to .tiptap's top. We render the overlay in a .band-layer inside .paper
|
||||
// pageBreaks.ts reports (via pm-pagecount) where a single continuous table box crosses
|
||||
// a page boundary (a too-tall cell's content flowing across pages, or a between-rows
|
||||
// push), each as one band in document px relative to .tiptap's top. We render the
|
||||
// overlay in a .band-layer inside .paper
|
||||
// (in document px) so the same `transform: scale()` scales it identically to the
|
||||
// .tiptap page background. Two pieces per break:
|
||||
// • band — a solid page-coloured mask (content width) hiding the table's borders
|
||||
@@ -143,51 +145,30 @@
|
||||
if (gapStripeStyles.length) gapStripeStyles = [];
|
||||
return;
|
||||
}
|
||||
const tRect = tiptap.getBoundingClientRect();
|
||||
const z = appliedZoom / 100;
|
||||
const pageWidth = tiptap.offsetWidth; // unscaled doc px = full page width
|
||||
// A too-tall row break produces one in-cell spacer per column, all tagged with
|
||||
// the same band key. Group them so each band can span its columns' real gaps.
|
||||
const spacersByKey = new Map<string, HTMLElement[]>();
|
||||
for (const sp of Array.from(
|
||||
tiptap.querySelectorAll<HTMLElement>('[data-page-break-spacer-incell]'),
|
||||
)) {
|
||||
const key = sp.dataset.pageBreakBoundary;
|
||||
if (key === undefined) continue;
|
||||
const arr = spacersByKey.get(key);
|
||||
if (arr) arr.push(sp);
|
||||
else spacersByKey.set(key, [sp]);
|
||||
}
|
||||
// Everything below is in document px (relative to .tiptap's top). The .band-layer
|
||||
// lives inside the scaled .paper, so the transform applies the scaling — no `* z`.
|
||||
const border = 1; // 1px page-edge line, like the CSS .tiptap background
|
||||
bandStyles = tableBandsDoc.map((b) => {
|
||||
// Anchor the band's vertical extent to the real rendered extent of this break's
|
||||
// spacers (converted to doc px), so its close/open lines hug the actual content
|
||||
// and the mask never eats content. Across columns: top = the lowest column's
|
||||
// content end (max spacer top), bottom = the highest column's resume (min spacer
|
||||
// bottom). Fall back to the theoretical placement when no spacer is found.
|
||||
const group = spacersByKey.get(String(b.key));
|
||||
let top: number;
|
||||
let height: number;
|
||||
if (group && group.length > 0) {
|
||||
let maxTop = -Infinity; // lowest column's content end
|
||||
let minBottom = Infinity; // highest column's resume
|
||||
for (const sp of group) {
|
||||
const r = sp.getBoundingClientRect();
|
||||
maxTop = Math.max(maxTop, (r.top - tRect.top) / z);
|
||||
minBottom = Math.min(minBottom, (r.bottom - tRect.top) / z);
|
||||
}
|
||||
top = maxTop;
|
||||
height = Math.max(0, minBottom - maxTop);
|
||||
} else {
|
||||
top = b.closeY;
|
||||
height = b.height;
|
||||
}
|
||||
// Solid white mask over the table band — hides the table's vertical borders
|
||||
// bleeding through the page's bottom/top margins. The gap region is repainted
|
||||
// by the full-width stripe below, so a solid fill here is fine.
|
||||
return { top, left: b.left, width: b.width, height };
|
||||
// Use pure page geometry: the band spans exactly the inter-page region — from the
|
||||
// closing page's content-bottom (closeY) down through margin/gap/margin to the next
|
||||
// page's content-top. This is precisely where the table must show no vertical
|
||||
// borders, and pagination guarantees no real content ever sits there, so a solid
|
||||
// mask here can never eat content. (Anchoring the band to the break's spacers
|
||||
// instead under-covered atomic-push / leaf-jump breaks, where the spacer renders
|
||||
// below content-bottom — after the empty gap left by the pushed block — leaving the
|
||||
// top of the page's bottom margin unmasked and the borders bleeding through it.)
|
||||
// Solid page-coloured fill; the gap region is repainted by the stripe below.
|
||||
// Overhang 1px past each side: the table's outer L/R borders are centred on the
|
||||
// content edges (b.left and b.left+b.width), so their outer halves sit just beyond
|
||||
// the content box. Widening the mask by `border` on each side swallows them whole
|
||||
// instead of leaving a half-pixel sliver bleeding into the page margins.
|
||||
return {
|
||||
top: b.closeY,
|
||||
left: b.left - border,
|
||||
width: b.width + 2 * border,
|
||||
height: b.height,
|
||||
};
|
||||
});
|
||||
// Full-page-width gap stripe: the dark page gap + its two page-edge lines, at the
|
||||
// true surface bottom (closeY + marginBottom = N·cycle − gap). Covers the entire
|
||||
|
||||
+123
-25
@@ -55,17 +55,49 @@ export type PageBreakDebugSnapshot = {
|
||||
// Unscaled document px relative to .tiptap's top — same values handed to
|
||||
// Editor.svelte via pm-pagecount to render the mask + gap overlay.
|
||||
tableBreakBands: TableBreakBand[];
|
||||
// Live snapshot of the *rendered* table-break overlay + table geometry (captured from
|
||||
// the DOM at dump time), so we can tell whether the band-layer actually masks the
|
||||
// table's borders bleeding through each gap. All rects are unscaled document px
|
||||
// relative to .tiptap's top-left — the same space as `leaves`/`tableBreakBands` — so
|
||||
// they line up directly. null until the first dump after the overlay mounts.
|
||||
overlay: OverlayDebug | null;
|
||||
};
|
||||
|
||||
// One in-cell table page break. All values are unscaled document px relative to
|
||||
// .tiptap's top; Editor.svelte renders the mask + gap overlay from them inside the
|
||||
// zoomed .paper. Carried on the pm-pagecount event detail.
|
||||
export type OverlayDebug = {
|
||||
scale: number; // measured .paper transform scale (1 at 100% zoom)
|
||||
paperFound: boolean;
|
||||
bandLayerFound: boolean;
|
||||
// .band-layer's top/left in doc px relative to .tiptap. Should be ~(0,0): a non-zero
|
||||
// value means the overlay's coordinate origin is offset from the page, so every band
|
||||
// is painted off its intended gap (a prime suspect for "mask present but not covering").
|
||||
bandLayerOrigin: { top: number; left: number } | null;
|
||||
// Resolved page colours, to confirm the mask is opaque and the right colour.
|
||||
cssVars: { pageBg: string; pageBorder: string; bg: string };
|
||||
// Rendered .table-break-band mask rects (one per band) + their paint-relevant styles.
|
||||
bands: Array<{
|
||||
top: number; left: number; right: number; bottom: number;
|
||||
width: number; height: number; background: string; zIndex: string; position: string;
|
||||
}>;
|
||||
// Rendered .page-gap-stripe rects.
|
||||
stripes: Array<{ top: number; left: number; right: number; bottom: number; height: number }>;
|
||||
// Every <table> in the doc: its rendered box + the x of each vertical cell border
|
||||
// (the lines that bleed). A table taller than CONTENT_HEIGHT spans page gaps, so its
|
||||
// verticalEdges are exactly the lines a band must cover in each gap.
|
||||
tables: Array<{
|
||||
top: number; left: number; right: number; bottom: number;
|
||||
borderColor: string;
|
||||
verticalEdges: number[];
|
||||
}>;
|
||||
};
|
||||
|
||||
// One page boundary crossed by a single continuous table box. All values are unscaled
|
||||
// document px relative to .tiptap's top; Editor.svelte renders the mask + gap overlay
|
||||
// from them inside the zoomed .paper. Carried on the pm-pagecount event detail.
|
||||
export type TableBreakBand = {
|
||||
// Rounded openY = page-content-top where the cell content resumes. Matches the
|
||||
// `data-page-break-boundary` attribute on this break's in-cell spacers, so
|
||||
// Editor.svelte can group the (one per column) spacers belonging to this band.
|
||||
// Rounded openY (page-content-top where the table resumes) — the dedup id, so two
|
||||
// columns breaking at the same boundary collapse to one band.
|
||||
key: number;
|
||||
closeY: number; // content-end of the closing page (band/mask top fallback)
|
||||
closeY: number; // content-end of the closing page (band/mask top)
|
||||
height: number; // bandSpan = marginBottom + gap + marginTop
|
||||
left: number; // content-area left (mask left)
|
||||
width: number; // content-area width (mask width)
|
||||
@@ -206,9 +238,79 @@ export const PageBreaks = Extension.create({
|
||||
viewportTop: r.top,
|
||||
};
|
||||
});
|
||||
return { ...snap, renderedSpacers };
|
||||
return { ...snap, renderedSpacers, overlay: captureOverlay(tipRect) };
|
||||
});
|
||||
|
||||
// Capture the rendered table-break overlay + table geometry from the live DOM.
|
||||
// The overlay (.band-layer) is a sibling of the editor's host inside .paper, so we
|
||||
// reach it by climbing to .paper. Viewport rects are converted to unscaled doc px
|
||||
// relative to .tiptap's top-left (divide by the measured paper scale) so they sit
|
||||
// in the same coordinate space as the bands/leaves and can be compared directly.
|
||||
function captureOverlay(tipRect: DOMRect): OverlayDebug {
|
||||
const dom = editorView.dom;
|
||||
const offH = dom.offsetHeight;
|
||||
const scale = offH ? tipRect.height / offH : 1;
|
||||
const toDoc = (r: DOMRect) => ({
|
||||
top: (r.top - tipRect.top) / scale,
|
||||
bottom: (r.bottom - tipRect.top) / scale,
|
||||
left: (r.left - tipRect.left) / scale,
|
||||
right: (r.right - tipRect.left) / scale,
|
||||
});
|
||||
const paper = dom.closest('.paper') as HTMLElement | null;
|
||||
const bandLayer = paper?.querySelector('.band-layer') as HTMLElement | null;
|
||||
const bandEls = paper
|
||||
? Array.from(paper.querySelectorAll<HTMLElement>('.table-break-band'))
|
||||
: [];
|
||||
const stripeEls = paper
|
||||
? Array.from(paper.querySelectorAll<HTMLElement>('.page-gap-stripe'))
|
||||
: [];
|
||||
const cs = getComputedStyle(dom);
|
||||
return {
|
||||
scale,
|
||||
paperFound: !!paper,
|
||||
bandLayerFound: !!bandLayer,
|
||||
bandLayerOrigin: bandLayer
|
||||
? (() => { const d = toDoc(bandLayer.getBoundingClientRect()); return { top: d.top, left: d.left }; })()
|
||||
: null,
|
||||
cssVars: {
|
||||
pageBg: cs.getPropertyValue('--color-page-bg').trim(),
|
||||
pageBorder: cs.getPropertyValue('--color-page-border').trim(),
|
||||
bg: cs.getPropertyValue('--color-bg').trim(),
|
||||
},
|
||||
bands: bandEls.map((e) => {
|
||||
const d = toDoc(e.getBoundingClientRect());
|
||||
const ecs = getComputedStyle(e);
|
||||
return {
|
||||
top: d.top, left: d.left, right: d.right, bottom: d.bottom,
|
||||
width: d.right - d.left, height: d.bottom - d.top,
|
||||
background: ecs.backgroundColor, zIndex: ecs.zIndex, position: ecs.position,
|
||||
};
|
||||
}),
|
||||
stripes: stripeEls.map((e) => {
|
||||
const d = toDoc(e.getBoundingClientRect());
|
||||
return { top: d.top, left: d.left, right: d.right, bottom: d.bottom, height: d.bottom - d.top };
|
||||
}),
|
||||
tables: Array.from(dom.querySelectorAll<HTMLElement>('table')).map((t) => {
|
||||
const d = toDoc(t.getBoundingClientRect());
|
||||
// x of each vertical cell border: use the first body row's cells' left edges
|
||||
// plus the row's right edge. These are the lines that bleed across gaps.
|
||||
const firstRow = t.querySelector('tbody > tr:not([data-page-break-spacer])') as HTMLElement | null;
|
||||
const cells = firstRow
|
||||
? (Array.from(firstRow.children) as HTMLElement[]).filter(c => c.tagName === 'TD' || c.tagName === 'TH')
|
||||
: [];
|
||||
const verticalEdges: number[] = [];
|
||||
for (const c of cells) verticalEdges.push(toDoc(c.getBoundingClientRect()).left);
|
||||
if (cells.length) verticalEdges.push(toDoc(cells[cells.length - 1].getBoundingClientRect()).right);
|
||||
const firstCell = cells[0];
|
||||
return {
|
||||
top: d.top, left: d.left, right: d.right, bottom: d.bottom,
|
||||
borderColor: firstCell ? getComputedStyle(firstCell).borderTopColor : '',
|
||||
verticalEdges,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function docPosBeforeElement(el: HTMLElement): number | null {
|
||||
const parent = el.parentNode;
|
||||
if (!parent) return null;
|
||||
@@ -620,13 +722,6 @@ export const PageBreaks = Extension.create({
|
||||
docPos: number;
|
||||
height: number;
|
||||
row: { columns: number } | null;
|
||||
// True when the spacer sits inside a too-tall table cell; marked on the
|
||||
// rendered DOM so Editor.svelte can align each close/open band to the
|
||||
// spacer's actual position.
|
||||
incell: boolean;
|
||||
// Rounded openY of this break's band (null for non-cell spacers); written
|
||||
// to the spacer DOM so a band can find all its columns' spacers.
|
||||
bandKey: number | null;
|
||||
}[] = [];
|
||||
// Full-width close/open borders for in-cell table breaks (see Leaf.inTableCell).
|
||||
// openY is the page-content-top where the cell content resumes; the band runs
|
||||
@@ -705,8 +800,18 @@ export const PageBreaks = Extension.create({
|
||||
}
|
||||
}
|
||||
|
||||
// Record a close/open band for breaks inside a too-tall table cell.
|
||||
if (leaf.inTableCell && bandOpenY !== null && spacerHeight > 0) {
|
||||
// A leaf needs a close/open band whenever its break sits inside the single
|
||||
// continuous table box, where the table's outer left/right borders bleed
|
||||
// through the borderless page gap:
|
||||
// • inTableCell — a too-tall cell whose content flows across the page, or
|
||||
// • a between-rows push (spacerRow set) — the bridging spacer <tr> is
|
||||
// borderless, but the browser still paints the table's outer side
|
||||
// borders continuously across it (grey lines through the margins + gap).
|
||||
// Both get the same page-coloured mask + gap stripe overlay (Editor.svelte).
|
||||
// First-row pushes use a block spacer before the wrapper (spacerRow === null)
|
||||
// → the whole table moves to the next page, no internal gap, so 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);
|
||||
}
|
||||
@@ -745,8 +850,6 @@ export const PageBreaks = Extension.create({
|
||||
docPos: spacerDocPos,
|
||||
height: h,
|
||||
row: spacerRow,
|
||||
incell: !!leaf.inTableCell,
|
||||
bandKey: leaf.inTableCell && bandOpenY !== null ? Math.round(bandOpenY) : null,
|
||||
});
|
||||
placementsDebug.push({
|
||||
leafIndex: i,
|
||||
@@ -810,12 +913,6 @@ export const PageBreaks = Extension.create({
|
||||
}
|
||||
const spacerEl = document.createElement('div');
|
||||
spacerEl.dataset.pageBreakSpacer = 'true';
|
||||
// Tag in-cell spacers (with their band key) so Editor.svelte can group
|
||||
// a break's per-column spacers and align the band to their real extent.
|
||||
if (p.incell) {
|
||||
spacerEl.dataset.pageBreakSpacerIncell = 'true';
|
||||
if (p.bandKey !== null) spacerEl.dataset.pageBreakBoundary = String(p.bandKey);
|
||||
}
|
||||
spacerEl.style.height = `${p.height}px`;
|
||||
spacerEl.style.pointerEvents = 'none';
|
||||
spacerEl.style.userSelect = 'none';
|
||||
@@ -872,6 +969,7 @@ export const PageBreaks = Extension.create({
|
||||
placements: placementsDebug,
|
||||
renderedSpacers: [],
|
||||
tableBreakBands,
|
||||
overlay: null, // filled in live by the debug accessor (captureOverlay)
|
||||
};
|
||||
|
||||
isUpdating = false;
|
||||
|
||||
Reference in new issue
Block a user