mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 03:59:03 +00:00
fix(grid): prevent clipping last row in grid snapshot export
This commit is contained in:
@@ -329,4 +329,46 @@ describe("renderCodeSnapshotHtml", () => {
|
||||
await expect(savePngDataUrlToFile("data:image/png;base64,AA==", "snapshot.png")).rejects.toThrow("disk full");
|
||||
expect(writeFile).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("encompasses the last row bottom edge and relaxes clone height for grid snapshots", async () => {
|
||||
toPng.mockResolvedValue("data:image/png;base64,test");
|
||||
const container = document.createElement("div");
|
||||
container.className = "dbx-grid-snapshot";
|
||||
container.innerHTML = "<table><tbody><tr><td>row1</td></tr><tr><td>row2</td></tr></tbody></table>";
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Mock layout measurements
|
||||
Object.defineProperty(container, "offsetWidth", { value: 400 });
|
||||
Object.defineProperty(container, "offsetHeight", { value: 300 });
|
||||
Object.defineProperty(container, "scrollWidth", { value: 400 });
|
||||
Object.defineProperty(container, "scrollHeight", { value: 298 });
|
||||
container.getBoundingClientRect = () => ({ top: 10, bottom: 310, left: 0, right: 400, width: 400, height: 300 }) as DOMRect;
|
||||
|
||||
const lastRow = container.querySelector("tr:last-child") as HTMLTableRowElement;
|
||||
lastRow.getBoundingClientRect = () => ({ top: 280, bottom: 318, left: 0, right: 400, width: 400, height: 38 }) as DOMRect;
|
||||
|
||||
try {
|
||||
await snapshotElementToPng(container);
|
||||
|
||||
// Height should encompass lastRow.bottom (318) - container.top (10) = 308
|
||||
expect(toPng).toHaveBeenLastCalledWith(
|
||||
container,
|
||||
expect.objectContaining({
|
||||
width: 400,
|
||||
height: 308,
|
||||
}),
|
||||
);
|
||||
|
||||
const options = toPng.mock.calls.at(-1)?.[1];
|
||||
expect(typeof options?.onclone).toBe("function");
|
||||
|
||||
const clone = container.cloneNode(true) as HTMLElement;
|
||||
clone.style.height = "308px";
|
||||
options.onclone(clone);
|
||||
expect(clone.style.height).toBe("auto");
|
||||
expect(clone.style.minHeight).toBe("308px");
|
||||
} finally {
|
||||
container.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -343,8 +343,34 @@ export async function renderCodeSnapshotHtml(source: CodeSnapshotSource, options
|
||||
export async function snapshotElementToPng(element: HTMLElement): Promise<string> {
|
||||
const domtoimage = (await import("dom-to-image-more")).default;
|
||||
// 预览区允许滚动,因此导出尺寸必须覆盖完整内容,而不是只取当前可见宽度。
|
||||
const width = Math.max(element.offsetWidth, element.scrollWidth);
|
||||
const height = Math.max(element.offsetHeight, element.scrollHeight);
|
||||
const rect = typeof element.getBoundingClientRect === "function" ? element.getBoundingClientRect() : null;
|
||||
let borderTop = 0;
|
||||
let borderBottom = 0;
|
||||
if (typeof window !== "undefined" && typeof window.getComputedStyle === "function" && element instanceof (window.Element || Object)) {
|
||||
try {
|
||||
const computedStyle = window.getComputedStyle(element);
|
||||
borderTop = parseFloat(computedStyle.borderTopWidth) || 0;
|
||||
borderBottom = parseFloat(computedStyle.borderBottomWidth) || 0;
|
||||
} catch {
|
||||
// Ignored for non-DOM elements in test runners
|
||||
}
|
||||
}
|
||||
|
||||
let width = Math.max(element.offsetWidth || 0, element.scrollWidth || 0, rect ? Math.ceil(rect.width) : 0);
|
||||
let height = Math.max(element.offsetHeight || 0, element.scrollHeight || 0, (element.scrollHeight || 0) + Math.ceil(borderTop + borderBottom), rect ? Math.ceil(rect.height) : 0);
|
||||
|
||||
if (rect && typeof element.querySelector === "function") {
|
||||
// If element contains table rows, ensure height fully encloses the bottom edge of the last row
|
||||
const lastRow = element.querySelector("tr:last-child");
|
||||
if (lastRow && typeof lastRow.getBoundingClientRect === "function") {
|
||||
const rowRect = lastRow.getBoundingClientRect();
|
||||
const requiredHeight = Math.ceil(rowRect.bottom - rect.top) + Math.ceil(borderBottom);
|
||||
if (requiredHeight > height) {
|
||||
height = requiredHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const basePixels = width * height;
|
||||
const targetScale = Math.min(MAX_EXPORT_PIXEL_RATIO, Math.max(1, typeof window === "undefined" ? 1 : window.devicePixelRatio || 1));
|
||||
// Keep the output canvas within browser limits without rejecting a larger
|
||||
@@ -352,6 +378,8 @@ export async function snapshotElementToPng(element: HTMLElement): Promise<string
|
||||
// full DOM at a reduced scale.
|
||||
const scale = Math.min(targetScale, MAX_EXPORT_CANVAS_EDGE / width, MAX_EXPORT_CANVAS_EDGE / height, Math.sqrt(MAX_EXPORT_CANVAS_PIXELS / basePixels));
|
||||
const useLegacyLineNumberFallback = legacyWebViewExportFallbackEnabled();
|
||||
const isGridSnapshot = typeof element.classList?.contains === "function" && element.classList.contains("dbx-grid-snapshot");
|
||||
const hasOnclone = useLegacyLineNumberFallback || isGridSnapshot;
|
||||
return domtoimage.toPng(element, {
|
||||
quality: 1,
|
||||
width,
|
||||
@@ -366,11 +394,16 @@ export async function snapshotElementToPng(element: HTMLElement): Promise<string
|
||||
return undefined;
|
||||
}
|
||||
: undefined,
|
||||
onclone: useLegacyLineNumberFallback
|
||||
onclone: hasOnclone
|
||||
? (clone: Node) => {
|
||||
if (typeof (clone as ParentNode).querySelectorAll === "function") {
|
||||
if (useLegacyLineNumberFallback && typeof (clone as ParentNode).querySelectorAll === "function") {
|
||||
materializeSnapshotCloneLineNumbers(clone as ParentNode);
|
||||
}
|
||||
if (isGridSnapshot && clone && (clone as HTMLElement).style) {
|
||||
// Prevent rigid fixed height from clipping table rows inside overflow:hidden
|
||||
(clone as HTMLElement).style.height = "auto";
|
||||
(clone as HTMLElement).style.minHeight = `${height}px`;
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
style: {
|
||||
|
||||
@@ -126,4 +126,20 @@ describe("grid snapshot rendering", () => {
|
||||
const unstripedHtml = renderGridSnapshotHtml(source, { appearance: "light", stripedRows: false });
|
||||
expect(unstripedHtml).not.toContain("background:rgb(240, 240, 240)");
|
||||
});
|
||||
|
||||
it("defines explicit line-height and safe bottom padding on the last row to prevent clipping", () => {
|
||||
const source = {
|
||||
columns: ["id", "val"],
|
||||
rows: [
|
||||
[1, "first"],
|
||||
[2, "second"],
|
||||
],
|
||||
};
|
||||
const standardHtml = renderGridSnapshotHtml(source, { appearance: "light" });
|
||||
expect(standardHtml).toContain("line-height: 1.4");
|
||||
expect(standardHtml).toContain("padding:4px 12px 6px");
|
||||
|
||||
const compactHtml = renderGridSnapshotHtml(source, { appearance: "light", compact: true });
|
||||
expect(compactHtml).toContain("padding:3px 8px 5px");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,14 +90,15 @@ export const GRID_SNAPSHOT_CSS = `
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
|
||||
}
|
||||
.dbx-grid-snapshot__bar { display: flex; width: 100%; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--dbx-grid-snapshot-border); }
|
||||
.dbx-grid-snapshot__dot { width: 12px; height: 12px; border-radius: 50%; flex: none; }
|
||||
.dbx-grid-snapshot__title { margin-left: 4px; font-size: 12px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dbx-grid-snapshot__table { border-collapse: separate; border-spacing: 0; table-layout: auto; width: 100%; }
|
||||
.dbx-grid-snapshot__cell { border-right: 1px solid var(--dbx-grid-snapshot-border); border-bottom: 1px solid var(--dbx-grid-snapshot-border); padding: 7px 10px; white-space: nowrap; vertical-align: top; }
|
||||
.dbx-grid-snapshot__table { border-collapse: separate; border-spacing: 0; table-layout: auto; width: 100%; line-height: 1.4; }
|
||||
.dbx-grid-snapshot__cell { border-right: 1px solid var(--dbx-grid-snapshot-border); border-bottom: 1px solid var(--dbx-grid-snapshot-border); padding: 7px 10px; white-space: nowrap; vertical-align: top; line-height: 1.4; }
|
||||
.dbx-grid-snapshot__cell--wrapped { max-width: 420px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.dbx-grid-snapshot__cell:last-child { border-right: 0; }
|
||||
.dbx-grid-snapshot__table tbody tr:last-child .dbx-grid-snapshot__cell { border-bottom: 0; }
|
||||
@@ -136,6 +137,7 @@ export function renderGridSnapshotHtml(source: GridSnapshotSource, options: Grid
|
||||
const wrapCells = options.wrapCells === true;
|
||||
const compact = options.compact === true;
|
||||
const cellPadding = compact ? "3px 8px" : "4px 12px";
|
||||
const lastRowCellPadding = compact ? "3px 8px 5px" : "4px 12px 6px";
|
||||
const headerPadding = compact ? "4px 8px" : "6px 8px";
|
||||
const fontSize = compact ? "12px" : "13px";
|
||||
const rowNumberHeader = showRowNumbers ? `<th class="dbx-grid-snapshot__cell dbx-grid-snapshot__cell--header dbx-grid-snapshot__cell--row-number" style="background:${GRID_SNAPSHOT_HEADER[appearance]};color:${GRID_SNAPSHOT_MUTED[appearance]};padding:${headerPadding}">#</th>` : "";
|
||||
@@ -151,15 +153,16 @@ export function renderGridSnapshotHtml(source: GridSnapshotSource, options: Grid
|
||||
: "";
|
||||
const displayRows: GridSnapshotCellValue[][] = transpose ? source.columns.map((_, columnIndex) => source.rows.map((row) => row[columnIndex] ?? null)) : source.rows;
|
||||
const body = displayRows
|
||||
.map(
|
||||
(row, rowIndex) =>
|
||||
`<tr style="background:${options.stripedRows !== false && rowIndex % 2 === 1 ? GRID_SNAPSHOT_STRIPED_ROW[appearance] : GRID_SNAPSHOT_BACKGROUND[appearance]}">${showRowNumbers ? `<td class="dbx-grid-snapshot__cell dbx-grid-snapshot__cell--row-number" style="background:${GRID_SNAPSHOT_ROW_NUMBER[appearance]};color:${GRID_SNAPSHOT_MUTED[appearance]};padding:${cellPadding}">${rowIndex + 1}</td>` : ""}${transpose && showFieldNames ? renderFieldCell(source.columns[rowIndex] ?? "", rowIndex, "td") : ""}${row
|
||||
.map((value) => {
|
||||
const nullClass = value === null ? " dbx-grid-snapshot__cell--null" : "";
|
||||
return `<td class="dbx-grid-snapshot__cell${wrapCells ? " dbx-grid-snapshot__cell--wrapped" : ""}${nullClass}" style="color:${value === null ? GRID_SNAPSHOT_MUTED[appearance] : GRID_SNAPSHOT_TEXT[appearance]};padding:${cellPadding}">${escapeHtml(formatGridSnapshotCell(value))}</td>`;
|
||||
})
|
||||
.join("")}</tr>`,
|
||||
)
|
||||
.map((row, rowIndex) => {
|
||||
const isLastRow = rowIndex === displayRows.length - 1;
|
||||
const rowPadding = isLastRow ? lastRowCellPadding : cellPadding;
|
||||
return `<tr style="background:${options.stripedRows !== false && rowIndex % 2 === 1 ? GRID_SNAPSHOT_STRIPED_ROW[appearance] : GRID_SNAPSHOT_BACKGROUND[appearance]}">${showRowNumbers ? `<td class="dbx-grid-snapshot__cell dbx-grid-snapshot__cell--row-number" style="background:${GRID_SNAPSHOT_ROW_NUMBER[appearance]};color:${GRID_SNAPSHOT_MUTED[appearance]};padding:${rowPadding}">${rowIndex + 1}</td>` : ""}${transpose && showFieldNames ? renderFieldCell(source.columns[rowIndex] ?? "", rowIndex, "td") : ""}${row
|
||||
.map((value) => {
|
||||
const nullClass = value === null ? " dbx-grid-snapshot__cell--null" : "";
|
||||
return `<td class="dbx-grid-snapshot__cell${wrapCells ? " dbx-grid-snapshot__cell--wrapped" : ""}${nullClass}" style="color:${value === null ? GRID_SNAPSHOT_MUTED[appearance] : GRID_SNAPSHOT_TEXT[appearance]};padding:${rowPadding}">${escapeHtml(formatGridSnapshotCell(value))}</td>`;
|
||||
})
|
||||
.join("")}</tr>`;
|
||||
})
|
||||
.join("");
|
||||
const bar = options.showTrafficLights !== false || source.title ? renderSnapshotBar(source.title, appearance, options.showTrafficLights !== false) : "";
|
||||
return `<style>${GRID_SNAPSHOT_CSS}</style><div class="dbx-grid-snapshot" data-snapshot-appearance="${appearance}" style="background:${GRID_SNAPSHOT_BACKGROUND[appearance]};font-size:${fontSize};color:${GRID_SNAPSHOT_TEXT[appearance]};--dbx-grid-snapshot-border:${GRID_SNAPSHOT_BORDER[appearance]}">${bar}<table class="dbx-grid-snapshot__table">${header}<tbody>${body}</tbody></table></div>`;
|
||||
|
||||
Reference in New Issue
Block a user