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 }