diff --git a/src/data/sampleData.ts b/src/data/sampleData.ts
index 955877f..59481d7 100644
--- a/src/data/sampleData.ts
+++ b/src/data/sampleData.ts
@@ -31,7 +31,8 @@ export const initialState: QOfficeState = {
| Поставка | Разработка и контроль качества | Карина М. |
- `
+ `,
+ pageSetup: { size: "a4", orientation: "portrait", marginPreset: "normal" }
},
workbook: {
id: "qexcell-quarter-plan",
diff --git a/src/features/qword/QWord.test.tsx b/src/features/qword/QWord.test.tsx
index c6426bb..4daa929 100644
--- a/src/features/qword/QWord.test.tsx
+++ b/src/features/qword/QWord.test.tsx
@@ -159,6 +159,7 @@ describe("QWord", () => {
it("меняет поля, размер и ориентацию страницы на вкладке Макет", async () => {
const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame;
+ const onChange = vi.fn();
globalThis.ResizeObserver = class {
observe() {}
@@ -176,8 +177,21 @@ describe("QWord", () => {
html: "Текст страницы
"
};
+ function StatefulQWord() {
+ const [currentDocument, setCurrentDocument] = useState(document);
+ return (
+ {
+ onChange(nextDocument);
+ setCurrentDocument(nextDocument);
+ }}
+ />
+ );
+ }
+
try {
- const view = render();
+ const view = render();
const frame = view.container.querySelector(".paper-frame") as HTMLElement;
expect(frame.style.getPropertyValue("--qword-page-width")).toBe("794px");
@@ -194,6 +208,42 @@ describe("QWord", () => {
expect(frame.style.getPropertyValue("--qword-page-margin-top")).toBe("48px");
expect(frame.style.getPropertyValue("--qword-page-margin-right")).toBe("48px");
expect(frame.style.minHeight).toBe("816px");
+ expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ pageSetup: { size: "letter", orientation: "landscape", marginPreset: "narrow" } }));
+ } finally {
+ globalThis.ResizeObserver = originalResizeObserver;
+ window.requestAnimationFrame = originalRequestAnimationFrame;
+ }
+ });
+
+ it("применяет сохраненные в документе параметры страницы при открытии QWord", () => {
+ 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-saved-page-setup-test",
+ title: "Сохраненный макет.docx",
+ updatedAt: "2026-06-04T00:00:00.000Z",
+ html: "Текст страницы
",
+ pageSetup: { size: "letter", orientation: "landscape", marginPreset: "wide" }
+ };
+
+ try {
+ const view = render();
+ const frame = view.container.querySelector(".paper-frame") as HTMLElement;
+
+ expect(frame.style.getPropertyValue("--qword-page-width")).toBe("1056px");
+ expect(frame.style.getPropertyValue("--qword-page-height")).toBe("816px");
+ expect(frame.style.getPropertyValue("--qword-page-margin-left")).toBe("116px");
+ expect(frame.style.getPropertyValue("--qword-page-margin-top")).toBe("92px");
} finally {
globalThis.ResizeObserver = originalResizeObserver;
window.requestAnimationFrame = originalRequestAnimationFrame;
diff --git a/src/features/qword/QWord.tsx b/src/features/qword/QWord.tsx
index 2d2d923..ee7f3b5 100644
--- a/src/features/qword/QWord.tsx
+++ b/src/features/qword/QWord.tsx
@@ -32,7 +32,7 @@ import {
Rows3,
Underline
} from "lucide-react";
-import type { WordDocument } from "../../types";
+import type { WordDocument, WordPageMarginPreset, WordPageOrientation, WordPageSetup, WordPageSizeId } from "../../types";
import { downloadBlobFile, downloadTextFile } from "../../utils/download";
import { exportDocxBlob, importDocxFile } from "../../io/wordOffice";
import { replaceFileExtension } from "../../io/fileHelpers";
@@ -156,15 +156,6 @@ const styleGalleryOptions = [
];
type WordRibbonTabId = "file" | "home" | "insert" | "design" | "layout" | "references" | "mailings" | "review" | "view" | "help";
export type WordViewMode = "print" | "read" | "web";
-type WordPageSizeId = "a4" | "letter";
-type WordPageOrientation = "portrait" | "landscape";
-type WordPageMarginPreset = "normal" | "narrow" | "wide";
-type WordPageSetup = {
- size: WordPageSizeId;
- orientation: WordPageOrientation;
- marginPreset: WordPageMarginPreset;
-};
-
const wordRibbonTabs: Array<{ id: WordRibbonTabId; label: string }> = [
{ id: "file", label: "Файл" },
{ id: "home", label: "Главная" },
@@ -220,6 +211,18 @@ function qwordPageMargins(setup: WordPageSetup) {
return (wordPageMarginOptions.find((option) => option.id === setup.marginPreset) ?? wordPageMarginOptions[0]).margins;
}
+function normalizedWordPageSetup(setup?: WordDocument["pageSetup"]): WordPageSetup {
+ const candidateSize = setup?.size;
+ const candidateOrientation = setup?.orientation;
+ const candidateMarginPreset = setup?.marginPreset;
+ const size = candidateSize && wordPageSizeOptions.some((option) => option.id === candidateSize) ? candidateSize : defaultWordPageSetup.size;
+ const orientation =
+ candidateOrientation && wordPageOrientationOptions.some((option) => option.id === candidateOrientation) ? candidateOrientation : defaultWordPageSetup.orientation;
+ const marginPreset =
+ candidateMarginPreset && wordPageMarginOptions.some((option) => option.id === candidateMarginPreset) ? candidateMarginPreset : defaultWordPageSetup.marginPreset;
+ return { size, orientation, marginPreset };
+}
+
function clampedTableSize(value: number) {
return Number.isFinite(value) ? Math.min(8, Math.max(1, Math.trunc(value))) : 3;
}
@@ -308,11 +311,11 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange
const imageInputRef = useRef(null);
const [activeRibbonTab, setActiveRibbonTab] = useState("home");
const [internalViewMode, setInternalViewMode] = useState("print");
- const [pageSetup, setPageSetup] = useState(defaultWordPageSetup);
const [tableRowCount, setTableRowCount] = useState(3);
const [tableColumnCount, setTableColumnCount] = useState(3);
const [pageView, setPageView] = useState({ count: 1, current: 1, height: defaultPageViewHeight });
const wordCount = useMemo(() => countWords(document.html), [document.html]);
+ const pageSetup = useMemo(() => normalizedWordPageSetup(document.pageSetup), [document.pageSetup]);
const stageStyle = { "--qword-zoom": String(zoom / 100) } as CSSProperties;
const pageDimensions = qwordPageDimensions(pageSetup);
const pageMargins = qwordPageMargins(pageSetup);
@@ -340,6 +343,14 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange
[onViewModeChange]
);
+ const changePageSetup = useCallback(
+ (nextPageSetup: Partial) => {
+ onChange({ ...document, pageSetup: normalizedWordPageSetup({ ...pageSetup, ...nextPageSetup }) });
+ refreshPageView();
+ },
+ [document, onChange, pageSetup]
+ );
+
useLayoutEffect(() => {
const editor = editorRef.current;
if (!editor || editor.innerHTML === document.html) {
@@ -934,7 +945,7 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange