Render QWord print pages as separate sheets
This commit is contained in:
@@ -60,6 +60,47 @@ 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-print-pages-test",
|
||||||
|
title: "Страницы.docx",
|
||||||
|
updatedAt: "2026-06-02T00:00:00.000Z",
|
||||||
|
html: "<p>Первая страница</p><p>Вторая страница</p>"
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const view = render(<QWord document={document} onChange={vi.fn()} />);
|
||||||
|
const stage = view.getByLabelText("Редактор QWord");
|
||||||
|
const paper = view.getByLabelText("Содержимое документа");
|
||||||
|
const frame = view.container.querySelector(".paper-frame") as HTMLElement;
|
||||||
|
|
||||||
|
paper.style.setProperty("--qword-page-height", "1123px");
|
||||||
|
Object.defineProperty(paper, "scrollHeight", { configurable: true, value: 2400 });
|
||||||
|
fireEvent.scroll(stage);
|
||||||
|
|
||||||
|
await waitFor(() => expect(view.container.querySelectorAll(".page-sheet")).toHaveLength(3));
|
||||||
|
expect(frame.style.getPropertyValue("--qword-page-count")).toBe("3");
|
||||||
|
expect(frame.style.minHeight).toBe("3425px");
|
||||||
|
expect(frame.style.paddingBottom).toBe("56px");
|
||||||
|
expect(view.container.querySelectorAll(".page-break-guides span")).toHaveLength(2);
|
||||||
|
} finally {
|
||||||
|
globalThis.ResizeObserver = originalResizeObserver;
|
||||||
|
window.requestAnimationFrame = originalRequestAnimationFrame;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("переключает вкладки ленты и показывает команды выбранной вкладки", () => {
|
it("переключает вкладки ленты и показывает команды выбранной вкладки", () => {
|
||||||
const originalResizeObserver = globalThis.ResizeObserver;
|
const originalResizeObserver = globalThis.ResizeObserver;
|
||||||
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ function findInPage(query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultPageViewHeight = 1180;
|
const defaultPageViewHeight = 1180;
|
||||||
|
const defaultPageGap = 28;
|
||||||
|
|
||||||
function pageHeightFromStyles(element: HTMLElement) {
|
function pageHeightFromStyles(element: HTMLElement) {
|
||||||
const pageHeight = Number.parseFloat(window.getComputedStyle(element).getPropertyValue("--qword-page-height"));
|
const pageHeight = Number.parseFloat(window.getComputedStyle(element).getPropertyValue("--qword-page-height"));
|
||||||
@@ -270,6 +271,12 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
|||||||
const [pageView, setPageView] = useState({ count: 1, current: 1, height: defaultPageViewHeight });
|
const [pageView, setPageView] = useState({ count: 1, current: 1, height: defaultPageViewHeight });
|
||||||
const wordCount = useMemo(() => countWords(document.html), [document.html]);
|
const wordCount = useMemo(() => countWords(document.html), [document.html]);
|
||||||
const stageStyle = { "--qword-zoom": String(zoom / 100) } as CSSProperties;
|
const stageStyle = { "--qword-zoom": String(zoom / 100) } as CSSProperties;
|
||||||
|
const pageFrameStyle = {
|
||||||
|
"--qword-page-count": String(pageView.count),
|
||||||
|
"--qword-page-gap": `${defaultPageGap}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;
|
||||||
const activeRibbonTabLabel = wordRibbonTabs.find((tab) => tab.id === activeRibbonTab)?.label ?? "Главная";
|
const activeRibbonTabLabel = wordRibbonTabs.find((tab) => tab.id === activeRibbonTab)?.label ?? "Главная";
|
||||||
const stageClassName = ["document-stage", `qword-view-${viewMode}`].join(" ");
|
const stageClassName = ["document-stage", `qword-view-${viewMode}`].join(" ");
|
||||||
|
|
||||||
@@ -998,7 +1005,12 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="paper-frame">
|
<div className="paper-frame" style={pageFrameStyle}>
|
||||||
|
<div className="page-sheets" aria-hidden="true">
|
||||||
|
{Array.from({ length: pageView.count }, (_, index) => (
|
||||||
|
<span key={index} className="page-sheet" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<article
|
<article
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
className="paper document-content"
|
className="paper document-content"
|
||||||
@@ -1015,9 +1027,7 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP
|
|||||||
{pageView.count > 1 ? (
|
{pageView.count > 1 ? (
|
||||||
<div className="page-break-guides" aria-hidden="true">
|
<div className="page-break-guides" aria-hidden="true">
|
||||||
{Array.from({ length: pageView.count - 1 }, (_, index) => (
|
{Array.from({ length: pageView.count - 1 }, (_, index) => (
|
||||||
<span key={index} style={{ top: `${(index + 1) * pageView.height}px` }}>
|
<span key={index} style={{ top: `${(index + 1) * pageView.height + index * defaultPageGap}px` }} />
|
||||||
Страница {index + 2}
|
|
||||||
</span>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
+39
-30
@@ -1053,28 +1053,54 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.paper-frame {
|
.paper-frame {
|
||||||
|
--qword-page-gap: 28px;
|
||||||
|
--qword-page-height: 1123px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: var(--qword-page-width, 794px);
|
width: var(--qword-page-width, 794px);
|
||||||
zoom: var(--qword-zoom, 1);
|
zoom: var(--qword-zoom, 1);
|
||||||
margin: 0 auto 38px;
|
margin: 0 auto 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paper {
|
.page-sheets {
|
||||||
--qword-page-height: 1123px;
|
position: absolute;
|
||||||
min-height: var(--qword-page-height);
|
inset: 0 0 auto;
|
||||||
|
z-index: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--qword-page-gap);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-sheet {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
height: var(--qword-page-height);
|
||||||
padding: 72px 86px;
|
|
||||||
border: 1px solid #c7d1dd;
|
border: 1px solid #c7d1dd;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper {
|
||||||
|
min-height: var(--qword-page-height);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 72px 86px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qword-view-read .page-ruler,
|
.qword-view-read .page-ruler,
|
||||||
.qword-view-read .vertical-page-ruler,
|
.qword-view-read .vertical-page-ruler,
|
||||||
|
.qword-view-read .page-sheets,
|
||||||
|
.qword-view-read .page-break-guides,
|
||||||
.qword-view-web .page-ruler,
|
.qword-view-web .page-ruler,
|
||||||
.qword-view-web .vertical-page-ruler {
|
.qword-view-web .vertical-page-ruler,
|
||||||
|
.qword-view-web .page-sheets,
|
||||||
|
.qword-view-web .page-break-guides {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,6 +1112,9 @@ button {
|
|||||||
.qword-view-read .paper {
|
.qword-view-read .paper {
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
padding: 56px 72px;
|
padding: 56px 72px;
|
||||||
|
border: 1px solid #c7d1dd;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.qword-view-web .paper-frame {
|
.qword-view-web .paper-frame {
|
||||||
@@ -1096,6 +1125,7 @@ button {
|
|||||||
.qword-view-web .paper {
|
.qword-view-web .paper {
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
padding: 36px 48px;
|
padding: 36px 48px;
|
||||||
|
background: #ffffff;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -1104,6 +1134,7 @@ button {
|
|||||||
.page-break-guides {
|
.page-break-guides {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
z-index: 2;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1111,30 +1142,8 @@ button {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
display: grid;
|
height: var(--qword-page-gap);
|
||||||
place-items: center;
|
background: #dedede;
|
||||||
height: 24px;
|
|
||||||
border-top: 1px dashed #94a3b8;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 11px;
|
|
||||||
transform: translateY(-12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-break-guides span::before,
|
|
||||||
.page-break-guides span::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 11px;
|
|
||||||
width: calc(50% - 58px);
|
|
||||||
border-top: 1px dashed #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-break-guides span::before {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-break-guides span::after {
|
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-content h1 {
|
.document-content h1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user