Persist QWord page setup in documents

This commit is contained in:
Курнат Андрей
2026-06-04 21:35:37 +03:00
parent c1f8162afd
commit e5cd922f7d
4 changed files with 89 additions and 16 deletions
+2 -1
View File
@@ -31,7 +31,8 @@ export const initialState: QOfficeState = {
<tr><td>Поставка</td><td>Разработка и контроль качества</td><td>Карина М.</td></tr>
</tbody>
</table>
`
`,
pageSetup: { size: "a4", orientation: "portrait", marginPreset: "normal" }
},
workbook: {
id: "qexcell-quarter-plan",
+51 -1
View File
@@ -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: "<p>Текст страницы</p>"
};
function StatefulQWord() {
const [currentDocument, setCurrentDocument] = useState(document);
return (
<QWord
document={currentDocument}
onChange={(nextDocument) => {
onChange(nextDocument);
setCurrentDocument(nextDocument);
}}
/>
);
}
try {
const view = render(<QWord document={document} onChange={vi.fn()} />);
const view = render(<StatefulQWord />);
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: "<p>Текст страницы</p>",
pageSetup: { size: "letter", orientation: "landscape", marginPreset: "wide" }
};
try {
const view = render(<QWord document={document} onChange={vi.fn()} />);
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;
+25 -14
View File
@@ -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<HTMLInputElement>(null);
const [activeRibbonTab, setActiveRibbonTab] = useState<WordRibbonTabId>("home");
const [internalViewMode, setInternalViewMode] = useState<WordViewMode>("print");
const [pageSetup, setPageSetup] = useState<WordPageSetup>(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<WordPageSetup>) => {
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
<select
aria-label="Поля страницы"
value={pageSetup.marginPreset}
onChange={(event) => setPageSetup((current) => ({ ...current, marginPreset: event.target.value as WordPageMarginPreset }))}
onChange={(event) => changePageSetup({ marginPreset: event.target.value as WordPageMarginPreset })}
>
{wordPageMarginOptions.map((option) => (
<option key={option.id} value={option.id}>
@@ -948,7 +959,7 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange
<select
aria-label="Размер страницы"
value={pageSetup.size}
onChange={(event) => setPageSetup((current) => ({ ...current, size: event.target.value as WordPageSizeId }))}
onChange={(event) => changePageSetup({ size: event.target.value as WordPageSizeId })}
>
{wordPageSizeOptions.map((option) => (
<option key={option.id} value={option.id}>
@@ -962,7 +973,7 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange
<select
aria-label="Ориентация страницы"
value={pageSetup.orientation}
onChange={(event) => setPageSetup((current) => ({ ...current, orientation: event.target.value as WordPageOrientation }))}
onChange={(event) => changePageSetup({ orientation: event.target.value as WordPageOrientation })}
>
{wordPageOrientationOptions.map((option) => (
<option key={option.id} value={option.id}>
+11
View File
@@ -1,10 +1,21 @@
export type AppId = "word" | "sheets" | "slides" | "mail";
export type WordPageSizeId = "a4" | "letter";
export type WordPageOrientation = "portrait" | "landscape";
export type WordPageMarginPreset = "normal" | "narrow" | "wide";
export interface WordPageSetup {
size: WordPageSizeId;
orientation: WordPageOrientation;
marginPreset: WordPageMarginPreset;
}
export interface WordDocument {
id: string;
title: string;
updatedAt: string;
html: string;
pageSetup?: WordPageSetup;
}
export interface Workbook {