Persist QWord page setup in documents
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user