diff --git a/src/features/qword/QWord.test.tsx b/src/features/qword/QWord.test.tsx index 86fa12d..c6426bb 100644 --- a/src/features/qword/QWord.test.tsx +++ b/src/features/qword/QWord.test.tsx @@ -156,6 +156,50 @@ 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-page-setup-test", + title: "Макет.docx", + updatedAt: "2026-06-04T00:00:00.000Z", + html: "

Текст страницы

" + }; + + try { + const view = render(); + const frame = view.container.querySelector(".paper-frame") as HTMLElement; + + expect(frame.style.getPropertyValue("--qword-page-width")).toBe("794px"); + expect(frame.style.getPropertyValue("--qword-page-height")).toBe("1123px"); + expect(frame.style.getPropertyValue("--qword-page-margin-left")).toBe("86px"); + + fireEvent.click(screen.getByRole("tab", { name: "Макет" })); + fireEvent.change(screen.getByLabelText("Поля страницы"), { target: { value: "narrow" } }); + fireEvent.change(screen.getByLabelText("Размер страницы"), { target: { value: "letter" } }); + fireEvent.change(screen.getByLabelText("Ориентация страницы"), { target: { value: "landscape" } }); + + await waitFor(() => expect(frame.style.getPropertyValue("--qword-page-height")).toBe("816px")); + expect(frame.style.getPropertyValue("--qword-page-width")).toBe("1056px"); + 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"); + } 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 c8a62fe..2d2d923 100644 --- a/src/features/qword/QWord.tsx +++ b/src/features/qword/QWord.tsx @@ -156,6 +156,14 @@ 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: "Файл" }, @@ -175,6 +183,20 @@ const wordViewModes: Array<{ id: WordViewMode; label: string }> = [ { id: "read", label: "Чтение" }, { id: "web", label: "Веб-документ" } ]; +const wordPageSizeOptions: Array<{ id: WordPageSizeId; label: string; width: number; height: number }> = [ + { id: "a4", label: "A4", width: 794, height: 1123 }, + { id: "letter", label: "Letter", width: 816, height: 1056 } +]; +const wordPageOrientationOptions: Array<{ id: WordPageOrientation; label: string }> = [ + { id: "portrait", label: "Книжная" }, + { id: "landscape", label: "Альбомная" } +]; +const wordPageMarginOptions: Array<{ id: WordPageMarginPreset; label: string; margins: { top: number; right: number; bottom: number; left: number } }> = [ + { id: "normal", label: "Обычные", margins: { top: 72, right: 86, bottom: 72, left: 86 } }, + { id: "narrow", label: "Узкие", margins: { top: 48, right: 48, bottom: 48, left: 48 } }, + { id: "wide", label: "Широкие", margins: { top: 92, right: 116, bottom: 92, left: 116 } } +]; +const defaultWordPageSetup: WordPageSetup = { size: "a4", orientation: "portrait", marginPreset: "normal" }; const tableSizeOptions = [1, 2, 3, 4, 5, 6, 7, 8]; function findInPage(query: string) { @@ -189,6 +211,15 @@ function pageHeightFromStyles(element: HTMLElement) { return Number.isFinite(pageHeight) && pageHeight > 0 ? pageHeight : defaultPageViewHeight; } +function qwordPageDimensions(setup: WordPageSetup) { + const size = wordPageSizeOptions.find((option) => option.id === setup.size) ?? wordPageSizeOptions[0]; + return setup.orientation === "landscape" ? { width: size.height, height: size.width } : { width: size.width, height: size.height }; +} + +function qwordPageMargins(setup: WordPageSetup) { + return (wordPageMarginOptions.find((option) => option.id === setup.marginPreset) ?? wordPageMarginOptions[0]).margins; +} + function clampedTableSize(value: number) { return Number.isFinite(value) ? Math.min(8, Math.max(1, Math.trunc(value))) : 3; } @@ -277,14 +308,23 @@ 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 stageStyle = { "--qword-zoom": String(zoom / 100) } as CSSProperties; + const pageDimensions = qwordPageDimensions(pageSetup); + const pageMargins = qwordPageMargins(pageSetup); const pageFrameStyle = { "--qword-page-count": String(pageView.count), "--qword-page-gap": `${defaultPageGap}px`, + "--qword-page-width": `${pageDimensions.width}px`, + "--qword-page-height": `${pageDimensions.height}px`, + "--qword-page-margin-top": `${pageMargins.top}px`, + "--qword-page-margin-right": `${pageMargins.right}px`, + "--qword-page-margin-bottom": `${pageMargins.bottom}px`, + "--qword-page-margin-left": `${pageMargins.left}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; @@ -356,7 +396,7 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange const observer = new ResizeObserver(refreshPageView); observer.observe(editor); return () => observer.disconnect(); - }, [document.html, zoom]); + }, [document.html, pageDimensions.height, pageDimensions.width, pageMargins.bottom, pageMargins.left, pageMargins.right, pageMargins.top, zoom]); useLayoutEffect(() => { if (!restoreActiveTableCellAfterRenderRef.current) { @@ -886,6 +926,56 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange ) : null} + {activeRibbonTab === "layout" ? ( +
+
+ + + +
+ Параметры страницы +
+ ) : null} + {activeRibbonTab === "home" || activeRibbonTab === "layout" ? (
diff --git a/src/styles.css b/src/styles.css index c80fa1f..c8396b2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -516,6 +516,7 @@ button { .word-clipboard-group .word-ribbon-title, .word-font-group .word-ribbon-title, +.word-page-setup-group .word-ribbon-title, .word-paragraph-group .word-ribbon-title, .word-styles-group .word-ribbon-title, .word-insert-group .word-ribbon-title, @@ -621,6 +622,19 @@ button { grid-template-columns: minmax(0, 1fr); } +.word-page-setup-group { + flex: 0 0 272px; + grid-template-columns: minmax(0, 1fr); +} + +.word-page-setup-controls { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + align-content: start; + gap: 7px; + min-width: 0; +} + .word-paragraph-group > .segmented-tools, .word-paragraph-group > .compact-command { align-self: start; @@ -1086,7 +1100,7 @@ button { z-index: 1; width: 100%; margin: 0; - padding: 72px 86px; + padding: var(--qword-page-margin-top, 72px) var(--qword-page-margin-right, 86px) var(--qword-page-margin-bottom, 72px) var(--qword-page-margin-left, 86px); border: 0; background: transparent; box-shadow: none;