Respect QWord page breaks in page layout

This commit is contained in:
Курнат Андрей
2026-06-04 21:24:43 +03:00
parent 500d368af4
commit 0b993b7709
2 changed files with 72 additions and 6 deletions
+56 -1
View File
@@ -3,7 +3,7 @@ import { useState } from "react";
import { afterEach, describe, expect, it } from "vitest"; import { afterEach, describe, expect, it } from "vitest";
import { vi } from "vitest"; import { vi } from "vitest";
import type { WordDocument } from "../../types"; import type { WordDocument } from "../../types";
import { QWord, qwordCurrentPageFromViewport } from "./QWord"; import { QWord, qwordCurrentPageFromViewport, qwordPageCountFromContent } from "./QWord";
afterEach(() => { afterEach(() => {
cleanup(); cleanup();
@@ -20,6 +20,21 @@ describe("qwordCurrentPageFromViewport", () => {
expect(qwordCurrentPageFromViewport({ stageTop: 34, paperTop: -1200, pageHeight: 1123, pageCount: 3, zoom: 1 })).toBe(2); expect(qwordCurrentPageFromViewport({ stageTop: 34, paperTop: -1200, pageHeight: 1123, pageCount: 3, zoom: 1 })).toBe(2);
expect(qwordCurrentPageFromViewport({ stageTop: 34, paperTop: -1200, pageHeight: 1123, pageCount: 3, zoom: 1.2 })).toBe(1); expect(qwordCurrentPageFromViewport({ stageTop: 34, paperTop: -1200, pageHeight: 1123, pageCount: 3, zoom: 1.2 })).toBe(1);
}); });
it("does not advance the current page while the viewport is inside the inter-page gap", () => {
expect(qwordCurrentPageFromViewport({ stageTop: 0, paperTop: -110, pageHeight: 100, pageGap: 20, pageCount: 3, zoom: 1 })).toBe(1);
expect(qwordCurrentPageFromViewport({ stageTop: 0, paperTop: -125, pageHeight: 100, pageGap: 20, pageCount: 3, zoom: 1 })).toBe(2);
});
});
describe("qwordPageCountFromContent", () => {
it("keeps manual page breaks as a minimum page count even when content height is short", () => {
expect(qwordPageCountFromContent({ scrollHeight: 300, pageHeight: 1123, manualPageBreakCount: 2 })).toBe(3);
});
it("keeps content height page count when it exceeds manual page breaks", () => {
expect(qwordPageCountFromContent({ scrollHeight: 2400, pageHeight: 1123, manualPageBreakCount: 1 })).toBe(3);
});
}); });
describe("QWord", () => { describe("QWord", () => {
@@ -101,6 +116,46 @@ describe("QWord", () => {
} }
}); });
it("показывает отдельный лист для ручного разрыва страницы даже при коротком содержимом", async () => {
const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame;
globalThis.ResizeObserver = class {
observe() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
callback(0);
return 1;
}) as typeof window.requestAnimationFrame;
const document: WordDocument = {
id: "word-manual-break-pages-test",
title: "Разрыв.docx",
updatedAt: "2026-06-04T00:00:00.000Z",
html: '<p>Первая страница</p><span data-qoffice-page-break="true" contenteditable="false"></span><p>Вторая страница</p>'
};
try {
const view = render(<QWord document={document} onChange={vi.fn()} />);
const stage = view.getByLabelText("Редактор QWord");
const paper = view.getByLabelText("Содержимое документа");
const frame = view.container.querySelector(".paper-frame") as HTMLElement;
paper.style.setProperty("--qword-page-height", "1123px");
Object.defineProperty(paper, "scrollHeight", { configurable: true, value: 300 });
fireEvent.scroll(stage);
await waitFor(() => expect(view.container.querySelectorAll(".page-sheet")).toHaveLength(2));
expect(frame.style.getPropertyValue("--qword-page-count")).toBe("2");
expect(frame.style.minHeight).toBe("2274px");
expect(view.container.querySelectorAll(".page-break-guides span")).toHaveLength(1);
} finally {
globalThis.ResizeObserver = originalResizeObserver;
window.requestAnimationFrame = originalRequestAnimationFrame;
}
});
it("переключает вкладки ленты и показывает команды выбранной вкладки", () => { it("переключает вкладки ленты и показывает команды выбранной вкладки", () => {
const originalResizeObserver = globalThis.ResizeObserver; const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame; const originalRequestAnimationFrame = window.requestAnimationFrame;
+16 -5
View File
@@ -250,12 +250,21 @@ function keepEditorSelection(event: MouseEvent<HTMLButtonElement>) {
event.preventDefault(); event.preventDefault();
} }
export function qwordCurrentPageFromViewport(input: { stageTop: number; paperTop: number; pageHeight: number; pageCount: number; zoom: number }) { export function qwordCurrentPageFromViewport(input: { stageTop: number; paperTop: number; pageHeight: number; pageCount: number; zoom: number; pageGap?: number }) {
const pageCount = Math.max(1, input.pageCount); const pageCount = Math.max(1, input.pageCount);
const zoom = Number.isFinite(input.zoom) && input.zoom > 0 ? input.zoom : 1; const zoom = Number.isFinite(input.zoom) && input.zoom > 0 ? input.zoom : 1;
const visualPageHeight = Math.max(1, input.pageHeight * zoom); const pageGap = Number.isFinite(input.pageGap) && input.pageGap ? Math.max(0, input.pageGap) : 0;
const visualPageStride = Math.max(1, (input.pageHeight + pageGap) * zoom);
const visiblePaperOffset = Math.max(0, input.stageTop - input.paperTop); const visiblePaperOffset = Math.max(0, input.stageTop - input.paperTop);
return Math.min(pageCount, Math.max(1, Math.floor(visiblePaperOffset / visualPageHeight) + 1)); return Math.min(pageCount, Math.max(1, Math.floor(visiblePaperOffset / visualPageStride) + 1));
}
export function qwordPageCountFromContent(input: { scrollHeight: number; pageHeight: number; manualPageBreakCount?: number }) {
const pageHeight = Number.isFinite(input.pageHeight) && input.pageHeight > 0 ? input.pageHeight : defaultPageViewHeight;
const scrollHeight = Number.isFinite(input.scrollHeight) && input.scrollHeight > 0 ? input.scrollHeight : 0;
const manualPageBreakCount =
Number.isFinite(input.manualPageBreakCount) && input.manualPageBreakCount ? Math.max(0, Math.trunc(input.manualPageBreakCount)) : 0;
return Math.max(1, Math.ceil(scrollHeight / pageHeight), manualPageBreakCount + 1);
} }
export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange, onViewModeChange }: QWordProps) { export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange, onViewModeChange }: QWordProps) {
@@ -315,7 +324,8 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange
paperTop: paperRect.top, paperTop: paperRect.top,
pageHeight, pageHeight,
pageCount, pageCount,
zoom: Number.isFinite(currentZoom) ? currentZoom : 1 zoom: Number.isFinite(currentZoom) ? currentZoom : 1,
pageGap: defaultPageGap
}); });
} }
@@ -327,7 +337,8 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange
} }
const height = pageHeightFromStyles(editor); const height = pageHeightFromStyles(editor);
const count = Math.max(1, Math.ceil(editor.scrollHeight / height)); const manualPageBreakCount = editor.querySelectorAll('[data-qoffice-page-break="true"]').length;
const count = qwordPageCountFromContent({ scrollHeight: editor.scrollHeight, pageHeight: height, manualPageBreakCount });
const currentPage = currentPageFromStage(count, height); const currentPage = currentPageFromStage(count, height);
setPageView((current) => setPageView((current) =>
current.count === count && current.height === height && current.current === currentPage ? current : { count, current: currentPage, height } current.count === count && current.height === height && current.current === currentPage ? current : { count, current: currentPage, height }