Render QWord print pages as separate sheets

This commit is contained in:
Курнат Андрей
2026-06-02 09:27:13 +03:00
parent c72e78ac82
commit be7a0e5c98
3 changed files with 94 additions and 34 deletions
+41
View File
@@ -60,6 +60,47 @@ 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-print-pages-test",
title: "Страницы.docx",
updatedAt: "2026-06-02T00:00:00.000Z",
html: "<p>Первая страница</p><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: 2400 });
fireEvent.scroll(stage);
await waitFor(() => expect(view.container.querySelectorAll(".page-sheet")).toHaveLength(3));
expect(frame.style.getPropertyValue("--qword-page-count")).toBe("3");
expect(frame.style.minHeight).toBe("3425px");
expect(frame.style.paddingBottom).toBe("56px");
expect(view.container.querySelectorAll(".page-break-guides span")).toHaveLength(2);
} finally {
globalThis.ResizeObserver = originalResizeObserver;
window.requestAnimationFrame = originalRequestAnimationFrame;
}
});
it("переключает вкладки ленты и показывает команды выбранной вкладки", () => {
const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame;
+14 -4
View File
@@ -180,6 +180,7 @@ function findInPage(query: string) {
}
const defaultPageViewHeight = 1180;
const defaultPageGap = 28;
function pageHeightFromStyles(element: HTMLElement) {
const pageHeight = Number.parseFloat(window.getComputedStyle(element).getPropertyValue("--qword-page-height"));
@@ -270,6 +271,12 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
const [pageView, setPageView] = useState({ count: 1, current: 1, height: defaultPageViewHeight });
const wordCount = useMemo(() => countWords(document.html), [document.html]);
const stageStyle = { "--qword-zoom": String(zoom / 100) } as CSSProperties;
const pageFrameStyle = {
"--qword-page-count": String(pageView.count),
"--qword-page-gap": `${defaultPageGap}px`,
minHeight: `${pageView.count * pageView.height + Math.max(0, pageView.count - 1) * defaultPageGap}px`,
paddingBottom: pageView.count > 1 ? `${(pageView.count - 1) * defaultPageGap}px` : undefined
} as CSSProperties;
const activeRibbonTabLabel = wordRibbonTabs.find((tab) => tab.id === activeRibbonTab)?.label ?? "Главная";
const stageClassName = ["document-stage", `qword-view-${viewMode}`].join(" ");
@@ -998,7 +1005,12 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
))}
</div>
<div className="paper-frame">
<div className="paper-frame" style={pageFrameStyle}>
<div className="page-sheets" aria-hidden="true">
{Array.from({ length: pageView.count }, (_, index) => (
<span key={index} className="page-sheet" />
))}
</div>
<article
ref={editorRef}
className="paper document-content"
@@ -1015,9 +1027,7 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
{pageView.count > 1 ? (
<div className="page-break-guides" aria-hidden="true">
{Array.from({ length: pageView.count - 1 }, (_, index) => (
<span key={index} style={{ top: `${(index + 1) * pageView.height}px` }}>
Страница {index + 2}
</span>
<span key={index} style={{ top: `${(index + 1) * pageView.height + index * defaultPageGap}px` }} />
))}
</div>
) : null}
+39 -30
View File
@@ -1053,28 +1053,54 @@ button {
}
.paper-frame {
--qword-page-gap: 28px;
--qword-page-height: 1123px;
position: relative;
width: var(--qword-page-width, 794px);
zoom: var(--qword-zoom, 1);
margin: 0 auto 38px;
}
.paper {
--qword-page-height: 1123px;
min-height: var(--qword-page-height);
.page-sheets {
position: absolute;
inset: 0 0 auto;
z-index: 0;
display: flex;
flex-direction: column;
gap: var(--qword-page-gap);
pointer-events: none;
}
.page-sheet {
display: block;
width: 100%;
margin: 0;
padding: 72px 86px;
height: var(--qword-page-height);
border: 1px solid #c7d1dd;
background: #ffffff;
box-shadow: var(--shadow);
}
.paper {
min-height: var(--qword-page-height);
position: relative;
z-index: 1;
width: 100%;
margin: 0;
padding: 72px 86px;
border: 0;
background: transparent;
box-shadow: none;
outline: 0;
}
.qword-view-read .page-ruler,
.qword-view-read .vertical-page-ruler,
.qword-view-read .page-sheets,
.qword-view-read .page-break-guides,
.qword-view-web .page-ruler,
.qword-view-web .vertical-page-ruler {
.qword-view-web .vertical-page-ruler,
.qword-view-web .page-sheets,
.qword-view-web .page-break-guides {
display: none;
}
@@ -1086,6 +1112,9 @@ button {
.qword-view-read .paper {
min-height: auto;
padding: 56px 72px;
border: 1px solid #c7d1dd;
background: #ffffff;
box-shadow: var(--shadow);
}
.qword-view-web .paper-frame {
@@ -1096,6 +1125,7 @@ button {
.qword-view-web .paper {
min-height: auto;
padding: 36px 48px;
background: #ffffff;
border-right: 0;
border-left: 0;
box-shadow: none;
@@ -1104,6 +1134,7 @@ button {
.page-break-guides {
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
}
@@ -1111,30 +1142,8 @@ button {
position: absolute;
left: 0;
right: 0;
display: grid;
place-items: center;
height: 24px;
border-top: 1px dashed #94a3b8;
color: #64748b;
font-size: 11px;
transform: translateY(-12px);
}
.page-break-guides span::before,
.page-break-guides span::after {
content: "";
position: absolute;
top: 11px;
width: calc(50% - 58px);
border-top: 1px dashed #94a3b8;
}
.page-break-guides span::before {
left: 0;
}
.page-break-guides span::after {
right: 0;
height: var(--qword-page-gap);
background: #dedede;
}
.document-content h1 {