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 setPageSetup((current) => ({ ...current, size: event.target.value as WordPageSizeId }))} + onChange={(event) => changePageSetup({ size: event.target.value as WordPageSizeId })} > {wordPageSizeOptions.map((option) => (