Add QExcell XLSX page setup support
This commit is contained in:
@@ -32,7 +32,7 @@ import {
|
|||||||
Underline,
|
Underline,
|
||||||
WrapText
|
WrapText
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { CellFormat, SheetChart, SheetChartType, Workbook } from "../../types";
|
import type { CellFormat, SheetChart, SheetChartType, SheetPageMarginPreset, SheetPageOrientation, SheetPageSetup, SheetPageSizeId, Workbook } from "../../types";
|
||||||
import { cellId, columnLabel, columnLabelsForCount, evaluateCell, formatCellValueForDisplay, mergedCellInfo, parseCellId, summarizeColumn, usedSheetBounds } from "../../utils/spreadsheet";
|
import { cellId, columnLabel, columnLabelsForCount, evaluateCell, formatCellValueForDisplay, mergedCellInfo, parseCellId, summarizeColumn, usedSheetBounds } from "../../utils/spreadsheet";
|
||||||
import { downloadBlobFile, downloadTextFile } from "../../utils/download";
|
import { downloadBlobFile, downloadTextFile } from "../../utils/download";
|
||||||
import { chartDataPoints, exportXlsxBlob, importXlsxFile } from "../../io/excellOffice";
|
import { chartDataPoints, exportXlsxBlob, importXlsxFile } from "../../io/excellOffice";
|
||||||
@@ -95,6 +95,20 @@ const chartTypeOptions: Array<{ value: SheetChartType; label: string }> = [
|
|||||||
{ value: "line", label: "Линейная" },
|
{ value: "line", label: "Линейная" },
|
||||||
{ value: "pie", label: "Круговая" }
|
{ value: "pie", label: "Круговая" }
|
||||||
];
|
];
|
||||||
|
const sheetPageSizeOptions: Array<{ id: SheetPageSizeId; label: string }> = [
|
||||||
|
{ id: "a4", label: "A4" },
|
||||||
|
{ id: "letter", label: "Letter" }
|
||||||
|
];
|
||||||
|
const sheetPageOrientationOptions: Array<{ id: SheetPageOrientation; label: string }> = [
|
||||||
|
{ id: "portrait", label: "Книжная" },
|
||||||
|
{ id: "landscape", label: "Альбомная" }
|
||||||
|
];
|
||||||
|
const sheetPageMarginOptions: Array<{ id: SheetPageMarginPreset; label: string }> = [
|
||||||
|
{ id: "normal", label: "Обычные" },
|
||||||
|
{ id: "narrow", label: "Узкие" },
|
||||||
|
{ id: "wide", label: "Широкие" }
|
||||||
|
];
|
||||||
|
const defaultSheetPageSetup: SheetPageSetup = { size: "a4", orientation: "portrait", marginPreset: "normal" };
|
||||||
|
|
||||||
const horizontalAlignOptions = [
|
const horizontalAlignOptions = [
|
||||||
{ value: "left", label: "По левому краю", icon: AlignLeft },
|
{ value: "left", label: "По левому краю", icon: AlignLeft },
|
||||||
@@ -159,6 +173,18 @@ function normalizedCellFormat(format: CellFormat): CellFormat | undefined {
|
|||||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizedSheetPageSetup(setup?: SheetPageSetup): SheetPageSetup {
|
||||||
|
const candidateSize = setup?.size;
|
||||||
|
const candidateOrientation = setup?.orientation;
|
||||||
|
const candidateMarginPreset = setup?.marginPreset;
|
||||||
|
const size = candidateSize && sheetPageSizeOptions.some((option) => option.id === candidateSize) ? candidateSize : defaultSheetPageSetup.size;
|
||||||
|
const orientation =
|
||||||
|
candidateOrientation && sheetPageOrientationOptions.some((option) => option.id === candidateOrientation) ? candidateOrientation : defaultSheetPageSetup.orientation;
|
||||||
|
const marginPreset =
|
||||||
|
candidateMarginPreset && sheetPageMarginOptions.some((option) => option.id === candidateMarginPreset) ? candidateMarginPreset : defaultSheetPageSetup.marginPreset;
|
||||||
|
return { size, orientation, marginPreset };
|
||||||
|
}
|
||||||
|
|
||||||
function cellInputStyle(format?: CellFormat): CSSProperties {
|
function cellInputStyle(format?: CellFormat): CSSProperties {
|
||||||
const textDecoration = [format?.underline ? "underline" : "", format?.strike ? "line-through" : ""].filter(Boolean).join(" ");
|
const textDecoration = [format?.underline ? "underline" : "", format?.strike ? "line-through" : ""].filter(Boolean).join(" ");
|
||||||
return {
|
return {
|
||||||
@@ -276,6 +302,7 @@ export function QExcell({ workbook, onChange }: QExcellProps) {
|
|||||||
const activeHiddenColumns = activeSheet.hiddenColumns ?? emptyHiddenItems;
|
const activeHiddenColumns = activeSheet.hiddenColumns ?? emptyHiddenItems;
|
||||||
const activeHiddenRows = activeSheet.hiddenRows ?? emptyHiddenItems;
|
const activeHiddenRows = activeSheet.hiddenRows ?? emptyHiddenItems;
|
||||||
const activeCharts = activeSheet.charts ?? emptyCharts;
|
const activeCharts = activeSheet.charts ?? emptyCharts;
|
||||||
|
const activeSheetPageSetup = normalizedSheetPageSetup(activeSheet.pageSetup);
|
||||||
const selectedRawValue = activeSheet.cells[selectedCell] ?? "";
|
const selectedRawValue = activeSheet.cells[selectedCell] ?? "";
|
||||||
const selectedHyperlink = activeHyperlinks[selectedCell] ?? "";
|
const selectedHyperlink = activeHyperlinks[selectedCell] ?? "";
|
||||||
const selectedComment = activeComments[selectedCell] ?? "";
|
const selectedComment = activeComments[selectedCell] ?? "";
|
||||||
@@ -418,6 +445,15 @@ export function QExcell({ workbook, onChange }: QExcellProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateActiveSheetPageSetup(pageSetupPatch: Partial<SheetPageSetup>) {
|
||||||
|
onChange({
|
||||||
|
...workbook,
|
||||||
|
sheets: workbook.sheets.map((sheet) =>
|
||||||
|
sheet.id === activeSheet.id ? { ...sheet, pageSetup: normalizedSheetPageSetup({ ...activeSheetPageSetup, ...pageSetupPatch }) } : sheet
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addSheet() {
|
function addSheet() {
|
||||||
const id = `sheet-${workbook.sheets.length + 1}`;
|
const id = `sheet-${workbook.sheets.length + 1}`;
|
||||||
onChange({
|
onChange({
|
||||||
@@ -854,6 +890,39 @@ export function QExcell({ workbook, onChange }: QExcellProps) {
|
|||||||
onBlur={(event) => updateActiveSheetPrintArea(normalizedPrintAreaInput(event.target.value))}
|
onBlur={(event) => updateActiveSheetPrintArea(normalizedPrintAreaInput(event.target.value))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="office-field">
|
||||||
|
<span>Размер</span>
|
||||||
|
<select value={activeSheetPageSetup.size} onChange={(event) => updateActiveSheetPageSetup({ size: event.target.value as SheetPageSizeId })}>
|
||||||
|
{sheetPageSizeOptions.map((option) => (
|
||||||
|
<option key={option.id} value={option.id}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="office-field">
|
||||||
|
<span>Ориентация</span>
|
||||||
|
<select
|
||||||
|
value={activeSheetPageSetup.orientation}
|
||||||
|
onChange={(event) => updateActiveSheetPageSetup({ orientation: event.target.value as SheetPageOrientation })}
|
||||||
|
>
|
||||||
|
{sheetPageOrientationOptions.map((option) => (
|
||||||
|
<option key={option.id} value={option.id}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="office-field">
|
||||||
|
<span>Поля</span>
|
||||||
|
<select value={activeSheetPageSetup.marginPreset} onChange={(event) => updateActiveSheetPageSetup({ marginPreset: event.target.value as SheetPageMarginPreset })}>
|
||||||
|
{sheetPageMarginOptions.map((option) => (
|
||||||
|
<option key={option.id} value={option.id}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="office-tool-section">
|
<div className="office-tool-section">
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
buildColumnWidthsXml,
|
buildColumnWidthsXml,
|
||||||
buildHyperlinksXml,
|
buildHyperlinksXml,
|
||||||
buildMergeCellsXml,
|
buildMergeCellsXml,
|
||||||
|
buildPageMarginsXml,
|
||||||
|
buildPageSetupXml,
|
||||||
buildSheetPrXml,
|
buildSheetPrXml,
|
||||||
buildSheetViewsXml,
|
buildSheetViewsXml,
|
||||||
createCellStyleRegistry,
|
createCellStyleRegistry,
|
||||||
@@ -32,6 +34,7 @@ import {
|
|||||||
worksheetXmlToHyperlinks,
|
worksheetXmlToHyperlinks,
|
||||||
worksheetCommentsXmlToComments,
|
worksheetCommentsXmlToComments,
|
||||||
worksheetXmlToMergedCells,
|
worksheetXmlToMergedCells,
|
||||||
|
worksheetXmlToPageSetup,
|
||||||
worksheetXmlToRowHeights,
|
worksheetXmlToRowHeights,
|
||||||
worksheetXmlToShowGridLines,
|
worksheetXmlToShowGridLines,
|
||||||
worksheetXmlToTabColor,
|
worksheetXmlToTabColor,
|
||||||
@@ -739,6 +742,45 @@ describe("excellOffice OOXML helpers", () => {
|
|||||||
expect(imported.sheets[1].printArea).toBe("B2:C4,E1:E3");
|
expect(imported.sheets[1].printArea).toBe("B2:C4,E1:E3");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("сохраняет и импортирует параметры страницы XLSX через pageMargins и pageSetup", async () => {
|
||||||
|
expect(
|
||||||
|
worksheetXmlToPageSetup({
|
||||||
|
worksheet: {
|
||||||
|
pageMargins: { left: "0.25", right: "0.25", top: "0.75", bottom: "0.75", header: "0.3", footer: "0.3" },
|
||||||
|
pageSetup: { paperSize: "1", orientation: "landscape" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).toEqual({ size: "letter", orientation: "landscape", marginPreset: "narrow" });
|
||||||
|
expect(buildPageMarginsXml({ size: "a4", orientation: "portrait", marginPreset: "wide" })).toBe(
|
||||||
|
'<pageMargins left="1" right="1" top="1" bottom="1" header="0.5" footer="0.5"/>'
|
||||||
|
);
|
||||||
|
expect(buildPageSetupXml({ size: "letter", orientation: "landscape", marginPreset: "normal" })).toBe(
|
||||||
|
'<pageSetup paperSize="1" orientation="landscape"/>'
|
||||||
|
);
|
||||||
|
|
||||||
|
const blob = await exportXlsxBlob({
|
||||||
|
id: "book-1",
|
||||||
|
title: "Page setup.xlsx",
|
||||||
|
updatedAt: "2026-06-01T00:00:00.000Z",
|
||||||
|
activeSheet: "sheet-1",
|
||||||
|
sheets: [
|
||||||
|
{
|
||||||
|
id: "sheet-1",
|
||||||
|
name: "Layout",
|
||||||
|
pageSetup: { size: "letter", orientation: "landscape", marginPreset: "narrow" },
|
||||||
|
cells: { A1: "Layout" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const zip = await JSZip.loadAsync(await blob.arrayBuffer());
|
||||||
|
const sheetXml = (await zip.file("xl/worksheets/sheet1.xml")?.async("string")) ?? "";
|
||||||
|
const imported = await importXlsxFile(new File([blob], "Page setup.xlsx", { type: blob.type }));
|
||||||
|
|
||||||
|
expect(sheetXml).toContain('<pageMargins left="0.25" right="0.25" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>');
|
||||||
|
expect(sheetXml).toContain('<pageSetup paperSize="1" orientation="landscape"/>');
|
||||||
|
expect(imported.sheets[0].pageSetup).toEqual({ size: "letter", orientation: "landscape", marginPreset: "narrow" });
|
||||||
|
});
|
||||||
|
|
||||||
it("сохраняет и импортирует скрытые листы XLSX через sheet state", async () => {
|
it("сохраняет и импортирует скрытые листы XLSX через sheet state", async () => {
|
||||||
const blob = await exportXlsxBlob({
|
const blob = await exportXlsxBlob({
|
||||||
id: "book-1",
|
id: "book-1",
|
||||||
|
|||||||
+115
-4
@@ -1,4 +1,15 @@
|
|||||||
import type { CellFormat, MergedCellRange, SheetChart, SheetChartType, SheetFrozenPane, Workbook } from "../types";
|
import type {
|
||||||
|
CellFormat,
|
||||||
|
MergedCellRange,
|
||||||
|
SheetChart,
|
||||||
|
SheetChartType,
|
||||||
|
SheetFrozenPane,
|
||||||
|
SheetPageMarginPreset,
|
||||||
|
SheetPageOrientation,
|
||||||
|
SheetPageSetup,
|
||||||
|
SheetPageSizeId,
|
||||||
|
Workbook
|
||||||
|
} from "../types";
|
||||||
import { evaluateCell, expandRange } from "../utils/spreadsheet";
|
import { evaluateCell, expandRange } from "../utils/spreadsheet";
|
||||||
import { officeTitleFromFileName, readOfficeFileAsArrayBuffer, XLSX_MIME_TYPE } from "./fileHelpers";
|
import { officeTitleFromFileName, readOfficeFileAsArrayBuffer, XLSX_MIME_TYPE } from "./fileHelpers";
|
||||||
|
|
||||||
@@ -19,6 +30,7 @@ export interface ImportedExcellWorkbook {
|
|||||||
showGridLines?: boolean;
|
showGridLines?: boolean;
|
||||||
zoomScale?: number;
|
zoomScale?: number;
|
||||||
printArea?: string;
|
printArea?: string;
|
||||||
|
pageSetup?: SheetPageSetup;
|
||||||
cells: Record<string, string>;
|
cells: Record<string, string>;
|
||||||
mergedCells?: MergedCellRange[];
|
mergedCells?: MergedCellRange[];
|
||||||
hyperlinks?: Record<string, string>;
|
hyperlinks?: Record<string, string>;
|
||||||
@@ -77,6 +89,7 @@ interface ParsedSheet {
|
|||||||
showGridLines?: boolean;
|
showGridLines?: boolean;
|
||||||
zoomScale?: number;
|
zoomScale?: number;
|
||||||
printArea?: string;
|
printArea?: string;
|
||||||
|
pageSetup?: SheetPageSetup;
|
||||||
cells: Record<string, string>;
|
cells: Record<string, string>;
|
||||||
mergedCells?: MergedCellRange[];
|
mergedCells?: MergedCellRange[];
|
||||||
hyperlinks?: Record<string, string>;
|
hyperlinks?: Record<string, string>;
|
||||||
@@ -210,6 +223,16 @@ const builtinNumberFormats: Record<number, string> = {
|
|||||||
48: "##0.0E+0",
|
48: "##0.0E+0",
|
||||||
49: "@"
|
49: "@"
|
||||||
};
|
};
|
||||||
|
const defaultSheetPageSetup: SheetPageSetup = { size: "a4", orientation: "portrait", marginPreset: "normal" };
|
||||||
|
const xlsxPageSizePaperIds: Record<SheetPageSizeId, number> = {
|
||||||
|
letter: 1,
|
||||||
|
a4: 9
|
||||||
|
};
|
||||||
|
const xlsxPageMargins: Record<SheetPageMarginPreset, { left: number; right: number; top: number; bottom: number; header: number; footer: number }> = {
|
||||||
|
normal: { left: 0.7, right: 0.7, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3 },
|
||||||
|
narrow: { left: 0.25, right: 0.25, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3 },
|
||||||
|
wide: { left: 1, right: 1, top: 1, bottom: 1, header: 0.5, footer: 0.5 }
|
||||||
|
};
|
||||||
|
|
||||||
export async function importXlsxFile(file: File): Promise<ImportedExcellWorkbook> {
|
export async function importXlsxFile(file: File): Promise<ImportedExcellWorkbook> {
|
||||||
if (!file.name.toLowerCase().endsWith(".xlsx")) {
|
if (!file.name.toLowerCase().endsWith(".xlsx")) {
|
||||||
@@ -320,7 +343,8 @@ export async function exportXlsxBlob(workbook: Workbook): Promise<Blob> {
|
|||||||
sheet.hiddenRows,
|
sheet.hiddenRows,
|
||||||
sheet.showGridLines,
|
sheet.showGridLines,
|
||||||
sheet.zoomScale,
|
sheet.zoomScale,
|
||||||
legacyDrawingRelationshipId
|
legacyDrawingRelationshipId,
|
||||||
|
sheet.pageSetup
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const relationshipsXml = buildWorksheetRelationshipsXml(
|
const relationshipsXml = buildWorksheetRelationshipsXml(
|
||||||
@@ -371,6 +395,7 @@ export function parsedWorksheetToSheet(
|
|||||||
const hiddenRows = worksheetXmlToHiddenRows(worksheetXml);
|
const hiddenRows = worksheetXmlToHiddenRows(worksheetXml);
|
||||||
const showGridLines = worksheetXmlToShowGridLines(worksheetXml);
|
const showGridLines = worksheetXmlToShowGridLines(worksheetXml);
|
||||||
const zoomScale = worksheetXmlToZoomScale(worksheetXml);
|
const zoomScale = worksheetXmlToZoomScale(worksheetXml);
|
||||||
|
const pageSetup = worksheetXmlToPageSetup(worksheetXml);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: sheet.id || `sheet-${index + 1}`,
|
id: sheet.id || `sheet-${index + 1}`,
|
||||||
@@ -385,6 +410,7 @@ export function parsedWorksheetToSheet(
|
|||||||
...(Object.keys(hiddenRows).length > 0 ? { hiddenRows } : {}),
|
...(Object.keys(hiddenRows).length > 0 ? { hiddenRows } : {}),
|
||||||
...(showGridLines === false ? { showGridLines } : {}),
|
...(showGridLines === false ? { showGridLines } : {}),
|
||||||
...(zoomScale ? { zoomScale } : {}),
|
...(zoomScale ? { zoomScale } : {}),
|
||||||
|
...(pageSetup ? { pageSetup } : {}),
|
||||||
cells: worksheetXmlToCells(worksheetXml, sharedStrings, styleTable, date1904),
|
cells: worksheetXmlToCells(worksheetXml, sharedStrings, styleTable, date1904),
|
||||||
mergedCells: worksheetXmlToMergedCells(worksheetXml),
|
mergedCells: worksheetXmlToMergedCells(worksheetXml),
|
||||||
hyperlinks: worksheetXmlToHyperlinks(worksheetXml, relationshipsXml),
|
hyperlinks: worksheetXmlToHyperlinks(worksheetXml, relationshipsXml),
|
||||||
@@ -540,6 +566,18 @@ export function worksheetXmlToZoomScale(worksheetXml: unknown) {
|
|||||||
return normalizedSheetZoomScale(sheetView.zoomScale);
|
return normalizedSheetZoomScale(sheetView.zoomScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function worksheetXmlToPageSetup(worksheetXml: unknown): SheetPageSetup | undefined {
|
||||||
|
const worksheet = childRecord(worksheetXml, "worksheet");
|
||||||
|
const pageSetup = childRecord(worksheet, "pageSetup");
|
||||||
|
const pageMargins = childRecord(worksheet, "pageMargins");
|
||||||
|
const size = sheetPageSizeFromPaperId(pageSetup.paperSize) ?? defaultSheetPageSetup.size;
|
||||||
|
const orientation = normalizedSheetPageOrientation(pageSetup.orientation) ?? defaultSheetPageSetup.orientation;
|
||||||
|
const marginPreset = sheetPageMarginPresetFromRecord(pageMargins) ?? defaultSheetPageSetup.marginPreset;
|
||||||
|
const hasPageSetup = Object.keys(pageSetup).length > 0;
|
||||||
|
const hasPageMargins = Object.keys(pageMargins).length > 0;
|
||||||
|
return hasPageSetup || hasPageMargins ? { size, orientation, marginPreset } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function worksheetXmlToAutoFilter(worksheetXml: unknown) {
|
export function worksheetXmlToAutoFilter(worksheetXml: unknown) {
|
||||||
const worksheet = childRecord(worksheetXml, "worksheet");
|
const worksheet = childRecord(worksheetXml, "worksheet");
|
||||||
const autoFilter = childRecord(worksheet, "autoFilter");
|
const autoFilter = childRecord(worksheet, "autoFilter");
|
||||||
@@ -804,7 +842,8 @@ export function buildWorksheetXml(
|
|||||||
hiddenRows: Record<string, boolean> = {},
|
hiddenRows: Record<string, boolean> = {},
|
||||||
showGridLines?: boolean,
|
showGridLines?: boolean,
|
||||||
zoomScale?: number,
|
zoomScale?: number,
|
||||||
legacyDrawingRelationshipId = ""
|
legacyDrawingRelationshipId = "",
|
||||||
|
pageSetup?: SheetPageSetup
|
||||||
) {
|
) {
|
||||||
const normalizedRowHeights = normalizedSheetRowHeights(rowHeights);
|
const normalizedRowHeights = normalizedSheetRowHeights(rowHeights);
|
||||||
const normalizedHiddenRows = normalizedSheetHiddenRows(hiddenRows);
|
const normalizedHiddenRows = normalizedSheetHiddenRows(hiddenRows);
|
||||||
@@ -845,9 +884,11 @@ export function buildWorksheetXml(
|
|||||||
const sheetPrXml = buildSheetPrXml(tabColor);
|
const sheetPrXml = buildSheetPrXml(tabColor);
|
||||||
const sheetViewsXml = buildSheetViewsXml(frozenPane, showGridLines, zoomScale);
|
const sheetViewsXml = buildSheetViewsXml(frozenPane, showGridLines, zoomScale);
|
||||||
const colsXml = buildColumnWidthsXml(columnWidths, hiddenColumns);
|
const colsXml = buildColumnWidthsXml(columnWidths, hiddenColumns);
|
||||||
|
const pageMarginsXml = buildPageMarginsXml(pageSetup);
|
||||||
|
const pageSetupXml = buildPageSetupXml(pageSetup);
|
||||||
|
|
||||||
return xmlDeclaration(
|
return xmlDeclaration(
|
||||||
`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">${sheetPrXml}${sheetViewsXml}${colsXml}<sheetData>${rowXml}</sheetData>${autoFilterXml}${mergeCellsXml}${hyperlinksXml}${drawingXml}${legacyDrawingXml}</worksheet>`
|
`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">${sheetPrXml}${sheetViewsXml}${colsXml}<sheetData>${rowXml}</sheetData>${autoFilterXml}${mergeCellsXml}${hyperlinksXml}${pageMarginsXml}${pageSetupXml}${drawingXml}${legacyDrawingXml}</worksheet>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -940,6 +981,24 @@ export function buildSheetPrXml(tabColor?: string) {
|
|||||||
return color ? `<sheetPr><tabColor rgb="FF${color}"/></sheetPr>` : "";
|
return color ? `<sheetPr><tabColor rgb="FF${color}"/></sheetPr>` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildPageMarginsXml(pageSetup?: SheetPageSetup) {
|
||||||
|
if (!pageSetup) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const margins = xlsxPageMargins[normalizedSheetPageSetup(pageSetup).marginPreset];
|
||||||
|
return `<pageMargins left="${formatSheetDimension(margins.left)}" right="${formatSheetDimension(margins.right)}" top="${formatSheetDimension(margins.top)}" bottom="${formatSheetDimension(margins.bottom)}" header="${formatSheetDimension(margins.header)}" footer="${formatSheetDimension(margins.footer)}"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPageSetupXml(pageSetup?: SheetPageSetup) {
|
||||||
|
if (!pageSetup) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizedSheetPageSetup(pageSetup);
|
||||||
|
return `<pageSetup paperSize="${xlsxPageSizePaperIds[normalized.size]}" orientation="${normalized.orientation}"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildColumnWidthsXml(columnWidths: Record<string, number> = {}, hiddenColumns: Record<string, boolean> = {}) {
|
export function buildColumnWidthsXml(columnWidths: Record<string, number> = {}, hiddenColumns: Record<string, boolean> = {}) {
|
||||||
const columnsByNumber = new Map<number, { width?: number; hidden?: boolean }>();
|
const columnsByNumber = new Map<number, { width?: number; hidden?: boolean }>();
|
||||||
normalizedSheetColumnWidths(columnWidths).forEach(({ column, width }) => {
|
normalizedSheetColumnWidths(columnWidths).forEach(({ column, width }) => {
|
||||||
@@ -1914,6 +1973,58 @@ function normalizedSheetZoomScale(value: unknown) {
|
|||||||
return zoomScale >= 10 && zoomScale <= 400 && zoomScale !== 100 ? zoomScale : 0;
|
return zoomScale >= 10 && zoomScale <= 400 && zoomScale !== 100 ? zoomScale : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizedSheetPageSetup(pageSetup?: SheetPageSetup): SheetPageSetup {
|
||||||
|
const size = isSheetPageSizeId(pageSetup?.size) ? pageSetup.size : defaultSheetPageSetup.size;
|
||||||
|
const orientation = normalizedSheetPageOrientation(pageSetup?.orientation) ?? defaultSheetPageSetup.orientation;
|
||||||
|
const marginPreset = isSheetPageMarginPreset(pageSetup?.marginPreset) ? pageSetup.marginPreset : defaultSheetPageSetup.marginPreset;
|
||||||
|
return { size, orientation, marginPreset };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedSheetPageOrientation(value: unknown): SheetPageOrientation | undefined {
|
||||||
|
return value === "portrait" || value === "landscape" ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSheetPageSizeId(value: unknown): value is SheetPageSizeId {
|
||||||
|
return value === "a4" || value === "letter";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSheetPageMarginPreset(value: unknown): value is SheetPageMarginPreset {
|
||||||
|
return value === "normal" || value === "narrow" || value === "wide";
|
||||||
|
}
|
||||||
|
|
||||||
|
function sheetPageSizeFromPaperId(value: unknown): SheetPageSizeId | undefined {
|
||||||
|
const paperSize = positiveIntegerAttribute(value);
|
||||||
|
return (Object.keys(xlsxPageSizePaperIds) as SheetPageSizeId[]).find((sizeId) => xlsxPageSizePaperIds[sizeId] === paperSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sheetPageMarginPresetFromRecord(record: XmlRecord): SheetPageMarginPreset | undefined {
|
||||||
|
const left = positiveNumberAttribute(record.left);
|
||||||
|
const right = positiveNumberAttribute(record.right);
|
||||||
|
const top = positiveNumberAttribute(record.top);
|
||||||
|
const bottom = positiveNumberAttribute(record.bottom);
|
||||||
|
const header = positiveNumberAttribute(record.header);
|
||||||
|
const footer = positiveNumberAttribute(record.footer);
|
||||||
|
if (!left || !right || !top || !bottom || !header || !footer) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Object.keys(xlsxPageMargins) as SheetPageMarginPreset[]).find((preset) => {
|
||||||
|
const margins = xlsxPageMargins[preset];
|
||||||
|
return (
|
||||||
|
isCloseDimension(left, margins.left) &&
|
||||||
|
isCloseDimension(right, margins.right) &&
|
||||||
|
isCloseDimension(top, margins.top) &&
|
||||||
|
isCloseDimension(bottom, margins.bottom) &&
|
||||||
|
isCloseDimension(header, margins.header) &&
|
||||||
|
isCloseDimension(footer, margins.footer)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCloseDimension(actual: number, expected: number) {
|
||||||
|
return Math.abs(actual - expected) <= 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
function buildRowAttributes(height?: number, hidden?: boolean) {
|
function buildRowAttributes(height?: number, hidden?: boolean) {
|
||||||
return `${height ? ` ht="${formatSheetDimension(height)}" customHeight="1"` : ""}${hidden ? ' hidden="1"' : ""}`;
|
return `${height ? ` ht="${formatSheetDimension(height)}" customHeight="1"` : ""}${hidden ? ' hidden="1"' : ""}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ export type AppId = "word" | "sheets" | "slides" | "mail";
|
|||||||
export type WordPageSizeId = "a4" | "letter";
|
export type WordPageSizeId = "a4" | "letter";
|
||||||
export type WordPageOrientation = "portrait" | "landscape";
|
export type WordPageOrientation = "portrait" | "landscape";
|
||||||
export type WordPageMarginPreset = "normal" | "narrow" | "wide";
|
export type WordPageMarginPreset = "normal" | "narrow" | "wide";
|
||||||
|
export type SheetPageSizeId = "a4" | "letter";
|
||||||
|
export type SheetPageOrientation = "portrait" | "landscape";
|
||||||
|
export type SheetPageMarginPreset = "normal" | "narrow" | "wide";
|
||||||
|
|
||||||
export interface WordPageSetup {
|
export interface WordPageSetup {
|
||||||
size: WordPageSizeId;
|
size: WordPageSizeId;
|
||||||
@@ -10,6 +13,12 @@ export interface WordPageSetup {
|
|||||||
marginPreset: WordPageMarginPreset;
|
marginPreset: WordPageMarginPreset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SheetPageSetup {
|
||||||
|
size: SheetPageSizeId;
|
||||||
|
orientation: SheetPageOrientation;
|
||||||
|
marginPreset: SheetPageMarginPreset;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WordDocument {
|
export interface WordDocument {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -40,6 +49,7 @@ export interface Sheet {
|
|||||||
showGridLines?: boolean;
|
showGridLines?: boolean;
|
||||||
zoomScale?: number;
|
zoomScale?: number;
|
||||||
printArea?: string;
|
printArea?: string;
|
||||||
|
pageSetup?: SheetPageSetup;
|
||||||
cells: Record<string, string>;
|
cells: Record<string, string>;
|
||||||
mergedCells?: MergedCellRange[];
|
mergedCells?: MergedCellRange[];
|
||||||
hyperlinks?: Record<string, string>;
|
hyperlinks?: Record<string, string>;
|
||||||
|
|||||||
Reference in New Issue
Block a user