diff --git a/src/features/qword/QWord.test.tsx b/src/features/qword/QWord.test.tsx index 21390da..fd4b535 100644 --- a/src/features/qword/QWord.test.tsx +++ b/src/features/qword/QWord.test.tsx @@ -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: "

Первая страница

Вторая страница

" + }; + + try { + const view = render(); + 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; diff --git a/src/features/qword/QWord.tsx b/src/features/qword/QWord.tsx index 016aa06..f66956a 100644 --- a/src/features/qword/QWord.tsx +++ b/src/features/qword/QWord.tsx @@ -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 ))} -
+
+
1 ? ( ) : null} diff --git a/src/styles.css b/src/styles.css index 522ea83..c80fa1f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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 {