diff --git a/src/features/qword/QWord.test.tsx b/src/features/qword/QWord.test.tsx index 0ab4c29..86fa12d 100644 --- a/src/features/qword/QWord.test.tsx +++ b/src/features/qword/QWord.test.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { afterEach, describe, expect, it } from "vitest"; import { vi } from "vitest"; import type { WordDocument } from "../../types"; -import { QWord, qwordCurrentPageFromViewport } from "./QWord"; +import { QWord, qwordCurrentPageFromViewport, qwordPageCountFromContent } from "./QWord"; afterEach(() => { 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.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", () => { @@ -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: '

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

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

' + }; + + 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: 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("переключает вкладки ленты и показывает команды выбранной вкладки", () => { const originalResizeObserver = globalThis.ResizeObserver; const originalRequestAnimationFrame = window.requestAnimationFrame; diff --git a/src/features/qword/QWord.tsx b/src/features/qword/QWord.tsx index 47a3310..c8a62fe 100644 --- a/src/features/qword/QWord.tsx +++ b/src/features/qword/QWord.tsx @@ -250,12 +250,21 @@ function keepEditorSelection(event: MouseEvent) { 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 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); - 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) { @@ -315,7 +324,8 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange paperTop: paperRect.top, pageHeight, 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 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); setPageView((current) => current.count === count && current.height === height && current.current === currentPage ? current : { count, current: currentPage, height }